From 19af70846115da95b60e4b2da6af758e4919d7f7 Mon Sep 17 00:00:00 2001 From: ikpil Date: Tue, 14 Mar 2023 14:02:43 +0900 Subject: [PATCH] Initial commit" --- .gitignore | 399 + DotRecast.sln | 120 + README.dm | 1 - README.md | 1 + resources/all_tiles_navmesh.bin | Bin 0 -> 70980 bytes resources/all_tiles_tilecache.bin | Bin 0 -> 47643 bytes resources/annotation_test.obj | 3562 ++++ resources/bridge.obj | 87 + resources/convex.obj | 408 + resources/dungeon.obj | 15234 ++++++++++++++++ resources/dungeon_all_tiles_navmesh.bin | Bin 0 -> 36228 bytes resources/dungeon_all_tiles_navmesh_32bit.bin | Bin 0 -> 32460 bytes resources/dungeon_all_tiles_tilecache.bin | Bin 0 -> 18767 bytes resources/fonts/DroidSans.ttf | Bin 0 -> 190044 bytes resources/graph.zip | Bin 0 -> 12056 bytes resources/graph_v4_1_16.zip | Bin 0 -> 9049 bytes resources/house.obj | 7321 ++++++++ resources/nav_test.obj | 3506 ++++ resources/test.voxels | Bin 0 -> 9021169 bytes resources/test_boundstree.zip | Bin 0 -> 218183 bytes resources/test_tiles.voxels | Bin 0 -> 11162953 bytes src/DotRecast.Core/ArrayUtils.cs | 41 + src/DotRecast.Core/AtomicBoolean.cs | 18 + src/DotRecast.Core/AtomicFloat.cs | 28 + src/DotRecast.Core/AtomicInteger.cs | 66 + src/DotRecast.Core/AtomicLong.cs | 52 + src/DotRecast.Core/ByteBuffer.cs | 128 + src/DotRecast.Core/ByteOrder.cs | 8 + src/DotRecast.Core/CollectionExtensions.cs | 29 + src/DotRecast.Core/ConvexUtils.cs | 85 + src/DotRecast.Core/DemoMath.cs | 78 + src/DotRecast.Core/DotRecast.Core.csproj | 9 + src/DotRecast.Core/Loader.cs | 32 + src/DotRecast.Core/NodeQueue.cs | 72 + src/DotRecast.Detour.Crowd/Crowd.cs | 1171 ++ src/DotRecast.Detour.Crowd/CrowdAgent.cs | 191 + .../CrowdAgentAnimation.cs | 29 + .../CrowdAgentParams.cs | 61 + src/DotRecast.Detour.Crowd/CrowdConfig.cs | 66 + src/DotRecast.Detour.Crowd/CrowdTelemetry.cs | 79 + .../DotRecast.Detour.Crowd.csproj | 12 + src/DotRecast.Detour.Crowd/LocalBoundary.cs | 136 + .../ObstacleAvoidanceQuery.cs | 508 + src/DotRecast.Detour.Crowd/PathCorridor.cs | 561 + src/DotRecast.Detour.Crowd/PathQuery.cs | 31 + src/DotRecast.Detour.Crowd/PathQueryResult.cs | 26 + src/DotRecast.Detour.Crowd/PathQueue.cs | 82 + src/DotRecast.Detour.Crowd/ProximityGrid.cs | 129 + .../SweepCircleCircleResult.cs | 33 + .../Tracking/CrowdAgentDebugInfo.cs | 28 + .../Tracking/ObstacleAvoidanceDebugData.cs | 124 + .../AddColliderQueueItem.cs | 44 + .../Colliders/AbstractCollider.cs | 44 + .../Colliders/BoxCollider.cs | 79 + .../Colliders/CapsuleCollider.cs | 49 + .../Colliders/Collider.cs | 27 + .../Colliders/CompositeCollider.cs | 65 + .../Colliders/ConvexTrimeshCollider.cs | 46 + .../Colliders/CylinderCollider.cs | 48 + .../Colliders/SphereCollider.cs | 45 + .../Colliders/TrimeshCollider.cs | 61 + .../DotRecast.Detour.Dynamic.csproj | 17 + .../DynamicNavMesh.cs | 226 + .../DynamicNavMeshConfig.cs | 56 + src/DotRecast.Detour.Dynamic/DynamicTile.cs | 154 + .../DynamicTileCheckpoint.cs | 61 + src/DotRecast.Detour.Dynamic/Io/ByteUtils.cs | 77 + .../Io/LZ4VoxelTileCompressor.cs | 40 + src/DotRecast.Detour.Dynamic/Io/VoxelFile.cs | 148 + .../Io/VoxelFileReader.cs | 125 + .../Io/VoxelFileWriter.cs | 89 + src/DotRecast.Detour.Dynamic/Io/VoxelTile.cs | 181 + .../RemoveColliderQueueItem.cs | 41 + .../UpdateQueueItem.cs | 29 + src/DotRecast.Detour.Dynamic/VoxelQuery.cs | 160 + src/DotRecast.Detour.Extras/BVTreeBuilder.cs | 55 + .../DotRecast.Detour.Extras.csproj | 13 + .../Jumplink/AbstractGroundSampler.cs | 48 + .../Jumplink/ClimbTrajectory.cs | 13 + src/DotRecast.Detour.Extras/Jumplink/Edge.cs | 6 + .../Jumplink/EdgeExtractor.cs | 58 + .../Jumplink/EdgeSampler.cs | 25 + .../Jumplink/EdgeSamplerFactory.cs | 79 + .../Jumplink/GroundSample.cs | 7 + .../Jumplink/GroundSampler.cs | 9 + .../Jumplink/GroundSegment.cs | 9 + .../Jumplink/JumpLink.cs | 15 + .../Jumplink/JumpLinkBuilder.cs | 84 + .../Jumplink/JumpLinkBuilderConfig.cs | 33 + .../Jumplink/JumpLinkType.cs | 5 + .../Jumplink/JumpSegment.cs | 7 + .../Jumplink/JumpSegmentBuilder.cs | 95 + .../Jumplink/JumpTrajectory.cs | 41 + .../Jumplink/NavMeshGroundSampler.cs | 91 + .../Jumplink/Trajectory.cs | 16 + .../Jumplink/TrajectorySampler.cs | 76 + src/DotRecast.Detour.Extras/ObjExporter.cs | 64 + src/DotRecast.Detour.Extras/PolyUtils.cs | 80 + .../Unity/Astar/BVTreeCreator.cs | 30 + .../Unity/Astar/GraphConnectionReader.cs | 47 + .../Unity/Astar/GraphData.cs | 44 + .../Unity/Astar/GraphMeshData.cs | 62 + .../Unity/Astar/GraphMeshDataReader.cs | 151 + .../Unity/Astar/GraphMeta.cs | 39 + .../Unity/Astar/GraphMetaReader.cs | 38 + .../Unity/Astar/LinkBuilder.cs | 66 + .../Unity/Astar/Meta.cs | 73 + .../Unity/Astar/MetaReader.cs | 54 + .../Unity/Astar/NodeIndexReader.cs | 38 + .../Unity/Astar/NodeLink2.cs | 35 + .../Unity/Astar/NodeLink2Reader.cs | 49 + .../Unity/Astar/OffMeshLinkCreator.cs | 63 + .../Astar/UnityAStarPathfindingImporter.cs | 75 + .../Astar/UnityAStarPathfindingReader.cs | 67 + .../Unity/Astar/ZipBinaryReader.cs | 37 + src/DotRecast.Detour.Extras/Vector3f.cs | 35 + .../AbstractTileLayersBuilder.cs | 77 + .../CompressedTile.cs | 34 + .../DotRecast.Detour.TileCache.csproj | 16 + .../Io/Compress/FastLz.cs | 576 + .../Io/Compress/FastLzTileCacheCompressor.cs | 39 + .../Io/Compress/LZ4TileCacheCompressor.cs | 34 + .../Io/Compress/TileCacheCompressorFactory.cs | 28 + .../Io/TileCacheLayerHeaderReader.cs | 60 + .../Io/TileCacheLayerHeaderWriter.cs | 53 + .../Io/TileCacheReader.cs | 94 + .../Io/TileCacheSetHeader.cs | 33 + .../Io/TileCacheWriter.cs | 74 + .../ObstacleRequest.cs | 24 + .../ObstacleRequestAction.cs | 23 + .../ObstacleState.cs | 24 + src/DotRecast.Detour.TileCache/TileCache.cs | 604 + .../TileCacheBuilder.cs | 1842 ++ .../TileCacheCompressor.cs | 26 + .../TileCacheContour.cs | 26 + .../TileCacheContourSet.cs | 24 + .../TileCacheLayer.cs | 28 + .../TileCacheLayerHeader.cs | 35 + .../TileCacheMeshProcess.cs | 24 + .../TileCacheObstacle.cs | 50 + .../TileCacheParams.cs | 32 + .../TileCachePolyMesh.cs | 33 + .../TileCacheStorageParams.cs | 34 + src/DotRecast.Detour/BVNode.cs | 34 + .../ClosestPointOnPolyResult.cs | 41 + .../ConvexConvexIntersection.cs | 237 + src/DotRecast.Detour/DefaultQueryFilter.cs | 101 + src/DotRecast.Detour/DefaultQueryHeuristic.cs | 39 + src/DotRecast.Detour/DetourBuilder.cs | 31 + src/DotRecast.Detour/DetourCommon.cs | 636 + src/DotRecast.Detour/DotRecast.Detour.csproj | 12 + .../FindDistanceToWallResult.cs | 45 + .../FindLocalNeighbourhoodResult.cs | 42 + src/DotRecast.Detour/FindNearestPolyQuery.cs | 53 + src/DotRecast.Detour/FindNearestPolyResult.cs | 46 + src/DotRecast.Detour/FindPolysAroundResult.cs | 49 + src/DotRecast.Detour/FindRandomPointResult.cs | 41 + .../GetPolyWallSegmentsResult.cs | 43 + src/DotRecast.Detour/Io/DetourWriter.cs | 82 + src/DotRecast.Detour/Io/IOUtils.cs | 58 + src/DotRecast.Detour/Io/MeshDataReader.cs | 201 + src/DotRecast.Detour/Io/MeshDataWriter.cs | 142 + src/DotRecast.Detour/Io/MeshSetReader.cs | 127 + src/DotRecast.Detour/Io/MeshSetWriter.cs | 78 + src/DotRecast.Detour/Io/NavMeshParamReader.cs | 19 + src/DotRecast.Detour/Io/NavMeshParamWriter.cs | 18 + src/DotRecast.Detour/Io/NavMeshSetHeader.cs | 33 + src/DotRecast.Detour/Io/NavMeshTileHeader.cs | 6 + src/DotRecast.Detour/LegacyNavMeshQuery.cs | 730 + src/DotRecast.Detour/Link.cs | 41 + src/DotRecast.Detour/MeshData.cs | 45 + src/DotRecast.Detour/MeshHeader.cs | 78 + src/DotRecast.Detour/MeshTile.cs | 45 + .../MoveAlongSurfaceResult.cs | 45 + src/DotRecast.Detour/NavMesh.cs | 1426 ++ src/DotRecast.Detour/NavMeshBuilder.cs | 601 + .../NavMeshDataCreateParams.cs | 101 + src/DotRecast.Detour/NavMeshParams.cs | 38 + src/DotRecast.Detour/NavMeshQuery.cs | 3055 ++++ src/DotRecast.Detour/Node.cs | 61 + src/DotRecast.Detour/NodePool.cs | 98 + src/DotRecast.Detour/NodeQueue.cs | 62 + src/DotRecast.Detour/OffMeshConnection.cs | 43 + src/DotRecast.Detour/Poly.cs | 70 + src/DotRecast.Detour/PolyDetail.cs | 31 + src/DotRecast.Detour/PolyQuery.cs | 6 + .../PolygonByCircleConstraint.cs | 94 + src/DotRecast.Detour/QueryData.cs | 32 + src/DotRecast.Detour/QueryFilter.cs | 28 + src/DotRecast.Detour/QueryHeuristic.cs | 25 + src/DotRecast.Detour/RaycastHit.cs | 39 + src/DotRecast.Detour/Result.cs | 82 + src/DotRecast.Detour/Status.cs | 54 + src/DotRecast.Detour/StraightPathItem.cs | 47 + src/DotRecast.Detour/VectorPtr.cs | 46 + .../Builder/AbstractNavMeshBuilder.cs | 78 + .../Builder/SampleAreaModifications.cs | 46 + .../Builder/SoloNavMeshBuilder.cs | 70 + .../Builder/TileNavMeshBuilder.cs | 127 + .../DotRecast.Recast.Demo.csproj | 24 + src/DotRecast.Recast.Demo/Draw/DebugDraw.cs | 603 + .../Draw/DebugDrawPrimitives.cs | 27 + src/DotRecast.Recast.Demo/Draw/DrawMode.cs | 50 + .../Draw/GLCheckerTexture.cs | 62 + src/DotRecast.Recast.Demo/Draw/GLU.cs | 432 + .../Draw/LegacyOpenGLDraw.cs | 123 + .../Draw/ModernOpenGLDraw.cs | 264 + .../Draw/NavMeshRenderer.cs | 254 + src/DotRecast.Recast.Demo/Draw/OpenGLDraw.cs | 35 + .../Draw/OpenGLVertex.cs | 43 + .../Draw/RecastDebugDraw.cs | 1136 ++ .../Geom/ChunkyTriMesh.cs | 263 + .../Geom/DemoInputGeomProvider.cs | 185 + .../Geom/DemoOffMeshConnection.cs | 43 + .../Geom/Intersections.cs | 113 + .../Geom/NavMeshRaycast.cs | 79 + .../Geom/NavMeshUtils.cs | 44 + .../Geom/PolyMeshRaycast.cs | 67 + src/DotRecast.Recast.Demo/RecastDemo.cs | 687 + src/DotRecast.Recast.Demo/Sample.cs | 86 + .../Settings/SettingsUI.cs | 336 + .../Tools/ConvexVolumeTool.cs | 204 + .../Tools/CrowdProfilingTool.cs | 393 + src/DotRecast.Recast.Demo/Tools/CrowdTool.cs | 735 + .../Tools/CrowdToolParams.cs | 46 + .../Tools/DemoObjImporter.cs | 14 + .../Tools/DynamicUpdateTool.cs | 675 + .../Tools/Gizmos/BoxGizmo.cs | 68 + .../Tools/Gizmos/CapsuleGizmo.cs | 80 + .../Tools/Gizmos/ColliderGizmo.cs | 8 + .../Tools/Gizmos/CompositeGizmo.cs | 17 + .../Tools/Gizmos/CylinderGizmo.cs | 82 + .../Tools/Gizmos/GizmoFactory.cs | 28 + .../Tools/Gizmos/GizmoHelper.cs | 162 + .../Tools/Gizmos/SphereGizmo.cs | 38 + .../Tools/Gizmos/TrimeshGizmo.cs | 28 + .../Tools/JumpLinkBuilderTool.cs | 427 + .../Tools/JumpLinkBuilderToolParams.cs | 45 + .../Tools/OffMeshConnectionTool.cs | 109 + src/DotRecast.Recast.Demo/Tools/PathUtils.cs | 171 + src/DotRecast.Recast.Demo/Tools/PolyUtils.cs | 110 + .../Tools/SteerTarget.cs | 15 + .../Tools/TestNavmeshTool.cs | 880 + src/DotRecast.Recast.Demo/Tools/Tool.cs | 43 + .../Tools/ToolUIModule.cs | 26 + src/DotRecast.Recast.Demo/Tools/ToolsUI.cs | 82 + src/DotRecast.Recast.Demo/UI/Mouse.cs | 131 + src/DotRecast.Recast.Demo/UI/MouseListener.cs | 28 + src/DotRecast.Recast.Demo/UI/NuklearGL.cs | 333 + src/DotRecast.Recast.Demo/UI/NuklearUI.cs | 150 + .../UI/NuklearUIHelper.cs | 74 + .../UI/NuklearUIModule.cs | 29 + src/DotRecast.Recast.Demo/imgui.ini | 5 + src/DotRecast.Recast/AreaModification.cs | 69 + src/DotRecast.Recast/CompactCell.cs | 30 + src/DotRecast.Recast/CompactHeightfield.cs | 72 + src/DotRecast.Recast/CompactSpan.cs | 36 + src/DotRecast.Recast/Contour.cs | 42 + src/DotRecast.Recast/ContourSet.cs | 53 + src/DotRecast.Recast/ConvexVolume.cs | 28 + src/DotRecast.Recast/DotRecast.Recast.csproj | 12 + src/DotRecast.Recast/Geom/ChunkyTriMesh.cs | 246 + .../Geom/ChunkyTriMeshNode.cs | 9 + .../Geom/ConvexVolumeProvider.cs | 26 + .../Geom/InputGeomProvider.cs | 31 + .../Geom/SimpleInputGeomProvider.cs | 136 + .../Geom/SingleTrimeshInputGeomProvider.cs | 64 + src/DotRecast.Recast/Geom/TriMesh.cs | 51 + src/DotRecast.Recast/Heightfield.cs | 60 + src/DotRecast.Recast/HeightfieldLayerSet.cs | 77 + src/DotRecast.Recast/InputGeomReader.cs | 5 + src/DotRecast.Recast/ObjImporter.cs | 138 + src/DotRecast.Recast/PartitionType.cs | 10 + src/DotRecast.Recast/PolyMesh.cs | 69 + src/DotRecast.Recast/PolyMeshDetail.cs | 45 + src/DotRecast.Recast/Recast.cs | 121 + src/DotRecast.Recast/RecastArea.cs | 584 + src/DotRecast.Recast/RecastBuilder.cs | 319 + src/DotRecast.Recast/RecastBuilderConfig.cs | 99 + src/DotRecast.Recast/RecastBuilderResult.cs | 56 + src/DotRecast.Recast/RecastCommon.cs | 84 + src/DotRecast.Recast/RecastCompact.cs | 186 + src/DotRecast.Recast/RecastConfig.cs | 185 + src/DotRecast.Recast/RecastConstants.cs | 84 + src/DotRecast.Recast/RecastContour.cs | 1019 ++ .../RecastFilledVolumeRasterization.cs | 802 + src/DotRecast.Recast/RecastFilter.cs | 204 + src/DotRecast.Recast/RecastLayers.cs | 506 + src/DotRecast.Recast/RecastMesh.cs | 1213 ++ src/DotRecast.Recast/RecastMeshDetail.cs | 1334 ++ src/DotRecast.Recast/RecastRasterization.cs | 483 + src/DotRecast.Recast/RecastRegion.cs | 1618 ++ src/DotRecast.Recast/RecastVectors.cs | 97 + src/DotRecast.Recast/RecastVoxelization.cs | 73 + src/DotRecast.Recast/Span.cs | 33 + src/DotRecast.Recast/Telemetry.cs | 57 + .../AbstractCrowdTest.cs | 149 + .../DotRecast.Detour.Crowd.Test/Crowd1Test.cs | 706 + .../DotRecast.Detour.Crowd.Test/Crowd4Test.cs | 363 + .../Crowd4VelocityTest.cs | 118 + .../DotRecast.Detour.Crowd.Test.csproj | 28 + .../PathCorridorTest.cs | 80 + .../RecastTestMeshBuilder.cs | 110 + .../SampleAreaModifications.cs | 52 + .../DotRecast.Detour.Dynamic.Test.csproj | 26 + .../DynamicNavMeshTest.cs | 76 + .../Io/VoxelFileReaderTest.cs | 78 + .../Io/VoxelFileReaderWriterTest.cs | 100 + .../SampleAreaModifications.cs | 52 + .../VoxelQueryTest.cs | 105 + .../DotRecast.Detour.Extras.Test.csproj | 26 + .../UnityAStarPathfindingImporterTest.cs | 128 + .../AbstractDetourTest.cs | 67 + .../ConvexConvexIntersectionTest.cs | 43 + .../DotRecast.Detour.Test.csproj | 26 + .../FindDistanceToWallTest.cs | 66 + .../FindLocalNeighbourhoodTest.cs | 67 + .../FindNearestPolyTest.cs | 77 + test/DotRecast.Detour.Test/FindPathTest.cs | 158 + .../FindPolysAroundCircleTest.cs | 83 + .../FindPolysAroundShapeTest.cs | 131 + .../GetPolyWallSegmentsTest.cs | 75 + .../Io/MeshDataReaderWriterTest.cs | 118 + .../Io/MeshSetReaderTest.cs | 113 + .../Io/MeshSetReaderWriterTest.cs | 120 + .../MoveAlongSurfaceTest.cs | 68 + .../NavMeshBuilderTest.cs | 64 + .../PolygonByCircleConstraintTest.cs | 87 + test/DotRecast.Detour.Test/RandomPointTest.cs | 123 + .../RecastTestMeshBuilder.cs | 111 + .../SampleAreaModifications.cs | 52 + .../TestDetourBuilder.cs | 91 + .../TestTiledNavMeshBuilder.cs | 120 + .../TiledFindPathTest.cs | 69 + .../AbstractTileCacheTest.cs | 74 + .../DotRecast.Detour.TileCache.Test.csproj | 25 + .../Io/TileCacheReaderTest.cs | 223 + .../Io/TileCacheReaderWriterTest.cs | 137 + .../SampleAreaModifications.cs | 55 + .../TempObstaclesTest.cs | 92 + .../TestTileLayerBuilder.cs | 120 + .../TileCacheFindPathTest.cs | 61 + .../TileCacheNavigationTest.cs | 103 + .../TileCacheTest.cs | 272 + .../DotRecast.Recast.Test.csproj | 25 + .../DotRecast.Recast.Test/RecastLayersTest.cs | 157 + .../RecastSoloMeshTest.cs | 337 + test/DotRecast.Recast.Test/RecastTest.cs | 55 + .../RecastTileMeshTest.cs | 167 + .../SampleAreaModifications.cs | 60 + 350 files changed, 78647 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 DotRecast.sln delete mode 100644 README.dm create mode 100644 README.md create mode 100644 resources/all_tiles_navmesh.bin create mode 100644 resources/all_tiles_tilecache.bin create mode 100644 resources/annotation_test.obj create mode 100644 resources/bridge.obj create mode 100644 resources/convex.obj create mode 100644 resources/dungeon.obj create mode 100644 resources/dungeon_all_tiles_navmesh.bin create mode 100644 resources/dungeon_all_tiles_navmesh_32bit.bin create mode 100644 resources/dungeon_all_tiles_tilecache.bin create mode 100644 resources/fonts/DroidSans.ttf create mode 100644 resources/graph.zip create mode 100644 resources/graph_v4_1_16.zip create mode 100644 resources/house.obj create mode 100644 resources/nav_test.obj create mode 100644 resources/test.voxels create mode 100644 resources/test_boundstree.zip create mode 100644 resources/test_tiles.voxels create mode 100644 src/DotRecast.Core/ArrayUtils.cs create mode 100644 src/DotRecast.Core/AtomicBoolean.cs create mode 100644 src/DotRecast.Core/AtomicFloat.cs create mode 100644 src/DotRecast.Core/AtomicInteger.cs create mode 100644 src/DotRecast.Core/AtomicLong.cs create mode 100644 src/DotRecast.Core/ByteBuffer.cs create mode 100644 src/DotRecast.Core/ByteOrder.cs create mode 100644 src/DotRecast.Core/CollectionExtensions.cs create mode 100644 src/DotRecast.Core/ConvexUtils.cs create mode 100644 src/DotRecast.Core/DemoMath.cs create mode 100644 src/DotRecast.Core/DotRecast.Core.csproj create mode 100644 src/DotRecast.Core/Loader.cs create mode 100644 src/DotRecast.Core/NodeQueue.cs create mode 100644 src/DotRecast.Detour.Crowd/Crowd.cs create mode 100644 src/DotRecast.Detour.Crowd/CrowdAgent.cs create mode 100644 src/DotRecast.Detour.Crowd/CrowdAgentAnimation.cs create mode 100644 src/DotRecast.Detour.Crowd/CrowdAgentParams.cs create mode 100644 src/DotRecast.Detour.Crowd/CrowdConfig.cs create mode 100644 src/DotRecast.Detour.Crowd/CrowdTelemetry.cs create mode 100644 src/DotRecast.Detour.Crowd/DotRecast.Detour.Crowd.csproj create mode 100644 src/DotRecast.Detour.Crowd/LocalBoundary.cs create mode 100644 src/DotRecast.Detour.Crowd/ObstacleAvoidanceQuery.cs create mode 100644 src/DotRecast.Detour.Crowd/PathCorridor.cs create mode 100644 src/DotRecast.Detour.Crowd/PathQuery.cs create mode 100644 src/DotRecast.Detour.Crowd/PathQueryResult.cs create mode 100644 src/DotRecast.Detour.Crowd/PathQueue.cs create mode 100644 src/DotRecast.Detour.Crowd/ProximityGrid.cs create mode 100644 src/DotRecast.Detour.Crowd/SweepCircleCircleResult.cs create mode 100644 src/DotRecast.Detour.Crowd/Tracking/CrowdAgentDebugInfo.cs create mode 100644 src/DotRecast.Detour.Crowd/Tracking/ObstacleAvoidanceDebugData.cs create mode 100644 src/DotRecast.Detour.Dynamic/AddColliderQueueItem.cs create mode 100644 src/DotRecast.Detour.Dynamic/Colliders/AbstractCollider.cs create mode 100644 src/DotRecast.Detour.Dynamic/Colliders/BoxCollider.cs create mode 100644 src/DotRecast.Detour.Dynamic/Colliders/CapsuleCollider.cs create mode 100644 src/DotRecast.Detour.Dynamic/Colliders/Collider.cs create mode 100644 src/DotRecast.Detour.Dynamic/Colliders/CompositeCollider.cs create mode 100644 src/DotRecast.Detour.Dynamic/Colliders/ConvexTrimeshCollider.cs create mode 100644 src/DotRecast.Detour.Dynamic/Colliders/CylinderCollider.cs create mode 100644 src/DotRecast.Detour.Dynamic/Colliders/SphereCollider.cs create mode 100644 src/DotRecast.Detour.Dynamic/Colliders/TrimeshCollider.cs create mode 100644 src/DotRecast.Detour.Dynamic/DotRecast.Detour.Dynamic.csproj create mode 100644 src/DotRecast.Detour.Dynamic/DynamicNavMesh.cs create mode 100644 src/DotRecast.Detour.Dynamic/DynamicNavMeshConfig.cs create mode 100644 src/DotRecast.Detour.Dynamic/DynamicTile.cs create mode 100644 src/DotRecast.Detour.Dynamic/DynamicTileCheckpoint.cs create mode 100644 src/DotRecast.Detour.Dynamic/Io/ByteUtils.cs create mode 100644 src/DotRecast.Detour.Dynamic/Io/LZ4VoxelTileCompressor.cs create mode 100644 src/DotRecast.Detour.Dynamic/Io/VoxelFile.cs create mode 100644 src/DotRecast.Detour.Dynamic/Io/VoxelFileReader.cs create mode 100644 src/DotRecast.Detour.Dynamic/Io/VoxelFileWriter.cs create mode 100644 src/DotRecast.Detour.Dynamic/Io/VoxelTile.cs create mode 100644 src/DotRecast.Detour.Dynamic/RemoveColliderQueueItem.cs create mode 100644 src/DotRecast.Detour.Dynamic/UpdateQueueItem.cs create mode 100644 src/DotRecast.Detour.Dynamic/VoxelQuery.cs create mode 100644 src/DotRecast.Detour.Extras/BVTreeBuilder.cs create mode 100644 src/DotRecast.Detour.Extras/DotRecast.Detour.Extras.csproj create mode 100644 src/DotRecast.Detour.Extras/Jumplink/AbstractGroundSampler.cs create mode 100644 src/DotRecast.Detour.Extras/Jumplink/ClimbTrajectory.cs create mode 100644 src/DotRecast.Detour.Extras/Jumplink/Edge.cs create mode 100644 src/DotRecast.Detour.Extras/Jumplink/EdgeExtractor.cs create mode 100644 src/DotRecast.Detour.Extras/Jumplink/EdgeSampler.cs create mode 100644 src/DotRecast.Detour.Extras/Jumplink/EdgeSamplerFactory.cs create mode 100644 src/DotRecast.Detour.Extras/Jumplink/GroundSample.cs create mode 100644 src/DotRecast.Detour.Extras/Jumplink/GroundSampler.cs create mode 100644 src/DotRecast.Detour.Extras/Jumplink/GroundSegment.cs create mode 100644 src/DotRecast.Detour.Extras/Jumplink/JumpLink.cs create mode 100644 src/DotRecast.Detour.Extras/Jumplink/JumpLinkBuilder.cs create mode 100644 src/DotRecast.Detour.Extras/Jumplink/JumpLinkBuilderConfig.cs create mode 100644 src/DotRecast.Detour.Extras/Jumplink/JumpLinkType.cs create mode 100644 src/DotRecast.Detour.Extras/Jumplink/JumpSegment.cs create mode 100644 src/DotRecast.Detour.Extras/Jumplink/JumpSegmentBuilder.cs create mode 100644 src/DotRecast.Detour.Extras/Jumplink/JumpTrajectory.cs create mode 100644 src/DotRecast.Detour.Extras/Jumplink/NavMeshGroundSampler.cs create mode 100644 src/DotRecast.Detour.Extras/Jumplink/Trajectory.cs create mode 100644 src/DotRecast.Detour.Extras/Jumplink/TrajectorySampler.cs create mode 100644 src/DotRecast.Detour.Extras/ObjExporter.cs create mode 100644 src/DotRecast.Detour.Extras/PolyUtils.cs create mode 100644 src/DotRecast.Detour.Extras/Unity/Astar/BVTreeCreator.cs create mode 100644 src/DotRecast.Detour.Extras/Unity/Astar/GraphConnectionReader.cs create mode 100644 src/DotRecast.Detour.Extras/Unity/Astar/GraphData.cs create mode 100644 src/DotRecast.Detour.Extras/Unity/Astar/GraphMeshData.cs create mode 100644 src/DotRecast.Detour.Extras/Unity/Astar/GraphMeshDataReader.cs create mode 100644 src/DotRecast.Detour.Extras/Unity/Astar/GraphMeta.cs create mode 100644 src/DotRecast.Detour.Extras/Unity/Astar/GraphMetaReader.cs create mode 100644 src/DotRecast.Detour.Extras/Unity/Astar/LinkBuilder.cs create mode 100644 src/DotRecast.Detour.Extras/Unity/Astar/Meta.cs create mode 100644 src/DotRecast.Detour.Extras/Unity/Astar/MetaReader.cs create mode 100644 src/DotRecast.Detour.Extras/Unity/Astar/NodeIndexReader.cs create mode 100644 src/DotRecast.Detour.Extras/Unity/Astar/NodeLink2.cs create mode 100644 src/DotRecast.Detour.Extras/Unity/Astar/NodeLink2Reader.cs create mode 100644 src/DotRecast.Detour.Extras/Unity/Astar/OffMeshLinkCreator.cs create mode 100644 src/DotRecast.Detour.Extras/Unity/Astar/UnityAStarPathfindingImporter.cs create mode 100644 src/DotRecast.Detour.Extras/Unity/Astar/UnityAStarPathfindingReader.cs create mode 100644 src/DotRecast.Detour.Extras/Unity/Astar/ZipBinaryReader.cs create mode 100644 src/DotRecast.Detour.Extras/Vector3f.cs create mode 100644 src/DotRecast.Detour.TileCache/AbstractTileLayersBuilder.cs create mode 100644 src/DotRecast.Detour.TileCache/CompressedTile.cs create mode 100644 src/DotRecast.Detour.TileCache/DotRecast.Detour.TileCache.csproj create mode 100644 src/DotRecast.Detour.TileCache/Io/Compress/FastLz.cs create mode 100644 src/DotRecast.Detour.TileCache/Io/Compress/FastLzTileCacheCompressor.cs create mode 100644 src/DotRecast.Detour.TileCache/Io/Compress/LZ4TileCacheCompressor.cs create mode 100644 src/DotRecast.Detour.TileCache/Io/Compress/TileCacheCompressorFactory.cs create mode 100644 src/DotRecast.Detour.TileCache/Io/TileCacheLayerHeaderReader.cs create mode 100644 src/DotRecast.Detour.TileCache/Io/TileCacheLayerHeaderWriter.cs create mode 100644 src/DotRecast.Detour.TileCache/Io/TileCacheReader.cs create mode 100644 src/DotRecast.Detour.TileCache/Io/TileCacheSetHeader.cs create mode 100644 src/DotRecast.Detour.TileCache/Io/TileCacheWriter.cs create mode 100644 src/DotRecast.Detour.TileCache/ObstacleRequest.cs create mode 100644 src/DotRecast.Detour.TileCache/ObstacleRequestAction.cs create mode 100644 src/DotRecast.Detour.TileCache/ObstacleState.cs create mode 100644 src/DotRecast.Detour.TileCache/TileCache.cs create mode 100644 src/DotRecast.Detour.TileCache/TileCacheBuilder.cs create mode 100644 src/DotRecast.Detour.TileCache/TileCacheCompressor.cs create mode 100644 src/DotRecast.Detour.TileCache/TileCacheContour.cs create mode 100644 src/DotRecast.Detour.TileCache/TileCacheContourSet.cs create mode 100644 src/DotRecast.Detour.TileCache/TileCacheLayer.cs create mode 100644 src/DotRecast.Detour.TileCache/TileCacheLayerHeader.cs create mode 100644 src/DotRecast.Detour.TileCache/TileCacheMeshProcess.cs create mode 100644 src/DotRecast.Detour.TileCache/TileCacheObstacle.cs create mode 100644 src/DotRecast.Detour.TileCache/TileCacheParams.cs create mode 100644 src/DotRecast.Detour.TileCache/TileCachePolyMesh.cs create mode 100644 src/DotRecast.Detour.TileCache/TileCacheStorageParams.cs create mode 100644 src/DotRecast.Detour/BVNode.cs create mode 100644 src/DotRecast.Detour/ClosestPointOnPolyResult.cs create mode 100644 src/DotRecast.Detour/ConvexConvexIntersection.cs create mode 100644 src/DotRecast.Detour/DefaultQueryFilter.cs create mode 100644 src/DotRecast.Detour/DefaultQueryHeuristic.cs create mode 100644 src/DotRecast.Detour/DetourBuilder.cs create mode 100644 src/DotRecast.Detour/DetourCommon.cs create mode 100644 src/DotRecast.Detour/DotRecast.Detour.csproj create mode 100644 src/DotRecast.Detour/FindDistanceToWallResult.cs create mode 100644 src/DotRecast.Detour/FindLocalNeighbourhoodResult.cs create mode 100644 src/DotRecast.Detour/FindNearestPolyQuery.cs create mode 100644 src/DotRecast.Detour/FindNearestPolyResult.cs create mode 100644 src/DotRecast.Detour/FindPolysAroundResult.cs create mode 100644 src/DotRecast.Detour/FindRandomPointResult.cs create mode 100644 src/DotRecast.Detour/GetPolyWallSegmentsResult.cs create mode 100644 src/DotRecast.Detour/Io/DetourWriter.cs create mode 100644 src/DotRecast.Detour/Io/IOUtils.cs create mode 100644 src/DotRecast.Detour/Io/MeshDataReader.cs create mode 100644 src/DotRecast.Detour/Io/MeshDataWriter.cs create mode 100644 src/DotRecast.Detour/Io/MeshSetReader.cs create mode 100644 src/DotRecast.Detour/Io/MeshSetWriter.cs create mode 100644 src/DotRecast.Detour/Io/NavMeshParamReader.cs create mode 100644 src/DotRecast.Detour/Io/NavMeshParamWriter.cs create mode 100644 src/DotRecast.Detour/Io/NavMeshSetHeader.cs create mode 100644 src/DotRecast.Detour/Io/NavMeshTileHeader.cs create mode 100644 src/DotRecast.Detour/LegacyNavMeshQuery.cs create mode 100644 src/DotRecast.Detour/Link.cs create mode 100644 src/DotRecast.Detour/MeshData.cs create mode 100644 src/DotRecast.Detour/MeshHeader.cs create mode 100644 src/DotRecast.Detour/MeshTile.cs create mode 100644 src/DotRecast.Detour/MoveAlongSurfaceResult.cs create mode 100644 src/DotRecast.Detour/NavMesh.cs create mode 100644 src/DotRecast.Detour/NavMeshBuilder.cs create mode 100644 src/DotRecast.Detour/NavMeshDataCreateParams.cs create mode 100644 src/DotRecast.Detour/NavMeshParams.cs create mode 100644 src/DotRecast.Detour/NavMeshQuery.cs create mode 100644 src/DotRecast.Detour/Node.cs create mode 100644 src/DotRecast.Detour/NodePool.cs create mode 100644 src/DotRecast.Detour/NodeQueue.cs create mode 100644 src/DotRecast.Detour/OffMeshConnection.cs create mode 100644 src/DotRecast.Detour/Poly.cs create mode 100644 src/DotRecast.Detour/PolyDetail.cs create mode 100644 src/DotRecast.Detour/PolyQuery.cs create mode 100644 src/DotRecast.Detour/PolygonByCircleConstraint.cs create mode 100644 src/DotRecast.Detour/QueryData.cs create mode 100644 src/DotRecast.Detour/QueryFilter.cs create mode 100644 src/DotRecast.Detour/QueryHeuristic.cs create mode 100644 src/DotRecast.Detour/RaycastHit.cs create mode 100644 src/DotRecast.Detour/Result.cs create mode 100644 src/DotRecast.Detour/Status.cs create mode 100644 src/DotRecast.Detour/StraightPathItem.cs create mode 100644 src/DotRecast.Detour/VectorPtr.cs create mode 100644 src/DotRecast.Recast.Demo/Builder/AbstractNavMeshBuilder.cs create mode 100644 src/DotRecast.Recast.Demo/Builder/SampleAreaModifications.cs create mode 100644 src/DotRecast.Recast.Demo/Builder/SoloNavMeshBuilder.cs create mode 100644 src/DotRecast.Recast.Demo/Builder/TileNavMeshBuilder.cs create mode 100644 src/DotRecast.Recast.Demo/DotRecast.Recast.Demo.csproj create mode 100644 src/DotRecast.Recast.Demo/Draw/DebugDraw.cs create mode 100644 src/DotRecast.Recast.Demo/Draw/DebugDrawPrimitives.cs create mode 100644 src/DotRecast.Recast.Demo/Draw/DrawMode.cs create mode 100644 src/DotRecast.Recast.Demo/Draw/GLCheckerTexture.cs create mode 100644 src/DotRecast.Recast.Demo/Draw/GLU.cs create mode 100644 src/DotRecast.Recast.Demo/Draw/LegacyOpenGLDraw.cs create mode 100644 src/DotRecast.Recast.Demo/Draw/ModernOpenGLDraw.cs create mode 100644 src/DotRecast.Recast.Demo/Draw/NavMeshRenderer.cs create mode 100644 src/DotRecast.Recast.Demo/Draw/OpenGLDraw.cs create mode 100644 src/DotRecast.Recast.Demo/Draw/OpenGLVertex.cs create mode 100644 src/DotRecast.Recast.Demo/Draw/RecastDebugDraw.cs create mode 100644 src/DotRecast.Recast.Demo/Geom/ChunkyTriMesh.cs create mode 100644 src/DotRecast.Recast.Demo/Geom/DemoInputGeomProvider.cs create mode 100644 src/DotRecast.Recast.Demo/Geom/DemoOffMeshConnection.cs create mode 100644 src/DotRecast.Recast.Demo/Geom/Intersections.cs create mode 100644 src/DotRecast.Recast.Demo/Geom/NavMeshRaycast.cs create mode 100644 src/DotRecast.Recast.Demo/Geom/NavMeshUtils.cs create mode 100644 src/DotRecast.Recast.Demo/Geom/PolyMeshRaycast.cs create mode 100644 src/DotRecast.Recast.Demo/RecastDemo.cs create mode 100644 src/DotRecast.Recast.Demo/Sample.cs create mode 100644 src/DotRecast.Recast.Demo/Settings/SettingsUI.cs create mode 100644 src/DotRecast.Recast.Demo/Tools/ConvexVolumeTool.cs create mode 100644 src/DotRecast.Recast.Demo/Tools/CrowdProfilingTool.cs create mode 100644 src/DotRecast.Recast.Demo/Tools/CrowdTool.cs create mode 100644 src/DotRecast.Recast.Demo/Tools/CrowdToolParams.cs create mode 100644 src/DotRecast.Recast.Demo/Tools/DemoObjImporter.cs create mode 100644 src/DotRecast.Recast.Demo/Tools/DynamicUpdateTool.cs create mode 100644 src/DotRecast.Recast.Demo/Tools/Gizmos/BoxGizmo.cs create mode 100644 src/DotRecast.Recast.Demo/Tools/Gizmos/CapsuleGizmo.cs create mode 100644 src/DotRecast.Recast.Demo/Tools/Gizmos/ColliderGizmo.cs create mode 100644 src/DotRecast.Recast.Demo/Tools/Gizmos/CompositeGizmo.cs create mode 100644 src/DotRecast.Recast.Demo/Tools/Gizmos/CylinderGizmo.cs create mode 100644 src/DotRecast.Recast.Demo/Tools/Gizmos/GizmoFactory.cs create mode 100644 src/DotRecast.Recast.Demo/Tools/Gizmos/GizmoHelper.cs create mode 100644 src/DotRecast.Recast.Demo/Tools/Gizmos/SphereGizmo.cs create mode 100644 src/DotRecast.Recast.Demo/Tools/Gizmos/TrimeshGizmo.cs create mode 100644 src/DotRecast.Recast.Demo/Tools/JumpLinkBuilderTool.cs create mode 100644 src/DotRecast.Recast.Demo/Tools/JumpLinkBuilderToolParams.cs create mode 100644 src/DotRecast.Recast.Demo/Tools/OffMeshConnectionTool.cs create mode 100644 src/DotRecast.Recast.Demo/Tools/PathUtils.cs create mode 100644 src/DotRecast.Recast.Demo/Tools/PolyUtils.cs create mode 100644 src/DotRecast.Recast.Demo/Tools/SteerTarget.cs create mode 100644 src/DotRecast.Recast.Demo/Tools/TestNavmeshTool.cs create mode 100644 src/DotRecast.Recast.Demo/Tools/Tool.cs create mode 100644 src/DotRecast.Recast.Demo/Tools/ToolUIModule.cs create mode 100644 src/DotRecast.Recast.Demo/Tools/ToolsUI.cs create mode 100644 src/DotRecast.Recast.Demo/UI/Mouse.cs create mode 100644 src/DotRecast.Recast.Demo/UI/MouseListener.cs create mode 100644 src/DotRecast.Recast.Demo/UI/NuklearGL.cs create mode 100644 src/DotRecast.Recast.Demo/UI/NuklearUI.cs create mode 100644 src/DotRecast.Recast.Demo/UI/NuklearUIHelper.cs create mode 100644 src/DotRecast.Recast.Demo/UI/NuklearUIModule.cs create mode 100644 src/DotRecast.Recast.Demo/imgui.ini create mode 100644 src/DotRecast.Recast/AreaModification.cs create mode 100644 src/DotRecast.Recast/CompactCell.cs create mode 100644 src/DotRecast.Recast/CompactHeightfield.cs create mode 100644 src/DotRecast.Recast/CompactSpan.cs create mode 100644 src/DotRecast.Recast/Contour.cs create mode 100644 src/DotRecast.Recast/ContourSet.cs create mode 100644 src/DotRecast.Recast/ConvexVolume.cs create mode 100644 src/DotRecast.Recast/DotRecast.Recast.csproj create mode 100644 src/DotRecast.Recast/Geom/ChunkyTriMesh.cs create mode 100644 src/DotRecast.Recast/Geom/ChunkyTriMeshNode.cs create mode 100644 src/DotRecast.Recast/Geom/ConvexVolumeProvider.cs create mode 100644 src/DotRecast.Recast/Geom/InputGeomProvider.cs create mode 100644 src/DotRecast.Recast/Geom/SimpleInputGeomProvider.cs create mode 100644 src/DotRecast.Recast/Geom/SingleTrimeshInputGeomProvider.cs create mode 100644 src/DotRecast.Recast/Geom/TriMesh.cs create mode 100644 src/DotRecast.Recast/Heightfield.cs create mode 100644 src/DotRecast.Recast/HeightfieldLayerSet.cs create mode 100644 src/DotRecast.Recast/InputGeomReader.cs create mode 100644 src/DotRecast.Recast/ObjImporter.cs create mode 100644 src/DotRecast.Recast/PartitionType.cs create mode 100644 src/DotRecast.Recast/PolyMesh.cs create mode 100644 src/DotRecast.Recast/PolyMeshDetail.cs create mode 100644 src/DotRecast.Recast/Recast.cs create mode 100644 src/DotRecast.Recast/RecastArea.cs create mode 100644 src/DotRecast.Recast/RecastBuilder.cs create mode 100644 src/DotRecast.Recast/RecastBuilderConfig.cs create mode 100644 src/DotRecast.Recast/RecastBuilderResult.cs create mode 100644 src/DotRecast.Recast/RecastCommon.cs create mode 100644 src/DotRecast.Recast/RecastCompact.cs create mode 100644 src/DotRecast.Recast/RecastConfig.cs create mode 100644 src/DotRecast.Recast/RecastConstants.cs create mode 100644 src/DotRecast.Recast/RecastContour.cs create mode 100644 src/DotRecast.Recast/RecastFilledVolumeRasterization.cs create mode 100644 src/DotRecast.Recast/RecastFilter.cs create mode 100644 src/DotRecast.Recast/RecastLayers.cs create mode 100644 src/DotRecast.Recast/RecastMesh.cs create mode 100644 src/DotRecast.Recast/RecastMeshDetail.cs create mode 100644 src/DotRecast.Recast/RecastRasterization.cs create mode 100644 src/DotRecast.Recast/RecastRegion.cs create mode 100644 src/DotRecast.Recast/RecastVectors.cs create mode 100644 src/DotRecast.Recast/RecastVoxelization.cs create mode 100644 src/DotRecast.Recast/Span.cs create mode 100644 src/DotRecast.Recast/Telemetry.cs create mode 100644 test/DotRecast.Detour.Crowd.Test/AbstractCrowdTest.cs create mode 100644 test/DotRecast.Detour.Crowd.Test/Crowd1Test.cs create mode 100644 test/DotRecast.Detour.Crowd.Test/Crowd4Test.cs create mode 100644 test/DotRecast.Detour.Crowd.Test/Crowd4VelocityTest.cs create mode 100644 test/DotRecast.Detour.Crowd.Test/DotRecast.Detour.Crowd.Test.csproj create mode 100644 test/DotRecast.Detour.Crowd.Test/PathCorridorTest.cs create mode 100644 test/DotRecast.Detour.Crowd.Test/RecastTestMeshBuilder.cs create mode 100644 test/DotRecast.Detour.Crowd.Test/SampleAreaModifications.cs create mode 100644 test/DotRecast.Detour.Dynamic.Test/DotRecast.Detour.Dynamic.Test.csproj create mode 100644 test/DotRecast.Detour.Dynamic.Test/DynamicNavMeshTest.cs create mode 100644 test/DotRecast.Detour.Dynamic.Test/Io/VoxelFileReaderTest.cs create mode 100644 test/DotRecast.Detour.Dynamic.Test/Io/VoxelFileReaderWriterTest.cs create mode 100644 test/DotRecast.Detour.Dynamic.Test/SampleAreaModifications.cs create mode 100644 test/DotRecast.Detour.Dynamic.Test/VoxelQueryTest.cs create mode 100644 test/DotRecast.Detour.Extras.Test/DotRecast.Detour.Extras.Test.csproj create mode 100644 test/DotRecast.Detour.Extras.Test/Unity/Astar/UnityAStarPathfindingImporterTest.cs create mode 100644 test/DotRecast.Detour.Test/AbstractDetourTest.cs create mode 100644 test/DotRecast.Detour.Test/ConvexConvexIntersectionTest.cs create mode 100644 test/DotRecast.Detour.Test/DotRecast.Detour.Test.csproj create mode 100644 test/DotRecast.Detour.Test/FindDistanceToWallTest.cs create mode 100644 test/DotRecast.Detour.Test/FindLocalNeighbourhoodTest.cs create mode 100644 test/DotRecast.Detour.Test/FindNearestPolyTest.cs create mode 100644 test/DotRecast.Detour.Test/FindPathTest.cs create mode 100644 test/DotRecast.Detour.Test/FindPolysAroundCircleTest.cs create mode 100644 test/DotRecast.Detour.Test/FindPolysAroundShapeTest.cs create mode 100644 test/DotRecast.Detour.Test/GetPolyWallSegmentsTest.cs create mode 100644 test/DotRecast.Detour.Test/Io/MeshDataReaderWriterTest.cs create mode 100644 test/DotRecast.Detour.Test/Io/MeshSetReaderTest.cs create mode 100644 test/DotRecast.Detour.Test/Io/MeshSetReaderWriterTest.cs create mode 100644 test/DotRecast.Detour.Test/MoveAlongSurfaceTest.cs create mode 100644 test/DotRecast.Detour.Test/NavMeshBuilderTest.cs create mode 100644 test/DotRecast.Detour.Test/PolygonByCircleConstraintTest.cs create mode 100644 test/DotRecast.Detour.Test/RandomPointTest.cs create mode 100644 test/DotRecast.Detour.Test/RecastTestMeshBuilder.cs create mode 100644 test/DotRecast.Detour.Test/SampleAreaModifications.cs create mode 100644 test/DotRecast.Detour.Test/TestDetourBuilder.cs create mode 100644 test/DotRecast.Detour.Test/TestTiledNavMeshBuilder.cs create mode 100644 test/DotRecast.Detour.Test/TiledFindPathTest.cs create mode 100644 test/DotRecast.Detour.TileCache.Test/AbstractTileCacheTest.cs create mode 100644 test/DotRecast.Detour.TileCache.Test/DotRecast.Detour.TileCache.Test.csproj create mode 100644 test/DotRecast.Detour.TileCache.Test/Io/TileCacheReaderTest.cs create mode 100644 test/DotRecast.Detour.TileCache.Test/Io/TileCacheReaderWriterTest.cs create mode 100644 test/DotRecast.Detour.TileCache.Test/SampleAreaModifications.cs create mode 100644 test/DotRecast.Detour.TileCache.Test/TempObstaclesTest.cs create mode 100644 test/DotRecast.Detour.TileCache.Test/TestTileLayerBuilder.cs create mode 100644 test/DotRecast.Detour.TileCache.Test/TileCacheFindPathTest.cs create mode 100644 test/DotRecast.Detour.TileCache.Test/TileCacheNavigationTest.cs create mode 100644 test/DotRecast.Detour.TileCache.Test/TileCacheTest.cs create mode 100644 test/DotRecast.Recast.Test/DotRecast.Recast.Test.csproj create mode 100644 test/DotRecast.Recast.Test/RecastLayersTest.cs create mode 100644 test/DotRecast.Recast.Test/RecastSoloMeshTest.cs create mode 100644 test/DotRecast.Recast.Test/RecastTest.cs create mode 100644 test/DotRecast.Recast.Test/RecastTileMeshTest.cs create mode 100644 test/DotRecast.Recast.Test/SampleAreaModifications.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1bb9163 --- /dev/null +++ b/.gitignore @@ -0,0 +1,399 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +#*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml +.idea/ \ No newline at end of file diff --git a/DotRecast.sln b/DotRecast.sln new file mode 100644 index 0000000..e07d120 --- /dev/null +++ b/DotRecast.sln @@ -0,0 +1,120 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{8ED75CF7-A3D6-423D-8499-9316DD413DAD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotRecast.Core", "src\DotRecast.Core\DotRecast.Core.csproj", "{C19E4BFA-63A0-4815-9815-869A9DC52DBC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotRecast.Recast", "src\DotRecast.Recast\DotRecast.Recast.csproj", "{38933A87-4568-40A5-A3DA-E2445E8C2B99}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotRecast.Detour", "src\DotRecast.Detour\DotRecast.Detour.csproj", "{FFE40BBF-843B-41FA-8504-F4ABD166762E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotRecast.Detour.Crowd", "src\DotRecast.Detour.Crowd\DotRecast.Detour.Crowd.csproj", "{FA7EF26A-BA47-43FD-86F8-0A33CFDF643F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotRecast.Detour.Dynamic", "src\DotRecast.Detour.Dynamic\DotRecast.Detour.Dynamic.csproj", "{53AF87DA-37F8-4504-B623-B2113F4438CA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotRecast.Detour.Extras", "src\DotRecast.Detour.Extras\DotRecast.Detour.Extras.csproj", "{17E4F2F0-FC27-416E-9CB6-9F2CAAC49C9D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotRecast.Detour.TileCache", "src\DotRecast.Detour.TileCache\DotRecast.Detour.TileCache.csproj", "{DEB16B90-CCD4-497E-A2E9-4CC66FD7EF47}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{A7CB8D8B-70DA-4567-8316-0659FCAE1C73}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotRecast.Recast.Test", "test\DotRecast.Recast.Test\DotRecast.Recast.Test.csproj", "{88754FE2-A05A-4D4D-A81A-90418AD32362}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotRecast.Detour.Test", "test\DotRecast.Detour.Test\DotRecast.Detour.Test.csproj", "{554CB5BD-D58A-4856-BFE1-666A62C9BEA3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotRecast.Detour.Crowd.Test", "test\DotRecast.Detour.Crowd.Test\DotRecast.Detour.Crowd.Test.csproj", "{F9C5B52E-C01D-4514-94E9-B1A6895352E2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotRecast.Detour.Dynamic.Test", "test\DotRecast.Detour.Dynamic.Test\DotRecast.Detour.Dynamic.Test.csproj", "{67C68B34-118A-439C-88E1-D6D1ED78DC59}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotRecast.Detour.Extras.Test", "test\DotRecast.Detour.Extras.Test\DotRecast.Detour.Extras.Test.csproj", "{7BAA69B2-EDC7-4603-B16F-BC7B24353F81}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotRecast.Detour.TileCache.Test", "test\DotRecast.Detour.TileCache.Test\DotRecast.Detour.TileCache.Test.csproj", "{3CAA7306-088E-4373-A406-99755CC2B605}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotRecast.Recast.Demo", "src\DotRecast.Recast.Demo\DotRecast.Recast.Demo.csproj", "{023E1E6A-4895-4573-89AE-3D5D8E0B39C8}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {FFE40BBF-843B-41FA-8504-F4ABD166762E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FFE40BBF-843B-41FA-8504-F4ABD166762E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FFE40BBF-843B-41FA-8504-F4ABD166762E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FFE40BBF-843B-41FA-8504-F4ABD166762E}.Release|Any CPU.Build.0 = Release|Any CPU + {38933A87-4568-40A5-A3DA-E2445E8C2B99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {38933A87-4568-40A5-A3DA-E2445E8C2B99}.Debug|Any CPU.Build.0 = Debug|Any CPU + {38933A87-4568-40A5-A3DA-E2445E8C2B99}.Release|Any CPU.ActiveCfg = Release|Any CPU + {38933A87-4568-40A5-A3DA-E2445E8C2B99}.Release|Any CPU.Build.0 = Release|Any CPU + {C19E4BFA-63A0-4815-9815-869A9DC52DBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C19E4BFA-63A0-4815-9815-869A9DC52DBC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C19E4BFA-63A0-4815-9815-869A9DC52DBC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C19E4BFA-63A0-4815-9815-869A9DC52DBC}.Release|Any CPU.Build.0 = Release|Any CPU + {88754FE2-A05A-4D4D-A81A-90418AD32362}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {88754FE2-A05A-4D4D-A81A-90418AD32362}.Debug|Any CPU.Build.0 = Debug|Any CPU + {88754FE2-A05A-4D4D-A81A-90418AD32362}.Release|Any CPU.ActiveCfg = Release|Any CPU + {88754FE2-A05A-4D4D-A81A-90418AD32362}.Release|Any CPU.Build.0 = Release|Any CPU + {554CB5BD-D58A-4856-BFE1-666A62C9BEA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {554CB5BD-D58A-4856-BFE1-666A62C9BEA3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {554CB5BD-D58A-4856-BFE1-666A62C9BEA3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {554CB5BD-D58A-4856-BFE1-666A62C9BEA3}.Release|Any CPU.Build.0 = Release|Any CPU + {FA7EF26A-BA47-43FD-86F8-0A33CFDF643F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FA7EF26A-BA47-43FD-86F8-0A33CFDF643F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FA7EF26A-BA47-43FD-86F8-0A33CFDF643F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FA7EF26A-BA47-43FD-86F8-0A33CFDF643F}.Release|Any CPU.Build.0 = Release|Any CPU + {F9C5B52E-C01D-4514-94E9-B1A6895352E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F9C5B52E-C01D-4514-94E9-B1A6895352E2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F9C5B52E-C01D-4514-94E9-B1A6895352E2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F9C5B52E-C01D-4514-94E9-B1A6895352E2}.Release|Any CPU.Build.0 = Release|Any CPU + {53AF87DA-37F8-4504-B623-B2113F4438CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {53AF87DA-37F8-4504-B623-B2113F4438CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {53AF87DA-37F8-4504-B623-B2113F4438CA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {53AF87DA-37F8-4504-B623-B2113F4438CA}.Release|Any CPU.Build.0 = Release|Any CPU + {67C68B34-118A-439C-88E1-D6D1ED78DC59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {67C68B34-118A-439C-88E1-D6D1ED78DC59}.Debug|Any CPU.Build.0 = Debug|Any CPU + {67C68B34-118A-439C-88E1-D6D1ED78DC59}.Release|Any CPU.ActiveCfg = Release|Any CPU + {67C68B34-118A-439C-88E1-D6D1ED78DC59}.Release|Any CPU.Build.0 = Release|Any CPU + {17E4F2F0-FC27-416E-9CB6-9F2CAAC49C9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {17E4F2F0-FC27-416E-9CB6-9F2CAAC49C9D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {17E4F2F0-FC27-416E-9CB6-9F2CAAC49C9D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {17E4F2F0-FC27-416E-9CB6-9F2CAAC49C9D}.Release|Any CPU.Build.0 = Release|Any CPU + {7BAA69B2-EDC7-4603-B16F-BC7B24353F81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7BAA69B2-EDC7-4603-B16F-BC7B24353F81}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7BAA69B2-EDC7-4603-B16F-BC7B24353F81}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7BAA69B2-EDC7-4603-B16F-BC7B24353F81}.Release|Any CPU.Build.0 = Release|Any CPU + {DEB16B90-CCD4-497E-A2E9-4CC66FD7EF47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DEB16B90-CCD4-497E-A2E9-4CC66FD7EF47}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DEB16B90-CCD4-497E-A2E9-4CC66FD7EF47}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DEB16B90-CCD4-497E-A2E9-4CC66FD7EF47}.Release|Any CPU.Build.0 = Release|Any CPU + {3CAA7306-088E-4373-A406-99755CC2B605}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3CAA7306-088E-4373-A406-99755CC2B605}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3CAA7306-088E-4373-A406-99755CC2B605}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3CAA7306-088E-4373-A406-99755CC2B605}.Release|Any CPU.Build.0 = Release|Any CPU + {023E1E6A-4895-4573-89AE-3D5D8E0B39C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {023E1E6A-4895-4573-89AE-3D5D8E0B39C8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {023E1E6A-4895-4573-89AE-3D5D8E0B39C8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {023E1E6A-4895-4573-89AE-3D5D8E0B39C8}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {FFE40BBF-843B-41FA-8504-F4ABD166762E} = {8ED75CF7-A3D6-423D-8499-9316DD413DAD} + {38933A87-4568-40A5-A3DA-E2445E8C2B99} = {8ED75CF7-A3D6-423D-8499-9316DD413DAD} + {C19E4BFA-63A0-4815-9815-869A9DC52DBC} = {8ED75CF7-A3D6-423D-8499-9316DD413DAD} + {88754FE2-A05A-4D4D-A81A-90418AD32362} = {A7CB8D8B-70DA-4567-8316-0659FCAE1C73} + {554CB5BD-D58A-4856-BFE1-666A62C9BEA3} = {A7CB8D8B-70DA-4567-8316-0659FCAE1C73} + {FA7EF26A-BA47-43FD-86F8-0A33CFDF643F} = {8ED75CF7-A3D6-423D-8499-9316DD413DAD} + {F9C5B52E-C01D-4514-94E9-B1A6895352E2} = {A7CB8D8B-70DA-4567-8316-0659FCAE1C73} + {53AF87DA-37F8-4504-B623-B2113F4438CA} = {8ED75CF7-A3D6-423D-8499-9316DD413DAD} + {67C68B34-118A-439C-88E1-D6D1ED78DC59} = {A7CB8D8B-70DA-4567-8316-0659FCAE1C73} + {17E4F2F0-FC27-416E-9CB6-9F2CAAC49C9D} = {8ED75CF7-A3D6-423D-8499-9316DD413DAD} + {7BAA69B2-EDC7-4603-B16F-BC7B24353F81} = {A7CB8D8B-70DA-4567-8316-0659FCAE1C73} + {DEB16B90-CCD4-497E-A2E9-4CC66FD7EF47} = {8ED75CF7-A3D6-423D-8499-9316DD413DAD} + {3CAA7306-088E-4373-A406-99755CC2B605} = {A7CB8D8B-70DA-4567-8316-0659FCAE1C73} + {023E1E6A-4895-4573-89AE-3D5D8E0B39C8} = {8ED75CF7-A3D6-423D-8499-9316DD413DAD} + EndGlobalSection +EndGlobal diff --git a/README.dm b/README.dm deleted file mode 100644 index c0dd1e6..0000000 --- a/README.dm +++ /dev/null @@ -1 +0,0 @@ -DotRecast diff --git a/README.md b/README.md new file mode 100644 index 0000000..653c7d4 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# DotRecast \ No newline at end of file diff --git a/resources/all_tiles_navmesh.bin b/resources/all_tiles_navmesh.bin new file mode 100644 index 0000000000000000000000000000000000000000..aa91037db6adb1f23b8cbc64a28a8d95b3fb20f2 GIT binary patch literal 70980 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⩔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 literal 0 HcmV?d00001 diff --git a/resources/all_tiles_tilecache.bin b/resources/all_tiles_tilecache.bin new file mode 100644 index 0000000000000000000000000000000000000000..52b7c997e0afe62e2d91a06ebc8fac8ab371dbea GIT binary patch literal 47643 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 TjoE$}&1zS{AbPziF4+G7`?`^2 literal 0 HcmV?d00001 diff --git a/resources/annotation_test.obj b/resources/annotation_test.obj new file mode 100644 index 0000000..4128928 --- /dev/null +++ b/resources/annotation_test.obj @@ -0,0 +1,3562 @@ +mtllib nav_test.mtl +o nav_test.obj +v -9.168389 0.639097 6.997247 +v -7.293580 0.639097 6.768842 +v -9.168389 1.138378 6.997247 +v -7.293580 1.138378 6.768842 +v -9.086612 0.639097 7.668489 +v -7.211802 0.639097 7.440084 +v -9.086612 1.138378 7.668489 +v -7.211802 1.138378 7.440084 +v -9.240796 1.052578 6.402913 +v -7.365998 1.052578 6.174510 +v -9.240796 1.551854 6.402913 +v -7.365998 1.551854 6.174510 +v -9.159019 1.052578 7.074154 +v -7.284223 1.052578 6.845750 +v -9.159019 1.551854 7.074154 +v -7.284223 1.551854 6.845750 +v -9.313214 1.462714 5.808478 +v -7.438418 1.462714 5.580075 +v -9.313214 1.961990 5.808478 +v -7.438418 1.961990 5.580075 +v -9.231441 1.462714 6.479712 +v -7.356643 1.462714 6.251309 +v -9.231441 1.961990 6.479712 +v -7.356643 1.961990 6.251309 +v -9.385417 1.914500 5.215819 +v -7.510621 1.914500 4.987416 +v -9.385417 2.413783 5.215819 +v -7.510621 2.413783 4.987416 +v -9.303641 1.914500 5.887060 +v -7.428843 1.914500 5.658656 +v -9.303641 2.413783 5.887060 +v -7.428843 2.413783 5.658656 +v -9.458552 2.322021 4.615508 +v -7.583755 2.322021 4.387105 +v -9.458552 2.821298 4.615508 +v -7.583755 2.821298 4.387105 +v -9.376777 2.322021 5.286750 +v -7.501979 2.322021 5.058346 +v -9.376777 2.821298 5.286750 +v -7.501979 2.821298 5.058346 +v -9.531463 2.755649 4.017038 +v -7.656667 2.755649 3.788635 +v -9.531463 3.254925 4.017038 +v -7.656667 3.254925 3.788635 +v -9.449687 2.755649 4.688279 +v -7.574889 2.755649 4.459876 +v -9.449687 3.254925 4.688279 +v -7.574889 3.254925 4.459876 +v -9.601859 3.147897 3.439209 +v -7.727061 3.147897 3.210805 +v -9.601859 3.647173 3.439209 +v -7.727061 3.647173 3.210805 +v -9.520082 3.147897 4.110450 +v -7.645286 3.147897 3.882046 +v -9.520082 3.647173 4.110450 +v -7.645286 3.647173 3.882046 +vn -0.642935 -0.577351 -0.503291 +vn 0.503291 -0.577350 -0.642935 +vn -0.642935 0.577351 -0.503291 +vn 0.503291 0.577350 -0.642935 +vn -0.503291 -0.577350 0.642935 +vn 0.642935 -0.577351 0.503291 +vn -0.503291 0.577350 0.642935 +vn 0.642935 0.577351 0.503291 +vn -0.642934 -0.577351 -0.503291 +vn 0.503292 -0.577350 -0.642934 +vn -0.642934 0.577351 -0.503291 +vn 0.503292 0.577350 -0.642934 +vn -0.503291 -0.577350 0.642935 +vn 0.642934 -0.577350 0.503292 +vn -0.503291 0.577350 0.642935 +vn 0.642934 0.577350 0.503292 +vn -0.642934 -0.577350 -0.503292 +vn 0.503291 -0.577350 -0.642934 +vn -0.642934 0.577350 -0.503292 +vn 0.503291 0.577350 -0.642934 +vn -0.503292 -0.577351 0.642933 +vn 0.642934 -0.577350 0.503291 +vn -0.503292 0.577351 0.642933 +vn 0.642934 0.577350 0.503291 +vn -0.642934 -0.577350 -0.503292 +vn 0.503291 -0.577350 -0.642935 +vn -0.642934 0.577350 -0.503292 +vn 0.503291 0.577350 -0.642935 +vn -0.503291 -0.577350 0.642934 +vn 0.642935 -0.577351 0.503290 +vn -0.503291 0.577350 0.642934 +vn 0.642935 0.577351 0.503290 +vn -0.642934 -0.577350 -0.503292 +vn 0.503292 -0.577350 -0.642934 +vn -0.642934 0.577350 -0.503292 +vn 0.503292 0.577350 -0.642934 +vn -0.503292 -0.577350 0.642934 +vn 0.642934 -0.577350 0.503292 +vn -0.503292 0.577350 0.642934 +vn 0.642934 0.577350 0.503292 +vn -0.642934 -0.577350 -0.503292 +vn 0.503291 -0.577350 -0.642935 +vn -0.642934 0.577350 -0.503292 +vn 0.503291 0.577350 -0.642935 +vn -0.503292 -0.577350 0.642934 +vn 0.642934 -0.577351 0.503291 +vn -0.503292 0.577350 0.642934 +vn 0.642934 0.577351 0.503291 +vn -0.642935 -0.577351 -0.503291 +vn 0.503292 -0.577350 -0.642934 +vn -0.642935 0.577351 -0.503291 +vn 0.503292 0.577350 -0.642934 +vn -0.503291 -0.577350 0.642935 +vn 0.642934 -0.577350 0.503292 +vn -0.503291 0.577350 0.642935 +vn 0.642934 0.577350 0.503292 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +g Generic +usemtl Default +f 4/1/4 2/2/2 1/3/1 3/4/3 +f 6/5/6 8/6/8 7/7/7 5/8/5 +f 6/5/6 5/8/5 1/3/1 2/2/2 +f 8/6/8 6/5/6 2/2/2 4/1/4 +f 7/7/7 8/6/8 4/1/4 3/4/3 +f 5/8/5 7/7/7 3/4/3 1/3/1 +f 12/9/12 10/10/10 9/11/9 11/12/11 +f 14/13/14 16/14/16 15/15/15 13/16/13 +f 14/13/14 13/16/13 9/11/9 10/10/10 +f 16/14/16 14/13/14 10/10/10 12/9/12 +f 15/15/15 16/14/16 12/9/12 11/12/11 +f 13/16/13 15/15/15 11/12/11 9/11/9 +f 20/17/20 18/18/18 17/19/17 19/20/19 +f 22/21/22 24/22/24 23/23/23 21/24/21 +f 22/21/22 21/24/21 17/19/17 18/18/18 +f 24/22/24 22/21/22 18/18/18 20/17/20 +f 23/23/23 24/22/24 20/17/20 19/20/19 +f 21/24/21 23/23/23 19/20/19 17/19/17 +f 28/25/28 26/26/26 25/27/25 27/28/27 +f 30/29/30 32/30/32 31/31/31 29/32/29 +f 30/29/30 29/32/29 25/27/25 26/26/26 +f 32/30/32 30/29/30 26/26/26 28/25/28 +f 31/31/31 32/30/32 28/25/28 27/28/27 +f 29/32/29 31/31/31 27/28/27 25/27/25 +f 36/33/36 34/34/34 33/35/33 35/36/35 +f 38/37/38 40/38/40 39/39/39 37/40/37 +f 38/37/38 37/40/37 33/35/33 34/34/34 +f 40/38/40 38/37/38 34/34/34 36/33/36 +f 39/39/39 40/38/40 36/33/36 35/36/35 +f 37/40/37 39/39/39 35/36/35 33/35/33 +f 44/41/44 42/42/42 41/43/41 43/44/43 +f 46/45/46 48/46/48 47/47/47 45/48/45 +f 46/45/46 45/48/45 41/43/41 42/42/42 +f 48/46/48 46/45/46 42/42/42 44/41/44 +f 47/47/47 48/46/48 44/41/44 43/44/43 +f 45/48/45 47/47/47 43/44/43 41/43/41 +f 52/49/52 50/50/50 49/51/49 51/52/51 +f 54/53/54 56/54/56 55/55/55 53/56/53 +f 54/53/54 53/56/53 49/51/49 50/50/50 +f 56/54/56 54/53/54 50/50/50 52/49/52 +f 55/55/55 56/54/56 52/49/52 51/52/51 +f 53/56/53 55/55/55 51/52/51 49/51/49 +v -12.775614 0.306258 -5.156080 +v 2.835261 0.306258 -7.057927 +v -12.775614 0.677216 -5.156080 +v 2.835261 0.677216 -7.057927 +v -10.822945 0.306258 10.871975 +v 4.787932 0.306258 8.970127 +v -10.822945 0.677216 10.871975 +v 4.787932 0.677216 8.970127 +v -7.293286 0.307455 0.979346 +v -4.322771 0.307455 0.617452 +v -4.684664 0.307455 -2.353060 +v -7.655179 0.307455 -1.991167 +v -7.293286 0.678413 0.979346 +v -4.322771 0.678413 0.617452 +v -4.684664 0.678413 -2.353060 +v -7.655179 0.678413 -1.991167 +v 0.902461 0.678883 3.165966 +v 1.264354 0.678826 6.136479 +v -1.706159 0.678827 6.498372 +v -2.068052 0.679208 3.527859 +v -1.706159 0.307868 6.498372 +v 1.264354 0.307867 6.136479 +v 0.902461 0.307925 3.165966 +v -2.068052 0.308249 3.527859 +v -3.425020 0.679393 7.986427 +v -6.395535 0.679252 8.348320 +v -6.757427 0.679216 5.377819 +v -6.757427 0.308498 5.377819 +v -6.395535 0.308294 8.348320 +v -3.425020 0.308435 7.986427 +v -3.786912 0.680008 5.015926 +v -3.786912 0.309049 5.015926 +vn -0.262337 -0.942870 -0.205369 +vn 0.205367 -0.942875 -0.262320 +vn -0.262635 0.942746 -0.205554 +vn 0.205639 0.942742 -0.262583 +vn -0.355815 -0.816687 0.454332 +vn 0.335634 -0.904611 0.262733 +vn -0.355909 0.816255 0.455034 +vn 0.335918 0.904340 0.263302 +vn 0.262962 -0.904544 -0.335637 +vn -0.335745 -0.904605 -0.262612 +vn -0.355807 -0.816428 0.454804 +vn 0.454699 -0.816419 0.355963 +vn 0.262672 0.904546 -0.335856 +vn -0.335747 0.904495 -0.262988 +vn -0.355864 0.816560 0.454522 +vn 0.454548 0.816575 0.355798 +vn -0.355652 0.816607 0.454604 +vn -0.454443 0.816647 -0.355766 +vn 0.356082 0.816529 -0.454407 +vn 0.262653 0.942785 0.205353 +vn 0.355755 -0.816589 -0.454555 +vn -0.454803 -0.816488 -0.355671 +vn -0.356080 -0.816394 0.454651 +vn 0.262333 -0.942818 0.205610 +vn -0.335659 0.904720 -0.262326 +vn 0.355853 0.816737 -0.454212 +vn 0.335572 0.904622 0.262774 +vn 0.335908 -0.904451 0.262933 +vn 0.355947 -0.816306 -0.454914 +vn -0.335871 -0.904446 -0.262999 +vn -0.262777 0.904593 0.335648 +vn -0.262932 -0.904466 0.335869 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +g Generic +usemtl Default +f 60/57/60 58/58/58 57/59/57 59/60/59 +f 62/61/62 64/62/64 63/63/63 61/64/61 +f 64/62/64 62/61/62 58/58/58 60/57/60 +f 61/64/61 63/63/63 59/60/59 57/59/57 +f 67/65/67 71/66/71 72/67/72 68/68/68 +f 70/69/70 66/70/66 65/71/65 69/72/69 +f 66/70/66 70/69/70 71/66/71 67/65/67 +f 69/72/69 65/71/65 68/68/68 72/67/72 +f 79/73/79 73/74/73 76/75/76 80/76/80 +f 74/77/74 78/78/78 77/79/77 75/80/75 +f 78/78/78 74/77/74 73/74/73 79/73/79 +f 75/80/75 77/79/77 80/76/80 76/75/76 +f 88/81/88 87/82/87 83/83/83 84/84/84 +f 81/85/81 86/86/86 85/87/85 82/88/82 +f 86/86/86 81/85/81 87/82/87 88/81/88 +f 82/88/82 85/87/85 84/84/84 83/83/83 +f 60/57/60 73/74/73 74/77/74 64/62/64 +f 64/62/64 74/77/74 75/80/75 81/85/81 +f 64/62/64 81/85/81 82/88/82 63/63/63 +f 81/85/81 75/80/75 76/75/76 87/82/87 +f 63/63/63 82/88/82 83/83/83 59/60/59 +f 83/83/83 69/72/69 59/60/59 +f 72/67/72 59/60/59 69/72/69 +f 59/60/59 72/67/72 71/66/71 60/57/60 +f 69/72/69 83/83/83 87/82/87 70/69/70 +f 87/82/87 76/75/76 70/69/70 +f 73/74/73 60/57/60 76/75/76 +f 60/57/60 71/66/71 70/69/70 76/75/76 +f 58/58/58 67/65/67 68/68/68 57/59/57 +f 58/58/58 80/76/80 66/70/66 67/65/67 +f 79/73/79 80/76/80 58/58/58 +f 58/58/58 62/61/62 78/78/78 79/73/79 +f 86/86/86 77/79/77 78/78/78 62/61/62 +f 61/64/61 85/87/85 86/86/86 62/61/62 +f 77/79/77 86/86/86 88/81/88 80/76/80 +f 88/81/88 84/84/84 65/71/65 66/70/66 +f 88/81/88 66/70/66 80/76/80 +f 65/71/65 57/59/57 68/68/68 +f 85/87/85 61/64/61 57/59/57 84/84/84 +f 84/84/84 57/59/57 65/71/65 +v -6.931853 0.656053 -0.226530 +v -5.553494 0.656053 1.064669 +v -6.931853 1.155333 -0.226530 +v -5.553494 1.155333 1.064669 +v -7.394143 0.656053 0.266968 +v -6.015786 0.656053 1.558167 +v -7.394143 1.155333 0.266968 +v -6.015786 1.155333 1.558167 +v -6.522531 1.069533 -0.663483 +v -5.144181 1.069533 0.627710 +v -6.522531 1.568810 -0.663483 +v -5.144181 1.568810 0.627710 +v -6.984822 1.069533 -0.169992 +v -5.606471 1.069533 1.121207 +v -6.984822 1.568810 -0.169992 +v -5.606471 1.568810 1.121207 +v -6.113135 1.479670 -1.100518 +v -4.734784 1.479670 0.190680 +v -6.113135 1.978946 -1.100518 +v -4.734784 1.978946 0.190680 +v -6.575427 1.479670 -0.607021 +v -5.197075 1.479670 0.684178 +v -6.575427 1.978946 -0.607021 +v -5.197075 1.978946 0.684178 +v -5.704967 1.931456 -1.536234 +v -4.326616 1.931456 -0.245041 +v -5.704967 2.430738 -1.536234 +v -4.326616 2.430738 -0.245041 +v -6.167257 1.931456 -1.042743 +v -4.788906 1.931456 0.248456 +v -6.167257 2.430738 -1.042743 +v -4.788906 2.430738 0.248456 +v -5.291526 2.338977 -1.977585 +v -3.913175 2.338977 -0.686386 +v -5.291526 2.838253 -1.977585 +v -3.913175 2.838253 -0.686386 +v -5.753817 2.338977 -1.484087 +v -4.375465 2.338977 -0.192894 +v -5.753817 2.838253 -1.484087 +v -4.375465 2.838253 -0.192894 +v -4.879350 2.772604 -2.417581 +v -3.500998 2.772604 -1.126383 +v -4.879350 3.271881 -2.417581 +v -3.500998 3.271881 -1.126383 +v -5.341640 2.772604 -1.924084 +v -3.963290 2.772604 -0.632891 +v -5.341640 3.271881 -1.924084 +v -3.963290 3.271881 -0.632891 +v -4.481393 3.164852 -2.842401 +v -3.103040 3.164852 -1.551202 +v -4.481393 3.664129 -2.842401 +v -3.103040 3.664129 -1.551202 +v -4.943684 3.164852 -2.348904 +v -3.565333 3.164852 -1.057711 +v -4.943684 3.664129 -2.348904 +v -3.565333 3.664129 -1.057711 +vn -0.026645 -0.577351 -0.816061 +vn 0.816062 -0.577350 -0.026644 +vn -0.026645 0.577351 -0.816061 +vn 0.816062 0.577350 -0.026644 +vn -0.816062 -0.577350 0.026645 +vn 0.026644 -0.577350 0.816062 +vn -0.816062 0.577350 0.026645 +vn 0.026644 0.577350 0.816062 +vn -0.026641 -0.577349 -0.816063 +vn 0.816062 -0.577350 -0.026644 +vn -0.026641 0.577349 -0.816063 +vn 0.816062 0.577350 -0.026644 +vn -0.816061 -0.577351 0.026640 +vn 0.026644 -0.577351 0.816061 +vn -0.816061 0.577351 0.026640 +vn 0.026644 0.577351 0.816061 +vn -0.026643 -0.577351 -0.816061 +vn 0.816062 -0.577350 -0.026643 +vn -0.026643 0.577351 -0.816061 +vn 0.816062 0.577350 -0.026643 +vn -0.816062 -0.577350 0.026643 +vn 0.026643 -0.577351 0.816061 +vn -0.816062 0.577350 0.026643 +vn 0.026643 0.577351 0.816061 +vn -0.026642 -0.577349 -0.816062 +vn 0.816062 -0.577350 -0.026644 +vn -0.026642 0.577349 -0.816062 +vn 0.816062 0.577350 -0.026644 +vn -0.816062 -0.577351 0.026641 +vn 0.026644 -0.577351 0.816061 +vn -0.816062 0.577351 0.026641 +vn 0.026644 0.577351 0.816061 +vn -0.026643 -0.577351 -0.816061 +vn 0.816062 -0.577351 -0.026641 +vn -0.026643 0.577351 -0.816061 +vn 0.816062 0.577351 -0.026641 +vn -0.816062 -0.577350 0.026645 +vn 0.026642 -0.577349 0.816062 +vn -0.816062 0.577350 0.026645 +vn 0.026642 0.577349 0.816062 +vn -0.026643 -0.577351 -0.816061 +vn 0.816061 -0.577351 -0.026640 +vn -0.026643 0.577351 -0.816061 +vn 0.816061 0.577351 -0.026640 +vn -0.816062 -0.577350 0.026644 +vn 0.026641 -0.577349 0.816063 +vn -0.816062 0.577350 0.026644 +vn 0.026641 0.577349 0.816063 +vn -0.026643 -0.577351 -0.816061 +vn 0.816061 -0.577351 -0.026640 +vn -0.026643 0.577351 -0.816061 +vn 0.816061 0.577351 -0.026640 +vn -0.816062 -0.577350 0.026644 +vn 0.026640 -0.577349 0.816063 +vn -0.816062 0.577350 0.026644 +vn 0.026640 0.577349 0.816063 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +g Generic +usemtl Default +f 92/89/92 90/90/90 89/91/89 91/92/91 +f 94/93/94 96/94/96 95/95/95 93/96/93 +f 94/93/94 93/96/93 89/91/89 90/90/90 +f 96/94/96 94/93/94 90/90/90 92/89/92 +f 95/95/95 96/94/96 92/89/92 91/92/91 +f 93/96/93 95/95/95 91/92/91 89/91/89 +f 100/97/100 98/98/98 97/99/97 99/100/99 +f 102/101/102 104/102/104 103/103/103 101/104/101 +f 102/101/102 101/104/101 97/99/97 98/98/98 +f 104/102/104 102/101/102 98/98/98 100/97/100 +f 103/103/103 104/102/104 100/97/100 99/100/99 +f 101/104/101 103/103/103 99/100/99 97/99/97 +f 108/105/108 106/106/106 105/107/105 107/108/107 +f 110/109/110 112/110/112 111/111/111 109/112/109 +f 110/109/110 109/112/109 105/107/105 106/106/106 +f 112/110/112 110/109/110 106/106/106 108/105/108 +f 111/111/111 112/110/112 108/105/108 107/108/107 +f 109/112/109 111/111/111 107/108/107 105/107/105 +f 116/113/116 114/114/114 113/115/113 115/116/115 +f 118/117/118 120/118/120 119/119/119 117/120/117 +f 118/117/118 117/120/117 113/115/113 114/114/114 +f 120/118/120 118/117/118 114/114/114 116/113/116 +f 119/119/119 120/118/120 116/113/116 115/116/115 +f 117/120/117 119/119/119 115/116/115 113/115/113 +f 124/121/124 122/122/122 121/123/121 123/124/123 +f 126/125/126 128/126/128 127/127/127 125/128/125 +f 126/125/126 125/128/125 121/123/121 122/122/122 +f 128/126/128 126/125/126 122/122/122 124/121/124 +f 127/127/127 128/126/128 124/121/124 123/124/123 +f 125/128/125 127/127/127 123/124/123 121/123/121 +f 132/129/132 130/130/130 129/131/129 131/132/131 +f 134/133/134 136/134/136 135/135/135 133/136/133 +f 134/133/134 133/136/133 129/131/129 130/130/130 +f 136/134/136 134/133/134 130/130/130 132/129/132 +f 135/135/135 136/134/136 132/129/132 131/132/131 +f 133/136/133 135/135/135 131/132/131 129/131/129 +f 140/137/140 138/138/138 137/139/137 139/140/139 +f 142/141/142 144/142/144 143/143/143 141/144/141 +f 142/141/142 141/144/141 137/139/137 138/138/138 +f 144/142/144 142/141/142 138/138/138 140/137/140 +f 143/143/143 144/142/144 140/137/140 139/140/139 +f 141/144/141 143/143/143 139/140/139 137/139/137 +v -3.320525 0.543237 0.414581 +v -1.883557 0.543237 0.239517 +v -3.320525 4.628759 0.414581 +v -1.883557 4.628759 0.239517 +v -3.176692 0.543237 1.595193 +v -1.739726 0.543237 1.420130 +v -3.176692 4.628759 1.595193 +v -1.739726 4.628759 1.420130 +vn -0.642934 -0.577350 -0.503291 +vn 0.503291 -0.577350 -0.642934 +vn -0.642934 0.577350 -0.503291 +vn 0.503291 0.577350 -0.642934 +vn -0.503291 -0.577350 0.642934 +vn 0.642934 -0.577350 0.503292 +vn -0.503291 0.577350 0.642934 +vn 0.642934 0.577350 0.503292 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +g Generic +usemtl Default +f 148/145/148 146/146/146 145/147/145 147/148/147 +f 150/149/150 152/150/152 151/151/151 149/152/149 +f 150/149/150 149/152/149 145/147/145 146/146/146 +f 152/150/152 150/149/150 146/146/146 148/145/148 +f 151/151/151 152/150/152 148/145/148 147/148/147 +f 149/152/149 151/151/151 147/148/147 145/147/145 +v 0.157878 0.543237 1.852437 +v 1.035386 0.543237 1.745532 +v 0.157878 4.628759 1.852437 +v 1.035386 4.628759 1.745532 +v 0.255071 0.543237 2.650228 +v 1.132579 0.543237 2.543323 +v 0.255071 4.628759 2.650228 +v 1.132579 4.628759 2.543323 +vn -0.642934 -0.577350 -0.503291 +vn 0.503291 -0.577350 -0.642934 +vn -0.642934 0.577350 -0.503291 +vn 0.503291 0.577350 -0.642934 +vn -0.503291 -0.577350 0.642934 +vn 0.642934 -0.577350 0.503291 +vn -0.503291 0.577350 0.642934 +vn 0.642934 0.577350 0.503291 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +g Generic +usemtl Default +f 156/153/156 154/154/154 153/155/153 155/156/155 +f 158/157/158 160/158/160 159/159/159 157/160/157 +f 158/157/158 157/160/157 153/155/153 154/154/154 +f 160/158/160 158/157/158 154/154/154 156/153/156 +f 159/159/159 160/158/160 156/153/156 155/156/155 +f 157/160/157 159/159/159 155/156/155 153/155/153 +v -5.574921 0.543237 2.997040 +v -4.697420 0.543237 2.890136 +v -5.574921 4.628759 2.997040 +v -4.697420 4.628759 2.890136 +v -5.477728 0.543237 3.794831 +v -4.600226 0.543237 3.687927 +v -5.477728 4.628759 3.794831 +v -4.600226 4.628759 3.687927 +vn -0.642934 -0.577350 -0.503292 +vn 0.503291 -0.577350 -0.642934 +vn -0.642934 0.577350 -0.503292 +vn 0.503291 0.577350 -0.642934 +vn -0.503292 -0.577350 0.642934 +vn 0.642934 -0.577350 0.503291 +vn -0.503292 0.577350 0.642934 +vn 0.642934 0.577350 0.503291 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +g Generic +usemtl Default +f 164/161/164 162/162/162 161/163/161 163/164/163 +f 166/165/166 168/166/168 167/167/167 165/168/165 +f 166/165/166 165/168/165 161/163/161 162/162/162 +f 168/166/168 166/165/166 162/162/162 164/161/164 +f 167/167/167 168/166/168 164/161/164 163/164/163 +f 165/168/165 167/167/167 163/164/163 161/163/161 +v -0.152375 0.543237 -0.694201 +v 0.725133 0.543237 -0.801106 +v -0.152375 4.628759 -0.694201 +v 0.725133 4.628759 -0.801106 +v -0.055180 0.543237 0.103595 +v 0.822326 0.543237 -0.003309 +v -0.055180 4.628759 0.103595 +v 0.822326 4.628759 -0.003309 +vn -0.642934 -0.577350 -0.503291 +vn 0.503292 -0.577350 -0.642934 +vn -0.642934 0.577350 -0.503291 +vn 0.503292 0.577350 -0.642934 +vn -0.503292 -0.577350 0.642934 +vn 0.642934 -0.577350 0.503292 +vn -0.503292 0.577350 0.642934 +vn 0.642934 0.577350 0.503292 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +g Generic +usemtl Default +f 172/169/172 170/170/170 169/171/169 171/172/171 +f 174/173/174 176/174/176 175/175/175 173/176/173 +f 174/173/174 173/176/173 169/171/169 170/170/170 +f 176/174/176 174/173/174 170/170/170 172/169/172 +f 175/175/175 176/174/176 172/169/172 171/172/171 +f 173/176/173 175/175/175 171/172/171 169/171/169 +v -0.540378 0.543237 -3.879036 +v 0.337129 0.543237 -3.985941 +v -0.540378 4.628759 -3.879036 +v 0.337129 4.628759 -3.985941 +v -0.443184 0.543237 -3.081246 +v 0.434323 0.543237 -3.188151 +v -0.443184 4.628759 -3.081246 +v 0.434323 4.628759 -3.188151 +vn -0.642934 -0.577350 -0.503292 +vn 0.503291 -0.577350 -0.642934 +vn -0.642934 0.577350 -0.503292 +vn 0.503291 0.577350 -0.642934 +vn -0.503292 -0.577350 0.642934 +vn 0.642934 -0.577350 0.503291 +vn -0.503292 0.577350 0.642934 +vn 0.642934 0.577350 0.503291 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +g Generic +usemtl Default +f 180/177/180 178/178/178 177/179/177 179/180/179 +f 182/181/182 184/182/184 183/183/183 181/184/181 +f 182/181/182 181/184/181 177/179/177 178/178/178 +f 184/182/184 182/181/182 178/178/178 180/177/180 +f 183/183/183 184/182/184 180/177/180 179/180/179 +f 181/184/181 183/183/183 179/180/179 177/179/177 +v 2.307724 0.543237 -0.257528 +v 3.185230 0.543237 -0.364433 +v 2.307724 4.628759 -0.257528 +v 3.185230 4.628759 -0.364433 +v 2.404918 0.543237 0.540265 +v 3.282423 0.543237 0.433360 +v 2.404918 4.628759 0.540265 +v 3.282423 4.628759 0.433360 +vn -0.642934 -0.577350 -0.503291 +vn 0.503291 -0.577350 -0.642934 +vn -0.642934 0.577350 -0.503291 +vn 0.503291 0.577350 -0.642934 +vn -0.503291 -0.577350 0.642934 +vn 0.642934 -0.577350 0.503291 +vn -0.503291 0.577350 0.642934 +vn 0.642934 0.577350 0.503291 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +g Generic +usemtl Default +f 188/185/188 186/186/186 185/187/185 187/188/187 +f 190/189/190 192/190/192 191/191/191 189/192/189 +f 190/189/190 189/192/189 185/187/185 186/186/186 +f 192/190/192 190/189/190 186/186/186 188/185/188 +f 191/191/191 192/190/192 188/185/188 187/188/187 +f 189/192/189 191/191/191 187/188/187 185/187/185 +v 1.694893 0.543237 -3.351745 +v 2.572400 0.543237 -3.458649 +v 1.694893 4.628759 -3.351745 +v 2.572400 4.628759 -3.458649 +v 1.792087 0.543237 -2.553948 +v 2.669595 0.543237 -2.660853 +v 1.792087 4.628759 -2.553948 +v 2.669595 4.628759 -2.660853 +vn -0.642934 -0.577350 -0.503292 +vn 0.503292 -0.577350 -0.642934 +vn -0.642934 0.577350 -0.503292 +vn 0.503292 0.577350 -0.642934 +vn -0.503292 -0.577350 0.642934 +vn 0.642934 -0.577350 0.503291 +vn -0.503292 0.577350 0.642934 +vn 0.642934 0.577350 0.503291 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +g Generic +usemtl Default +f 196/193/196 194/194/194 193/195/193 195/196/195 +f 198/197/198 200/198/200 199/199/199 197/200/197 +f 198/197/198 197/200/197 193/195/193 194/194/194 +f 200/198/200 198/197/198 194/194/194 196/193/196 +f 199/199/199 200/198/200 196/193/196 195/196/195 +f 197/200/197 199/199/199 195/196/195 193/195/193 +v -9.504584 0.619314 -4.221548 +v -8.067597 0.619314 -4.396614 +v -9.504584 1.725753 -4.221548 +v -8.067597 1.725753 -4.396614 +v -9.360755 0.619314 -3.040947 +v -7.923768 0.619314 -3.216013 +v -9.360755 1.725753 -3.040947 +v -7.923768 1.725753 -3.216013 +vn -0.642934 -0.577350 -0.503292 +vn 0.503292 -0.577350 -0.642934 +vn -0.642934 0.577350 -0.503292 +vn 0.503292 0.577350 -0.642934 +vn -0.503292 -0.577350 0.642934 +vn 0.642934 -0.577350 0.503292 +vn -0.503292 0.577350 0.642934 +vn 0.642934 0.577350 0.503292 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +g Generic +usemtl Default +f 204/201/204 202/202/202 201/203/201 203/204/203 +f 206/205/206 208/206/208 207/207/207 205/208/205 +f 206/205/206 205/208/205 201/203/201 202/202/202 +f 208/206/208 206/205/206 202/202/202 204/201/204 +f 207/207/207 208/206/208 204/201/204 203/204/203 +f 205/208/205 207/207/207 203/204/203 201/203/201 +v -8.399732 0.619314 -3.150882 +v -7.163228 0.619314 -2.398156 +v -8.399732 1.725753 -3.150882 +v -7.163228 1.725753 -2.398156 +v -9.018167 0.619314 -2.134972 +v -7.781669 0.619314 -1.382251 +v -9.018167 1.725753 -2.134972 +v -7.781669 1.725753 -1.382251 +vn -0.192948 -0.577351 -0.793371 +vn 0.793371 -0.577351 -0.192945 +vn -0.192948 0.577351 -0.793371 +vn 0.793371 0.577351 -0.192945 +vn -0.793371 -0.577350 0.192948 +vn 0.192946 -0.577349 0.793372 +vn -0.793371 0.577350 0.192948 +vn 0.192946 0.577349 0.793372 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +g Generic +usemtl Default +f 212/209/212 210/210/210 209/211/209 211/212/211 +f 214/213/214 216/214/216 215/215/215 213/216/213 +f 214/213/214 213/216/213 209/211/209 210/210/210 +f 216/214/216 214/213/214 210/210/210 212/209/212 +f 215/215/215 216/214/216 212/209/212 211/212/211 +f 213/216/213 215/215/215 211/212/211 209/211/209 +v -8.941129 1.714142 -3.533442 +v -7.601893 1.714142 -4.082976 +v -8.941129 2.820580 -3.533442 +v -7.601893 2.820580 -4.082976 +v -8.489663 1.714142 -2.433123 +v -7.150416 1.714142 -2.982626 +v -8.489663 2.820580 -2.433123 +v -7.150416 2.820580 -2.982626 +vn -0.753303 -0.577345 -0.314970 +vn 0.314968 -0.577355 -0.753297 +vn -0.753303 0.577345 -0.314970 +vn 0.314968 0.577355 -0.753297 +vn -0.314977 -0.577351 0.753296 +vn 0.753297 -0.577349 0.314978 +vn -0.314977 0.577351 0.753296 +vn 0.753297 0.577349 0.314978 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +g Generic +usemtl Default +f 220/217/220 218/218/218 217/219/217 219/220/219 +f 222/221/222 224/222/224 223/223/223 221/224/221 +f 222/221/222 221/224/221 217/219/217 218/218/218 +f 224/222/224 222/221/222 218/218/218 220/217/220 +f 223/223/223 224/222/224 220/217/220 219/220/219 +f 221/224/221 223/223/223 219/220/219 217/219/217 +v -1.593735 0.619314 -5.185313 +v -0.156767 0.619314 -5.360376 +v -1.593735 1.725753 -5.185313 +v -0.156767 1.725753 -5.360376 +v -1.449904 0.619314 -4.004714 +v -0.012937 0.619314 -4.179778 +v -1.449904 1.725753 -4.004714 +v -0.012937 1.725753 -4.179778 +vn -0.642934 -0.577350 -0.503292 +vn 0.503292 -0.577350 -0.642934 +vn -0.642934 0.577350 -0.503292 +vn 0.503292 0.577350 -0.642934 +vn -0.503291 -0.577350 0.642934 +vn 0.642934 -0.577350 0.503291 +vn -0.503291 0.577350 0.642934 +vn 0.642934 0.577350 0.503291 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +g Generic +usemtl Default +f 228/225/228 226/226/226 225/227/225 227/228/227 +f 230/229/230 232/230/232 231/231/231 229/232/229 +f 230/229/230 229/232/229 225/227/225 226/226/226 +f 232/230/232 230/229/230 226/226/226 228/225/228 +f 231/231/231 232/230/232 228/225/228 227/228/227 +f 229/232/229 231/231/231 227/228/227 225/227/225 +v 2.004582 0.619314 -4.082850 +v 3.441532 0.619314 -4.257911 +v 2.004582 1.725753 -4.082850 +v 3.441532 1.725753 -4.257911 +v 2.148414 0.619314 -2.902237 +v 3.585365 0.619314 -3.077299 +v 2.148414 1.725753 -2.902237 +v 3.585365 1.725753 -3.077299 +vn -0.642934 -0.577350 -0.503292 +vn 0.503291 -0.577350 -0.642935 +vn -0.642934 0.577350 -0.503292 +vn 0.503291 0.577350 -0.642935 +vn -0.503291 -0.577350 0.642934 +vn 0.642934 -0.577350 0.503291 +vn -0.503291 0.577350 0.642934 +vn 0.642934 0.577350 0.503291 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +g Generic +usemtl Default +f 236/233/236 234/234/234 233/235/233 235/236/235 +f 238/237/238 240/238/240 239/239/239 237/240/237 +f 238/237/238 237/240/237 233/235/233 234/234/234 +f 240/238/240 238/237/238 234/234/234 236/233/236 +f 239/239/239 240/238/240 236/233/236 235/236/235 +f 237/240/237 239/239/239 235/236/235 233/235/233 +v -4.083880 3.659909 -2.871198 +v 1.794116 3.659909 -3.587305 +v -4.083880 3.991883 -2.871198 +v 1.794116 3.991883 -3.587305 +v -3.836304 3.659909 -0.839023 +v 2.041692 3.659909 -1.555130 +v -3.836304 3.991883 -0.839023 +v 2.041692 3.991883 -1.555130 +vn -0.642934 -0.577350 -0.503291 +vn 0.503291 -0.577350 -0.642934 +vn -0.642934 0.577350 -0.503291 +vn 0.503291 0.577350 -0.642934 +vn -0.503292 -0.577350 0.642934 +vn 0.642934 -0.577350 0.503291 +vn -0.503292 0.577350 0.642934 +vn 0.642934 0.577350 0.503291 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +g Generic +usemtl Default +f 244/241/244 242/242/242 241/243/241 243/244/243 +f 246/245/246 248/246/248 247/247/247 245/248/245 +f 246/245/246 245/248/245 241/243/241 242/242/242 +f 248/246/248 246/245/246 242/242/242 244/241/244 +f 247/247/247 248/246/248 244/241/244 243/244/243 +f 245/248/245 247/247/247 243/244/243 241/243/241 +v -9.850474 3.564384 1.529655 +v -3.972494 3.564384 0.813551 +v -9.850474 3.896351 1.529655 +v -3.972494 3.896351 0.813551 +v -9.602899 3.564384 3.561831 +v -3.724918 3.564384 2.845726 +v -9.602899 3.896351 3.561831 +v -3.724918 3.896351 2.845726 +vn -0.642934 -0.577350 -0.503292 +vn 0.503292 -0.577350 -0.642934 +vn -0.642934 0.577350 -0.503292 +vn 0.503292 0.577350 -0.642934 +vn -0.503292 -0.577350 0.642934 +vn 0.642934 -0.577350 0.503292 +vn -0.503292 0.577350 0.642934 +vn 0.642934 0.577350 0.503292 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +g Generic +usemtl Default +f 252/249/252 250/250/250 249/251/249 251/252/251 +f 254/253/254 256/254/256 255/255/255 253/256/253 +f 254/253/254 253/256/253 249/251/249 250/250/250 +f 256/254/256 254/253/254 250/250/250 252/249/252 +f 255/255/255 256/254/256 252/249/252 251/252/251 +f 253/256/253 255/255/255 251/252/251 249/251/249 +v -3.299606 5.817862 3.498628 +v -1.762773 3.656707 -1.795818 +v -3.265831 6.126930 3.382274 +v -1.728992 3.965776 -1.912173 +v -1.333564 5.817862 4.069325 +v 0.203276 3.656707 -1.225129 +v -1.299784 6.126930 3.952964 +v 0.237049 3.965776 -1.341489 +vn -0.763055 -0.326792 0.557632 +vn -0.463362 -0.748237 -0.474802 +vn -0.645556 0.748251 0.152898 +vn -0.345879 0.326812 -0.879524 +vn 0.345884 -0.326797 0.879527 +vn 0.645565 -0.748241 -0.152908 +vn 0.463358 0.748248 0.474789 +vn 0.763048 0.326807 -0.557634 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +g Generic +usemtl Default +f 260/257/260 258/258/258 257/259/257 259/260/259 +f 262/261/262 264/262/264 263/263/263 261/264/261 +f 262/261/262 261/264/261 257/259/257 258/258/258 +f 264/262/264 262/261/262 258/258/258 260/257/260 +f 263/263/263 264/262/264 260/257/260 259/260/259 +f 261/264/261 263/263/263 259/260/259 257/259/257 +v -4.417480 3.133695 0.716653 +v -2.980511 3.133695 0.541589 +v -4.417480 4.240132 0.716653 +v -2.980511 4.240132 0.541589 +v -4.273647 3.133695 1.897267 +v -2.836678 3.133695 1.722203 +v -4.273647 4.240132 1.897267 +v -2.836678 4.240132 1.722203 +vn -0.642934 -0.577350 -0.503291 +vn 0.503291 -0.577350 -0.642935 +vn -0.642934 0.577350 -0.503291 +vn 0.503291 0.577350 -0.642935 +vn -0.503291 -0.577350 0.642934 +vn 0.642934 -0.577350 0.503291 +vn -0.503291 0.577350 0.642934 +vn 0.642934 0.577350 0.503291 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +g Generic +usemtl Default +f 268/265/268 266/266/266 265/267/265 267/268/267 +f 270/269/270 272/270/272 271/271/271 269/272/269 +f 270/269/270 269/272/269 265/267/265 266/266/266 +f 272/270/272 270/269/270 266/266/266 268/265/268 +f 271/271/271 272/270/272 268/265/268 267/268/267 +f 269/272/269 271/271/271 267/268/267 265/267/265 +v -4.336576 0.619314 4.570268 +v -3.449573 0.619314 3.426261 +v -4.336576 1.725753 4.570268 +v -3.449573 1.725753 3.426261 +v -3.396662 0.619314 5.299027 +v -2.509660 0.619314 4.155022 +v -3.396662 1.725753 5.299027 +v -2.509660 1.725753 4.155022 +vn -0.810037 -0.577350 0.102502 +vn -0.102502 -0.577350 -0.810037 +vn -0.810037 0.577350 0.102502 +vn -0.102502 0.577350 -0.810037 +vn 0.102502 -0.577350 0.810037 +vn 0.810037 -0.577350 -0.102502 +vn 0.102502 0.577350 0.810037 +vn 0.810037 0.577350 -0.102502 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +g Generic +usemtl Default +f 276/273/276 274/274/274 273/275/273 275/276/275 +f 278/277/278 280/278/280 279/279/279 277/280/277 +f 278/277/278 277/280/277 273/275/273 274/274/274 +f 280/278/280 278/277/278 274/274/274 276/273/276 +f 279/279/279 280/278/280 276/273/276 275/276/275 +f 277/280/277 279/279/279 275/276/275 273/275/273 +v 1.869175 2.074568 8.179482 +v 1.869175 2.812895 8.179482 +v 2.018039 2.074568 8.959009 +v 2.018039 2.812895 8.959009 +v 2.668088 2.075765 8.028793 +v 2.669106 2.814092 8.028549 +v 2.796757 2.814092 8.812168 +v 2.795720 2.075765 8.812414 +v 2.620193 1.033927 7.743900 +v 4.048961 1.032154 7.511232 +v 2.605152 5.117556 7.620445 +v 4.033914 5.115785 7.387717 +v 2.811342 1.070114 8.917229 +v 4.240109 1.068340 8.684560 +v 2.796301 5.153741 8.793774 +v 4.225061 5.151969 8.561047 +v 2.609618 4.143578 7.651554 +v 2.608366 4.484301 7.641277 +v 2.800346 4.294529 8.821438 +v 2.801599 3.953813 8.831716 +v 3.912072 4.320283 7.433913 +v 3.867723 4.655152 7.430815 +v 4.059699 4.465383 8.610977 +v 4.104046 4.130512 8.614075 +v -0.821884 1.038199 8.304567 +v 0.606876 1.036426 8.071840 +v -0.836932 5.121830 8.181052 +v 0.591829 5.120058 7.948326 +v -0.630749 1.074386 9.477837 +v 0.798024 1.072612 9.245169 +v -0.645790 5.158014 9.354383 +v 0.782978 5.156243 9.121655 +v 0.596055 4.211296 7.977474 +v 0.597315 3.870573 7.987812 +v 0.789296 3.680803 9.167973 +v 0.788036 4.021526 9.157635 +v -0.755239 4.027966 8.203297 +v -0.710885 3.693096 8.206394 +v -0.518910 3.503325 9.386557 +v -0.563259 3.838201 9.383458 +v -1.153111 4.130231 5.487880 +v 3.488600 4.738882 4.830677 +v -1.187541 4.455303 5.545753 +v 3.454168 5.063955 4.888550 +v -0.376845 3.362888 10.259881 +v 4.264865 3.971540 9.602686 +v -0.411271 3.687961 10.317795 +v 4.230440 4.296612 9.660601 +vn -0.475890 -0.817216 -0.325095 +vn -0.779428 0.333535 -0.530326 +vn -0.530453 -0.333078 0.779537 +vn -0.326265 0.815780 0.477549 +vn -0.881205 -0.263496 -0.392488 +vn -0.836189 0.532140 -0.132721 +vn -0.699810 0.271172 0.660857 +vn -0.746006 -0.536909 0.393959 +vn -0.794482 -0.498397 -0.346986 +vn 0.549217 -0.355231 -0.756420 +vn -0.873625 0.395087 -0.284050 +vn 0.069020 0.408415 -0.910183 +vn -0.704407 -0.250041 0.664297 +vn 0.442467 -0.645599 0.622434 +vn -0.340172 0.829225 0.443474 +vn 0.573407 0.244828 0.781833 +vn -0.453585 -0.859151 -0.236898 +vn -0.348075 0.920168 -0.179261 +vn -0.430451 0.658380 0.617453 +vn -0.516110 -0.840104 0.166902 +vn 0.299159 -0.179810 -0.937109 +vn 0.242471 0.459734 -0.854314 +vn 0.460561 0.447686 0.766460 +vn 0.610676 -0.134239 0.780420 +vn -0.384489 -0.559208 -0.734475 +vn 0.552105 -0.354031 -0.754879 +vn -0.762893 0.314820 -0.564697 +vn 0.219616 0.645091 -0.731865 +vn -0.403307 -0.241482 0.882627 +vn 0.438453 -0.646794 0.624033 +vn -0.231239 0.685571 0.690305 +vn 0.763518 0.353862 0.540205 +vn 0.446803 0.863638 -0.233447 +vn 0.318096 -0.828588 -0.460714 +vn 0.642880 -0.765057 0.037325 +vn 0.170593 0.919303 0.354654 +vn -0.603267 0.134156 -0.786175 +vn -0.310041 -0.540715 -0.781986 +vn -0.086818 -0.585451 0.806046 +vn -0.301012 0.353213 0.885795 +vn -0.598562 -0.549183 -0.583200 +vn 0.420800 -0.682965 -0.597064 +vn -0.437678 0.894171 -0.094316 +vn 0.415457 0.730182 -0.542430 +vn -0.694607 -0.568916 0.440291 +vn 0.373992 -0.895053 0.242921 +vn -0.313723 0.219120 0.923886 +vn 0.372614 0.876706 0.304213 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +g Generic +usemtl Default +f 283/281/283 284/282/284 282/283/282 +f 283/281/283 282/283/282 281/284/281 +f 285/285/285 281/284/281 282/283/282 +f 285/285/285 282/283/282 286/286/286 +f 288/287/288 283/281/283 281/284/281 +f 288/287/288 281/284/281 285/285/285 +f 284/282/284 287/288/287 286/286/286 +f 284/282/284 286/286/286 282/283/282 +f 294/289/294 293/290/293 289/291/289 +f 294/289/294 289/291/289 290/292/290 +f 296/293/296 294/289/294 290/292/290 +f 296/293/296 290/292/290 292/294/292 +f 295/295/295 296/293/296 292/294/292 +f 295/295/295 292/294/292 291/296/291 +f 301/297/301 302/298/302 292/294/292 +f 301/297/301 292/294/292 290/292/290 +f 301/297/301 290/292/290 289/291/289 +f 301/297/301 289/291/289 297/299/297 +f 298/300/298 291/296/291 292/294/292 +f 298/300/298 292/294/292 302/298/302 +f 296/293/296 295/295/295 299/301/299 +f 296/293/296 299/301/299 303/302/303 +f 296/293/296 303/302/303 304/303/304 +f 296/293/296 304/303/304 294/289/294 +f 304/303/304 300/304/300 293/290/293 +f 304/303/304 293/290/293 294/289/294 +f 299/301/299 295/295/295 291/296/291 +f 299/301/299 291/296/291 298/300/298 +f 310/305/310 309/306/309 305/307/305 +f 310/305/310 305/307/305 306/308/306 +f 311/309/311 312/310/312 308/311/308 +f 311/309/311 308/311/308 307/312/307 +f 309/306/309 311/309/311 307/312/307 +f 309/306/309 307/312/307 305/307/305 +f 314/313/314 306/308/306 318/314/318 +f 318/314/318 306/308/306 305/307/305 +f 318/314/318 305/307/305 317/315/317 +f 317/315/317 305/307/305 307/312/307 +f 317/315/317 307/312/307 308/311/308 +f 317/315/317 308/311/308 313/316/313 +f 320/317/320 316/318/316 312/310/312 +f 320/317/320 312/310/312 311/309/311 +f 320/317/320 311/309/311 309/306/309 +f 320/317/320 309/306/309 319/319/319 +f 319/319/319 309/306/309 310/305/310 +f 319/319/319 310/305/310 315/320/315 +f 312/310/312 316/318/316 313/316/313 +f 312/310/312 313/316/313 308/311/308 +f 315/320/315 310/305/310 306/308/306 +f 315/320/315 306/308/306 314/313/314 +f 324/321/324 322/322/322 321/323/321 +f 324/321/324 321/323/321 323/324/323 +f 326/325/326 328/326/328 327/327/327 +f 326/325/326 327/327/327 325/328/325 +f 325/328/325 327/327/327 320/317/320 +f 325/328/325 320/317/320 319/319/319 +f 317/315/317 323/324/323 321/323/321 +f 317/315/317 321/323/321 318/314/318 +f 328/326/328 326/325/326 304/303/304 +f 328/326/328 304/303/304 303/302/303 +f 301/297/301 322/322/322 324/321/324 +f 301/297/301 324/321/324 302/298/302 +f 320/317/320 327/327/327 328/326/328 +f 320/317/320 328/326/328 316/318/316 +f 328/326/328 303/302/303 299/301/299 +f 328/326/328 299/301/299 316/318/316 +f 298/300/298 313/316/313 316/318/316 +f 298/300/298 316/318/316 299/301/299 +f 324/321/324 323/324/323 298/300/298 +f 324/321/324 298/300/298 302/298/302 +f 313/316/313 298/300/298 323/324/323 +f 313/316/313 323/324/323 317/315/317 +f 297/299/297 314/313/314 318/314/318 +f 297/299/297 318/314/318 321/323/321 +f 297/299/297 321/323/321 322/322/322 +f 297/299/297 322/322/322 301/297/301 +f 314/313/314 297/299/297 300/304/300 +f 314/313/314 300/304/300 315/320/315 +f 326/325/326 325/328/325 319/319/319 +f 326/325/326 319/319/319 315/320/315 +f 326/325/326 315/320/315 300/304/300 +f 326/325/326 300/304/300 304/303/304 +f 288/287/288 293/290/293 300/304/300 +f 288/287/288 300/304/300 287/288/287 +f 293/290/293 288/287/288 285/285/285 +f 293/290/293 285/285/285 289/291/289 +f 289/291/289 285/285/285 286/286/286 +f 289/291/289 286/286/286 297/299/297 +f 297/299/297 286/286/286 287/288/287 +f 297/299/297 287/288/287 300/304/300 +f 283/281/283 288/287/288 287/288/287 +f 283/281/283 287/288/287 284/282/284 +v -1.071893 5.255306 4.383661 +v -1.071893 6.180741 4.383661 +v -1.180408 5.255306 5.150532 +v -1.180408 6.180741 5.150532 +v -1.657819 5.255306 5.760402 +v -1.657819 6.180741 5.760402 +v -2.376205 5.255306 6.049857 +v -2.376205 6.180741 6.049857 +v -3.143074 5.255306 5.941348 +v -3.143074 6.180741 5.941348 +v -3.752949 5.255306 5.463940 +v -3.752949 6.180741 5.463940 +v -4.042406 5.255306 4.745554 +v -4.042406 6.180741 4.745554 +v -3.933896 5.255306 3.978684 +v -3.933896 6.180741 3.978684 +v -3.456483 5.255306 3.368807 +v -3.456483 6.180741 3.368807 +v -2.738097 5.255306 3.079351 +v -2.738097 6.180741 3.079351 +v -1.971228 5.255306 3.187861 +v -1.971228 6.180741 3.187861 +v -1.361355 5.255306 3.665275 +v -1.361355 6.180741 3.665275 +vn 0.881556 -0.459701 -0.107399 +vn 0.881556 0.459701 -0.107399 +vn 0.817148 -0.459701 0.347770 +vn 0.817148 0.459701 0.347770 +vn 0.533785 -0.459701 0.709752 +vn 0.533785 0.459701 0.709752 +vn 0.107399 -0.459700 0.881556 +vn 0.107399 0.459700 0.881556 +vn -0.347764 -0.459701 0.817151 +vn -0.347764 0.459701 0.817151 +vn -0.709750 -0.459701 0.533788 +vn -0.709750 0.459701 0.533788 +vn -0.881556 -0.459700 0.107399 +vn -0.881556 0.459700 0.107399 +vn -0.817150 -0.459701 -0.347766 +vn -0.817150 0.459701 -0.347766 +vn -0.533786 -0.459701 -0.709751 +vn -0.533786 0.459701 -0.709751 +vn -0.107398 -0.459700 -0.881556 +vn -0.107398 0.459700 -0.881556 +vn 0.347768 -0.459701 -0.817149 +vn 0.347768 0.459701 -0.817149 +vn 0.709751 -0.459701 -0.533787 +vn 0.709751 0.459701 -0.533787 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +g Generic +usemtl Default +f 331/329/331 329/330/329 330/331/330 332/332/332 +f 333/333/333 331/329/331 332/332/332 334/334/334 +f 335/335/335 333/333/333 334/334/334 336/336/336 +f 337/337/337 335/335/335 336/336/336 338/338/338 +f 339/339/339 337/337/337 338/338/338 340/340/340 +f 341/341/341 339/339/339 340/340/340 342/342/342 +f 343/343/343 341/341/341 342/342/342 344/344/344 +f 345/345/345 343/343/343 344/344/344 346/346/346 +f 347/347/347 345/345/345 346/346/346 348/348/348 +f 349/349/349 347/347/347 348/348/348 350/350/350 +f 351/351/351 349/349/349 350/350/350 352/352/352 +f 329/330/329 351/351/351 352/352/352 330/331/330 +f 352/352/352 350/350/350 348/348/348 346/346/346 344/344/344 342/342/342 340/340/340 338/338/338 336/336/336 334/334/334 332/332/332 330/331/330 +f 329/330/329 331/329/331 333/333/333 335/335/335 337/337/337 339/339/339 341/341/341 343/343/343 345/345/345 347/347/347 349/349/349 351/351/351 +v 6.535600 0.352674 -0.819244 +v 7.485196 0.387642 -0.934932 +v 8.434794 0.490333 -1.050620 +v 9.384391 0.616432 -1.166308 +v 10.333988 0.668722 -1.281996 +v 11.283586 0.616432 -1.397683 +v 12.233180 0.461595 -1.513371 +v 13.182778 0.250151 -1.629059 +v 14.132375 0.039294 -1.744747 +v 15.081969 -0.073016 -1.860435 +v 16.031569 -0.103597 -1.976123 +v 6.535600 0.656240 -0.819244 +v 7.485196 0.692934 -0.934932 +v 8.434794 0.797731 -1.050620 +v 9.384391 0.925565 -1.166308 +v 10.333988 0.978385 -1.281996 +v 11.283586 0.925565 -1.397683 +v 12.233180 0.768992 -1.513371 +v 13.182778 0.552792 -1.629059 +v 14.132375 0.340157 -1.744747 +v 15.081969 0.227165 -1.860435 +v 16.031569 0.197357 -1.976123 +v 6.651287 0.369316 0.130352 +v 7.600883 0.490333 0.014665 +v 8.550483 0.732014 -0.101024 +v 9.500078 0.981286 -0.216711 +v 10.449676 1.073040 -0.332399 +v 11.399274 0.981286 -0.448087 +v 12.348868 0.710202 -0.563775 +v 13.298465 0.375586 -0.679463 +v 14.248063 0.095850 -0.795150 +v 15.197658 -0.037722 -0.910839 +v 16.147255 -0.073016 -1.026526 +v 16.147255 0.227165 -1.026526 +v 15.197658 0.262314 -0.910839 +v 14.248063 0.396454 -0.795150 +v 13.298465 0.682983 -0.679463 +v 12.348868 1.027865 -0.563775 +v 11.399274 1.291669 -0.448087 +v 10.449676 1.382822 -0.332399 +v 9.500078 1.291669 -0.216711 +v 8.550483 1.048776 -0.101024 +v 7.600883 0.797731 0.014665 +v 6.651287 0.672884 0.130352 +v 6.766975 0.400746 1.079949 +v 7.716572 0.616432 0.964261 +v 8.666170 0.981286 0.848573 +v 9.615767 1.250078 0.732886 +v 10.565364 1.331208 0.617198 +v 11.514960 1.250078 0.501510 +v 12.464557 0.974047 0.385822 +v 13.414153 0.554879 0.270134 +v 14.363751 0.232864 0.154446 +v 15.313346 0.079210 0.038759 +v 16.262943 0.039294 -0.076929 +v 16.262943 0.340157 -0.076929 +v 15.313346 0.379810 0.038759 +v 14.363751 0.535615 0.154446 +v 13.414153 0.865679 0.270134 +v 12.464557 1.285819 0.385822 +v 11.514960 1.561758 0.501510 +v 10.565364 1.642555 0.617198 +v 9.615767 1.561758 0.732886 +v 8.666170 1.291669 0.848573 +v 7.716572 0.925565 0.964261 +v 6.766975 0.706379 1.079949 +v 6.882663 0.418730 2.029546 +v 7.832259 0.668722 1.913858 +v 8.781858 1.073040 1.798170 +v 9.731454 1.331208 1.682482 +v 10.681052 1.401506 1.566794 +v 11.630649 1.331208 1.451106 +v 12.580243 1.073040 1.335418 +v 13.529841 0.652759 1.219731 +v 14.479438 0.357181 1.104043 +v 15.429034 0.237933 0.988355 +v 16.378632 0.215185 0.872667 +v 16.378632 0.516098 0.872667 +v 15.429034 0.539077 0.988355 +v 14.479438 0.663227 1.104043 +v 13.529841 0.963173 1.219731 +v 12.580243 1.382822 1.335418 +v 11.630649 1.642555 1.451106 +v 10.681052 1.711075 1.566794 +v 9.731454 1.642555 1.682482 +v 8.781858 1.382822 1.798170 +v 7.832259 0.978385 1.913858 +v 6.882663 0.724780 2.029546 +v 6.998351 0.400746 2.979142 +v 7.947948 0.616432 2.863455 +v 8.897546 0.981286 2.747767 +v 9.847143 1.250078 2.632079 +v 10.796739 1.331208 2.516391 +v 11.746337 1.250078 2.400703 +v 12.695931 0.981286 2.285016 +v 13.645530 0.616432 2.169327 +v 14.595126 0.393507 2.053640 +v 15.544721 0.331762 1.937952 +v 16.494320 0.323938 1.822264 +v 16.494320 0.626424 1.822264 +v 15.544721 0.634429 1.937952 +v 14.595126 0.699139 2.053640 +v 13.645530 0.925565 2.169327 +v 12.695931 1.291669 2.285016 +v 11.746337 1.561758 2.400703 +v 10.796739 1.642555 2.516391 +v 9.847143 1.561758 2.632079 +v 8.897546 1.291669 2.747767 +v 7.947948 0.925565 2.863455 +v 6.998351 0.706379 2.979142 +v 7.114038 0.369316 3.928739 +v 8.063635 0.490333 3.813051 +v 9.013234 0.732014 3.697363 +v 9.962830 0.981286 3.581676 +v 10.912427 1.073040 3.465988 +v 11.862025 0.981286 3.350300 +v 12.811620 0.732014 3.234612 +v 13.761216 0.490333 3.118924 +v 14.710814 0.369316 3.003236 +v 15.660409 0.352674 2.887548 +v 16.610008 0.352674 2.771861 +v 16.610008 0.656240 2.771861 +v 15.660409 0.656240 2.887548 +v 14.710814 0.672884 3.003236 +v 13.761216 0.797731 3.118924 +v 12.811620 1.048776 3.234612 +v 11.862025 1.291669 3.350300 +v 10.912427 1.382822 3.465988 +v 9.962830 1.291669 3.581676 +v 9.013234 1.048776 3.697363 +v 8.063635 0.797731 3.813051 +v 7.114038 0.672884 3.928739 +v 7.229727 0.352674 4.878336 +v 8.179323 0.388949 4.762649 +v 9.128922 0.490333 4.646960 +v 10.078518 0.616432 4.531273 +v 11.028115 0.668722 4.415585 +v 11.977712 0.616432 4.299897 +v 12.927308 0.490333 4.184209 +v 13.876904 0.387642 4.068521 +v 14.826503 0.371040 3.952834 +v 15.776097 0.405728 3.837146 +v 16.725695 0.425575 3.721458 +v 16.725695 0.731882 3.721458 +v 15.776097 0.711574 3.837146 +v 14.826503 0.674609 3.952834 +v 13.876904 0.692934 4.068521 +v 12.927308 0.797731 4.184209 +v 11.977712 0.925565 4.299897 +v 11.028115 0.978385 4.415585 +v 10.078518 0.925565 4.531273 +v 9.128922 0.797731 4.646960 +v 8.179323 0.694827 4.762649 +v 7.229727 0.656240 4.878336 +v 7.345414 0.466570 5.827933 +v 8.295011 0.547578 5.712246 +v 9.244610 0.483213 5.596557 +v 10.194206 0.412086 5.480869 +v 11.143804 0.418730 5.365181 +v 12.093400 0.400746 5.249494 +v 13.042994 0.369316 5.133805 +v 13.992593 0.391265 5.018118 +v 14.942189 0.504597 4.902431 +v 15.891786 0.643761 4.786742 +v 16.841383 0.701469 4.671055 +v 16.841383 1.011765 4.671055 +v 15.891786 0.953471 4.786742 +v 14.942189 0.812392 4.902431 +v 13.992593 0.696736 5.018118 +v 13.042994 0.672884 5.133805 +v 12.093400 0.706379 5.249494 +v 11.143804 0.724780 5.365181 +v 10.194206 0.717206 5.480869 +v 9.244610 0.793107 5.596557 +v 8.295011 0.859762 5.712246 +v 7.345414 0.776464 5.827933 +v 7.461102 0.836555 6.777530 +v 8.410699 1.023553 6.661842 +v 9.360297 0.836555 6.546154 +v 10.309895 0.466570 6.430467 +v 11.259490 0.352674 6.314778 +v 12.209088 0.352674 6.199091 +v 13.158683 0.371040 6.083403 +v 14.108281 0.504597 5.967715 +v 15.057878 0.771319 5.852027 +v 16.007473 1.046421 5.736340 +v 16.957071 1.147682 5.620651 +v 16.957071 1.458108 5.620651 +v 16.007473 1.357511 5.736340 +v 15.057878 1.089450 5.852027 +v 14.108281 0.812392 5.967715 +v 13.158683 0.674609 6.083403 +v 12.209088 0.656240 6.199091 +v 11.259490 0.656240 6.314778 +v 10.309895 0.776464 6.430467 +v 9.360297 1.156783 6.546154 +v 8.410699 1.346700 6.661842 +v 7.461102 1.156783 6.777530 +v 7.576790 1.023553 7.727126 +v 8.526386 1.188442 7.611438 +v 9.475986 1.023553 7.495750 +v 10.425582 0.547578 7.380062 +v 11.375178 0.353981 7.264375 +v 12.324777 0.352674 7.148686 +v 13.274371 0.405728 7.032999 +v 14.223968 0.643761 6.917311 +v 15.173566 1.046421 6.801623 +v 16.123161 1.343064 6.685935 +v 17.072758 1.432600 6.570248 +v 17.072758 1.744754 6.570248 +v 16.123161 1.655584 6.685935 +v 15.173566 1.357511 6.801623 +v 14.223968 0.953471 6.917311 +v 13.274371 0.711574 7.032999 +v 12.324777 0.656240 7.148686 +v 11.375178 0.658133 7.264375 +v 10.425582 0.859762 7.380062 +v 9.475986 1.346700 7.495750 +v 8.526386 1.505388 7.611438 +v 7.576790 1.346700 7.727126 +v 7.692478 0.836555 8.676722 +v 8.642075 1.023553 8.561034 +v 9.591673 0.836555 8.445347 +v 10.541268 0.466570 8.329659 +v 11.490867 0.352674 8.213970 +v 12.440463 0.352674 8.098283 +v 13.390059 0.425575 7.982595 +v 14.339656 0.701469 7.866907 +v 15.289254 1.147682 7.751220 +v 16.238850 1.432600 7.635531 +v 17.188446 1.510183 7.519844 +v 7.692478 1.156783 8.676722 +v 8.642075 1.346700 8.561034 +v 9.591673 1.156783 8.445347 +v 10.541268 0.776464 8.329659 +v 11.490867 0.656240 8.213970 +v 12.440463 0.656240 8.098283 +v 13.390059 0.731882 7.982595 +v 14.339656 1.011765 7.866907 +v 15.289254 1.458108 7.751220 +v 16.238850 1.744754 7.635531 +v 17.188446 1.820374 7.519844 +vn -0.599026 -0.602062 -0.527910 +vn 0.050826 -0.714581 -0.697704 +vn 0.110539 -0.740282 -0.663147 +vn 0.068305 -0.791033 -0.607948 +vn -0.067464 -0.826818 -0.558409 +vn -0.220364 -0.810985 -0.541980 +vn -0.312819 -0.765434 -0.562366 +vn -0.310167 -0.731317 -0.607431 +vn -0.235293 -0.721301 -0.651431 +vn -0.146453 -0.720505 -0.677808 +vn 0.497113 -0.591273 -0.635040 +vn -0.673059 0.528950 -0.516918 +vn -0.214383 0.627951 -0.748143 +vn -0.244448 0.573498 -0.781886 +vn -0.195686 0.552643 -0.810119 +vn -0.099946 0.558569 -0.823415 +vn 0.008650 0.577861 -0.816090 +vn 0.097853 0.605519 -0.789792 +vn 0.119938 0.639695 -0.759213 +vn 0.058512 0.673582 -0.736793 +vn -0.026391 0.691911 -0.721500 +vn 0.510733 0.564070 -0.648827 +vn -0.636587 -0.765097 0.096869 +vn 0.243364 -0.969112 0.039942 +vn 0.296439 -0.944376 0.142401 +vn 0.202625 -0.945466 0.255022 +vn 0.038919 -0.947516 0.317330 +vn -0.134932 -0.932967 0.333715 +vn -0.273840 -0.911088 0.308108 +vn -0.300219 -0.924670 0.234209 +vn -0.206048 -0.966808 0.151087 +vn -0.082062 -0.991596 0.100013 +vn 0.695273 -0.718229 -0.027242 +vn 0.709541 0.690960 -0.138295 +vn 0.084344 0.992606 -0.087289 +vn 0.215678 0.969716 -0.114604 +vn 0.318789 0.933719 -0.162917 +vn 0.288834 0.927346 -0.237916 +vn 0.139049 0.942800 -0.302974 +vn -0.038532 0.947194 -0.318338 +vn -0.203287 0.933954 -0.293945 +vn -0.303199 0.922896 -0.237346 +vn -0.257204 0.954734 -0.149428 +vn -0.770621 0.637095 0.015922 +vn -0.611009 -0.785914 0.094909 +vn 0.309377 -0.950758 0.018605 +vn 0.330547 -0.939656 0.088237 +vn 0.190193 -0.969929 0.151870 +vn 0.020843 -0.984928 0.171703 +vn -0.150180 -0.970266 0.189817 +vn -0.308041 -0.927864 0.210190 +vn -0.329400 -0.923914 0.194626 +vn -0.204784 -0.965387 0.161530 +vn -0.068907 -0.986607 0.147847 +vn 0.702926 -0.711047 0.017523 +vn 0.700835 0.687586 -0.189884 +vn 0.069750 0.985420 -0.155188 +vn 0.209274 0.964095 -0.163481 +vn 0.335981 0.925650 -0.174037 +vn 0.311742 0.930325 -0.193166 +vn 0.149069 0.969065 -0.196701 +vn -0.021008 0.984904 -0.171823 +vn -0.189856 0.970498 -0.148620 +vn -0.334154 0.933955 -0.126767 +vn -0.317712 0.944096 -0.087989 +vn -0.788491 0.613893 0.037649 +vn -0.613516 -0.786143 0.074682 +vn 0.303138 -0.952222 -0.037151 +vn 0.317442 -0.947484 -0.038790 +vn 0.171703 -0.984929 -0.020833 +vn -0.000031 -1.000000 0.000039 +vn -0.171685 -0.984928 0.020991 +vn -0.320490 -0.946391 0.040385 +vn -0.316495 -0.946991 0.055123 +vn -0.169818 -0.982385 0.077982 +vn -0.041434 -0.992752 0.112813 +vn 0.707821 -0.706351 0.007587 +vn 0.695169 0.693527 -0.189104 +vn 0.039320 0.988596 -0.145367 +vn 0.166982 0.977636 -0.127844 +vn 0.313375 0.945008 -0.093569 +vn 0.319572 0.945981 -0.054709 +vn 0.171636 0.984904 -0.022475 +vn -0.000043 1.000000 -0.000034 +vn -0.171841 0.984904 0.020860 +vn -0.318278 0.947194 0.039025 +vn -0.305022 0.951604 0.037580 +vn -0.781929 0.616023 0.095404 +vn -0.641295 -0.765086 0.058177 +vn 0.228429 -0.969783 -0.085684 +vn 0.261573 -0.951515 -0.161862 +vn 0.151503 -0.969324 -0.193539 +vn -0.021001 -0.984928 -0.171682 +vn -0.193092 -0.969929 -0.148167 +vn -0.296212 -0.945466 -0.135469 +vn -0.250965 -0.963185 -0.096388 +vn -0.112666 -0.993312 -0.025260 +vn -0.018339 -0.999200 0.035536 +vn 0.705864 -0.707020 -0.043341 +vn 0.697463 0.703445 -0.136787 +vn 0.014976 0.997486 -0.069265 +vn 0.106696 0.993220 -0.046144 +vn 0.246332 0.969140 0.009378 +vn 0.293675 0.951714 0.089414 +vn 0.192035 0.969807 0.150325 +vn 0.020851 0.984903 0.171847 +vn -0.150426 0.970143 0.190246 +vn -0.256521 0.945341 0.201317 +vn -0.222547 0.962215 0.156895 +vn -0.754377 0.638420 0.152757 +vn -0.675241 -0.734843 0.063685 +vn 0.118792 -0.989218 -0.085657 +vn 0.157385 -0.966855 -0.201053 +vn 0.094325 -0.951515 -0.292784 +vn -0.038560 -0.947484 -0.317470 +vn -0.165018 -0.939656 -0.299693 +vn -0.209409 -0.944376 -0.253579 +vn -0.142467 -0.975746 -0.166202 +vn -0.038468 -0.997398 -0.060972 +vn 0.011450 -0.999861 0.012149 +vn 0.708895 -0.702842 -0.058999 +vn 0.693587 0.709641 -0.123879 +vn -0.016267 0.998809 -0.046007 +vn 0.030011 0.999395 -0.017564 +vn 0.134792 0.989619 0.049859 +vn 0.203306 0.966094 0.159148 +vn 0.160941 0.951269 0.263029 +vn 0.038904 0.947162 0.318386 +vn -0.086977 0.939483 0.331370 +vn -0.143412 0.943787 0.297824 +vn -0.107136 0.974403 0.197637 +vn -0.721435 0.671438 0.169420 +vn -0.676722 -0.726492 0.119404 +vn 0.044928 -0.998382 0.034851 +vn 0.017095 -0.998542 -0.051192 +vn 0.002788 -0.978146 -0.207902 +vn -0.039266 -0.953079 -0.300165 +vn -0.092341 -0.950757 -0.295861 +vn -0.087286 -0.969736 -0.228021 +vn -0.009467 -0.992899 -0.118585 +vn 0.070787 -0.997491 0.000420 +vn 0.077588 -0.991887 0.100702 +vn 0.729717 -0.683588 0.014874 +vn 0.674057 0.707296 -0.213023 +vn -0.084884 0.983981 -0.156769 +vn -0.083368 0.991253 -0.102311 +vn -0.005273 0.999943 -0.009270 +vn 0.076243 0.990585 0.113699 +vn 0.087305 0.968659 0.232545 +vn 0.040110 0.951586 0.304755 +vn 0.010646 0.952089 0.305635 +vn 0.004997 0.977760 0.209667 +vn -0.034190 0.997958 0.053954 +vn -0.729579 0.680585 0.067219 +vn -0.635658 -0.727218 0.259024 +vn 0.039333 -0.961732 0.271154 +vn -0.137465 -0.957450 0.253757 +vn -0.129259 -0.989870 0.058725 +vn -0.046866 -0.992288 -0.114753 +vn -0.025705 -0.988873 -0.146527 +vn 0.025003 -0.994393 -0.102745 +vn 0.136203 -0.990513 -0.018254 +vn 0.214018 -0.971281 0.103968 +vn 0.172436 -0.955708 0.238511 +vn 0.755635 -0.643259 0.123425 +vn 0.644344 0.685570 -0.338843 +vn -0.176109 0.933268 -0.313044 +vn -0.225667 0.945862 -0.233280 +vn -0.154735 0.981077 -0.116383 +vn -0.037547 0.999278 0.005775 +vn 0.020768 0.994831 0.099400 +vn 0.052134 0.987864 0.146311 +vn 0.157049 0.982054 0.104429 +vn 0.172396 0.982522 -0.070214 +vn -0.031789 0.969133 -0.244479 +vn -0.774993 0.621770 -0.113085 +vn -0.614110 -0.734644 0.288386 +vn 0.037162 -0.955783 0.291717 +vn -0.247938 -0.917069 0.312268 +vn -0.256379 -0.946881 0.194126 +vn -0.076923 -0.996657 0.027514 +vn 0.014613 -0.999399 -0.031445 +vn 0.112773 -0.993474 -0.017084 +vn 0.265926 -0.963190 0.039343 +vn 0.323267 -0.933484 0.155265 +vn 0.220989 -0.934749 0.278224 +vn 0.762125 -0.632263 0.139315 +vn 0.638094 0.689688 -0.342296 +vn -0.221099 0.921316 -0.319831 +vn -0.329283 0.908367 -0.257764 +vn -0.280638 0.945862 -0.163057 +vn -0.124207 0.990379 -0.061016 +vn -0.018246 0.999820 0.005245 +vn 0.086584 0.996041 0.020138 +vn 0.286958 0.956545 -0.051730 +vn 0.272502 0.939817 -0.206125 +vn -0.033691 0.955810 -0.292048 +vn -0.786435 0.608576 -0.105618 +vn -0.643540 -0.761356 0.078699 +vn -0.000319 -1.000000 0.000407 +vn -0.292106 -0.955783 0.033964 +vn -0.280714 -0.959253 0.032134 +vn -0.079081 -0.996824 0.009362 +vn 0.032279 -0.999471 -0.003932 +vn 0.163733 -0.986486 -0.006001 +vn 0.337538 -0.941095 0.020224 +vn 0.360082 -0.927949 0.096186 +vn 0.208215 -0.963843 0.166293 +vn 0.748825 -0.661451 0.041747 +vn 0.657609 0.721687 -0.216142 +vn -0.207816 0.964545 -0.162679 +vn -0.363560 0.921316 -0.137843 +vn -0.346170 0.933268 -0.095798 +vn -0.172563 0.983981 -0.044762 +vn -0.035218 0.999322 -0.010758 +vn 0.082937 0.996504 -0.010097 +vn 0.288978 0.956719 -0.034363 +vn 0.296553 0.954354 -0.035558 +vn -0.000565 1.000000 -0.000442 +vn -0.765765 0.636361 0.092994 +vn -0.472038 -0.643086 0.603010 +vn 0.078113 -0.761356 0.643611 +vn -0.132506 -0.734644 0.665387 +vn -0.098830 -0.727218 0.679254 +vn 0.041474 -0.719840 0.692899 +vn 0.112202 -0.706581 0.698680 +vn 0.209777 -0.690552 0.692194 +vn 0.328519 -0.648610 0.686572 +vn 0.327923 -0.633767 0.700575 +vn 0.212216 -0.658507 0.722033 +vn 0.658530 -0.542480 0.521588 +vn -0.541870 0.502924 0.673383 +vn 0.093119 0.634566 0.767239 +vn 0.276022 0.628194 0.727451 +vn 0.257878 0.667479 0.698549 +vn 0.129422 0.704689 0.697613 +vn 0.057278 0.709991 0.701878 +vn -0.044956 0.707296 0.705487 +vn -0.174229 0.685570 0.706851 +vn -0.179082 0.689688 0.701613 +vn -0.051932 0.721687 0.690268 +vn 0.626124 0.606414 0.490132 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +g Cube +usemtl Default +f 365/353/365 354/354/354 353/355/353 364/356/364 +f 366/357/366 355/358/355 354/354/354 365/353/365 +f 367/359/367 356/360/356 355/358/355 366/357/366 +f 368/361/368 357/362/357 356/360/356 367/359/367 +f 369/363/369 358/364/358 357/362/357 368/361/368 +f 370/365/370 359/366/359 358/364/358 369/363/369 +f 371/367/371 360/368/360 359/366/359 370/365/370 +f 372/369/372 361/370/361 360/368/360 371/367/371 +f 373/371/373 362/372/362 361/370/361 372/369/372 +f 374/373/374 363/374/363 362/372/362 373/371/373 +f 376/375/376 375/376/375 353/355/353 354/354/354 +f 377/377/377 376/375/376 354/354/354 355/358/355 +f 378/378/378 377/377/377 355/358/355 356/360/356 +f 379/379/379 378/378/378 356/360/356 357/362/357 +f 380/380/380 379/379/379 357/362/357 358/364/358 +f 381/381/381 380/380/380 358/364/358 359/366/359 +f 382/382/382 381/381/381 359/366/359 360/368/360 +f 383/383/383 382/382/382 360/368/360 361/370/361 +f 384/384/384 383/383/383 361/370/361 362/372/362 +f 385/385/385 384/384/384 362/372/362 363/374/363 +f 386/386/386 385/385/385 363/374/363 374/373/374 +f 387/387/387 386/386/386 374/373/374 373/371/373 +f 388/388/388 387/387/387 373/371/373 372/369/372 +f 389/389/389 388/388/388 372/369/372 371/367/371 +f 390/390/390 389/389/389 371/367/371 370/365/370 +f 391/391/391 390/390/390 370/365/370 369/363/369 +f 392/392/392 391/391/391 369/363/369 368/361/368 +f 393/393/393 392/392/392 368/361/368 367/359/367 +f 394/394/394 393/393/393 367/359/367 366/357/366 +f 395/395/395 394/394/394 366/357/366 365/353/365 +f 396/396/396 395/395/395 365/353/365 364/356/364 +f 375/376/375 396/396/396 364/356/364 353/355/353 +f 398/397/398 397/398/397 375/376/375 376/375/376 +f 399/399/399 398/397/398 376/375/376 377/377/377 +f 400/400/400 399/399/399 377/377/377 378/378/378 +f 401/401/401 400/400/400 378/378/378 379/379/379 +f 402/402/402 401/401/401 379/379/379 380/380/380 +f 403/403/403 402/402/402 380/380/380 381/381/381 +f 404/404/404 403/403/403 381/381/381 382/382/382 +f 405/405/405 404/404/404 382/382/382 383/383/383 +f 406/406/406 405/405/405 383/383/383 384/384/384 +f 407/407/407 406/406/406 384/384/384 385/385/385 +f 408/408/408 407/407/407 385/385/385 386/386/386 +f 409/409/409 408/408/408 386/386/386 387/387/387 +f 410/410/410 409/409/409 387/387/387 388/388/388 +f 411/411/411 410/410/410 388/388/388 389/389/389 +f 412/412/412 411/411/411 389/389/389 390/390/390 +f 413/413/413 412/412/412 390/390/390 391/391/391 +f 414/414/414 413/413/413 391/391/391 392/392/392 +f 415/415/415 414/414/414 392/392/392 393/393/393 +f 416/416/416 415/415/415 393/393/393 394/394/394 +f 417/417/417 416/416/416 394/394/394 395/395/395 +f 418/418/418 417/417/417 395/395/395 396/396/396 +f 397/398/397 418/418/418 396/396/396 375/376/375 +f 420/419/420 419/420/419 397/398/397 398/397/398 +f 421/421/421 420/419/420 398/397/398 399/399/399 +f 422/422/422 421/421/421 399/399/399 400/400/400 +f 423/423/423 422/422/422 400/400/400 401/401/401 +f 424/424/424 423/423/423 401/401/401 402/402/402 +f 425/425/425 424/424/424 402/402/402 403/403/403 +f 426/426/426 425/425/425 403/403/403 404/404/404 +f 427/427/427 426/426/426 404/404/404 405/405/405 +f 428/428/428 427/427/427 405/405/405 406/406/406 +f 429/429/429 428/428/428 406/406/406 407/407/407 +f 430/430/430 429/429/429 407/407/407 408/408/408 +f 431/431/431 430/430/430 408/408/408 409/409/409 +f 432/432/432 431/431/431 409/409/409 410/410/410 +f 433/433/433 432/432/432 410/410/410 411/411/411 +f 434/434/434 433/433/433 411/411/411 412/412/412 +f 435/435/435 434/434/434 412/412/412 413/413/413 +f 436/436/436 435/435/435 413/413/413 414/414/414 +f 437/437/437 436/436/436 414/414/414 415/415/415 +f 438/438/438 437/437/437 415/415/415 416/416/416 +f 439/439/439 438/438/438 416/416/416 417/417/417 +f 440/440/440 439/439/439 417/417/417 418/418/418 +f 419/420/419 440/440/440 418/418/418 397/398/397 +f 442/441/442 441/442/441 419/420/419 420/419/420 +f 443/443/443 442/441/442 420/419/420 421/421/421 +f 444/444/444 443/443/443 421/421/421 422/422/422 +f 445/445/445 444/444/444 422/422/422 423/423/423 +f 446/446/446 445/445/445 423/423/423 424/424/424 +f 447/447/447 446/446/446 424/424/424 425/425/425 +f 448/448/448 447/447/447 425/425/425 426/426/426 +f 449/449/449 448/448/448 426/426/426 427/427/427 +f 450/450/450 449/449/449 427/427/427 428/428/428 +f 451/451/451 450/450/450 428/428/428 429/429/429 +f 452/452/452 451/451/451 429/429/429 430/430/430 +f 453/453/453 452/452/452 430/430/430 431/431/431 +f 454/454/454 453/453/453 431/431/431 432/432/432 +f 455/455/455 454/454/454 432/432/432 433/433/433 +f 456/456/456 455/455/455 433/433/433 434/434/434 +f 457/457/457 456/456/456 434/434/434 435/435/435 +f 458/458/458 457/457/457 435/435/435 436/436/436 +f 459/459/459 458/458/458 436/436/436 437/437/437 +f 460/460/460 459/459/459 437/437/437 438/438/438 +f 461/461/461 460/460/460 438/438/438 439/439/439 +f 462/462/462 461/461/461 439/439/439 440/440/440 +f 441/442/441 462/462/462 440/440/440 419/420/419 +f 464/463/464 463/464/463 441/442/441 442/441/442 +f 465/465/465 464/463/464 442/441/442 443/443/443 +f 466/466/466 465/465/465 443/443/443 444/444/444 +f 467/467/467 466/466/466 444/444/444 445/445/445 +f 468/468/468 467/467/467 445/445/445 446/446/446 +f 469/469/469 468/468/468 446/446/446 447/447/447 +f 470/470/470 469/469/469 447/447/447 448/448/448 +f 471/471/471 470/470/470 448/448/448 449/449/449 +f 472/472/472 471/471/471 449/449/449 450/450/450 +f 473/473/473 472/472/472 450/450/450 451/451/451 +f 474/474/474 473/473/473 451/451/451 452/452/452 +f 475/475/475 474/474/474 452/452/452 453/453/453 +f 476/476/476 475/475/475 453/453/453 454/454/454 +f 477/477/477 476/476/476 454/454/454 455/455/455 +f 478/478/478 477/477/477 455/455/455 456/456/456 +f 479/479/479 478/478/478 456/456/456 457/457/457 +f 480/480/480 479/479/479 457/457/457 458/458/458 +f 481/481/481 480/480/480 458/458/458 459/459/459 +f 482/482/482 481/481/481 459/459/459 460/460/460 +f 483/483/483 482/482/482 460/460/460 461/461/461 +f 484/484/484 483/483/483 461/461/461 462/462/462 +f 463/464/463 484/484/484 462/462/462 441/442/441 +f 486/485/486 485/486/485 463/464/463 464/463/464 +f 487/487/487 486/485/486 464/463/464 465/465/465 +f 488/488/488 487/487/487 465/465/465 466/466/466 +f 489/489/489 488/488/488 466/466/466 467/467/467 +f 490/490/490 489/489/489 467/467/467 468/468/468 +f 491/491/491 490/490/490 468/468/468 469/469/469 +f 492/492/492 491/491/491 469/469/469 470/470/470 +f 493/493/493 492/492/492 470/470/470 471/471/471 +f 494/494/494 493/493/493 471/471/471 472/472/472 +f 495/495/495 494/494/494 472/472/472 473/473/473 +f 496/496/496 495/495/495 473/473/473 474/474/474 +f 497/497/497 496/496/496 474/474/474 475/475/475 +f 498/498/498 497/497/497 475/475/475 476/476/476 +f 499/499/499 498/498/498 476/476/476 477/477/477 +f 500/500/500 499/499/499 477/477/477 478/478/478 +f 501/501/501 500/500/500 478/478/478 479/479/479 +f 502/502/502 501/501/501 479/479/479 480/480/480 +f 503/503/503 502/502/502 480/480/480 481/481/481 +f 504/504/504 503/503/503 481/481/481 482/482/482 +f 505/505/505 504/504/504 482/482/482 483/483/483 +f 506/506/506 505/505/505 483/483/483 484/484/484 +f 485/486/485 506/506/506 484/484/484 463/464/463 +f 508/507/508 507/508/507 485/486/485 486/485/486 +f 509/509/509 508/507/508 486/485/486 487/487/487 +f 510/510/510 509/509/509 487/487/487 488/488/488 +f 511/511/511 510/510/510 488/488/488 489/489/489 +f 512/512/512 511/511/511 489/489/489 490/490/490 +f 513/513/513 512/512/512 490/490/490 491/491/491 +f 514/514/514 513/513/513 491/491/491 492/492/492 +f 515/515/515 514/514/514 492/492/492 493/493/493 +f 516/516/516 515/515/515 493/493/493 494/494/494 +f 517/517/517 516/516/516 494/494/494 495/495/495 +f 518/518/518 517/517/517 495/495/495 496/496/496 +f 519/519/519 518/518/518 496/496/496 497/497/497 +f 520/520/520 519/519/519 497/497/497 498/498/498 +f 521/521/521 520/520/520 498/498/498 499/499/499 +f 522/522/522 521/521/521 499/499/499 500/500/500 +f 523/523/523 522/522/522 500/500/500 501/501/501 +f 524/524/524 523/523/523 501/501/501 502/502/502 +f 525/525/525 524/524/524 502/502/502 503/503/503 +f 526/526/526 525/525/525 503/503/503 504/504/504 +f 527/527/527 526/526/526 504/504/504 505/505/505 +f 528/528/528 527/527/527 505/505/505 506/506/506 +f 507/508/507 528/528/528 506/506/506 485/486/485 +f 530/529/530 529/530/529 507/508/507 508/507/508 +f 531/531/531 530/529/530 508/507/508 509/509/509 +f 532/532/532 531/531/531 509/509/509 510/510/510 +f 533/533/533 532/532/532 510/510/510 511/511/511 +f 534/534/534 533/533/533 511/511/511 512/512/512 +f 535/535/535 534/534/534 512/512/512 513/513/513 +f 536/536/536 535/535/535 513/513/513 514/514/514 +f 537/537/537 536/536/536 514/514/514 515/515/515 +f 538/538/538 537/537/537 515/515/515 516/516/516 +f 539/539/539 538/538/538 516/516/516 517/517/517 +f 540/540/540 539/539/539 517/517/517 518/518/518 +f 541/541/541 540/540/540 518/518/518 519/519/519 +f 542/542/542 541/541/541 519/519/519 520/520/520 +f 543/543/543 542/542/542 520/520/520 521/521/521 +f 544/544/544 543/543/543 521/521/521 522/522/522 +f 545/545/545 544/544/544 522/522/522 523/523/523 +f 546/546/546 545/545/545 523/523/523 524/524/524 +f 547/547/547 546/546/546 524/524/524 525/525/525 +f 548/548/548 547/547/547 525/525/525 526/526/526 +f 549/549/549 548/548/548 526/526/526 527/527/527 +f 550/550/550 549/549/549 527/527/527 528/528/528 +f 529/530/529 550/550/550 528/528/528 507/508/507 +f 552/551/552 551/552/551 529/530/529 530/529/530 +f 553/553/553 552/551/552 530/529/530 531/531/531 +f 554/554/554 553/553/553 531/531/531 532/532/532 +f 555/555/555 554/554/554 532/532/532 533/533/533 +f 556/556/556 555/555/555 533/533/533 534/534/534 +f 557/557/557 556/556/556 534/534/534 535/535/535 +f 558/558/558 557/557/557 535/535/535 536/536/536 +f 559/559/559 558/558/558 536/536/536 537/537/537 +f 560/560/560 559/559/559 537/537/537 538/538/538 +f 561/561/561 560/560/560 538/538/538 539/539/539 +f 562/562/562 561/561/561 539/539/539 540/540/540 +f 563/563/563 562/562/562 540/540/540 541/541/541 +f 564/564/564 563/563/563 541/541/541 542/542/542 +f 565/565/565 564/564/564 542/542/542 543/543/543 +f 566/566/566 565/565/565 543/543/543 544/544/544 +f 567/567/567 566/566/566 544/544/544 545/545/545 +f 568/568/568 567/567/567 545/545/545 546/546/546 +f 569/569/569 568/568/568 546/546/546 547/547/547 +f 570/570/570 569/569/569 547/547/547 548/548/548 +f 571/571/571 570/570/570 548/548/548 549/549/549 +f 572/572/572 571/571/571 549/549/549 550/550/550 +f 551/552/551 572/572/572 550/550/550 529/530/529 +f 574/573/574 585/574/585 584/575/584 573/576/573 +f 575/577/575 586/578/586 585/574/585 574/573/574 +f 576/579/576 587/580/587 586/578/586 575/577/575 +f 577/581/577 588/582/588 587/580/587 576/579/576 +f 578/583/578 589/584/589 588/582/588 577/581/577 +f 579/585/579 590/586/590 589/584/589 578/583/578 +f 580/587/580 591/588/591 590/586/590 579/585/579 +f 581/589/581 592/590/592 591/588/591 580/587/580 +f 582/591/582 593/592/593 592/590/592 581/589/581 +f 583/593/583 594/594/594 593/592/593 582/591/582 +f 574/573/574 573/576/573 551/552/551 552/551/552 +f 575/577/575 574/573/574 552/551/552 553/553/553 +f 576/579/576 575/577/575 553/553/553 554/554/554 +f 577/581/577 576/579/576 554/554/554 555/555/555 +f 578/583/578 577/581/577 555/555/555 556/556/556 +f 579/585/579 578/583/578 556/556/556 557/557/557 +f 580/587/580 579/585/579 557/557/557 558/558/558 +f 581/589/581 580/587/580 558/558/558 559/559/559 +f 582/591/582 581/589/581 559/559/559 560/560/560 +f 583/593/583 582/591/582 560/560/560 561/561/561 +f 594/594/594 583/593/583 561/561/561 562/562/562 +f 593/592/593 594/594/594 562/562/562 563/563/563 +f 592/590/592 593/592/593 563/563/563 564/564/564 +f 591/588/591 592/590/592 564/564/564 565/565/565 +f 590/586/590 591/588/591 565/565/565 566/566/566 +f 589/584/589 590/586/590 566/566/566 567/567/567 +f 588/582/588 589/584/589 567/567/567 568/568/568 +f 587/580/587 588/582/588 568/568/568 569/569/569 +f 586/578/586 587/580/587 569/569/569 570/570/570 +f 585/574/585 586/578/586 570/570/570 571/571/571 +f 584/575/584 585/574/585 571/571/571 572/572/572 +f 573/576/573 584/575/584 572/572/572 551/552/551 +v 9.244040 3.542752 5.448152 +v 8.721201 0.891598 0.179218 +v 9.229363 3.839593 5.300248 +v 8.706525 1.188440 0.031314 +v 12.977483 3.542752 5.077682 +v 12.454644 0.891598 -0.191252 +v 12.962806 3.839593 4.929777 +v 12.439967 1.188440 -0.339157 +vn -0.498027 -0.257759 0.827967 +vn -0.599982 -0.774742 -0.199490 +vn -0.549076 0.774743 0.313511 +vn -0.651031 0.257761 -0.713946 +vn 0.651031 -0.257760 0.713946 +vn 0.549076 -0.774743 -0.313512 +vn 0.599981 0.774743 0.199489 +vn 0.498026 0.257761 -0.827967 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +g Generic +usemtl Default +f 598/595/598 596/596/596 595/597/595 597/598/597 +f 600/599/600 602/600/602 601/601/601 599/602/599 +f 600/599/600 599/602/599 595/597/595 596/596/596 +f 602/600/602 600/599/600 596/596/596 598/595/598 +f 601/601/601 602/600/602 598/595/598 597/598/597 +f 599/602/599 601/601/601 597/598/597 595/597/595 +v 10.655820 3.835317 5.389103 +v 10.341024 2.300720 2.216710 +v 10.655284 5.639900 5.383704 +v 10.341024 5.639900 2.216710 +v 11.455583 3.835317 5.309742 +v 11.140786 2.300720 2.137349 +v 11.455049 5.639900 5.304344 +v 11.140786 5.639900 2.137349 +vn -0.434325 -0.456966 0.776237 +vn -0.719726 -0.617016 -0.318255 +vn -0.517001 0.578506 0.630904 +vn -0.631539 0.577350 -0.517518 +vn 0.578405 -0.456967 0.675744 +vn 0.643145 -0.617016 -0.453492 +vn 0.630907 0.578507 0.516998 +vn 0.517518 0.577350 -0.631539 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +g Generic +usemtl Default +f 606/603/606 604/604/604 603/605/603 605/606/605 +f 608/607/608 610/608/610 609/609/609 607/610/607 +f 608/607/608 607/610/607 603/605/603 604/604/604 +f 610/608/610 608/607/608 604/604/604 606/603/606 +f 609/609/609 610/608/610 606/603/606 605/606/605 +f 607/610/607 609/609/609 605/606/605 603/605/603 +v 9.616676 3.522911 9.044438 +v 9.243063 3.522911 5.279329 +v 9.616676 3.854878 9.044438 +v 9.243063 3.854878 5.279329 +v 13.101874 3.522911 8.698602 +v 12.728263 3.522911 4.933492 +v 13.101874 3.854878 8.698602 +v 12.728263 3.854878 4.933492 +vn -0.517518 -0.577350 0.631539 +vn -0.631539 -0.577350 -0.517518 +vn -0.517518 0.577350 0.631539 +vn -0.631539 0.577350 -0.517518 +vn 0.631539 -0.577350 0.517518 +vn 0.517518 -0.577350 -0.631539 +vn 0.631539 0.577350 0.517518 +vn 0.517518 0.577350 -0.631539 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +g Generic +usemtl Default +f 614/611/614 612/612/612 611/613/611 613/614/613 +f 616/615/616 618/616/618 617/617/617 615/618/615 +f 616/615/616 615/618/615 611/613/611 612/612/612 +f 618/616/618 616/615/616 612/612/612 614/611/614 +f 617/617/617 618/616/618 614/611/614 613/614/613 +f 615/618/615 617/617/617 613/614/613 611/613/611 +v -3.903490 0.567013 -5.386715 +v -3.840990 0.567013 -5.386715 +v -3.903490 3.692013 -5.386715 +v -3.840990 3.692013 -5.386715 +v -3.903490 0.567013 -5.324215 +v -3.840990 0.567013 -5.324215 +v -3.903490 3.692013 -5.324215 +v -3.840990 3.692013 -5.324215 +vn -0.577350 -0.577350 -0.577350 +vn 0.577350 -0.577350 -0.577350 +vn -0.577350 0.577350 -0.577350 +vn 0.577350 0.577350 -0.577350 +vn -0.577350 -0.577350 0.577350 +vn 0.577350 -0.577350 0.577350 +vn -0.577350 0.577350 0.577350 +vn 0.577350 0.577350 0.577350 +vt 0.995049 0.995049 +vt 0.995049 0.995049 +vt 0.004950 0.995049 +vt 0.004950 0.995049 +vt 0.995049 0.004950 +vt 0.995049 0.004950 +vt 0.004950 0.004950 +vt 0.004950 0.004950 +g Cube +usemtl Default +f 622/619/622 620/620/620 619/621/619 621/622/621 +f 624/623/624 626/624/626 625/625/625 623/626/623 +f 624/623/624 623/626/623 619/621/619 620/620/620 +f 626/624/626 624/623/624 620/620/620 622/619/622 +f 625/625/625 626/624/626 622/619/622 621/622/621 +f 623/626/623 625/625/625 621/622/621 619/621/619 +v -2.897977 3.608987 -2.048939 +v -2.835477 3.608987 -2.048939 +v -2.897977 6.733987 -2.048939 +v -2.835477 6.733987 -2.048939 +v -2.897977 3.608987 -1.986439 +v -2.835477 3.608987 -1.986439 +v -2.897977 6.733987 -1.986439 +v -2.835477 6.733987 -1.986439 +vn -0.577350 -0.577350 -0.577350 +vn 0.577350 -0.577350 -0.577350 +vn -0.577350 0.577350 -0.577350 +vn 0.577350 0.577350 -0.577350 +vn -0.577350 -0.577350 0.577350 +vn 0.577350 -0.577350 0.577350 +vn -0.577350 0.577350 0.577350 +vn 0.577350 0.577350 0.577350 +vt 0.995049 0.995049 +vt 0.995049 0.995049 +vt 0.004950 0.995049 +vt 0.004950 0.995049 +vt 0.995049 0.004950 +vt 0.995049 0.004950 +vt 0.004950 0.004950 +vt 0.004950 0.004950 +g Cube +usemtl Default +f 630/627/630 628/628/628 627/629/627 629/630/629 +f 632/631/632 634/632/634 633/633/633 631/634/631 +f 632/631/632 631/634/631 627/629/627 628/628/628 +f 634/632/634 632/631/632 628/628/628 630/627/630 +f 633/633/633 634/632/634 630/627/630 629/630/629 +f 631/634/631 633/633/633 629/630/629 627/629/627 +v 5.034212 0.352674 -12.430222 +v 5.983809 0.387642 -12.545910 +v 6.933407 0.490333 -12.661598 +v 7.883003 0.616432 -12.777286 +v 8.832601 0.668722 -12.892973 +v 9.782198 0.616432 -13.008661 +v 10.731791 0.461595 -13.124350 +v 11.681392 0.250151 -13.240037 +v 12.630986 0.039294 -13.355724 +v 13.580585 -0.073016 -13.471413 +v 14.530181 -0.103597 -13.587101 +v 5.034212 0.656240 -12.430222 +v 5.983809 0.692934 -12.545910 +v 6.933407 0.797731 -12.661598 +v 7.883003 0.925565 -12.777286 +v 8.832601 0.978385 -12.892973 +v 9.782198 0.925565 -13.008661 +v 10.731791 0.768992 -13.124350 +v 11.681392 0.552792 -13.240037 +v 12.630986 0.340157 -13.355724 +v 13.580585 0.227165 -13.471413 +v 14.530181 0.197357 -13.587101 +v 5.149900 0.369316 -11.480626 +v 6.099496 0.490333 -11.596313 +v 7.049095 0.732014 -11.712002 +v 7.998691 0.981286 -11.827689 +v 8.948289 1.073040 -11.943377 +v 9.897886 0.981286 -12.059065 +v 10.847480 0.710202 -12.174753 +v 11.797080 0.375586 -12.290441 +v 12.746675 0.095850 -12.406129 +v 13.696271 -0.037722 -12.521816 +v 14.645869 -0.073016 -12.637505 +v 14.645869 0.227165 -12.637505 +v 13.696271 0.262314 -12.521816 +v 12.746675 0.396454 -12.406129 +v 11.797080 0.682983 -12.290441 +v 10.847480 1.027865 -12.174753 +v 9.897886 1.291669 -12.059065 +v 8.948289 1.382822 -11.943377 +v 7.998691 1.291669 -11.827689 +v 7.049095 1.048776 -11.712002 +v 6.099496 0.797731 -11.596313 +v 5.149900 0.672884 -11.480626 +v 5.265588 0.400746 -10.531029 +v 6.215184 0.616432 -10.646716 +v 7.164783 0.981286 -10.762404 +v 8.114379 1.250078 -10.878092 +v 9.063976 1.331208 -10.993780 +v 10.013573 1.250078 -11.109468 +v 10.963168 0.974047 -11.225156 +v 11.912766 0.554879 -11.340843 +v 12.862363 0.232864 -11.456532 +v 13.811959 0.079210 -11.572219 +v 14.761558 0.039294 -11.687907 +v 14.761558 0.340157 -11.687907 +v 13.811959 0.379810 -11.572219 +v 12.862363 0.535615 -11.456532 +v 11.912766 0.865679 -11.340843 +v 10.963168 1.285819 -11.225156 +v 10.013573 1.561758 -11.109468 +v 9.063976 1.642555 -10.993780 +v 8.114379 1.561758 -10.878092 +v 7.164783 1.291669 -10.762404 +v 6.215184 0.925565 -10.646716 +v 5.265588 0.706379 -10.531029 +v 5.381276 0.418730 -9.581432 +v 6.330872 0.668722 -9.697121 +v 7.280471 1.073040 -9.812808 +v 8.230067 1.331208 -9.928495 +v 9.179665 1.401506 -10.044184 +v 10.129261 1.331208 -10.159872 +v 11.078855 1.073040 -10.275559 +v 12.028455 0.652759 -10.391247 +v 12.978049 0.357181 -10.506935 +v 13.927648 0.237933 -10.622623 +v 14.877244 0.215185 -10.738311 +v 14.877244 0.516098 -10.738311 +v 13.927648 0.539077 -10.622623 +v 12.978049 0.663227 -10.506935 +v 12.028455 0.963173 -10.391247 +v 11.078855 1.382822 -10.275559 +v 10.129261 1.642555 -10.159872 +v 9.179665 1.711075 -10.044184 +v 8.230067 1.642555 -9.928495 +v 7.280471 1.382822 -9.812808 +v 6.330872 0.978385 -9.697121 +v 5.381276 0.724780 -9.581432 +v 5.496964 0.400746 -8.631835 +v 6.446560 0.616432 -8.747522 +v 7.396158 0.981286 -8.863211 +v 8.345755 1.250078 -8.978899 +v 9.295352 1.331208 -9.094586 +v 10.244949 1.250078 -9.210276 +v 11.194543 0.981286 -9.325962 +v 12.144143 0.616432 -9.441650 +v 13.093738 0.393507 -9.557338 +v 14.043336 0.331762 -9.673025 +v 14.992932 0.323938 -9.788713 +v 14.992932 0.626424 -9.788713 +v 14.043336 0.634429 -9.673025 +v 13.093738 0.699139 -9.557338 +v 12.144143 0.925565 -9.441650 +v 11.194543 1.291669 -9.325962 +v 10.244949 1.561758 -9.210276 +v 9.295352 1.642555 -9.094586 +v 8.345755 1.561758 -8.978899 +v 7.396158 1.291669 -8.863211 +v 6.446560 0.925565 -8.747522 +v 5.496964 0.706379 -8.631835 +v 5.612652 0.369316 -7.682239 +v 6.562247 0.490333 -7.797927 +v 7.511847 0.732014 -7.913614 +v 8.461443 0.981286 -8.029303 +v 9.411040 1.073040 -8.144990 +v 10.360638 0.981286 -8.260678 +v 11.310231 0.732014 -8.376366 +v 12.259831 0.490333 -8.492054 +v 13.209426 0.369316 -8.607741 +v 14.159022 0.352674 -8.723430 +v 15.108621 0.352674 -8.839117 +v 15.108621 0.656240 -8.839117 +v 14.159022 0.656240 -8.723430 +v 13.209426 0.672884 -8.607741 +v 12.259831 0.797731 -8.492054 +v 11.310231 1.048776 -8.376366 +v 10.360638 1.291669 -8.260678 +v 9.411040 1.382822 -8.144990 +v 8.461443 1.291669 -8.029303 +v 7.511847 1.048776 -7.913614 +v 6.562247 0.797731 -7.797927 +v 5.612652 0.672884 -7.682239 +v 5.728339 0.352674 -6.732641 +v 6.677936 0.388949 -6.848330 +v 7.627534 0.490333 -6.964017 +v 8.577130 0.616432 -7.079705 +v 9.526728 0.668722 -7.195393 +v 10.476323 0.616432 -7.311081 +v 11.425920 0.490333 -7.426768 +v 12.375518 0.387642 -7.542457 +v 13.325114 0.371040 -7.658144 +v 14.274711 0.405728 -7.773832 +v 15.224309 0.425575 -7.889520 +v 15.224309 0.731882 -7.889520 +v 14.274711 0.711574 -7.773832 +v 13.325114 0.674609 -7.658144 +v 12.375518 0.692934 -7.542457 +v 11.425920 0.797731 -7.426768 +v 10.476323 0.925565 -7.311081 +v 9.526728 0.978385 -7.195393 +v 8.577130 0.925565 -7.079705 +v 7.627534 0.797731 -6.964017 +v 6.677936 0.694827 -6.848330 +v 5.728339 0.656240 -6.732641 +v 5.844028 0.466570 -5.783045 +v 6.793623 0.547578 -5.898733 +v 7.743222 0.483213 -6.014421 +v 8.692819 0.412086 -6.130109 +v 9.642416 0.418730 -6.245796 +v 10.592011 0.400746 -6.361485 +v 11.541606 0.369316 -6.477172 +v 12.491206 0.391265 -6.592860 +v 13.440801 0.504597 -6.708548 +v 14.390399 0.643761 -6.824236 +v 15.339995 0.701469 -6.939923 +v 15.339995 1.011765 -6.939923 +v 14.390399 0.953471 -6.824236 +v 13.440801 0.812392 -6.708548 +v 12.491206 0.696736 -6.592860 +v 11.541606 0.672884 -6.477172 +v 10.592011 0.706379 -6.361485 +v 9.642416 0.724780 -6.245796 +v 8.692819 0.717206 -6.130109 +v 7.743222 0.793107 -6.014421 +v 6.793623 0.859762 -5.898733 +v 5.844028 0.776464 -5.783045 +v 5.959715 0.836555 -4.833448 +v 6.909311 1.023553 -4.949136 +v 7.858910 0.836555 -5.064823 +v 8.808506 0.466570 -5.180511 +v 9.758104 0.352674 -5.296199 +v 10.707700 0.352674 -5.411887 +v 11.657294 0.371040 -5.527575 +v 12.606894 0.504597 -5.643263 +v 13.556489 0.771319 -5.758950 +v 14.506087 1.046421 -5.874639 +v 15.455684 1.147682 -5.990326 +v 15.455684 1.458108 -5.990326 +v 14.506087 1.357511 -5.874639 +v 13.556489 1.089450 -5.758950 +v 12.606894 0.812392 -5.643263 +v 11.657294 0.674609 -5.527575 +v 10.707700 0.656240 -5.411887 +v 9.758104 0.656240 -5.296199 +v 8.808506 0.776464 -5.180511 +v 7.858910 1.156783 -5.064823 +v 6.909311 1.346700 -4.949136 +v 5.959715 1.156783 -4.833448 +v 6.075403 1.023553 -3.883852 +v 7.024999 1.188442 -3.999540 +v 7.974598 1.023553 -4.115228 +v 8.924194 0.547578 -4.230916 +v 9.873793 0.353981 -4.346604 +v 10.823388 0.352674 -4.462291 +v 11.772983 0.405728 -4.577979 +v 12.722583 0.643761 -4.693666 +v 13.672177 1.046421 -4.809355 +v 14.621774 1.343064 -4.925042 +v 15.571372 1.432600 -5.040730 +v 15.571372 1.744754 -5.040730 +v 14.621774 1.655584 -4.925042 +v 13.672177 1.357511 -4.809355 +v 12.722583 0.953471 -4.693666 +v 11.772983 0.711574 -4.577979 +v 10.823388 0.656240 -4.462291 +v 9.873793 0.658133 -4.346604 +v 8.924194 0.859762 -4.230916 +v 7.974598 1.346700 -4.115228 +v 7.024999 1.505388 -3.999540 +v 6.075403 1.346700 -3.883852 +v 6.191091 0.836555 -2.934255 +v 7.140687 1.023553 -3.049943 +v 8.090285 0.836555 -3.165631 +v 9.039882 0.466570 -3.281319 +v 9.989479 0.352674 -3.397007 +v 10.939075 0.352674 -3.512695 +v 11.888671 0.425575 -3.628382 +v 12.838269 0.701469 -3.744071 +v 13.787866 1.147682 -3.859758 +v 14.737462 1.432600 -3.975446 +v 15.687060 1.510183 -4.091134 +v 6.191091 1.156783 -2.934255 +v 7.140687 1.346700 -3.049943 +v 8.090285 1.156783 -3.165631 +v 9.039882 0.776464 -3.281319 +v 9.989479 0.656240 -3.397007 +v 10.939075 0.656240 -3.512695 +v 11.888671 0.731882 -3.628382 +v 12.838269 1.011765 -3.744071 +v 13.787866 1.458108 -3.859758 +v 14.737462 1.744754 -3.975446 +v 15.687060 1.820374 -4.091134 +vn -0.599026 -0.602063 -0.527909 +vn 0.050825 -0.714581 -0.697704 +vn 0.110539 -0.740282 -0.663147 +vn 0.068305 -0.791033 -0.607948 +vn -0.067464 -0.826818 -0.558408 +vn -0.220365 -0.810985 -0.541980 +vn -0.312818 -0.765434 -0.562366 +vn -0.310166 -0.731317 -0.607431 +vn -0.235293 -0.721301 -0.651431 +vn -0.146454 -0.720505 -0.677808 +vn 0.497112 -0.591272 -0.635041 +vn -0.673059 0.528950 -0.516917 +vn -0.214383 0.627951 -0.748143 +vn -0.244448 0.573498 -0.781886 +vn -0.195686 0.552643 -0.810119 +vn -0.099946 0.558569 -0.823415 +vn 0.008650 0.577861 -0.816090 +vn 0.097853 0.605520 -0.789792 +vn 0.119939 0.639695 -0.759213 +vn 0.058513 0.673582 -0.736793 +vn -0.026391 0.691911 -0.721500 +vn 0.510732 0.564070 -0.648828 +vn -0.636587 -0.765097 0.096869 +vn 0.243364 -0.969112 0.039942 +vn 0.296439 -0.944376 0.142401 +vn 0.202625 -0.945466 0.255022 +vn 0.038919 -0.947516 0.317330 +vn -0.134932 -0.932967 0.333716 +vn -0.273840 -0.911088 0.308108 +vn -0.300219 -0.924670 0.234209 +vn -0.206048 -0.966808 0.151086 +vn -0.082062 -0.991596 0.100013 +vn 0.695273 -0.718229 -0.027243 +vn 0.709541 0.690960 -0.138296 +vn 0.084344 0.992606 -0.087289 +vn 0.215679 0.969716 -0.114603 +vn 0.318789 0.933719 -0.162917 +vn 0.288834 0.927346 -0.237916 +vn 0.139049 0.942800 -0.302974 +vn -0.038532 0.947194 -0.318338 +vn -0.203287 0.933954 -0.293945 +vn -0.303199 0.922896 -0.237346 +vn -0.257204 0.954734 -0.149428 +vn -0.770621 0.637095 0.015922 +vn -0.611009 -0.785914 0.094909 +vn 0.309377 -0.950757 0.018605 +vn 0.330547 -0.939656 0.088237 +vn 0.190193 -0.969929 0.151870 +vn 0.020843 -0.984928 0.171703 +vn -0.150180 -0.970266 0.189817 +vn -0.308041 -0.927864 0.210190 +vn -0.329400 -0.923914 0.194626 +vn -0.204784 -0.965387 0.161529 +vn -0.068907 -0.986607 0.147847 +vn 0.702926 -0.711047 0.017524 +vn 0.700835 0.687586 -0.189883 +vn 0.069750 0.985420 -0.155188 +vn 0.209274 0.964095 -0.163481 +vn 0.335981 0.925650 -0.174037 +vn 0.311741 0.930325 -0.193165 +vn 0.149069 0.969065 -0.196701 +vn -0.021008 0.984904 -0.171823 +vn -0.189856 0.970498 -0.148620 +vn -0.334154 0.933955 -0.126767 +vn -0.317712 0.944096 -0.087989 +vn -0.788491 0.613893 0.037649 +vn -0.613516 -0.786143 0.074682 +vn 0.303138 -0.952222 -0.037151 +vn 0.317442 -0.947484 -0.038790 +vn 0.171703 -0.984929 -0.020833 +vn -0.000031 -1.000000 0.000039 +vn -0.171686 -0.984928 0.020991 +vn -0.320490 -0.946391 0.040385 +vn -0.316495 -0.946991 0.055123 +vn -0.169818 -0.982385 0.077982 +vn -0.041434 -0.992752 0.112813 +vn 0.707821 -0.706351 0.007588 +vn 0.695169 0.693527 -0.189104 +vn 0.039320 0.988596 -0.145367 +vn 0.166982 0.977636 -0.127844 +vn 0.313375 0.945008 -0.093569 +vn 0.319572 0.945981 -0.054709 +vn 0.171637 0.984904 -0.022475 +vn -0.000043 1.000000 -0.000034 +vn -0.171841 0.984904 0.020860 +vn -0.318278 0.947194 0.039025 +vn -0.305022 0.951604 0.037580 +vn -0.781929 0.616023 0.095404 +vn -0.641295 -0.765086 0.058177 +vn 0.228429 -0.969783 -0.085684 +vn 0.261573 -0.951515 -0.161862 +vn 0.151503 -0.969324 -0.193539 +vn -0.021001 -0.984928 -0.171682 +vn -0.193093 -0.969929 -0.148167 +vn -0.296211 -0.945467 -0.135469 +vn -0.250965 -0.963185 -0.096388 +vn -0.112666 -0.993312 -0.025260 +vn -0.018339 -0.999200 0.035536 +vn 0.705864 -0.707020 -0.043341 +vn 0.697463 0.703445 -0.136787 +vn 0.014976 0.997486 -0.069265 +vn 0.106697 0.993220 -0.046144 +vn 0.246332 0.969140 0.009378 +vn 0.293675 0.951714 0.089414 +vn 0.192035 0.969807 0.150325 +vn 0.020851 0.984903 0.171847 +vn -0.150426 0.970143 0.190246 +vn -0.256521 0.945341 0.201317 +vn -0.222547 0.962215 0.156895 +vn -0.754377 0.638420 0.152757 +vn -0.675241 -0.734843 0.063685 +vn 0.118792 -0.989218 -0.085657 +vn 0.157385 -0.966855 -0.201053 +vn 0.094325 -0.951515 -0.292784 +vn -0.038560 -0.947484 -0.317470 +vn -0.165018 -0.939656 -0.299693 +vn -0.209409 -0.944376 -0.253579 +vn -0.142467 -0.975746 -0.166202 +vn -0.038468 -0.997398 -0.060972 +vn 0.011450 -0.999861 0.012149 +vn 0.708895 -0.702842 -0.059000 +vn 0.693587 0.709641 -0.123879 +vn -0.016267 0.998809 -0.046007 +vn 0.030011 0.999395 -0.017564 +vn 0.134792 0.989619 0.049859 +vn 0.203306 0.966094 0.159148 +vn 0.160941 0.951269 0.263029 +vn 0.038904 0.947163 0.318386 +vn -0.086977 0.939483 0.331370 +vn -0.143412 0.943787 0.297824 +vn -0.107136 0.974403 0.197637 +vn -0.721435 0.671438 0.169420 +vn -0.676722 -0.726492 0.119404 +vn 0.044928 -0.998382 0.034851 +vn 0.017094 -0.998543 -0.051192 +vn 0.002788 -0.978146 -0.207902 +vn -0.039266 -0.953079 -0.300165 +vn -0.092341 -0.950757 -0.295861 +vn -0.087286 -0.969736 -0.228021 +vn -0.009467 -0.992899 -0.118585 +vn 0.070787 -0.997491 0.000420 +vn 0.077588 -0.991887 0.100702 +vn 0.729717 -0.683588 0.014874 +vn 0.674057 0.707296 -0.213024 +vn -0.084884 0.983981 -0.156769 +vn -0.083368 0.991253 -0.102311 +vn -0.005273 0.999943 -0.009270 +vn 0.076243 0.990585 0.113699 +vn 0.087305 0.968659 0.232545 +vn 0.040110 0.951586 0.304755 +vn 0.010646 0.952089 0.305635 +vn 0.004997 0.977760 0.209667 +vn -0.034190 0.997958 0.053954 +vn -0.729579 0.680585 0.067219 +vn -0.635658 -0.727218 0.259024 +vn 0.039333 -0.961732 0.271154 +vn -0.137465 -0.957450 0.253757 +vn -0.129258 -0.989870 0.058725 +vn -0.046866 -0.992288 -0.114753 +vn -0.025705 -0.988873 -0.146527 +vn 0.025003 -0.994393 -0.102745 +vn 0.136203 -0.990513 -0.018254 +vn 0.214018 -0.971281 0.103967 +vn 0.172435 -0.955708 0.238511 +vn 0.755635 -0.643259 0.123425 +vn 0.644344 0.685570 -0.338843 +vn -0.176109 0.933268 -0.313044 +vn -0.225667 0.945862 -0.233280 +vn -0.154735 0.981077 -0.116382 +vn -0.037547 0.999278 0.005775 +vn 0.020768 0.994831 0.099400 +vn 0.052134 0.987864 0.146311 +vn 0.157049 0.982054 0.104429 +vn 0.172396 0.982522 -0.070214 +vn -0.031789 0.969133 -0.244479 +vn -0.774993 0.621770 -0.113085 +vn -0.614110 -0.734644 0.288386 +vn 0.037162 -0.955783 0.291716 +vn -0.247938 -0.917069 0.312268 +vn -0.256379 -0.946882 0.194126 +vn -0.076923 -0.996657 0.027514 +vn 0.014613 -0.999399 -0.031445 +vn 0.112773 -0.993474 -0.017084 +vn 0.265926 -0.963190 0.039342 +vn 0.323267 -0.933484 0.155264 +vn 0.220989 -0.934749 0.278224 +vn 0.762125 -0.632263 0.139315 +vn 0.638094 0.689687 -0.342297 +vn -0.221099 0.921316 -0.319831 +vn -0.329283 0.908367 -0.257764 +vn -0.280638 0.945862 -0.163057 +vn -0.124206 0.990379 -0.061016 +vn -0.018246 0.999820 0.005245 +vn 0.086584 0.996041 0.020138 +vn 0.286958 0.956546 -0.051730 +vn 0.272502 0.939816 -0.206126 +vn -0.033691 0.955810 -0.292048 +vn -0.786435 0.608576 -0.105618 +vn -0.643540 -0.761356 0.078699 +vn -0.000319 -1.000000 0.000408 +vn -0.292106 -0.955783 0.033964 +vn -0.280714 -0.959253 0.032134 +vn -0.079081 -0.996824 0.009362 +vn 0.032279 -0.999471 -0.003932 +vn 0.163733 -0.986486 -0.006001 +vn 0.337538 -0.941095 0.020224 +vn 0.360082 -0.927949 0.096186 +vn 0.208215 -0.963843 0.166293 +vn 0.748825 -0.661451 0.041746 +vn 0.657609 0.721687 -0.216142 +vn -0.207816 0.964545 -0.162679 +vn -0.363560 0.921316 -0.137843 +vn -0.346170 0.933268 -0.095798 +vn -0.172563 0.983981 -0.044762 +vn -0.035218 0.999322 -0.010758 +vn 0.082937 0.996504 -0.010097 +vn 0.288978 0.956719 -0.034363 +vn 0.296553 0.954354 -0.035559 +vn -0.000565 1.000000 -0.000443 +vn -0.765765 0.636361 0.092994 +vn -0.472038 -0.643086 0.603009 +vn 0.078113 -0.761356 0.643611 +vn -0.132506 -0.734644 0.665387 +vn -0.098830 -0.727218 0.679254 +vn 0.041474 -0.719840 0.692899 +vn 0.112202 -0.706581 0.698680 +vn 0.209777 -0.690552 0.692194 +vn 0.328519 -0.648610 0.686572 +vn 0.327923 -0.633767 0.700575 +vn 0.212216 -0.658508 0.722033 +vn 0.658530 -0.542480 0.521588 +vn -0.541870 0.502924 0.673383 +vn 0.093119 0.634566 0.767239 +vn 0.276022 0.628194 0.727451 +vn 0.257877 0.667480 0.698549 +vn 0.129423 0.704689 0.697613 +vn 0.057278 0.709991 0.701878 +vn -0.044956 0.707296 0.705487 +vn -0.174229 0.685570 0.706851 +vn -0.179082 0.689687 0.701613 +vn -0.051932 0.721687 0.690268 +vn 0.626124 0.606414 0.490133 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +vt 0.000000 0.000000 +g Cube +usemtl Default +f 647/635/647 636/636/636 635/637/635 646/638/646 +f 648/639/648 637/640/637 636/636/636 647/635/647 +f 649/641/649 638/642/638 637/640/637 648/639/648 +f 650/643/650 639/644/639 638/642/638 649/641/649 +f 651/645/651 640/646/640 639/644/639 650/643/650 +f 652/647/652 641/648/641 640/646/640 651/645/651 +f 653/649/653 642/650/642 641/648/641 652/647/652 +f 654/651/654 643/652/643 642/650/642 653/649/653 +f 655/653/655 644/654/644 643/652/643 654/651/654 +f 656/655/656 645/656/645 644/654/644 655/653/655 +f 658/657/658 657/658/657 635/637/635 636/636/636 +f 659/659/659 658/657/658 636/636/636 637/640/637 +f 660/660/660 659/659/659 637/640/637 638/642/638 +f 661/661/661 660/660/660 638/642/638 639/644/639 +f 662/662/662 661/661/661 639/644/639 640/646/640 +f 663/663/663 662/662/662 640/646/640 641/648/641 +f 664/664/664 663/663/663 641/648/641 642/650/642 +f 665/665/665 664/664/664 642/650/642 643/652/643 +f 666/666/666 665/665/665 643/652/643 644/654/644 +f 667/667/667 666/666/666 644/654/644 645/656/645 +f 668/668/668 667/667/667 645/656/645 656/655/656 +f 669/669/669 668/668/668 656/655/656 655/653/655 +f 670/670/670 669/669/669 655/653/655 654/651/654 +f 671/671/671 670/670/670 654/651/654 653/649/653 +f 672/672/672 671/671/671 653/649/653 652/647/652 +f 673/673/673 672/672/672 652/647/652 651/645/651 +f 674/674/674 673/673/673 651/645/651 650/643/650 +f 675/675/675 674/674/674 650/643/650 649/641/649 +f 676/676/676 675/675/675 649/641/649 648/639/648 +f 677/677/677 676/676/676 648/639/648 647/635/647 +f 678/678/678 677/677/677 647/635/647 646/638/646 +f 657/658/657 678/678/678 646/638/646 635/637/635 +f 680/679/680 679/680/679 657/658/657 658/657/658 +f 681/681/681 680/679/680 658/657/658 659/659/659 +f 682/682/682 681/681/681 659/659/659 660/660/660 +f 683/683/683 682/682/682 660/660/660 661/661/661 +f 684/684/684 683/683/683 661/661/661 662/662/662 +f 685/685/685 684/684/684 662/662/662 663/663/663 +f 686/686/686 685/685/685 663/663/663 664/664/664 +f 687/687/687 686/686/686 664/664/664 665/665/665 +f 688/688/688 687/687/687 665/665/665 666/666/666 +f 689/689/689 688/688/688 666/666/666 667/667/667 +f 690/690/690 689/689/689 667/667/667 668/668/668 +f 691/691/691 690/690/690 668/668/668 669/669/669 +f 692/692/692 691/691/691 669/669/669 670/670/670 +f 693/693/693 692/692/692 670/670/670 671/671/671 +f 694/694/694 693/693/693 671/671/671 672/672/672 +f 695/695/695 694/694/694 672/672/672 673/673/673 +f 696/696/696 695/695/695 673/673/673 674/674/674 +f 697/697/697 696/696/696 674/674/674 675/675/675 +f 698/698/698 697/697/697 675/675/675 676/676/676 +f 699/699/699 698/698/698 676/676/676 677/677/677 +f 700/700/700 699/699/699 677/677/677 678/678/678 +f 679/680/679 700/700/700 678/678/678 657/658/657 +f 702/701/702 701/702/701 679/680/679 680/679/680 +f 703/703/703 702/701/702 680/679/680 681/681/681 +f 704/704/704 703/703/703 681/681/681 682/682/682 +f 705/705/705 704/704/704 682/682/682 683/683/683 +f 706/706/706 705/705/705 683/683/683 684/684/684 +f 707/707/707 706/706/706 684/684/684 685/685/685 +f 708/708/708 707/707/707 685/685/685 686/686/686 +f 709/709/709 708/708/708 686/686/686 687/687/687 +f 710/710/710 709/709/709 687/687/687 688/688/688 +f 711/711/711 710/710/710 688/688/688 689/689/689 +f 712/712/712 711/711/711 689/689/689 690/690/690 +f 713/713/713 712/712/712 690/690/690 691/691/691 +f 714/714/714 713/713/713 691/691/691 692/692/692 +f 715/715/715 714/714/714 692/692/692 693/693/693 +f 716/716/716 715/715/715 693/693/693 694/694/694 +f 717/717/717 716/716/716 694/694/694 695/695/695 +f 718/718/718 717/717/717 695/695/695 696/696/696 +f 719/719/719 718/718/718 696/696/696 697/697/697 +f 720/720/720 719/719/719 697/697/697 698/698/698 +f 721/721/721 720/720/720 698/698/698 699/699/699 +f 722/722/722 721/721/721 699/699/699 700/700/700 +f 701/702/701 722/722/722 700/700/700 679/680/679 +f 724/723/724 723/724/723 701/702/701 702/701/702 +f 725/725/725 724/723/724 702/701/702 703/703/703 +f 726/726/726 725/725/725 703/703/703 704/704/704 +f 727/727/727 726/726/726 704/704/704 705/705/705 +f 728/728/728 727/727/727 705/705/705 706/706/706 +f 729/729/729 728/728/728 706/706/706 707/707/707 +f 730/730/730 729/729/729 707/707/707 708/708/708 +f 731/731/731 730/730/730 708/708/708 709/709/709 +f 732/732/732 731/731/731 709/709/709 710/710/710 +f 733/733/733 732/732/732 710/710/710 711/711/711 +f 734/734/734 733/733/733 711/711/711 712/712/712 +f 735/735/735 734/734/734 712/712/712 713/713/713 +f 736/736/736 735/735/735 713/713/713 714/714/714 +f 737/737/737 736/736/736 714/714/714 715/715/715 +f 738/738/738 737/737/737 715/715/715 716/716/716 +f 739/739/739 738/738/738 716/716/716 717/717/717 +f 740/740/740 739/739/739 717/717/717 718/718/718 +f 741/741/741 740/740/740 718/718/718 719/719/719 +f 742/742/742 741/741/741 719/719/719 720/720/720 +f 743/743/743 742/742/742 720/720/720 721/721/721 +f 744/744/744 743/743/743 721/721/721 722/722/722 +f 723/724/723 744/744/744 722/722/722 701/702/701 +f 746/745/746 745/746/745 723/724/723 724/723/724 +f 747/747/747 746/745/746 724/723/724 725/725/725 +f 748/748/748 747/747/747 725/725/725 726/726/726 +f 749/749/749 748/748/748 726/726/726 727/727/727 +f 750/750/750 749/749/749 727/727/727 728/728/728 +f 751/751/751 750/750/750 728/728/728 729/729/729 +f 752/752/752 751/751/751 729/729/729 730/730/730 +f 753/753/753 752/752/752 730/730/730 731/731/731 +f 754/754/754 753/753/753 731/731/731 732/732/732 +f 755/755/755 754/754/754 732/732/732 733/733/733 +f 756/756/756 755/755/755 733/733/733 734/734/734 +f 757/757/757 756/756/756 734/734/734 735/735/735 +f 758/758/758 757/757/757 735/735/735 736/736/736 +f 759/759/759 758/758/758 736/736/736 737/737/737 +f 760/760/760 759/759/759 737/737/737 738/738/738 +f 761/761/761 760/760/760 738/738/738 739/739/739 +f 762/762/762 761/761/761 739/739/739 740/740/740 +f 763/763/763 762/762/762 740/740/740 741/741/741 +f 764/764/764 763/763/763 741/741/741 742/742/742 +f 765/765/765 764/764/764 742/742/742 743/743/743 +f 766/766/766 765/765/765 743/743/743 744/744/744 +f 745/746/745 766/766/766 744/744/744 723/724/723 +f 768/767/768 767/768/767 745/746/745 746/745/746 +f 769/769/769 768/767/768 746/745/746 747/747/747 +f 770/770/770 769/769/769 747/747/747 748/748/748 +f 771/771/771 770/770/770 748/748/748 749/749/749 +f 772/772/772 771/771/771 749/749/749 750/750/750 +f 773/773/773 772/772/772 750/750/750 751/751/751 +f 774/774/774 773/773/773 751/751/751 752/752/752 +f 775/775/775 774/774/774 752/752/752 753/753/753 +f 776/776/776 775/775/775 753/753/753 754/754/754 +f 777/777/777 776/776/776 754/754/754 755/755/755 +f 778/778/778 777/777/777 755/755/755 756/756/756 +f 779/779/779 778/778/778 756/756/756 757/757/757 +f 780/780/780 779/779/779 757/757/757 758/758/758 +f 781/781/781 780/780/780 758/758/758 759/759/759 +f 782/782/782 781/781/781 759/759/759 760/760/760 +f 783/783/783 782/782/782 760/760/760 761/761/761 +f 784/784/784 783/783/783 761/761/761 762/762/762 +f 785/785/785 784/784/784 762/762/762 763/763/763 +f 786/786/786 785/785/785 763/763/763 764/764/764 +f 787/787/787 786/786/786 764/764/764 765/765/765 +f 788/788/788 787/787/787 765/765/765 766/766/766 +f 767/768/767 788/788/788 766/766/766 745/746/745 +f 790/789/790 789/790/789 767/768/767 768/767/768 +f 791/791/791 790/789/790 768/767/768 769/769/769 +f 792/792/792 791/791/791 769/769/769 770/770/770 +f 793/793/793 792/792/792 770/770/770 771/771/771 +f 794/794/794 793/793/793 771/771/771 772/772/772 +f 795/795/795 794/794/794 772/772/772 773/773/773 +f 796/796/796 795/795/795 773/773/773 774/774/774 +f 797/797/797 796/796/796 774/774/774 775/775/775 +f 798/798/798 797/797/797 775/775/775 776/776/776 +f 799/799/799 798/798/798 776/776/776 777/777/777 +f 800/800/800 799/799/799 777/777/777 778/778/778 +f 801/801/801 800/800/800 778/778/778 779/779/779 +f 802/802/802 801/801/801 779/779/779 780/780/780 +f 803/803/803 802/802/802 780/780/780 781/781/781 +f 804/804/804 803/803/803 781/781/781 782/782/782 +f 805/805/805 804/804/804 782/782/782 783/783/783 +f 806/806/806 805/805/805 783/783/783 784/784/784 +f 807/807/807 806/806/806 784/784/784 785/785/785 +f 808/808/808 807/807/807 785/785/785 786/786/786 +f 809/809/809 808/808/808 786/786/786 787/787/787 +f 810/810/810 809/809/809 787/787/787 788/788/788 +f 789/790/789 810/810/810 788/788/788 767/768/767 +f 812/811/812 811/812/811 789/790/789 790/789/790 +f 813/813/813 812/811/812 790/789/790 791/791/791 +f 814/814/814 813/813/813 791/791/791 792/792/792 +f 815/815/815 814/814/814 792/792/792 793/793/793 +f 816/816/816 815/815/815 793/793/793 794/794/794 +f 817/817/817 816/816/816 794/794/794 795/795/795 +f 818/818/818 817/817/817 795/795/795 796/796/796 +f 819/819/819 818/818/818 796/796/796 797/797/797 +f 820/820/820 819/819/819 797/797/797 798/798/798 +f 821/821/821 820/820/820 798/798/798 799/799/799 +f 822/822/822 821/821/821 799/799/799 800/800/800 +f 823/823/823 822/822/822 800/800/800 801/801/801 +f 824/824/824 823/823/823 801/801/801 802/802/802 +f 825/825/825 824/824/824 802/802/802 803/803/803 +f 826/826/826 825/825/825 803/803/803 804/804/804 +f 827/827/827 826/826/826 804/804/804 805/805/805 +f 828/828/828 827/827/827 805/805/805 806/806/806 +f 829/829/829 828/828/828 806/806/806 807/807/807 +f 830/830/830 829/829/829 807/807/807 808/808/808 +f 831/831/831 830/830/830 808/808/808 809/809/809 +f 832/832/832 831/831/831 809/809/809 810/810/810 +f 811/812/811 832/832/832 810/810/810 789/790/789 +f 834/833/834 833/834/833 811/812/811 812/811/812 +f 835/835/835 834/833/834 812/811/812 813/813/813 +f 836/836/836 835/835/835 813/813/813 814/814/814 +f 837/837/837 836/836/836 814/814/814 815/815/815 +f 838/838/838 837/837/837 815/815/815 816/816/816 +f 839/839/839 838/838/838 816/816/816 817/817/817 +f 840/840/840 839/839/839 817/817/817 818/818/818 +f 841/841/841 840/840/840 818/818/818 819/819/819 +f 842/842/842 841/841/841 819/819/819 820/820/820 +f 843/843/843 842/842/842 820/820/820 821/821/821 +f 844/844/844 843/843/843 821/821/821 822/822/822 +f 845/845/845 844/844/844 822/822/822 823/823/823 +f 846/846/846 845/845/845 823/823/823 824/824/824 +f 847/847/847 846/846/846 824/824/824 825/825/825 +f 848/848/848 847/847/847 825/825/825 826/826/826 +f 849/849/849 848/848/848 826/826/826 827/827/827 +f 850/850/850 849/849/849 827/827/827 828/828/828 +f 851/851/851 850/850/850 828/828/828 829/829/829 +f 852/852/852 851/851/851 829/829/829 830/830/830 +f 853/853/853 852/852/852 830/830/830 831/831/831 +f 854/854/854 853/853/853 831/831/831 832/832/832 +f 833/834/833 854/854/854 832/832/832 811/812/811 +f 856/855/856 867/856/867 866/857/866 855/858/855 +f 857/859/857 868/860/868 867/856/867 856/855/856 +f 858/861/858 869/862/869 868/860/868 857/859/857 +f 859/863/859 870/864/870 869/862/869 858/861/858 +f 860/865/860 871/866/871 870/864/870 859/863/859 +f 861/867/861 872/868/872 871/866/871 860/865/860 +f 862/869/862 873/870/873 872/868/872 861/867/861 +f 863/871/863 874/872/874 873/870/873 862/869/862 +f 864/873/864 875/874/875 874/872/874 863/871/863 +f 865/875/865 876/876/876 875/874/875 864/873/864 +f 856/855/856 855/858/855 833/834/833 834/833/834 +f 857/859/857 856/855/856 834/833/834 835/835/835 +f 858/861/858 857/859/857 835/835/835 836/836/836 +f 859/863/859 858/861/858 836/836/836 837/837/837 +f 860/865/860 859/863/859 837/837/837 838/838/838 +f 861/867/861 860/865/860 838/838/838 839/839/839 +f 862/869/862 861/867/861 839/839/839 840/840/840 +f 863/871/863 862/869/862 840/840/840 841/841/841 +f 864/873/864 863/871/863 841/841/841 842/842/842 +f 865/875/865 864/873/864 842/842/842 843/843/843 +f 876/876/876 865/875/865 843/843/843 844/844/844 +f 875/874/875 876/876/876 844/844/844 845/845/845 +f 874/872/874 875/874/875 845/845/845 846/846/846 +f 873/870/873 874/872/874 846/846/846 847/847/847 +f 872/868/872 873/870/873 847/847/847 848/848/848 +f 871/866/871 872/868/872 848/848/848 849/849/849 +f 870/864/870 871/866/871 849/849/849 850/850/850 +f 869/862/869 870/864/870 850/850/850 851/851/851 +f 868/860/868 869/862/869 851/851/851 852/852/852 +f 867/856/867 868/860/868 852/852/852 853/853/853 +f 866/857/866 867/856/867 853/853/853 854/854/854 +f 855/858/855 866/857/866 854/854/854 833/834/833 +v 9.795984 0.567013 -8.397772 +v 9.858484 0.567013 -8.397772 +v 9.795984 3.692013 -8.397772 +v 9.858484 3.692013 -8.397772 +v 9.795984 0.567013 -8.335272 +v 9.858484 0.567013 -8.335272 +v 9.795984 3.692013 -8.335272 +v 9.858484 3.692013 -8.335272 +vn -0.577350 -0.577350 -0.577350 +vn 0.577350 -0.577350 -0.577350 +vn -0.577350 0.577350 -0.577350 +vn 0.577350 0.577350 -0.577350 +vn -0.577350 -0.577350 0.577350 +vn 0.577350 -0.577350 0.577350 +vn -0.577350 0.577350 0.577350 +vn 0.577350 0.577350 0.577350 +vt 0.995049 0.995049 +vt 0.995049 0.995049 +vt 0.004950 0.995049 +vt 0.004950 0.995049 +vt 0.995049 0.004950 +vt 0.995049 0.004950 +vt 0.004950 0.004950 +vt 0.004950 0.004950 +g Cube +usemtl Default +f 880/877/880 878/878/878 877/879/877 879/880/879 +f 882/881/882 884/882/884 883/883/883 881/884/881 +f 882/881/882 881/884/881 877/879/877 878/878/878 +f 884/882/884 882/881/882 878/878/878 880/877/880 +f 883/883/883 884/882/884 880/877/880 879/880/879 +f 881/884/881 883/883/883 879/880/879 877/879/877 diff --git a/resources/bridge.obj b/resources/bridge.obj new file mode 100644 index 0000000..a30fee0 --- /dev/null +++ b/resources/bridge.obj @@ -0,0 +1,87 @@ +# Blender v2.83.5 OBJ File: '' +# www.blender.org +o Cube.001 +v -2.087493 4.416752 -12.092585 +v 2.181665 4.416749 10.462940 +v 2.181665 4.416752 -12.092585 +v -0.524636 -0.804926 20.192308 +v 0.542654 -0.804926 20.192308 +v -2.087493 3.565045 -0.287136 +v -2.087493 4.416749 10.462940 +v 2.181665 3.651016 9.952583 +v 1.114376 3.651016 9.952583 +v 2.181665 3.565045 -0.287136 +v 0.047086 -0.780343 -20.766577 +v -1.020204 -0.780343 -20.766577 +v 0.047086 3.651016 9.952583 +v -1.591925 -0.804926 20.192308 +v -1.020204 3.651016 9.952583 +v 0.047086 3.684258 -11.824307 +v -1.020204 3.565045 -0.287136 +v -1.020204 3.684258 -11.824307 +v 2.181665 3.684258 -11.824307 +v 1.114376 3.565045 -0.287136 +v 1.114376 3.684258 -11.824307 +v 0.047086 3.565045 -0.287136 +v 1.114376 -0.780343 -20.766577 +v 2.181665 -0.780343 -20.766577 +v -2.087493 3.684258 -11.824307 +v -2.087493 -0.780343 -20.766577 +v -2.659215 -0.804926 20.192308 +v -2.087493 3.651016 9.952583 +v 1.609943 -0.804926 20.192308 +s 1 +f 1 2 3 +f 4 5 2 +f 1 6 7 +f 8 5 9 +f 2 10 3 +f 11 12 1 +f 13 14 15 +f 16 17 18 +f 19 20 21 +f 21 22 16 +f 23 16 11 +f 24 21 23 +f 18 6 25 +f 12 25 26 +f 11 18 12 +f 15 27 28 +f 17 28 6 +f 22 15 17 +f 9 4 13 +f 20 13 22 +f 10 9 20 +f 1 7 2 +f 5 29 2 +f 2 7 4 +f 7 27 14 +f 14 4 7 +f 28 27 7 +f 1 26 25 +f 25 6 1 +f 6 28 7 +f 8 29 5 +f 19 24 3 +f 2 29 8 +f 8 10 2 +f 10 19 3 +f 12 26 1 +f 1 3 11 +f 3 24 23 +f 23 11 3 +f 13 4 14 +f 16 22 17 +f 19 10 20 +f 21 20 22 +f 23 21 16 +f 24 19 21 +f 18 17 6 +f 12 18 25 +f 11 16 18 +f 15 14 27 +f 17 15 28 +f 22 13 15 +f 9 5 4 +f 20 9 13 +f 10 8 9 diff --git a/resources/convex.obj b/resources/convex.obj new file mode 100644 index 0000000..10acc62 --- /dev/null +++ b/resources/convex.obj @@ -0,0 +1,408 @@ +# Blender v2.83.5 OBJ File: 'convex.blend' +# www.blender.org +o Cube +v 1.305899 2.880516 2.691374 +v 1.305899 2.499842 3.072048 +v 0.381956 2.894057 3.085589 +v 3.079623 2.430736 1.226648 +v 2.701850 2.830478 1.234663 +v 3.056046 2.798204 0.295109 +v 2.915617 -3.144600 -0.158337 +v 2.915617 -2.758400 -1.090707 +v 2.529417 -3.144600 -1.090707 +v 2.529417 -1.282239 -2.953068 +v 2.915617 -1.282239 -2.566868 +v 2.915617 -0.349869 -2.953068 +v 1.053257 -3.144600 -2.566868 +v 1.053257 -2.758400 -2.953068 +v 0.120887 -3.144600 -2.953068 +v 2.915617 -0.108101 -2.953068 +v 2.932010 0.830836 -2.573903 +v 2.529417 0.824269 -2.953068 +v 2.618234 2.750781 -1.085477 +v 2.995637 2.357680 -1.093115 +v 3.036258 2.782483 -0.162588 +v 1.053257 2.300430 -2.953068 +v 1.061504 2.699598 -2.575518 +v 0.120887 2.686630 -2.953068 +v 0.120887 2.686630 -2.953068 +v 1.061504 2.699598 -2.575518 +v 1.053257 2.300430 -2.953068 +v 3.036258 2.782483 -0.162588 +v 2.996289 2.370983 -1.087093 +v 2.618234 2.750781 -1.085477 +v 2.529417 0.824269 -2.953068 +v 2.932010 0.830836 -2.573903 +v 2.915617 -0.108101 -2.953068 +v 0.120887 -3.144600 -2.953068 +v 1.053257 -2.758400 -2.953068 +v 1.053257 -3.144600 -2.566868 +v 2.915617 -0.349869 -2.953068 +v 2.915617 -1.282239 -2.566868 +v 2.529417 -1.282239 -2.953068 +v 2.529417 -3.144600 -1.090707 +v 2.915617 -2.758400 -1.090707 +v 2.915617 -3.144600 -0.158337 +v 3.056046 2.798204 0.295109 +v 2.701850 2.830478 1.234663 +v 3.079623 2.430736 1.226648 +v 0.381956 2.894057 3.085589 +v 1.305899 2.499842 3.072048 +v 1.305899 2.880516 2.691374 +v 3.079623 1.035117 2.622268 +v 2.701850 1.043132 3.022010 +v 3.056046 0.103578 2.989736 +v 3.036258 -0.354120 2.974014 +v 2.618234 -1.277009 2.942313 +v 2.996289 -1.278624 2.562515 +v 0.120887 -3.144600 2.878162 +v 1.053257 -3.144600 2.491961 +v 1.061504 -2.767050 2.891130 +v 2.932010 -2.765435 1.022368 +v 2.529417 -3.144600 1.015801 +v 2.915617 -3.144600 0.083431 +v -2.915612 -0.108101 -2.953068 +v -2.932004 0.830836 -2.573903 +v -2.529412 0.824269 -2.953068 +v -3.036253 2.782483 -0.162588 +v -2.618229 2.750781 -1.085477 +v -2.996284 2.370983 -1.087093 +v -1.053251 2.300430 -2.953068 +v -1.061499 2.699598 -2.575518 +v -0.120881 2.686630 -2.953068 +v -2.915612 -0.349869 -2.953068 +v -2.529412 -1.282239 -2.953068 +v -2.915612 -1.282239 -2.566868 +v -0.120881 -3.144600 -2.953068 +v -1.053251 -3.144600 -2.566868 +v -1.053251 -2.758400 -2.953068 +v -2.915612 -2.758400 -1.090707 +v -2.529412 -3.144600 -1.090707 +v -2.915612 -3.144600 -0.158337 +v -3.056041 0.103578 2.989736 +v -2.701844 1.043132 3.022010 +v -3.079617 1.035117 2.622268 +v -0.381951 2.894057 3.085589 +v -1.305893 2.880516 2.691374 +v -1.305893 2.499842 3.072048 +v -3.079617 2.430736 1.226648 +v -2.701844 2.830478 1.234663 +v -3.056041 2.798204 0.295109 +v -0.120881 -3.144600 2.878162 +v -1.061499 -2.767050 2.891130 +v -1.053251 -3.144600 2.491961 +v -3.036253 -0.354120 2.974014 +v -2.996284 -1.278624 2.562515 +v -2.618229 -1.277009 2.942313 +v -2.529412 -3.144600 1.015801 +v -2.932004 -2.765435 1.022368 +v -2.915612 -3.144600 0.083431 +v 3.079623 1.035117 2.622268 +v 2.701850 1.043132 3.022010 +v 3.056046 0.103578 2.989736 +v 3.036258 -0.354120 2.974014 +v 2.618234 -1.277009 2.942313 +v 2.996289 -1.278624 2.562515 +v 0.120887 -3.144600 2.878162 +v 1.053257 -3.144600 2.491961 +v 1.061504 -2.767050 2.891130 +v 2.932010 -2.765435 1.022368 +v 2.529417 -3.144600 1.015801 +v 2.915617 -3.144600 0.083431 +v -2.915612 -0.108101 -2.953068 +v -2.932004 0.830836 -2.573903 +v -2.529412 0.824269 -2.953068 +v -3.036253 2.782483 -0.162588 +v -2.618229 2.750781 -1.085477 +v -2.996284 2.370983 -1.087093 +v -1.053251 2.300430 -2.953068 +v -1.061499 2.699598 -2.575518 +v -0.120881 2.686630 -2.953068 +v -2.915612 -0.349869 -2.953068 +v -2.529412 -1.282239 -2.953068 +v -2.915612 -1.282239 -2.566868 +v -0.120881 -3.144600 -2.953068 +v -1.053251 -3.144600 -2.566868 +v -1.053251 -2.758400 -2.953068 +v -2.915612 -2.758400 -1.090707 +v -2.529412 -3.144600 -1.090707 +v -2.915612 -3.144600 -0.158337 +v -3.056041 0.103578 2.989736 +v -2.701844 1.043132 3.022010 +v -3.079617 1.035117 2.622268 +v -0.381951 2.894057 3.085589 +v -1.305893 2.880516 2.691374 +v -1.305893 2.499842 3.072048 +v -3.079617 2.430736 1.226648 +v -2.701844 2.830478 1.234663 +v -3.056041 2.798204 0.295109 +v -0.120881 -3.144600 2.878162 +v -1.061499 -2.767050 2.891130 +v -1.053251 -3.144600 2.491961 +v -3.036253 -0.354120 2.974014 +v -2.996284 -1.278624 2.562515 +v -2.618229 -1.277009 2.942313 +v -2.529412 -3.144600 1.015801 +v -2.932004 -2.765435 1.022368 +v -2.915612 -3.144600 0.083431 +v 0.591196 2.693114 -2.764293 +v 2.827246 2.766632 -0.624033 +v 2.878948 2.814341 0.764886 +v 0.843927 2.887286 2.888481 +v -2.827241 2.766632 -0.624033 +v -0.591190 2.693114 -2.764293 +v -0.843922 2.887286 2.888481 +v -2.878943 2.814341 0.764886 +v 0.000003 2.686630 -2.953068 +v 1.839869 2.725190 -1.830498 +v 3.046152 2.790344 0.066260 +v -1.839864 2.725190 -1.830498 +v -3.046147 2.790344 0.066260 +v -2.003869 2.855497 1.963019 +v 0.000003 2.894057 3.085589 +v 2.003874 2.855497 1.963019 +s off +f 50 49 160 +f 74 77 76 +f 58 57 59 +f 41 32 58 +f 35 38 40 +f 156 160 154 +f 36 94 77 +f 53 84 93 +f 156 68 67 +f 76 95 62 +f 158 86 85 +f 95 94 89 +f 32 31 154 +f 145 27 25 +f 29 146 28 +f 31 32 33 +f 34 35 36 +f 37 38 39 +f 40 41 42 +f 147 45 43 +f 47 148 46 +f 49 50 51 +f 52 53 54 +f 55 56 57 +f 58 59 60 +f 61 62 63 +f 149 66 64 +f 67 150 69 +f 70 71 72 +f 73 74 75 +f 76 77 78 +f 79 80 81 +f 151 84 82 +f 85 152 87 +f 88 89 90 +f 91 92 93 +f 94 95 96 +f 35 75 31 +f 49 45 160 +f 45 44 160 +f 160 48 47 +f 47 50 160 +f 76 72 71 +f 71 75 76 +f 75 74 76 +f 58 54 53 +f 53 57 58 +f 57 56 59 +f 60 42 41 +f 41 38 32 +f 38 37 33 +f 32 38 33 +f 32 29 54 +f 29 28 155 +f 155 43 45 +f 29 155 45 +f 58 60 41 +f 52 54 49 +f 54 58 32 +f 49 54 29 +f 49 51 52 +f 45 49 29 +f 40 36 35 +f 35 39 38 +f 38 41 40 +f 155 28 146 +f 146 30 154 +f 154 26 145 +f 145 25 153 +f 153 69 150 +f 150 68 156 +f 156 65 149 +f 149 64 157 +f 157 87 152 +f 152 86 158 +f 158 83 151 +f 151 82 159 +f 159 46 148 +f 148 48 160 +f 160 44 147 +f 147 43 155 +f 155 146 147 +f 146 154 160 +f 154 145 150 +f 145 153 150 +f 150 156 154 +f 156 149 158 +f 149 157 152 +f 158 149 152 +f 158 151 160 +f 151 159 148 +f 160 151 148 +f 160 147 146 +f 156 158 160 +f 96 78 77 +f 77 74 36 +f 74 73 36 +f 73 34 36 +f 36 40 90 +f 40 42 59 +f 90 40 59 +f 42 60 59 +f 59 56 90 +f 56 55 90 +f 55 88 90 +f 90 94 36 +f 94 96 77 +f 88 55 57 +f 57 53 93 +f 53 52 50 +f 52 51 50 +f 50 47 53 +f 47 46 159 +f 159 82 84 +f 47 159 84 +f 84 53 47 +f 89 88 57 +f 91 93 80 +f 93 89 57 +f 80 93 84 +f 80 79 91 +f 67 63 156 +f 63 62 156 +f 62 66 156 +f 66 65 156 +f 78 96 76 +f 96 95 76 +f 95 92 66 +f 92 91 81 +f 91 79 81 +f 81 85 66 +f 85 87 157 +f 157 64 66 +f 85 157 66 +f 92 81 66 +f 70 72 61 +f 72 76 62 +f 62 61 72 +f 66 62 95 +f 85 81 158 +f 81 80 158 +f 80 84 158 +f 84 83 158 +f 94 90 89 +f 89 93 95 +f 93 92 95 +f 31 27 154 +f 27 26 154 +f 154 30 29 +f 32 154 29 +f 145 26 27 +f 29 30 146 +f 147 44 45 +f 47 48 148 +f 149 65 66 +f 67 68 150 +f 151 83 84 +f 85 86 152 +f 34 73 75 +f 75 71 63 +f 71 70 63 +f 70 61 63 +f 63 67 27 +f 67 69 153 +f 153 25 27 +f 67 153 27 +f 35 34 75 +f 37 39 31 +f 39 35 31 +f 31 33 37 +f 27 31 75 +f 75 63 27 +l 60 108 +l 59 107 +l 57 105 +l 54 102 +l 53 101 +l 51 99 +l 49 97 +l 48 1 +l 47 2 +l 44 5 +l 43 6 +l 42 7 +l 40 9 +l 39 10 +l 37 12 +l 35 14 +l 34 15 +l 33 16 +l 31 18 +l 30 19 +l 28 21 +l 27 22 +l 26 23 +l 25 24 +l 52 100 +l 38 11 +l 58 106 +l 55 103 +l 50 98 +l 56 104 +l 45 4 +l 36 13 +l 46 3 +l 32 17 +l 41 8 +l 29 20 +l 61 109 +l 62 110 +l 63 111 +l 64 112 +l 65 113 +l 66 114 +l 67 115 +l 68 116 +l 69 117 +l 70 118 +l 71 119 +l 72 120 +l 73 121 +l 74 122 +l 75 123 +l 76 124 +l 77 125 +l 78 126 +l 79 127 +l 80 128 +l 81 129 +l 82 130 +l 83 131 +l 84 132 +l 85 133 +l 86 134 +l 87 135 +l 88 136 +l 89 137 +l 90 138 +l 91 139 +l 92 140 +l 93 141 +l 94 142 +l 95 143 +l 96 144 diff --git a/resources/dungeon.obj b/resources/dungeon.obj new file mode 100644 index 0000000..4905542 --- /dev/null +++ b/resources/dungeon.obj @@ -0,0 +1,15234 @@ +v 32.471557617 31.175949097 3.788104773 +v 31.950048447 31.175949097 2.338263273 +v 31.688222885 31.175949097 0.819886208 +v 34.222843170 31.175949097 6.309397697 +v 33.236907959 31.175949097 5.125359535 +v 45.539302826 31.175949097 7.342514992 +v 39.693061829 31.175949097 8.885383606 +v 36.730842590 31.175949097 8.079663277 +v 35.399402618 31.175949097 7.304241180 +v 38.176704407 31.175949097 8.612103462 +v 48.521808624 31.175949097 -3.622624636 +v 49.026046753 31.175949097 2.402715683 +v 47.718185425 31.175949097 5.180017471 +v 46.723342896 31.175949097 6.356580257 +v 48.493606567 31.175949097 3.848578453 +v 49.305145264 31.175949097 -0.654410124 +v 49.299327850 31.175949097 0.886361718 +v 42.752204895 31.175949097 8.629374504 +v 41.233833313 31.175949097 8.891200066 +v 44.202045441 31.175949097 8.107864380 +v 49.043319702 31.175949097 -2.172783852 +v 38.241161346 31.175949097 -8.463897705 +v 44.262527466 31.175949097 -7.914186001 +v 46.770526886 31.175949097 -6.143918514 +v 47.756462097 31.175949097 -4.959880829 +v 45.593963623 31.175949097 -7.138762951 +v 41.300308228 31.175949097 -8.719906807 +v 42.816661835 31.175949097 -8.446626663 +v 31.694038391 31.175949097 -0.720889091 +v 31.967319489 31.175949097 -2.237243176 +v 33.275184631 31.175949097 -5.014545441 +v 32.499759674 31.175949097 -3.683105707 +v 35.454067230 31.175949097 -7.177039623 +v 36.791320801 31.175949097 -7.942388058 +v 34.270027161 31.175949097 -6.191106319 +v 39.759536743 31.175949097 -8.725723267 +v 31.788227081 9.998181343 3.466857910 +v 31.788227081 13.729361534 3.467402458 +v 32.028339386 13.729361534 3.467821598 +v 32.028339386 9.998181343 3.467277050 +v 32.028583527 13.729488373 3.327821970 +v 32.028583527 9.998181343 3.327277660 +v 31.788471222 9.998181343 3.326858521 +v 31.788471222 13.729488373 3.327402830 +v 31.788461685 16.147314072 3.332412243 +v 32.028575897 16.147314072 3.332831383 +v 32.028331757 16.147188187 3.472831011 +v 31.788217545 16.147188187 3.472411871 +v 31.736412048 16.147670746 2.534013748 +v 31.496299744 16.147670746 2.533594608 +v 31.496307373 14.267010689 2.528585196 +v 31.345476151 14.589399338 1.754027605 +v 31.345466614 16.147884369 1.759037018 +v 31.585578918 16.147884369 1.759456158 +v 31.585588455 14.589399338 1.754446745 +v 31.736419678 14.267010689 2.529004335 +v 31.213439941 14.785192490 0.919718027 +v 31.213432312 16.148014069 0.924727559 +v 31.453556061 14.785192490 0.920137167 +v 31.453544617 16.148014069 0.925146699 +v 31.166309357 16.148063660 0.074689411 +v 31.406433105 14.858482361 0.070098996 +v 31.406423569 16.148063660 0.075108476 +v 31.166316986 14.858482361 0.069679931 +v 31.800228119 9.998181343 -3.409051180 +v 31.800226212 13.749429703 -3.408686399 +v 31.799911499 13.749273300 -3.228687048 +v 31.799911499 9.998181343 -3.229051828 +v 32.040023804 9.998181343 -3.228632689 +v 32.040023804 13.749273300 -3.228267908 +v 32.040340424 13.749429703 -3.408267260 +v 32.040340424 9.998181343 -3.408632040 +v 31.216400146 14.795266151 -0.776435494 +v 31.216392517 16.148019791 -0.771425962 +v 31.456514359 14.795266151 -0.776016355 +v 31.456504822 16.148019791 -0.771006942 +v 31.591457367 14.590011597 -1.608003974 +v 31.591447830 16.147884369 -1.602994561 +v 31.351343155 14.590011597 -1.608423114 +v 31.351333618 16.147884369 -1.603413701 +v 32.040016174 16.147327423 -3.223258734 +v 31.799903870 16.147327423 -3.223677874 +v 31.800216675 16.147483826 -3.403676987 +v 32.040328979 16.147483826 -3.403257847 +v 31.745027542 16.147678375 -2.403040648 +v 31.504913330 16.147678375 -2.403459787 +v 31.745037079 14.277850151 -2.408050060 +v 31.504922867 14.277850151 -2.408469200 +v 46.721595764 24.693101883 6.354872704 +v 45.537879944 24.693101883 7.340530396 +v 47.716171265 24.693101883 5.178639412 +v 48.491386414 24.693101883 3.847570896 +v 46.722469330 23.774042130 6.355726242 +v 45.538589478 23.774042130 7.341522694 +v 47.717178345 23.774042130 5.179327965 +v 48.492496490 23.774042130 3.848074436 +v 42.751548767 24.693101883 8.627023697 +v 41.233592987 24.693101883 8.888769150 +v 44.200992584 24.693101883 8.105663300 +v 39.693244934 24.693101883 8.882948875 +v 44.201519012 23.774042130 8.106764793 +v 41.233711243 23.774042130 8.889985085 +v 39.693153381 23.774042130 8.884165764 +v 42.751876831 23.774042130 8.628198624 +v 49.023681641 24.693101883 2.402109146 +v 49.296894073 24.693101883 0.886175275 +v 49.302715302 24.693101883 -0.654170871 +v 49.024864197 23.774042130 2.402412415 +v 49.298110962 23.774042130 0.886268497 +v 49.303928375 23.774042130 -0.654290438 +v 49.040969849 24.693101883 -2.172126293 +v 48.519607544 24.693101883 -3.621569395 +v 49.042144775 23.774042130 -2.172455311 +v 48.520709991 23.774042130 -3.622097015 +v 47.754474640 24.693101883 -4.958458900 +v 46.768821716 24.693101883 -6.142173767 +v 45.592586517 24.693101883 -7.136748314 +v 44.261516571 24.693101883 -7.911962032 +v 42.816055298 24.693101883 -8.444260597 +v 45.593276978 23.774042130 -7.137755394 +v 46.769672394 23.774042130 -6.143045902 +v 47.755466461 23.774042130 -4.959169388 +v 42.816360474 23.774042130 -8.445443153 +v 44.262020111 23.774042130 -7.913074017 +v 41.300121307 24.693101883 -8.717472076 +v 39.759777069 24.693101883 -8.723292351 +v 38.241821289 24.693101883 -8.461545944 +v 39.759654999 23.774042130 -8.724507332 +v 41.300216675 23.774042130 -8.718688965 +v 38.241493225 23.774042130 -8.462721825 +v 49.023681641 17.277132034 2.402109146 +v 48.491386414 17.277132034 3.847570896 +v 49.296894073 17.277132034 0.886175275 +v 49.302715302 17.277132034 -0.654170871 +v 46.721595764 17.277132034 6.354872704 +v 45.537879944 17.277132034 7.340530396 +v 47.716171265 17.277132034 5.178639412 +v 42.751548767 17.277132034 8.627023697 +v 41.233592987 17.277132034 8.888769150 +v 44.200992584 17.277132034 8.105663300 +v 39.693244934 17.277132034 8.882948875 +v 46.721595764 16.372127533 6.354872704 +v 45.537879944 16.372127533 7.340530396 +v 47.716171265 16.372127533 5.178639412 +v 48.491386414 16.372127533 3.847570896 +v 46.721595764 10.354028702 6.354872704 +v 45.537879944 10.354028702 7.340530396 +v 47.716171265 10.354028702 5.178639412 +v 48.491386414 10.354028702 3.847570896 +v 42.751548767 16.372127533 8.627023697 +v 44.200992584 16.372127533 8.105663300 +v 44.200992584 10.354028702 8.105663300 +v 42.751548767 10.354028702 8.627023697 +v 45.442817688 10.109920502 7.175106049 +v 46.599250793 10.109920502 6.208469391 +v 46.599605560 9.998180389 6.208224297 +v 45.443206787 9.998180389 7.174926758 +v 45.872833252 9.998180389 5.478765011 +v 47.570262909 10.109920502 5.055705547 +v 47.570568085 9.998180389 5.055403233 +v 44.854145050 9.998181343 6.330346107 +v 46.728168488 9.998180389 4.463228226 +v 44.136096954 10.109920502 7.926244736 +v 42.718795776 10.109920502 8.439061165 +v 42.454551697 9.998181343 7.443909645 +v 43.703060150 9.998181343 6.992096901 +v 44.136512756 9.998181343 7.926135540 +v 42.719226837 9.998181343 8.439026833 +v 49.023681641 16.372127533 2.402109146 +v 49.296894073 16.372127533 0.886175275 +v 49.302715302 16.372127533 -0.654170871 +v 49.023681641 10.354028702 2.402109146 +v 49.296894073 10.354028702 0.886175275 +v 49.302715302 10.354028702 -0.654170871 +v 48.326347351 10.109920502 3.751841307 +v 48.326595306 9.998180389 3.751490116 +v 47.394161224 9.998180389 3.314592838 +v 48.086002350 9.998180389 -0.567131877 +v 48.844528198 10.109920502 2.336492777 +v 47.850578308 9.998180389 2.067759991 +v 48.844711304 9.998180389 2.336104155 +v 49.109176636 9.998180389 0.852250695 +v 49.109066010 10.109920502 0.852665544 +v 48.083549500 9.998180389 0.760612607 +v 49.111961365 9.998180389 -0.654983759 +v 49.111923218 10.109920502 -0.654555559 +v 41.233592987 16.372127533 8.888769150 +v 39.693244934 16.372127533 8.882948875 +v 41.233592987 10.354028702 8.888769150 +v 39.693244934 10.354028702 8.882948875 +v 38.242927551 10.109920502 8.430583000 +v 39.726757050 10.109920502 8.695119858 +v 39.727172852 9.998181343 8.695234299 +v 38.243316650 9.998181343 8.430766106 +v 39.818809509 9.998181343 7.669605255 +v 37.264827728 9.998181343 6.980216503 +v 41.146553040 9.998181343 7.672055721 +v 41.234405518 9.998181343 8.698015213 +v 41.233978271 10.109920502 8.697976112 +v 41.300121307 17.277132034 -8.717472076 +v 42.816055298 17.277132034 -8.444260597 +v 39.759777069 17.277132034 -8.723292351 +v 38.241821289 17.277132034 -8.461545944 +v 49.040969849 17.277132034 -2.172126293 +v 48.519607544 17.277132034 -3.621569395 +v 47.754474640 17.277132034 -4.958458900 +v 46.768821716 17.277132034 -6.142173767 +v 45.592586517 17.277132034 -7.136748314 +v 44.261516571 17.277132034 -7.911962032 +v 49.040969849 16.372127533 -2.172126293 +v 48.519607544 16.372127533 -3.621569395 +v 45.592586517 16.372127533 -7.136748314 +v 46.768821716 16.372127533 -6.142173767 +v 47.754474640 16.372127533 -4.958458900 +v 44.261516571 16.372127533 -7.911962032 +v 42.816055298 16.372127533 -8.444260597 +v 43.728538513 9.998181343 -6.814738750 +v 44.877174377 9.998181343 -6.148745060 +v 45.469352722 9.998180389 -6.991144657 +v 41.174560547 9.998181343 -7.504128933 +v 45.892707825 9.998180389 -5.293409824 +v 49.040969849 10.354028702 -2.172126293 +v 48.519607544 10.354028702 -3.621569395 +v 48.852970123 9.998180389 -2.139802933 +v 47.857856750 9.998180389 -1.875129580 +v 48.853004456 10.109920502 -2.139374256 +v 48.340190887 10.109920502 -3.556675434 +v 47.406040192 9.998180389 -3.123638630 +v 48.340080261 9.998180389 -3.557091475 +v 47.754474640 10.354028702 -4.958458900 +v 46.768821716 10.354028702 -6.142173767 +v 45.592586517 10.354028702 -7.136748314 +v 44.261516571 10.354028702 -7.911962032 +v 42.816055298 10.354028702 -8.444260597 +v 47.588871002 9.998180389 -4.863785744 +v 46.744293213 9.998180389 -4.274724007 +v 47.589050293 10.109920502 -4.863395691 +v 46.622417450 10.109920502 -6.019829273 +v 46.622169495 9.998180389 -6.020182610 +v 45.469654083 10.109920502 -6.990839005 +v 44.165786743 10.109920502 -7.746923447 +v 44.165435791 9.998181343 -7.747171879 +v 41.300121307 16.372127533 -8.717472076 +v 39.759777069 16.372127533 -8.723292351 +v 38.241821289 16.372127533 -8.461545944 +v 41.300121307 10.354028702 -8.717472076 +v 39.759777069 10.354028702 -8.723292351 +v 38.241821289 10.354028702 -8.461545944 +v 42.750438690 10.109920502 -8.265105247 +v 42.750049591 9.998181343 -8.265288353 +v 42.481704712 9.998181343 -7.271155834 +v 41.266613007 10.109920502 -8.529643059 +v 41.266197205 9.998181343 -8.529757500 +v 39.759391785 10.109920502 -8.532499313 +v 39.758964539 9.998181343 -8.532538414 +v 39.846813202 9.998181343 -7.506579876 +v 38.538818359 9.998181343 -7.278433323 +v 38.274574280 10.109920502 -8.273585320 +v 38.274143219 9.998181343 -8.273550034 +v 36.731849670 24.693101883 8.077439308 +v 38.177310944 24.693101883 8.609737396 +v 35.400783539 24.693101883 7.302225113 +v 34.224548340 24.693101883 6.307652473 +v 33.238891602 24.693101883 5.123937607 +v 36.731346130 23.774042130 8.078551292 +v 38.177009583 23.774042130 8.610920906 +v 35.400093079 23.774042130 7.303232670 +v 34.223693848 23.774042130 6.308525085 +v 33.237899780 23.774042130 5.124648571 +v 31.952400208 24.693101883 2.337605953 +v 31.690650940 24.693101883 0.819646955 +v 32.473762512 24.693101883 3.787048340 +v 32.472660065 23.774042130 3.787576437 +v 31.951225281 23.774042130 2.337934732 +v 31.696472168 24.693101883 -0.720702767 +v 31.689437866 23.774042130 0.819766641 +v 32.501983643 24.693101883 -3.682098389 +v 33.277198792 24.693101883 -5.013166428 +v 31.969684601 24.693101883 -2.236636877 +v 32.500873566 23.774042130 -3.682601929 +v 33.276191711 23.774042130 -5.013856411 +v 31.968502045 23.774042130 -2.236939907 +v 31.695255280 23.774042130 -0.720795989 +v 35.455490112 24.693101883 -7.175054550 +v 36.792377472 24.693101883 -7.940186977 +v 34.271774292 24.693101883 -6.189398766 +v 36.791851044 23.774042130 -7.941287518 +v 35.454776764 23.774042130 -7.176047325 +v 34.270900726 23.774042130 -6.190252781 +v 32.473762512 17.277132034 3.787048340 +v 31.952400208 17.277132034 2.337605953 +v 33.238891602 17.277132034 5.123937607 +v 31.690650940 17.277132034 0.819646955 +v 31.693531036 17.277132034 0.057565335 +v 36.731849670 17.277132034 8.077439308 +v 38.177310944 17.277132034 8.609737396 +v 35.400783539 17.277132034 7.302225113 +v 34.224548340 17.277132034 6.307652473 +v 36.731849670 16.372127533 8.077439308 +v 38.177310944 16.372127533 8.609737396 +v 35.400783539 16.372127533 7.302225113 +v 34.224548340 16.372127533 6.307652473 +v 33.238891602 16.372127533 5.123937607 +v 32.473762512 16.372127533 3.787048340 +v 32.217067719 15.177541733 3.073410511 +v 31.952400208 16.372127533 2.337605953 +v 36.731849670 10.354028702 8.077439308 +v 35.400783539 10.354028702 7.302225113 +v 38.177310944 10.354028702 8.609737396 +v 38.511661530 9.998181343 7.436633110 +v 36.827930450 9.998181343 7.912648678 +v 33.586009979 9.998181343 3.286829948 +v 36.827579498 10.109920502 7.912400246 +v 35.523715973 10.109920502 7.156316757 +v 35.524017334 9.998181343 7.156622887 +v 36.116191864 9.998181343 6.314222813 +v 35.100658417 9.998181343 5.458888531 +v 32.473762512 10.354028702 3.787048340 +v 33.238891602 10.354028702 5.123937607 +v 34.224548340 10.354028702 6.307652473 +v 34.371196747 9.998181343 6.185661316 +v 33.404495239 9.998181343 5.029264927 +v 33.404315948 10.109920502 5.028873920 +v 34.370952606 10.109920502 6.185307503 +v 34.249076843 9.998181343 4.440203190 +v 32.653285980 9.998181343 3.722570896 +v 32.653179169 10.109920502 3.722154856 +v 32.390846252 9.998181343 3.008769512 +v 32.390743256 10.109920502 3.009335756 +v 32.215225220 10.354028702 3.068289518 +v 33.416843414 9.998181343 2.826718807 +v 32.210395813 9.998181343 3.071496487 +v 32.216464996 13.597610474 3.071733236 +v 30.841369629 9.998181343 3.065892220 +v 30.841371536 13.593065262 3.065892696 +v 29.847984314 9.998181343 3.064158440 +v 29.847984314 13.593065262 3.064158440 +v 32.101707458 13.597610474 3.071532965 +v 32.095634460 9.998181343 3.071296215 +v 31.952400208 14.024101257 2.337605953 +v 31.824810028 14.315032005 1.630335212 +v 31.710052490 14.315032005 1.630134821 +v 29.849193573 14.013492584 2.370918036 +v 30.842580795 14.013491631 2.372651577 +v 30.843887329 14.319043159 1.625072956 +v 29.850498199 14.319043159 1.623339295 +v 30.845237732 14.504489899 0.850766778 +v 30.846607208 14.566659927 0.065888450 +v 29.853219986 14.566660881 0.064154625 +v 29.851850510 14.504489899 0.849032998 +v 31.575893402 14.506861687 0.819446802 +v 31.690650940 14.506861687 0.819646955 +v 31.690650940 16.372127533 0.819646955 +v 31.693531036 16.372127533 0.057565335 +v 31.578773499 14.563559532 0.057368152 +v 31.693531036 14.563559532 0.057568435 +v 31.837966919 14.025511742 2.338305235 +v 32.397361755 9.998181343 -2.868865728 +v 32.501983643 17.277132034 -3.682098389 +v 33.277198792 17.277132034 -5.013166428 +v 31.969684601 17.277132034 -2.236636877 +v 31.696472168 17.277132034 -0.720702767 +v 36.792377472 17.277132034 -7.940186977 +v 35.455490112 17.277132034 -7.175054550 +v 34.271774292 17.277132034 -6.189398766 +v 33.422950745 9.998181343 -2.681992054 +v 31.696472168 14.500898361 -0.720702767 +v 31.696472168 16.372127533 -0.720702767 +v 31.581714630 14.500898361 -0.720903039 +v 32.221408844 9.998181343 -2.947216749 +v 32.106651306 9.998181343 -2.947417021 +v 29.858455658 9.998181343 -2.935833454 +v 33.597019196 9.998181343 -3.145344257 +v 32.666774750 9.998181343 -3.586017847 +v 32.397502899 10.109920502 -2.868808270 +v 32.667022705 10.109920502 -3.586368561 +v 32.501983643 10.354028702 -3.682098389 +v 32.230220795 10.354028702 -2.944126129 +v 32.501983643 16.372127533 -3.682098389 +v 30.851840973 9.998181343 -2.934099674 +v 31.969684601 16.372127533 -2.236636877 +v 30.850643158 14.013491631 -2.246689558 +v 30.849338531 14.319043159 -1.499111056 +v 29.857255936 14.013492584 -2.248423100 +v 29.858455658 13.593065262 -2.935833454 +v 30.851842880 13.593065262 -2.934099674 +v 29.855951309 14.319043159 -1.500844717 +v 31.832874298 14.318614006 -1.495679021 +v 31.718116760 14.318614006 -1.495879292 +v 30.847988129 14.504489899 -0.724804401 +v 29.854598999 14.504489899 -0.726538181 +v 33.277198792 16.372127533 -5.013166428 +v 32.223587036 15.128337860 -2.926110983 +v 31.969684601 14.016605377 -2.236636877 +v 32.227390289 13.587356567 -2.936434984 +v 36.792377472 16.372127533 -7.940186977 +v 35.455490112 16.372127533 -7.175054550 +v 34.271774292 16.372127533 -6.189398766 +v 34.265201569 9.998181343 -4.297754765 +v 36.139221191 9.998181343 -6.164871693 +v 36.792377472 10.354028702 -7.940186977 +v 35.455490112 10.354028702 -7.175054550 +v 36.857269287 10.109920502 -7.760769367 +v 36.856853485 9.998181343 -7.760660172 +v 37.290306091 9.998181343 -6.826621532 +v 35.550552368 10.109920502 -7.009631634 +v 35.550159454 9.998181343 -7.009452343 +v 34.271774292 10.354028702 -6.189398766 +v 33.277198792 10.354028702 -5.013166428 +v 33.422801971 9.998181343 -4.889929771 +v 34.394119263 10.109920502 -6.042995930 +v 33.423107147 10.109920502 -4.890232563 +v 35.120536804 9.998181343 -5.313291073 +v 34.393764496 9.998181343 -6.042750835 +v 31.854927063 14.016605377 -2.236837149 +v 32.112632751 13.587356567 -2.936635256 +v 10.356585503 15.374712944 5.018982410 +v 29.294418335 16.423784256 3.660708904 +v 29.291988373 15.376764297 5.051695347 +v 10.358929634 16.423786163 3.627655268 +v 29.296293259 16.884586334 2.586380243 +v 10.360805511 16.884588242 2.553326368 +v 29.298488617 17.185401917 1.329080343 +v 10.362999916 17.185403824 1.296026111 +v 29.300714493 17.272243500 0.053253978 +v 10.365226746 17.272245407 0.020199960 +v 29.302951813 17.185401917 -1.229294538 +v 10.367465019 17.185403824 -1.262348533 +v 29.305149078 16.884586334 -2.486595154 +v 10.369660378 16.884588242 -2.519649506 +v 29.307025909 16.422849655 -3.563069105 +v 10.371539116 16.422851563 -3.596122742 +v 29.309436798 15.374711037 -4.943069458 +v 10.373947144 15.381685257 -4.975730896 +v 29.304344177 15.435887337 -2.026943684 +v 28.841117859 14.941126823 -2.793433666 +v 28.839639664 15.304441452 -1.946416855 +v 29.305885315 15.072572708 -2.908475876 +v 28.638072968 14.662881851 -2.633832216 +v 28.636623383 14.979496956 -1.803709745 +v 29.309436798 15.026430130 -4.943069458 +v 29.138191223 15.381684303 -4.729002476 +v 29.138191223 15.242951393 -4.729002476 +v 28.639835358 13.952214241 -3.643826246 +v 28.843130112 14.116405487 -3.946170568 +v 29.308036804 14.219081879 -4.141609192 +v 28.840187073 14.013489723 -2.260160208 +v 28.841386795 13.593063354 -2.947570324 +v 28.636323929 14.589397430 -1.631963611 +v 28.637676239 14.267008781 -2.406255484 +v 28.639068604 13.729486465 -3.204560280 +v 28.837913513 15.541136742 -0.957117617 +v 29.302547455 15.672581673 -0.997330248 +v 28.635015488 15.184299469 -0.882141113 +v 28.836151123 15.609466553 0.052426800 +v 29.300714493 15.740912437 0.053353190 +v 28.633384705 15.245204926 0.052069638 +v 28.634868622 14.785190582 -0.797887027 +v 28.838882446 14.319040298 -1.512581468 +v 28.837530136 14.504487991 -0.738275290 +v 28.633384705 14.858480453 0.052066229 +v 28.836151123 14.566658020 0.052417602 +v 28.639068604 9.998180389 -3.204016447 +v 28.841384888 9.998180389 -2.947570324 +v 28.830911636 9.998180389 3.052421331 +v 29.116922379 10.109930038 -4.061254501 +v 29.118087769 10.109925270 -4.728369236 +v 29.118087769 9.998066902 -4.728369236 +v 28.843128204 9.998180389 -3.945131063 +v 29.308036804 10.354040146 -4.141703129 +v 29.116922379 9.998066902 -4.061254501 +v 29.309436798 10.354034424 -4.943069458 +v 28.639841080 9.998180389 -3.646778345 +v 29.298891068 15.672581673 1.097742677 +v 28.834398270 15.541136742 1.055923820 +v 28.631757736 15.202935219 0.984243691 +v 29.297094345 15.435887337 2.127355576 +v 28.832672119 15.304441452 2.045222759 +v 28.630126953 15.017164230 1.918454051 +v 28.631908417 14.795264244 0.898266375 +v 28.834779739 14.504487991 0.837295890 +v 28.630455017 14.590009689 1.730486751 +v 28.833427429 14.319040298 1.611602187 +v 28.629058838 14.277847290 2.530798197 +v 28.831195831 14.941862106 2.890551567 +v 28.628692627 14.678530693 2.739474773 +v 29.295558929 15.073307991 3.007130861 +v 28.832122803 14.013489723 2.359180450 +v 28.627624512 13.749271393 3.351529121 +v 28.626873016 13.975434303 3.781867743 +v 29.293388367 14.220697403 4.250654221 +v 28.829160690 14.118021011 4.056437016 +v 29.291988373 15.028306961 5.051695347 +v 29.121498108 15.383560181 4.835417271 +v 29.121498108 15.244828224 4.835417271 +v 28.627624512 9.998180389 3.351893425 +v 28.830913544 13.593063354 3.052421331 +v 28.626873016 9.998180389 3.781398773 +v 28.829162598 9.998180389 4.055243969 +v 29.291988373 10.354028702 5.051671505 +v 29.293388367 10.354027748 4.250892162 +v 29.101392746 9.998066902 4.835268974 +v 29.102554321 9.998066902 4.170360088 +v 29.101392746 10.109920502 4.835268974 +v 29.102554321 10.109919548 4.170360088 +v 10.544443130 15.381685257 -4.761458397 +v 10.544443130 15.242953300 -4.761458397 +v 10.373948097 15.026432037 -4.976121902 +v 10.373948097 10.354028702 -4.976121902 +v 11.038537025 9.998181343 -3.275253773 +v 10.836583138 9.998181343 -3.976563454 +v 10.564364433 10.109919548 -4.760755062 +v 11.039286613 9.998181343 -3.704759359 +v 10.834840775 9.998181343 -2.979003191 +v 10.564364433 9.998067856 -4.760755062 +v 10.824367523 9.998181343 3.020988703 +v 10.822616577 9.998181343 4.023811817 +v 10.356501579 10.354034424 5.018618584 +v 10.547670364 9.998067856 4.802883148 +v 11.026322365 9.998181343 3.723418236 +v 11.027094841 9.998181343 3.280656099 +v 10.547670364 10.109925270 4.802883148 +v 10.527830124 15.381774902 4.802964687 +v 10.356585503 15.026519775 5.018982410 +v 10.527830124 15.243041992 4.802964687 +v 10.372631073 14.220699310 -4.174578667 +v 10.370459557 15.073310852 -2.930491924 +v 10.834573746 14.941128731 -2.824866056 +v 10.836585999 14.116407394 -3.977602959 +v 11.037467957 14.678532600 -2.662835360 +v 11.039287567 13.975436211 -3.705228567 +v 11.037103653 14.277850151 -2.454159260 +v 11.038536072 13.749273300 -3.274889231 +v 11.036034584 15.017166138 -1.841814637 +v 10.833095551 15.304443359 -1.977849364 +v 10.368923187 15.435888290 -2.050716639 +v 9.840255737 14.013492584 -2.293326378 +v 10.833642960 14.013491631 -2.291592836 +v 10.832337379 14.319043159 -1.544014096 +v 11.035706520 14.590011597 -1.653847337 +v 11.034403801 15.202938080 -0.907604218 +v 10.831368446 15.541138649 -0.988549888 +v 10.367126465 15.672584534 -1.021103144 +v 11.032777786 15.245207787 0.024569876 +v 10.830013275 15.609468460 0.024212779 +v 10.365303040 15.740915298 0.023286272 +v 11.034254074 14.795266151 -0.821626842 +v 9.838950157 14.319043159 -1.545748115 +v 10.830985069 14.504489899 -0.769707739 +v 9.837597847 14.504489899 -0.771441817 +v 9.836218834 14.566660881 0.019251090 +v 10.829605103 14.566659927 0.020985158 +v 11.032777786 14.858482361 0.024573289 +v 10.834841728 13.593065262 -2.979002953 +v 9.841454506 9.998181343 -2.980736971 +v 9.841455460 13.593065262 -2.980736732 +v 10.372630119 10.354027748 -4.174252510 +v 10.563604355 9.998067856 -4.093901634 +v 10.563604355 10.109919548 -4.093901634 +v 11.031147003 15.184301376 0.958780825 +v 10.827854156 15.541138649 1.024491310 +v 10.363469124 15.672584534 1.073969722 +v 10.361672401 15.435888290 2.103582621 +v 11.029538155 14.979497910 1.880349159 +v 10.826127052 15.304443359 2.013790369 +v 10.828235626 14.504489899 0.805863440 +v 9.834848404 14.504489899 0.804129362 +v 11.031293869 14.785192490 0.874526620 +v 10.826883316 14.319043159 1.580169678 +v 9.833496094 14.319043159 1.578435779 +v 10.825578690 14.013491631 2.327747822 +v 11.029838562 14.589399338 1.708603024 +v 11.028089523 14.662883759 2.710471869 +v 10.824651718 14.941864014 2.859119177 +v 10.360135078 15.072574615 2.985115051 +v 11.028487206 14.267010689 2.482894659 +v 11.026327133 13.952216148 3.720466137 +v 9.832191467 14.013492584 2.326014042 +v 11.027093887 13.729488373 3.281200171 +v 10.824368477 13.593065262 3.020988941 +v 10.822615623 14.118022919 4.025004864 +v 10.357982635 14.219083786 4.218248844 +v 9.830981255 9.998181343 3.019254923 +v 10.357981682 10.354040146 4.217919350 +v 9.830982208 13.593065262 3.019254923 +v 10.549234390 9.998067856 4.138958454 +v 10.549234390 10.109930038 4.138958454 +v 5.309868813 16.884586334 -4.066629410 +v 5.965365887 16.423784256 -4.917809963 +v 7.409113407 16.423784256 -4.080895424 +v 4.787422657 16.423784256 -6.099878311 +v 5.892444134 15.376764297 -6.944730759 +v 6.814069748 15.376764297 -6.019875526 +v 3.933958769 16.884586334 -5.447355747 +v 3.955558538 16.423784256 -7.546542645 +v 5.241590500 15.376764297 -8.076605797 +v 9.319129944 16.422849655 -3.599782944 +v 9.020275116 16.423784256 -3.646166325 +v 9.321538925 15.374711037 -4.979783535 +v 7.943663597 15.376764297 -5.365069389 +v 6.996253967 16.884586334 -3.089062452 +v 8.878190041 16.884586334 -2.581273079 +v 9.317251205 16.884586334 -2.523308992 +v 2.935139656 17.185401917 -4.683700562 +v 1.799856663 17.185401917 -6.658026218 +v 1.921602488 17.272243500 -3.908794165 +v 4.542731285 17.185401917 -3.070481777 +v 2.962290764 16.884586334 -7.137146473 +v 3.472298384 16.423784256 -9.458216667 +v 3.526463032 16.423784256 -9.159214973 +v 2.461080551 16.884586334 -9.020846367 +v 4.863285065 15.376764297 -9.455788612 +v 2.397970200 16.884586334 -9.460092545 +v 1.214250684 17.185401917 -8.858910561 +v -0.135156274 17.272243500 -9.464513779 +v -0.050951496 17.272243500 -8.694589615 +v -1.322819948 17.185401917 -8.529404640 +v 1.140670061 17.185401917 -9.462286949 +v -1.417704940 17.185401917 -9.466752052 +v 0.620294213 17.272243500 -6.171846390 +v -1.727916718 16.884586334 -5.203984261 +v -0.565482378 17.185401917 -5.683104515 +v 0.902725697 17.185401917 -3.129805088 +v -2.569650412 16.884586334 -8.367468834 +v -2.675005198 16.884586334 -9.468947411 +v -3.751479387 16.422849655 -9.470826149 +v -5.005669594 15.374711037 -8.051085472 +v -5.131479740 15.374711037 -9.473235130 +v -3.637160063 16.422849655 -8.228823662 +v -2.723167658 16.422849655 -4.793770790 +v -0.096093923 16.884586334 -2.366149664 +v -3.999043703 15.374711037 -4.267893314 +v 3.764290810 17.272243500 -2.059657097 +v 2.981748581 17.185401917 -1.043506265 +v 6.513077736 17.185401917 -1.928307772 +v 6.022782326 17.272243500 -0.750450432 +v 5.529903412 17.185401917 0.433612496 +v 8.711903572 17.185401917 -1.335014701 +v 9.315055847 17.185401917 -1.266008496 +v 8.543165207 17.272243500 -0.070394248 +v 9.312817574 17.272243500 0.016540131 +v 0.715803146 15.374711037 1.898883700 +v 1.557805777 16.422849655 0.805522621 +v 4.633044720 16.422849655 2.588180780 +v 4.102715492 15.374711037 3.862213135 +v 5.046727657 16.884586334 1.594367266 +v 2.214611053 16.884586334 -0.047358394 +v 9.310590744 17.185401917 1.292366385 +v 9.308396339 16.884586334 2.549666405 +v 8.207252502 16.884586334 2.447146654 +v 8.373538017 17.185401917 1.200888276 +v 9.306520462 16.423784256 3.623995066 +v 8.064883232 16.422849655 3.514165878 +v 7.882367134 15.374711037 4.882045746 +v 9.304092407 15.376764297 5.014981270 +v -2.047555685 15.374711037 -0.874144256 +v -0.951261282 16.422849655 -1.712323785 +v 5.892444134 15.028306961 -6.944730759 +v 5.671233654 15.244828224 -6.775876999 +v 4.984215736 15.244828224 -7.970758915 +v 6.644100189 15.383560181 -5.799521923 +v 5.671233654 15.383560181 -6.775876999 +v 6.644100189 15.244828224 -5.799521923 +v 6.814069748 15.028306961 -6.019875526 +v 8.853220940 14.941126823 -2.830147266 +v 8.651939392 13.952214241 -3.680540323 +v 8.650176048 14.662881851 -2.670546055 +v 9.317987442 15.072572708 -2.945189476 +v 8.855233192 14.116405487 -3.982884407 +v 9.320139885 14.219081879 -4.178322792 +v 7.836515903 15.383560181 -5.108233452 +v 7.836515903 15.244828224 -5.108233452 +v 7.943663597 15.028306961 -5.365069389 +v 9.321538925 15.026430130 -4.979783535 +v 9.150295258 15.242951393 -4.765716553 +v 9.150295258 15.381684303 -4.765716553 +v 7.943655014 10.354028702 -5.365047932 +v 6.814056873 10.354028702 -6.019856930 +v 5.892426014 10.354028702 -6.944716454 +v 5.241590500 15.028306961 -8.076605797 +v 5.671510220 10.109920502 -6.777579784 +v 5.241569042 10.354028702 -8.076597214 +v 5.671510220 9.998180389 -6.777579784 +v 6.643928051 9.998180389 -5.801239014 +v 6.643928051 10.109920502 -5.801239014 +v 9.321538925 10.354034424 -4.979783535 +v 9.320139885 10.354040146 -4.178416729 +v 8.651944160 9.998180389 -3.683492422 +v 7.835906506 9.998180389 -5.109846592 +v 7.835906506 10.109920502 -5.109846592 +v 9.130189896 10.109925270 -4.765082836 +v 9.129025459 10.109930038 -4.097968102 +v 9.130189896 9.998180389 -4.765082836 +v 9.129025459 9.998180389 -4.097968102 +v 8.855231285 9.998180389 -3.981844902 +v 8.651171684 9.998180389 -3.240730286 +v 8.651172638 13.729486465 -3.241274357 +v 8.853487968 9.998180389 -2.984283924 +v 8.853489876 13.593063354 -2.984283924 +v 8.649779320 14.267008781 -2.442969084 +v 4.647006512 15.383560181 -9.285297394 +v 4.984215736 15.383560181 -7.970758915 +v 4.647006512 15.244828224 -9.285297394 +v 4.863285065 15.028306961 -9.455788612 +v 2.818720818 15.073307991 -9.459357262 +v 1.730043888 15.017164230 -8.793926239 +v 2.702141523 14.941862106 -8.994995117 +v 1.856812716 15.304441452 -8.996471405 +v 1.542076707 14.590009689 -8.794254303 +v 2.551064491 14.678530693 -8.792492867 +v 1.938945532 15.435887337 -9.460893631 +v 4.062243938 14.220697403 -9.457186699 +v 2.865756989 13.593063354 -9.994748116 +v 2.864011288 13.593063354 -8.994712830 +v 2.170770407 14.013489723 -8.995923042 +v 2.342388153 14.277847290 -8.792857170 +v 3.593457460 13.975434303 -8.790673256 +v 3.163118601 13.749271393 -8.791424751 +v 3.868026495 14.118021011 -8.992959976 +v 4.863260746 10.354028702 -9.455788612 +v 4.984924793 9.998180389 -7.972333431 +v 3.866833925 9.998180389 -8.992960930 +v 3.592988729 9.998180389 -8.790673256 +v 3.981950045 10.109919548 -9.266352654 +v 4.062481880 10.354027748 -9.457186699 +v 3.981950045 9.998180389 -9.266352654 +v 4.646858215 10.109920502 -9.265192032 +v 4.646858215 9.998180389 -9.265192032 +v 4.984924793 10.109920502 -7.972333431 +v 3.163483381 9.998180389 -8.791422844 +v 2.864011049 9.998180389 -8.994710922 +v 2.865756750 9.998180389 -9.994747162 +v 0.909332395 15.672581673 -9.462690353 +v 0.867513537 15.541136742 -8.998197556 +v 0.795833468 15.202935219 -8.795557022 +v 2.172516346 14.013488770 -9.995958328 +v 1.423191905 14.319040298 -8.997227669 +v 1.424937487 14.319040298 -9.997262955 +v 0.709856033 14.795264244 -8.795706749 +v 0.648885727 14.504487991 -8.998579025 +v 0.650631309 14.504487991 -9.998614311 +v -1.905951262 9.998180389 -0.982230663 +v -3.834289074 9.998181343 -4.335646629 +v -1.905951262 10.109925270 -0.982230663 +v -3.834289312 10.109925270 -4.335646629 +v -2.047555685 10.354034424 -0.874144256 +v -2.047555685 15.026430130 -0.874144256 +v -3.999043703 10.354034424 -4.267893314 +v -0.135983437 15.609466553 -8.999949455 +v -0.136340588 15.245204926 -8.797183990 +v -0.135057062 15.740912437 -9.464513779 +v -0.135992631 14.566658020 -8.999949455 +v -0.134247005 14.566658020 -9.999984741 +v -0.136344001 14.858480453 -8.797183990 +v -1.185740471 15.672581673 -9.466347694 +v -2.215353489 15.435887337 -9.468144417 +v -0.926685452 14.504487991 -9.001329422 +v -0.986297250 14.785190582 -8.798667908 +v -0.924939871 14.504487991 -10.001364708 +v -1.070551395 15.184299469 -8.798814774 +v -1.145527720 15.541136742 -9.001711845 +v -2.134827137 15.304441452 -9.003438950 +v -1.992120028 14.979496956 -8.800423622 +v -1.820373774 14.589397430 -8.800123215 +v -1.700991750 14.319040298 -9.002680779 +v -1.699246168 14.319040298 -10.002716064 +v -3.096885920 15.072572708 -9.469683647 +v -2.981843472 14.941126823 -9.004917145 +v -2.822242498 14.662881851 -8.801872253 +v -2.448570251 14.013489723 -9.003986359 +v -2.594665527 14.267008781 -8.801475525 +v -2.446824312 14.013488770 -10.004021645 +v -4.828895092 15.242951393 -8.073754311 +v -4.828895092 15.381684303 -8.073754311 +v -3.834159613 15.381684303 -4.335542679 +v -5.005669594 15.026430130 -8.051085472 +v -3.834159613 15.242951393 -4.335542679 +v -3.999043703 15.026430130 -4.267893314 +v -3.135980606 13.593063354 -9.005186081 +v -3.832236767 13.952214241 -8.803635597 +v -4.134581089 14.116405487 -9.006929398 +v -3.392970800 13.729486465 -8.802868843 +v -4.330019474 14.219081879 -9.471836090 +v -5.131479740 15.026430130 -9.473235130 +v -4.917412758 15.381684303 -9.301991463 +v -4.917412758 15.242951393 -9.301991463 +v -1.905799270 15.242951393 -0.982163668 +v -1.905799270 15.381684303 -0.982163668 +v -3.134235144 9.998181343 -10.005220413 +v -3.135980844 9.998181343 -9.005184174 +v -3.392427206 9.998181343 -8.802867889 +v -4.828992844 9.998181343 -8.073887825 +v -4.828992844 10.109925270 -8.073887825 +v -5.005669594 10.354034424 -8.051085472 +v -4.133541584 9.998181343 -9.006927490 +v -3.835189104 9.998181343 -8.803640366 +v -4.916779518 10.109925270 -9.281886101 +v -5.131479740 10.354034424 -9.473235130 +v -4.916779518 9.998181343 -9.281886101 +v -3.134234905 13.593063354 -10.005221367 +v -4.330113411 10.354040146 -9.471836090 +v -4.249664783 9.998181343 -9.280721664 +v -4.249664783 10.109930038 -9.280721664 +v 9.316448212 15.435887337 -2.063657284 +v 8.851742744 15.304441452 -1.983130693 +v 8.648727417 14.979496956 -1.840423584 +v 8.850015640 15.541136742 -0.993831396 +v 9.314651489 15.672581673 -1.034044147 +v 8.647118568 15.184299469 -0.918855011 +v 8.852290154 14.013489723 -2.296873808 +v 8.648427010 14.589397430 -1.668677330 +v 8.850984573 14.319040298 -1.549295425 +v 8.646971703 14.785190582 -0.834600866 +v 8.849633217 14.504487991 -0.774989128 +v 8.848253250 15.609466553 0.015712952 +v 8.645487785 15.245204926 0.015355791 +v 9.312817574 15.740912437 0.016639344 +v 9.310994148 15.672581673 1.061028719 +v 8.846501350 15.541136742 1.019209862 +v 8.643860817 15.202935219 0.947529793 +v 8.848253250 14.566658020 0.015703756 +v 8.645487785 14.858480453 0.015352380 +v 8.644010544 14.795264244 0.861552477 +v 8.846882820 14.504487991 0.800582051 +v 8.639726639 9.998180389 3.315179348 +v 8.843014717 9.998180389 3.015707970 +v 4.171304703 9.998180389 3.697805405 +v 8.638977051 9.998180389 3.744684696 +v 9.309197426 15.435887337 2.090641737 +v 8.844775200 15.304441452 2.008509159 +v 8.642230034 15.017164230 1.881740332 +v 9.307661057 15.073307991 2.970417023 +v 8.843298912 14.941862106 2.853837729 +v 8.640796661 14.678530693 2.702761173 +v 8.642558098 14.590009689 1.693773031 +v 8.845531464 14.319040298 1.574888349 +v 8.641160965 14.277847290 2.494084597 +v 8.844226837 14.013489723 2.322466850 +v 8.639728546 13.749271393 3.314815044 +v 8.843016624 13.593063354 3.015707731 +v 9.305490494 14.220697403 4.213940144 +v 8.841263771 14.118021011 4.019722939 +v 8.638977051 13.975434303 3.745153666 +v 9.304092407 15.028306961 5.014981270 +v 9.133601189 15.244828224 4.798703671 +v 9.133601189 15.383560181 4.798703671 +v 7.906223297 15.381684303 4.705427647 +v 7.906223297 15.242951393 4.705427647 +v 7.882367134 15.026430130 4.882045746 +v 7.882367134 10.354034424 4.882045746 +v 9.113495827 10.109920502 4.798554897 +v 9.114656448 10.109919548 4.133646488 +v 9.114656448 9.998180389 4.133646488 +v 9.113495827 9.998180389 4.798554897 +v 9.304092407 10.354028702 5.014957428 +v 9.305490494 10.354027748 4.214178562 +v 8.841264725 9.998180389 4.018530369 +v 7.906068802 9.998180389 4.705486774 +v 7.906068802 10.109925270 4.705486774 +v 4.102715492 15.026430130 3.862213135 +v 4.171470165 15.242951393 3.697786808 +v 4.171470165 15.381684303 3.697786808 +v 4.102715492 10.354034424 3.862213135 +v 4.171304703 10.109925270 3.697804928 +v 0.824607968 9.998180389 1.757830381 +v 0.824607849 10.109925270 1.757830381 +v 0.715803146 10.354034424 1.898883700 +v 0.824772000 15.381684303 1.757855892 +v 0.715803146 15.026430130 1.898883700 +v 0.824772000 15.242951393 1.757855892 +v -8.753063202 5.376532555 4.954570293 +v 0.230767518 6.425604343 3.563241482 +v 0.230767518 5.378584385 4.954230309 +v -8.753147125 6.425606728 3.563240767 +v 0.230766773 6.886405945 2.488911390 +v -8.753147125 6.886408329 2.488910437 +v 0.230766773 7.187221527 1.231609106 +v -8.753147125 7.187223434 1.231608272 +v 0.230766773 7.274062157 -0.044219129 +v -8.753147125 7.274064541 -0.044219974 +v 0.230766773 7.187221527 -1.326769710 +v -8.753147125 7.187223434 -1.326770663 +v 0.230766773 6.886405945 -2.584072113 +v -8.753147125 6.886408329 -2.584073305 +v 0.230766773 6.424669266 -3.660547972 +v -8.753147125 6.424670696 -3.660548687 +v 0.230766773 5.376530647 -5.040550232 +v -8.753147125 5.383505344 -5.040158749 +v -8.085589409 4.268830299 2.417313814 +v -8.299681664 3.594884872 3.000001907 +v -8.299681664 4.015311241 2.306759596 +v -8.292544365 4.320862770 1.559179902 +v -9.000000000 4.015311241 2.306759596 +v -8.085588455 3.731307507 3.215620279 +v -9.000000000 3.594884872 3.000001907 +v -8.085589409 4.664703846 2.644890785 +v -8.085588455 3.954035997 3.654886723 +v -8.753063202 5.028339386 4.954570293 +v -8.582195282 5.383594036 4.738254070 +v -8.598057747 5.244861126 4.782492161 +v -8.753063202 4.220902920 4.153835297 +v -9.000000000 7.255914688 2.405723095 +v -9.000000000 7.254602909 3.184659481 +v -9.500000000 7.254602909 3.184647322 +v -9.500000000 7.255914688 2.405723095 +v -9.500000000 3.594884872 3.000001907 +v -8.753063202 5.074394226 2.920700073 +v -9.500000000 7.256734848 1.625433087 +v -9.000000000 7.256734848 1.625224113 +v -9.000000000 4.320862770 1.559179902 +v -8.753064156 5.437708378 2.039165974 +v -9.500000000 4.015311241 2.306759596 +v -8.085589409 4.591218948 1.643020630 +v -8.085589409 4.981317997 1.814767003 +v -8.292540550 4.943684101 2.793893814 +v -8.292540550 5.306262970 1.948563099 +v -9.999998093 4.320862770 1.559180021 +v -9.500000000 4.320862770 1.559179902 +v -9.000000000 7.257231712 0.773615956 +v -9.500000000 7.257231712 0.773804307 +v -9.000000000 7.257397652 -0.044181678 +v -9.500000000 7.257397652 -0.044181675 +v -8.753064156 5.674404144 1.009551167 +v -9.000000000 4.568480015 -0.044245362 +v -8.753064156 5.742734909 -0.041133784 +v -8.292540550 5.542958260 0.959262550 +v -8.292540550 5.611288548 -0.041018460 +v -9.500000000 4.506309509 0.740634203 +v -8.085589409 5.186120987 0.893197119 +v -8.085589409 4.787011623 0.808942795 +v -8.292540550 4.506309509 0.740634203 +v -8.085589409 5.247027397 -0.041015293 +v -8.085589409 4.860301971 -0.041011877 +v -8.292540550 4.568480015 -0.044245362 +v -9.000000000 4.506309509 0.740634203 +v -9.500000000 4.568480015 -0.044245347 +v -9.999997139 4.568480492 -0.000007104 +v -9.999997139 4.506309986 0.784872413 +v 0.230767518 5.030126095 4.954230309 +v 0.044036582 5.246647835 4.782487869 +v 0.059897333 5.385379791 4.738249779 +v -8.753147125 0.355853915 4.954206467 +v -8.085588455 0.000001048 3.657839060 +v -8.292540550 0.000001429 3.958587885 +v 0.039793111 -0.000113393 4.738136292 +v -8.085588455 0.000001048 3.215075970 +v -8.299681664 0.000001429 3.003443718 +v -8.562355995 -0.000112630 4.738137245 +v -8.562355995 0.111745358 4.738137245 +v 0.039793111 0.111739635 4.738136292 +v -8.292540550 4.119843006 3.959781170 +v -8.753064156 0.355859280 4.153505802 +v -9.000000000 0.000001429 3.003443718 +v -9.500000000 -0.002705861 3.183461428 +v -9.000000000 -0.002705861 3.183461428 +v -9.500000000 0.000001429 3.003443718 +v -9.999998093 4.015311718 2.306759834 +v -9.999997139 3.594885111 3.000001431 +v -9.999998093 0.000001292 3.000001431 +v -8.561950684 -0.000112630 4.074211597 +v -8.561950684 0.111749932 4.074211597 +v -9.000000000 0.000001429 -3.000231981 +v -9.999998093 0.000001292 -2.999999762 +v -9.500000000 0.000001429 -3.000231981 +v -8.299681664 0.000001429 -3.000231981 +v -8.562355042 -0.000112630 -4.825516224 +v 0.039793123 -0.000113393 -4.825516224 +v 0.039793123 0.111745358 -4.825516224 +v 0.230766773 0.355853915 -5.040550232 +v 0.230766773 5.028249741 -5.040550232 +v 0.230767518 0.355848223 4.954206467 +v -8.562355042 0.111739255 -4.825516224 +v 0.059897345 5.383503914 -4.826185226 +v 0.059897345 5.244771004 -4.826185226 +v -8.582277298 5.383505344 -4.826184273 +v -8.582277298 5.244772911 -4.826184273 +v -8.753147125 5.028251648 -5.040549755 +v -8.292540550 0.000001429 -4.041800499 +v -8.085590363 0.000001048 -3.770350456 +v -8.085590363 0.000001048 -3.340844154 +v -8.753147125 0.355848223 -5.040549755 +v -9.000000000 7.255914688 -2.456159115 +v -9.000000000 7.256734371 -1.676682115 +v -9.500000000 7.256734371 -1.676682115 +v -9.500000000 7.255914688 -2.456159115 +v -9.500000000 7.254604816 -3.248138905 +v -9.000000000 7.254604816 -3.248138905 +v -8.753064156 5.674404144 -1.085524917 +v -8.292540550 5.542958260 -1.053782225 +v -8.753064156 5.437708378 -2.115139723 +v -9.000000000 7.257230759 -0.869287789 +v -9.500000000 7.257230759 -0.869287610 +v -9.000000000 4.506309509 -0.834939539 +v -9.000000000 4.320862770 -1.609247208 +v -9.500000000 4.506309509 -0.834939539 +v -9.500000000 4.320862770 -1.609247208 +v -8.299681664 4.506309509 -0.834939539 +v -8.085590363 4.591831207 -1.719435334 +v -8.085590363 4.797085762 -0.887213409 +v -8.292540550 4.320862770 -1.609247208 +v -8.292540550 5.306262970 -2.043083191 +v -8.085590363 5.018985748 -1.907402754 +v -8.085590363 5.204757690 -0.973190904 +v -9.999998093 4.506309986 -0.790701210 +v -9.999997139 4.320862770 -1.565008759 +v -9.500000000 4.015311241 -2.356827021 +v -9.500000000 3.594884872 -3.000231743 +v -9.999997139 4.015311718 -2.312588453 +v -9.000000000 3.594884872 -3.000231743 +v -9.000000000 4.015311241 -2.356827021 +v -8.753064156 5.075129986 -2.994916439 +v -8.292544365 4.942948341 -2.845862865 +v -8.085590363 4.680352211 -2.728424549 +v -8.085590363 4.279669762 -2.519748211 +v -8.292540550 4.015311241 -2.356827021 +v -8.753064156 4.222518921 -4.239005566 +v -8.299681664 3.594884872 -3.000231743 +v -8.085590363 3.751092434 -3.340479851 +v -8.085590363 3.977255583 -3.770819902 +v -9.500000000 0.000001429 -3.252239466 +v -9.999997139 3.594885111 -2.999999762 +v -9.000000000 0.000001429 -3.252239466 +v -8.292544365 4.118226528 -3.998601675 +v -8.753064156 0.355847448 -4.238679409 +v -8.561952591 0.111738876 -4.158661842 +v -8.561952591 -0.000113012 -4.158661842 +v 11.438048363 13.578246117 -18.801122665 +v 11.792361259 13.578246117 -19.005466461 +v 12.150432587 13.368027687 -18.578733444 +v 12.462369919 13.078775406 -18.214624405 +v 11.797165871 13.078775406 -17.828817368 +v 11.628574371 13.368027687 -18.277658463 +v 12.537845612 13.368027687 -19.040140152 +v 12.955503464 13.078775406 -18.804676056 +v 11.035212517 13.578246117 -18.730283737 +v 11.040124893 13.705832481 -19.307260513 +v 11.035212517 13.368027687 -18.173225403 +v 11.040124893 13.078775406 -17.693790436 +v 11.245326996 13.705832481 -19.344982147 +v 13.217087746 13.078775406 -19.527807236 +v 12.744085312 13.368027687 -19.606222153 +v 12.744275093 13.368027687 -20.208702087 +v 12.195487976 13.578246117 -19.702953339 +v 13.215572357 13.078775406 -20.296792984 +v 12.055417061 13.578246117 -19.318668365 +v 11.558197975 13.705832481 -19.611412048 +v 12.195678711 13.578246117 -20.111970901 +v 11.628130913 13.705832481 -19.807983398 +v 11.425251007 13.705832481 -19.450613022 +v 11.042902946 13.748605728 -19.906599045 +v 11.626614571 13.705832481 -20.016616821 +v 12.055965424 13.578246117 -20.496385574 +v 12.538393021 13.368027687 -20.774915695 +v 12.951137543 13.078775406 -21.018886566 +v 12.151271820 13.368027687 -21.236564636 +v 11.793200493 13.578246117 -20.809831619 +v 12.455681801 13.078775406 -21.606990814 +v 11.629601479 13.368027687 -21.537969589 +v 11.788961411 13.078775406 -21.990169525 +v 11.439075470 13.578246117 -21.014505386 +v 11.418563843 13.705832481 -20.371000290 +v 11.032533646 13.578246117 -21.085718155 +v 11.237122536 13.705832481 -20.474004745 +v 11.032533646 13.368027687 -21.642776489 +v 11.031394005 13.078775406 -22.122211456 +v 11.553833008 13.705832481 -20.212152481 +v 11.027621269 13.705832481 -20.508741379 +v 10.765499115 13.325869560 -17.371828079 +v 10.589583397 13.486545563 -17.125236511 +v 10.589580536 14.006744385 -17.938571930 +v 10.765499115 13.814806938 -18.067226410 +v 10.912987709 13.542448044 -18.201259613 +v 10.912987709 13.172634125 -17.547973633 +v 10.765499115 14.032635689 -18.638780594 +v 10.912987709 13.764249802 -18.741415024 +v 10.403736115 13.592807770 -16.911849976 +v 10.403735161 14.096672058 -17.795053482 +v 9.857499123 14.013492584 -17.601242065 +v 9.857499123 14.319044113 -18.348821640 +v 9.857499123 13.593065262 -16.907999039 +v 10.403735161 14.346630096 -18.426431656 +v 10.589580536 14.256196976 -18.528282166 +v 10.765499115 14.173540115 -19.273298264 +v 10.912987709 13.898954391 -19.323276520 +v 10.589580536 14.419042587 -19.218427658 +v 10.403735161 14.509475708 -19.163867950 +v 9.857499123 14.504489899 -19.123128891 +v 10.589580536 14.466053009 -19.916234970 +v 10.776260376 14.215442657 -19.916215897 +v 10.912987709 13.949377060 -19.916213989 +v 9.857499123 14.566660881 -19.908008575 +v 10.403735161 14.556488037 -19.916397095 +v 12.951138496 9.993399620 -21.018886566 +v 12.955503464 9.993399620 -18.804676056 +v 13.217088699 9.993399620 -19.527805328 +v 12.462368965 9.993399620 -18.214622498 +v 11.797164917 9.993399620 -17.828817368 +v 12.455682755 9.993399620 -21.606988907 +v 11.788962364 9.993399620 -21.990169525 +v 11.040123940 9.993399620 -17.693790436 +v 13.215572357 9.993399620 -20.296792984 +v 10.912989616 9.993399620 -17.548355103 +v 10.765501022 9.993399620 -17.369794846 +v 10.589581490 9.993399620 -17.126068115 +v 9.857499123 9.998181343 -16.907999039 +v 10.403736115 9.994852066 -16.912084579 +v 10.765499115 14.186361313 -20.558336258 +v 10.912986755 13.905885696 -20.506532669 +v 10.589581490 14.419042587 -20.622749329 +v 9.857499123 14.504489899 -20.698701859 +v 10.403736115 14.509475708 -20.664417267 +v 10.765499115 14.058550835 -21.201557159 +v 10.912986755 13.764671326 -21.087100983 +v 10.589581490 14.256196976 -21.312894821 +v 9.857499123 14.319044113 -21.473009109 +v 10.403736115 14.346630096 -21.401855469 +v 10.912986755 13.549904823 -21.645406723 +v 10.912986755 13.186244965 -22.217958450 +v 10.765499115 13.825572968 -21.766845703 +v 10.765499115 13.341844559 -22.484550476 +v 10.589578629 14.006237984 -21.872920990 +v 10.589580536 13.485433578 -22.677083969 +v 9.857499123 14.013492584 -22.220588684 +v 10.403736115 14.097177505 -22.031974792 +v 10.403737068 13.593919754 -22.905027390 +v 9.857499123 13.593065262 -22.908000946 +v 10.765499115 9.993399620 -22.484228134 +v 10.912987709 9.993399620 -22.218212128 +v 11.031394958 9.993399620 -22.122211456 +v 9.857499123 9.998181343 -22.908000946 +v 10.589583397 9.993399620 -22.707220078 +v 10.403737068 9.994852066 -22.904792786 +v 4.858413219 15.381685257 -14.934789658 +v 9.327021599 16.423784256 -16.325420380 +v 9.327021599 15.376764297 -14.934431076 +v 3.478802681 16.422851563 -16.327211380 +v 9.327021599 16.884586334 -17.399749756 +v 2.402328014 16.884588242 -17.392444611 +v 9.327021599 17.185401917 -18.657051086 +v 1.145025015 17.185403824 -18.650850296 +v 9.327021599 17.272243500 -19.932880402 +v 4.858804226 15.374711037 -10.468370438 +v -0.137525633 17.272245407 -19.933252335 +v 1.145023227 17.185401917 -10.468370438 +v -0.137527332 17.272243500 -10.468370438 +v 2.402325630 16.884586334 -10.468370438 +v 3.478801250 16.422849655 -10.468370438 +v -0.137525663 17.272245407 -29.403888702 +v 1.138338208 17.185403824 -21.214948654 +v 9.327021599 17.185401917 -21.215431213 +v 9.327021599 16.884586334 -22.472732544 +v 2.395640373 16.884588242 -22.473354340 +v 9.327021599 16.422849655 -23.549209595 +v 3.469970226 16.423786163 -23.538587570 +v 9.327021599 15.374711037 -24.929210663 +v 3.478802681 16.422851563 -29.403888702 +v 4.860601902 15.381685257 -24.928821564 +v 2.402328014 16.884588242 -29.403888702 +v 1.145024896 17.185403824 -29.403888702 +v 4.858413219 15.381685257 -29.403888702 +v -5.136315823 15.374712944 -14.934705734 +v -3.744987965 16.423784256 -10.468370438 +v -5.135976791 15.376764297 -10.468370438 +v -3.744986534 16.423786163 -16.327211380 +v -2.670657635 16.884586334 -10.468370438 +v -2.670656204 16.884588242 -17.392444611 +v -1.413355708 17.185401917 -10.468370438 +v -1.413353801 17.185403824 -18.650850296 +v -9.608413696 15.374712944 -14.934091568 +v -9.608496666 17.185403824 -18.657052994 +v -9.608496666 17.272245407 -19.932880402 +v -9.608496666 16.884588242 -17.399749756 +v -9.608496666 16.423786163 -16.325420380 +v -1.420040607 17.185403824 -21.214948654 +v -1.413353801 17.185403824 -29.403888702 +v -2.670656204 16.884588242 -29.403888702 +v -2.677342892 16.884588242 -22.473354340 +v -3.744986534 16.423786163 -29.403888702 +v -3.743196487 16.423786163 -23.540378571 +v -5.136315823 15.374712944 -29.403804779 +v -9.608496666 16.422851563 -23.549209595 +v -5.133429050 15.381685257 -24.931009293 +v -9.608496666 16.884588242 -22.472734451 +v -9.608496666 17.185403824 -21.215431213 +v -9.608496666 15.381685257 -24.928819656 +v 9.327021599 15.028306961 -14.934431076 +v 9.156151772 15.383560181 -15.150411606 +v 9.156151772 15.244828224 -15.150411606 +v 8.862455368 14.118021011 -15.928879738 +v 8.659689903 14.678530693 -17.245491028 +v 8.659689903 13.975434303 -16.203096390 +v 9.327021599 14.220697403 -15.735473633 +v 8.862455368 14.941862106 -17.094766617 +v 9.327021599 15.073307991 -16.978998184 +v 4.644438744 15.242953300 -15.150722504 +v 4.644438744 15.381685257 -15.150722504 +v 4.858804226 15.026432037 -14.934789658 +v 4.858804226 10.354028702 -14.934789658 +v 4.643770218 10.109919548 -15.154069901 +v 9.136047363 10.109920502 -15.150524139 +v 9.136047363 9.998181343 -15.150524139 +v 4.643770218 9.998181343 -15.154069901 +v 9.327021599 10.354028702 -14.934454918 +v 3.860055447 9.998181343 -15.931425095 +v 8.862454414 9.998181343 -15.930072784 +v 8.659689903 13.749271393 -16.633434296 +v 8.659689903 9.998181343 -16.203563690 +v 8.862455368 13.593063354 -16.932897568 +v 8.659689903 9.998181343 -16.633069992 +v 8.862454414 9.998181343 -16.932897568 +v 9.327021599 10.354027748 -15.735235214 +v 9.136047363 10.109919548 -15.815433502 +v 9.136047363 9.998181343 -15.815433502 +v 3.558393955 13.952214241 -11.135701180 +v 2.708353996 14.941126823 -10.932935715 +v 3.861092567 14.116405487 -10.932935715 +v 4.057342529 14.219081879 -10.468370438 +v 2.824207306 15.072572708 -10.468370438 +v 4.858804226 15.026430130 -10.468370438 +v 4.644438744 15.381684303 -10.639239311 +v 4.644438744 15.242951393 -10.639239311 +v 4.643770218 10.109925270 -10.659343719 +v 4.858804226 10.354034424 -10.468370438 +v 4.643770218 9.998181343 -10.659343719 +v 3.860053062 9.998181343 -10.932935715 +v 3.118583202 9.998181343 -11.135701180 +v 3.119127274 13.729486465 -11.135701180 +v 2.862490654 9.998181343 -9.932899475 +v 2.862490654 13.593063354 -9.932898521 +v 2.862490654 13.593063354 -10.932935715 +v 2.862490654 9.998181343 -10.932937622 +v 4.057436466 10.354040146 -10.468370438 +v 3.561346054 9.998181343 -11.135701180 +v 3.976654530 10.109930038 -10.659343719 +v 3.976654530 9.998181343 -10.659343719 +v 1.942673564 15.435887337 -10.468370438 +v 2.548398018 14.662881851 -11.135701180 +v 1.861335874 15.304441452 -10.932935715 +v 2.175079346 14.013488770 -9.932898521 +v 2.175079823 14.013489723 -10.932935715 +v 2.320821285 14.267008781 -11.135701180 +v 0.913058519 15.672581673 -10.468370438 +v 0.872035027 15.541136742 -10.932935715 +v 1.718274593 14.979496956 -11.135701180 +v 0.796704412 15.184299469 -11.135701180 +v -0.137626544 15.740912437 -10.468370438 +v -0.137511060 15.609466553 -10.932935715 +v -0.137507826 15.245204926 -11.135701180 +v 1.427499771 14.319040298 -9.932898521 +v 0.653192222 14.504487991 -9.932898521 +v 1.427499771 14.319040298 -10.932935715 +v 1.546528220 14.589397430 -11.135701180 +v 0.712450206 14.785190582 -11.135701180 +v 0.653192282 14.504487991 -10.932935715 +v -0.137504414 14.858480453 -11.135701180 +v -0.137501866 14.566658020 -10.932935715 +v -0.137501940 14.566658020 -9.932898521 +v -3.137510300 9.998181343 -10.932937622 +v 3.159098625 9.998181343 -16.652217865 +v 2.862492561 9.998181343 -16.934700012 +v 3.588604927 9.998181343 -16.220819473 +v 8.659689903 15.017164230 -18.066513062 +v 8.862455368 15.304441452 -17.940097809 +v 9.327021599 15.435887337 -17.858776093 +v 8.659689903 14.277847290 -17.454166412 +v 8.862455368 14.013489723 -17.626138687 +v 8.862455368 14.319040298 -18.373718262 +v 8.659689903 14.590009689 -18.254480362 +v 8.862455368 15.541136742 -18.929397583 +v 9.327021599 15.672581673 -18.888389587 +v 8.659689903 15.202935219 -19.000724792 +v 8.659689903 14.858480453 -19.932903290 +v 8.659689903 15.245204926 -19.932899475 +v 8.862455368 15.609466553 -19.932895660 +v 9.327021599 15.740912437 -19.932781219 +v 8.862455368 14.504487991 -19.148025513 +v 8.659689903 14.795264244 -19.086702347 +v 8.862455368 14.566658020 -19.932905197 +v -3.135706425 9.998181343 -16.932899475 +v 2.862493038 9.998181343 -22.931098938 +v 8.862454414 9.998181343 -22.932899475 +v 8.659689903 15.184299469 -20.867111206 +v 8.862455368 15.541136742 -20.942441940 +v 9.327021599 15.672581673 -20.983467102 +v 8.659689903 14.979496956 -21.788682938 +v 8.862456322 15.304441452 -21.931743622 +v 8.659689903 14.662881851 -22.618804932 +v 9.327021599 15.435887337 -22.013080597 +v 8.862456322 14.941126823 -22.778760910 +v 8.659689903 14.785190582 -20.782857895 +v 8.862455368 14.504487991 -20.723600388 +v 8.862455368 14.319040298 -21.497907639 +v 8.659689903 14.589397430 -21.616935730 +v 8.862455368 14.013489723 -22.245487213 +v 8.659689903 14.267008781 -22.391227722 +v 9.327021599 15.072572708 -22.894615173 +v 8.862455368 14.116405487 -23.931501389 +v 8.659689903 13.729486465 -23.189535141 +v 8.862455368 13.593063354 -22.932899475 +v 8.659689903 13.952214241 -23.628801346 +v 9.136047363 10.109930038 -24.047061920 +v 9.136047363 9.998181343 -24.047061920 +v 8.862455368 9.998181343 -23.930461884 +v 9.327021599 10.354040146 -24.127843857 +v 3.574571609 9.998181343 -23.659011841 +v 3.143173695 9.998181343 -23.229505539 +v 8.659689903 9.998181343 -23.631753922 +v 3.865317822 9.998181343 -23.934371948 +v 3.588604927 9.998181343 -28.736331940 +v 3.159098625 9.998181343 -28.736331940 +v 8.659689903 9.998181343 -23.188991547 +v 2.862492561 9.998181343 -28.939510345 +v 9.327021599 14.219081879 -24.127750397 +v 9.327021599 15.026430130 -24.929210663 +v 9.156151772 15.381684303 -24.714845657 +v 9.156151772 15.242951393 -24.714845657 +v 9.327021599 10.354034424 -24.929210663 +v 4.860936642 10.354034424 -24.931011200 +v 4.644866943 10.109925270 -24.711727142 +v 4.644866943 9.998181343 -24.711727142 +v 9.136047363 9.998181343 -24.714178085 +v 9.136047363 10.109925270 -24.714178085 +v 4.861299992 15.026519775 -24.931093216 +v 4.644983768 15.381774902 -24.715158463 +v 4.644983768 15.243041992 -24.715158463 +v 4.644979954 15.383560181 -29.226558685 +v 4.644438744 15.242953300 -29.233018875 +v 4.858804226 15.026432037 -29.403888702 +v 4.057260036 14.220699310 -29.403806686 +v 2.708355427 14.941128731 -28.939508438 +v 3.861094475 14.116407394 -28.939508438 +v 2.813170910 15.073310852 -29.403806686 +v 2.546679020 14.678532600 -28.736331940 +v 3.589074135 13.975436211 -28.736331940 +v 4.858804226 10.354028702 -29.403888702 +v 4.644866943 10.109920502 -29.206455231 +v 4.644866943 9.998181343 -29.206455231 +v 3.865318775 9.998181343 -28.932861328 +v 3.158733845 13.749273300 -28.736331940 +v 4.056933880 10.354027748 -29.403806686 +v 2.862492561 13.593065262 -29.932897568 +v 2.862492561 9.998182297 -29.932899475 +v 2.862492561 13.593065262 -28.939510345 +v 1.861337543 15.304443359 -28.939508438 +v 1.933394313 15.435888290 -29.403806686 +v 1.725657225 15.017166138 -28.736331940 +v 2.338002682 14.277850151 -28.736331940 +v 2.175081253 14.013491631 -28.939508438 +v 2.175081253 14.013492584 -29.932897568 +v 1.537689686 14.590011597 -28.736331940 +v 1.427501559 14.319043159 -28.939508438 +v 0.872036457 15.541138649 -28.939508438 +v 0.903779268 15.672584534 -29.403806686 +v 0.791445196 15.202938080 -28.736331940 +v -0.140611842 15.740915298 -29.403806686 +v -0.140727192 15.609468460 -28.939094543 +v -0.140733764 14.858482361 -28.736331940 +v -0.140730351 15.245207787 -28.736331940 +v 1.427501559 14.319043159 -29.932897568 +v 0.653193831 14.504489899 -28.939510345 +v 0.705467701 14.795266151 -28.736331940 +v 0.653193951 14.504489899 -29.932899475 +v -0.137500286 14.566659927 -28.939510345 +v -0.137500197 14.566660881 -29.932897568 +v -3.137508869 9.998181343 -28.939510345 +v 3.976916313 10.109919548 -29.212692261 +v 3.976916313 9.998181343 -29.212692261 +v -1.141009688 15.541136742 -10.932935715 +v -1.069683313 15.202935219 -11.135701180 +v -1.182017684 15.672581673 -10.468370438 +v -2.211632252 15.435887337 -10.468370438 +v -2.003895283 15.017164230 -11.135701180 +v -2.975640297 14.941862106 -10.932935715 +v -2.130310535 15.304441452 -10.932935715 +v -0.922381461 14.504487991 -9.932898521 +v -1.696689010 14.319040298 -9.932898521 +v -0.922381401 14.504487991 -10.932935715 +v -0.983705938 14.795264244 -11.135701180 +v -1.815927625 14.590009689 -11.135701180 +v -1.696688890 14.319040298 -10.932935715 +v -2.444268703 14.013488770 -9.932898521 +v -3.091409206 15.073307991 -10.468370438 +v -2.824917316 14.678530693 -11.135701180 +v -2.616240501 14.277847290 -11.135701180 +v -2.444268703 14.013489723 -10.932935715 +v -3.867311954 13.975434303 -11.135701180 +v -3.436972618 13.749271393 -11.135701180 +v -3.137510300 13.593063354 -9.932898521 +v -3.137510300 13.593063354 -10.932935715 +v -4.141528130 14.118021011 -10.932935715 +v -3.437336922 9.998181343 -11.135702133 +v -3.866843224 9.998181343 -11.135702133 +v -3.137510300 9.998181343 -9.932899475 +v -3.418189049 9.998181343 -16.636293411 +v -4.140335083 9.998181343 -10.932937622 +v -4.138982296 9.998181343 -15.935336113 +v -3.849586487 9.998181343 -16.206787109 +v -4.334934235 14.220697403 -10.468370438 +v -5.135976791 15.028306961 -10.468370438 +v -4.919996262 15.383560181 -10.639239311 +v -4.919996262 15.244828224 -10.639239311 +v -4.919999599 15.243041992 -15.150640488 +v -4.919999599 15.381774902 -15.150640488 +v -5.136315823 15.026519775 -14.934705734 +v -4.919883251 10.109925270 -15.154071808 +v -4.919883251 10.109920502 -10.659344673 +v -4.919883251 9.998181343 -10.659344673 +v -4.916337013 9.998181343 -15.151620865 +v -5.135952473 10.354034424 -14.934789658 +v -5.135952950 10.354028702 -10.468370438 +v -4.254973412 10.109919548 -10.659344673 +v -4.254973412 9.998181343 -10.659344673 +v -4.335172176 10.354027748 -10.468370438 +v -8.940937996 13.952216148 -16.233774185 +v -9.144116402 14.941864014 -17.094768524 +v -9.144117355 14.118022919 -15.928879738 +v -9.608413696 15.026519775 -14.934091568 +v -9.437545776 15.381774902 -15.150407791 +v -9.437545776 15.243041992 -15.150407791 +v -9.608413696 14.219083786 -15.734826088 +v -9.608413696 15.072574615 -16.967962265 +v -9.144118309 9.998181343 -16.932897568 +v -8.934705734 9.998181343 -16.676807404 +v -9.144119263 9.998181343 -15.930072784 +v -9.411063194 9.998181343 -15.151620865 +v -8.940937996 9.998181343 -16.230821609 +v -9.411063194 10.109925270 -15.151620865 +v -9.608496666 10.354034424 -14.934453964 +v -9.417300224 10.109930038 -15.814449310 +v -9.608414650 10.354040146 -15.735155106 +v -9.417300224 9.998181343 -15.814449310 +v -8.940937996 13.729488373 -16.673040390 +v -9.144117355 13.593065262 -16.932897568 +v -10.137507439 9.998181343 -16.932897568 +v -10.137506485 13.593065262 -16.932897568 +v -9.608414650 15.435888290 -17.849494934 +v -8.940939903 14.662883759 -17.243770599 +v -9.144117355 15.304443359 -17.940097809 +v -8.940939903 14.267010689 -17.471347809 +v -10.137507439 14.013492584 -17.626140594 +v -9.144117355 14.013491631 -17.626140594 +v -8.940939903 14.979497910 -18.073894501 +v -9.608414650 15.672584534 -18.879110336 +v -9.144116402 15.541138649 -18.929399490 +v -8.940939903 15.184301376 -18.995464325 +v -9.608414650 15.740915298 -19.929794312 +v -9.143704414 15.609468460 -19.929679871 +v -8.940939903 15.245207787 -19.929676056 +v -8.940939903 14.589399338 -18.245639801 +v -9.144117355 14.319043159 -18.373720169 +v -8.940939903 14.785192490 -19.079717636 +v -10.137507439 14.319043159 -18.373720169 +v -10.137506485 14.504489899 -19.148027420 +v -9.144116402 14.504489899 -19.148027420 +v -8.940939903 14.858482361 -19.929672241 +v -9.144117355 14.566659927 -19.932907104 +v -10.137506485 14.566660881 -19.932907104 +v -3.137508392 9.998181343 -22.931098938 +v -10.137507439 9.998181343 -22.932899475 +v -9.608414650 15.672584534 -20.974185944 +v -9.144116402 15.541138649 -20.942443848 +v -8.940939903 15.202938080 -20.861852646 +v -9.144116402 15.304443359 -21.931743622 +v -8.940939903 15.017166138 -21.796064377 +v -9.608414650 15.435888290 -22.003801346 +v -9.144116402 14.941128731 -22.778762817 +v -10.137507439 14.504489899 -20.723600388 +v -8.940939903 14.795266151 -20.775875092 +v -10.137506485 14.319043159 -21.497907639 +v -9.144117355 14.504489899 -20.723600388 +v -8.940939903 14.590011597 -21.608097076 +v -10.137506485 14.013492584 -22.245487213 +v -9.144116402 14.319043159 -21.497907639 +v -8.940939903 14.678532600 -22.617086411 +v -9.608414650 15.073310852 -22.883577347 +v -8.940939903 14.277850151 -22.408409119 +v -9.144116402 14.013491631 -22.245487213 +v -10.137506485 13.593065262 -22.932899475 +v -9.144117355 13.593065262 -22.932899475 +v -8.940939903 13.749273300 -23.229141235 +v -8.940939903 13.975436211 -23.659481049 +v -9.144116402 14.116407394 -23.931501389 +v -3.863620758 9.998181343 -23.644977570 +v -4.135071278 9.998181343 -23.934373856 +v -8.940939903 9.998181343 -23.659011841 +v -3.434114456 9.998181343 -23.213581085 +v -8.940939903 9.998181343 -23.229505539 +v -9.144118309 9.998181343 -22.932899475 +v -3.393599987 9.998181343 -28.730098724 +v -3.839584827 9.998181343 -28.736330032 +v -9.137470245 9.998181343 -23.935726166 +v -9.417302132 9.998181343 -24.047323227 +v -9.417302132 10.109919548 -24.047323227 +v -9.608496666 10.354028702 -24.929210663 +v -9.608414650 10.354027748 -24.127340317 +v -9.411063194 10.109920502 -24.715274811 +v -9.411063194 9.998181343 -24.715274811 +v -3.137508869 9.998182297 -29.932899475 +v -4.140333652 9.998181343 -28.939510345 +v -4.255957127 10.109930038 -29.212692261 +v -4.255957127 9.998181343 -29.212692261 +v -1.141008139 15.541138649 -28.939508438 +v -1.191296935 15.672584534 -29.403806686 +v -1.074942827 15.184301376 -28.736331940 +v -2.130308628 15.304443359 -28.939510345 +v -2.220911503 15.435888290 -29.403806686 +v -1.996512532 14.979497910 -28.736331940 +v -2.975638866 14.941864014 -28.939508438 +v -2.826636791 14.662883759 -28.736331940 +v -0.922379851 14.504489899 -28.939508438 +v -0.990688443 14.785192490 -28.736331940 +v -0.922379732 14.504489899 -29.932897568 +v -1.696687222 14.319043159 -28.939510345 +v -1.824766159 14.589399338 -28.736331940 +v -2.444266796 14.013491631 -28.939510345 +v -2.599059105 14.267010689 -28.736331940 +v -1.696687341 14.319043159 -29.932899475 +v -2.444266796 14.013492584 -29.932899475 +v -3.102445602 15.072574615 -29.403804779 +v -4.141526699 14.118022919 -28.939510345 +v -3.137508869 13.593065262 -28.939510345 +v -3.137508869 13.593065262 -29.932897568 +v -3.836632729 13.952216148 -28.736330032 +v -3.397366047 13.729488373 -28.736330032 +v -4.335580826 14.219083786 -29.403804779 +v -5.136315823 15.026519775 -29.403804779 +v -4.919999599 15.381774902 -29.232936859 +v -4.919455051 15.242951393 -29.226558685 +v -4.918786526 10.109925270 -29.206455231 +v -4.918786526 10.109919548 -24.711729050 +v -4.918786526 9.998181343 -24.711729050 +v -4.918786526 9.998181343 -29.206455231 +v -5.135952473 10.354034424 -29.403888702 +v -5.133820057 15.026432037 -24.931009293 +v -5.133820057 10.354028702 -24.931009293 +v -4.919454575 15.381685257 -24.715076447 +v -4.919454575 15.242953300 -24.715076447 +v -4.335251331 10.354040146 -29.403806686 +v -9.431168556 15.244828224 -24.715387344 +v -9.608496666 15.026432037 -24.929210663 +v -9.431168556 15.383560181 -24.715387344 +v -9.608414650 14.220699310 -24.127666473 +v -9.194107056 2.851734638 2.761604309 +v -9.184239388 4.903903484 2.737780094 +v -9.217930794 4.903903484 2.771472692 +v -9.260048866 2.851734638 2.737920523 +v -9.251623154 4.903903484 2.737780094 +v -9.194107056 2.851734638 2.999632597 +v -9.184238434 4.903903484 2.975809097 +v -9.217930794 4.903903484 3.009500980 +v -9.260048866 2.851734638 2.975808382 +v -9.251622200 4.903903484 2.975809097 +v -9.194107056 2.851734638 2.951984406 +v -9.217930794 4.903903484 2.942117214 +v -9.174771309 2.851734638 2.975808382 +v -9.241754532 2.851734638 2.951984406 +v -9.174771309 2.851734638 2.738004923 +v -9.241754532 2.851734638 2.999632597 +v -9.241754532 2.851734638 2.523857117 +v -9.194107056 2.851734638 2.523857117 +v -9.217930794 4.903903484 2.533725500 +v -9.260048866 2.851734638 2.500032902 +v -9.241754532 2.851734638 2.714097023 +v -9.241754532 2.851734638 2.476208925 +v -9.251622200 4.903903484 2.500033379 +v -9.194107056 2.851734638 2.476208925 +v -9.174771309 2.851734638 2.500032902 +v -9.184238434 4.903903484 2.500033379 +v -9.194107056 2.851734638 2.714097023 +v -9.217930794 4.903903484 2.704088449 +v -9.217930794 4.903903484 2.466341496 +v -9.241754532 2.851734638 2.047632456 +v -9.194107056 2.851734638 2.047632456 +v -9.217930794 4.903903484 2.057500839 +v -9.260048866 2.851734638 2.023808241 +v -9.241754532 2.851734638 1.999984503 +v -9.251622200 4.903903484 2.023808718 +v -9.194107056 2.851734638 1.999984503 +v -9.174771309 2.851734638 2.023808241 +v -9.184238434 4.903903484 2.023808718 +v -9.260048866 2.851734638 2.261920691 +v -9.241754532 2.851734638 2.285828829 +v -9.217930794 4.903903484 2.295696974 +v -9.174771309 2.851734638 2.262004614 +v -9.194107056 2.851734638 2.238096952 +v -9.217930794 4.903903484 2.228312969 +v -9.194107056 2.851734638 2.285828829 +v -9.184239388 4.903903484 2.262005091 +v -9.241754532 2.851734638 2.238096952 +v -9.251623154 4.903903484 2.262005091 +v -9.241754532 2.851734638 1.761872530 +v -9.251623154 4.903903484 1.785780668 +v -9.217930794 4.903903484 1.752088666 +v -9.174771309 2.851734638 1.785780668 +v -9.184239388 4.903903484 1.785780668 +v -9.194107056 2.851734638 1.809604406 +v -9.217930794 4.903903484 1.819472432 +v -9.260048866 2.851734638 1.785696149 +v -9.217930794 4.903903484 1.990117311 +v -9.194107056 2.851734638 1.761872530 +v -9.241754532 2.851734638 1.809604406 +v -9.241754532 2.851734638 1.571408272 +v -9.194107056 2.851734638 1.571408272 +v -9.217930794 4.903903484 1.581276536 +v -9.260048866 2.851734638 3.202257156 +v -9.174771309 2.851734638 3.202257156 +v -9.251623154 2.764342785 2.975809097 +v -9.174771309 2.764550686 3.202257156 +v -9.174771309 2.764550686 2.975808382 +v -9.194107056 2.764285803 2.999632597 +v -9.251623154 2.764400005 3.202257156 +v -9.217930794 2.764285803 2.975809097 +v -9.217930794 2.764400005 3.202223778 +v -9.217930794 2.851734638 3.202223778 +v -9.217930794 2.764285564 2.737780094 +v -9.217930794 2.764285564 2.771472692 +v -9.184239388 2.764371395 2.737920523 +v -9.260048866 2.764551163 2.737920523 +v -9.241754532 2.764342785 2.714097023 +v -9.194107056 2.764285803 2.951984882 +v -9.217930794 2.764285564 2.500033379 +v -9.194107056 2.764285564 2.523857117 +v -9.174771309 2.764551163 2.500032902 +v -9.251623154 2.764342785 2.500032902 +v -9.194107056 2.764285564 2.476209641 +v -9.217930794 2.764285564 2.295696497 +v -9.184239388 2.764370918 2.261920691 +v -9.260048866 2.764551163 2.261920691 +v -9.217930794 2.764285564 2.262004614 +v -9.241754532 2.764342785 2.238096952 +v -9.217930794 2.764285803 2.023808718 +v -9.251623154 2.764342785 2.023808718 +v -9.194107056 2.764285803 2.047632456 +v -9.194107056 2.764285803 1.999985099 +v -9.174771309 2.764550686 2.023808241 +v -9.260048866 2.764551163 1.785696149 +v -9.184239388 2.764371395 1.785696149 +v -9.217930794 2.764285564 1.819471836 +v -9.217930794 2.764285564 1.785780668 +v -9.251623154 2.764342785 1.547584295 +v -9.241754532 2.764342785 1.761872530 +v -9.194107056 2.764285564 1.571408272 +v -9.174771309 2.851734638 1.547583818 +v -9.174771309 2.764551163 1.547583818 +v -9.260048866 2.851734638 1.547583818 +v -9.194107056 2.851734638 1.523760080 +v -9.241754532 2.851734638 1.523760080 +v -9.217930794 4.903903484 1.513892889 +v -9.184238434 4.903903484 1.547584534 +v -9.251622200 4.903903484 1.547584534 +v -9.194107056 2.851734638 1.333379865 +v -9.184239388 4.903903484 1.309556246 +v -9.217930794 4.903903484 1.343248129 +v -9.260048866 2.851734638 1.309471726 +v -9.251623154 4.903903484 1.309556246 +v -9.241754532 2.851734638 1.285648108 +v -9.217930794 4.903903484 1.275864244 +v -9.174771309 2.851734638 1.309556246 +v -9.241754532 2.851734638 1.333379865 +v -9.194107056 2.851734638 1.285648108 +v -9.174771309 2.851734638 0.833331823 +v -9.194107056 2.851734638 0.809423327 +v -9.184239388 4.903903484 0.833331823 +v -9.194107056 2.851734638 0.857155442 +v -9.241754532 2.851734638 0.857155442 +v -9.217930794 4.903903484 0.867023766 +v -9.260048866 2.851734638 0.833247066 +v -9.241754532 2.851734638 0.809423327 +v -9.251623154 4.903903484 0.833331823 +v -9.194107056 2.851734638 1.095183849 +v -9.174771309 2.851734638 1.071359396 +v -9.184238434 4.903903484 1.071360111 +v -9.241754532 2.851734638 1.095183849 +v -9.217930794 4.903903484 1.105052114 +v -9.260048866 2.851734638 1.071359396 +v -9.241754532 2.851734638 1.047535658 +v -9.251622200 4.903903484 1.071360111 +v -9.194107056 2.851734638 1.047535658 +v -9.217930794 4.903903484 1.037668347 +v -9.217930794 4.903903484 0.799639761 +v -9.194107056 2.851734638 0.618959129 +v -9.184238434 4.903903484 0.595135331 +v -9.217930794 4.903903484 0.628827274 +v -9.260048866 2.851734638 0.595134735 +v -9.251622200 4.903903484 0.595135331 +v -9.194107056 2.851734638 0.571310997 +v -9.217930794 4.903903484 0.561443686 +v -9.174771309 2.851734638 0.595134735 +v -9.241754532 2.851734638 0.618959129 +v -9.241754532 2.851734638 0.571310997 +v -9.241754532 2.851734638 0.380930841 +v -9.194107056 2.851734638 0.380930841 +v -9.217930794 4.903903484 0.390799046 +v -9.174771309 2.851734638 0.357107073 +v -9.184239388 4.903903484 0.357107073 +v -9.260048866 2.851734638 0.357022703 +v -9.251623154 4.903903484 0.357107073 +v -9.241754532 2.851734638 0.333198935 +v -9.217930794 4.903903484 0.323415101 +v -9.194107056 2.851734638 0.333198935 +v -9.194107056 2.851734638 0.142734706 +v -9.184238434 4.903903484 0.118910953 +v -9.217930794 4.903903484 0.152602926 +v -9.260048866 2.851734638 0.118910357 +v -9.251622200 4.903903484 0.118910953 +v -9.194107056 2.851734638 0.095086604 +v -9.217930794 4.903903484 0.085219286 +v -9.174771309 2.851734638 0.118910357 +v -9.241754532 2.851734638 0.142734706 +v -9.241754532 2.851734638 0.095086604 +v -9.217930794 2.764285564 1.547584534 +v -9.194107056 2.764285564 1.523760676 +v -9.260048866 2.764551163 1.309471726 +v -9.184239388 2.764371395 1.309471726 +v -9.217930794 2.764285564 1.343247890 +v -9.217930794 2.764285564 1.309556246 +v -9.241754532 2.764342785 1.285648108 +v -9.194107056 2.764285803 1.095183849 +v -9.251623154 2.764342785 1.071359873 +v -9.174771309 2.764551163 1.071359396 +v -9.217930794 2.764285803 1.071360111 +v -9.194107056 2.764285803 1.047536373 +v -9.217930794 2.764285564 0.833331823 +v -9.241754532 2.764342785 0.809423327 +v -9.260048866 2.764550686 0.833247066 +v -9.217930794 2.764285564 0.867023468 +v -9.184239388 2.764371395 0.833247066 +v -9.251623154 2.764342785 0.595135033 +v -9.194107056 2.764285803 0.618959129 +v -9.217930794 2.764285803 0.595135331 +v -9.174771309 2.764551163 0.595134735 +v -9.194107056 2.764285803 0.571311593 +v -9.184239388 2.764371395 0.357022703 +v -9.217930794 2.764285564 0.390798748 +v -9.260048866 2.764551163 0.357022703 +v -9.217930794 2.764285564 0.357107073 +v -9.241754532 2.764342785 0.333198935 +v -9.217930794 2.764285803 0.118910953 +v -9.194107056 2.764285803 0.142734706 +v -9.174771309 2.764551163 0.118910357 +v -9.251623154 2.764342785 0.118910655 +v -9.194107056 2.764285803 0.095087208 +v -9.241754532 0.663206697 2.999632597 +v -9.194107056 0.663206935 2.951984406 +v -9.194107056 0.663206697 2.761604309 +v -9.260048866 0.663206697 2.737920523 +v -9.174771309 0.663206935 2.975808382 +v -9.194107056 0.663206697 2.999632597 +v -9.174771309 0.663206697 2.738004923 +v -9.260048866 0.663206935 2.975808382 +v -9.241754532 0.663206935 2.951984406 +v -9.194107056 0.663206697 2.714097023 +v -9.241754532 0.663206816 2.523857117 +v -9.194107056 0.663206816 2.476208925 +v -9.174771309 0.663206816 2.500032902 +v -9.194107056 0.663206816 2.523857117 +v -9.241754532 0.663206697 2.714097023 +v -9.260048866 0.663206816 2.500032902 +v -9.241754532 0.663206816 2.476208925 +v -9.174771309 0.663206816 2.023808241 +v -9.194107056 0.663206816 1.999984503 +v -9.194107056 0.663206816 2.047632217 +v -9.241754532 0.663206816 2.047632217 +v -9.260048866 0.663206816 2.023808241 +v -9.241754532 0.663206816 1.999984503 +v -9.194107056 0.663206816 2.285828829 +v -9.174771309 0.663206816 2.262005091 +v -9.241754532 0.663206816 2.285828829 +v -9.260048866 0.663206816 2.261920691 +v -9.241754532 0.663206816 2.238096952 +v -9.194107056 0.663206816 2.238096952 +v -9.194107056 0.663206816 1.761872530 +v -9.194107056 0.663206816 1.809604406 +v -9.260048866 0.663206816 1.785696149 +v -9.241754532 0.663206816 1.761872530 +v -9.241754532 0.663206816 1.571408272 +v -9.194107056 0.663206816 1.571408272 +v -9.174771309 0.663206816 1.785780311 +v -9.241754532 0.663206816 1.809604406 +v -9.174771309 0.663206697 3.202257156 +v -9.174771309 0.585616350 3.202257156 +v -9.251623154 0.585465491 2.975808382 +v -9.251623154 0.585465491 3.202257156 +v -9.260048866 0.663206697 3.202257156 +v -9.217930794 0.663206697 3.202223778 +v -9.174771309 0.585616350 2.975808382 +v -9.260048866 0.585616350 2.737920523 +v -9.184239388 0.585465550 2.737920523 +v -9.194107056 0.585465491 2.999632597 +v -9.217930794 0.585465491 3.202223778 +v -9.224709511 0.001949469 2.975809097 +v -9.217930794 0.585465550 2.771472692 +v -9.194107056 0.585465491 2.951984406 +v -9.224709511 0.001949608 2.737780094 +v -9.241754532 0.585465550 2.714097023 +v -9.251623154 0.585465550 2.500032902 +v -9.224709511 0.001949469 2.500033379 +v -9.194107056 0.585465550 2.523857117 +v -9.174771309 0.585616410 2.500032902 +v -9.194107056 0.585465550 2.476208925 +v -9.260048866 0.585616410 2.261920691 +v -9.184239388 0.585465550 2.261920691 +v -9.217930794 0.585465550 2.295696497 +v -9.251623154 0.585465550 2.023808241 +v -9.224709511 0.001949469 2.023808718 +v -9.194107056 0.585465550 2.047632217 +v -9.194107056 0.585465550 1.999984503 +v -9.174771309 0.585616410 2.023808241 +v -9.224709511 0.001949678 2.262005091 +v -9.241754532 0.585465550 2.238096952 +v -9.260048866 0.585616410 1.785696149 +v -9.184239388 0.585465372 1.785696149 +v -9.174771309 0.663206816 1.547583818 +v -9.251623154 0.585465431 1.547583818 +v -9.260048866 0.663206816 1.547583818 +v -9.174771309 0.585616469 1.547583818 +v -9.217930794 0.585465372 1.819472313 +v -9.224709511 0.001949678 1.785780668 +v -9.194107056 0.585465431 1.571408272 +v -9.241754532 0.585465372 1.761872530 +v -9.241754532 0.663206816 1.523760080 +v -9.194107056 0.663206816 1.523760080 +v -9.241754532 0.663206875 1.285648108 +v -9.194107056 0.663206875 1.285648108 +v -9.194107056 0.663206875 1.333379865 +v -9.260048866 0.663206875 1.309471726 +v -9.241754532 0.663206875 1.333379865 +v -9.174771309 0.663206875 1.309556246 +v -9.260048866 0.663206875 1.071359396 +v -9.241754532 0.663206875 1.095183849 +v -9.241754532 0.663206875 1.047535658 +v -9.194107056 0.663206875 1.047535658 +v -9.174771309 0.663206875 1.071359396 +v -9.194107056 0.663206875 1.095183849 +v -9.241754532 0.663206875 0.809423327 +v -9.194107056 0.663206875 0.809423327 +v -9.194107056 0.663206875 0.857155442 +v -9.260048866 0.663206875 0.833247066 +v -9.174771309 0.663206875 0.833331823 +v -9.241754532 0.663206875 0.857155442 +v -9.241754532 0.663206875 0.618959129 +v -9.194107056 0.663206875 0.618959129 +v -9.260048866 0.663206875 0.595134735 +v -9.241754532 0.663206875 0.571310997 +v -9.194107056 0.663206875 0.571310997 +v -9.174771309 0.663206875 0.595134735 +v -9.194107056 0.663206875 0.380930841 +v -9.260048866 0.663206875 0.357022703 +v -9.241754532 0.663206875 0.380930841 +v -9.174771309 0.663206875 0.357107073 +v -9.194107056 0.663206875 0.333198935 +v -9.241754532 0.663206875 0.333198935 +v -9.194107056 0.663206875 0.142734706 +v -9.241754532 0.663206875 0.142734706 +v -9.194107056 0.663206875 0.095086604 +v -9.174771309 0.663206875 0.118910357 +v -9.260048866 0.663206875 0.118910357 +v -9.241754532 0.663206875 0.095086604 +v -9.224709511 0.001949469 1.547584534 +v -9.194107056 0.585465431 1.523760080 +v -9.260048866 0.585616469 1.309471726 +v -9.224709511 0.001949678 1.309556246 +v -9.217930794 0.585465431 1.343247890 +v -9.241754532 0.585465431 1.285648108 +v -9.184239388 0.585465431 1.309471726 +v -9.194107056 0.585465431 1.095183849 +v -9.251623154 0.585465431 1.071359396 +v -9.224709511 0.001949330 1.071360111 +v -9.174771309 0.585616469 1.071359396 +v -9.194107056 0.585465431 1.047535658 +v -9.184239388 0.585465431 0.833247066 +v -9.224709511 0.001949748 0.833331823 +v -9.241754532 0.585465491 0.809423327 +v -9.260048866 0.585616469 0.833247066 +v -9.217930794 0.585465431 0.867023468 +v -9.251623154 0.585465491 0.595134735 +v -9.174771309 0.585616529 0.595134735 +v -9.260048866 0.585616529 0.357022703 +v -9.184239388 0.585465491 0.357022703 +v -9.224709511 0.001949330 0.595135331 +v -9.194107056 0.585465491 0.618959129 +v -9.194107056 0.585465491 0.571310997 +v -9.217930794 0.585465491 0.390798748 +v -9.224709511 0.001949748 0.357107073 +v -9.241754532 0.585465491 0.333198935 +v -9.194107056 0.585465491 0.142734706 +v -9.224709511 0.001949399 0.118910953 +v -9.174771309 0.585616529 0.118910357 +v -9.251623154 0.585465491 0.118910357 +v -9.194107056 0.585465491 0.095086604 +v -9.241754532 2.851734638 -0.095293581 +v -9.194107056 2.851734638 -0.095293581 +v -9.217930794 4.903903484 -0.085425362 +v -9.260048866 2.851734638 -0.119201690 +v -9.174771309 2.851734638 -0.119117334 +v -9.194107056 2.851734638 -0.143025458 +v -9.184239388 4.903903484 -0.119117334 +v -9.241754532 2.851734638 -0.143025458 +v -9.251623154 4.903903484 -0.119117334 +v -9.217930794 4.903903484 -0.152809307 +v -9.194107056 2.851734638 -0.333489835 +v -9.184238434 4.903903484 -0.357313603 +v -9.217930794 4.903903484 -0.323621780 +v -9.260048866 2.851734638 -0.357314199 +v -9.251622200 4.903903484 -0.357313603 +v -9.194107056 2.851734638 -0.381137967 +v -9.217930794 4.903903484 -0.391005576 +v -9.241754532 2.851734638 -0.333489835 +v -9.241754532 2.851734638 -0.381137967 +v -9.174771309 2.851734638 -0.357314199 +v -9.194107056 2.851734638 -0.571518302 +v -9.184239388 4.903903484 -0.595342040 +v -9.217930794 4.903903484 -0.561650217 +v -9.260048866 2.851734638 -0.595426559 +v -9.251623154 4.903903484 -0.595342040 +v -9.241754532 2.851734638 -0.619250298 +v -9.217930794 4.903903484 -0.629034042 +v -9.194107056 2.851734638 -0.619250298 +v -9.194107056 2.851734638 -0.809714198 +v -9.184238434 4.903903484 -0.833538115 +v -9.217930794 4.903903484 -0.799846351 +v -9.260048866 2.851734638 -0.833538711 +v -9.251622200 4.903903484 -0.833538115 +v -9.174771309 2.851734638 -0.595342159 +v -9.241754532 2.851734638 -0.571518302 +v -9.174771309 2.851734638 -0.833538711 +v -9.241754532 2.851734638 -0.809714198 +v -9.241754532 2.851734638 -1.095474720 +v -9.251623154 4.903903484 -1.071566582 +v -9.217930794 4.903903484 -1.105258584 +v -9.174771309 2.851734638 -1.071566582 +v -9.184239388 4.903903484 -1.071566582 +v -9.194107056 2.851734638 -1.047742844 +v -9.217930794 4.903903484 -1.037874579 +v -9.260048866 2.851734638 -1.071650743 +v -9.194107056 2.851734638 -0.857362509 +v -9.217930794 4.903903484 -0.867230058 +v -9.241754532 2.851734638 -1.047742844 +v -9.241754532 2.851734638 -0.857362509 +v -9.194107056 2.851734638 -1.095474720 +v -9.194107056 2.851734638 -1.285938621 +v -9.174771309 2.851734638 -1.309763074 +v -9.184238434 4.903903484 -1.309762597 +v -9.241754532 2.851734638 -1.285938621 +v -9.217930794 4.903903484 -1.276070833 +v -9.260048866 2.851734638 -1.309763074 +v -9.241754532 2.851734638 -1.333587050 +v -9.251622200 4.903903484 -1.309762597 +v -9.194107056 2.851734638 -1.333587050 +v -9.194107056 2.851734638 -1.523967266 +v -9.184239388 4.903903484 -1.547790885 +v -9.217930794 4.903903484 -1.514099121 +v -9.260048866 2.851734638 -1.547875404 +v -9.251623154 4.903903484 -1.547790885 +v -9.241754532 2.851734638 -1.571699142 +v -9.217930794 4.903903484 -1.581482887 +v -9.174771309 2.851734638 -1.547790885 +v -9.217930794 4.903903484 -1.343454361 +v -9.217930794 2.764285564 -0.119117334 +v -9.241754532 2.764342785 -0.143025458 +v -9.260048866 2.764550686 -0.119201690 +v -9.184239388 2.764370918 -0.119201690 +v -9.217930794 2.764285564 -0.085425660 +v -9.217930794 2.764285803 -0.357313752 +v -9.194107056 2.764285803 -0.333490014 +v -9.174771309 2.764550686 -0.357314199 +v -9.194107056 2.764285803 -0.381137490 +v -9.251623154 2.764342785 -0.357313901 +v -9.260048866 2.764551163 -0.595426559 +v -9.184239388 2.764371395 -0.595426559 +v -9.217930794 2.764285564 -0.561650217 +v -9.217930794 2.764285564 -0.595342159 +v -9.251623154 2.764342785 -0.833538413 +v -9.241754532 2.764342785 -0.619250298 +v -9.194107056 2.764285564 -0.809714556 +v -9.174771309 2.764551163 -0.833538711 +v -9.217930794 2.764285564 -0.833538294 +v -9.194107056 2.764285803 -0.857361853 +v -9.217930794 2.764285564 -1.071566582 +v -9.217930794 2.764285564 -1.037874579 +v -9.184239388 2.764370918 -1.071650743 +v -9.260048866 2.764551163 -1.071650743 +v -9.241754532 2.764342785 -1.095474720 +v -9.251623154 2.764342785 -1.309763074 +v -9.194107056 2.764285803 -1.285939097 +v -9.174771309 2.764550686 -1.309763074 +v -9.217930794 2.764285803 -1.309762716 +v -9.194107056 2.764285803 -1.333586216 +v -9.241754532 2.851734638 -1.523967266 +v -9.217930794 2.764285564 -1.514099121 +v -9.184239388 2.764371395 -1.547875404 +v -9.260048866 2.764551163 -1.547875404 +v -9.194107056 2.851734638 -1.571699262 +v -9.260048866 2.851734638 -1.785987616 +v -9.241754532 2.851734638 -1.762163401 +v -9.217930794 4.903903484 -1.752295256 +v -9.241754532 2.851734638 -1.809811354 +v -9.251622200 4.903903484 -1.785987139 +v -9.194107056 2.851734638 -1.809811354 +v -9.174771309 2.851734638 -1.785987616 +v -9.217930794 4.903903484 -1.819678783 +v -9.194107056 2.851734638 -1.762163401 +v -9.184238434 4.903903484 -1.785987139 +v -9.194107056 2.851734638 -2.000191689 +v -9.184239388 4.903903484 -2.024015427 +v -9.217930794 4.903903484 -1.990323782 +v -9.260048866 2.851734638 -2.024100065 +v -9.251623154 4.903903484 -2.024015427 +v -9.241754532 2.851734638 -2.000191689 +v -9.174771309 2.851734638 -2.024015665 +v -9.241754532 2.851734638 -2.238387823 +v -9.194107056 2.851734638 -2.238387823 +v -9.217930794 4.903903484 -2.228519678 +v -9.260048866 2.851734638 -2.262212276 +v -9.194107056 2.851734638 -2.047923565 +v -9.217930794 4.903903484 -2.057707310 +v -9.241754532 2.851734638 -2.286036015 +v -9.251622200 4.903903484 -2.262211800 +v -9.194107056 2.851734638 -2.286036015 +v -9.174771309 2.851734638 -2.262212276 +v -9.184238434 4.903903484 -2.262211800 +v -9.241754532 2.851734638 -2.047923565 +v -9.217930794 4.903903484 -2.295903683 +v -9.241754532 2.851734638 -2.524148226 +v -9.260048866 2.851734638 -2.500324249 +v -9.251623154 4.903903484 -2.500240326 +v -9.241754532 2.851734638 -2.476416349 +v -9.194107056 2.851734638 -2.476416349 +v -9.217930794 4.903903484 -2.466548204 +v -9.194107056 2.851734638 -2.524148226 +v -9.217930794 4.903903484 -2.533931971 +v -9.174771309 2.851734638 -2.500240326 +v -9.184239388 4.903903484 -2.500240326 +v -9.241754532 2.851734638 -2.714612246 +v -9.184238434 4.903903484 -2.738436222 +v -9.217930794 4.903903484 -2.704744339 +v -9.260048866 2.851734638 -2.738436699 +v -9.251622200 4.903903484 -2.738436222 +v -9.194107056 2.851734638 -2.714612246 +v -9.174770355 2.851734638 -2.738436699 +v -9.241754532 2.851734638 -2.762260437 +v -9.194107056 2.851734638 -2.762260437 +v -9.217930794 4.903903484 -2.772128105 +v -9.241754532 2.851734638 -3.000288248 +v -9.251622200 4.903903484 -2.976464748 +v -9.217930794 4.903903484 -3.010156393 +v -9.194107056 2.851734638 -3.000288248 +v -9.184238434 4.903903484 -2.976464748 +v -9.194107056 2.851734638 -2.952640533 +v -9.217930794 4.903903484 -2.942772388 +v -9.260048866 2.851734638 -2.976464748 +v -9.174771309 2.851734638 -2.976464272 +v -9.241754532 2.851734638 -2.952640533 +v -9.217930794 2.764285564 -1.547791004 +v -9.241754532 2.764342785 -1.571699262 +v -9.251623154 2.764342785 -1.785987496 +v -9.194107056 2.764285564 -1.762163520 +v -9.217930794 2.764285564 -1.785987139 +v -9.174771309 2.764551163 -1.785987616 +v -9.217930794 2.764285564 -2.024015665 +v -9.217930794 2.764285564 -1.990323663 +v -9.184239388 2.764371395 -2.024100065 +v -9.260048866 2.764551163 -2.024099827 +v -9.241754532 2.764342785 -2.047923803 +v -9.194107056 2.764285564 -1.809810996 +v -9.217930794 2.764285803 -2.262211800 +v -9.194107056 2.764285803 -2.238388062 +v -9.174771309 2.764550686 -2.262212276 +v -9.251623154 2.764342785 -2.262211800 +v -9.194107056 2.764285803 -2.286035538 +v -9.217930794 2.764285564 -2.500240326 +v -9.260048866 2.764550686 -2.500324249 +v -9.217930794 2.764285564 -2.466548204 +v -9.241754532 2.764342785 -2.524148226 +v -9.184239388 2.764371395 -2.500324249 +v -9.251622200 2.764342785 -2.738436460 +v -9.194107056 2.764285803 -2.714612246 +v -9.174770355 2.764551163 -2.738436699 +v -9.217930794 2.764285803 -2.738436222 +v -9.194107056 2.764285803 -2.762260199 +v -9.251623154 2.764342785 -2.976464748 +v -9.217930794 2.764285564 -2.976464748 +v -9.194107056 2.764285564 -2.952641010 +v -9.174771309 2.764551163 -2.976464272 +v -9.174771309 2.851734400 -3.202257156 +v -9.217930794 2.764285088 -3.201863527 +v -9.194107056 2.764399529 -2.999536991 +v -9.260048866 2.764550447 -3.202257156 +v -9.260048866 2.851734400 -3.202257156 +v -9.184239388 2.764342308 -3.202257156 +v -9.217930794 2.851734400 -3.201863527 +v -9.194107056 0.663206875 -0.333490014 +v -9.241754532 0.663206875 -0.333490014 +v -9.194107056 0.663206875 -0.095293581 +v -9.260048866 0.663206875 -0.119201690 +v -9.241754532 0.663206875 -0.143025458 +v -9.194107056 0.663206875 -0.143025458 +v -9.174771309 0.663206875 -0.119117633 +v -9.241754532 0.663206875 -0.095293581 +v -9.174771309 0.663206875 -0.357314348 +v -9.260048866 0.663206875 -0.357314348 +v -9.241754532 0.663206875 -0.619250298 +v -9.194107056 0.663206875 -0.619250298 +v -9.194107056 0.663206875 -0.381138086 +v -9.194107056 0.663206875 -0.571518421 +v -9.260048866 0.663206875 -0.595426559 +v -9.241754532 0.663206875 -0.571518421 +v -9.241754532 0.663206875 -0.381138086 +v -9.174771309 0.663206875 -0.595342159 +v -9.241754532 0.663206935 -0.809714556 +v -9.194107056 0.663206935 -0.809714556 +v -9.260048866 0.663206935 -0.833538890 +v -9.241754532 0.663206935 -0.857362628 +v -9.194107056 0.663206935 -0.857362628 +v -9.174771309 0.663206935 -0.833538890 +v -9.241754532 0.663206697 -1.095474839 +v -9.194107056 0.663206697 -1.095474839 +v -9.260048866 0.663206697 -1.071651101 +v -9.194107056 0.663206697 -1.047743082 +v -9.241754532 0.663206697 -1.047743082 +v -9.174771309 0.663206697 -1.071566582 +v -9.194107056 0.663206697 -1.285939097 +v -9.174771309 0.663206935 -1.309763074 +v -9.241754532 0.663206697 -1.285939097 +v -9.260048866 0.663206935 -1.309763074 +v -9.241754532 0.663206935 -1.333587050 +v -9.194107056 0.663206935 -1.333587050 +v -9.194107056 0.663206697 -1.523967385 +v -9.260048866 0.663206697 -1.547875524 +v -9.241754532 0.663206697 -1.571699262 +v -9.194107056 0.663206697 -1.571699262 +v -9.260048866 0.585616529 -0.119201690 +v -9.224709511 0.001949748 -0.119117334 +v -9.217930794 0.585465491 -0.085425660 +v -9.184239388 0.585465491 -0.119201690 +v -9.241754532 0.585465491 -0.143025458 +v -9.194107056 0.585465491 -0.333490014 +v -9.224709511 0.001949399 -0.357313752 +v -9.174771309 0.585616529 -0.357314348 +v -9.251623154 0.585465491 -0.357314348 +v -9.194107056 0.585465491 -0.381138086 +v -9.260048866 0.585616350 -0.595426559 +v -9.184239388 0.585465491 -0.595426559 +v -9.251623154 0.585465491 -0.833538711 +v -9.174771309 0.585616350 -0.833538711 +v -9.217930794 0.585465491 -0.561650217 +v -9.224709511 0.001949748 -0.595342159 +v -9.194107056 0.585465491 -0.809714556 +v -9.241754532 0.585465491 -0.619250298 +v -9.224709511 0.001949399 -0.833538294 +v -9.194107056 0.585465491 -0.857362628 +v -9.184239388 0.585465491 -1.071651101 +v -9.224709511 0.001949608 -1.071566701 +v -9.241754532 0.585465491 -1.095474839 +v -9.217930794 0.585465491 -1.037874579 +v -9.260048866 0.585616350 -1.071651101 +v -9.251623154 0.585465491 -1.309763074 +v -9.194107056 0.585465491 -1.285939097 +v -9.194107056 0.585465491 -1.333587050 +v -9.224709511 0.001949399 -1.309762716 +v -9.174771309 0.585616350 -1.309763074 +v -9.184239388 0.585465491 -1.547875524 +v -9.224709511 0.001949608 -1.547790885 +v -9.241754532 0.585465491 -1.571699262 +v -9.260048866 0.585616350 -1.547875524 +v -9.217930794 0.585465491 -1.514099121 +v -9.241754532 0.663206697 -1.523967385 +v -9.174771309 0.663206697 -1.547791004 +v -9.194107056 0.663206816 -1.762163520 +v -9.174771309 0.663206816 -1.785987616 +v -9.241754532 0.663206816 -1.762163520 +v -9.260048866 0.663206816 -1.785987616 +v -9.241754532 0.663206816 -1.809811592 +v -9.194107056 0.663206816 -1.809811592 +v -9.260048866 0.663206816 -2.024100065 +v -9.194107056 0.663206816 -2.000191927 +v -9.174771309 0.663206816 -2.024015903 +v -9.241754532 0.663206816 -2.000191927 +v -9.194107056 0.663206816 -2.047923803 +v -9.194107056 0.663206816 -2.238387823 +v -9.241754532 0.663206816 -2.238387823 +v -9.194107056 0.663206816 -2.286036253 +v -9.174771309 0.663206816 -2.262212276 +v -9.241754532 0.663206816 -2.047923803 +v -9.241754532 0.663206816 -2.286036253 +v -9.260048866 0.663206816 -2.262212276 +v -9.194107056 0.663206816 -2.476416588 +v -9.174771309 0.663206816 -2.500240326 +v -9.194107056 0.663206816 -2.524148226 +v -9.241754532 0.663206816 -2.476416588 +v -9.260048866 0.663206816 -2.500324488 +v -9.241754532 0.663206816 -2.524148226 +v -9.194107056 0.663206816 -2.714612484 +v -9.241754532 0.663206816 -2.714612484 +v -9.260048866 0.663206816 -2.738436699 +v -9.241754532 0.663206816 -2.762260437 +v -9.174770355 0.663206816 -2.738436699 +v -9.241754532 0.663206875 -3.000288486 +v -9.174771309 0.663206875 -2.976464748 +v -9.194107056 0.663206875 -2.952641010 +v -9.241754532 0.663206875 -2.952641010 +v -9.194107056 0.663206816 -2.762260437 +v -9.260048866 0.663206875 -2.976464748 +v -9.194107056 0.663206875 -3.000288486 +v -9.251623154 0.585465550 -1.785987616 +v -9.174771309 0.585616410 -1.785987616 +v -9.260048866 0.585616410 -2.024100065 +v -9.184239388 0.585465550 -2.024100065 +v -9.194107056 0.585465550 -1.762163520 +v -9.224709511 0.001949469 -1.785987139 +v -9.194107056 0.585465550 -1.809811711 +v -9.217930794 0.585465550 -1.990323782 +v -9.224709511 0.001949678 -2.024015665 +v -9.194107056 0.585465550 -2.238387823 +v -9.224709511 0.001949469 -2.262211800 +v -9.174771309 0.585616410 -2.262212276 +v -9.251623154 0.585465550 -2.262212276 +v -9.194107056 0.585465550 -2.286036253 +v -9.241754532 0.585465550 -2.047923803 +v -9.260048866 0.585616410 -2.500324488 +v -9.224709511 0.001949678 -2.500240326 +v -9.217930794 0.585465550 -2.466548204 +v -9.241754532 0.585465550 -2.524148226 +v -9.184239388 0.585465550 -2.500324488 +v -9.194107056 0.585465550 -2.714612484 +v -9.224708557 0.001949469 -2.738436222 +v -9.174770355 0.585616469 -2.738436699 +v -9.251622200 0.585465372 -2.738436699 +v -9.194107056 0.585465431 -2.762260437 +v -9.217930794 0.663206875 -3.201863527 +v -9.260048866 0.663206875 -3.202257156 +v -9.174771309 0.663206875 -3.202257156 +v -9.174771309 0.585616469 -2.975544691 +v -9.260048866 0.585616469 -3.202257156 +v -9.251623154 0.585465431 -2.976464748 +v -9.184239388 0.585465431 -3.202257156 +v -9.224709511 0.001949678 -2.976464748 +v -9.217930794 0.585465431 -2.942772865 +v -9.194107056 0.585465431 -3.000288486 +v -9.217930794 0.585465431 -3.201863527 +v -9.342898369 -0.000000013 -3.231861830 +v -9.342898369 -0.000000013 3.241882324 +v -9.342898369 4.903903484 3.241882324 +v -9.124868393 4.903903484 -3.231861830 +v -9.124868393 4.903903484 3.241882324 +v -9.124868393 -0.000000013 3.241882324 +v -9.124868393 -0.000000013 -3.231861830 +v -9.342898369 4.903903484 -3.231861830 +v -10.522571564 5.376530647 4.996312141 +v -11.944499016 5.376530647 4.868019581 +v -10.522571564 6.424669266 3.616309643 +v -11.764372826 6.424669266 3.499822855 +v -10.522571564 6.886405945 2.539833784 +v -13.171661377 6.430963993 3.262741804 +v -12.903519630 6.891765594 2.222413063 +v -11.623864174 6.886405945 2.432556391 +v -11.459753036 7.187221527 1.186010599 +v -10.522571564 7.187221527 1.282531500 +v -12.589709282 7.192582607 1.004901886 +v -14.142993927 5.574815273 4.435072899 +v -16.842653275 6.576874256 3.338032007 +v -13.740724564 6.623157978 3.103520393 +v -13.518838882 5.382621288 4.609709740 +v -16.202087402 7.625217438 2.103313446 +v -13.430030823 7.083959579 2.075096607 +v -13.066421509 7.384777069 0.871518970 +v -15.707345963 8.086019516 1.149680018 +v -18.450126648 8.610714912 0.656888247 +v -19.308334351 7.562372684 1.751571774 +v -17.787288666 9.071516991 -0.188589811 +v -17.011562347 9.372335434 -1.178062677 +v -15.128343582 8.386837006 0.033630669 +v -14.540811539 8.473677635 -1.098863363 +v -12.697456360 7.471618652 -0.349792302 +v -12.271274567 7.279423714 -0.230548218 +v -12.326546669 7.384777546 -1.577539444 +v -10.522571564 7.274062157 -0.000019173 +v -11.292346954 7.274062157 -0.085567750 +v -11.951164246 7.192582607 -1.472508669 +v -11.125818253 7.187221527 -1.350481272 +v -10.522571564 7.187221527 -1.275847554 +v -11.637353897 6.891765594 -2.690018654 +v -11.962936401 7.083960533 -2.781116486 +v -10.522571564 6.886405945 -2.533149719 +v -10.961707115 6.886405945 -2.597026825 +v -16.224405289 9.459175110 -2.182115555 +v -18.626115799 10.376508713 -2.704424858 +v -19.570501328 10.075691223 -1.874399185 +v -20.377454758 9.614888191 -1.165164828 +v -21.422256470 8.566545486 -0.246882081 +v -23.146909714 9.572109222 -2.623607874 +v -14.657373428 9.071517944 -4.180932045 +v -15.760075569 10.075691223 -5.223401070 +v -13.993212700 8.609780312 -5.028099060 +v -14.951511383 9.613953590 -5.934051991 +v -13.141783714 7.560319424 -6.114133835 +v -12.875452042 7.624282837 -4.308912277 +v -13.371181488 8.086019516 -3.353374243 +v -12.239946365 6.574821472 -5.533876419 +v -11.651623726 6.622223377 -3.811594248 +v -11.252530098 5.572762966 -5.132627487 +v -11.024242401 5.380568027 -5.068752766 +v -10.522571564 6.425604343 -3.607479572 +v -10.821478844 6.425604343 -3.662166119 +v -10.522571564 5.378584385 -4.998468876 +v -11.368676186 6.430028915 -3.732426405 +v -13.950182915 8.386837006 -2.237324953 +v -15.433100700 9.372335434 -3.191458941 +v -17.667814255 10.463348389 -3.546681166 +v -18.845424652 11.468912125 -5.169534683 +v -16.704462051 10.376508713 -4.393375397 +v -16.659719467 11.081255913 -6.463191032 +v -17.741708755 11.382072449 -5.822793007 +v -15.733344078 10.619517326 -7.011487007 +v -16.207056046 11.416520119 -7.968554020 +v -17.204814911 11.878255844 -7.564476013 +v -16.370790482 11.411161423 -9.472515106 +v -14.545764923 9.570056915 -7.714380741 +v -14.991180420 10.369996071 -9.472515106 +v -14.927967072 10.367058754 -8.486566544 +v -13.914962769 8.564492226 -6.845079422 +v -19.943355560 11.382072449 -4.519700527 +v -18.370176315 12.179073334 -7.092521667 +v -19.558938980 12.265913010 -6.611088753 +v -17.447265625 11.887731552 -9.463213921 +v -18.704566956 12.188549042 -9.463213921 +v -20.741472244 12.179073334 -6.132178783 +v -21.906833649 11.878255844 -5.660224438 +v -21.262947083 12.188549042 -9.463213921 +v -19.987119675 12.275389671 -9.463213921 +v -21.025344849 11.081254959 -3.879302502 +v -21.949874878 10.620451927 -3.332099915 +v -22.902603149 11.417453766 -5.256952286 +v -24.191875458 10.369110107 -4.734813690 +v -22.520248413 11.887731552 -9.463213921 +v -24.985912323 10.363021851 -9.472431183 +v -23.594579697 11.426930428 -9.463213921 +v -11.921522141 5.244771004 4.691285133 +v -13.464874268 5.250683784 4.400579453 +v -13.464874268 5.389416695 4.400579453 +v -11.921522141 5.383503914 4.691285133 +v -11.944499016 5.028249741 4.868019581 +v -13.518838882 5.034162521 4.609709740 +v -10.522571564 5.028249741 4.996312141 +v -10.693440437 5.244771004 4.781946659 +v -10.693440437 5.383503914 4.781946659 +v -10.522571564 4.220901012 4.194850445 +v -10.522571564 5.074391842 2.961715221 +v -10.987136841 5.306261063 1.998843908 +v -10.987136841 4.942946434 2.845861673 +v -10.522571564 5.437706947 2.080181360 +v -11.189902306 4.981316566 1.855782747 +v -11.189902306 4.664701939 2.685906410 +v -10.987136841 4.118224621 3.998601198 +v -10.987136841 3.594882727 2.999999046 +v -10.987136841 4.015309334 2.312587738 +v -11.189902306 3.954033852 3.695902348 +v -11.189902306 4.268828392 2.458329201 +v -11.189902306 3.731305599 3.256635666 +v -10.522571564 5.674401283 1.050566673 +v -11.189902306 5.186119080 0.934212565 +v -11.189902306 4.591217518 1.684036255 +v -10.987136841 5.542956352 1.009543061 +v -10.987136841 5.611286163 -0.000002925 +v -10.522571564 5.742732048 -0.000118388 +v -11.189902306 5.247025013 0.000000297 +v -10.987136841 4.320860386 1.565007925 +v -11.189902306 4.787009716 0.849958301 +v -10.987136841 4.506307602 0.790700376 +v -11.189902306 4.860300064 0.000003708 +v -10.987136841 4.568477631 0.000006273 +v -14.080591202 5.442877769 4.228303909 +v -14.080591202 5.581610680 4.228303909 +v -14.142993927 5.226356506 4.435072899 +v -16.743244171 6.583669662 3.146288157 +v -16.743244171 6.444936752 3.146288395 +v -16.842651367 6.228415489 3.338032246 +v -16.842641830 1.554137230 3.338010311 +v -11.944499016 0.355853915 4.868019581 +v -10.522571564 0.355859280 4.194944382 +v -13.518832207 0.359884471 4.609686375 +v -13.464838028 0.115775906 4.400473118 +v -11.921388626 0.111744970 4.691383362 +v -11.921388626 0.000000567 4.691383362 +v -13.462984085 0.004034261 4.393279076 +v -10.987136841 0.000000545 3.997561693 +v -10.713544846 0.000000571 4.781278133 +v -10.713544846 0.111745164 4.781278133 +v -10.713544846 0.111750051 4.114162445 +v -10.713544846 0.000000542 4.114162445 +v -10.522571564 0.355853915 4.996312141 +v -11.189902306 0.000000532 3.698854685 +v -11.189902306 0.000000513 3.256092072 +v -10.987138748 0.000000502 2.999999285 +v -10.987138748 0.000000239 -3.000002146 +v -11.189903259 0.000000226 -3.299829006 +v -11.189903259 0.000000207 -3.729335070 +v -14.073675156 -0.000450481 4.214264393 +v -17.247392654 1.200104237 2.855069160 +v -17.247392654 1.400104165 2.855069160 +v -16.737829208 1.200104237 3.131706238 +v -17.743127823 1.600104690 2.561540604 +v -16.743198395 1.310028553 3.146185875 +v -16.221801758 1.000105262 3.387467623 +v -16.737829208 1.000105023 3.131706238 +v -16.221801758 0.800105274 3.387467623 +v -15.696773529 0.800104618 3.623860836 +v -14.142986298 0.552077711 4.435049057 +v -14.073494911 0.200105667 4.214318752 +v -14.619277954 0.400105536 4.038478851 +v -14.080564499 0.307969123 4.228194237 +v -15.696773529 0.600105345 3.623860836 +v -15.159379005 0.600104690 3.842149973 +v -15.159379005 0.400105536 3.842149973 +v -14.619277954 0.200104803 4.038478851 +v -11.522525787 0.199350193 -4.998117447 +v -11.522524834 0.399350166 -4.998117447 +v -11.312547684 -0.000552034 -4.930464745 +v -11.313628197 0.197789952 -4.928623199 +v -11.077886581 0.004034458 -4.860519409 +v -11.730351448 0.599349976 -5.073661804 +v -11.730351448 0.399350077 -5.073661804 +v -10.522571564 5.674401283 -1.044509530 +v -11.189902306 5.204754829 -0.932175219 +v -10.987136841 5.542956352 -1.003501534 +v -11.189902306 5.018983364 -1.866387129 +v -11.189902306 4.797083855 -0.846197724 +v -11.189902306 4.591829300 -1.678419709 +v -10.522571564 5.437706947 -2.074124336 +v -10.987136841 5.306261063 -1.992802382 +v -10.987136841 4.506307602 -0.784873307 +v -10.987136841 4.015309334 -2.306760550 +v -10.987136841 4.320860386 -1.559180737 +v -11.189902306 4.279667377 -2.478732586 +v -11.189902306 4.680350304 -2.687409163 +v -12.338356018 1.198818803 -5.345024109 +v -12.533795357 1.199349403 -5.453479290 +v -12.139158249 0.999349713 -5.248617649 +v -12.337719917 0.999349594 -5.347032547 +v -12.724548340 1.399349451 -5.566424370 +v -12.724548340 1.599349260 -5.566424370 +v -12.533795357 1.399349451 -5.453479290 +v -17.743127823 1.400104165 2.561540604 +v -11.937133789 0.599349856 -5.157656670 +v -11.937134743 0.799349844 -5.157656193 +v -12.139158249 0.799349785 -5.248617649 +v -19.160146713 2.200103760 1.573689103 +v -19.160099030 2.000426531 1.573725939 +v -13.269801140 1.999348879 -5.946537971 +v -19.592878342 2.200426340 1.222027302 +v -13.269801140 2.199348927 -5.946537971 +v -18.688013077 2.000103712 1.929206133 +v -18.688011169 1.800426364 1.929207444 +v -13.088128090 1.799349070 -5.809739113 +v -19.592884064 2.400103569 1.222023010 +v -13.436311722 2.199348927 -6.081855297 +v -18.234491348 1.800103784 2.245227337 +v -18.234491348 1.600103617 2.245227337 +v -12.913619041 1.599349260 -5.688138008 +v -12.913618088 1.799349189 -5.688138008 +v -13.088128090 1.999348998 -5.809739113 +v -20.024847031 2.600103378 0.844038010 +v -23.146888733 4.549372673 -2.623620272 +v -24.191854477 5.346374035 -4.734823227 +v -23.146909714 9.223649979 -2.623607874 +v -21.422256470 8.218087196 -0.246882007 +v -21.260068893 8.573341370 -0.389510125 +v -19.175127029 7.569168091 1.581561685 +v -21.260068893 8.434608459 -0.389510125 +v -19.175127029 7.430434704 1.581561685 +v -19.308334351 7.213913918 1.751571774 +v -21.422237396 3.543808699 -0.246898398 +v -19.308319092 2.539635181 1.751552343 +v -19.175062180 2.295526981 1.581469178 +v -20.024841309 2.400426388 0.844043970 +v -21.259988785 3.299700022 -0.389589429 +v -20.442436218 2.800103426 0.450453132 +v -20.870805740 2.800103188 0.014673174 +v -20.870805740 3.000103235 0.014673110 +v -20.442432404 2.600426197 0.450456142 +v -21.249750137 3.200103283 -0.401151925 +v -21.631483078 3.400102854 -0.852646232 +v -21.631483078 3.200103283 -0.852646172 +v -21.249750137 3.000103235 -0.401151925 +v -21.983184814 3.600101948 -1.301658869 +v -22.960983276 4.305263996 -2.733731031 +v -22.318733215 3.805263281 -1.764104128 +v -22.639333725 3.805263281 -2.242145777 +v -22.639333725 4.005263329 -2.242145777 +v -22.318899155 3.600747585 -1.764345169 +v -21.983184814 3.400102139 -1.301658869 +v -14.730253220 9.577030182 -7.605214596 +v -15.126667023 10.235299110 -8.406121254 +v -14.730253220 9.438297272 -7.605214596 +v -15.126667023 10.374031067 -8.406121254 +v -14.545764923 9.171813011 -7.714380741 +v -14.075992584 8.571465492 -6.703580856 +v -14.075993538 8.432733536 -6.703579426 +v -13.914962769 8.166248322 -6.845079422 +v -17.301195145 9.664704323 -8.797198296 +v -16.291198730 8.954035759 -8.797198296 +v -17.141239166 9.942949295 -8.999963760 +v -15.988500595 9.118226051 -8.999963760 +v -17.025384903 10.074394226 -9.464529037 +v -15.792250633 9.220903397 -9.464529037 +v -14.927967072 9.968814850 -8.486566544 +v -15.205154419 10.369996071 -9.301646233 +v -15.205154419 10.231264114 -9.301646233 +v -14.990788460 9.978288651 -9.464529037 +v -12.338684082 6.581794739 -5.343604088 +v -13.274061203 7.567292213 -5.945445061 +v -12.338685989 6.443062305 -5.343603611 +v -13.141783714 7.162075520 -6.114133835 +v -12.239946365 6.176578522 -5.533876419 +v -13.274061203 7.428560257 -5.945445061 +v -11.314546585 5.579736233 -4.927428246 +v -14.545763016 4.500739098 -7.714381695 +v -14.990787506 5.307215691 -9.463213921 +v -15.792155266 5.357183456 -9.463213921 +v -13.914960861 3.495175600 -6.845081329 +v -14.927965164 5.297739983 -8.486567497 +v -16.730466843 8.731307983 -8.797198296 +v -16.987102509 8.594884872 -8.999963760 +v -16.987100601 4.999999523 -8.998647690 +v -15.989538193 4.999999523 -8.998647690 +v -16.731008530 4.999999523 -8.795883179 +v -16.288244247 4.999999523 -8.795883179 +v -10.522571564 5.075127602 -2.953901052 +v -10.987136841 4.943681717 -2.838132381 +v -10.522571564 4.222517490 -4.197426319 +v -11.189902306 3.977253675 -3.729803801 +v -10.987136841 4.119840622 -4.004019737 +v -10.522571564 5.030126095 -4.998468876 +v -11.077722549 5.387540817 -4.861165047 +v -10.693440437 5.385379791 -4.782487869 +v -11.077723503 5.248808861 -4.861164570 +v -10.693440437 5.246647835 -4.782487869 +v -11.024242401 5.032286644 -5.068752766 +v -11.252530098 5.174519539 -5.132627487 +v -11.314546585 5.441003799 -4.927427292 +v -12.239945412 1.505504131 -5.533878326 +v -13.141781807 2.491002083 -6.114135265 +v -15.032476425 4.604507923 -8.194774628 +v -14.938087463 4.604507923 -7.995305538 +v -15.032476425 4.804508209 -8.194774628 +v -14.730827332 4.306592941 -7.604877949 +v -15.120501518 4.804507256 -8.400219917 +v -15.120501518 5.004507065 -8.400219917 +v -15.127285004 5.103593826 -8.405874252 +v -15.205821991 5.113069534 -9.272240639 +v -15.205821991 4.999999523 -9.272240639 +v -15.872937202 5.113074780 -9.272240639 +v -11.189902306 3.751090527 -3.299464226 +v -10.987136841 3.594882727 -3.000002384 +v -11.252530098 0.503444910 -5.132629395 +v -13.274473190 2.296855927 -5.944923878 +v -12.338994026 1.311358094 -5.343014717 +v -14.076495171 3.301029444 -6.703143120 +v -14.485184669 3.804508448 -7.230883598 +v -14.608549118 3.804508448 -7.414827347 +v -14.608549118 4.004508495 -7.414827347 +v -14.485260963 3.599347115 -7.230987549 +v -14.356073380 3.599347115 -7.052937508 +v -14.356073380 3.399347782 -7.052937508 +v -14.220742226 3.399347782 -6.880163670 +v -14.725962639 4.204507828 -7.604827881 +v -14.835721016 4.404508591 -7.798212051 +v -14.938087463 4.404508591 -7.995305538 +v -14.835721970 4.204507828 -7.798211575 +v -14.725962639 4.004508495 -7.604827881 +v -10.987138748 0.000000195 -4.002827168 +v -11.024241447 0.361212969 -5.068754196 +v -10.522571564 0.355848223 -4.998444557 +v -10.522571564 0.355847448 -4.197664261 +v -10.713545799 0.000000153 -4.782374859 +v -10.713545799 0.111738972 -4.117465496 +v -10.713545799 0.000000182 -4.117465496 +v -10.713545799 0.111739635 -4.782374859 +v -11.077886581 0.117104389 -4.860519409 +v -11.314742088 0.309298813 -4.926791668 +v -13.436311722 2.399348736 -6.081855297 +v -13.602527618 2.599348783 -6.227298737 +v -13.763211250 2.599348783 -6.378746033 +v -13.763211250 2.799348593 -6.378746033 +v -13.602527618 2.399348736 -6.227298737 +v -14.220742226 3.199348211 -6.880163670 +v -14.073856354 3.199347496 -6.706433773 +v -14.073856354 2.999348164 -6.706433773 +v -13.928043365 2.999348402 -6.546429634 +v -13.928043365 2.799348593 -6.546429634 +v -15.872937202 4.999999523 -9.272240639 +v -18.131317139 9.981318474 -8.797198296 +v -17.988256454 10.306262970 -8.999963760 +v -17.906917572 10.437708855 -9.464529037 +v -19.052888870 10.186120033 -8.797198296 +v -18.977558136 10.542957306 -8.999963760 +v -18.303064346 9.591218948 -8.797198296 +v -19.137142181 9.787012100 -8.797198296 +v -18.936534882 10.674402237 -9.464529037 +v -17.528772354 9.268830299 -8.797198296 +v -17.674510956 9.016634941 -8.998647690 +v -17.674514771 9.015310287 -10.000000954 +v -16.987102509 8.594883919 -10.000000954 +v -18.422090530 9.322186470 -8.998647690 +v -19.196399689 9.506309509 -8.999963760 +v -19.196399689 9.506309509 -10.000000954 +v -18.422092438 9.320861816 -10.000000954 +v -19.987100601 10.247028351 -8.797198296 +v -19.987104416 10.611288071 -8.999963760 +v -19.987218857 10.742734909 -9.464529991 +v -20.919277191 10.204757690 -8.797198296 +v -20.990602493 10.542957306 -8.999963760 +v -21.031610489 10.674402237 -9.464529991 +v -19.987096786 9.860301971 -8.797198296 +v -19.987094879 9.568479538 -8.999963760 +v -19.987094879 9.568479538 -10.000000954 +v -20.833299637 9.797085762 -8.797198296 +v -20.771974564 9.506309509 -8.999963760 +v -20.771974564 9.506309509 -10.000000954 +v -22.961076736 9.440172195 -2.733668089 +v -22.961076736 9.578904152 -2.733668089 +v -23.991712570 10.375905991 -4.815942287 +v -23.991712570 10.237173080 -4.815942287 +v -24.191875458 10.020651817 -4.734813690 +v -22.061225891 10.437708855 -9.464529037 +v -22.825233459 9.943684578 -8.999963760 +v -21.979904175 10.306262970 -8.999963760 +v -22.941001892 10.075129509 -9.464529037 +v -24.184528351 9.222518921 -9.464529037 +v -23.716903687 8.977254868 -8.797198296 +v -22.674510956 9.680353165 -8.797198296 +v -23.991121292 9.119842529 -8.999963760 +v -22.465831757 9.279668808 -8.797198296 +v -23.286565781 8.751092911 -8.797198296 +v -22.293861389 9.015311241 -8.999963760 +v -22.987102509 8.594884872 -8.999963760 +v -22.987102509 8.594883919 -10.000000954 +v -21.665519714 9.591831207 -8.797198296 +v -21.546281815 9.320861816 -8.999963760 +v -21.546281815 9.320861816 -10.000000954 +v -21.853488922 10.018985748 -8.797198296 +v -22.293861389 9.015310287 -10.000000954 +v -24.985912323 10.014828682 -9.472431183 +v -24.769596100 10.370082855 -9.301564217 +v -24.769596100 10.231350899 -9.301564217 +v -24.985546112 5.355849743 -9.464529037 +v -22.944475174 4.205263615 -2.735925198 +v -22.944475174 4.005263329 -2.735925198 +v -23.230068207 4.204940796 -3.239145756 +v -23.229717255 4.405263901 -3.238497257 +v -23.969812393 5.005263329 -4.803016186 +v -23.741117477 4.604940891 -4.269253254 +v -23.969812393 4.805263042 -4.803016186 +v -23.741048813 4.805263042 -4.269100666 +v -23.495750427 4.605263710 -3.750710487 +v -23.496076584 4.404941082 -3.751371861 +v -23.991611481 5.102264881 -4.815990448 +v -23.286930084 4.999999523 -8.797198296 +v -23.716436386 4.999999523 -8.797197342 +v -23.989929199 4.999999523 -8.999961853 +v -24.769477844 4.986489296 -9.281723022 +v -22.987102509 4.999999523 -8.999961853 +v -16.987102509 4.999999523 -10.000000000 +v -22.987102509 4.999999523 -10.000000000 +v -24.184766769 5.355849266 -9.464529037 +v -24.769477844 5.111741543 -9.273554802 +v -24.104564667 5.111741066 -9.273554802 +v -24.104564667 4.999999523 -9.273554802 +v -25.015216827 10.370100975 -10.486470222 +v -24.881961823 10.370100975 -11.907939911 +v -23.635223389 11.418239594 -10.481653214 +v -23.514402390 11.418239594 -11.723038673 +v -22.558753967 11.879976273 -10.477895737 +v -23.272411346 11.424533844 -13.129491806 +v -22.233024597 11.885335922 -12.857720375 +v -22.447633743 11.879976273 -11.578805923 +v -21.201667786 12.180791855 -11.410345078 +v -21.301460266 12.180791855 -10.473506927 +v -21.016616821 12.186153412 -12.539662361 +v -24.441345215 10.568386078 -14.104910851 +v -23.334888458 11.570444107 -16.800724030 +v -23.111204147 11.616727829 -13.697996140 +v -24.618160248 10.376192093 -13.481369019 +v -22.102413177 12.618787766 -16.155851364 +v -22.083871841 12.077529907 -13.383714676 +v -20.881570816 12.378347397 -13.015907288 +v -21.150512695 13.079589844 -15.657785416 +v -20.648149490 13.604285240 -18.398828506 +v -21.739830017 12.555942535 -19.260852814 +v -19.804990768 14.065087318 -17.733043671 +v -18.818231583 14.365905762 -16.953866959 +v -20.036491394 13.380407333 -15.074891090 +v -18.906053543 13.467247963 -14.483409882 +v -19.661554337 12.465188980 -12.642681122 +v -19.782285690 12.272994041 -12.216917038 +v -18.435110092 12.378347397 -12.267487526 +v -20.018917084 12.267632484 -10.469030380 +v -19.930683136 12.267632484 -11.238502502 +v -18.541450500 12.186153412 -11.892474174 +v -18.666357040 12.180791855 -11.067559242 +v -18.743097305 12.180791855 -10.464576721 +v -17.325042725 11.885335922 -11.574416161 +v -17.232809067 12.077530861 -11.899678230 +v -17.485801697 11.879976273 -10.460188866 +v -17.420392990 11.879976273 -10.899098396 +v -17.816932678 14.452745438 -16.163211823 +v -17.286243439 15.370079041 -18.563083649 +v -18.112966537 15.069261551 -19.510362625 +v -18.819381714 14.608458519 -20.319784164 +v -19.734010696 13.560115814 -21.367786407 +v -17.351280212 14.565679550 -23.084133148 +v -15.823597908 14.065088272 -14.589212418 +v -14.777286530 15.069261551 -15.688269615 +v -14.978754044 13.603350639 -13.922099113 +v -14.069461823 14.607523918 -14.877229691 +v -13.895698547 12.553890228 -13.066884995 +v -15.701837540 12.617853165 -12.806856155 +v -16.655639648 13.079589844 -13.305916786 +v -14.479099274 11.568391800 -12.167078018 +v -16.203424454 11.615793228 -11.584771156 +v -14.883792877 10.566333771 -11.181069374 +v -14.948463440 10.374137878 -10.953004837 +v -16.411479950 11.419174194 -10.456439018 +v -16.355749130 11.419174194 -10.755152702 +v -15.020498276 10.372154236 -10.451583862 +v -16.283580780 11.423599243 -11.302101135 +v -17.769660950 13.380407333 -13.888811111 +v -16.810358047 14.365905762 -15.368389130 +v -16.447336197 15.456918716 -17.601848602 +v -14.820383072 16.462482452 -18.773788452 +v -15.604010582 15.370079041 -16.635547638 +v -13.534363747 16.074825287 -16.583580017 +v -14.170981407 16.375642776 -17.667797089 +v -12.989304543 15.613087654 -15.655296326 +v -12.030590057 16.410091400 -16.125663757 +v -12.431182861 16.871826172 -17.124828339 +v -10.526067734 16.404731750 -16.284149170 +v -12.290560722 14.563627243 -14.465271950 +v -10.530882835 15.363566399 -14.904547691 +v -11.517045021 15.360629082 -14.844776154 +v -13.162057877 13.558062553 -13.837507248 +v -15.466381073 16.375642776 -19.873981476 +v -12.899066925 17.172643661 -18.291830063 +v -13.376347542 17.259483337 -19.482267380 +v -10.531611443 16.881301880 -17.360651016 +v -10.527222633 17.182119370 -18.617944717 +v -13.851127625 17.172643661 -20.666463852 +v -14.319011688 16.871826172 -21.833465576 +v -10.518292427 17.182119370 -21.176307678 +v -10.522746086 17.268959045 -19.900487900 +v -16.102998734 16.074825287 -20.958198547 +v -16.646970749 15.614022255 -21.884632111 +v -14.718805313 16.411024094 -22.830635071 +v -15.236440659 15.362680435 -24.121723175 +v -10.513904572 16.881301880 -22.433601379 +v -10.496080399 15.356592178 -24.899217606 +v -10.510154724 16.420501709 -23.507926941 +v -24.705308914 10.238341331 -11.884346008 +v -24.409217834 10.244254112 -13.426673889 +v -24.409217834 10.382987022 -13.426673889 +v -24.705308914 10.377074242 -11.884346008 +v -24.881961823 10.021820068 -11.907939911 +v -24.618160248 10.027732849 -13.481369019 +v -25.015216827 10.021820068 -10.486470222 +v -24.800256729 10.238341331 -10.656588554 +v -24.800256729 10.377074242 -10.656588554 +v -24.213760376 9.214471817 -10.483672142 +v -22.980632782 10.067962646 -10.479368210 +v -22.016145706 10.299831390 -10.940568924 +v -22.863159180 9.936516762 -10.943525314 +v -22.099105835 10.431277275 -10.476290703 +v -21.872377396 9.974886894 -11.142833710 +v -22.702497482 9.658271790 -11.145730972 +v -24.015892029 9.111795425 -10.947548866 +v -23.017295837 8.588453293 -10.944063187 +v -22.329887390 9.008879662 -10.941663742 +v -22.333377838 9.008878708 -9.941633224 +v -23.712486267 8.947604179 -11.149256706 +v -23.020786285 8.588453293 -9.944032669 +v -22.474920273 9.262398720 -11.144936562 +v -23.273221970 8.724876404 -11.147723198 +v -21.069496155 10.667971611 -10.472697258 +v -20.950813293 10.179689407 -11.139616966 +v -21.700632095 9.584787369 -11.142234802 +v -21.026851654 10.536526680 -10.937115669 +v -20.017311096 10.604856491 -10.933591843 +v -20.018817902 10.736302376 -10.469030380 +v -20.016607285 10.240594864 -11.136356354 +v -21.582313538 9.314430237 -10.939054489 +v -21.585803986 9.314430237 -9.939023972 +v -20.866559982 9.780580521 -11.139323235 +v -20.811500549 9.499877930 -9.936321259 +v -20.808010101 9.499877930 -10.936351776 +v -20.016611099 9.853870392 -11.136356354 +v -20.017320633 9.562047958 -10.933591843 +v -20.020811081 9.562047958 -9.933561325 +v -24.234794617 10.436448097 -14.041786194 +v -24.234794617 10.575181007 -14.041786194 +v -24.441345215 10.219926834 -14.104910851 +v -23.143491745 11.577239990 -16.700647354 +v -23.143491745 11.438507080 -16.700645447 +v -23.334888458 11.221985817 -16.800722122 +v -23.334865570 6.547707558 -16.800710678 +v -24.881961823 5.349424362 -11.907939911 +v -24.213853836 5.349429607 -10.483672142 +v -24.618135452 5.353454590 -13.481361389 +v -24.409112930 5.109346390 -13.426637650 +v -24.705408096 5.105315208 -11.884212494 +v -24.705408096 4.993570805 -11.884212494 +v -24.401924133 4.997604370 -13.424758911 +v -24.014850616 4.993570805 -10.947545052 +v -24.799518585 4.993570805 -10.676690102 +v -24.799518585 5.105315685 -10.676690102 +v -24.132406235 5.105320454 -10.674362183 +v -24.132406235 4.993570805 -10.674362183 +v -25.015216827 5.349424362 -10.486470222 +v -23.715438843 4.993570805 -11.149267197 +v -23.272678375 4.993570805 -11.147721291 +v -23.017295837 4.993570805 -10.944065094 +v -23.020786285 4.993570805 -9.944033623 +v -17.017330170 4.993570805 -10.923122406 +v -16.716798782 4.993570328 -11.124839783 +v -17.020822525 4.993570805 -9.923090935 +v -16.287294388 4.993570328 -11.123340607 +v -24.220779419 4.993119717 -14.034821510 +v -22.850515366 6.193674564 -17.203773499 +v -22.850515366 6.393674374 -17.203773499 +v -23.128929138 6.193674564 -16.695180893 +v -22.555257797 6.593675137 -17.698482513 +v -23.143390656 6.303598881 -16.700599670 +v -23.386489868 5.993675709 -16.180047989 +v -23.128929138 5.993675232 -16.695180893 +v -23.386489868 5.793675423 -16.180047989 +v -23.624713898 5.793674946 -15.655849457 +v -24.441320419 5.545648098 -14.104903221 +v -24.220834732 5.193675995 -14.034641266 +v -24.043090820 5.393675804 -14.579807281 +v -24.234685898 5.301539421 -14.041759491 +v -23.624713898 5.593675613 -15.655849457 +v -23.844877243 5.593675137 -15.119219780 +v -23.844877243 5.393675804 -15.119219780 +v -24.043090820 5.193675041 -14.579807281 +v -15.017359734 5.192920685 -11.451532364 +v -15.017359734 5.392920494 -11.451531410 +v -15.085744858 4.993018150 -11.241790771 +v -15.087582588 5.191360474 -11.242877960 +v -15.156508446 4.997604847 -11.007375717 +v -14.941089630 5.592920303 -11.659092903 +v -14.941089630 5.392920494 -11.659092903 +v -18.974433899 10.667971611 -10.465384483 +v -19.084438324 10.198325157 -11.133102417 +v -19.013818741 10.536526680 -10.930088997 +v -18.150232315 10.012554169 -11.129841805 +v -19.170413971 9.790654182 -11.133402824 +v -18.338197708 9.585399628 -11.130497932 +v -17.944824219 10.431277275 -10.461791039 +v -18.024524689 10.299831390 -10.926635742 +v -19.235937119 9.499877930 -9.930821419 +v -19.232446671 9.499877930 -10.930851936 +v -18.461633682 9.314430237 -9.928118706 +v -17.710567474 9.008879662 -10.925539970 +v -18.458143234 9.314430237 -10.928149223 +v -17.537889481 9.273237228 -11.127704620 +v -17.329214096 9.673920631 -11.126976013 +v -17.714057922 9.008878708 -9.925509453 +v -14.667607307 6.192389011 -12.266146660 +v -14.558470726 6.192919731 -12.461206436 +v -14.764708519 5.992919922 -12.067286491 +v -14.665601730 5.992919922 -12.265502930 +v -14.444860458 6.392919540 -12.651563644 +v -14.444860458 6.592919350 -12.651563644 +v -14.558470726 6.392919540 -12.461206436 +v -22.555257797 6.393674374 -17.698482513 +v -14.856374741 5.592920303 -11.865580559 +v -14.856374741 5.792920113 -11.865581512 +v -14.764708519 5.792920113 -12.067286491 +v -21.562465668 7.193674088 -19.112043381 +v -21.562503815 6.993996620 -19.111997604 +v -14.062846184 6.992918968 -13.195486069 +v -21.209297180 7.193996429 -19.543546677 +v -14.062846184 7.192919254 -13.195486069 +v -21.919630051 6.993674278 -18.641155243 +v -21.919630051 6.793996811 -18.641153336 +v -14.200278282 6.792919159 -13.014291763 +v -21.209291458 7.393673897 -19.543550491 +v -13.926948547 7.192919254 -13.361523628 +v -22.237232208 6.793673992 -18.188739777 +v -22.237232208 6.593673706 -18.188739777 +v -14.322487831 6.592919350 -12.840208054 +v -14.322487831 6.792919636 -12.840208054 +v -14.200278282 6.992919445 -13.014291763 +v -20.829801559 7.593673706 -19.974193573 +v -17.351268768 9.542943001 -23.084110260 +v -15.236431122 10.339944839 -24.121700287 +v -17.351280212 14.217220306 -23.084133148 +v -19.734010696 13.211657524 -21.367786407 +v -19.591949463 13.566911697 -21.205101013 +v -21.570287704 12.562738419 -19.127052307 +v -19.591949463 13.428178787 -21.205101013 +v -21.570287704 12.424005508 -19.127052307 +v -21.739830017 12.207484245 -19.260852814 +v -19.733995438 8.537379265 -21.367767334 +v -21.739810944 7.533205509 -19.260837555 +v -21.570194244 7.289097309 -19.126987457 +v -20.829807281 7.393996716 -19.974185944 +v -19.591871262 8.293270111 -21.205020905 +v -20.434761047 7.793673515 -20.390405655 +v -19.997489929 7.793673515 -20.817251205 +v -19.997489929 7.993673325 -20.817251205 +v -20.434764862 7.593996525 -20.390401840 +v -19.580345154 8.193674088 -21.194742203 +v -19.127519608 8.393672943 -21.574895859 +v -19.127519608 8.193674088 -21.574895859 +v -19.580345154 7.993673325 -21.194742203 +v -18.677282333 8.593671799 -21.925027847 +v -17.241806030 9.298833847 -22.897821426 +v -18.213668823 8.798833847 -22.258960724 +v -17.734512329 8.798833847 -22.577890396 +v -17.734512329 8.998833656 -22.577890396 +v -18.213428497 8.594318390 -22.259124756 +v -18.677282333 8.393672943 -21.925027847 +v -12.399082184 14.570600510 -14.650138855 +v -11.596796989 15.228869438 -15.043756485 +v -12.399082184 14.431867599 -14.650139809 +v -11.596796989 15.367601395 -15.043754578 +v -12.290560722 14.165383339 -14.465271950 +v -13.302993774 13.565035820 -13.999031067 +v -13.302995682 13.426303864 -13.999031067 +v -13.162057877 13.159818649 -13.837507248 +v -11.198131561 14.658274651 -17.216905594 +v -11.201657295 13.947606087 -16.206914902 +v -10.995925903 14.936519623 -17.056241989 +v -10.999949455 14.111796379 -15.903511047 +v -10.531768799 15.067964554 -16.938766479 +v -10.536072731 14.214473724 -15.705640793 +v -11.517045021 14.962385178 -14.844776154 +v -10.701003075 15.363566399 -15.119117737 +v -10.701003075 15.224834442 -15.119117737 +v -10.538870811 14.971858978 -14.904184341 +v -14.669025421 11.575365067 -12.266479492 +v -14.063923836 12.560862541 -13.199748993 +v -14.669026375 11.436632156 -12.266481400 +v -13.895698547 12.155645370 -13.066884995 +v -14.479099274 11.170148849 -12.167078018 +v -14.063923836 12.422130585 -13.199749947 +v -15.088773727 10.573307037 -11.243801117 +v -12.290559769 9.494309425 -14.465270042 +v -10.540185928 10.300786018 -14.904186249 +v -10.537387848 10.350753784 -15.705550194 +v -13.162055969 8.488745689 -13.837506294 +v -11.517044067 10.291310310 -14.844774246 +v -11.200123787 13.724878311 -16.646181107 +v -10.996463776 13.588455200 -16.902107239 +v -10.997779846 9.993570328 -16.902109146 +v -11.001261711 9.993570328 -15.904552460 +v -11.201436996 9.993570328 -16.646726608 +v -11.202982903 9.993570328 -16.203966141 +v -17.065053940 10.068697929 -10.458720207 +v -17.179199219 9.937252045 -10.923685074 +v -15.821536064 9.216087341 -10.454379082 +v -16.286827087 8.970824242 -11.123337746 +v -16.013319016 9.113410950 -10.919615746 +v -15.020498276 10.023696899 -10.451583862 +v -15.155863762 10.381111145 -11.007209778 +v -15.235881805 10.378950119 -10.623204231 +v -15.155864716 10.242379189 -11.007210732 +v -15.235881805 10.240218163 -10.623204231 +v -14.948463440 10.025856972 -10.953004837 +v -14.883792877 10.168089867 -11.181069374 +v -15.088775635 10.434574127 -11.243801117 +v -14.479097366 6.499074459 -12.167078018 +v -13.895696640 7.484572411 -13.066883087 +v -11.808470726 9.598077774 -14.950302124 +v -12.008268356 9.598077774 -14.856611252 +v -11.808470726 9.798078537 -14.950302124 +v -12.399416924 9.300163269 -14.650714874 +v -11.602720261 9.798077583 -15.037611008 +v -11.602720261 9.998077393 -15.037611008 +v -11.597042084 10.097164154 -15.044374466 +v -10.730405807 10.106639862 -15.119886398 +v -10.730405807 9.993570328 -15.119886398 +v -10.728077888 10.106645584 -15.786997795 +v -16.717163086 8.744661331 -11.124839783 +v -17.017330170 8.588453293 -10.923120499 +v -17.020820618 8.588453293 -9.923089981 +v -14.883790970 5.497014999 -11.181069374 +v -14.064443588 7.290426254 -13.200163841 +v -14.669614792 6.304928303 -12.266791344 +v -13.303430557 8.294599533 -13.999534607 +v -12.774266243 8.798078537 -14.406379700 +v -12.589893341 8.798078537 -14.529100418 +v -12.589893341 8.998079300 -14.529100418 +v -12.774162292 8.592917442 -14.406455994 +v -12.952661514 8.592917442 -14.277889252 +v -12.952661514 8.392917633 -14.277889252 +v -13.125906944 8.392917633 -14.143162727 +v -12.399484634 9.198078156 -14.645851135 +v -12.205717087 9.398078918 -14.754933357 +v -12.008268356 9.398078918 -14.856611252 +v -12.205718040 9.198078156 -14.754934311 +v -12.399484634 8.998079300 -14.645851135 +v -16.014513016 4.993570328 -10.919622421 +v -14.948462486 5.354783058 -10.953003883 +v -15.020523071 5.349418640 -10.451583862 +v -15.821298599 5.349417686 -10.454379082 +v -15.235923767 4.993570328 -10.643310547 +v -15.900829315 5.105309486 -10.645630836 +v -15.900829315 4.993570328 -10.645630836 +v -15.235923767 5.105309963 -10.643310547 +v -15.156508446 5.110674858 -11.007375717 +v -15.089410782 5.302869320 -11.243998528 +v -13.926948547 7.392919064 -13.361523628 +v -13.780925751 7.592919350 -13.527230263 +v -13.628918648 7.592919350 -13.687385559 +v -13.628918648 7.792919159 -13.687385559 +v -13.780925751 7.392919064 -13.527230263 +v -13.125906944 8.192918777 -14.143162727 +v -13.300148010 8.192917824 -13.996884346 +v -13.300148010 7.992918491 -13.996884346 +v -13.460660934 7.992918968 -13.851630211 +v -13.460660934 7.792919159 -13.851630211 +v -10.728077888 9.993570328 -15.786997795 +v -11.195234299 14.974888802 -18.047021866 +v -10.992969513 15.299833298 -17.903255463 +v -10.528691292 15.431279182 -17.820295334 +v -11.192017555 15.179690361 -18.968587875 +v -10.989516258 15.536527634 -18.892549515 +v -11.194635391 14.584789276 -18.218769073 +v -11.191723824 14.780582428 -19.052841187 +v -10.525097847 15.667972565 -18.849905014 +v -11.197337151 14.262400627 -17.444480896 +v -10.995380402 14.010205269 -17.589515686 +v -9.994033813 14.008880615 -17.586023331 +v -9.996433258 13.588454247 -16.898616791 +v -10.992771149 14.315756798 -18.337091446 +v -10.988752365 14.499879837 -19.111391068 +v -9.988721848 14.499879837 -19.107900620 +v -9.991424561 14.314432144 -18.333597183 +v -11.188756943 15.240598679 -19.902793884 +v -10.985992432 15.604858398 -19.902090073 +v -10.521430016 15.736305237 -19.900583267 +v -11.185503006 15.198328018 -20.834964752 +v -10.982489586 15.536527634 -20.905582428 +v -10.517784119 15.667972565 -20.944969177 +v -11.188756943 14.853872299 -19.902790070 +v -10.985992432 14.562049866 -19.902080536 +v -9.985961914 14.562049866 -19.898590088 +v -11.185803413 14.790656090 -20.748987198 +v -10.983252525 14.499879837 -20.686954498 +v -9.983222008 14.499879837 -20.683464050 +v -17.241868973 14.433742523 -22.897914886 +v -17.241868973 14.572474480 -22.897914886 +v -15.156010628 15.369476318 -23.921278000 +v -15.156010628 15.230743408 -23.921278000 +v -15.236440659 15.014222145 -24.121723175 +v -10.514191628 15.431279182 -21.974576950 +v -10.976085663 14.937254906 -22.740201950 +v -10.979036331 15.299833298 -21.894876480 +v -10.511120796 15.068699837 -22.854347229 +v -10.506779671 14.216089249 -24.097867966 +v -11.175738335 13.970825195 -23.632574081 +v -11.179376602 14.673923492 -22.590187073 +v -10.972016335 14.113412857 -23.906082153 +v -11.180105209 14.273239136 -22.381509781 +v -11.177240372 13.744663239 -23.202238083 +v -10.977940559 14.008881569 -22.208833694 +v -10.975521088 13.588455200 -22.902070999 +v -9.975490570 13.588454247 -22.898580551 +v -11.182898521 14.585401535 -21.581203461 +v -10.980549812 14.314432144 -21.461257935 +v -9.980519295 14.314432144 -21.457767487 +v -11.182242393 15.012556076 -21.769170761 +v -9.977910042 14.008880615 -22.205343246 +v -10.496080399 15.008399010 -24.899217606 +v -10.667700768 15.363653183 -24.683498383 +v -10.667700768 15.224921227 -24.683498383 +v -10.503984451 10.349420547 -24.898880005 +v -17.239669800 9.198833466 -22.881307602 +v -17.239669800 8.998833656 -22.881307602 +v -16.735456467 9.198511124 -23.165142059 +v -16.736104965 9.398834229 -23.164793015 +v -15.169013023 9.998833656 -23.899423599 +v -15.703571320 9.598510742 -23.672592163 +v -15.169013023 9.798833847 -23.899423599 +v -15.703723907 9.798833847 -23.672523499 +v -16.222967148 9.598834038 -23.429035187 +v -16.222305298 9.398511887 -23.429361343 +v -15.155962944 10.095834732 -23.921176910 +v -11.177239418 9.993570328 -23.202602386 +v -11.175741196 9.993570328 -23.632106781 +v -10.972023010 9.993570328 -23.904890060 +v -10.687541962 9.980059624 -24.683450699 +v -10.975522995 9.993570328 -22.902070999 +v -9.996434212 9.993570328 -16.898616791 +v -9.975491524 9.993570328 -22.898578644 +v -10.506779671 10.349419594 -24.098104477 +v -10.695711136 10.105312347 -24.683479309 +v -10.698031425 10.105311394 -24.018569946 +v -10.698031425 9.993570328 -24.018569946 +v 18.151639938 16.422849655 -39.982330322 +v 20.731946945 15.374711037 -40.315002441 +v 18.820676804 15.374711037 -38.775352478 +v 19.747991562 16.422849655 -41.182910919 +v 17.629753113 16.884586334 -40.923835754 +v 18.982986450 16.884586334 -41.973312378 +v 17.020200729 17.185401917 -42.023498535 +v 18.176250458 17.185401917 -42.818588257 +v 22.158515930 16.422847748 -44.359470367 +v 19.015058517 17.272243500 -45.918804169 +v 20.193902969 17.185401917 -45.329006195 +v 21.180492401 16.884586334 -44.811431885 +v 23.330167770 15.374711037 -43.793838501 +v 22.288990021 16.884586334 -48.230606079 +v 23.356256485 16.422849655 -48.090103149 +v 24.724452972 15.374711037 -47.909973145 +v 21.132287979 17.185401917 -49.331924438 +v 22.389589310 16.884586334 -49.331924438 +v 23.463920593 16.423784256 -49.331924438 +v 24.854907990 15.376764297 -49.331924438 +v 21.042442322 17.185401917 -48.394718170 +v 18.505952835 17.185401917 -48.728652954 +v 19.770866394 17.272243500 -48.562126160 +v 19.856460571 17.272243500 -49.331924438 +v 18.573909760 17.185401917 -49.331924438 +v 17.286430359 17.272243500 -43.806549072 +v 16.426416397 17.185401917 -44.773418427 +v 17.777982712 17.185401917 -46.515922546 +v 16.398406982 17.272243500 -43.145240784 +v 16.772066116 16.884586334 -47.060039520 +v 17.259407043 16.884586334 -48.892765045 +v 15.817419052 16.423784256 -47.575325012 +v 15.601982117 16.884586334 -45.628391266 +v 16.194267273 16.423784256 -49.032993317 +v 16.240131378 16.422849655 -49.331924438 +v 17.316606522 16.884586334 -49.331924438 +v 14.912005424 16.423784256 -46.542243958 +v 13.975111961 15.376764297 -47.516983032 +v 14.464689255 15.376764297 -48.408912659 +v 14.649476051 16.423784256 -46.300395966 +v 15.170322418 16.884586334 -45.360763550 +v 14.860130310 15.374711037 -49.331924438 +v 6.673092842 15.374712944 -32.725341797 +v 17.220638275 16.422851563 -39.476814270 +v 17.890140533 15.381685257 -38.270545959 +v 5.997828484 16.423786163 -33.941822052 +v 16.689041138 16.884586334 -40.382247925 +v 5.476475239 16.884588242 -34.881168365 +v 16.078891754 17.185401917 -41.481575012 +v 4.866324902 17.185403824 -35.980499268 +v 15.465692520 17.272245407 -42.638774872 +v 4.247186661 17.272245407 -37.096027374 +v 14.837350845 17.185401917 -43.718509674 +v 15.779872894 17.185401917 -44.261104584 +v 3.624784470 17.185403824 -38.217433929 +v 14.227202415 16.884586334 -44.817840576 +v 3.014636040 16.884588242 -39.316761017 +v 13.715051651 16.423786163 -45.792980194 +v 3.933006287 17.185401917 -35.473697662 +v 3.317041874 17.185401917 -35.048015594 +v 2.558747768 17.272243500 -35.967140198 +v 4.542557240 16.884586334 -34.374034882 +v 4.134359837 16.884586334 -33.951190948 +v 3.314471245 17.272243500 -36.589561462 +v 2.492240429 16.422851563 -40.257987976 +v 13.039787292 15.374712944 -47.009456635 +v 1.822737694 15.381685257 -41.464256287 +v 1.726429939 17.185401917 -36.979194641 +v 0.669525146 17.272243500 -33.767730713 +v -0.472972870 17.185401917 -34.307502747 +v 0.819322586 16.884586334 -37.894798279 +v 2.692678452 17.185401917 -37.711303711 +v 1.841757774 17.185401917 -33.227012634 +v 3.769361496 16.423784256 -32.242866516 +v 5.248190880 15.376764297 -31.325889587 +v 4.852749825 15.374711037 -30.402873993 +v 3.518613338 16.423784256 -30.701808929 +v 2.890336990 16.884586334 -32.690055847 +v 3.472748280 16.422849655 -30.402873993 +v 2.453473568 16.884586334 -30.842037201 +v 4.828486443 16.423784256 -33.241661072 +v 5.737767696 15.376764297 -32.217819214 +v 5.063403130 16.423784256 -33.434406281 +v 2.396272659 16.884586334 -30.402873993 +v 1.206927299 17.185401917 -31.006147385 +v 1.138970375 17.185401917 -30.402873993 +v -0.057985306 17.272243500 -31.172676086 +v -0.143579483 17.272243500 -30.402873993 +v -1.329563141 17.185401917 -31.340084076 +v -1.419406891 17.185401917 -30.402873993 +v -2.576108932 16.884586334 -31.504192352 +v -1.386344910 16.884586334 -34.814693451 +v -2.676710129 16.884586334 -30.402873993 +v -3.751039505 16.423784256 -30.402873993 +v -3.643377304 16.422849655 -31.644699097 +v -5.011572838 15.374711037 -31.824825287 +v -5.142028809 15.376764297 -30.402873993 +v -2.529266357 16.422849655 -35.398258209 +v -3.617288589 15.374711037 -35.940959930 +v -1.115442276 15.374711037 -39.509117126 +v -0.062291145 16.422849655 -38.602146149 +v 1.561239243 16.422849655 -39.752471924 +v 0.892202377 15.374711037 -40.959449768 +v 2.083126068 16.884586334 -38.810966492 +v 18.820676804 15.026430130 -38.775352478 +v 18.866195679 15.242951393 -39.045677185 +v 20.609476089 15.242951393 -40.444480896 +v 18.866195679 15.381684303 -39.045677185 +v 20.609476089 15.381684303 -40.444480896 +v 18.432121277 14.219081879 -39.476325989 +v 18.284229279 14.662881851 -41.119606018 +v 18.560924530 13.729486465 -40.620433807 +v 18.173898697 14.267008781 -41.318649292 +v 17.834285736 15.072572708 -40.554851532 +v 18.743295670 14.116405487 -39.873195648 +v 18.184436798 14.941126823 -40.881404877 +v 18.773887634 13.952214241 -40.236244202 +v 18.259162903 13.593063354 -40.746593475 +v 17.406908035 15.435887337 -41.325855255 +v 17.773792267 15.304441452 -41.622222900 +v 16.470603943 15.435888290 -40.828010559 +v 15.970950127 15.672584534 -41.728263855 +v 15.549583435 15.541138649 -41.530700684 +v 16.083345413 14.277850151 -40.150325775 +v 16.690469742 13.975436211 -39.056442261 +v 16.184612274 14.678532600 -39.967868805 +v 15.694966316 14.590011597 -40.850086212 +v 15.786183357 15.017166138 -40.685733795 +v 17.000125885 14.116407394 -38.917198181 +v 16.440719604 14.941128731 -39.925106049 +v 17.890331268 15.026432037 -38.270202637 +v 17.636901855 15.381685257 -38.374713898 +v 17.636901855 15.242953300 -38.374713898 +v 17.501281738 14.220699310 -38.970996857 +v 16.897548676 15.073310852 -40.058773041 +v 17.384096146 13.593065262 -40.272407532 +v 16.515522003 13.593065262 -39.790332794 +v 16.181930542 14.013491631 -40.391376495 +v 17.881778717 14.979496956 -41.845649719 +v 17.050506592 14.013492584 -40.873451233 +v 17.925899506 14.013489723 -41.347816467 +v 16.688812256 14.319040298 -41.516834259 +v 16.481632233 13.749273300 -39.432716370 +v 15.819142342 14.319043159 -41.045024872 +v 16.029674530 15.304443359 -40.665699005 +v 15.332825661 15.202938080 -41.502567291 +v 15.443385124 14.504489899 -41.722049713 +v 15.291101456 14.795266151 -41.577743530 +v 20.731946945 15.026430130 -40.315002441 +v 20.731946945 10.354034424 -40.315002441 +v 18.775318146 9.998181343 -40.233661652 +v 18.742792130 9.998181343 -39.874103546 +v 18.820676804 10.354034424 -38.775352478 +v 16.999622345 9.998181343 -38.918109894 +v 16.690242767 9.998181343 -39.056854248 +v 18.432167053 10.354040146 -39.476242065 +v 17.501123428 10.354027748 -38.971282959 +v 17.890331268 10.354028702 -38.270202637 +v 20.609426498 9.998181343 -40.444320679 +v 18.883455276 9.998181343 -39.056011200 +v 18.560031891 9.998181343 -39.639484406 +v 18.883455276 10.109926224 -39.056011200 +v 20.609426498 10.109925270 -40.444320679 +v 18.560031891 10.109930038 -39.639484406 +v 18.259164810 9.998181343 -40.746593475 +v 18.560661316 9.998181343 -40.620910645 +v 16.515522003 9.998181343 -39.790332794 +v 17.384096146 9.998181343 -40.272407532 +v 17.295192719 10.109919548 -38.948501587 +v 17.295192719 9.998066902 -38.948501587 +v 17.619159698 10.109919548 -38.365627289 +v 17.619159698 9.998067856 -38.365627289 +v 16.481809616 9.998181343 -39.432395935 +v 13.603816986 9.998181343 -45.036472321 +v 6.109062672 9.998181343 -34.698329926 +v 6.673092842 15.026519775 -32.725341797 +v 6.717516899 15.243041992 -32.997402191 +v 6.717516899 15.381774902 -32.997402191 +v 6.672841549 10.354034424 -32.725624084 +v 6.734807014 10.109926224 -33.007129669 +v 6.627419472 9.998181343 -34.183063507 +v 6.595716476 9.998181343 -33.821502686 +v 6.734807014 9.998067856 -33.007129669 +v 6.412554264 9.998181343 -34.570194244 +v 23.172214508 15.242951393 -43.876388550 +v 23.330167770 15.026430130 -43.793838501 +v 23.172214508 15.381684303 -43.876388550 +v 23.330167770 10.354034424 -43.793838501 +v 23.172218323 10.109925270 -43.876220703 +v 23.172218323 9.998180389 -43.876220703 +v 15.382289886 9.998180389 -46.354846954 +v 24.547792435 15.242951393 -47.933521271 +v 24.724452972 15.026430130 -47.909973145 +v 24.547792435 15.381684303 -47.933521271 +v 23.586244583 13.975434303 -48.664596558 +v 22.335172653 14.277847290 -48.664596558 +v 23.155904770 13.749271393 -48.664596558 +v 24.053865433 14.220697403 -49.331924438 +v 22.694572449 14.941862106 -48.867359161 +v 23.860460281 14.118021011 -48.867359161 +v 22.543849945 14.678530693 -48.664596558 +v 22.810340881 15.073307991 -49.331924438 +v 24.638927460 15.383560181 -49.161056519 +v 24.638927460 15.244828224 -49.161056519 +v 24.854907990 15.028306961 -49.331924438 +v 21.930564880 15.435887337 -49.331924438 +v 20.900951385 15.672581673 -49.331924438 +v 22.856441498 13.593063354 -49.867401123 +v 22.856443405 13.593063354 -48.867359161 +v 22.163200378 14.013489723 -48.867359161 +v 22.163200378 14.013488770 -49.867397308 +v 21.415620804 14.319040298 -48.867359161 +v 21.415620804 14.319040298 -49.867397308 +v 21.534858704 14.590009689 -48.664596558 +v 21.722827911 15.017164230 -48.664596558 +v 20.788616180 15.202935219 -48.664596558 +v 21.849243164 15.304441452 -48.867359161 +v 20.859943390 15.541136742 -48.867359161 +v 19.856441498 15.245204926 -48.664596558 +v 19.856443405 15.609466553 -48.867359161 +v 19.856559753 15.740912437 -49.331924438 +v 20.641313553 14.504487991 -49.867401123 +v 20.641313553 14.504487991 -48.867359161 +v 20.702638626 14.795264244 -48.664596558 +v 19.856433868 14.566658020 -48.867359161 +v 19.856433868 14.566658020 -49.867397308 +v 19.856437683 14.858480453 -48.664596558 +v 19.065740585 14.504487991 -49.867397308 +v 19.065740585 14.504487991 -48.867359161 +v 19.006483078 14.785190582 -48.664596558 +v 18.922229767 15.184299469 -48.664596558 +v 23.156269073 9.998180389 -48.664596558 +v 22.856441498 9.998180389 -48.867359161 +v 16.856441498 9.998180389 -48.867359161 +v 16.600349426 9.998180389 -48.664596558 +v 16.856441498 9.998178482 -49.867397308 +v 22.856449127 9.998178482 -49.867393494 +v 24.547851563 10.109925270 -47.933364868 +v 24.547851563 9.998180389 -47.933364868 +v 24.724452972 10.354034424 -47.909973145 +v 23.585773468 9.998180389 -48.664596558 +v 23.859268188 9.998180389 -48.867359161 +v 24.854885101 10.354028702 -49.331924438 +v 24.054103851 10.354027748 -49.331924438 +v 24.638816833 10.109920502 -49.140953064 +v 24.638816833 9.998180389 -49.140953064 +v 23.973905563 10.109919548 -49.140953064 +v 23.973905563 9.998180389 -49.140953064 +v 15.350305557 9.998180389 -45.994308472 +v 15.059673309 14.566659927 -42.413398743 +v 16.311960220 14.504489899 -42.204124451 +v 15.930086136 14.566658020 -42.885616302 +v 15.549570084 14.504487991 -43.572086334 +v 14.678782463 14.504489899 -43.099658966 +v 16.804738998 14.566658020 -43.370445251 +v 17.188076019 14.504487991 -42.678886414 +v 16.907741547 15.672581673 -42.226379395 +v 17.294172287 15.541136742 -42.487483978 +v 17.434993744 15.184299469 -42.651672363 +v 16.398359299 15.740912437 -43.145328522 +v 16.804733276 15.609466553 -43.370452881 +v 16.982080460 14.858480453 -43.468750000 +v 17.394145966 14.785190582 -42.725360870 +v 16.982078552 15.245204926 -43.468750000 +v 17.798515320 14.589397430 -41.995861053 +v 17.563465118 14.319040298 -42.001663208 +v 16.571832657 14.795264244 -44.208854675 +v 16.318229675 15.541136742 -44.248130798 +v 16.530149460 15.202935219 -44.284049988 +v 15.892028809 15.672581673 -44.058773041 +v 15.392861366 15.435887337 -44.959293365 +v 16.077236176 15.017164230 -45.101131439 +v 15.838605881 15.304441452 -45.113391876 +v 15.057744980 15.609468460 -42.416015625 +v 15.464122772 15.740915298 -42.641433716 +v 14.572686195 15.541138649 -43.290817261 +v 14.954242706 15.672584534 -43.560108185 +v 14.092594147 15.304443359 -44.155822754 +v 14.454587936 15.435888290 -44.460357666 +v 14.880453110 14.858482361 -42.317623138 +v 14.880453110 15.245207787 -42.317619324 +v 14.427095413 15.184301376 -43.134456635 +v 14.467983246 14.785192490 -43.060787201 +v 14.303024292 14.319043159 -43.776679993 +v 14.063219070 14.589399338 -43.790069580 +v 15.171600342 14.319043159 -44.258758545 +v 16.424221039 14.504487991 -44.056915283 +v 14.808811188 14.013492584 -44.912406921 +v 16.048828125 14.319040298 -44.734138489 +v 16.168361664 14.590009689 -44.936729431 +v 13.682369232 14.941864014 -44.894939423 +v 13.979873657 14.979497910 -43.940235138 +v 13.577028275 14.662883759 -44.666057587 +v 13.687467575 14.267010689 -44.467075348 +v 13.940235138 14.013491631 -44.430332184 +v 13.603816032 13.593065262 -45.036472321 +v 14.966337204 15.073307991 -45.728763580 +v 15.173831940 13.975434303 -46.730911255 +v 15.780364990 14.277847290 -45.636699677 +v 15.382465363 13.749271393 -46.354526520 +v 15.686395645 14.013489723 -45.387985229 +v 15.350305557 13.593063354 -45.994308472 +v 14.863548279 14.118021011 -46.872444153 +v 15.679196358 14.678530693 -45.819213867 +v 14.363464355 14.220697403 -46.816375732 +v 15.428781509 14.941862106 -45.852733612 +v 14.475652695 13.593063354 -45.509483337 +v 17.776258469 15.435887337 -49.331924438 +v 16.894725800 15.072572708 -49.331924438 +v 18.846897125 15.541136742 -48.867359161 +v 18.805873871 15.672581673 -49.331924438 +v 17.857597351 15.304441452 -48.867359161 +v 18.000658035 14.979496956 -48.664596558 +v 18.172405243 14.589397430 -48.664596558 +v 18.291433334 14.319040298 -48.867359161 +v 18.291431427 14.319040298 -49.867397308 +v 17.398113251 14.267008781 -48.664596558 +v 16.856441498 13.593063354 -48.867359161 +v 17.543853760 14.013489723 -48.867359161 +v 17.170534134 14.662881851 -48.664596558 +v 17.543853760 14.013488770 -49.867397308 +v 15.661590576 14.219081879 -49.331924438 +v 17.010580063 14.941126823 -48.867359161 +v 15.857839584 14.116405487 -48.867359161 +v 16.160539627 13.952214241 -48.664596558 +v 16.599805832 13.729486465 -48.664596558 +v 16.856441498 13.593063354 -49.867397308 +v 13.975111961 15.028306961 -47.516983032 +v 14.229267120 15.383560181 -47.410919189 +v 14.229267120 15.244828224 -47.410919189 +v 14.711436272 15.244828224 -48.280220032 +v 14.711436272 15.383560181 -48.280220032 +v 14.464689255 15.028306961 -48.408912659 +v 14.860130310 15.026430130 -49.331924438 +v 15.074495316 15.242951393 -49.161056519 +v 15.074495316 15.381684303 -49.161056519 +v 15.661497116 10.354040146 -49.331924438 +v 14.464710236 10.354028702 -48.408901215 +v 14.860130310 10.354034424 -49.331924438 +v 13.975122452 10.354028702 -47.516960144 +v 14.472393036 9.998181343 -45.518547058 +v 14.864127159 9.998180389 -46.871398926 +v 15.174060822 9.998180389 -46.730503082 +v 14.363349915 10.354027748 -46.816581726 +v 14.569259644 10.109919548 -46.839023590 +v 14.246905327 10.109920502 -47.420566559 +v 14.569259644 9.998180389 -46.839023590 +v 14.246905327 9.998180389 -47.420566559 +v 15.858880043 9.998180389 -48.867359161 +v 16.157585144 9.998180389 -48.664596558 +v 14.709774971 10.109920502 -48.279747009 +v 14.709774971 9.998180389 -48.279747009 +v 15.742279053 10.109930038 -49.140953064 +v 15.742279053 9.998180389 -49.140953064 +v 15.075164795 10.109925270 -49.140953064 +v 15.075164795 9.998180389 -49.140953064 +v 13.039787292 15.026519775 -47.009456635 +v 12.995363235 15.381774902 -46.737400055 +v 12.995363235 15.243041992 -46.737400055 +v 13.428371429 14.219083786 -46.309329987 +v 14.026791573 15.072574615 -45.231132507 +v 13.116581917 14.118022919 -45.914340973 +v 13.086893082 13.952216148 -45.549156189 +v 13.300060272 13.729488373 -45.165081024 +v 13.300325394 9.998181343 -45.164604187 +v 3.197357178 9.998181343 -39.944465637 +v 13.085459709 9.998181343 -45.551734924 +v 3.231069088 9.998181343 -40.302406311 +v 13.428532600 10.354040146 -46.309043884 +v 13.117163658 9.998181343 -45.913299561 +v 13.040038109 10.354034424 -47.009178162 +v 13.299910545 10.109930038 -46.146965027 +v 13.299910545 9.998067856 -46.146965027 +v 12.978073120 9.998067856 -46.727668762 +v 12.978073120 10.109926224 -46.727668762 +v 4.746542931 15.073307991 -34.006038666 +v 6.284508705 14.219083786 -33.425472260 +v 5.686088085 15.072574615 -34.503669739 +v 6.030510902 14.941864014 -34.839862823 +v 5.258291245 15.435888290 -35.274444580 +v 6.596298218 14.118022919 -33.820461273 +v 5.237226486 13.593063354 -34.225318909 +v 4.904067993 14.013492584 -34.822391510 +v 6.109063148 13.593065262 -34.698329926 +v 6.625987053 13.952216148 -34.185646057 +v 6.135851860 14.662883759 -35.068740845 +v 6.025412083 14.267010689 -35.267723083 +v 6.412818909 13.729488373 -34.569721222 +v 5.772644043 14.013491631 -35.304470062 +v 2.328782558 9.998181343 -39.462390900 +v 1.152217865 9.998181343 -39.113891602 +v 4.330589294 9.998180389 -33.379955292 +v 4.362574100 9.998180389 -33.740489960 +v 5.237226486 9.998180389 -34.225318909 +v 6.284347057 10.354040146 -33.425758362 +v 5.349416256 14.220697403 -32.918426514 +v 6.412969112 9.998067856 -33.587833405 +v 6.412969112 10.109930038 -33.587833405 +v 5.001443863 15.244828224 -31.454582214 +v 5.001443863 15.383560181 -31.454582214 +v 5.483613491 15.383560181 -32.323879242 +v 5.483613491 15.244828224 -32.323879242 +v 5.737767696 15.028306961 -32.217819214 +v 5.248190880 15.028306961 -31.325889587 +v 4.852749825 15.026430130 -30.402873993 +v 4.638384819 15.242951393 -30.573743820 +v 4.638384819 15.381684303 -30.573743820 +v 4.051288605 14.219081879 -30.402873993 +v 2.818153858 15.072572708 -30.402875900 +v 2.702300072 14.941126823 -30.867439270 +v 1.936620712 15.435887337 -30.402873993 +v 1.855282784 15.304441452 -30.867439270 +v 2.542345047 14.662881851 -31.070205688 +v 1.712222099 14.979496956 -31.070205688 +v 3.855039597 14.116405487 -30.867441177 +v 3.552340508 13.952214241 -31.070205688 +v 2.856437206 13.593063354 -30.867441177 +v 2.314766884 14.267008781 -31.070205688 +v 3.113073349 13.729486465 -31.070205688 +v 2.856437206 13.593063354 -29.867403030 +v 2.169026375 14.013488770 -29.867403030 +v 0.907005310 15.672581673 -30.402873993 +v 0.865982056 15.541136742 -30.867441177 +v 0.790650368 15.184299469 -31.070205688 +v -0.143679619 15.740912437 -30.402873993 +v -0.143562317 15.609466553 -30.867441177 +v -0.143560410 15.245204926 -31.070205688 +v 1.540474892 14.589397430 -31.070205688 +v 2.169026375 14.013489723 -30.867439270 +v 1.421446800 14.319040298 -30.867441177 +v 1.421447754 14.319040298 -29.867403030 +v 0.706397057 14.785190582 -31.070205688 +v 0.647138596 14.504487991 -30.867441177 +v 0.647139549 14.504487991 -29.867403030 +v 4.320018768 15.435887337 -34.775508881 +v 3.820850372 15.672581673 -35.676029205 +v 3.874273777 15.304441452 -34.621406555 +v 3.394650459 15.541136742 -35.486671448 +v 4.284097672 14.941862106 -33.882064819 +v 4.849331856 14.118021011 -32.862358093 +v 4.033683300 14.678530693 -33.915588379 +v 4.026483536 14.013489723 -34.346813202 +v 4.330414295 13.749271393 -33.380271912 +v 3.932514668 14.277847290 -34.098102570 +v 4.362574577 13.593063354 -33.740489960 +v 4.539047241 13.975434303 -33.003890991 +v 3.182729244 15.202935219 -35.450752258 +v 3.635644913 15.017164230 -34.633670807 +v 3.141046047 14.795264244 -35.525947571 +v 3.544517040 14.590009689 -34.798072815 +v 3.664050102 14.319040298 -35.000663757 +v 4.051383018 10.354040146 -30.402873993 +v 4.852749825 10.354034424 -30.402873993 +v 5.248169422 10.354028702 -31.325901031 +v 5.737757683 10.354028702 -32.217842102 +v 3.112530708 9.998180389 -31.070205688 +v 2.856438637 9.998180389 -30.867443085 +v 2.856437206 9.998180389 -29.867403030 +v 3.853999615 9.998180389 -30.867441177 +v 3.555293560 9.998180389 -31.070205688 +v 4.623218060 9.998180389 -30.557798386 +v 5.003104687 9.998180389 -31.455053329 +v 5.003104687 10.109920502 -31.455053329 +v 4.623218060 10.109920502 -30.557798386 +v 4.848752975 9.998180389 -32.863403320 +v 4.538818359 9.998180389 -33.004299164 +v 3.970601082 10.109930038 -30.593849182 +v 3.970601082 9.998180389 -30.593849182 +v 5.465974808 10.109920502 -32.314231873 +v 5.465974808 9.998180389 -32.314231873 +v 5.143620014 10.109919548 -32.895774841 +v 5.349530697 10.354027748 -32.918220520 +v 5.143620014 9.998180389 -32.895774841 +v 1.152847290 10.109930038 -40.095317841 +v 1.152847290 9.998181343 -40.095317841 +v 0.970087051 9.998181343 -39.860694885 +v 1.280713081 10.354040146 -40.258556366 +v 2.328783035 13.593065262 -39.462390900 +v 1.453714371 9.998181343 -38.988208771 +v 1.453716278 13.593063354 -38.988208771 +v 4.248755932 15.740915298 -37.093368530 +v 4.655133724 15.609468460 -37.318782806 +v 3.741929054 15.672584534 -38.006538391 +v 5.140193939 15.541138649 -36.443981171 +v 4.758635998 15.672584534 -36.174694061 +v 2.805136681 15.672581673 -37.508422852 +v 3.314519405 15.740912437 -36.589473724 +v 4.163295269 15.541138649 -38.204101563 +v 5.620284557 15.304443359 -35.578979492 +v 2.908140182 14.566658020 -36.364356995 +v 3.288658142 14.504487991 -35.677886963 +v 4.541278839 14.319043159 -35.476043701 +v 4.165520668 14.504489899 -36.153064728 +v 5.733006477 14.979497910 -35.794563293 +v 5.285783291 15.184301376 -36.600345612 +v 5.649660110 14.589399338 -35.944732666 +v 5.244895458 14.785192490 -36.674011230 +v 5.409854889 14.319043159 -35.958118439 +v 4.380053520 15.202938080 -38.232234955 +v 4.832426071 14.858482361 -37.417175293 +v 4.421778202 14.795266151 -38.157058716 +v 4.653206348 14.566659927 -37.321403503 +v 4.832426071 15.245207787 -37.417179108 +v 5.034096718 14.504489899 -36.635139465 +v 3.788245678 14.566660881 -36.817073822 +v 2.908144951 15.609466553 -36.364349365 +v 2.730801582 15.245204926 -36.266048431 +v 2.730799198 14.858480453 -36.266052246 +v 2.524804115 14.504487991 -37.055912018 +v 3.399456501 14.504487991 -37.540740967 +v 4.269494057 14.504489899 -38.012752533 +v 3.025161266 14.319043159 -38.207698822 +v 2.305970669 15.435887337 -38.408943176 +v 1.939085960 15.304441452 -38.112579346 +v 2.418706894 15.541136742 -37.247318268 +v 1.831100464 14.979496956 -37.889152527 +v 2.277885914 15.184299469 -37.083129883 +v 1.428648949 14.662881851 -38.615196228 +v 2.318733692 14.785190582 -37.009437561 +v 2.149413109 14.319040298 -37.733139038 +v 1.786979675 14.013489723 -38.386985779 +v 1.914364815 14.589397430 -37.738937378 +v 1.538980484 14.267008781 -38.416152954 +v -0.143557549 14.858480453 -31.070205688 +v -0.143554688 14.566658020 -30.867439270 +v -0.143553734 14.566658020 -29.867403030 +v -0.928434372 14.504487991 -29.867401123 +v -0.928434372 14.504487991 -30.867439270 +v -1.075737000 15.202935219 -31.070205688 +v -0.989757538 14.795264244 -31.070205688 +v -1.147062302 15.541136742 -30.867441177 +v -1.188070297 15.672581673 -30.402873993 +v -2.217683792 15.435887337 -30.402873993 +v -2.136363029 15.304441452 -30.867439270 +v -2.009947777 15.017164230 -31.070205688 +v -1.821979523 14.590009689 -31.070205688 +v -1.702740669 14.319040298 -29.867401123 +v -1.702741623 14.319040298 -30.867439270 +v -2.981693268 14.941862106 -30.867439270 +v -2.830970764 14.678530693 -31.070205688 +v -3.097460747 15.073307991 -30.402873993 +v -3.459335327 15.242951393 -35.858413696 +v -3.459335327 15.381684303 -35.858413696 +v -4.834913254 15.381684303 -31.801279068 +v -4.834913254 15.242951393 -31.801279068 +v -5.011572838 15.026430130 -31.824825287 +v -3.617288589 15.026430130 -35.940959930 +v -2.622293472 14.277847290 -31.070205688 +v -2.450321198 14.013489723 -30.867439270 +v -2.450321198 14.013488770 -29.867401123 +v -3.443025589 13.749271393 -31.070205688 +v -3.143564224 13.593063354 -30.867439270 +v -3.143561363 13.593063354 -29.867401123 +v -3.873365402 13.975434303 -31.070205688 +v -4.340984344 14.220697403 -30.402873993 +v -4.147581100 14.118021011 -30.867439270 +v -4.926047325 15.244828224 -30.573741913 +v -4.926047325 15.383560181 -30.573741913 +v -5.142028809 15.028306961 -30.402873993 +v -3.143563271 9.998180389 -30.867441177 +v -3.459339142 9.998180389 -35.858577728 +v -3.443388939 9.998180389 -31.070205688 +v -3.143561363 9.998180389 -29.867403030 +v -3.459339142 10.109925270 -35.858577728 +v -4.834972382 10.109925270 -31.801433563 +v -5.011572838 10.354034424 -31.824825287 +v -3.617288589 10.354034424 -35.940959930 +v -4.834972382 9.998180389 -31.801433563 +v -3.872895241 9.998180389 -31.070205688 +v -4.146388054 9.998180389 -30.867439270 +v -5.142004013 10.354028702 -30.402873993 +v -4.341223717 10.354027748 -30.402873993 +v -4.925933838 10.109920502 -30.593847275 +v -4.261025429 10.109919548 -30.593847275 +v -4.261025429 9.998180389 -30.593847275 +v -4.925933838 9.998180389 -30.593847275 +v 0.937560081 9.998181343 -39.501136780 +v -0.992921829 9.998181343 -39.379798889 +v -0.992971420 15.242951393 -39.379638672 +v -0.992971420 15.381684303 -39.379638672 +v -1.115442276 15.026430130 -39.509117126 +v -1.115442276 10.354034424 -39.509117126 +v -0.992921829 10.109925270 -39.379798889 +v 3.231245995 13.749273300 -40.302085876 +v 3.022637367 9.998181343 -40.677947998 +v 3.022408962 13.975436211 -40.678356171 +v 3.197357178 13.593065262 -39.944465637 +v 1.822546959 15.026432037 -41.464599609 +v 2.075977325 15.242953300 -41.360088348 +v 2.075977325 15.381685257 -41.360088348 +v 2.713257313 9.998181343 -40.816692352 +v 2.093720436 9.998067856 -41.369171143 +v 2.093720436 10.109919548 -41.369171143 +v 1.822546959 10.354028702 -41.464599609 +v 3.242274284 15.435888290 -38.906787872 +v 1.528442383 14.941126823 -38.853397369 +v 1.878594398 15.072572708 -39.179950714 +v 2.662373066 14.013492584 -38.861351013 +v 3.530948639 14.013491631 -39.343425751 +v 3.893736839 14.319043159 -38.689773560 +v 3.629533768 14.277850151 -39.584476471 +v 2.712753773 14.116407394 -40.817600250 +v 3.528266907 14.678532600 -39.766933441 +v 3.683204651 15.304443359 -39.069103241 +v 2.815331459 15.073310852 -39.676025391 +v 3.926695824 15.017166138 -39.049068451 +v 3.272159576 14.941128731 -39.809696198 +v 4.017912865 14.590011597 -38.884716034 +v 1.280758858 14.219081879 -40.258476257 +v 2.211596489 14.220699310 -40.763801575 +v 0.938992500 13.952214241 -39.498558044 +v 0.969583511 14.116405487 -39.861606598 +v 1.151953697 13.729486465 -39.114364624 +v 0.846682549 15.381684303 -40.689121246 +v 0.846682549 15.242951393 -40.689121246 +v 0.892202377 15.026430130 -40.959449768 +v 0.892202377 10.354034424 -40.959449768 +v 2.211755276 10.354027748 -40.763519287 +v 0.829423904 9.998181343 -40.678791046 +v 0.829423904 10.109926224 -40.678791046 +v 2.417686462 10.109919548 -40.786300659 +v 2.417686462 9.998066902 -40.786300659 +v 19.847118378 22.273571014 -69.295516968 +v 19.847118378 22.264093399 -64.533531189 +v 18.564567566 22.177253723 -64.533531189 +v 18.564567566 22.186729431 -69.295516968 +v 17.307264328 21.876436234 -64.533531189 +v 17.307264328 21.885911942 -69.295516968 +v 16.230789185 21.414699554 -64.533531189 +v 16.230791092 21.409341812 -69.304817200 +v 14.850788116 20.365238190 -64.533531189 +v 14.851180077 20.368175507 -69.304817200 +v 18.564567566 21.380252838 -62.923557281 +v 19.847118378 21.467092514 -62.923557281 +v 17.307264328 21.079437256 -62.923561096 +v 16.230789185 20.617698669 -62.923561096 +v 14.850788116 19.568237305 -62.923557281 +v 17.307264328 20.073871613 -60.915550232 +v 16.230789185 18.607959747 -58.926403046 +v 16.230789185 19.612133026 -60.915550232 +v 17.307264328 19.069698334 -58.926403046 +v 14.850787163 17.558500290 -58.926403046 +v 14.850787163 18.562671661 -60.915550232 +v 18.564567566 20.374689102 -60.915550232 +v 19.847118378 20.461528778 -60.915550232 +v 18.564565659 19.370515823 -58.926403046 +v 19.847118378 19.457355499 -58.926403046 +v 24.845911026 20.361202240 -69.304733276 +v 24.845569611 20.367290497 -64.533531189 +v 23.454578400 21.415634155 -64.533531189 +v 23.454578400 21.425109863 -69.295516968 +v 22.380249023 21.876436234 -64.533531189 +v 22.380249023 21.885911942 -69.295516968 +v 21.122945786 22.177253723 -64.533531189 +v 21.122945786 22.186729431 -69.295516968 +v 23.454578400 20.618633270 -62.923557281 +v 24.845569611 19.570289612 -62.923557281 +v 22.380249023 21.079435349 -62.923557281 +v 21.122945786 21.380252838 -62.923557281 +v 21.122945786 20.374689102 -60.915550232 +v 22.380249023 20.073871613 -60.915550232 +v 21.122945786 19.370515823 -58.926403046 +v 22.380249023 19.069698334 -58.926403046 +v 24.845569611 18.564725876 -60.915550232 +v 23.454578400 18.608894348 -58.926403046 +v 23.454578400 19.613067627 -60.915550232 +v 24.845569611 17.560552597 -58.926403046 +v 17.307264328 18.084199905 -56.921527863 +v 16.230787277 16.620403290 -54.928943634 +v 16.230789185 17.622463226 -56.921527863 +v 17.307264328 17.082141876 -54.928943634 +v 14.850786209 15.570942879 -54.928943634 +v 14.850786209 16.573001862 -56.921527863 +v 18.564565659 18.385017395 -56.921527863 +v 19.847116470 18.471858978 -56.921527863 +v 18.564565659 17.382957458 -54.928943634 +v 19.847116470 17.469799042 -54.928943634 +v 16.230787277 16.428209305 -54.486366272 +v 17.307262421 16.889945984 -54.486366272 +v 14.850786209 15.378747940 -54.486366272 +v 17.307262421 16.884588242 -50.361312866 +v 16.230787277 16.422851563 -50.361312866 +v 14.851177216 15.381685257 -50.361312866 +v 19.847116470 17.272245407 -50.361312866 +v 18.564565659 17.190763474 -54.486366272 +v 19.847116470 17.277603149 -54.486366272 +v 18.564565659 17.185403824 -50.361312866 +v 22.380247116 18.084199905 -56.921527863 +v 21.122945786 17.382957458 -54.928943634 +v 21.122945786 18.385017395 -56.921527863 +v 22.380247116 17.082139969 -54.928943634 +v 23.454578400 17.623397827 -56.921527863 +v 24.845567703 16.575054169 -56.921527863 +v 23.454578400 16.621337891 -54.928943634 +v 24.845569611 15.572996140 -54.928943634 +v 22.380247116 16.884588242 -50.361312866 +v 21.122943878 17.190763474 -54.486366272 +v 22.380247116 16.889945984 -54.486366272 +v 21.122943878 17.185403824 -50.361312866 +v 23.454576492 16.429143906 -54.486366272 +v 24.845567703 15.380802155 -54.486366272 +v 24.845907211 15.374712944 -50.361396790 +v 23.454576492 16.423786163 -50.361312866 +v 15.065155029 20.229444504 -69.133949280 +v 15.065154076 20.372211456 -64.533569336 +v 15.065155029 20.233478546 -64.533569336 +v 15.065155029 20.368175507 -69.133949280 +v 14.850788116 20.026432037 -69.296829224 +v 14.850788116 20.016956329 -64.533531189 +v 17.388771057 19.267009735 -68.629501343 +v 17.991315842 19.979499817 -68.629501343 +v 17.161193848 19.662883759 -68.629501343 +v 16.885383606 20.072574615 -69.296829224 +v 17.001237869 19.941129684 -68.832267761 +v 17.766918182 20.435890198 -69.296829224 +v 15.848499298 19.116405487 -68.832267761 +v 17.848257065 20.304443359 -68.832267761 +v 16.151197433 18.952217102 -68.629501343 +v 15.652250290 19.219083786 -69.296829224 +v 17.534513474 19.013490677 -69.832305908 +v 17.534511566 19.014816284 -68.830947876 +v 16.847101212 18.593065262 -68.832267761 +v 16.847101212 18.593063354 -69.832305908 +v 16.590465546 18.729488373 -68.629501343 +v 18.837556839 20.541137695 -68.832267761 +v 18.912887573 20.184299469 -68.629501343 +v 18.796533585 20.672582626 -69.296829224 +v 19.847219467 20.740915298 -69.296829224 +v 19.847103119 20.609468460 -68.832267761 +v 19.847099304 20.245208740 -68.629501343 +v 18.997142792 19.785192490 -68.629501343 +v 19.056400299 19.504489899 -69.832305908 +v 19.056400299 19.504489899 -68.832267761 +v 18.282091141 19.320365906 -68.830947876 +v 18.163064957 19.589399338 -68.629501343 +v 19.847097397 19.858482361 -68.629501343 +v 19.847093582 19.566659927 -69.832305908 +v 19.847093582 19.566659927 -68.832267761 +v 18.282093048 19.319042206 -69.832305908 +v 23.146928787 14.998180389 -68.629501343 +v 24.614154816 15.003443718 -64.519569397 +v 23.576435089 14.998180389 -68.629501343 +v 22.847103119 14.998180389 -68.832260132 +v 15.061660767 15.002688408 -64.519554138 +v 15.652154922 15.355363846 -69.295516968 +v 16.847101212 14.998180389 -69.832305908 +v 16.847099304 14.998180389 -68.830947876 +v 15.849536896 14.998180389 -68.830947876 +v 16.591007233 14.998180389 -68.628189087 +v 16.148244858 14.998180389 -68.628189087 +v 14.850786209 15.355358124 -69.295516968 +v 14.850786209 15.345883369 -64.533531189 +v 15.065820694 14.998180389 -69.104545593 +v 15.065818787 15.101775169 -64.533576965 +v 15.065820694 15.111249924 -69.104545593 +v 15.732936859 14.998180389 -69.104545593 +v 15.732936859 15.111255646 -69.104545593 +v 15.065154076 19.575210571 -62.923599243 +v 15.065155029 19.436477661 -62.923599243 +v 14.850788116 19.219955444 -62.923557281 +v 15.065153122 18.569644928 -60.915592194 +v 15.065154076 18.430913925 -60.915592194 +v 14.850787163 18.214391708 -60.915550232 +v 14.850784302 13.543317795 -60.915550232 +v 14.850785255 14.548881531 -62.923561096 +v 15.061659813 14.802688599 -64.116996765 +v 15.065818787 14.304773331 -62.923606873 +v 15.061660767 14.602687836 -63.719551086 +v 15.061660767 14.802688599 -64.519554138 +v 15.061659813 14.602687836 -64.116996765 +v 15.061660767 14.402688980 -63.319549561 +v 15.061660767 14.402688980 -63.719551086 +v 15.061659813 14.202688217 -62.919067383 +v 15.061660767 14.202688217 -63.319549561 +v 15.061660767 14.002689362 -62.516796112 +v 15.061660767 14.002689362 -62.919067383 +v 15.061660767 13.802688599 -62.117893219 +v 15.061660767 13.802688599 -62.516796112 +v 15.065818787 13.299209595 -60.915596008 +v 15.061663628 13.597527504 -61.721931458 +v 15.061662674 13.597527504 -62.118125916 +v 15.061663628 13.397527695 -61.326660156 +v 15.061663628 13.397527695 -61.721931458 +v 15.061664581 13.197527885 -60.916912079 +v 15.061663628 13.197528839 -61.326660156 +v 14.850784302 12.539145470 -58.926403046 +v 14.850787163 17.210218430 -58.926403046 +v 15.065153122 17.565471649 -58.926445007 +v 15.065154076 17.426740646 -58.926445007 +v 15.061664581 12.997529030 -60.527023315 +v 15.065818787 12.295036316 -58.926448822 +v 15.061664581 12.597529411 -59.705848694 +v 15.061664581 12.797529221 -60.103534698 +v 15.061664581 12.997528076 -60.916912079 +v 15.061664581 12.797529221 -60.527023315 +v 15.061664581 12.597529411 -60.103534698 +v 24.614154816 14.203443527 -62.919086456 +v 24.614154816 14.203121185 -63.320079803 +v 24.614154816 13.803443909 -62.117912292 +v 24.614154816 13.803443909 -62.516815186 +v 24.614154816 14.003443718 -62.919086456 +v 24.614154816 14.003443718 -62.516815186 +v 24.614152908 13.598928452 -62.118114471 +v 24.614154816 14.803442955 -64.117019653 +v 24.614154816 14.603444099 -63.719570160 +v 24.614154816 14.403120995 -63.720081329 +v 24.614154816 14.603120804 -64.117134094 +v 24.614154816 14.803443909 -64.519569397 +v 24.614154816 14.403444290 -63.319568634 +v 24.614152908 12.998283386 -60.916931152 +v 24.614152908 13.198284149 -60.916931152 +v 24.614152908 13.198284149 -61.326679230 +v 24.614152908 13.398283005 -61.326679230 +v 24.614152908 13.398283005 -61.721950531 +v 24.614152908 13.598281860 -61.721950531 +v 24.614151001 12.798283577 -60.103549957 +v 24.614151001 12.598283768 -59.705867767 +v 15.061664581 12.397529602 -59.705848694 +v 24.614151001 12.598606110 -60.103549957 +v 24.614151001 12.798283577 -60.527042389 +v 15.061666489 12.197528839 -58.921619415 +v 24.614151001 12.998283386 -60.527042389 +v 24.614151001 12.198284149 -58.921638489 +v 24.614151001 12.398607254 -59.705860138 +v 20.779275894 20.202938080 -68.629501343 +v 20.891609192 20.672582626 -69.296829224 +v 20.850601196 20.541137695 -68.832267761 +v 21.839902878 20.304443359 -68.832267761 +v 21.921224594 20.435890198 -69.296829224 +v 21.713487625 20.017166138 -68.629501343 +v 22.685232162 19.941864014 -68.832267761 +v 20.693298340 19.795265198 -68.629501343 +v 20.631973267 19.504489899 -69.832305908 +v 21.406280518 19.319042206 -69.832305908 +v 20.631973267 19.504489899 -68.832267761 +v 21.525520325 19.590011597 -68.629501343 +v 22.153862000 19.013490677 -69.832305908 +v 21.406280518 19.319042206 -68.832267761 +v 22.534509659 19.678533554 -68.629501343 +v 22.325832367 19.277849197 -68.629501343 +v 22.153862000 19.013492584 -68.832267761 +v 24.629589081 20.235353470 -64.533569336 +v 24.629594803 20.368263245 -69.133865356 +v 24.629594803 20.229530334 -69.133865356 +v 24.629589081 20.374086380 -64.533569336 +v 24.845569611 20.018833160 -64.533531189 +v 24.845909119 20.013008118 -69.304733276 +v 22.801000595 20.073310852 -69.296829224 +v 23.851119995 19.118022919 -68.832267761 +v 23.576902390 18.975435257 -68.629501343 +v 24.044528961 19.220699310 -69.296829224 +v 23.146564484 18.749273300 -68.629501343 +v 22.847103119 18.593063354 -69.832305908 +v 22.847103119 18.593065262 -68.832267761 +v 23.849927902 14.998180389 -68.832260132 +v 24.629476547 14.984669685 -69.114028931 +v 23.964565277 14.998180389 -69.105857849 +v 22.847103119 14.998180389 -69.832305908 +v 23.964565277 15.109921455 -69.105857849 +v 24.044765472 15.354029655 -69.296829224 +v 24.629476547 15.109922409 -69.105857849 +v 24.845544815 15.354030609 -69.296829224 +v 24.845544815 15.344554901 -64.533531189 +v 24.629476547 15.100444794 -64.533576965 +v 24.845569611 19.221830368 -62.923557281 +v 24.845544815 14.547553062 -62.923557281 +v 24.845569611 18.216266632 -60.915550232 +v 24.629589081 18.432788849 -60.915592194 +v 24.629589081 19.438352585 -62.923599243 +v 24.629589081 19.577083588 -62.923599243 +v 24.629589081 18.571521759 -60.915592194 +v 24.629589081 17.428615570 -58.926445007 +v 24.629589081 17.567348480 -58.926445007 +v 24.629476547 14.303443909 -62.923603058 +v 24.845544815 13.541989326 -60.915550232 +v 24.629476547 13.297880173 -60.915596008 +v 24.845569611 17.212093353 -58.926403046 +v 24.845544815 12.537815094 -58.926403046 +v 24.629476547 12.293706894 -58.926448822 +v 15.065153122 16.441242218 -56.921569824 +v 14.850786209 15.222661972 -54.928943634 +v 14.850786209 16.224720001 -56.921527863 +v 15.065153122 15.439184189 -54.928985596 +v 15.065153122 15.577916145 -54.928985596 +v 15.065152168 16.579975128 -56.921569824 +v 15.061664581 12.397529602 -59.308055878 +v 14.850784302 11.553647041 -56.921527863 +v 15.061666489 10.997529984 -56.523735046 +v 15.063741684 11.196999550 -56.922222137 +v 15.065818787 11.309538841 -56.921573639 +v 15.061666489 11.397529602 -57.324699402 +v 15.061666489 11.597529411 -57.723960876 +v 15.061666489 11.397529602 -57.723960876 +v 15.061666489 11.197529793 -57.324699402 +v 15.061665535 10.997529984 -56.922870636 +v 15.061666489 11.997529030 -58.512027740 +v 15.061664581 12.197528839 -59.308055878 +v 15.061666489 11.997529030 -58.921619415 +v 15.061666489 11.797529221 -58.128948212 +v 15.061666489 11.797529221 -58.512027740 +v 15.061666489 11.597529411 -58.128948212 +v 14.850784302 10.551588058 -54.928943634 +v 15.065153122 15.246988297 -54.486324310 +v 14.850786209 15.030466080 -54.486366272 +v 14.850784302 10.359393120 -54.486366272 +v 15.065818787 10.307478905 -54.928993225 +v 15.061666489 10.797530174 -56.124694824 +v 15.061666489 10.597530365 -55.722717285 +v 15.061666489 10.797530174 -56.523735046 +v 15.061666489 10.597530365 -56.124694824 +v 15.061666489 10.397530556 -55.324447632 +v 15.061666489 10.397530556 -55.722717285 +v 15.063743591 10.195970535 -54.928028107 +v 15.061666489 10.197530746 -55.324447632 +v 15.061666489 9.997628212 -54.927124023 +v 15.065818787 10.002214432 -54.486320496 +v 15.065818787 10.115284920 -54.486320496 +v 24.614151001 11.798606873 -58.512046814 +v 24.614151001 11.998606682 -58.921596527 +v 24.614151001 12.398283958 -59.308078766 +v 24.614151001 12.198606491 -59.308071136 +v 24.614151001 11.998284340 -58.512046814 +v 24.614151001 11.798284531 -58.128967285 +v 24.614151001 11.398284912 -57.723983765 +v 24.614151001 11.598284721 -58.128967285 +v 24.614151001 11.598285675 -57.723983765 +v 24.614151001 11.398284912 -57.324714661 +v 24.614151001 11.198284149 -57.324714661 +v 24.614149094 11.198285103 -56.922889709 +v 24.614149094 10.798285484 -56.523754120 +v 24.614149094 10.998285294 -56.523754120 +v 24.614149094 10.798284531 -56.124717712 +v 24.614151001 10.998285294 -56.922889709 +v 24.614149094 10.598284721 -55.722736359 +v 24.614149094 10.598285675 -56.124717712 +v 24.614149094 9.997730255 -54.927211761 +v 24.614149094 10.398285866 -55.324470520 +v 24.614149094 10.198285103 -55.324470520 +v 24.622045517 10.002214432 -54.486320496 +v 24.614149094 10.398285866 -55.722736359 +v 24.614149094 10.198286057 -54.927082062 +v 15.065152168 15.385721207 -54.486324310 +v 15.065151215 15.242953300 -50.532180786 +v 15.065151215 15.381685257 -50.532180786 +v 14.850786209 15.026432037 -50.361312866 +v 16.120515823 13.975436211 -51.028869629 +v 17.371587753 14.277850151 -51.028869629 +v 16.550857544 13.749273300 -51.028869629 +v 15.848496437 14.116407394 -50.825691223 +v 17.162912369 14.678532600 -51.028869629 +v 17.001235962 14.941128731 -50.825691223 +v 17.534509659 14.013491631 -50.825691223 +v 16.847099304 13.593065262 -50.825691223 +v 16.847099304 13.593065262 -49.832305908 +v 17.534509659 14.013492584 -49.832305908 +v 15.652330399 14.220699310 -50.361396790 +v 16.896419525 15.073310852 -50.361396790 +v 17.983934402 15.017166138 -51.028869629 +v 17.848253250 15.304443359 -50.825691223 +v 17.776197433 15.435888290 -50.361396790 +v 18.837554932 15.541138649 -50.825691223 +v 18.805810928 15.672584534 -50.361396790 +v 18.918146133 15.202938080 -51.028869629 +v 19.850324631 14.858482361 -51.028869629 +v 19.850320816 15.245207787 -51.028869629 +v 19.850202560 15.740915298 -50.361396790 +v 19.850318909 15.609468460 -50.826107025 +v 19.004123688 14.795266151 -51.028869629 +v 19.847091675 14.566659927 -50.825691223 +v 19.056396484 14.504489899 -50.825691223 +v 18.171901703 14.590011597 -51.028869629 +v 19.056396484 14.504489899 -49.832302094 +v 18.282089233 14.319043159 -49.832305908 +v 19.847091675 14.566660881 -49.832305908 +v 18.282089233 14.319043159 -50.825691223 +v 23.106412888 9.998181343 -51.028869629 +v 23.549175262 9.998181343 -51.028869629 +v 22.847099304 9.998181343 -50.825691223 +v 16.847097397 9.998181343 -50.825691223 +v 16.847097397 9.998181343 -49.832302094 +v 16.550491333 9.998181343 -51.028869629 +v 16.120986938 9.998181343 -51.028869629 +v 15.849535942 9.998181343 -50.825691223 +v 15.065820694 9.998181343 -50.552104950 +v 14.850786209 10.354028702 -50.361312866 +v 15.065820694 10.109919548 -50.552104950 +v 15.652656555 10.354027748 -50.361396790 +v 15.732674599 10.109919548 -50.552509308 +v 15.732674599 9.998181343 -50.552509308 +v 24.845567703 16.226596832 -56.921527863 +v 24.845542908 11.552317619 -56.921527863 +v 24.629587173 16.581850052 -56.921569824 +v 24.629587173 16.443117142 -56.921569824 +v 24.629589081 15.441058159 -54.928985596 +v 24.845569611 15.224536896 -54.928943634 +v 24.629589081 15.579791069 -54.928985596 +v 24.629474640 11.308209419 -56.921573639 +v 24.845542908 10.358064651 -54.486366272 +v 24.845567703 15.032342911 -54.486366272 +v 24.845542908 10.550258636 -54.928943634 +v 24.629474640 10.306149483 -54.928989410 +v 24.629474640 10.113956451 -54.486320496 +v 20.850599289 15.541138649 -50.825691223 +v 20.900888443 15.672584534 -50.361396790 +v 20.700279236 14.785192490 -51.028869629 +v 20.784534454 15.184301376 -51.028869629 +v 21.930501938 15.435888290 -50.361396790 +v 21.839899063 15.304443359 -50.825691223 +v 22.685230255 14.941864014 -50.825691223 +v 21.706104279 14.979497910 -51.028869629 +v 21.406278610 14.319043159 -50.825691223 +v 20.631971359 14.504489899 -50.825691223 +v 21.534357071 14.589399338 -51.028869629 +v 20.631971359 14.504489899 -49.832305908 +v 22.153858185 14.013491631 -50.825691223 +v 22.308650970 14.267010689 -51.028869629 +v 22.536228180 14.662883759 -51.028869629 +v 21.406278610 14.319043159 -49.832302094 +v 22.153858185 14.013492584 -49.832302094 +v 24.629587173 15.248864174 -54.486324310 +v 24.629587173 15.387597084 -54.486324310 +v 24.845907211 15.026519775 -50.361396790 +v 24.629589081 15.243041992 -50.532264709 +v 24.629589081 15.381774902 -50.532264709 +v 23.546222687 13.952216148 -51.028869629 +v 23.851118088 14.118022919 -50.825691223 +v 23.106956482 13.729488373 -51.028869629 +v 22.847099304 13.593065262 -50.825691223 +v 22.847099304 13.593065262 -49.832305908 +v 22.812036514 15.072574615 -50.361396790 +v 24.045171738 14.219083786 -50.361396790 +v 24.629474640 9.998181343 -50.552104950 +v 24.629474640 10.109925270 -50.552104950 +v 24.845542908 10.354034424 -50.361312866 +v 23.849924088 9.998181343 -50.825691223 +v 23.965547562 10.109930038 -50.552509308 +v 23.965547562 9.998181343 -50.552509308 +v 24.044841766 10.354040146 -50.361396790 +v 22.847099304 9.998181343 -49.832302094 +v 29.770776749 14.983057976 -79.765068054 +v 29.990617752 14.983057976 -79.764480591 +v 29.770776749 14.983057976 -76.728233337 +v 29.990617752 19.614944458 -79.764816284 +v 29.990617752 14.983057976 -76.724792480 +v 29.770776749 19.614944458 -79.764595032 +v 29.990617752 18.641347885 -76.728233337 +v 29.990617752 19.552772522 -78.981636047 +v 29.770776749 19.552772522 -78.978973389 +v 29.990617752 19.367326736 -78.206016541 +v 29.770776749 19.367326736 -78.206016541 +v 29.770776749 18.641349792 -76.728233337 +v 29.990617752 19.061775208 -77.458442688 +v 29.770776749 19.061775208 -77.458442688 +v 29.990617752 19.367326736 -81.333724976 +v 29.770776749 19.367326736 -81.331604004 +v 29.990617752 19.552772522 -80.557205200 +v 29.770776749 19.552772522 -80.555305481 +v 29.990617752 14.983057976 -82.789794922 +v 29.770776749 14.983057976 -82.789558411 +v 29.990617752 19.061775208 -82.081695557 +v 29.770776749 19.061775208 -82.079597473 +v 29.990617752 18.641347885 -82.789794922 +v 29.770776749 18.641349792 -82.789558411 +v 25.150344849 36.803638458 -78.226249695 +v 23.507511139 36.803638458 -76.151039124 +v 22.379907608 37.816291809 -77.288597107 +v 25.149658203 37.588485718 -78.859718323 +v 23.760309219 38.146606445 -79.075546265 +v 26.582733154 37.451553345 -80.917556763 +v 26.741977692 36.803638458 -80.926628113 +v 23.620162964 40.143218994 -80.539779663 +v 23.214408875 38.849967957 -80.607490540 +v 22.805299759 38.849967957 -79.122245789 +v 23.983345032 38.146606445 -80.741073608 +v 22.286933899 40.141529083 -79.104644775 +v 21.626663208 38.849967957 -77.977058411 +v 21.465209961 40.042587280 -77.248382568 +v 21.989391327 38.146606445 -77.923149109 +v 18.922386169 38.849967957 -77.383468628 +v 18.781166077 39.983886719 -75.779251099 +v 19.296573639 37.549797058 -77.759208679 +v 19.071321487 38.146606445 -77.910560608 +v 26.055217743 35.033866882 -80.746879578 +v 24.823348999 35.031005859 -76.757247925 +v 25.464570999 32.528594971 -77.825080872 +v 21.454284668 32.629238129 -76.140113831 +v 24.596004486 35.184875488 -75.502090454 +v 25.541439056 32.444717407 -80.763412476 +v 18.820507050 35.188179016 -77.004943848 +v 19.020227432 36.803642273 -77.550704956 +v 18.437503815 32.598598480 -74.781242371 +v 15.987350464 38.849967957 -75.045997620 +v 15.820146561 40.176586151 -73.660858154 +v 16.128425598 38.146606445 -75.713554382 +v 16.336780548 37.600803375 -75.754692078 +v 16.051029205 36.803642273 -74.879219055 +v 15.816267014 35.004508972 -73.347824097 +v 15.454005241 32.571670532 -72.626029968 +v 12.481450081 32.488109589 -72.487487793 +v 12.481454849 36.824386597 -71.998634338 +v 12.481455803 34.988109589 -72.289489746 +v 9.841464043 32.488113403 -72.487487793 +v 9.841464043 36.824386597 -71.998641968 +v 9.841464043 37.488113403 -72.269149780 +v 12.481452942 37.488109589 -72.269142151 +v 9.841464043 34.988113403 -72.289505005 +v 9.841464043 38.150741577 -73.263992310 +v 12.481452942 38.150741577 -73.263984680 +v 9.841464043 38.831653595 -73.557495117 +v 9.841464043 39.987960815 -72.482528687 +v 12.481452942 39.987960815 -72.482521057 +v 12.481452942 38.831653595 -73.557495117 +v 24.103481293 34.949363708 -83.712203979 +v 25.275901794 32.545532227 -84.405128479 +v 23.822326660 36.803642273 -83.586334229 +v 21.669319153 36.803638458 -86.960464478 +v 20.021114349 32.419254303 -86.266639709 +v 24.059520721 37.374099731 -83.686401367 +v 23.201112747 39.767883301 -83.417564392 +v 23.296180725 38.849967957 -82.913650513 +v 23.527416229 38.146606445 -82.990409851 +v 21.588678360 37.490589142 -86.695175171 +v 21.198863983 38.146606445 -86.152320862 +v 21.211910248 38.849967957 -86.241333008 +v 20.951198578 34.872016907 -86.784126282 +v 18.210243225 34.756584167 -89.659729004 +v 18.384002686 32.098144531 -89.750251770 +v 18.141355515 36.803638458 -88.407386780 +v 21.635330200 40.005462646 -87.013442993 +v 18.081920624 37.317695618 -88.053428650 +v 17.714733124 38.146606445 -87.833557129 +v 17.707719803 38.849967957 -88.143836975 +v 18.099239349 39.993171692 -89.274894714 +v 15.259215355 39.788761139 -87.509864807 +v 14.867755890 38.849967957 -86.730918884 +v 14.875638962 38.146606445 -86.616233826 +v 15.287878036 37.231719971 -87.092933655 +v 15.288704872 36.803642273 -87.207962036 +v 15.292680740 34.746036530 -87.760856628 +v 15.425393105 32.172321320 -87.813186646 +v 12.481456757 34.988113403 -87.337501526 +v 12.481456757 32.488113403 -87.139503479 +v 9.841465950 34.988113403 -87.337501526 +v 9.841465950 32.488113403 -87.139511108 +v 12.481451988 36.803638458 -87.625076294 +v 9.841465950 36.824386597 -87.628364563 +v 12.481458664 37.488113403 -87.354400635 +v 12.481458664 38.831653595 -86.051231384 +v 9.841455460 38.831653595 -86.069503784 +v 9.841456413 38.150741577 -86.363006592 +v 12.452425003 39.987964630 -87.164901733 +v 9.841456413 39.987960815 -87.144470215 +v 9.841456413 37.488113403 -87.357849121 +v 12.481458664 38.150741577 -86.348823547 +v 29.305988312 21.413707733 -76.206016541 +v 29.305988312 20.366687775 -74.815032959 +v 24.837379456 20.371608734 -74.815391541 +v 23.466409683 21.406253815 -76.199089050 +v 25.255584717 14.946889877 -76.231483459 +v 26.604797363 14.982656479 -76.265853882 +v 27.231506348 14.950581551 -76.265853882 +v 23.980312347 21.871791840 -77.274734497 +v 24.203563690 21.958866119 -77.837463379 +v 24.520853043 21.999008179 -78.534027100 +v 23.593200684 21.483489990 -76.369125366 +v 29.305988312 21.874507904 -77.280349731 +v 29.305988312 22.175323486 -78.537651062 +v 24.800132751 22.060703278 -79.218650818 +v 29.305988312 22.262165070 -79.813476563 +v 24.956825256 22.130088806 -79.813652039 +v 23.457771301 21.412771225 -70.348968506 +v 23.082004547 21.573951721 -75.407218933 +v 22.381294250 21.874509811 -74.702331543 +v 22.381294250 21.874507904 -70.348968506 +v 24.837772369 20.364633560 -70.348968506 +v 18.565612793 22.107646942 -72.990287781 +v 19.841440201 22.250955582 -73.242851257 +v 19.841440201 22.262165070 -70.348968506 +v 21.123991013 22.175323486 -70.348968506 +v 21.123991013 22.175325394 -74.250564575 +v 25.056758881 22.139398575 -80.535820007 +v 24.705877304 22.122812271 -81.095756531 +v 29.305988312 22.175323486 -81.096031189 +v 29.305988312 21.874507904 -82.353332520 +v 23.299259186 21.874511719 -82.353866577 +v 23.448938370 21.413709641 -83.419189453 +v 29.305988312 21.412771225 -83.429809570 +v 29.305988312 20.364633560 -84.809814453 +v 24.839569092 20.371608734 -84.809417725 +v 23.457771301 21.412773132 -89.284484863 +v 24.837379456 20.371608734 -89.284484863 +v 22.797550201 21.693103790 -82.773315430 +v 22.375690460 21.873327255 -83.476203918 +v 22.381294250 21.874511719 -89.284484863 +v 21.123992920 22.175325394 -89.284484863 +v 22.168815613 21.903715134 -83.783325195 +v 21.120126724 22.067075729 -84.549667358 +v 19.841442108 22.176996231 -84.137710571 +v 19.841442108 22.262166977 -89.284484863 +v 18.565612793 22.175325394 -89.284484863 +v 18.565612793 22.175323486 -70.348968506 +v 18.272354126 21.979415894 -73.085937500 +v 17.308311462 21.518264771 -73.613197327 +v 17.308309555 21.874507904 -70.348968506 +v 16.233982086 20.961898804 -74.645462036 +v 16.233980179 21.413707733 -70.348968506 +v 14.842990875 20.366687775 -70.348968506 +v 16.933029175 21.336425781 -73.846252441 +v 14.842651367 20.219360352 -74.815307617 +v 15.540415764 20.409646988 -75.513656616 +v 15.635255814 20.520387650 -75.291046143 +v 15.281488419 20.586505890 -75.753463745 +v 14.728040695 20.909824371 -76.207351685 +v 15.799945831 20.644830704 -75.183647156 +v 10.370471001 21.413709641 -76.206024170 +v 13.212484360 21.168054581 -77.276458740 +v 14.610939026 20.942386627 -76.341934204 +v 10.370471001 21.874511719 -77.280349731 +v 10.370553970 20.364635468 -74.814689636 +v 13.357571602 21.493791580 -79.813629150 +v 12.988756180 21.191238403 -78.535026550 +v 10.370471001 22.175325394 -78.537651062 +v 13.057863235 21.173599243 -77.588134766 +v 10.370471001 22.262166977 -79.813484192 +v 13.967662811 21.837032318 -81.095771790 +v 10.370471001 22.175325394 -81.096031189 +v 13.763244629 21.696022034 -80.502403259 +v 14.216711998 21.863300323 -81.803222656 +v 14.747447968 21.848104477 -82.353805542 +v 10.370471001 21.874511719 -82.353332520 +v 14.845539093 20.371608734 -84.811607361 +v 10.370471001 21.412773132 -83.429809570 +v 10.982007980 20.373481750 -84.595985413 +v 10.370471001 20.371608734 -84.809417725 +v 15.094427109 21.729990005 -82.688247681 +v 16.235771179 21.413709641 -83.420799255 +v 16.762920380 21.712316513 -82.950569153 +v 18.560976028 22.168703079 -83.647201538 +v 17.301679611 21.976741791 -83.235755920 +v 17.308311462 21.874511719 -89.284484863 +v 14.842651367 20.364635468 -89.284408569 +v 16.233982086 21.413709641 -89.284484863 +v 24.488052368 30.094154358 -75.618225098 +v 25.518449783 28.812402725 -75.841156006 +v 24.347496033 28.985239029 -75.019706726 +v 23.141761780 30.100467682 -74.650505066 +v 24.634094238 26.688255310 -75.590003967 +v 23.490127563 27.013437271 -73.939460754 +v 23.440853119 28.999212265 -74.159996033 +v 22.817611694 27.110204697 -72.569808960 +v 21.266868591 26.662124634 -71.392883301 +v 19.449996948 28.567934036 -72.402175903 +v 19.529581070 29.983152390 -73.100250244 +v 19.787189484 26.169006348 -71.135566711 +v 22.061599731 24.255449295 -74.742469788 +v 20.893354416 24.193212509 -74.051712036 +v 21.115722656 24.546749115 -72.695671082 +v 22.511524200 24.635454178 -73.533271790 +v 22.808015823 25.561082840 -72.700889587 +v 23.368392944 25.587879181 -74.153945923 +v 21.280704498 25.101297379 -71.769996643 +v 19.866724014 24.839723587 -71.833221436 +v 19.801246643 24.161594391 -72.210723877 +v 23.329086304 24.272464752 -74.836639404 +v 23.955390930 25.325462341 -75.683670044 +v 23.781864166 23.778326035 -75.961318970 +v 22.722534180 23.988742828 -75.623329163 +v 23.200160980 23.616571426 -76.630523682 +v 23.091220856 23.616294861 -76.484764099 +v 22.626701355 22.961791992 -76.785835266 +v 22.330196381 23.193660736 -76.181762695 +v 26.550949097 29.874471664 -77.890174866 +v 26.447105408 28.526062012 -76.735382080 +v 26.842082977 28.425216675 -77.812469482 +v 26.544471741 28.660789490 -80.678894043 +v 26.738830566 28.452816010 -79.091911316 +v 25.884006500 26.118581772 -77.365158081 +v 25.705062866 26.361009598 -76.578338623 +v 25.819625854 26.084964752 -78.917701721 +v 25.796188354 26.212837219 -79.443344116 +v 25.092411041 24.798793793 -77.526428223 +v 25.752429962 26.067216873 -78.103294373 +v 24.865108490 24.997421265 -76.733055115 +v 24.665222168 23.828989029 -77.714935303 +v 25.286739349 24.730936050 -78.227584839 +v 25.378070831 24.778564453 -79.131469727 +v 23.948272705 23.776203156 -76.147186279 +v 24.426811218 23.831975937 -77.107467651 +v 23.688209534 23.579959869 -77.967208862 +v 23.549936295 23.637470245 -77.448776245 +v 25.604660034 24.897699356 -79.702133179 +v 25.027832031 24.062568665 -78.296684265 +v 25.358690262 24.106132507 -79.120513916 +v 25.383094788 24.321857452 -79.871215820 +v 24.432258606 23.809860229 -79.742805481 +v 24.290679932 23.616666794 -79.205200195 +v 29.305988312 20.063230515 -76.859596252 +v 29.135120392 20.373481750 -75.031013489 +v 24.623407364 20.371608734 -75.031326294 +v 29.135120392 20.234748840 -75.031013489 +v 24.623407364 20.232875824 -75.031326294 +v 29.305988312 20.018226624 -74.815032959 +v 19.794349670 23.879894257 -73.666061401 +v 19.805347443 22.543739319 -73.894897461 +v 20.892026901 20.662504196 -70.348968506 +v 19.841341019 20.730834961 -70.348968506 +v 20.851001740 20.531059265 -70.813537598 +v 21.840303421 20.294363022 -70.813537598 +v 19.841457367 20.599388123 -70.813537598 +v 21.921642303 20.425807953 -70.348968506 +v 22.803176880 20.062494278 -70.348968506 +v 23.537363052 18.942136765 -71.016304016 +v 22.527366638 19.652805328 -71.016304016 +v 22.687320709 19.931049347 -70.813537598 +v 23.840061188 19.106327057 -70.813537598 +v 24.036310196 19.209003448 -70.348968506 +v 21.767883301 23.401014328 -75.542915344 +v 20.757532120 23.470703125 -75.025962830 +v 19.773284912 23.212747574 -74.474105835 +v 20.944026947 22.566518784 -74.784950256 +v 22.061439514 22.344963074 -75.191322327 +v 22.683864594 22.077983856 -75.817459106 +v 23.025321960 21.929023743 -76.520858765 +v 24.623407364 20.371606827 -70.519836426 +v 24.623407364 20.232873917 -70.519836426 +v 24.837772369 20.016351700 -70.348968506 +v 24.036403656 15.343961716 -70.348968506 +v 24.837772369 20.016353607 -74.815391541 +v 24.837772369 15.343955994 -70.348968506 +v 23.098094940 18.719408035 -71.016304016 +v 23.097551346 14.950710297 -71.016304016 +v 22.841457367 14.950710297 -70.813537598 +v 23.839019775 14.950710297 -70.813537598 +v 23.540313721 14.950710297 -71.016304016 +v 22.841457367 18.582984924 -70.813537598 +v 24.622737885 14.950710297 -70.539947510 +v 23.955623627 14.950710297 -70.539947510 +v 23.955623627 15.099852562 -70.539947510 +v 24.622737885 15.099847794 -70.539947510 +v 24.837770462 15.343947411 -74.815391541 +v 24.606698990 14.947907448 -75.034667969 +v 21.894195557 14.901648521 -72.612403870 +v 24.612802505 15.079806328 -75.034667969 +v 23.336404800 14.936596870 -74.047241211 +v 23.298706055 15.231988907 -75.839904785 +v 23.259780884 14.938171387 -75.032234192 +v 25.088092804 15.331356049 -76.757781982 +v 22.979141235 15.738905907 -77.437652588 +v 29.305988312 19.210618973 -75.616073608 +v 29.305988312 15.343950272 -74.815055847 +v 28.841423035 19.931785583 -76.975364685 +v 28.841423035 19.107944489 -75.809478760 +v 29.305988312 15.343950272 -75.615837097 +v 28.638660431 14.950710297 -76.084167480 +v 28.638660431 18.965354919 -76.083694458 +v 28.638660431 18.739192963 -76.514038086 +v 28.638660431 14.950710297 -76.513671875 +v 28.841423035 14.950710297 -75.810668945 +v 29.115013123 14.950710297 -75.031127930 +v 24.735479355 15.422969818 -77.767738342 +v 26.498107910 14.949935913 -76.926391602 +v 29.115013123 15.099842072 -75.031127930 +v 29.115013123 15.099841118 -75.696037292 +v 29.115013123 14.950710297 -75.696037292 +v 22.299789429 19.256931305 -71.016304016 +v 22.154045105 19.003412247 -70.813537598 +v 21.406467438 19.308963776 -70.813537598 +v 21.525495529 19.579319000 -71.016304016 +v 20.775671005 20.174221039 -71.016304016 +v 21.697242737 19.969417572 -71.016304016 +v 20.691417694 19.775112152 -71.016304016 +v 20.632160187 19.494409561 -70.813537598 +v 19.841459274 20.235126495 -71.016304016 +v 19.841461182 19.848402023 -71.016304016 +v 19.841464996 19.556579590 -70.813537598 +v 16.841457367 14.950710297 -70.813537598 +v 20.586040497 14.937191010 -71.819656372 +v 18.894596100 14.938294411 -72.220695496 +v 20.435432434 15.061697960 -72.449722290 +v 18.696746826 15.379846573 -73.383460999 +v 18.772079468 15.265413284 -72.659072876 +v 17.032363892 15.983451843 -73.813018799 +v 21.754692078 14.977915764 -73.410140991 +v 20.325994492 15.450225830 -73.102951050 +v 21.950759888 15.306118011 -73.966735840 +v 21.694307327 15.916619301 -75.330863953 +v 20.282855988 16.405143738 -74.777481079 +v 21.458448410 16.824398041 -78.550865173 +v 21.431514740 16.321739197 -76.831939697 +v 18.578311920 15.797663689 -74.144912720 +v 20.631263733 15.616428375 -73.627922058 +v 18.694887161 16.794986725 -75.507049561 +v 23.809509277 23.007034302 -79.658699036 +v 23.685874939 22.939796448 -79.189224243 +v 24.038337708 23.547147751 -78.586608887 +v 23.465511322 22.890916824 -78.649032593 +v 29.305988312 20.425807953 -77.739372253 +v 29.305988312 20.662504196 -78.768989563 +v 28.841423035 20.531059265 -78.809997559 +v 28.638660431 20.007085800 -77.947113037 +v 28.638660431 19.668453217 -77.126091003 +v 28.841423035 20.294363022 -77.820693970 +v 28.638660431 19.267768860 -77.334770203 +v 28.638660431 20.192857742 -78.881324768 +v 28.638660431 19.579931259 -78.135078430 +v 28.638660431 19.785186768 -78.967300415 +v 28.841423035 20.599388123 -79.813499451 +v 29.305988312 20.730834961 -79.813377380 +v 28.638660431 20.235126495 -79.813499451 +v 28.841423035 19.494409561 -79.028625488 +v 29.841461182 19.308961868 -78.254318237 +v 28.841423035 19.308963776 -78.254318237 +v 29.841461182 19.494409561 -79.028625488 +v 28.841423035 19.556579590 -79.813507080 +v 29.841461182 19.556579590 -79.813507080 +v 28.638660431 19.848402023 -79.813499451 +v 28.841423035 19.003412247 -77.506736755 +v 28.841423035 18.582984924 -76.813499451 +v 29.841461182 18.582984924 -76.813499451 +v 29.841461182 19.003410339 -77.506736755 +v 28.841423035 14.950710297 -76.813499451 +v 29.841461182 14.988102913 -76.813499451 +v 26.069618225 14.947598457 -77.688873291 +v 27.280761719 14.949302673 -81.662971497 +v 26.861991882 14.947669029 -81.228546143 +v 28.196334839 14.950796127 -82.088394165 +v 28.841423035 14.950710297 -82.813499451 +v 28.196334839 14.950674057 -82.655242920 +v 26.867954254 15.440361977 -82.661079407 +v 26.867954254 15.730962753 -82.088844299 +v 29.841461182 14.988102913 -82.813499451 +v 26.535839081 14.949819565 -80.187927246 +v 26.665481567 14.947300911 -80.670341492 +v 25.893093109 14.945127487 -78.708297729 +v 25.875171661 14.947607040 -79.065750122 +v 26.184097290 14.950408936 -79.661888123 +v 24.564708710 15.443685532 -78.836112976 +v 22.942348480 16.209550858 -78.833389282 +v 24.546791077 15.496338844 -79.185707092 +v 24.855716705 15.576368332 -79.749267578 +v 22.946269989 16.330913544 -79.273895264 +v 23.259548187 16.457134247 -79.899765015 +v 22.733558655 22.972330093 -76.940849304 +v 23.201393127 22.975053787 -78.108131409 +v 23.039005280 23.035600662 -77.655403137 +v 23.137948990 21.997629166 -76.671897888 +v 23.481811523 22.259363174 -77.476325989 +v 23.680122375 22.266258240 -77.976188660 +v 23.961961746 22.227539063 -78.594932556 +v 24.210037231 22.282510757 -79.203063965 +v 24.349227905 22.352766037 -79.731590271 +v 21.507698059 17.028732300 -79.056488037 +v 21.912975311 17.337978363 -79.802665710 +v 20.085742950 17.311626434 -76.427032471 +v 20.119037628 17.919717789 -78.256080627 +v 18.678516388 17.549781799 -76.917640686 +v 20.157531738 18.068082809 -78.809753418 +v 20.502685547 18.281394958 -79.627601624 +v 18.786527634 19.179737091 -79.585365295 +v 26.305461884 29.938735962 -80.742683411 +v 25.685838699 30.050054550 -84.391563416 +v 25.666475296 28.903474808 -84.421432495 +v 25.907665253 26.450117111 -80.232681274 +v 26.151550293 26.630941391 -80.783760071 +v 25.638500214 27.246767044 -84.464599609 +v 25.065311432 26.102489471 -84.893760681 +v 25.663249969 24.491418839 -80.759208679 +v 25.920974731 25.083208084 -80.329757690 +v 26.520496368 25.468971252 -81.519325256 +v 26.707901001 24.878492355 -81.515037537 +v 25.468273163 24.755270004 -81.602767944 +v 24.205516815 24.160057068 -80.901245117 +v 24.522552490 23.985321045 -80.395317078 +v 25.201683044 24.895957947 -82.327247620 +v 25.152015686 24.942050934 -84.862052917 +v 24.254001617 24.662267685 -83.632957458 +v 22.481258392 23.758296967 -82.416992188 +v 22.934574127 24.070884705 -82.038009644 +v 23.021926880 24.178052902 -84.502494812 +v 22.100090027 23.673082352 -83.052085876 +v 22.501628876 23.353986740 -81.663024902 +v 21.430339813 29.881513596 -87.753814697 +v 23.237037659 27.381513596 -89.024147034 +v 23.450878143 26.034505844 -87.742164612 +v 22.340473175 28.903474808 -88.446884155 +v 18.217596054 29.553066254 -89.729858398 +v 18.321304321 27.053068161 -90.041275024 +v 18.924333572 25.752410889 -88.468887329 +v 18.244543076 28.903474808 -89.810775757 +v 18.851167679 24.901309967 -88.307090759 +v 21.652906418 23.939552307 -84.968963623 +v 19.944507599 24.132301331 -84.900215149 +v 23.233039856 25.185806274 -87.248916626 +v 21.900707245 23.957534790 -84.885879517 +v 21.899944305 23.594797134 -83.399215698 +v 19.810281754 23.811729431 -83.664077759 +v 20.965631485 23.643945694 -84.022010803 +v 28.841423035 20.531059265 -80.823043823 +v 29.305988312 20.662504196 -80.864067078 +v 29.305988312 20.425807953 -81.893684387 +v 28.638660431 20.174221039 -80.747711182 +v 28.638660431 19.969417572 -81.669281006 +v 28.638660431 19.579319000 -81.497535706 +v 28.638660431 19.775112152 -80.663459778 +v 28.841423035 20.294363022 -81.812339783 +v 28.841423035 19.931049347 -82.659362793 +v 28.638660431 19.652805328 -82.499404907 +v 29.305988312 19.209003448 -84.008346558 +v 29.305988312 20.062494278 -82.775215149 +v 29.305988312 20.016351700 -84.809814453 +v 29.135120392 20.371606827 -84.595443726 +v 29.135120392 20.232873917 -84.595443726 +v 24.623950958 20.371696472 -84.595756531 +v 24.623950958 20.232963562 -84.595756531 +v 24.840267181 20.016441345 -84.811691284 +v 24.126316071 22.467172623 -80.870460510 +v 22.876848221 22.344964981 -81.988014221 +v 23.611503601 23.333345413 -80.670333862 +v 22.431190491 22.181167603 -82.360595703 +v 23.953485489 23.180593491 -80.251121521 +v 24.437995911 22.389266968 -80.373077393 +v 28.841423035 19.308963776 -81.378509521 +v 28.841423035 19.494409561 -80.604202271 +v 29.841461182 19.494409561 -80.604202271 +v 28.841423035 19.003412247 -82.126083374 +v 29.841461182 19.308961868 -81.378509521 +v 29.841461182 19.003410339 -82.126083374 +v 28.638660431 19.256931305 -82.271827698 +v 28.841423035 18.582984924 -82.813499451 +v 29.841461182 18.582984924 -82.813499451 +v 28.638660431 18.719408035 -83.070137024 +v 28.638660431 18.942136765 -83.509399414 +v 28.841423035 19.106327057 -83.812103271 +v 28.638660431 14.950710297 -83.069587708 +v 28.841423035 14.950710297 -83.811058044 +v 29.305988312 15.343961716 -84.008445740 +v 28.638660431 14.950710297 -83.512351990 +v 28.196334839 14.950065613 -83.009132385 +v 26.867954254 14.944667816 -83.010108948 +v 29.115013123 14.950710297 -83.927658081 +v 29.115013123 14.950710297 -84.594779968 +v 29.115013123 15.099852562 -83.927658081 +v 29.115013123 15.099847794 -84.594779968 +v 25.952377319 15.555994034 -81.667083740 +v 25.533611298 15.441658974 -81.235389709 +v 25.271785736 16.370138168 -82.129051208 +v 24.623832703 15.084804535 -84.639930725 +v 29.305988312 15.343955994 -84.809814453 +v 24.623832703 14.946317673 -84.659896851 +v 25.271785736 14.928886414 -83.643524170 +v 25.207458496 15.607461929 -80.229797363 +v 25.337100983 15.596984863 -80.685585022 +v 23.611289978 16.304542542 -80.392143250 +v 23.740932465 16.228935242 -80.838890076 +v 24.356212616 15.971117020 -81.711555481 +v 23.937442780 16.058343887 -81.356719971 +v 22.438049316 17.129186630 -80.770584106 +v 22.308406830 17.217609406 -80.322029114 +v 23.053329468 16.767339706 -81.716812134 +v 22.634559631 16.920852661 -81.308265686 +v 23.968902588 16.563045502 -82.157836914 +v 25.271785736 15.919914246 -82.709167480 +v 25.271785736 15.354865074 -83.046897888 +v 23.967103958 15.168118477 -83.748222351 +v 23.968902588 15.525764465 -83.178428650 +v 23.968902588 14.925689697 -84.537361145 +v 23.968902588 16.310291290 -82.731681824 +v 24.623405457 20.232875824 -89.113616943 +v 24.623947144 20.373481750 -89.107162476 +v 24.837772369 20.016353607 -89.284484863 +v 24.036228180 19.210620880 -89.284408569 +v 22.792137146 20.063232422 -89.284408569 +v 24.839904785 15.343955994 -84.818161011 +v 24.623832703 14.950710297 -89.087051392 +v 24.623832703 15.099842072 -89.087051392 +v 23.567571640 14.950710297 -88.617889404 +v 23.568038940 18.965358734 -88.616928101 +v 23.137702942 18.739194870 -88.616928101 +v 24.035900116 15.343950272 -89.284408569 +v 24.837772369 15.343950272 -89.284484863 +v 23.844284058 14.950710297 -88.813461304 +v 23.138065338 14.950710297 -88.648902893 +v 23.955883026 14.950710297 -89.093292236 +v 23.955883026 15.099841118 -89.093292236 +v 23.840061188 19.106328964 -88.820106506 +v 22.687324524 19.931051254 -88.820106506 +v 22.525646210 19.668455124 -88.616928101 +v 20.873836517 19.212495804 -83.716064453 +v 19.406978607 19.888927460 -84.077728271 +v 19.414176941 21.462732315 -82.990173340 +v 19.958477020 19.926210403 -82.459869385 +v 20.875013351 19.308794022 -82.895057678 +v 21.772899628 23.068262100 -82.548583984 +v 21.598121643 22.997964859 -82.851722717 +v 21.872701645 22.243719101 -83.257759094 +v 22.105762482 23.112348557 -81.993980408 +v 22.056463242 22.266183853 -82.984947205 +v 20.782215118 22.993917465 -83.395591736 +v 19.773288727 23.212579727 -83.083015442 +v 19.805349350 22.443315506 -83.586601257 +v 20.941175461 22.310987473 -83.938491821 +v 20.991313934 18.459091187 -80.719329834 +v 20.861671448 18.407154083 -80.239631653 +v 21.606594086 17.911363602 -81.903717041 +v 21.187824249 18.329421997 -81.446632385 +v 19.342899323 19.541238785 -80.956283569 +v 19.187976837 19.356319427 -80.242729187 +v 19.540824890 19.720926285 -81.783370972 +v 22.522163391 17.274503708 -82.934059143 +v 22.522163391 17.592578888 -82.239845276 +v 22.484359741 16.313440323 -84.120674133 +v 22.518768311 16.689754486 -83.503875732 +v 20.859477997 18.254144669 -84.122451782 +v 20.770414352 20.192859650 -88.616928101 +v 19.838237762 20.235130310 -88.616928101 +v 19.838233948 19.848403931 -88.616928101 +v 21.704624176 20.007087708 -88.616928101 +v 20.684436798 19.785186768 -88.616928101 +v 21.840305328 20.294364929 -88.820106506 +v 20.851005554 20.531061172 -88.820106506 +v 21.912361145 20.425811768 -89.284408569 +v 20.882747650 20.662506104 -89.284408569 +v 22.316970825 19.267770767 -88.616928101 +v 22.154048920 19.003414154 -88.820106506 +v 22.841461182 18.582986832 -88.820106506 +v 22.841461182 18.582986832 -89.813499451 +v 21.406469345 19.308963776 -88.820106506 +v 21.516656876 19.579933167 -88.616928101 +v 22.137996674 19.003410339 -89.813499451 +v 19.838356018 20.730836868 -89.284408569 +v 19.838241577 20.599390030 -88.819694519 +v 20.632162094 19.494411469 -88.820106506 +v 21.390419006 19.308961868 -89.813499451 +v 20.632162094 19.494411469 -89.813499451 +v 19.841468811 19.556581497 -88.820106506 +v 19.841468811 19.556581497 -89.813499451 +v 20.751476288 17.103826523 -84.706436157 +v 19.339275360 18.912166595 -84.394371033 +v 21.049032211 15.863847733 -85.412651062 +v 19.170335770 17.632045746 -84.910621643 +v 20.875701904 14.949906349 -86.055358887 +v 22.390251160 15.629423141 -84.830459595 +v 18.301856995 14.937114716 -86.137916565 +v 22.807044983 14.988101959 -89.813499451 +v 22.841461182 14.950710297 -88.823219299 +v 16.841459274 14.950710297 -88.820114136 +v 16.831239700 14.988101959 -89.813499451 +v 15.818071365 27.487817764 -72.805198669 +v 15.743446350 28.903474808 -73.059967041 +v 16.720575333 28.934715271 -73.143966675 +v 18.047039032 28.909187317 -73.017089844 +v 15.684791565 30.003684998 -73.260215759 +v 18.525236130 26.654150009 -71.953231812 +v 18.162685394 26.808786392 -72.181945801 +v 17.223571777 27.178163528 -72.633407593 +v 17.245525360 25.270286560 -72.983283997 +v 16.740139008 25.390344620 -73.098541260 +v 16.748601913 27.326850891 -72.748352051 +v 18.209056854 25.080087662 -72.513275146 +v 18.169246674 24.404680252 -72.365806580 +v 18.578323364 24.984479904 -72.266166687 +v 18.462497711 24.405164719 -72.263618469 +v 18.657451630 23.969886780 -73.577072144 +v 17.135082245 24.324024200 -73.184768677 +v 18.401916504 23.923854828 -73.576492310 +v 15.684789658 25.591835022 -73.260215759 +v 16.730388641 24.162227631 -73.436096191 +v 15.793661118 24.871000290 -74.105880737 +v 17.519702911 23.677232742 -74.143333435 +v 17.169528961 23.476366043 -74.372077942 +v 15.101604462 24.741426468 -74.480918884 +v 16.451955795 22.982490540 -75.175552368 +v 12.481462479 29.988111496 -72.297737122 +v 12.481460571 28.858713150 -72.394294739 +v 12.481462479 26.238111496 -73.774490356 +v 12.481455803 27.488111496 -72.685501099 +v 9.841464043 28.858713150 -72.394302368 +v 9.841464043 29.988111496 -72.297760010 +v 9.841464043 27.488111496 -72.685501099 +v 9.841464043 26.238111496 -73.774505615 +v 12.481455803 25.478315353 -73.873489380 +v 14.326726913 24.516502380 -75.883964539 +v 9.841368675 24.988111496 -77.254035950 +v 9.841463089 25.488111496 -73.873512268 +v 13.640657425 23.942804337 -77.135154724 +v 12.481455803 24.591220856 -77.239486694 +v 13.473918915 23.923679352 -77.471260071 +v 12.481456757 23.876964569 -79.813499451 +v 13.399394989 23.645229340 -78.492370605 +v 9.841464996 24.988111496 -79.813499451 +v 17.767335892 20.425807953 -70.348968506 +v 18.796949387 20.662504196 -70.348968506 +v 18.837957382 20.531059265 -70.813537598 +v 17.848655701 20.294363022 -70.813537598 +v 18.909282684 20.192857742 -71.016304016 +v 17.003326416 19.931785583 -70.813537598 +v 17.975070953 20.007085800 -71.016304016 +v 17.154048920 19.668453217 -71.016304016 +v 18.163040161 19.579931259 -71.016304016 +v 18.995262146 19.785186768 -71.016304016 +v 16.887557983 20.063230515 -70.348968506 +v 19.056587219 19.494409561 -70.813537598 +v 15.837439537 19.107944489 -70.813537598 +v 16.111656189 18.965354919 -71.016304016 +v 17.362728119 19.267768860 -71.016304016 +v 15.644033432 19.210618973 -70.348968506 +v 18.765943527 23.200260162 -74.275917053 +v 18.538703918 23.132932663 -74.281570435 +v 18.655975342 22.460165024 -73.696899414 +v 18.379405975 22.330810547 -73.808197021 +v 17.710544586 22.778474808 -74.852195740 +v 17.357477188 22.535253525 -75.078155518 +v 17.420154572 21.845836639 -74.421234131 +v 18.282279968 19.308963776 -70.813537598 +v 17.534698486 19.003412247 -70.813537598 +v 16.541996002 18.739192963 -71.016304016 +v 16.841457367 18.582984924 -70.813537598 +v 15.058970451 20.373481750 -70.519836426 +v 14.842990875 20.018226624 -70.348968506 +v 15.058971405 20.234748840 -70.519836426 +v 14.843014717 15.343950272 -70.348968506 +v 15.838632584 14.950710297 -70.813537598 +v 15.643795013 15.343950272 -70.348968506 +v 14.843015671 15.343955994 -74.815391541 +v 16.541629791 14.950710297 -71.016304016 +v 16.112125397 14.950710297 -71.016304016 +v 17.295028687 14.950681686 -73.240432739 +v 16.225315094 14.950619698 -74.404861450 +v 15.059083939 14.950710297 -70.539947510 +v 15.723994255 14.950710297 -70.539947510 +v 15.723994255 15.099841118 -70.539947510 +v 15.059083939 15.099842072 -70.539947510 +v 15.059083939 15.099847794 -75.034667969 +v 15.062631607 14.950710297 -75.032218933 +v 15.993486404 16.043682098 -74.891418457 +v 17.580718994 18.601600647 -76.582916260 +v 17.608123779 18.033020020 -75.992507935 +v 18.956254959 16.238872528 -74.623527527 +v 16.658613205 17.280139923 -75.766830444 +v 16.858667374 16.648092270 -75.071937561 +v 14.842651367 19.999349594 -74.815307617 +v 15.058968544 20.170953751 -75.031242371 +v 15.689005852 16.945304871 -75.395370483 +v 14.469780922 17.697820663 -75.849273682 +v 14.633446693 16.660942078 -75.592468262 +v 15.504920006 18.060466766 -76.458518982 +v 15.058967590 20.286876678 -75.031242371 +v 15.580442429 22.254665375 -76.067657471 +v 16.570756912 21.943433762 -75.816146851 +v 16.149850845 21.184806824 -75.519653320 +v 15.663236618 21.165817261 -76.608116150 +v 15.639649391 20.889797211 -76.033630371 +v 15.230363846 20.653139114 -76.445938110 +v 17.006267548 21.571250916 -74.693077087 +v 16.605232239 21.943433762 -75.816146851 +v 14.933979988 22.035934448 -78.985809326 +v 14.907885551 21.989849091 -78.614784241 +v 15.965945244 20.967689514 -78.567970276 +v 15.890275955 21.048503876 -78.917999268 +v 15.283826828 22.158493042 -79.612579346 +v 17.397212982 19.778511047 -78.448715210 +v 16.154167175 21.231525421 -79.520309448 +v 17.266906738 19.912086487 -78.838211060 +v 14.990085602 21.806129456 -77.740325928 +v 15.364355087 21.519338608 -77.042915344 +v 16.191160202 20.680904388 -77.723434448 +v 14.957674980 22.561777115 -76.607536316 +v 15.367637634 22.353986740 -76.263748169 +v 14.490463257 22.840423584 -77.478836060 +v 16.308990479 19.685749054 -76.490653992 +v 16.175315857 20.300151825 -76.929840088 +v 18.414966583 18.401939392 -78.381942749 +v 17.587017059 19.222574234 -77.421653748 +v 18.426315308 18.667060852 -78.873901367 +v 17.356460571 20.098489761 -79.467292786 +v 15.592473984 19.060592651 -76.803924561 +v 14.817337990 18.983997345 -76.982727051 +v 10.541421890 20.371696472 -75.031005859 +v 10.541421890 20.232963562 -75.031005859 +v 10.370553970 20.016441345 -74.814689636 +v 13.694046021 19.710155487 -76.971282959 +v 13.428596497 20.853733063 -79.356292725 +v 13.365878105 20.853733063 -78.175544739 +v 13.549103737 20.215240479 -77.503173828 +v 14.390188217 22.835433960 -77.743637085 +v 14.358359337 22.587871552 -79.014625549 +v 14.372740746 22.729833603 -78.576995850 +v 14.723241806 22.743982315 -79.741401672 +v 13.407335281 23.436180115 -79.013099670 +v 13.797117233 23.424091339 -79.871200562 +v 10.370553970 20.062496185 -76.848564148 +v 10.370553970 19.209005356 -75.615425110 +v 10.834850311 19.107944489 -75.809478760 +v 11.038029671 18.942138672 -76.114372253 +v 11.038028717 19.652805328 -77.124366760 +v 10.834851265 19.931785583 -76.975364685 +v 10.370553017 20.662506104 -78.759712219 +v 10.370553017 20.425811768 -77.730094910 +v 10.370553017 20.730836868 -79.810394287 +v 10.834851265 20.531061172 -78.809997559 +v 10.834850311 20.294364929 -77.820701599 +v 11.038028717 19.969421387 -77.954490662 +v 11.038028717 19.256933212 -77.351943970 +v 11.038028717 19.579320908 -78.126243591 +v 10.834850311 19.308963776 -78.254318237 +v 9.841461182 19.308965683 -78.254318237 +v 9.841462135 19.494411469 -79.028625488 +v 11.038028717 19.775114059 -78.960319519 +v 11.038028717 20.174222946 -78.876060486 +v 10.834851265 19.494411469 -79.028625488 +v 10.835263252 20.599390030 -79.810279846 +v 11.038028717 19.848403931 -79.810272217 +v 9.841462135 19.556581497 -79.813507080 +v 11.038028717 20.235130310 -79.810279846 +v 10.834850311 19.556581497 -79.813507080 +v 12.184720993 16.376655579 -78.849044800 +v 12.877394676 19.252302170 -79.155899048 +v 13.270064354 19.252302170 -79.928024292 +v 13.095947266 18.804010391 -77.196586609 +v 12.300619125 17.382823944 -77.248023987 +v 12.916456223 19.252302170 -77.798713684 +v 12.728396416 17.382823944 -76.634635925 +v 13.275438309 18.355716705 -76.594451904 +v 13.140472412 17.382823944 -76.043762207 +v 10.370553017 15.343961716 -75.615753174 +v 10.370471001 15.343955994 -74.815055847 +v 10.834850311 18.582986832 -76.813499451 +v 9.841462135 18.582986832 -76.813499451 +v 9.841461182 19.003414154 -77.506736755 +v 11.038029671 18.719409943 -76.553642273 +v 10.834850311 19.003414154 -77.506736755 +v 10.834848404 14.951487541 -75.810676575 +v 11.038029671 14.951487541 -76.111419678 +v 11.044261932 14.951487541 -76.557403564 +v 12.175566673 14.938457489 -75.595718384 +v 10.567905426 14.951487541 -75.032218933 +v 10.567905426 15.099847794 -75.032218933 +v 10.561667442 15.099852562 -75.695053101 +v 10.561667442 14.951487541 -75.695053101 +v 11.428196907 14.924728394 -78.595741272 +v 12.577390671 16.376655579 -79.621170044 +v 11.820867538 14.924728394 -79.367866516 +v 10.834849358 14.937757492 -76.813499451 +v 9.841461182 14.988102913 -76.813499451 +v 9.797045708 18.615947723 -76.733764648 +v 9.841461182 14.988102913 -82.813499451 +v 9.841462135 19.003414154 -82.126091003 +v 9.841462135 18.582986832 -82.813499451 +v 9.841462135 19.308965683 -81.378509521 +v 9.841461182 19.494411469 -80.604202271 +v 12.481460571 28.903474808 -86.941497803 +v 15.313100815 28.903474808 -87.490287781 +v 15.296688080 29.545209885 -87.612686157 +v 12.481462479 29.988113403 -86.941497803 +v 12.481456757 27.488113403 -86.941490173 +v 9.841465950 29.988111496 -87.329231262 +v 9.841465950 28.858713150 -87.232696533 +v 9.841465950 27.488111496 -86.941505432 +v 14.277844429 23.393224716 -80.594512939 +v 14.455027580 23.561286926 -81.253829956 +v 12.481463432 24.737924576 -82.387504578 +v 14.723598480 23.582912445 -82.016731262 +v 15.364147186 23.638420105 -83.156723022 +v 9.841465950 26.238111496 -85.852500916 +v 12.481462479 26.238113403 -85.852493286 +v 15.360628128 27.045209885 -87.135848999 +v 15.797599792 25.972791672 -86.195816040 +v 12.481456757 25.488113403 -85.753501892 +v 9.841465950 25.488111496 -85.753494263 +v 15.738739014 25.267404556 -86.186729431 +v 17.516098022 23.335683823 -82.759796143 +v 18.653358459 23.667816162 -83.212455750 +v 18.540206909 24.219396591 -84.522186279 +v 17.128713608 23.555570602 -83.480865479 +v 18.667587280 22.528354645 -83.149971008 +v 18.762870789 23.123046875 -82.676109314 +v 17.769180298 22.723236084 -82.368392944 +v 17.548355103 22.285589218 -82.930236816 +v 18.299018860 21.698566437 -83.135536194 +v 18.299633026 22.112844467 -82.534400940 +v 17.343858719 22.503286362 -82.168144226 +v 17.795085907 20.623046875 -80.934585571 +v 18.047508240 20.899999619 -81.670639038 +v 16.956222534 21.655448914 -81.338508606 +v 18.500097275 21.186233521 -82.271835327 +v 16.428874969 22.516170502 -81.662994385 +v 17.385707855 21.768718719 -81.907676697 +v 18.294757843 20.863731384 -83.381980896 +v 18.220481873 19.797840118 -83.738861084 +v 17.601169586 20.357986450 -80.238296509 +v 15.655702591 22.447345734 -80.161003113 +v 16.550643921 21.352880478 -80.101470947 +v 16.722076416 21.495004654 -80.637100220 +v 15.806715012 22.605678558 -80.663780212 +v 17.070299149 22.044887543 -82.607398987 +v 16.659742355 20.853733063 -82.871192932 +v 15.355899811 20.853733063 -82.468498230 +v 17.451713562 20.503826141 -83.742225647 +v 15.295934677 23.441549301 -82.610466003 +v 15.500967979 23.194305420 -81.540473938 +v 15.980512619 22.986270905 -82.037948608 +v 15.275939941 23.389579773 -80.901260376 +v 15.670108795 23.259626389 -82.971130371 +v 16.010105133 22.582714081 -81.228546143 +v 16.294021606 22.860517502 -82.340133667 +v 17.028942108 23.000946045 -82.523216248 +v 16.547252655 23.328453064 -83.191894531 +v 15.091239929 23.186864853 -80.365119934 +v 13.821267128 20.853733063 -80.128417969 +v 14.063079834 20.853733063 -80.807723999 +v 14.746932030 20.853733063 -82.280418396 +v 14.285540581 20.853733063 -81.432670593 +v 13.734337807 19.252302170 -81.232284546 +v 9.841464996 24.988111496 -82.387512207 +v 10.834851265 20.531061172 -80.823043823 +v 10.370553017 20.662506104 -80.854789734 +v 10.834850311 19.494411469 -80.604202271 +v 11.038027763 19.785186768 -80.656471252 +v 11.038027763 20.192859650 -80.742454529 +v 10.370553017 20.425811768 -81.884399414 +v 10.370553017 20.063232422 -82.764175415 +v 10.834851265 20.294364929 -81.812347412 +v 11.038027763 19.579933167 -81.488693237 +v 11.038027763 20.007087708 -81.676666260 +v 11.038027763 19.668455124 -82.497688293 +v 10.547799110 20.234748840 -84.595985413 +v 15.059513092 20.232875824 -84.595672607 +v 15.059513092 20.371608734 -84.595672607 +v 10.370471001 20.016353607 -84.809814453 +v 14.845148087 20.016353607 -84.811607361 +v 14.552314758 19.252302170 -82.628776550 +v 14.338544846 16.376655579 -82.885971069 +v 16.162754059 19.252302170 -83.525245667 +v 10.834851265 19.003414154 -82.126091003 +v 10.834851265 19.308963776 -81.378509521 +v 11.038027763 19.267770767 -82.289009094 +v 10.834851265 19.106328964 -83.812103271 +v 10.834851265 19.931051254 -82.659362793 +v 10.370553017 19.210620880 -84.008270264 +v 11.038027763 18.965358734 -83.540084839 +v 11.038027763 18.739194870 -83.109741211 +v 10.834850311 18.582986832 -82.813499451 +v 13.707309723 19.186428070 -81.221466064 +v 13.041664124 16.376655579 -80.925430298 +v 13.527381897 14.924728394 -83.489402771 +v 11.570766449 14.924728394 -80.672126770 +v 10.834849358 14.937757492 -82.813499451 +v 17.518198013 19.252302170 -84.293426514 +v 18.020149231 18.335988998 -84.308212280 +v 18.102117538 15.832498550 -85.543647766 +v 18.853557587 15.782326698 -85.912292480 +v 16.129116058 16.376655579 -83.949760437 +v 15.060180664 14.937757492 -84.592330933 +v 17.285715103 16.376655579 -85.060470581 +v 18.787670135 20.662506104 -89.284408569 +v 17.758056641 20.425811768 -89.284408569 +v 16.876522064 20.062496185 -89.284408569 +v 17.848659515 20.294364929 -88.820106506 +v 18.837959290 20.531061172 -88.820106506 +v 18.904026031 20.174222946 -88.616928101 +v 16.581602097 18.719409943 -88.616928101 +v 17.379907608 19.256933212 -88.616928101 +v 17.152332306 19.652805328 -88.616928101 +v 17.003330231 19.931785583 -88.820106506 +v 15.837440491 19.107944489 -88.820106506 +v 16.142333984 18.942138672 -88.616928101 +v 16.841457367 18.582986832 -88.820106506 +v 16.831239700 18.582984924 -89.813499451 +v 17.534702301 19.003414154 -89.813499451 +v 19.056587219 19.494411469 -88.820106506 +v 17.982456207 19.969421387 -88.616928101 +v 18.988279343 19.775114059 -88.616928101 +v 18.154201508 19.579320908 -88.616928101 +v 17.769893646 14.937680244 -86.186363220 +v 16.585369110 14.950710297 -88.610694885 +v 17.534702301 19.003414154 -88.820106506 +v 18.282279968 19.308965683 -89.813499451 +v 19.056587219 19.494411469 -89.813499451 +v 18.282279968 19.308963776 -88.820106506 +v 16.139383316 14.950710297 -88.616928101 +v 10.370471001 15.343950272 -84.809814453 +v 11.038027763 14.937757492 -83.539611816 +v 10.567904472 15.099842072 -84.595870972 +v 15.060180664 15.099842072 -84.592330933 +v 14.845148087 15.343950272 -84.811607361 +v 10.567904472 14.937757492 -84.595870972 +v 10.841497421 14.937757492 -83.816322327 +v 11.038027763 14.937757492 -83.110107422 +v 10.370553017 15.343950272 -84.007942200 +v 10.561665535 15.099841118 -83.927925110 +v 10.561665535 14.937757492 -83.927925110 +v 15.643386841 19.209005356 -89.284408569 +v 14.842651367 20.016441345 -89.284408569 +v 15.058968544 20.371696472 -89.113540649 +v 15.059513092 20.232873917 -89.107162476 +v 15.643715858 15.343961716 -89.284408569 +v 15.838634491 14.950710297 -88.820114136 +v 14.843015671 15.343955994 -89.284484863 +v 15.060181618 14.950710297 -89.087051392 +v 15.060180664 15.099847794 -89.087051392 +v 15.723011017 14.950710297 -89.093292236 +v 15.723011017 15.099852562 -89.093292236 +f 1 2 3 +f 4 5 1 +f 6 7 4 +f 7 8 4 +f 8 9 4 +f 7 10 8 +f 4 1 3 +f 11 12 6 +f 12 13 6 +f 13 14 6 +f 12 15 13 +f 16 17 12 +f 6 18 7 +f 18 19 7 +f 6 20 18 +f 11 6 4 +f 11 16 12 +f 11 21 16 +f 22 23 11 +f 23 24 11 +f 24 25 11 +f 23 26 24 +f 22 27 23 +f 27 28 23 +f 29 30 3 +f 30 31 3 +f 31 22 3 +f 22 4 3 +f 22 11 4 +f 30 32 31 +f 31 33 22 +f 33 34 22 +f 31 35 33 +f 22 36 27 +f 37 38 39 +f 40 39 41 +f 41 42 40 +f 39 40 37 +f 43 44 38 +f 38 37 43 +f 45 46 47 +f 47 48 45 +f 49 46 45 +f 45 50 49 +f 51 52 53 +f 53 50 51 +f 49 54 55 +f 55 56 49 +f 54 49 50 +f 50 53 54 +f 48 47 39 +f 46 49 56 +f 50 45 44 +f 39 47 46 +f 44 45 48 +f 39 38 48 +f 44 51 50 +f 56 41 46 +f 46 41 39 +f 48 38 44 +f 52 57 58 +f 58 53 52 +f 59 55 54 +f 54 60 59 +f 60 54 53 +f 53 58 60 +f 61 58 57 +f 62 59 60 +f 60 63 62 +f 57 64 61 +f 63 60 58 +f 58 61 63 +f 65 66 67 +f 67 68 65 +f 69 70 71 +f 71 72 69 +f 72 71 66 +f 66 65 72 +f 64 73 74 +f 74 61 64 +f 75 62 63 +f 63 76 75 +f 76 63 61 +f 61 74 76 +f 77 75 76 +f 76 78 77 +f 73 79 80 +f 80 74 73 +f 78 76 74 +f 74 80 78 +f 81 82 83 +f 83 84 81 +f 81 85 86 +f 86 82 81 +f 87 77 78 +f 78 85 87 +f 79 88 86 +f 86 80 79 +f 85 78 80 +f 80 86 85 +f 82 86 88 +f 85 81 70 +f 84 83 66 +f 70 81 84 +f 66 83 82 +f 82 67 66 +f 66 71 84 +f 88 67 82 +f 84 71 70 +f 70 87 85 +f 6 14 89 +f 89 90 6 +f 14 13 91 +f 91 89 14 +f 13 15 92 +f 92 91 13 +f 90 89 93 +f 93 94 90 +f 89 91 95 +f 95 93 89 +f 91 92 96 +f 96 95 91 +f 97 98 19 +f 18 20 99 +f 99 97 18 +f 20 6 90 +f 90 99 20 +f 7 19 98 +f 98 100 7 +f 19 18 97 +f 99 90 94 +f 94 101 99 +f 100 98 102 +f 102 103 100 +f 98 97 104 +f 104 102 98 +f 97 99 101 +f 101 104 97 +f 15 12 105 +f 105 92 15 +f 12 17 106 +f 106 105 12 +f 107 106 17 +f 108 96 92 +f 105 106 109 +f 109 108 105 +f 110 109 106 +f 92 105 108 +f 17 16 107 +f 16 21 111 +f 111 107 16 +f 21 11 112 +f 112 111 21 +f 106 107 110 +f 107 111 113 +f 113 110 107 +f 111 112 114 +f 114 113 111 +f 11 25 115 +f 115 112 11 +f 25 24 116 +f 116 115 25 +f 24 26 117 +f 117 116 24 +f 26 23 118 +f 118 117 26 +f 23 28 119 +f 119 118 23 +f 120 121 116 +f 112 115 122 +f 122 114 112 +f 115 116 121 +f 121 122 115 +f 116 117 120 +f 123 124 118 +f 117 118 124 +f 124 120 117 +f 118 119 123 +f 28 27 125 +f 125 119 28 +f 27 36 126 +f 126 125 27 +f 127 126 36 +f 125 126 128 +f 119 125 129 +f 129 123 119 +f 130 128 126 +f 128 129 125 +f 96 108 131 +f 131 132 96 +f 108 109 133 +f 133 131 108 +f 134 133 109 +f 94 93 135 +f 135 136 94 +f 93 95 137 +f 137 135 93 +f 95 96 132 +f 132 137 95 +f 138 139 102 +f 104 101 140 +f 140 138 104 +f 101 94 136 +f 136 140 101 +f 103 102 139 +f 139 141 103 +f 102 104 138 +f 136 135 142 +f 142 143 136 +f 135 137 144 +f 144 142 135 +f 137 132 145 +f 145 144 137 +f 143 142 146 +f 146 147 143 +f 142 144 148 +f 148 146 142 +f 144 145 149 +f 149 148 144 +f 150 151 152 +f 152 153 150 +f 151 143 147 +f 147 152 151 +f 154 155 156 +f 156 157 154 +f 147 146 155 +f 155 154 147 +f 157 156 158 +f 155 159 160 +f 160 156 155 +f 146 148 159 +f 159 155 146 +f 161 158 162 +f 162 158 156 +f 163 164 153 +f 165 166 161 +f 164 163 167 +f 167 168 164 +f 153 152 163 +f 166 165 168 +f 168 167 166 +f 161 166 167 +f 167 157 161 +f 158 161 157 +f 165 161 162 +f 163 154 157 +f 157 167 163 +f 152 147 154 +f 154 163 152 +f 169 145 132 +f 131 133 170 +f 170 169 131 +f 171 170 133 +f 132 131 169 +f 145 169 172 +f 172 149 145 +f 169 170 173 +f 173 172 169 +f 174 173 170 +f 148 149 175 +f 175 159 148 +f 159 175 176 +f 176 160 159 +f 156 160 162 +f 177 162 160 +f 160 176 177 +f 165 162 178 +f 179 175 149 +f 162 180 178 +f 162 177 180 +f 176 181 180 +f 180 177 176 +f 175 179 181 +f 181 176 175 +f 149 172 179 +f 182 181 179 +f 172 173 183 +f 184 180 181 +f 181 182 184 +f 183 179 172 +f 179 183 182 +f 180 184 178 +f 185 182 183 +f 186 183 173 +f 178 184 182 +f 182 185 178 +f 183 186 185 +f 173 174 186 +f 140 136 143 +f 143 151 140 +f 141 139 187 +f 187 188 141 +f 139 138 150 +f 150 187 139 +f 138 140 151 +f 151 150 138 +f 188 187 189 +f 189 190 188 +f 187 150 153 +f 153 189 187 +f 191 192 193 +f 194 193 195 +f 196 195 165 +f 189 153 164 +f 165 197 198 +f 198 168 165 +f 164 199 189 +f 195 197 165 +f 199 164 168 +f 168 198 199 +f 193 198 197 +f 192 199 198 +f 198 193 192 +f 190 189 199 +f 199 192 190 +f 197 195 193 +f 123 129 200 +f 200 201 123 +f 129 128 202 +f 202 200 129 +f 203 202 128 +f 109 110 134 +f 110 113 204 +f 204 134 110 +f 113 114 205 +f 205 204 113 +f 114 122 206 +f 206 205 114 +f 122 121 207 +f 207 206 122 +f 121 120 208 +f 208 207 121 +f 120 124 209 +f 209 208 120 +f 124 123 201 +f 201 209 124 +f 133 134 171 +f 134 204 210 +f 210 171 134 +f 204 205 211 +f 211 210 204 +f 212 213 207 +f 205 206 214 +f 214 211 205 +f 206 207 213 +f 213 214 206 +f 207 208 212 +f 208 209 215 +f 215 212 208 +f 209 201 216 +f 216 215 209 +f 217 218 219 +f 165 178 220 +f 178 221 220 +f 221 217 220 +f 221 218 217 +f 170 171 174 +f 171 210 222 +f 222 174 171 +f 210 211 223 +f 223 222 210 +f 185 224 225 +f 226 186 174 +f 225 178 185 +f 186 226 224 +f 224 185 186 +f 174 222 226 +f 222 223 227 +f 228 225 224 +f 224 229 228 +f 227 226 222 +f 178 228 221 +f 178 225 228 +f 226 227 229 +f 229 224 226 +f 211 214 230 +f 230 223 211 +f 214 213 231 +f 231 230 214 +f 213 212 232 +f 232 231 213 +f 212 215 233 +f 233 232 212 +f 215 216 234 +f 234 233 215 +f 229 235 236 +f 227 237 235 +f 235 229 227 +f 236 228 229 +f 228 236 221 +f 221 236 235 +f 223 230 237 +f 237 227 223 +f 238 237 230 +f 235 239 221 +f 237 238 239 +f 239 235 237 +f 230 231 238 +f 219 239 238 +f 240 238 231 +f 238 240 219 +f 218 221 239 +f 239 219 218 +f 231 232 240 +f 240 241 242 +f 242 219 240 +f 219 242 217 +f 232 233 241 +f 241 240 232 +f 201 200 243 +f 243 216 201 +f 244 243 200 +f 200 202 244 +f 245 244 202 +f 216 243 246 +f 246 234 216 +f 243 244 247 +f 247 246 243 +f 248 247 244 +f 249 241 233 +f 242 250 251 +f 251 217 242 +f 217 251 220 +f 241 249 250 +f 250 242 241 +f 233 234 249 +f 234 246 252 +f 252 249 234 +f 220 251 250 +f 250 253 220 +f 249 252 253 +f 253 250 249 +f 252 254 255 +f 255 253 252 +f 246 247 254 +f 254 252 246 +f 256 220 253 +f 253 255 256 +f 220 256 257 +f 258 254 247 +f 259 255 254 +f 257 256 255 +f 9 8 260 +f 8 10 261 +f 261 260 8 +f 10 7 100 +f 100 261 10 +f 260 262 9 +f 263 264 5 +f 4 9 262 +f 262 263 4 +f 5 4 263 +f 262 260 265 +f 260 261 266 +f 266 265 260 +f 261 100 103 +f 103 266 261 +f 263 262 267 +f 267 268 263 +f 268 269 264 +f 265 267 262 +f 264 263 268 +f 3 2 270 +f 270 271 3 +f 2 1 272 +f 272 270 2 +f 5 264 1 +f 264 272 1 +f 3 271 29 +f 270 272 273 +f 273 274 270 +f 272 264 269 +f 269 273 272 +f 271 270 274 +f 275 271 276 +f 274 276 271 +f 31 32 277 +f 277 278 31 +f 32 30 279 +f 279 277 32 +f 30 29 275 +f 275 279 30 +f 271 275 29 +f 278 277 280 +f 280 281 278 +f 277 279 282 +f 282 280 277 +f 279 275 283 +f 283 282 279 +f 276 283 275 +f 284 285 34 +f 36 22 127 +f 22 34 285 +f 285 127 22 +f 33 35 286 +f 35 31 278 +f 278 286 35 +f 286 284 33 +f 34 33 284 +f 287 130 127 +f 288 287 285 +f 126 127 130 +f 127 285 287 +f 289 288 284 +f 286 278 281 +f 285 284 288 +f 281 289 286 +f 284 286 289 +f 290 291 274 +f 269 292 273 +f 292 290 273 +f 276 274 291 +f 291 293 276 +f 274 273 290 +f 293 294 276 +f 267 265 295 +f 265 266 296 +f 296 295 265 +f 266 103 141 +f 141 296 266 +f 295 297 267 +f 298 292 269 +f 268 267 297 +f 297 298 268 +f 269 268 298 +f 297 295 299 +f 295 296 300 +f 300 299 295 +f 296 141 188 +f 188 300 296 +f 299 301 297 +f 292 298 302 +f 302 303 292 +f 298 297 301 +f 301 302 298 +f 303 304 290 +f 305 306 304 +f 291 290 304 +f 304 306 291 +f 290 292 303 +f 301 299 307 +f 307 308 301 +f 299 300 309 +f 309 307 299 +f 300 188 190 +f 190 309 300 +f 310 196 311 +f 195 310 194 +f 312 196 165 +f 196 310 195 +f 313 191 194 +f 307 309 191 +f 193 194 191 +f 309 190 192 +f 192 191 309 +f 311 194 310 +f 308 307 313 +f 191 313 307 +f 313 314 308 +f 314 313 311 +f 194 311 313 +f 315 311 196 +f 311 315 314 +f 196 316 315 +f 317 316 196 +f 318 304 319 +f 304 303 319 +f 303 302 320 +f 320 319 303 +f 302 301 308 +f 308 320 302 +f 321 322 323 +f 312 317 196 +f 324 323 319 +f 312 325 317 +f 317 325 322 +f 320 308 314 +f 324 314 315 +f 315 321 324 +f 316 317 321 +f 321 315 316 +f 319 320 324 +f 322 321 317 +f 314 324 320 +f 323 324 321 +f 326 322 325 +f 322 326 327 +f 318 319 323 +f 323 327 318 +f 327 323 322 +f 326 328 329 +f 330 318 327 +f 312 331 328 +f 328 326 312 +f 329 328 332 +f 327 329 330 +f 318 330 333 +f 329 327 326 +f 334 335 44 +f 335 334 336 +f 336 337 335 +f 44 43 334 +f 42 41 338 +f 339 338 333 +f 333 330 339 +f 338 339 42 +f 42 339 334 +f 330 332 339 +f 332 330 329 +f 42 43 37 +f 37 40 42 +f 334 43 42 +f 340 305 333 +f 340 306 305 +f 341 306 340 +f 56 55 342 +f 333 305 304 +f 343 344 335 +f 335 344 51 +f 51 44 335 +f 344 345 52 +f 52 51 344 +f 346 345 344 +f 344 343 346 +f 335 337 343 +f 347 348 64 +f 64 57 347 +f 349 348 347 +f 345 346 350 +f 347 350 349 +f 350 347 345 +f 345 347 57 +f 57 52 345 +f 342 351 352 +f 352 341 342 +f 353 306 341 +f 341 352 353 +f 342 55 59 +f 59 351 342 +f 306 353 293 +f 294 293 353 +f 353 354 294 +f 293 291 306 +f 354 353 352 +f 62 355 351 +f 351 355 356 +f 356 352 351 +f 351 59 62 +f 352 356 354 +f 342 357 56 +f 41 56 357 +f 357 338 41 +f 338 357 340 +f 340 333 338 +f 357 342 341 +f 341 340 357 +f 333 304 318 +f 358 328 331 +f 332 328 358 +f 281 280 359 +f 359 360 281 +f 280 282 361 +f 361 359 280 +f 282 283 362 +f 362 361 282 +f 276 294 283 +f 294 362 283 +f 363 203 130 +f 364 363 287 +f 128 130 203 +f 130 287 363 +f 365 364 288 +f 289 281 360 +f 360 365 289 +f 287 288 364 +f 288 289 365 +f 325 312 326 +f 312 165 220 +f 366 331 312 +f 356 367 368 +f 355 62 75 +f 75 369 355 +f 355 369 367 +f 367 356 355 +f 339 332 370 +f 334 339 371 +f 372 336 334 +f 331 366 358 +f 366 373 374 +f 374 358 366 +f 375 376 377 +f 377 378 375 +f 376 375 358 +f 378 377 379 +f 358 374 376 +f 334 380 372 +f 370 371 339 +f 371 380 334 +f 358 370 332 +f 361 362 368 +f 368 381 361 +f 381 368 367 +f 362 294 354 +f 368 354 356 +f 354 368 362 +f 67 88 382 +f 383 382 88 +f 382 384 385 +f 385 386 382 +f 88 79 383 +f 383 387 384 +f 384 382 383 +f 382 386 67 +f 388 367 369 +f 369 389 388 +f 367 388 381 +f 369 75 77 +f 77 389 369 +f 79 73 390 +f 390 391 387 +f 387 383 390 +f 348 390 73 +f 348 349 391 +f 73 64 348 +f 390 383 79 +f 391 390 348 +f 359 361 381 +f 381 379 359 +f 360 359 379 +f 379 392 360 +f 381 393 379 +f 389 77 87 +f 394 381 388 +f 393 395 379 +f 394 395 393 +f 381 394 393 +f 203 363 396 +f 202 203 245 +f 396 245 203 +f 397 396 363 +f 398 397 364 +f 392 398 365 +f 365 360 392 +f 363 364 397 +f 364 365 398 +f 373 366 399 +f 366 312 399 +f 312 400 399 +f 312 220 400 +f 244 245 248 +f 245 396 401 +f 401 248 245 +f 396 397 402 +f 402 401 396 +f 403 258 248 +f 247 248 258 +f 404 259 258 +f 220 257 400 +f 255 259 257 +f 405 257 259 +f 259 404 405 +f 254 258 259 +f 248 401 403 +f 257 405 400 +f 258 403 404 +f 400 405 404 +f 406 403 401 +f 403 406 407 +f 404 407 400 +f 401 402 406 +f 407 404 403 +f 397 398 408 +f 408 402 397 +f 398 392 409 +f 409 408 398 +f 392 379 377 +f 377 409 392 +f 373 399 410 +f 411 412 410 +f 399 413 414 +f 414 410 399 +f 400 413 399 +f 411 406 402 +f 406 411 414 +f 414 407 406 +f 407 414 413 +f 402 408 411 +f 413 400 407 +f 376 412 409 +f 374 410 412 +f 409 377 376 +f 410 374 373 +f 412 376 374 +f 408 409 412 +f 412 411 408 +f 410 414 411 +f 389 415 394 +f 394 388 389 +f 415 416 395 +f 379 395 378 +f 87 415 389 +f 395 394 415 +f 415 87 70 +f 70 416 415 +f 68 67 386 +f 385 372 380 +f 380 386 385 +f 416 371 378 +f 416 70 69 +f 69 371 416 +f 378 395 416 +f 386 380 68 +f 378 370 358 +f 68 69 72 +f 72 65 68 +f 370 378 371 +f 358 375 378 +f 69 68 380 +f 380 371 69 +f 417 418 419 +f 420 421 418 +f 422 423 421 +f 424 425 423 +f 426 427 425 +f 428 429 427 +f 430 431 429 +f 432 433 431 +f 418 417 420 +f 427 426 428 +f 423 422 424 +f 429 428 430 +f 421 420 422 +f 431 430 432 +f 425 424 426 +f 433 432 434 +f 435 436 437 +f 438 436 435 +f 439 437 436 +f 437 439 440 +f 429 438 435 +f 438 429 431 +f 441 442 443 +f 441 433 442 +f 444 436 445 +f 438 445 436 +f 446 445 438 +f 446 433 441 +f 431 446 438 +f 446 431 433 +f 384 447 448 +f 436 444 439 +f 385 384 448 +f 447 449 450 +f 384 387 447 +f 448 450 451 +f 450 448 447 +f 440 452 437 +f 453 437 452 +f 435 437 453 +f 452 440 454 +f 427 435 453 +f 435 427 429 +f 454 455 452 +f 456 452 455 +f 425 453 456 +f 453 425 427 +f 453 452 456 +f 455 454 457 +f 458 459 460 +f 387 391 459 +f 387 459 447 +f 449 447 459 +f 459 458 449 +f 391 460 459 +f 461 460 462 +f 349 462 460 +f 391 349 460 +f 460 461 458 +f 457 458 461 +f 449 454 440 +f 454 449 458 +f 450 440 439 +f 440 450 449 +f 451 439 444 +f 439 451 450 +f 458 457 454 +f 463 464 451 +f 465 372 464 +f 448 464 372 +f 466 467 468 +f 469 470 466 +f 468 471 466 +f 470 472 467 +f 467 466 470 +f 466 471 469 +f 469 471 468 +f 451 464 448 +f 444 445 469 +f 444 463 451 +f 372 385 448 +f 470 445 446 +f 441 470 446 +f 445 470 469 +f 473 444 469 +f 473 463 444 +f 470 441 472 +f 474 455 475 +f 456 455 474 +f 475 457 476 +f 457 475 455 +f 423 456 474 +f 456 423 425 +f 477 475 478 +f 474 475 477 +f 421 474 477 +f 474 421 423 +f 478 476 479 +f 476 478 475 +f 480 462 481 +f 476 461 480 +f 461 476 457 +f 462 480 461 +f 350 481 462 +f 349 350 462 +f 479 480 482 +f 482 481 483 +f 483 484 482 +f 346 343 483 +f 346 483 481 +f 350 346 481 +f 481 482 480 +f 480 479 476 +f 485 479 486 +f 479 485 478 +f 487 478 485 +f 418 477 487 +f 477 418 421 +f 477 478 487 +f 484 483 488 +f 488 489 484 +f 482 486 479 +f 486 482 484 +f 343 488 483 +f 484 490 486 +f 491 485 492 +f 487 419 418 +f 487 485 491 +f 490 484 489 +f 492 486 490 +f 486 492 485 +f 493 419 491 +f 493 494 419 +f 419 487 491 +f 493 495 494 +f 372 465 336 +f 337 336 465 +f 496 497 465 +f 498 489 496 +f 499 491 492 +f 489 498 490 +f 490 499 492 +f 500 491 501 +f 491 500 493 +f 491 499 501 +f 499 490 498 +f 337 497 488 +f 343 337 488 +f 489 488 497 +f 497 496 489 +f 465 497 337 +f 499 502 503 +f 503 502 504 +f 505 504 500 +f 504 505 503 +f 503 505 499 +f 499 505 501 +f 500 501 505 +f 506 442 433 +f 507 443 442 +f 508 441 443 +f 433 434 506 +f 442 506 507 +f 443 507 508 +f 472 508 509 +f 463 473 510 +f 469 468 511 +f 512 467 472 +f 508 472 441 +f 473 469 513 +f 464 463 514 +f 515 468 467 +f 511 513 469 +f 510 514 463 +f 513 510 473 +f 515 511 468 +f 514 516 464 +f 467 512 515 +f 472 509 512 +f 499 498 517 +f 518 500 504 +f 502 499 519 +f 498 496 520 +f 465 464 516 +f 496 465 521 +f 522 504 502 +f 516 521 465 +f 520 517 498 +f 502 519 522 +f 517 519 499 +f 521 520 496 +f 504 522 518 +f 494 523 417 +f 493 524 525 +f 495 525 523 +f 493 518 524 +f 518 493 500 +f 525 495 493 +f 417 419 494 +f 523 494 495 +f 508 506 434 +f 508 507 506 +f 508 434 526 +f 434 527 526 +f 526 528 529 +f 527 528 526 +f 530 529 528 +f 531 532 533 +f 527 434 432 +f 529 530 531 +f 534 528 535 +f 527 535 528 +f 536 535 527 +f 528 534 530 +f 432 536 527 +f 536 432 430 +f 532 531 530 +f 537 538 539 +f 540 530 534 +f 532 539 538 +f 538 533 532 +f 530 540 532 +f 535 541 534 +f 541 535 542 +f 536 542 535 +f 430 543 536 +f 543 430 428 +f 543 542 536 +f 544 542 545 +f 428 546 543 +f 546 428 426 +f 542 544 541 +f 543 545 542 +f 546 545 543 +f 534 547 540 +f 548 539 549 +f 550 548 549 +f 540 549 539 +f 539 532 540 +f 549 540 547 +f 547 534 541 +f 548 537 539 +f 551 550 552 +f 547 552 549 +f 553 541 544 +f 541 553 547 +f 550 549 552 +f 552 547 553 +f 510 554 514 +f 514 555 516 +f 556 555 514 +f 513 533 510 +f 533 513 531 +f 511 526 529 +f 509 526 557 +f 526 509 508 +f 531 511 529 +f 526 511 557 +f 511 531 513 +f 514 554 556 +f 556 554 538 +f 533 538 554 +f 537 556 538 +f 554 510 533 +f 558 515 512 +f 512 559 558 +f 509 557 559 +f 559 512 509 +f 515 558 511 +f 559 557 511 +f 511 558 559 +f 545 560 544 +f 560 545 561 +f 426 562 546 +f 562 426 424 +f 546 561 545 +f 562 561 546 +f 424 563 562 +f 563 424 422 +f 561 564 560 +f 564 561 565 +f 562 565 561 +f 563 565 562 +f 551 552 566 +f 567 551 566 +f 566 553 568 +f 553 566 552 +f 544 568 553 +f 568 544 560 +f 567 566 569 +f 570 567 569 +f 570 569 571 +f 568 569 566 +f 572 560 564 +f 560 572 568 +f 572 571 569 +f 569 568 572 +f 573 565 574 +f 422 575 563 +f 575 422 420 +f 565 573 564 +f 563 574 565 +f 575 574 563 +f 576 564 573 +f 574 577 573 +f 578 570 571 +f 564 576 572 +f 573 579 576 +f 576 580 571 +f 571 572 576 +f 577 574 581 +f 575 581 574 +f 582 581 575 +f 579 573 577 +f 420 582 575 +f 582 420 417 +f 524 417 523 +f 524 523 525 +f 582 417 524 +f 521 516 579 +f 580 516 583 +f 583 516 555 +f 520 521 577 +f 584 581 582 +f 577 581 517 +f 524 584 582 +f 584 524 518 +f 583 585 580 +f 578 571 580 +f 585 578 580 +f 579 516 580 +f 577 521 579 +f 580 576 579 +f 520 577 517 +f 581 584 517 +f 519 586 587 +f 517 586 519 +f 517 587 586 +f 584 518 522 +f 517 584 587 +f 522 587 584 +f 587 522 519 +f 588 589 590 +f 591 592 593 +f 593 589 591 +f 594 591 589 +f 589 588 594 +f 595 596 592 +f 592 591 595 +f 597 598 599 +f 590 600 599 +f 599 598 590 +f 589 593 600 +f 600 590 589 +f 590 601 588 +f 602 597 603 +f 602 598 597 +f 601 590 598 +f 598 602 601 +f 594 604 605 +f 606 604 607 +f 604 594 588 +f 588 607 604 +f 608 595 591 +f 591 594 608 +f 605 608 594 +f 609 610 611 +f 612 610 609 +f 610 612 596 +f 596 595 610 +f 611 610 595 +f 609 611 613 +f 613 611 614 +f 595 608 611 +f 614 611 608 +f 608 605 614 +f 615 616 617 +f 618 616 615 +f 618 614 616 +f 613 614 618 +f 615 617 619 +f 616 614 605 +f 605 620 616 +f 617 616 620 +f 621 622 623 +f 620 605 604 +f 604 606 620 +f 622 620 606 +f 606 623 622 +f 620 622 617 +f 619 624 625 +f 619 617 624 +f 626 627 628 +f 626 629 627 +f 625 629 626 +f 625 624 629 +f 624 617 622 +f 630 621 631 +f 622 621 624 +f 629 624 621 +f 621 630 629 +f 627 629 630 +f 630 632 627 +f 607 633 606 +f 623 606 633 +f 633 634 623 +f 633 607 635 +f 634 633 636 +f 636 637 634 +f 638 603 639 +f 602 638 635 +f 638 602 603 +f 640 639 641 +f 640 638 639 +f 637 636 640 +f 635 601 602 +f 636 635 638 +f 638 640 636 +f 607 588 601 +f 601 635 607 +f 635 636 633 +f 642 643 644 +f 644 645 642 +f 637 646 647 +f 643 647 646 +f 646 644 643 +f 647 634 637 +f 648 649 650 +f 640 651 637 +f 646 637 651 +f 651 650 646 +f 644 646 650 +f 651 641 648 +f 651 640 641 +f 648 650 651 +f 652 650 649 +f 652 653 650 +f 654 652 655 +f 650 653 644 +f 654 653 652 +f 645 644 653 +f 653 654 645 +f 643 642 656 +f 631 623 634 +f 657 631 647 +f 634 647 631 +f 647 643 657 +f 656 657 643 +f 632 630 657 +f 623 631 621 +f 657 656 632 +f 631 657 630 +f 658 659 660 +f 661 593 592 +f 662 659 663 +f 663 661 662 +f 659 658 664 +f 664 663 659 +f 662 592 596 +f 592 662 661 +f 665 666 667 +f 668 669 665 +f 670 669 668 +f 668 603 597 +f 597 670 668 +f 593 661 671 +f 671 600 593 +f 661 663 672 +f 672 671 661 +f 663 664 673 +f 673 672 663 +f 672 673 674 +f 674 675 672 +f 600 671 676 +f 676 599 600 +f 671 672 675 +f 675 676 671 +f 674 676 675 +f 674 599 676 +f 670 599 674 +f 670 597 599 +f 677 673 664 +f 678 664 658 +f 679 658 680 +f 681 679 682 +f 681 683 684 +f 684 685 681 +f 679 681 685 +f 685 678 679 +f 658 679 678 +f 673 677 686 +f 687 674 686 +f 687 669 670 +f 686 674 673 +f 674 687 670 +f 688 689 684 +f 678 685 690 +f 664 678 677 +f 685 684 689 +f 689 690 685 +f 690 677 678 +f 691 686 677 +f 692 691 693 +f 693 694 692 +f 687 686 691 +f 691 692 687 +f 695 694 693 +f 695 687 692 +f 695 689 688 +f 695 693 689 +f 690 689 693 +f 688 684 696 +f 693 691 690 +f 677 690 691 +f 692 694 695 +f 666 696 697 +f 688 666 695 +f 688 696 666 +f 696 698 697 +f 697 698 699 +f 669 687 695 +f 667 697 700 +f 666 665 669 +f 556 537 699 +f 666 669 695 +f 699 700 697 +f 697 667 666 +f 612 701 702 +f 701 703 660 +f 703 704 680 +f 704 701 612 +f 704 703 701 +f 705 612 609 +f 706 707 708 +f 709 710 706 +f 705 708 707 +f 711 708 705 +f 707 706 710 +f 705 707 712 +f 713 714 715 +f 716 717 710 +f 717 716 718 +f 710 719 707 +f 719 710 717 +f 715 718 716 +f 718 715 714 +f 680 660 703 +f 702 660 659 +f 659 662 702 +f 682 680 704 +f 702 596 612 +f 660 680 658 +f 660 702 701 +f 596 702 662 +f 704 612 712 +f 612 705 712 +f 712 707 719 +f 712 720 704 +f 721 722 723 +f 722 724 725 +f 726 724 722 +f 727 724 726 +f 722 728 726 +f 720 725 724 +f 683 681 729 +f 682 729 681 +f 680 682 679 +f 721 729 727 +f 729 682 720 +f 723 683 721 +f 729 721 683 +f 722 712 719 +f 712 722 725 +f 722 717 723 +f 704 720 682 +f 717 722 719 +f 720 712 725 +f 714 730 718 +f 730 714 731 +f 731 714 713 +f 713 732 731 +f 718 723 717 +f 723 718 730 +f 727 728 721 +f 720 727 729 +f 728 722 721 +f 724 727 720 +f 726 728 727 +f 609 711 705 +f 711 609 613 +f 613 733 711 +f 711 734 708 +f 733 734 711 +f 735 708 734 +f 733 613 618 +f 710 709 716 +f 736 713 715 +f 737 716 709 +f 716 737 715 +f 736 715 737 +f 738 736 737 +f 708 735 706 +f 739 706 735 +f 706 739 709 +f 740 709 739 +f 709 740 737 +f 738 737 740 +f 741 738 740 +f 723 730 683 +f 684 683 742 +f 683 730 743 +f 744 742 743 +f 743 745 744 +f 746 744 745 +f 747 746 748 +f 743 742 683 +f 745 748 746 +f 733 749 734 +f 734 750 735 +f 750 734 749 +f 751 749 733 +f 618 751 733 +f 751 618 615 +f 739 752 740 +f 753 741 752 +f 741 740 752 +f 735 754 739 +f 754 735 750 +f 752 739 754 +f 615 755 751 +f 755 615 619 +f 619 756 755 +f 757 754 758 +f 754 757 752 +f 753 752 757 +f 759 753 757 +f 758 750 760 +f 750 758 754 +f 751 761 749 +f 755 761 751 +f 755 762 761 +f 749 760 750 +f 760 749 761 +f 761 763 760 +f 760 764 758 +f 759 757 765 +f 766 759 765 +f 764 760 763 +f 765 758 764 +f 758 765 757 +f 756 619 625 +f 625 767 756 +f 767 625 626 +f 756 762 755 +f 756 768 762 +f 767 768 756 +f 763 761 762 +f 762 769 763 +f 769 762 768 +f 770 764 771 +f 764 770 765 +f 766 765 770 +f 772 766 770 +f 763 771 764 +f 771 763 769 +f 773 774 775 +f 776 773 777 +f 774 627 632 +f 632 775 774 +f 775 777 773 +f 777 778 776 +f 771 779 770 +f 768 780 769 +f 767 781 768 +f 772 770 779 +f 779 771 782 +f 782 769 780 +f 769 782 771 +f 783 628 784 +f 784 785 786 +f 784 628 785 +f 784 773 776 +f 784 786 773 +f 786 774 773 +f 786 785 774 +f 628 774 785 +f 628 627 774 +f 780 768 781 +f 783 781 767 +f 626 783 767 +f 783 626 628 +f 748 778 747 +f 778 777 787 +f 787 747 778 +f 777 775 788 +f 788 787 777 +f 775 632 656 +f 656 788 775 +f 731 789 790 +f 789 731 732 +f 730 791 743 +f 791 731 790 +f 791 730 731 +f 792 793 745 +f 745 743 792 +f 793 794 748 +f 748 745 793 +f 778 748 794 +f 795 743 796 +f 795 792 743 +f 794 776 778 +f 793 797 798 +f 792 795 799 +f 793 798 794 +f 792 797 793 +f 792 799 797 +f 789 800 779 +f 780 791 782 +f 782 790 779 +f 800 772 779 +f 796 743 791 +f 779 790 789 +f 796 791 780 +f 791 790 782 +f 780 781 795 +f 794 784 776 +f 794 798 784 +f 801 781 783 +f 781 801 795 +f 796 780 795 +f 784 801 783 +f 801 784 798 +f 799 802 803 +f 801 798 797 +f 797 803 801 +f 803 802 795 +f 803 797 799 +f 795 801 803 +f 795 802 799 +f 668 665 804 +f 667 805 665 +f 805 667 806 +f 603 668 804 +f 700 806 667 +f 804 665 805 +f 806 807 805 +f 808 805 807 +f 804 805 808 +f 807 806 809 +f 639 804 808 +f 804 639 603 +f 537 548 810 +f 537 810 699 +f 810 811 700 +f 811 810 812 +f 700 699 810 +f 548 812 810 +f 813 812 814 +f 550 814 812 +f 806 700 811 +f 812 813 811 +f 548 550 812 +f 811 809 806 +f 809 811 813 +f 815 809 816 +f 817 807 815 +f 808 807 817 +f 641 808 817 +f 808 641 639 +f 809 815 807 +f 648 817 818 +f 817 648 641 +f 817 815 818 +f 816 819 815 +f 818 815 819 +f 819 816 820 +f 551 821 814 +f 550 551 814 +f 814 822 813 +f 822 814 821 +f 816 813 822 +f 813 816 809 +f 821 823 822 +f 823 821 824 +f 567 824 821 +f 822 820 816 +f 820 822 823 +f 551 567 821 +f 825 698 696 +f 555 556 699 +f 699 698 555 +f 826 555 698 +f 825 826 698 +f 827 828 825 +f 555 826 583 +f 827 825 696 +f 829 819 830 +f 818 819 829 +f 649 818 829 +f 818 649 648 +f 823 831 820 +f 830 820 831 +f 820 830 819 +f 829 830 832 +f 833 831 834 +f 832 830 833 +f 831 833 830 +f 829 652 649 +f 835 824 836 +f 570 578 836 +f 570 836 824 +f 567 570 824 +f 831 823 835 +f 824 835 823 +f 837 836 838 +f 835 834 831 +f 834 835 837 +f 838 839 837 +f 578 838 836 +f 578 585 838 +f 836 837 835 +f 585 840 838 +f 841 833 842 +f 839 838 840 +f 837 843 834 +f 843 837 839 +f 842 834 843 +f 834 842 833 +f 655 832 841 +f 832 655 652 +f 652 829 832 +f 832 833 841 +f 844 845 846 +f 847 655 846 +f 847 654 655 +f 848 846 845 +f 848 847 846 +f 849 845 844 +f 849 848 845 +f 844 846 655 +f 844 655 841 +f 850 849 844 +f 851 852 853 +f 853 854 851 +f 855 856 852 +f 852 851 855 +f 853 852 857 +f 857 854 853 +f 857 852 856 +f 857 858 854 +f 851 850 855 +f 858 857 828 +f 858 851 854 +f 851 859 850 +f 858 859 851 +f 826 840 585 +f 585 583 826 +f 840 825 839 +f 825 840 826 +f 839 828 843 +f 828 839 825 +f 850 844 855 +f 857 841 842 +f 841 857 856 +f 843 857 842 +f 857 843 828 +f 855 841 856 +f 841 855 844 +f 860 861 848 +f 848 849 860 +f 861 862 847 +f 847 848 861 +f 862 645 654 +f 654 847 862 +f 863 860 849 +f 827 864 859 +f 859 858 827 +f 864 863 850 +f 858 828 827 +f 849 850 863 +f 850 859 864 +f 696 684 865 +f 742 865 684 +f 865 827 696 +f 865 866 864 +f 866 867 863 +f 868 642 645 +f 645 862 868 +f 869 870 861 +f 861 860 869 +f 870 868 862 +f 862 861 870 +f 867 869 860 +f 863 864 866 +f 864 827 865 +f 860 863 867 +f 747 787 870 +f 787 788 868 +f 868 870 787 +f 870 869 747 +f 788 656 642 +f 642 868 788 +f 746 747 869 +f 744 746 867 +f 867 866 744 +f 866 865 742 +f 869 867 746 +f 742 744 866 +f 871 872 873 +f 872 871 874 +f 874 875 872 +f 875 874 876 +f 876 877 875 +f 877 876 878 +f 878 879 877 +f 879 878 880 +f 880 881 879 +f 881 880 882 +f 882 883 881 +f 883 882 884 +f 884 885 883 +f 885 884 886 +f 886 887 885 +f 887 886 888 +f 889 890 891 +f 892 891 893 +f 890 889 894 +f 895 893 891 +f 891 890 895 +f 894 896 897 +f 898 899 900 +f 901 871 898 +f 898 871 899 +f 902 903 904 +f 904 905 902 +f 905 904 906 +f 903 902 893 +f 907 876 874 +f 901 874 871 +f 905 908 909 +f 902 909 910 +f 911 878 876 +f 876 907 911 +f 909 902 905 +f 908 905 912 +f 896 894 889 +f 891 913 889 +f 913 891 892 +f 914 889 913 +f 889 914 896 +f 907 915 911 +f 911 915 916 +f 916 896 914 +f 896 916 915 +f 893 910 892 +f 910 893 902 +f 906 912 905 +f 917 918 912 +f 912 918 908 +f 919 909 908 +f 908 920 919 +f 921 919 920 +f 920 922 921 +f 923 880 878 +f 919 921 924 +f 925 926 927 +f 923 926 925 +f 880 923 925 +f 922 920 928 +f 913 929 914 +f 929 913 930 +f 926 914 929 +f 914 926 916 +f 892 930 913 +f 930 892 931 +f 931 892 910 +f 930 932 929 +f 932 930 933 +f 927 929 932 +f 929 927 926 +f 934 931 935 +f 931 933 930 +f 933 931 934 +f 935 910 909 +f 924 935 919 +f 928 936 922 +f 918 928 920 +f 937 936 928 +f 938 937 928 +f 917 938 918 +f 938 928 918 +f 910 935 931 +f 935 924 934 +f 909 919 935 +f 920 908 918 +f 878 911 923 +f 923 916 926 +f 911 916 923 +f 939 898 900 +f 900 940 939 +f 941 899 871 +f 871 873 941 +f 940 900 899 +f 899 941 940 +f 939 942 898 +f 943 944 945 +f 946 943 945 +f 947 946 945 +f 945 948 949 +f 944 948 945 +f 950 949 942 +f 897 951 944 +f 897 946 894 +f 894 947 890 +f 952 951 901 +f 898 952 901 +f 953 895 890 +f 893 895 903 +f 897 915 951 +f 954 904 903 +f 907 951 915 +f 901 951 907 +f 874 901 907 +f 915 897 896 +f 955 903 895 +f 956 906 904 +f 957 912 906 +f 958 957 906 +f 957 917 912 +f 959 958 906 +f 904 954 956 +f 903 955 954 +f 943 897 944 +f 943 946 897 +f 952 898 942 +f 951 952 944 +f 944 960 948 +f 948 960 961 +f 952 942 949 +f 944 952 961 +f 949 961 952 +f 961 949 948 +f 944 961 960 +f 890 947 953 +f 895 953 955 +f 906 956 959 +f 946 947 894 +f 954 955 953 +f 953 956 954 +f 962 953 947 +f 959 956 963 +f 964 956 953 +f 953 962 964 +f 947 965 962 +f 964 963 956 +f 966 967 968 +f 968 967 945 +f 969 968 950 +f 970 969 971 +f 972 968 969 +f 887 970 939 +f 970 973 974 +f 970 887 973 +f 877 879 881 +f 881 883 875 +f 883 885 872 +f 885 887 873 +f 939 873 887 +f 939 941 873 +f 939 940 941 +f 875 877 881 +f 872 875 883 +f 873 872 885 +f 971 939 970 +f 887 888 975 +f 975 973 887 +f 976 974 973 +f 973 975 976 +f 977 970 974 +f 974 976 977 +f 977 969 970 +f 968 972 966 +f 978 979 967 +f 980 965 967 +f 966 978 967 +f 969 981 972 +f 965 947 967 +f 979 980 967 +f 969 977 981 +f 945 967 947 +f 945 950 968 +f 950 971 969 +f 949 950 945 +f 942 939 971 +f 942 971 950 +f 982 983 984 +f 985 986 987 +f 984 985 982 +f 987 982 985 +f 988 927 989 +f 925 927 988 +f 932 989 927 +f 882 925 988 +f 988 989 990 +f 991 921 922 +f 922 992 991 +f 983 991 992 +f 992 984 983 +f 988 884 882 +f 925 882 880 +f 993 924 921 +f 994 993 991 +f 936 995 992 +f 995 996 984 +f 991 983 994 +f 992 922 936 +f 984 992 995 +f 921 991 993 +f 997 998 999 +f 998 997 1000 +f 990 989 1001 +f 1000 997 993 +f 999 1002 1003 +f 1002 999 998 +f 1001 1003 1002 +f 1003 1001 989 +f 989 932 1003 +f 997 934 924 +f 934 999 933 +f 999 934 997 +f 933 1003 932 +f 1003 933 999 +f 1004 995 936 +f 937 1004 936 +f 1005 996 995 +f 1004 1005 995 +f 924 993 997 +f 993 994 1000 +f 996 1006 985 +f 1006 1007 986 +f 1005 1008 996 +f 1009 1010 982 +f 1010 994 983 +f 1011 1001 1012 +f 1012 1002 1013 +f 1002 1012 1001 +f 998 1013 1002 +f 990 1001 1011 +f 1014 1000 1015 +f 1013 998 1014 +f 1015 1000 994 +f 1000 1014 998 +f 977 975 888 +f 977 976 975 +f 888 1011 1016 +f 977 888 1016 +f 1010 1009 1017 +f 1017 1015 1010 +f 1015 1018 1014 +f 1018 1015 1017 +f 1019 1014 1018 +f 994 1010 1015 +f 886 990 1011 +f 990 886 884 +f 884 988 990 +f 985 984 996 +f 986 985 1006 +f 982 987 1009 +f 983 982 1010 +f 1011 888 886 +f 962 1009 987 +f 1008 1006 996 +f 1020 986 1007 +f 1021 1007 1006 +f 1008 1021 1006 +f 1022 987 986 +f 1011 1012 1016 +f 1023 1013 1019 +f 1013 1023 1012 +f 1014 1019 1013 +f 1016 1012 1023 +f 964 1007 1021 +f 986 1020 1022 +f 1017 980 1018 +f 965 1017 1009 +f 987 1022 962 +f 1018 979 1019 +f 1019 978 1023 +f 978 1016 1023 +f 1016 981 977 +f 1016 978 1024 +f 979 1018 980 +f 978 1019 979 +f 981 1016 1024 +f 1021 963 964 +f 1009 962 965 +f 1022 1020 964 +f 1007 964 1020 +f 980 1017 965 +f 964 962 1022 +f 1025 972 981 +f 981 1024 1025 +f 1025 1024 978 +f 978 1026 1025 +f 1026 966 972 +f 966 1026 978 +f 972 1025 1026 +f 1027 1028 1029 +f 1030 1031 1032 +f 1032 1029 1030 +f 1033 1029 1028 +f 1034 1030 1029 +f 1029 1033 1034 +f 1029 1032 1027 +f 1027 1035 1036 +f 1032 1037 1035 +f 1035 1027 1032 +f 1031 1038 1037 +f 1037 1032 1031 +f 1028 1027 1039 +f 1040 1034 1033 +f 1033 1041 1040 +f 1042 1041 1043 +f 1044 1040 1041 +f 1028 1045 1033 +f 1043 1045 1046 +f 1047 1043 1048 +f 1041 1033 1045 +f 1045 1043 1041 +f 1045 1028 1049 +f 1049 1046 1045 +f 1036 1039 1027 +f 1039 1049 1028 +f 1039 1036 1050 +f 1046 1048 1043 +f 1051 1048 1050 +f 1049 1039 1050 +f 1046 1049 1050 +f 1048 1046 1050 +f 1043 1047 1042 +f 1041 1042 1044 +f 1052 1047 1051 +f 1053 1042 1047 +f 1047 1052 1053 +f 1054 1044 1042 +f 1042 1053 1054 +f 1055 1053 1052 +f 1052 1056 1055 +f 1057 1054 1053 +f 1053 1055 1057 +f 1058 1055 1056 +f 1059 1057 1055 +f 1055 1058 1059 +f 1060 1056 1061 +f 1062 1060 1063 +f 1056 1060 1058 +f 1064 1058 1060 +f 1060 1062 1064 +f 1065 1059 1058 +f 1058 1064 1065 +f 1051 1066 1052 +f 1056 1052 1066 +f 1066 1061 1056 +f 1048 1051 1047 +f 1061 1063 1060 +f 1066 1051 1050 +f 1061 1066 1050 +f 1063 1061 1050 +f 1067 1063 1050 +f 1063 1067 1062 +f 1068 1069 1070 +f 1070 1071 1068 +f 1071 1072 1073 +f 1073 1068 1071 +f 1038 1073 1072 +f 1072 1037 1038 +f 1072 1071 1074 +f 1074 1075 1072 +f 1075 1035 1037 +f 1037 1072 1075 +f 1069 1076 1077 +f 1077 1070 1069 +f 1077 1078 1079 +f 1076 1080 1078 +f 1078 1077 1076 +f 1070 1077 1081 +f 1081 1082 1070 +f 1082 1074 1071 +f 1071 1070 1082 +f 1079 1081 1077 +f 1083 1084 1075 +f 1075 1074 1083 +f 1074 1082 1085 +f 1085 1083 1074 +f 1084 1036 1035 +f 1035 1075 1084 +f 1082 1081 1086 +f 1086 1085 1082 +f 1081 1079 1087 +f 1087 1086 1081 +f 1083 1085 1088 +f 1088 1089 1083 +f 1090 1050 1036 +f 1036 1084 1090 +f 1089 1090 1084 +f 1084 1083 1089 +f 1086 1087 1091 +f 1091 1092 1086 +f 1085 1086 1092 +f 1092 1088 1085 +f 1093 1094 1095 +f 1096 1097 1031 +f 1098 1096 1094 +f 1094 1096 1030 +f 1099 1097 1096 +f 1095 1094 1034 +f 1100 1097 1099 +f 1101 1095 1040 +f 1038 1031 1097 +f 1031 1030 1096 +f 1030 1034 1094 +f 1034 1040 1095 +f 1097 1100 1038 +f 1100 1102 1073 +f 1068 1103 1104 +f 1102 1103 1068 +f 1105 1080 1076 +f 1073 1038 1100 +f 1104 1069 1068 +f 1068 1073 1102 +f 1069 1104 1106 +f 1106 1076 1069 +f 1076 1106 1105 +f 1107 1108 1090 +f 1090 1089 1107 +f 1089 1088 1109 +f 1109 1107 1089 +f 1108 1067 1050 +f 1050 1090 1108 +f 1092 1091 1110 +f 1110 1111 1092 +f 1088 1092 1111 +f 1111 1109 1088 +f 1112 1113 1108 +f 1108 1107 1112 +f 1113 1062 1067 +f 1067 1108 1113 +f 1107 1109 1114 +f 1114 1112 1107 +f 1111 1110 1115 +f 1115 1116 1111 +f 1109 1111 1116 +f 1116 1114 1109 +f 1062 1113 1117 +f 1064 1117 1118 +f 1119 1117 1113 +f 1117 1064 1062 +f 1120 1118 1117 +f 1118 1065 1064 +f 1119 1121 1122 +f 1122 1120 1119 +f 1117 1119 1120 +f 1113 1112 1119 +f 1116 1115 1123 +f 1112 1114 1121 +f 1121 1119 1112 +f 1114 1116 1124 +f 1124 1121 1114 +f 1123 1124 1116 +f 1121 1124 1125 +f 1125 1122 1121 +f 1126 1125 1124 +f 1124 1123 1126 +f 1104 1103 1127 +f 1103 1102 1128 +f 1102 1100 1129 +f 1130 1105 1106 +f 1106 1104 1131 +f 1040 1044 1101 +f 1044 1054 1093 +f 1054 1057 1098 +f 1057 1059 1099 +f 1093 1101 1044 +f 1098 1094 1093 +f 1098 1093 1054 +f 1099 1096 1098 +f 1099 1098 1057 +f 1093 1095 1101 +f 1129 1099 1059 +f 1125 1132 1131 +f 1131 1122 1125 +f 1126 1130 1132 +f 1132 1125 1126 +f 1118 1120 1127 +f 1065 1118 1128 +f 1120 1122 1131 +f 1059 1065 1129 +f 1128 1127 1103 +f 1129 1128 1102 +f 1100 1099 1129 +f 1131 1132 1106 +f 1127 1131 1104 +f 1106 1132 1130 +f 1128 1129 1065 +f 1127 1128 1118 +f 1131 1127 1120 +f 1133 1134 1135 +f 1134 1133 1136 +f 1136 1137 1134 +f 1137 1136 1138 +f 1138 1139 1137 +f 1139 1138 1140 +f 1140 1141 1139 +f 1142 1136 1133 +f 1143 1144 1145 +f 1144 1143 1140 +f 1140 1146 1144 +f 1146 1140 1138 +f 1138 1147 1146 +f 1147 1138 1136 +f 1136 1142 1147 +f 1141 1140 1143 +f 1148 1149 1143 +f 1143 1150 1141 +f 1150 1143 1149 +f 1149 1151 1150 +f 1151 1149 1152 +f 1152 1153 1151 +f 1153 1152 1154 +f 1154 1155 1153 +f 1156 1157 1154 +f 1154 1158 1156 +f 1158 1154 1152 +f 1152 1159 1158 +f 1159 1152 1149 +f 1149 1148 1159 +f 1157 1156 1160 +f 1155 1154 1157 +f 1161 1162 1163 +f 1162 1161 1164 +f 1164 1165 1162 +f 1165 1164 1166 +f 1166 1167 1165 +f 1167 1166 1168 +f 1168 1145 1167 +f 1169 1164 1161 +f 1143 1170 1171 +f 1170 1143 1168 +f 1168 1172 1170 +f 1172 1168 1166 +f 1166 1173 1172 +f 1173 1166 1164 +f 1164 1169 1173 +f 1145 1168 1143 +f 1171 1174 1143 +f 1143 1175 1148 +f 1175 1143 1174 +f 1174 1176 1175 +f 1176 1174 1177 +f 1177 1178 1176 +f 1178 1177 1179 +f 1179 1180 1178 +f 1181 1182 1179 +f 1179 1183 1181 +f 1183 1179 1177 +f 1177 1184 1183 +f 1184 1177 1174 +f 1174 1171 1184 +f 1182 1181 1185 +f 1180 1179 1182 +f 1186 1187 1135 +f 1186 1188 1187 +f 1189 1190 1191 +f 1192 1193 1189 +f 1194 1193 1192 +f 1186 1135 1192 +f 1135 1194 1192 +f 1194 1135 1134 +f 1188 1195 1196 +f 1196 1187 1188 +f 1186 1197 1195 +f 1195 1188 1186 +f 1187 1196 1133 +f 1133 1135 1187 +f 1186 1198 1197 +f 1199 1200 1201 +f 1201 1202 1199 +f 1198 1203 1200 +f 1200 1199 1198 +f 1204 1202 1205 +f 1201 1205 1202 +f 1198 1186 1203 +f 1205 1192 1189 +f 1191 1205 1189 +f 1206 1207 1191 +f 1208 1209 1206 +f 1210 1208 1080 +f 1192 1203 1186 +f 1205 1191 1207 +f 1203 1192 1211 +f 1192 1205 1211 +f 1205 1212 1211 +f 1205 1201 1213 +f 1213 1212 1205 +f 1200 1212 1213 +f 1213 1201 1200 +f 1203 1211 1212 +f 1212 1200 1203 +f 1214 1215 1216 +f 1217 1216 1218 +f 1219 1220 1221 +f 1219 1142 1220 +f 1217 1142 1219 +f 1147 1217 1218 +f 1217 1147 1142 +f 1222 1199 1202 +f 1223 1197 1198 +f 1223 1198 1199 +f 1202 1204 1224 +f 1225 1224 1204 +f 1214 1216 1225 +f 1214 1226 1227 +f 1228 1229 1230 +f 1227 1231 1230 +f 1232 1216 1217 +f 1219 1232 1217 +f 1226 1231 1227 +f 1233 1226 1214 +f 1233 1214 1225 +f 1216 1232 1225 +f 1230 1231 1228 +f 1232 1219 1223 +f 1142 1133 1196 +f 1197 1223 1219 +f 1195 1221 1220 +f 1220 1196 1195 +f 1197 1219 1221 +f 1221 1195 1197 +f 1196 1220 1142 +f 1222 1234 1232 +f 1225 1235 1224 +f 1202 1224 1222 +f 1199 1222 1223 +f 1234 1222 1224 +f 1224 1235 1234 +f 1232 1223 1222 +f 1218 1215 1236 +f 1218 1216 1215 +f 1237 1238 1215 +f 1146 1218 1236 +f 1218 1146 1147 +f 1239 1240 1230 +f 1230 1241 1227 +f 1227 1237 1214 +f 1229 1239 1230 +f 1215 1214 1237 +f 1237 1227 1241 +f 1241 1230 1240 +f 1242 1238 1243 +f 1236 1238 1242 +f 1236 1215 1238 +f 1243 1244 1245 +f 1238 1237 1244 +f 1144 1236 1242 +f 1236 1144 1146 +f 1244 1243 1238 +f 1246 1243 1247 +f 1247 1245 1248 +f 1245 1247 1243 +f 1242 1243 1246 +f 1145 1242 1246 +f 1242 1145 1144 +f 1249 1250 1251 +f 1249 1251 1240 +f 1239 1249 1240 +f 1244 1241 1252 +f 1241 1244 1237 +f 1240 1252 1241 +f 1252 1240 1251 +f 1251 1253 1252 +f 1252 1245 1244 +f 1245 1252 1253 +f 1253 1251 1254 +f 1250 1254 1251 +f 1255 1254 1256 +f 1257 1256 1254 +f 1250 1257 1254 +f 1253 1248 1245 +f 1248 1253 1255 +f 1254 1255 1253 +f 1225 1232 1234 +f 1258 1228 1231 +f 1234 1235 1225 +f 1226 1233 1259 +f 1231 1226 1260 +f 1204 1261 1225 +f 1233 1225 1261 +f 1193 1262 1190 +f 1194 1263 1193 +f 1264 1263 1194 +f 1134 1264 1194 +f 1264 1134 1137 +f 1265 1191 1190 +f 1191 1265 1206 +f 1266 1206 1265 +f 1206 1266 1208 +f 1080 1208 1266 +f 1190 1189 1193 +f 1078 1080 1266 +f 1267 1265 1268 +f 1265 1267 1266 +f 1078 1266 1267 +f 1268 1190 1262 +f 1190 1268 1265 +f 1264 1269 1263 +f 1270 1269 1264 +f 1262 1193 1263 +f 1263 1271 1262 +f 1271 1263 1269 +f 1137 1270 1264 +f 1270 1137 1139 +f 1272 1271 1273 +f 1270 1274 1269 +f 1269 1273 1271 +f 1275 1274 1270 +f 1273 1269 1274 +f 1139 1275 1270 +f 1275 1139 1141 +f 1268 1276 1267 +f 1079 1267 1276 +f 1262 1277 1268 +f 1079 1078 1267 +f 1277 1278 1276 +f 1278 1277 1272 +f 1087 1079 1276 +f 1087 1276 1278 +f 1277 1262 1271 +f 1091 1087 1278 +f 1276 1268 1277 +f 1271 1272 1277 +f 1259 1260 1226 +f 1260 1259 1210 +f 1279 1260 1280 +f 1261 1204 1207 +f 1260 1279 1231 +f 1261 1259 1233 +f 1259 1261 1209 +f 1209 1208 1210 +f 1130 1210 1105 +f 1207 1206 1209 +f 1080 1105 1210 +f 1209 1210 1259 +f 1210 1281 1260 +f 1205 1207 1204 +f 1207 1209 1261 +f 1274 1282 1273 +f 1282 1274 1283 +f 1275 1283 1274 +f 1284 1283 1275 +f 1141 1284 1275 +f 1284 1141 1150 +f 1283 1285 1282 +f 1285 1283 1286 +f 1286 1287 1285 +f 1284 1286 1283 +f 1288 1286 1284 +f 1288 1289 1286 +f 1150 1288 1284 +f 1288 1150 1151 +f 1273 1290 1272 +f 1291 1272 1290 +f 1272 1291 1278 +f 1091 1278 1291 +f 1110 1091 1291 +f 1290 1273 1282 +f 1290 1292 1291 +f 1110 1291 1292 +f 1282 1293 1290 +f 1115 1110 1292 +f 1115 1292 1294 +f 1123 1115 1294 +f 1292 1290 1293 +f 1295 1285 1287 +f 1293 1282 1285 +f 1293 1294 1292 +f 1285 1295 1293 +f 1294 1293 1295 +f 1287 1286 1289 +f 1296 1289 1288 +f 1296 1297 1289 +f 1151 1296 1288 +f 1296 1151 1153 +f 1287 1298 1295 +f 1295 1299 1294 +f 1123 1294 1299 +f 1289 1300 1287 +f 1126 1123 1299 +f 1299 1295 1298 +f 1298 1287 1300 +f 1210 1130 1281 +f 1301 1302 1303 +f 1303 1304 1301 +f 1305 1306 1307 +f 1305 1308 1309 +f 1280 1260 1281 +f 1308 1305 1303 +f 1306 1305 1310 +f 1306 1280 1311 +f 1280 1306 1312 +f 1313 1297 1296 +f 1314 1315 1316 +f 1314 1155 1315 +f 1313 1155 1314 +f 1300 1289 1297 +f 1153 1313 1296 +f 1313 1153 1155 +f 1317 1318 1319 +f 1307 1303 1305 +f 1281 1311 1280 +f 1311 1307 1306 +f 1320 1308 1321 +f 1322 1319 1320 +f 1317 1323 1318 +f 1303 1321 1308 +f 1299 1281 1130 +f 1307 1300 1303 +f 1307 1311 1300 +f 1297 1304 1303 +f 1311 1281 1298 +f 1304 1314 1317 +f 1298 1281 1299 +f 1130 1126 1299 +f 1300 1297 1303 +f 1300 1311 1298 +f 1304 1297 1313 +f 1314 1304 1313 +f 1324 1315 1155 +f 1155 1157 1324 +f 1323 1317 1314 +f 1323 1314 1316 +f 1325 1316 1315 +f 1315 1324 1325 +f 1316 1325 1323 +f 1319 1322 1317 +f 1303 1302 1321 +f 1301 1322 1321 +f 1322 1301 1304 +f 1321 1302 1301 +f 1320 1321 1322 +f 1304 1317 1322 +f 1326 1324 1157 +f 1157 1160 1326 +f 1327 1325 1324 +f 1324 1326 1327 +f 1328 1323 1325 +f 1325 1327 1328 +f 1329 1330 1331 +f 1332 1330 1329 +f 1328 1326 1160 +f 1328 1327 1326 +f 1331 1333 1334 +f 1328 1160 1329 +f 1160 1332 1329 +f 1332 1160 1156 +f 1318 1335 1336 +f 1336 1319 1318 +f 1337 1320 1319 +f 1308 1320 1338 +f 1337 1338 1320 +f 1319 1336 1337 +f 1318 1328 1335 +f 1328 1318 1323 +f 1339 1309 1334 +f 1309 1339 1310 +f 1335 1329 1340 +f 1338 1334 1309 +f 1329 1338 1340 +f 1341 1342 1312 +f 1310 1343 1312 +f 1338 1329 1331 +f 1343 1310 1339 +f 1329 1335 1328 +f 1312 1343 1341 +f 1334 1338 1331 +f 1332 1344 1330 +f 1345 1344 1332 +f 1156 1345 1332 +f 1345 1156 1158 +f 1330 1346 1333 +f 1334 1347 1339 +f 1348 1339 1347 +f 1339 1348 1343 +f 1341 1343 1348 +f 1349 1341 1348 +f 1333 1331 1330 +f 1347 1334 1333 +f 1350 1333 1346 +f 1333 1350 1347 +f 1349 1348 1351 +f 1351 1347 1350 +f 1347 1351 1348 +f 1345 1352 1344 +f 1353 1352 1345 +f 1158 1353 1345 +f 1353 1158 1159 +f 1346 1330 1344 +f 1344 1354 1346 +f 1354 1344 1352 +f 1159 1355 1353 +f 1355 1159 1148 +f 1355 1356 1353 +f 1357 1354 1358 +f 1353 1356 1352 +f 1352 1358 1354 +f 1358 1352 1356 +f 1359 1351 1360 +f 1350 1360 1351 +f 1359 1349 1351 +f 1346 1361 1350 +f 1362 1359 1360 +f 1362 1360 1363 +f 1361 1346 1354 +f 1364 1362 1363 +f 1354 1357 1361 +f 1363 1361 1357 +f 1361 1363 1360 +f 1360 1350 1361 +f 1310 1312 1306 +f 1312 1365 1280 +f 1309 1310 1305 +f 1338 1309 1308 +f 1335 1340 1366 +f 1366 1336 1335 +f 1336 1366 1367 +f 1367 1337 1336 +f 1312 1342 1365 +f 1337 1367 1338 +f 1366 1340 1338 +f 1338 1367 1366 +f 1368 1248 1369 +f 1248 1368 1247 +f 1255 1369 1248 +f 1370 1247 1368 +f 1246 1247 1370 +f 1167 1246 1370 +f 1246 1167 1145 +f 1370 1368 1371 +f 1372 1373 1374 +f 1374 1369 1372 +f 1165 1370 1371 +f 1370 1165 1167 +f 1369 1374 1368 +f 1371 1368 1374 +f 1375 1376 1377 +f 1375 1377 1256 +f 1257 1375 1256 +f 1378 1372 1369 +f 1256 1378 1255 +f 1369 1255 1378 +f 1378 1256 1377 +f 1377 1379 1378 +f 1372 1378 1379 +f 1379 1377 1380 +f 1376 1381 1380 +f 1376 1380 1377 +f 1382 1374 1373 +f 1371 1374 1382 +f 1162 1371 1382 +f 1371 1162 1165 +f 1373 1372 1383 +f 1380 1384 1379 +f 1384 1380 1385 +f 1381 1385 1380 +f 1383 1379 1384 +f 1379 1383 1372 +f 1386 1384 1387 +f 1381 1388 1385 +f 1387 1385 1389 +f 1388 1389 1385 +f 1384 1386 1383 +f 1383 1390 1373 +f 1385 1387 1384 +f 1391 1389 1258 +f 1392 1387 1391 +f 1228 1258 1393 +f 1388 1393 1258 +f 1391 1258 1394 +f 1395 1392 1396 +f 1392 1391 1397 +f 1258 1231 1279 +f 1163 1382 1398 +f 1382 1163 1162 +f 1398 1373 1390 +f 1382 1373 1398 +f 1399 1400 1163 +f 1390 1383 1386 +f 1399 1401 1400 +f 1399 1163 1398 +f 1402 1401 1399 +f 1400 1403 1161 +f 1161 1163 1400 +f 1401 1402 1403 +f 1403 1400 1401 +f 1399 1404 1402 +f 1405 1406 1407 +f 1407 1408 1405 +f 1409 1410 1406 +f 1406 1405 1409 +f 1396 1408 1395 +f 1407 1395 1408 +f 1399 1409 1404 +f 1409 1399 1410 +f 1389 1391 1387 +f 1258 1389 1388 +f 1386 1395 1390 +f 1387 1392 1386 +f 1395 1398 1390 +f 1398 1410 1399 +f 1406 1411 1412 +f 1412 1407 1406 +f 1410 1413 1411 +f 1411 1406 1410 +f 1395 1411 1413 +f 1395 1407 1412 +f 1412 1411 1395 +f 1398 1395 1413 +f 1410 1398 1413 +f 1395 1386 1392 +f 1414 1415 1416 +f 1417 1418 1419 +f 1420 1169 1417 +f 1173 1420 1421 +f 1420 1173 1169 +f 1420 1416 1421 +f 1417 1169 1418 +f 1422 1423 1279 +f 1424 1425 1396 +f 1423 1426 1394 +f 1427 1405 1408 +f 1428 1409 1405 +f 1428 1404 1409 +f 1408 1396 1425 +f 1426 1424 1397 +f 1403 1418 1169 +f 1169 1161 1403 +f 1404 1428 1417 +f 1404 1417 1419 +f 1419 1402 1404 +f 1402 1419 1418 +f 1418 1403 1402 +f 1427 1429 1430 +f 1408 1425 1427 +f 1430 1428 1427 +f 1425 1431 1429 +f 1424 1431 1425 +f 1405 1427 1428 +f 1424 1430 1429 +f 1429 1427 1425 +f 1423 1422 1432 +f 1430 1417 1428 +f 1416 1430 1424 +f 1433 1422 1434 +f 1426 1414 1424 +f 1426 1423 1414 +f 1414 1416 1424 +f 1434 1435 1433 +f 1432 1422 1433 +f 1414 1423 1432 +f 1417 1430 1420 +f 1430 1416 1420 +f 1421 1172 1173 +f 1421 1415 1436 +f 1421 1416 1415 +f 1172 1421 1436 +f 1437 1438 1415 +f 1432 1437 1414 +f 1415 1414 1437 +f 1433 1439 1432 +f 1437 1432 1439 +f 1440 1441 1433 +f 1435 1440 1433 +f 1439 1433 1441 +f 1436 1170 1172 +f 1438 1437 1442 +f 1443 1438 1444 +f 1436 1438 1443 +f 1436 1415 1438 +f 1170 1436 1443 +f 1444 1442 1445 +f 1442 1444 1438 +f 1446 1444 1447 +f 1171 1443 1446 +f 1443 1171 1170 +f 1447 1445 1448 +f 1445 1447 1444 +f 1443 1444 1446 +f 1449 1445 1442 +f 1439 1442 1437 +f 1441 1449 1439 +f 1449 1441 1450 +f 1442 1439 1449 +f 1450 1451 1449 +f 1452 1450 1441 +f 1452 1453 1450 +f 1440 1452 1441 +f 1445 1449 1451 +f 1451 1450 1454 +f 1453 1454 1450 +f 1448 1451 1455 +f 1455 1454 1456 +f 1457 1456 1454 +f 1453 1457 1454 +f 1451 1448 1445 +f 1454 1455 1451 +f 1394 1279 1423 +f 1397 1394 1426 +f 1397 1396 1392 +f 1394 1397 1391 +f 1279 1394 1258 +f 1279 1458 1422 +f 1396 1397 1424 +f 1434 1422 1459 +f 1424 1429 1431 +f 1184 1446 1460 +f 1446 1184 1171 +f 1460 1447 1461 +f 1446 1447 1460 +f 1461 1448 1462 +f 1448 1461 1447 +f 1455 1462 1448 +f 1463 1462 1464 +f 1462 1463 1461 +f 1183 1460 1465 +f 1460 1183 1184 +f 1464 1466 1463 +f 1465 1461 1463 +f 1460 1461 1465 +f 1457 1467 1456 +f 1468 1464 1462 +f 1456 1468 1455 +f 1467 1469 1470 +f 1468 1456 1470 +f 1462 1455 1468 +f 1470 1471 1468 +f 1467 1470 1456 +f 1469 1472 1473 +f 1469 1473 1470 +f 1471 1470 1473 +f 1464 1468 1471 +f 1466 1464 1474 +f 1465 1181 1183 +f 1475 1463 1466 +f 1465 1463 1475 +f 1181 1465 1475 +f 1473 1476 1471 +f 1471 1474 1464 +f 1476 1473 1477 +f 1474 1471 1476 +f 1472 1477 1473 +f 1478 1479 1477 +f 1472 1478 1477 +f 1480 1477 1479 +f 1477 1480 1476 +f 1481 1476 1480 +f 1474 1482 1466 +f 1476 1481 1474 +f 1483 1484 1485 +f 1486 1483 1487 +f 1458 1486 1488 +f 1486 1458 1489 +f 1483 1486 1490 +f 1458 1280 1365 +f 1279 1280 1458 +f 1491 1485 1484 +f 1487 1488 1486 +f 1485 1487 1483 +f 1488 1422 1458 +f 1491 1492 1493 +f 1494 1495 1493 +f 1493 1496 1494 +f 1488 1459 1422 +f 1493 1495 1491 +f 1497 1492 1491 +f 1496 1493 1492 +f 1492 1497 1496 +f 1498 1365 1342 +f 1499 1500 1501 +f 1355 1502 1356 +f 1503 1502 1355 +f 1356 1504 1358 +f 1504 1356 1502 +f 1148 1503 1355 +f 1503 1148 1175 +f 1503 1505 1502 +f 1506 1505 1503 +f 1502 1507 1504 +f 1506 1508 1505 +f 1175 1506 1503 +f 1506 1175 1176 +f 1507 1502 1505 +f 1505 1509 1507 +f 1510 1357 1511 +f 1357 1510 1363 +f 1364 1363 1510 +f 1512 1364 1510 +f 1511 1358 1504 +f 1358 1511 1357 +f 1512 1510 1513 +f 1504 1514 1511 +f 1511 1513 1510 +f 1515 1514 1516 +f 1514 1515 1513 +f 1513 1511 1514 +f 1514 1504 1507 +f 1507 1516 1514 +f 1516 1507 1509 +f 1517 1512 1513 +f 1517 1513 1515 +f 1518 1517 1515 +f 1519 1508 1506 +f 1519 1520 1508 +f 1176 1519 1506 +f 1519 1176 1178 +f 1509 1505 1508 +f 1518 1515 1521 +f 1522 1518 1521 +f 1516 1521 1515 +f 1508 1523 1509 +f 1521 1516 1524 +f 1524 1509 1523 +f 1509 1524 1516 +f 1525 1180 1526 +f 1178 1525 1519 +f 1525 1178 1180 +f 1523 1508 1520 +f 1525 1520 1519 +f 1526 1527 1528 +f 1526 1180 1527 +f 1365 1489 1458 +f 1484 1483 1499 +f 1489 1490 1486 +f 1490 1499 1483 +f 1529 1530 1531 +f 1499 1532 1484 +f 1531 1484 1532 +f 1533 1534 1535 +f 1533 1535 1530 +f 1527 1536 1537 +f 1534 1526 1528 +f 1528 1537 1534 +f 1536 1527 1180 +f 1180 1182 1536 +f 1534 1533 1526 +f 1537 1528 1527 +f 1531 1532 1529 +f 1500 1529 1532 +f 1532 1501 1500 +f 1499 1501 1532 +f 1538 1533 1529 +f 1530 1529 1533 +f 1529 1500 1538 +f 1499 1538 1500 +f 1526 1538 1525 +f 1523 1520 1499 +f 1523 1489 1524 +f 1498 1522 1521 +f 1524 1365 1521 +f 1538 1520 1525 +f 1490 1523 1499 +f 1490 1489 1523 +f 1489 1365 1524 +f 1538 1526 1533 +f 1520 1538 1499 +f 1521 1365 1498 +f 1537 1539 1540 +f 1541 1536 1182 +f 1539 1537 1536 +f 1182 1185 1541 +f 1540 1534 1537 +f 1536 1541 1539 +f 1540 1539 1541 +f 1475 1466 1542 +f 1482 1474 1481 +f 1540 1185 1542 +f 1542 1466 1482 +f 1185 1475 1542 +f 1475 1185 1181 +f 1540 1541 1185 +f 1530 1496 1497 +f 1497 1531 1530 +f 1535 1494 1496 +f 1496 1530 1535 +f 1497 1491 1531 +f 1484 1531 1491 +f 1485 1480 1487 +f 1535 1540 1494 +f 1540 1535 1534 +f 1480 1485 1481 +f 1487 1479 1488 +f 1542 1491 1495 +f 1494 1542 1495 +f 1478 1459 1488 +f 1491 1481 1485 +f 1479 1487 1480 +f 1481 1491 1482 +f 1488 1479 1478 +f 1491 1542 1482 +f 1542 1494 1540 +f 1543 1544 1545 +f 1546 1545 1547 +f 1548 1549 1550 +f 1551 1550 1552 +f 1553 1552 1554 +f 1555 1554 1549 +f 1546 1543 1545 +f 1556 1551 1552 +f 1553 1556 1552 +f 1543 1557 1544 +f 1555 1553 1554 +f 1548 1555 1549 +f 1558 1548 1550 +f 1551 1558 1550 +f 1559 1560 1561 +f 1562 1559 1561 +f 1563 1546 1547 +f 1564 1562 1565 +f 1566 1564 1565 +f 1560 1567 1568 +f 1557 1569 1544 +f 1567 1566 1568 +f 1563 1547 1570 +f 1569 1570 1544 +f 1560 1568 1561 +f 1562 1561 1565 +f 1566 1565 1571 +f 1566 1571 1568 +f 1572 1573 1574 +f 1575 1572 1574 +f 1576 1575 1577 +f 1578 1576 1577 +f 1573 1579 1580 +f 1579 1578 1580 +f 1581 1582 1583 +f 1584 1585 1586 +f 1582 1587 1583 +f 1587 1584 1588 +f 1589 1581 1590 +f 1587 1588 1583 +f 1581 1583 1590 +f 1589 1590 1586 +f 1584 1586 1588 +f 1573 1580 1574 +f 1575 1574 1577 +f 1591 1592 1593 +f 1594 1593 1595 +f 1596 1595 1597 +f 1598 1597 1592 +f 1578 1577 1599 +f 1578 1599 1580 +f 1594 1600 1593 +f 1591 1598 1592 +f 1598 1601 1597 +f 1601 1596 1597 +f 1596 1594 1595 +f 1602 1603 1604 +f 1548 1558 1605 +f 1558 1551 1605 +f 1555 1548 1606 +f 1605 1551 1607 +f 1608 1609 1555 +f 1609 1608 1610 +f 1610 1611 1607 +f 1612 1610 1609 +f 1612 1607 1610 +f 1613 1610 1608 +f 1611 1610 1613 +f 1614 1606 1548 +f 1605 1614 1548 +f 1555 1606 1608 +f 1607 1611 1605 +f 1615 1616 1617 +f 1615 1618 1616 +f 1615 1619 1618 +f 1615 1617 1619 +f 1556 1553 1543 +f 1557 1555 1609 +f 1551 1556 1546 +f 1553 1555 1557 +f 1620 1617 1609 +f 1607 1616 1620 +f 1618 1607 1551 +f 1612 1620 1607 +f 1612 1609 1620 +f 1557 1543 1553 +f 1609 1617 1557 +f 1616 1607 1618 +f 1543 1546 1556 +f 1617 1620 1616 +f 1551 1546 1618 +f 1621 1622 1623 +f 1621 1624 1622 +f 1621 1625 1624 +f 1621 1623 1625 +f 1569 1557 1567 +f 1624 1618 1546 +f 1619 1622 1617 +f 1618 1624 1619 +f 1567 1557 1617 +f 1563 1569 1560 +f 1546 1563 1559 +f 1617 1623 1567 +f 1623 1617 1622 +f 1567 1560 1569 +f 1560 1559 1563 +f 1546 1562 1624 +f 1559 1562 1546 +f 1622 1619 1624 +f 1566 1567 1584 +f 1564 1566 1587 +f 1624 1626 1625 +f 1562 1564 1582 +f 1625 1627 1623 +f 1584 1567 1623 +f 1628 1624 1562 +f 1623 1627 1584 +f 1582 1581 1562 +f 1562 1581 1628 +f 1626 1624 1628 +f 1584 1587 1566 +f 1627 1625 1626 +f 1587 1582 1564 +f 1629 1630 1628 +f 1629 1627 1630 +f 1629 1628 1626 +f 1629 1626 1627 +f 1631 1632 1633 +f 1631 1634 1632 +f 1631 1635 1634 +f 1631 1633 1635 +f 1585 1584 1579 +f 1589 1585 1573 +f 1581 1589 1572 +f 1632 1628 1581 +f 1630 1633 1627 +f 1628 1632 1630 +f 1579 1584 1627 +f 1581 1575 1632 +f 1573 1572 1589 +f 1579 1573 1585 +f 1633 1630 1632 +f 1572 1575 1581 +f 1627 1635 1579 +f 1635 1627 1633 +f 1575 1598 1636 +f 1596 1601 1576 +f 1601 1598 1575 +f 1594 1596 1578 +f 1636 1632 1575 +f 1578 1579 1594 +f 1634 1637 1635 +f 1632 1638 1634 +f 1576 1578 1596 +f 1575 1576 1601 +f 1594 1579 1635 +f 1638 1632 1636 +f 1639 1638 1637 +f 1639 1636 1638 +f 1637 1634 1638 +f 1635 1637 1594 +f 1640 1636 1598 +f 1641 1642 1637 +f 1636 1640 1641 +f 1639 1637 1641 +f 1639 1641 1636 +f 1643 1594 1637 +f 1600 1594 1643 +f 1591 1600 1603 +f 1598 1591 1602 +f 1643 1603 1600 +f 1642 1641 1640 +f 1603 1602 1591 +f 1637 1644 1643 +f 1602 1645 1598 +f 1598 1645 1640 +f 1644 1637 1642 +f 1646 1647 1648 +f 1643 1646 1648 +f 1603 1643 1649 +f 1645 1602 1604 +f 1647 1645 1650 +f 1651 1652 1653 +f 1654 1653 1655 +f 1656 1655 1657 +f 1658 1657 1652 +f 1603 1649 1604 +f 1645 1604 1650 +f 1647 1650 1648 +f 1643 1648 1649 +f 1659 1651 1653 +f 1651 1658 1652 +f 1658 1660 1657 +f 1656 1654 1655 +f 1654 1659 1653 +f 1661 1662 1663 +f 1664 1661 1663 +f 1665 1664 1666 +f 1667 1665 1666 +f 1668 1667 1669 +f 1670 1671 1672 +f 1673 1670 1674 +f 1675 1673 1674 +f 1676 1675 1677 +f 1678 1676 1679 +f 1671 1678 1679 +f 1664 1663 1666 +f 1667 1666 1669 +f 1670 1672 1674 +f 1675 1674 1677 +f 1676 1677 1679 +f 1671 1679 1672 +f 1662 1680 1663 +f 1668 1669 1680 +f 1681 1682 1683 +f 1684 1683 1685 +f 1686 1685 1687 +f 1688 1687 1682 +f 1684 1689 1683 +f 1690 1684 1685 +f 1686 1690 1685 +f 1688 1686 1687 +f 1681 1688 1682 +f 1689 1681 1683 +f 1691 1692 1693 +f 1692 1694 1695 +f 1696 1691 1693 +f 1692 1695 1693 +f 1696 1693 1697 +f 1698 1697 1699 +f 1700 1699 1695 +f 1701 1702 1703 +f 1704 1703 1705 +f 1706 1705 1707 +f 1708 1707 1702 +f 1709 1701 1703 +f 1704 1709 1703 +f 1698 1696 1697 +f 1710 1704 1705 +f 1706 1710 1705 +f 1701 1708 1702 +f 1708 1706 1707 +f 1694 1700 1695 +f 1711 1642 1644 +f 1711 1640 1642 +f 1711 1712 1640 +f 1711 1644 1712 +f 1646 1643 1658 +f 1658 1643 1644 +f 1713 1640 1645 +f 1712 1714 1644 +f 1640 1715 1712 +f 1645 1647 1659 +f 1714 1712 1715 +f 1644 1714 1658 +f 1645 1654 1713 +f 1651 1659 1647 +f 1715 1640 1713 +f 1658 1651 1646 +f 1659 1654 1645 +f 1716 1713 1715 +f 1716 1717 1713 +f 1716 1714 1717 +f 1716 1715 1714 +f 1654 1656 1673 +f 1660 1658 1671 +f 1656 1660 1670 +f 1717 1718 1714 +f 1713 1719 1717 +f 1671 1658 1714 +f 1719 1713 1654 +f 1673 1675 1654 +f 1671 1670 1660 +f 1670 1673 1656 +f 1720 1714 1718 +f 1718 1717 1719 +f 1714 1720 1671 +f 1654 1675 1719 +f 1721 1720 1722 +f 1721 1718 1720 +f 1721 1719 1718 +f 1721 1722 1719 +f 1723 1724 1725 +f 1723 1725 1726 +f 1723 1726 1727 +f 1723 1727 1724 +f 1719 1726 1722 +f 1726 1719 1725 +f 1675 1676 1665 +f 1661 1671 1720 +f 1725 1719 1675 +f 1722 1727 1720 +f 1678 1671 1661 +f 1720 1727 1661 +f 1675 1667 1725 +f 1727 1722 1726 +f 1661 1664 1678 +f 1665 1667 1675 +f 1664 1665 1676 +f 1725 1728 1724 +f 1662 1661 1688 +f 1668 1662 1681 +f 1667 1668 1689 +f 1688 1661 1727 +f 1728 1725 1667 +f 1724 1729 1727 +f 1730 1729 1731 +f 1730 1728 1729 +f 1731 1727 1729 +f 1727 1731 1688 +f 1729 1724 1728 +f 1688 1681 1662 +f 1667 1684 1728 +f 1689 1684 1667 +f 1681 1689 1668 +f 1686 1688 1694 +f 1694 1688 1731 +f 1690 1686 1692 +f 1684 1690 1691 +f 1730 1732 1728 +f 1730 1731 1732 +f 1732 1733 1731 +f 1728 1734 1732 +f 1735 1728 1684 +f 1692 1691 1690 +f 1733 1732 1734 +f 1691 1696 1684 +f 1734 1728 1735 +f 1684 1696 1735 +f 1731 1733 1694 +f 1694 1692 1686 +f 1736 1734 1733 +f 1736 1735 1734 +f 1736 1737 1735 +f 1736 1733 1737 +f 1738 1739 1740 +f 1738 1741 1739 +f 1738 1742 1741 +f 1738 1740 1742 +f 1696 1698 1709 +f 1708 1694 1733 +f 1741 1735 1696 +f 1700 1694 1708 +f 1698 1700 1701 +f 1737 1739 1733 +f 1735 1741 1737 +f 1701 1709 1698 +f 1733 1740 1708 +f 1709 1704 1696 +f 1740 1733 1739 +f 1708 1701 1700 +f 1739 1737 1741 +f 1696 1704 1741 +f 1743 1610 1607 +f 1744 1607 1620 +f 1745 1617 1616 +f 1746 1616 1618 +f 1747 1620 1609 +f 1748 1609 1610 +f 1745 1749 1617 +f 1746 1745 1616 +f 1748 1747 1609 +f 1743 1748 1610 +f 1750 1743 1607 +f 1751 1750 1607 +f 1744 1751 1607 +f 1747 1744 1620 +f 1752 1619 1617 +f 1753 1622 1624 +f 1754 1624 1625 +f 1755 1625 1623 +f 1756 1623 1622 +f 1757 1618 1619 +f 1757 1746 1618 +f 1749 1752 1617 +f 1758 1753 1624 +f 1759 1758 1624 +f 1754 1759 1624 +f 1756 1755 1623 +f 1755 1754 1625 +f 1753 1756 1622 +f 1760 1761 1634 +f 1762 1760 1635 +f 1763 1762 1633 +f 1764 1763 1632 +f 1765 1764 1632 +f 1761 1765 1632 +f 1766 1767 1627 +f 1768 1766 1626 +f 1769 1768 1626 +f 1770 1769 1628 +f 1767 1771 1627 +f 1770 1628 1630 +f 1771 1630 1627 +f 1766 1627 1626 +f 1769 1626 1628 +f 1762 1635 1633 +f 1763 1633 1632 +f 1772 1641 1637 +f 1761 1632 1634 +f 1760 1634 1635 +f 1773 1637 1638 +f 1774 1638 1636 +f 1775 1636 1641 +f 1776 1777 1642 +f 1773 1778 1637 +f 1779 1773 1638 +f 1774 1779 1638 +f 1775 1774 1636 +f 1778 1772 1637 +f 1747 1780 1781 +f 1782 1783 1784 +f 1785 1780 1748 +f 1784 1785 1748 +f 1747 1748 1780 +f 1784 1750 1782 +f 1748 1743 1784 +f 1781 1786 1747 +f 1743 1750 1784 +f 1787 1782 1750 +f 1744 1747 1749 +f 1751 1744 1745 +f 1750 1751 1746 +f 1749 1747 1786 +f 1745 1746 1751 +f 1786 1788 1749 +f 1750 1746 1787 +f 1749 1745 1744 +f 1786 1781 1789 +f 1790 1789 1781 +f 1789 1783 1782 +f 1783 1789 1790 +f 1789 1791 1786 +f 1782 1791 1789 +f 1792 1782 1787 +f 1782 1792 1793 +f 1793 1788 1786 +f 1793 1791 1782 +f 1786 1791 1793 +f 1788 1793 1792 +f 1788 1794 1795 +f 1796 1797 1798 +f 1792 1794 1788 +f 1787 1794 1792 +f 1798 1797 1799 +f 1800 1797 1796 +f 1795 1794 1787 +f 1799 1797 1800 +f 1795 1798 1788 +f 1787 1796 1795 +f 1757 1752 1756 +f 1746 1757 1753 +f 1796 1787 1746 +f 1752 1749 1755 +f 1755 1749 1788 +f 1753 1758 1746 +f 1755 1756 1752 +f 1798 1795 1796 +f 1756 1753 1757 +f 1746 1758 1796 +f 1788 1799 1755 +f 1799 1788 1798 +f 1767 1755 1799 +f 1801 1796 1758 +f 1759 1754 1766 +f 1800 1802 1799 +f 1758 1759 1768 +f 1796 1803 1800 +f 1754 1755 1767 +f 1799 1802 1767 +f 1758 1769 1801 +f 1768 1769 1758 +f 1767 1766 1754 +f 1802 1800 1803 +f 1766 1768 1759 +f 1803 1796 1801 +f 1804 1805 1806 +f 1807 1805 1804 +f 1808 1805 1807 +f 1803 1809 1802 +f 1801 1809 1803 +f 1810 1809 1801 +f 1802 1809 1810 +f 1806 1805 1808 +f 1801 1804 1810 +f 1760 1767 1802 +f 1804 1801 1769 +f 1769 1770 1763 +f 1810 1806 1802 +f 1771 1767 1760 +f 1770 1771 1762 +f 1808 1802 1806 +f 1802 1808 1760 +f 1806 1810 1804 +f 1760 1762 1771 +f 1763 1764 1769 +f 1762 1763 1770 +f 1769 1764 1804 +f 1761 1760 1778 +f 1765 1761 1773 +f 1764 1765 1779 +f 1778 1760 1808 +f 1811 1804 1764 +f 1773 1779 1765 +f 1778 1773 1761 +f 1808 1812 1778 +f 1764 1774 1811 +f 1779 1774 1764 +f 1813 1778 1812 +f 1814 1811 1774 +f 1774 1775 1776 +f 1772 1778 1813 +f 1775 1772 1777 +f 1774 1815 1814 +f 1777 1776 1775 +f 1813 1777 1772 +f 1812 1816 1813 +f 1776 1815 1774 +f 1817 1818 1812 +f 1811 1818 1817 +f 1807 1812 1808 +f 1812 1807 1817 +f 1804 1817 1807 +f 1817 1804 1811 +f 1819 1820 1814 +f 1811 1814 1820 +f 1816 1812 1819 +f 1820 1818 1811 +f 1812 1818 1820 +f 1820 1819 1812 +f 1821 1815 1640 +f 1822 1821 1640 +f 1777 1813 1644 +f 1813 1822 1712 +f 1815 1776 1640 +f 1823 1713 1717 +f 1824 1717 1714 +f 1825 1714 1715 +f 1826 1715 1713 +f 1777 1644 1642 +f 1776 1642 1640 +f 1822 1640 1712 +f 1813 1712 1644 +f 1826 1827 1715 +f 1825 1828 1714 +f 1823 1826 1713 +f 1828 1824 1714 +f 1827 1825 1715 +f 1829 1830 1719 +f 1831 1829 1719 +f 1832 1831 1719 +f 1833 1832 1722 +f 1834 1833 1720 +f 1830 1834 1718 +f 1830 1718 1719 +f 1832 1719 1722 +f 1835 1725 1724 +f 1836 1724 1727 +f 1833 1722 1720 +f 1837 1727 1726 +f 1838 1726 1725 +f 1834 1720 1718 +f 1837 1839 1727 +f 1835 1838 1725 +f 1839 1836 1727 +f 1840 1837 1726 +f 1838 1840 1726 +f 1841 1842 1729 +f 1843 1841 1728 +f 1844 1843 1728 +f 1845 1844 1728 +f 1846 1845 1732 +f 1842 1846 1731 +f 1842 1731 1729 +f 1841 1729 1728 +f 1845 1728 1732 +f 1846 1732 1731 +f 1847 1733 1734 +f 1848 1734 1735 +f 1849 1847 1734 +f 1848 1849 1734 +f 1847 1850 1733 +f 1851 1737 1733 +f 1852 1735 1737 +f 1853 1740 1739 +f 1854 1739 1741 +f 1855 1741 1742 +f 1856 1742 1740 +f 1850 1851 1733 +f 1853 1856 1740 +f 1854 1853 1739 +f 1857 1854 1741 +f 1858 1857 1741 +f 1855 1858 1741 +f 1852 1848 1735 +f 1856 1855 1742 +f 1819 1859 1816 +f 1814 1859 1819 +f 1860 1859 1814 +f 1816 1859 1860 +f 1861 1862 1863 +f 1864 1862 1861 +f 1865 1862 1864 +f 1863 1862 1865 +f 1833 1828 1865 +f 1824 1828 1833 +f 1823 1824 1834 +f 1864 1866 1865 +f 1861 1867 1864 +f 1867 1861 1826 +f 1826 1823 1830 +f 1860 1865 1816 +f 1814 1863 1860 +f 1828 1813 1816 +f 1861 1814 1815 +f 1822 1813 1828 +f 1821 1822 1825 +f 1815 1821 1827 +f 1863 1814 1861 +f 1828 1825 1822 +f 1865 1860 1863 +f 1825 1827 1821 +f 1816 1865 1828 +f 1827 1826 1815 +f 1815 1826 1861 +f 1867 1868 1866 +f 1866 1868 1869 +f 1870 1868 1867 +f 1871 1872 1873 +f 1873 1872 1874 +f 1875 1872 1871 +f 1874 1872 1875 +f 1869 1868 1870 +f 1834 1830 1823 +f 1869 1865 1866 +f 1865 1869 1833 +f 1826 1829 1867 +f 1866 1864 1867 +f 1833 1834 1824 +f 1830 1829 1826 +f 1870 1871 1869 +f 1874 1867 1829 +f 1832 1833 1839 +f 1831 1832 1837 +f 1839 1833 1869 +f 1829 1831 1840 +f 1867 1875 1870 +f 1829 1838 1874 +f 1840 1838 1829 +f 1839 1837 1832 +f 1869 1871 1839 +f 1837 1840 1831 +f 1875 1867 1874 +f 1871 1870 1875 +f 1836 1839 1846 +f 1835 1836 1842 +f 1838 1835 1841 +f 1846 1839 1871 +f 1876 1874 1838 +f 1842 1841 1835 +f 1846 1842 1836 +f 1871 1877 1846 +f 1838 1843 1876 +f 1841 1843 1838 +f 1843 1844 1849 +f 1878 1876 1843 +f 1845 1846 1850 +f 1850 1846 1877 +f 1844 1845 1847 +f 1849 1848 1843 +f 1877 1879 1850 +f 1847 1849 1844 +f 1850 1847 1845 +f 1843 1848 1878 +f 1874 1876 1873 +f 1876 1880 1881 +f 1877 1871 1881 +f 1873 1881 1871 +f 1881 1873 1876 +f 1881 1880 1877 +f 1877 1880 1882 +f 1883 1876 1878 +f 1882 1879 1877 +f 1882 1880 1876 +f 1879 1882 1883 +f 1876 1883 1882 +f 1879 1884 1885 +f 1878 1884 1883 +f 1883 1884 1879 +f 1885 1884 1878 +f 1886 1887 1888 +f 1889 1887 1886 +f 1890 1887 1889 +f 1888 1887 1890 +f 1856 1850 1879 +f 1889 1878 1848 +f 1851 1850 1856 +f 1852 1851 1853 +f 1848 1852 1854 +f 1885 1886 1879 +f 1878 1889 1885 +f 1879 1888 1856 +f 1854 1857 1848 +f 1856 1853 1851 +f 1888 1879 1886 +f 1848 1857 1889 +f 1886 1885 1889 +f 1853 1854 1852 +f 1891 1892 1893 +f 1894 1891 1893 +f 1895 1896 1897 +f 1892 1895 1897 +f 1898 1894 1899 +f 1892 1897 1893 +f 1894 1893 1899 +f 1898 1899 1900 +f 1896 1900 1897 +f 1901 1902 1903 +f 1904 1903 1905 +f 1906 1905 1907 +f 1906 1907 1902 +f 1904 1908 1903 +f 1909 1904 1905 +f 1906 1909 1905 +f 1901 1910 1902 +f 1910 1906 1902 +f 1908 1901 1903 +f 1911 1912 1913 +f 1914 1913 1915 +f 1916 1915 1917 +f 1918 1917 1912 +f 1919 1920 1921 +f 1922 1921 1923 +f 1911 1924 1912 +f 1925 1911 1913 +f 1916 1914 1915 +f 1919 1926 1920 +f 1914 1925 1913 +f 1927 1919 1921 +f 1922 1927 1921 +f 1924 1918 1912 +f 1928 1929 1930 +f 1931 1930 1932 +f 1933 1932 1934 +f 1935 1934 1929 +f 1936 1923 1937 +f 1936 1937 1920 +f 1938 1933 1934 +f 1935 1938 1934 +f 1928 1935 1929 +f 1939 1922 1923 +f 1936 1939 1923 +f 1933 1931 1932 +f 1931 1940 1930 +f 1926 1936 1920 +f 1941 1942 1943 +f 1944 1941 1945 +f 1946 1944 1945 +f 1947 1946 1948 +f 1949 1947 1948 +f 1942 1949 1943 +f 1950 1951 1952 +f 1953 1952 1954 +f 1955 1954 1956 +f 1957 1956 1951 +f 1941 1943 1945 +f 1946 1945 1948 +f 1949 1948 1958 +f 1949 1958 1943 +f 1959 1960 1961 +f 1959 1962 1960 +f 1959 1963 1962 +f 1959 1961 1963 +f 1961 1741 1704 +f 1706 1708 1895 +f 1710 1706 1892 +f 1704 1710 1891 +f 1742 1962 1740 +f 1741 1963 1742 +f 1895 1708 1740 +f 1963 1741 1961 +f 1891 1894 1704 +f 1740 1962 1895 +f 1704 1894 1961 +f 1895 1892 1706 +f 1892 1891 1710 +f 1962 1742 1963 +f 1964 1965 1966 +f 1964 1966 1967 +f 1964 1968 1965 +f 1964 1967 1968 +f 1968 1961 1894 +f 1896 1895 1910 +f 1898 1896 1901 +f 1894 1898 1908 +f 1960 1965 1962 +f 1961 1968 1960 +f 1910 1895 1962 +f 1910 1901 1896 +f 1894 1904 1968 +f 1901 1908 1898 +f 1908 1904 1894 +f 1965 1960 1968 +f 1966 1962 1965 +f 1962 1966 1910 +f 1925 1914 1904 +f 1924 1911 1906 +f 1904 1914 1969 +f 1911 1925 1909 +f 1967 1970 1966 +f 1968 1971 1967 +f 1906 1910 1924 +f 1909 1906 1911 +f 1904 1909 1925 +f 1924 1910 1966 +f 1969 1968 1904 +f 1972 1969 1971 +f 1970 1967 1971 +f 1966 1970 1924 +f 1971 1968 1969 +f 1972 1971 1970 +f 1926 1924 1970 +f 1916 1918 1919 +f 1914 1916 1927 +f 1918 1924 1926 +f 1969 1973 1974 +f 1972 1974 1969 +f 1972 1970 1974 +f 1974 1975 1970 +f 1973 1969 1914 +f 1927 1922 1914 +f 1970 1976 1926 +f 1914 1922 1973 +f 1976 1970 1975 +f 1926 1919 1918 +f 1975 1974 1973 +f 1919 1927 1916 +f 1977 1975 1976 +f 1977 1973 1975 +f 1977 1978 1973 +f 1977 1976 1978 +f 1979 1980 1981 +f 1979 1982 1980 +f 1979 1983 1982 +f 1979 1981 1983 +f 1939 1936 1933 +f 1922 1939 1938 +f 1931 1926 1976 +f 1982 1973 1922 +f 1978 1981 1976 +f 1973 1980 1978 +f 1936 1926 1931 +f 1976 1981 1931 +f 1981 1978 1980 +f 1922 1935 1982 +f 1980 1973 1982 +f 1931 1933 1936 +f 1938 1935 1922 +f 1933 1938 1939 +f 1984 1982 1935 +f 1983 1985 1981 +f 1940 1931 1942 +f 1928 1940 1941 +f 1935 1928 1944 +f 1982 1984 1983 +f 1942 1931 1981 +f 1942 1941 1940 +f 1981 1986 1942 +f 1941 1944 1928 +f 1935 1946 1984 +f 1944 1946 1935 +f 1986 1981 1985 +f 1985 1983 1984 +f 1987 1985 1986 +f 1987 1984 1985 +f 1987 1988 1984 +f 1987 1986 1988 +f 1949 1942 1957 +f 1947 1949 1950 +f 1946 1947 1989 +f 1984 1990 1988 +f 1988 1991 1986 +f 1957 1942 1986 +f 1992 1984 1946 +f 1950 1989 1947 +f 1957 1950 1949 +f 1991 1988 1990 +f 1989 1953 1946 +f 1986 1991 1957 +f 1946 1953 1992 +f 1990 1984 1992 +f 1953 1989 1952 +f 1950 1957 1951 +f 1989 1950 1952 +f 1957 1993 1956 +f 1955 1953 1954 +f 1994 1995 1996 +f 1997 1994 1998 +f 1999 1997 1998 +f 2000 1999 2001 +f 2002 2000 2003 +f 1995 2002 1996 +f 2004 2005 2006 +f 2007 2006 2008 +f 2002 2003 1996 +f 1994 1996 1998 +f 1999 1998 2001 +f 2000 2001 2003 +f 2009 2004 2006 +f 2004 2010 2005 +f 2007 2009 2006 +f 2011 2012 2013 +f 2014 2011 2013 +f 2010 2015 2016 +f 2017 2014 2018 +f 2019 2017 2018 +f 2012 2020 2021 +f 2022 2007 2008 +f 2020 2019 2021 +f 2022 2008 2016 +f 2010 2016 2005 +f 2012 2021 2013 +f 2014 2013 2018 +f 2019 2018 2023 +f 2019 2023 2021 +f 2024 2025 2026 +f 2027 2028 2029 +f 2030 2024 2031 +f 2025 2027 2029 +f 2032 2030 2033 +f 2028 2032 2033 +f 2028 2033 2029 +f 2025 2029 2026 +f 2024 2026 2031 +f 2030 2031 2033 +f 2034 2035 2036 +f 2037 2036 2038 +f 2037 2034 2036 +f 2034 2039 2035 +f 2039 2040 2035 +f 2041 2037 2038 +f 2042 2041 2043 +f 2040 2042 2043 +f 2044 2045 2046 +f 2047 2046 2048 +f 2049 2048 2050 +f 2051 2050 2045 +f 2041 2038 2043 +f 2040 2043 2035 +f 2052 2047 2048 +f 2051 2053 2050 +f 2049 2052 2048 +f 2047 2044 2046 +f 2053 2049 2050 +f 2044 2051 2045 +f 2054 1990 1991 +f 2054 1992 1990 +f 2054 2055 1992 +f 2054 1991 2055 +f 1992 2056 2055 +f 2000 1957 1991 +f 2056 1992 1953 +f 1993 1957 2000 +f 1955 1993 2002 +f 1953 1955 1995 +f 2055 2057 1991 +f 2058 2056 2057 +f 1991 2059 2000 +f 2059 1991 2057 +f 2058 2057 2059 +f 2057 2055 2056 +f 2000 2002 1993 +f 2002 1995 1955 +f 1995 1994 1953 +f 1953 1994 2056 +f 2060 2061 2062 +f 2060 2063 2061 +f 2060 2064 2063 +f 2060 2062 2064 +f 1997 1999 2004 +f 1994 1997 2009 +f 2010 2000 2059 +f 1999 2000 2010 +f 2058 2065 2056 +f 2063 2056 1994 +f 2065 2062 2059 +f 2056 2061 2065 +f 2058 2059 2065 +f 2009 2007 1994 +f 2062 2065 2061 +f 1994 2007 2063 +f 2061 2056 2063 +f 2010 2004 1999 +f 2059 2062 2010 +f 2004 2009 1997 +f 2066 2067 2068 +f 2066 2069 2067 +f 2066 2070 2069 +f 2066 2068 2070 +f 2020 2010 2062 +f 2069 2063 2007 +f 2063 2069 2064 +f 2015 2010 2020 +f 2022 2015 2012 +f 2007 2022 2011 +f 2064 2067 2062 +f 2012 2011 2022 +f 2020 2012 2015 +f 2062 2068 2020 +f 2007 2014 2069 +f 2068 2062 2067 +f 2011 2014 2007 +f 2067 2064 2069 +f 2071 2072 2073 +f 2071 2074 2072 +f 2071 2075 2074 +f 2071 2073 2075 +f 2014 2017 2027 +f 2032 2020 2068 +f 2070 2075 2068 +f 2069 2073 2070 +f 2017 2019 2028 +f 2072 2069 2014 +f 2019 2020 2032 +f 2032 2028 2019 +f 2075 2070 2073 +f 2028 2027 2017 +f 2068 2075 2032 +f 2027 2025 2014 +f 2014 2025 2072 +f 2073 2069 2072 +f 2076 2072 2025 +f 2074 2077 2075 +f 2030 2032 2040 +f 2024 2030 2039 +f 2025 2024 2034 +f 2072 2076 2074 +f 2040 2032 2075 +f 2078 2075 2077 +f 2039 2034 2024 +f 2034 2037 2025 +f 2025 2037 2076 +f 2077 2074 2076 +f 2040 2039 2030 +f 2075 2078 2040 +f 2079 2077 2078 +f 2079 2076 2077 +f 2079 2080 2076 +f 2079 2078 2080 +f 2037 2051 2081 +f 2052 2049 2042 +f 2049 2053 2041 +f 2053 2051 2037 +f 2082 2083 2084 +f 2082 2081 2083 +f 2084 2080 2083 +f 2083 2076 2081 +f 2078 2084 2052 +f 2052 2040 2078 +f 2081 2076 2037 +f 2076 2083 2080 +f 2080 2084 2078 +f 2042 2040 2052 +f 2037 2041 2053 +f 2085 2052 2084 +f 2081 2086 2087 +f 2088 2081 2051 +f 2082 2087 2081 +f 2082 2084 2087 +f 2047 2052 2085 +f 2051 2044 2089 +f 2087 2090 2084 +f 2090 2087 2086 +f 2086 2081 2088 +f 2085 2091 2047 +f 2091 2089 2044 +f 2051 2089 2088 +f 2084 2090 2085 +f 2092 1966 1965 +f 2093 1965 1968 +f 2094 1962 1963 +f 2095 1963 1961 +f 2096 1961 1960 +f 2097 1960 1962 +f 2094 2098 1962 +f 2099 2094 1963 +f 2095 2099 1963 +f 2092 2100 1966 +f 2093 2092 1965 +f 2096 2095 1961 +f 2101 2093 1968 +f 2098 2097 1962 +f 2102 1969 1974 +f 2103 1974 1970 +f 2104 1968 1967 +f 2100 1967 1966 +f 2105 1970 1971 +f 2106 1971 1969 +f 2107 2105 1971 +f 2106 2107 1971 +f 2102 2106 1969 +f 2108 2101 1968 +f 2104 2108 1968 +f 2100 2104 1967 +f 2109 2103 1970 +f 2105 2109 1970 +f 2110 2111 1975 +f 2112 2110 1973 +f 2113 2112 1973 +f 2114 2113 1973 +f 2115 2114 1978 +f 2111 2115 1976 +f 2110 1975 1973 +f 2114 1973 1978 +f 2115 1978 1976 +f 2116 1982 1983 +f 2117 1983 1981 +f 2118 1980 1982 +f 2111 1976 1975 +f 2119 1981 1980 +f 2120 2119 1980 +f 2118 2120 1980 +f 2116 2118 1982 +f 2121 2117 1981 +f 2119 2121 1981 +f 2122 2123 1986 +f 2124 2122 1985 +f 2125 2124 1984 +f 2126 2125 1984 +f 2127 2126 1984 +f 2123 2127 1988 +f 2128 1991 1990 +f 2129 1990 1992 +f 2130 1992 2055 +f 2131 2055 1991 +f 2122 1986 1985 +f 2124 1985 1984 +f 2127 1984 1988 +f 2123 1988 1986 +f 2132 2133 2134 +f 2135 2133 2136 +f 2136 2133 2132 +f 2137 2138 2139 +f 2140 2138 2137 +f 2134 2133 2135 +f 2141 2138 2140 +f 2139 2138 2141 +f 2098 1856 1888 +f 1890 2135 1888 +f 1889 2134 1890 +f 2132 1889 1857 +f 1855 1856 2098 +f 1858 1855 2094 +f 1857 1858 2099 +f 1888 2135 2098 +f 1857 2095 2132 +f 2135 1890 2134 +f 2098 2094 1855 +f 2134 1889 2132 +f 2094 2099 1858 +f 2099 2095 1857 +f 2100 2092 2097 +f 2135 2139 2100 +f 2095 2101 2140 +f 2137 2136 2140 +f 2092 2093 2096 +f 2139 2135 2137 +f 2093 2101 2095 +f 2097 2098 2100 +f 2095 2096 2093 +f 2136 2137 2135 +f 2132 2140 2136 +f 2100 2098 2135 +f 2140 2132 2095 +f 2096 2097 2092 +f 2109 2100 2139 +f 2142 2140 2101 +f 2104 2100 2109 +f 2108 2104 2105 +f 2101 2108 2107 +f 2101 2106 2142 +f 2105 2107 2108 +f 2139 2143 2109 +f 2107 2106 2101 +f 2109 2105 2104 +f 2103 2109 2115 +f 2102 2103 2111 +f 2106 2102 2110 +f 2115 2109 2143 +f 2144 2142 2106 +f 2115 2111 2103 +f 2111 2110 2102 +f 2143 2145 2115 +f 2106 2112 2144 +f 2110 2112 2106 +f 2141 2143 2139 +f 2143 2141 2146 +f 2140 2146 2141 +f 2146 2140 2142 +f 2146 2147 2143 +f 2142 2147 2146 +f 2148 2149 2144 +f 2142 2144 2149 +f 2149 2147 2142 +f 2143 2147 2149 +f 2145 2143 2148 +f 2149 2148 2143 +f 2144 2150 2148 +f 2151 2150 2144 +f 2145 2150 2151 +f 2152 2153 2154 +f 2155 2153 2152 +f 2156 2153 2155 +f 2154 2153 2156 +f 2148 2150 2145 +f 2156 2157 2154 +f 2157 2156 2118 +f 2117 2121 2123 +f 2123 2121 2152 +f 2116 2117 2122 +f 2118 2116 2124 +f 2154 2158 2152 +f 2114 2115 2121 +f 2113 2114 2119 +f 2151 2152 2145 +f 2144 2155 2151 +f 2112 2113 2120 +f 2121 2115 2145 +f 2156 2144 2112 +f 2120 2118 2112 +f 2152 2151 2155 +f 2119 2120 2113 +f 2145 2152 2121 +f 2112 2118 2156 +f 2155 2144 2156 +f 2121 2119 2114 +f 2159 2160 2157 +f 2161 2160 2159 +f 2158 2160 2161 +f 2162 2163 2164 +f 2157 2160 2158 +f 2165 2163 2166 +f 2164 2163 2165 +f 2166 2163 2162 +f 2152 2161 2123 +f 2161 2152 2158 +f 2158 2154 2157 +f 2118 2125 2157 +f 2123 2122 2117 +f 2122 2124 2116 +f 2124 2125 2118 +f 2125 2126 2167 +f 2126 2127 2128 +f 2127 2123 2168 +f 2165 2157 2125 +f 2168 2123 2161 +f 2159 2162 2161 +f 2157 2166 2159 +f 2166 2157 2165 +f 2128 2167 2126 +f 2161 2162 2168 +f 2168 2128 2127 +f 2125 2129 2165 +f 2162 2159 2166 +f 2167 2129 2125 +f 2128 2168 1991 +f 2130 2129 1992 +f 2167 2128 1990 +f 2129 2167 1990 +f 2168 2131 1991 +f 2169 2170 2059 +f 2171 2169 2057 +f 2172 2171 2056 +f 2173 2172 2056 +f 2174 2173 2056 +f 2170 2174 2065 +f 2175 2061 2063 +f 2169 2059 2057 +f 2171 2057 2056 +f 2174 2056 2065 +f 2170 2065 2059 +f 2176 2062 2061 +f 2176 2177 2062 +f 2178 2176 2061 +f 2175 2178 2061 +f 2179 2064 2062 +f 2180 2068 2067 +f 2181 2067 2069 +f 2182 2069 2070 +f 2183 2070 2068 +f 2184 2063 2064 +f 2185 2186 2069 +f 2182 2185 2069 +f 2183 2182 2070 +f 2180 2183 2068 +f 2181 2180 2067 +f 2186 2181 2069 +f 2184 2175 2063 +f 2177 2179 2062 +f 2187 2188 2075 +f 2188 2189 2075 +f 2190 2187 2073 +f 2191 2190 2073 +f 2192 2191 2072 +f 2191 2073 2072 +f 2192 2072 2074 +f 2189 2074 2075 +f 2187 2075 2073 +f 2193 2078 2077 +f 2194 2077 2076 +f 2194 2193 2077 +f 2195 2194 2076 +f 2196 2195 2076 +f 2193 2197 2078 +f 2198 2081 2087 +f 2199 2087 2084 +f 2200 2084 2083 +f 2201 2083 2081 +f 2202 2076 2080 +f 2197 2080 2078 +f 2200 2199 2084 +f 2201 2200 2083 +f 2203 2201 2081 +f 2198 2203 2081 +f 2204 2198 2087 +f 2202 2196 2076 +f 2197 2202 2080 +f 2199 2204 2087 +f 2170 2168 2162 +f 2205 2165 2129 +f 2131 2168 2170 +f 2130 2131 2169 +f 2129 2130 2171 +f 2129 2172 2205 +f 2169 2171 2130 +f 2162 2206 2170 +f 2171 2172 2129 +f 2170 2169 2131 +f 2174 2170 2177 +f 2173 2174 2176 +f 2172 2173 2178 +f 2207 2205 2172 +f 2177 2170 2206 +f 2172 2175 2207 +f 2178 2175 2172 +f 2176 2178 2173 +f 2206 2208 2177 +f 2177 2176 2174 +f 2164 2209 2162 +f 2165 2205 2164 +f 2209 2210 2206 +f 2205 2210 2209 +f 2206 2162 2209 +f 2209 2164 2205 +f 2208 2211 2212 +f 2211 2210 2205 +f 2206 2210 2211 +f 2211 2208 2206 +f 2205 2212 2211 +f 2212 2205 2207 +f 2212 2213 2208 +f 2207 2213 2212 +f 2214 2215 2216 +f 2217 2215 2214 +f 2218 2215 2217 +f 2216 2215 2218 +f 2219 2213 2207 +f 2208 2213 2219 +f 2184 2179 2180 +f 2179 2177 2183 +f 2175 2184 2181 +f 2183 2177 2208 +f 2217 2207 2175 +f 2219 2214 2208 +f 2207 2217 2219 +f 2183 2180 2179 +f 2180 2181 2184 +f 2175 2186 2217 +f 2216 2208 2214 +f 2208 2216 2183 +f 2214 2219 2217 +f 2181 2186 2175 +f 2220 2221 2222 +f 2223 2221 2220 +f 2224 2221 2223 +f 2225 2226 2227 +f 2228 2226 2225 +f 2229 2226 2228 +f 2227 2226 2229 +f 2222 2221 2224 +f 2217 2222 2218 +f 2185 2182 2187 +f 2182 2183 2188 +f 2186 2185 2190 +f 2218 2224 2216 +f 2188 2183 2216 +f 2220 2217 2186 +f 2188 2187 2182 +f 2224 2218 2222 +f 2190 2191 2186 +f 2187 2190 2185 +f 2222 2217 2220 +f 2216 2224 2188 +f 2186 2191 2220 +f 2228 2220 2191 +f 2189 2188 2197 +f 2192 2189 2193 +f 2191 2192 2194 +f 2223 2225 2224 +f 2220 2228 2223 +f 2197 2188 2224 +f 2227 2224 2225 +f 2193 2194 2192 +f 2225 2223 2228 +f 2197 2193 2189 +f 2194 2195 2191 +f 2224 2227 2197 +f 2191 2195 2228 +f 2230 2231 2198 +f 2203 2198 2231 +f 2204 2199 2232 +f 2232 2199 2233 +f 2203 2231 2234 +f 2232 2230 2204 +f 2234 2235 2203 +f 2233 2236 2232 +f 2199 2197 2227 +f 2202 2197 2199 +f 2196 2202 2200 +f 2195 2196 2201 +f 2235 2228 2195 +f 2201 2203 2195 +f 2200 2201 2196 +f 2227 2233 2199 +f 2195 2203 2235 +f 2199 2200 2202 +f 2235 2237 2238 +f 2229 2233 2227 +f 2233 2229 2238 +f 2238 2228 2235 +f 2228 2238 2229 +f 2238 2237 2233 +f 2236 2239 2240 +f 2240 2235 2234 +f 2239 2236 2233 +f 2233 2237 2239 +f 2235 2240 2239 +f 2239 2237 2235 +f 2241 2242 2243 +f 2244 2245 2246 +f 2245 2243 2242 +f 2242 2246 2245 +f 2243 2245 2244 +f 2247 2246 2242 +f 2248 2244 2247 +f 2247 2241 2248 +f 2244 2248 2243 +f 2243 2248 2241 +f 2246 2247 2244 +f 2242 2241 2247 +f 2249 2250 2251 +f 2250 2252 2251 +f 2251 2252 2253 +f 2252 2254 2255 +f 2254 2252 2250 +f 2256 2257 2258 +f 2259 2257 2256 +f 2256 2255 2259 +f 2255 2256 2252 +f 2252 2256 2253 +f 2253 2256 2258 +f 2260 2261 2262 +f 2262 2254 2260 +f 2254 2263 2260 +f 2250 2263 2254 +f 2262 2264 2265 +f 2255 2265 2266 +f 2255 2254 2262 +f 2265 2255 2262 +f 2267 2265 2264 +f 2268 2264 2269 +f 2261 2269 2264 +f 2270 2267 2268 +f 2264 2268 2267 +f 2271 2272 2270 +f 2267 2270 2272 +f 2272 2271 2273 +f 2264 2262 2261 +f 2266 2274 2275 +f 2272 2266 2267 +f 2265 2267 2266 +f 2273 2274 2272 +f 2266 2272 2274 +f 2274 2273 2276 +f 2259 2255 2266 +f 2259 2266 2275 +f 2258 2257 2277 +f 2257 2278 2277 +f 2275 2278 2257 +f 2257 2259 2275 +f 2279 2280 2278 +f 2278 2275 2279 +f 2277 2278 2281 +f 2278 2280 2281 +f 2275 2274 2279 +f 2274 2276 2279 +f 2282 2279 2283 +f 2279 2276 2283 +f 2281 2280 2284 +f 2280 2285 2284 +f 2282 2285 2280 +f 2280 2279 2282 +f 2286 2273 2271 +f 2287 2271 2288 +f 2270 2288 2271 +f 2271 2287 2286 +f 2289 2268 2290 +f 2269 2290 2268 +f 2288 2270 2289 +f 2268 2289 2270 +f 2290 2291 2289 +f 2292 2293 2294 +f 2294 2295 2296 +f 2295 2294 2293 +f 2294 2297 2292 +f 2298 2292 2297 +f 2296 2299 2294 +f 2300 2301 2302 +f 2303 2304 2305 +f 2302 2305 2304 +f 2304 2306 2302 +f 2299 2301 2297 +f 2300 2297 2301 +f 2297 2294 2299 +f 2306 2300 2302 +f 2284 2285 2303 +f 2285 2304 2303 +f 2306 2304 2285 +f 2285 2282 2306 +f 2282 2283 2306 +f 2283 2300 2306 +f 2307 2276 2273 +f 2298 2283 2307 +f 2276 2307 2283 +f 2297 2300 2298 +f 2283 2298 2300 +f 2308 2307 2286 +f 2273 2286 2307 +f 2292 2298 2308 +f 2307 2308 2298 +f 2309 2286 2287 +f 2309 2310 2311 +f 2311 2308 2309 +f 2286 2309 2308 +f 2308 2311 2292 +f 2312 2293 2313 +f 2311 2313 2293 +f 2314 2295 2312 +f 2293 2312 2295 +f 2293 2292 2311 +f 2315 2316 2317 +f 2314 2315 2318 +f 2317 2319 2320 +f 2315 2317 2320 +f 2315 2314 2316 +f 2312 2316 2314 +f 2318 2321 2314 +f 2320 2318 2315 +f 2295 2314 2321 +f 2321 2296 2295 +f 2313 2311 2310 +f 2310 2309 2322 +f 2323 2313 2324 +f 2310 2324 2313 +f 2313 2323 2312 +f 2316 2323 2325 +f 2316 2325 2317 +f 2323 2324 2326 +f 2323 2326 2325 +f 2316 2312 2323 +f 2327 2328 2329 +f 2324 2330 2326 +f 2327 2329 2330 +f 2327 2322 2328 +f 2331 2328 2322 +f 2324 2310 2327 +f 2322 2327 2310 +f 2324 2327 2330 +f 2332 2333 2331 +f 2291 2334 2332 +f 2331 2288 2332 +f 2289 2332 2288 +f 2322 2287 2331 +f 2288 2331 2287 +f 2287 2322 2309 +f 2332 2289 2291 +f 2333 2332 2334 +f 2328 2335 2329 +f 2336 2337 2334 +f 2337 2333 2334 +f 2328 2333 2335 +f 2333 2337 2335 +f 2328 2331 2333 +f 2338 2339 2340 +f 2263 2250 2341 +f 2341 2340 2263 +f 2339 2338 2342 +f 2342 2343 2339 +f 2340 2341 2338 +f 2341 2250 2249 +f 2342 2338 2344 +f 2338 2345 2344 +f 2338 2341 2345 +f 2341 2346 2345 +f 2346 2341 2249 +f 2345 2346 2344 +f 2346 2249 2344 +f 2344 2249 2347 +f 2348 2347 2251 +f 2249 2251 2347 +f 2349 2350 2351 +f 2351 2350 2348 +f 2352 2353 2349 +f 2350 2349 2353 +f 2351 2348 2253 +f 2251 2253 2348 +f 2350 2354 2348 +f 2355 2356 957 +f 2353 2357 2350 +f 2355 957 958 +f 2356 2355 2358 +f 2359 2358 2355 +f 2357 2353 2359 +f 2358 2359 2353 +f 2360 2349 2351 +f 2352 2361 2362 +f 2361 2352 2363 +f 2360 2351 2258 +f 2253 2258 2351 +f 2363 2349 2360 +f 2349 2363 2352 +f 2364 2363 2365 +f 2365 2363 2360 +f 2366 2361 2364 +f 2365 2360 2277 +f 2258 2277 2360 +f 2363 2364 2361 +f 2367 2356 2362 +f 2356 2367 917 +f 2353 2352 2358 +f 2362 2358 2352 +f 2358 2362 2356 +f 2356 917 957 +f 2362 2368 2367 +f 2367 938 917 +f 2368 2362 2361 +f 2369 2367 2368 +f 2367 2369 938 +f 2368 2370 2369 +f 2371 2369 2370 +f 2369 2371 937 +f 2369 937 938 +f 2370 2368 2366 +f 2361 2366 2368 +f 2339 2372 2373 +f 2373 2260 2340 +f 2260 2263 2340 +f 2374 2372 2343 +f 2372 2339 2343 +f 2340 2339 2373 +f 2261 2260 2375 +f 2373 2375 2260 +f 2375 2373 2376 +f 2372 2376 2373 +f 2376 2372 2377 +f 2374 2377 2372 +f 2377 2374 2378 +f 2354 2350 2357 +f 2348 2354 2347 +f 2342 2344 2379 +f 2347 2380 2344 +f 2374 2343 2381 +f 2343 2342 2379 +f 2379 2381 2343 +f 2382 2383 2384 +f 2384 2385 2382 +f 2381 2379 2383 +f 2383 2382 2381 +f 2385 2384 2386 +f 2387 2388 2389 +f 2387 2390 2386 +f 2388 2391 2380 +f 2380 2389 2388 +f 2389 2390 2387 +f 2389 2380 2386 +f 2386 2390 2389 +f 2391 2388 2383 +f 2379 2391 2383 +f 2387 2386 2384 +f 2383 2388 2384 +f 2388 2387 2384 +f 2386 2354 2357 +f 2347 2354 2380 +f 2386 2380 2354 +f 2386 2357 2392 +f 2344 2391 2379 +f 2391 2344 2380 +f 2357 2393 2392 +f 2359 2394 2393 +f 2359 2393 2357 +f 2355 2394 2359 +f 2355 958 959 +f 959 2394 2355 +f 2394 2395 2393 +f 2395 2396 2393 +f 2394 959 2395 +f 963 2395 959 +f 2385 2396 2397 +f 2398 2385 2397 +f 2393 2385 2392 +f 2392 2385 2386 +f 2385 2393 2396 +f 2399 2400 2401 +f 2400 2402 2403 +f 2400 2403 2401 +f 2401 2403 2404 +f 2405 2401 2404 +f 2406 2404 2407 +f 2378 2408 2403 +f 2408 2378 2374 +f 2404 2403 2407 +f 2409 2410 2411 +f 2411 2382 2409 +f 2398 2409 2385 +f 2408 2374 2381 +f 2382 2411 2408 +f 2381 2382 2408 +f 2382 2385 2409 +f 2412 2407 2413 +f 2411 2403 2408 +f 2414 2413 2410 +f 2410 2409 2415 +f 2413 2407 2411 +f 2407 2403 2411 +f 2410 2413 2411 +f 2416 2417 2410 +f 2397 2418 2398 +f 2398 2418 2419 +f 2419 2416 2415 +f 2420 2417 2410 +f 2414 2410 2417 +f 2412 2413 2421 +f 2419 2409 2398 +f 2413 2414 2422 +f 2410 2415 2416 +f 2415 2409 2419 +f 2385 2420 2410 +f 2410 2417 2404 +f 2423 2364 2365 +f 2423 2365 2281 +f 2277 2281 2365 +f 2424 2366 2425 +f 2425 2364 2423 +f 2364 2425 2366 +f 2424 2426 2427 +f 2428 2427 2426 +f 2429 2423 2284 +f 2281 2284 2423 +f 2430 2425 2429 +f 2429 2425 2423 +f 2426 2424 2430 +f 2425 2430 2424 +f 2366 2424 2370 +f 2370 2427 2371 +f 2427 2370 2424 +f 2371 1004 937 +f 2431 2371 2427 +f 2431 1005 1004 +f 2371 2431 1004 +f 2427 2428 2431 +f 2432 2433 2434 +f 2434 2428 2435 +f 2433 2431 2428 +f 2433 2432 1008 +f 2433 1008 1005 +f 2431 2433 1005 +f 2426 2435 2428 +f 2428 2434 2433 +f 2436 2437 2399 +f 2438 2439 2405 +f 2440 2441 2402 +f 2442 2440 2443 +f 2437 2442 2400 +f 2439 2436 2401 +f 2421 2444 2412 +f 2444 2445 2407 +f 2446 2438 2404 +f 2445 2446 2406 +f 2417 2422 2414 +f 2422 2421 2413 +f 2417 2438 2404 +f 2401 2405 2439 +f 2407 2412 2444 +f 2404 2406 2446 +f 2406 2407 2445 +f 2405 2404 2438 +f 2447 2448 2449 +f 2450 2447 2451 +f 2452 2453 2454 +f 2455 2450 2456 +f 2457 2458 2459 +f 2453 2457 2460 +f 2448 2452 2461 +f 2457 2460 2462 +f 2400 2399 2437 +f 2443 2400 2442 +f 2458 2402 2441 +f 2402 2443 2440 +f 2399 2401 2436 +f 2404 2438 2457 +f 2449 2451 2447 +f 2459 2460 2457 +f 2461 2449 2448 +f 2454 2461 2452 +f 2460 2454 2453 +f 2441 2459 2458 +f 2438 2460 2457 +f 2463 2464 2465 +f 2465 2466 2463 +f 2290 2269 2467 +f 2468 2467 2269 +f 2467 2468 2469 +f 2470 2469 2468 +f 2469 2470 2466 +f 2471 2466 2470 +f 2466 2465 2469 +f 2472 2463 2466 +f 2466 2471 2472 +f 2375 2468 2261 +f 2468 2375 2470 +f 2376 2470 2375 +f 2470 2376 2471 +f 2377 2471 2376 +f 2269 2261 2468 +f 2471 2377 2473 +f 2378 2473 2377 +f 2443 2402 2400 +f 2403 2474 2378 +f 2402 2457 2403 +f 2457 2474 2403 +f 2473 2378 2474 +f 2453 2452 2457 +f 2458 2457 2402 +f 2448 2447 2452 +f 2475 2462 2455 +f 2462 2474 2455 +f 2455 2474 2447 +f 2447 2474 2452 +f 2452 2474 2457 +f 2450 2455 2447 +f 2474 2476 2473 +f 2473 2472 2471 +f 2462 2477 2474 +f 2478 2479 2477 +f 2472 2473 2476 +f 2480 2477 2462 +f 2479 2476 2477 +f 2477 2476 2474 +f 2479 2481 2476 +f 2476 2481 2482 +f 2483 2482 2481 +f 2484 2481 2479 +f 2482 2485 2476 +f 2463 2472 2486 +f 2476 2486 2472 +f 2485 2487 2476 +f 2487 2486 2476 +f 2488 2489 2487 +f 2490 2487 2485 +f 2491 2485 2482 +f 2318 2320 2492 +f 2493 2494 2495 +f 2492 2495 2494 +f 2494 2493 2496 +f 2492 2497 2318 +f 2321 2318 2497 +f 2494 2498 2492 +f 2497 2492 2498 +f 2496 2499 2494 +f 2498 2494 2499 +f 2500 2501 2502 +f 2502 2503 2504 +f 2317 2325 2504 +f 2504 2505 2317 +f 2319 2317 2505 +f 2506 2496 2493 +f 2495 2492 2320 +f 2320 2319 2495 +f 2495 2507 2493 +f 2493 2508 2506 +f 2509 2319 2505 +f 2507 2508 2493 +f 2319 2507 2495 +f 2508 2509 2506 +f 2508 2507 2509 +f 2507 2319 2509 +f 2510 2511 2512 +f 2513 2514 2515 +f 2512 2515 2514 +f 2299 2296 2510 +f 2510 2516 2299 +f 2511 2497 2515 +f 2515 2498 2513 +f 2511 2510 2296 +f 2515 2512 2511 +f 2296 2321 2511 +f 2496 2506 2517 +f 2497 2511 2321 +f 2499 2513 2498 +f 2498 2515 2497 +f 2518 2509 2519 +f 2517 2520 2496 +f 2499 2496 2520 +f 2521 2517 2506 +f 2509 2518 2521 +f 2504 2503 2505 +f 2503 2502 2501 +f 2506 2509 2521 +f 2501 2500 2522 +f 2523 2524 2522 +f 2525 2503 2501 +f 2522 2526 2501 +f 2505 2503 2519 +f 2505 2519 2509 +f 2525 2501 2527 +f 2525 2519 2503 +f 2501 2526 2527 +f 2522 2524 2526 +f 2528 2430 2429 +f 2284 2303 2429 +f 2430 2529 2426 +f 2528 2429 2303 +f 2529 2430 2528 +f 2435 2426 2529 +f 2530 2529 2528 +f 2435 2531 2434 +f 2531 2435 2532 +f 2529 2532 2435 +f 2530 2305 2533 +f 2530 2528 2305 +f 2303 2305 2528 +f 2534 2535 2305 +f 2305 2302 2534 +f 2536 2537 2535 +f 2535 2534 2536 +f 2538 2533 2537 +f 2537 2536 2538 +f 2305 2535 2533 +f 2535 2537 2533 +f 2538 2536 2539 +f 2301 2516 2534 +f 2534 2516 2536 +f 2516 2540 2536 +f 2536 2540 2539 +f 2302 2301 2534 +f 2540 2512 2539 +f 2514 2539 2512 +f 2514 2513 2541 +f 2513 2499 2542 +f 2301 2299 2516 +f 2512 2540 2510 +f 2516 2510 2540 +f 2543 2544 2545 +f 2517 2521 2546 +f 2544 2546 2545 +f 2547 2545 2548 +f 2545 2549 2548 +f 2520 2542 2499 +f 2549 2546 2521 +f 2546 2549 2545 +f 2549 2550 2548 +f 2550 2551 2548 +f 2548 2551 2525 +f 2521 2518 2549 +f 2551 2550 2552 +f 2550 2518 2519 +f 2518 2550 2549 +f 2553 2434 2531 +f 2432 2554 1021 +f 2434 2553 2432 +f 2554 2432 2553 +f 2432 1021 1008 +f 2541 2555 2514 +f 2542 2541 2513 +f 2556 2557 2542 +f 2520 2517 2558 +f 2558 2546 2559 +f 2560 2559 2561 +f 2562 2563 2559 +f 2564 2565 2563 +f 2565 2558 2563 +f 2563 2558 2559 +f 2561 2546 2566 +f 2566 2546 2567 +f 2544 2567 2546 +f 2559 2546 2561 +f 2546 2558 2517 +f 2568 2567 2544 +f 2569 2566 2567 +f 2570 2561 2566 +f 2532 2571 2531 +f 2532 2530 2571 +f 2538 2539 2572 +f 2539 2514 2555 +f 2532 2529 2530 +f 2533 2538 2572 +f 2533 2573 2530 +f 2397 2531 2571 +f 2574 2571 2530 +f 2572 2573 2533 +f 2531 2397 2553 +f 2574 2530 2573 +f 2539 2555 2572 +f 2553 2396 2554 +f 2395 2554 2396 +f 1021 2554 2395 +f 2395 963 1021 +f 2396 2553 2397 +f 2397 2571 2420 +f 2420 2571 2575 +f 2420 2418 2397 +f 2574 2576 2571 +f 2571 2576 2577 +f 2577 2575 2571 +f 2577 2576 2578 +f 2578 2575 2577 +f 2576 2574 2573 +f 2579 2578 2573 +f 2573 2578 2576 +f 2573 2572 2579 +f 2420 2575 2578 +f 2578 2579 2420 +f 2580 2419 2420 +f 2420 2419 2418 +f 2572 2555 2579 +f 2555 2580 2579 +f 2579 2580 2420 +f 2555 2541 2580 +f 2421 2422 2417 +f 2417 2416 2419 +f 2580 2445 2421 +f 2421 2417 2580 +f 2580 2417 2419 +f 2438 2445 2557 +f 2557 2445 2580 +f 2438 2446 2445 +f 2439 2438 2436 +f 2557 2436 2438 +f 2557 2580 2541 +f 2445 2444 2421 +f 2451 2556 2581 +f 2456 2451 2581 +f 2556 2558 2582 +f 2583 2582 2584 +f 2542 2520 2556 +f 2585 2581 2582 +f 2582 2581 2556 +f 2586 2587 2565 +f 2558 2556 2520 +f 2582 2558 2584 +f 2588 2589 2587 +f 2589 2558 2587 +f 2584 2558 2589 +f 2587 2558 2565 +f 2590 2584 2589 +f 2454 2460 2461 +f 2459 2441 2460 +f 2541 2542 2557 +f 2557 2556 2441 +f 2441 2556 2460 +f 2440 2442 2441 +f 2441 2442 2557 +f 2436 2557 2442 +f 2437 2436 2442 +f 2525 2527 2548 +f 2548 2527 2526 +f 2551 2591 2525 +f 2552 2591 2551 +f 2525 2591 2552 +f 2519 2552 2550 +f 2552 2519 2525 +f 2460 2556 2461 +f 2461 2556 2451 +f 2449 2461 2451 +f 2585 2582 2462 +f 2456 2581 2455 +f 2451 2456 2450 +f 2581 2585 2475 +f 2460 2582 2462 +f 2480 2462 2582 +f 2478 2477 2584 +f 2479 2478 2590 +f 2462 2475 2585 +f 2477 2480 2583 +f 2475 2455 2581 +f 2481 2462 2587 +f 2583 2584 2477 +f 2588 2587 2481 +f 2582 2583 2480 +f 2584 2590 2478 +f 2589 2588 2484 +f 2590 2589 2479 +f 2462 2582 2587 +f 2592 2500 2593 +f 2502 2593 2500 +f 2593 2502 2594 +f 2594 2502 2504 +f 2325 2326 2594 +f 2594 2504 2325 +f 2595 2592 2596 +f 2593 2596 2592 +f 2592 2595 2597 +f 2598 2597 2595 +f 2599 2594 2326 +f 2596 2593 2599 +f 2599 2593 2594 +f 2522 2600 2523 +f 2600 2522 2500 +f 2523 2601 2602 +f 2523 2602 2603 +f 2601 2523 2600 +f 2600 2597 2601 +f 2500 2592 2600 +f 2597 2598 2604 +f 2605 2604 2598 +f 2604 2606 2607 +f 2601 2604 2607 +f 2604 2601 2597 +f 2601 2607 2602 +f 2597 2600 2592 +f 2608 2595 2609 +f 2609 2596 2610 +f 2610 2596 2599 +f 2610 2599 2330 +f 2326 2330 2599 +f 2596 2609 2595 +f 2611 2608 2612 +f 2613 2610 2329 +f 2330 2329 2610 +f 2612 2609 2613 +f 2613 2609 2610 +f 2609 2612 2608 +f 2598 2614 2605 +f 2615 2605 2614 +f 2605 2616 2606 +f 2595 2608 2598 +f 2614 2598 2608 +f 2604 2605 2606 +f 2605 2615 2616 +f 2608 2611 2614 +f 2617 2614 2611 +f 2614 2617 2615 +f 2618 2615 2617 +f 2615 2619 2616 +f 2615 2618 2619 +f 2469 2620 2467 +f 2291 2290 2621 +f 2467 2621 2290 +f 2620 2469 2465 +f 2621 2467 2620 +f 2621 2622 2291 +f 2620 2623 2621 +f 2465 2624 2620 +f 2622 2621 2623 +f 2334 2291 2622 +f 2624 2465 2464 +f 2623 2620 2624 +f 2625 2613 2335 +f 2329 2335 2613 +f 2625 2612 2613 +f 2626 2627 2628 +f 2628 2625 2337 +f 2629 2626 2628 +f 2335 2337 2625 +f 2630 2631 2632 +f 2626 2632 2631 +f 2631 2630 2633 +f 2633 2634 2635 +f 2634 2633 2630 +f 2636 2635 2634 +f 2635 2636 2637 +f 2617 2638 2618 +f 2639 2618 2638 +f 2618 2639 2640 +f 2618 2640 2619 +f 2638 2617 2641 +f 2631 2641 2626 +f 2627 2626 2641 +f 2641 2611 2627 +f 2612 2627 2611 +f 2628 2627 2625 +f 2611 2641 2617 +f 2627 2612 2625 +f 2635 2639 2633 +f 2635 2637 2642 +f 2639 2635 2642 +f 2639 2642 2640 +f 2633 2638 2631 +f 2641 2631 2638 +f 2638 2633 2639 +f 2623 2624 2643 +f 2644 2622 2645 +f 2643 2624 2464 +f 2622 2623 2645 +f 2336 2334 2644 +f 2334 2622 2644 +f 2645 2623 2643 +f 2629 2336 2643 +f 2643 2646 2629 +f 2629 2628 2336 +f 2644 2645 2643 +f 2632 2626 2629 +f 2337 2336 2628 +f 2336 2644 2643 +f 2586 2565 2482 +f 2485 2563 2562 +f 2565 2564 2491 +f 2587 2586 2483 +f 2564 2563 2485 +f 2587 2559 2481 +f 2483 2481 2587 +f 2485 2491 2564 +f 2482 2483 2586 +f 2481 2484 2588 +f 2562 2490 2485 +f 2491 2482 2565 +f 2484 2479 2589 +f 2559 2487 2481 +f 2570 2566 2647 +f 2560 2561 2489 +f 2561 2570 2648 +f 2562 2559 2487 +f 2559 2560 2488 +f 2487 2559 2567 +f 2489 2488 2560 +f 2648 2489 2561 +f 2649 2647 2566 +f 2487 2490 2562 +f 2650 2649 2569 +f 2488 2487 2559 +f 2647 2648 2570 +f 2650 2487 2567 +f 2651 2548 2526 +f 2544 2543 2652 +f 2547 2548 2651 +f 2545 2547 2653 +f 2543 2545 2654 +f 2568 2544 2655 +f 2569 2567 2650 +f 2566 2569 2649 +f 2567 2548 2650 +f 2567 2568 2656 +f 2655 2656 2568 +f 2654 2652 2543 +f 2656 2650 2567 +f 2652 2655 2544 +f 2653 2654 2545 +f 2651 2653 2547 +f 2548 2651 2650 +f 2649 2650 2647 +f 2655 2486 2650 +f 2648 2647 2489 +f 2650 2486 2647 +f 2647 2486 2489 +f 2489 2486 2487 +f 2486 2657 2463 +f 2464 2463 2657 +f 2653 2651 2654 +f 2656 2655 2650 +f 2651 2657 2654 +f 2654 2657 2655 +f 2657 2486 2655 +f 2652 2654 2655 +f 2526 2658 2651 +f 2658 2659 2651 +f 2651 2659 2660 +f 2464 2657 2646 +f 2651 2661 2657 +f 2662 2658 2526 +f 2523 2603 2663 +f 2663 2524 2523 +f 2664 2662 2663 +f 2524 2662 2526 +f 2524 2663 2662 +f 2662 2636 2658 +f 2662 2664 2637 +f 2665 2660 2629 +f 2658 2634 2659 +f 2646 2643 2464 +f 2659 2630 2660 +f 2665 2629 2646 +f 2661 2666 2657 +f 2657 2666 2646 +f 2660 2661 2651 +f 2632 2660 2630 +f 2637 2636 2662 +f 2634 2658 2636 +f 2630 2659 2634 +f 2632 2629 2660 +f 2660 2667 2668 +f 2667 2665 2646 +f 2646 2666 2667 +f 2666 2661 2668 +f 2668 2661 2660 +f 2665 2667 2660 +f 2668 2667 2666 +f 2669 2670 2671 +f 2670 2672 2671 +f 2671 2672 2673 +f 2672 2674 2675 +f 2674 2672 2670 +f 2676 2677 2678 +f 2679 2677 2676 +f 2676 2675 2679 +f 2675 2676 2672 +f 2672 2676 2673 +f 2673 2676 2678 +f 2680 2681 2682 +f 2682 2674 2680 +f 2674 2683 2680 +f 2670 2683 2674 +f 2682 2684 2685 +f 2675 2685 2686 +f 2675 2674 2682 +f 2685 2675 2682 +f 2687 2685 2684 +f 2688 2684 2689 +f 2681 2689 2684 +f 2690 2687 2688 +f 2684 2688 2687 +f 2691 2692 2690 +f 2687 2690 2692 +f 2692 2691 2693 +f 2684 2682 2681 +f 2686 2694 2695 +f 2692 2686 2687 +f 2685 2687 2686 +f 2693 2694 2692 +f 2686 2692 2694 +f 2694 2693 2696 +f 2679 2675 2686 +f 2679 2686 2695 +f 2678 2677 2697 +f 2677 2698 2697 +f 2695 2698 2677 +f 2677 2679 2695 +f 2699 2700 2698 +f 2698 2695 2699 +f 2697 2698 2701 +f 2698 2700 2701 +f 2695 2694 2699 +f 2694 2696 2699 +f 2702 2699 2703 +f 2699 2696 2703 +f 2701 2700 2704 +f 2700 2705 2704 +f 2702 2705 2700 +f 2700 2699 2702 +f 2706 2693 2691 +f 2707 2691 2708 +f 2690 2708 2691 +f 2691 2707 2706 +f 2709 2688 2710 +f 2689 2710 2688 +f 2708 2690 2709 +f 2688 2709 2690 +f 2710 2711 2709 +f 2712 2713 2714 +f 2714 2715 2716 +f 2715 2714 2713 +f 2714 2717 2712 +f 2718 2712 2717 +f 2716 2719 2714 +f 2720 2721 2722 +f 2723 2724 2725 +f 2722 2725 2724 +f 2724 2726 2722 +f 2719 2721 2717 +f 2720 2717 2721 +f 2717 2714 2719 +f 2726 2720 2722 +f 2704 2705 2723 +f 2705 2724 2723 +f 2726 2724 2705 +f 2705 2702 2726 +f 2702 2703 2726 +f 2703 2720 2726 +f 2727 2696 2693 +f 2718 2703 2727 +f 2696 2727 2703 +f 2717 2720 2718 +f 2703 2718 2720 +f 2728 2727 2706 +f 2693 2706 2727 +f 2712 2718 2728 +f 2727 2728 2718 +f 2729 2706 2707 +f 2729 2730 2731 +f 2731 2728 2729 +f 2706 2729 2728 +f 2728 2731 2712 +f 2732 2713 2733 +f 2731 2733 2713 +f 2734 2715 2732 +f 2713 2732 2715 +f 2713 2712 2731 +f 2735 2736 2737 +f 2734 2735 2738 +f 2737 2739 2740 +f 2735 2737 2740 +f 2735 2734 2736 +f 2732 2736 2734 +f 2738 2741 2734 +f 2740 2738 2735 +f 2715 2734 2741 +f 2741 2716 2715 +f 2733 2731 2730 +f 2730 2729 2742 +f 2743 2733 2744 +f 2730 2744 2733 +f 2733 2743 2732 +f 2736 2743 2745 +f 2736 2745 2737 +f 2743 2744 2746 +f 2743 2746 2745 +f 2736 2732 2743 +f 2747 2748 2749 +f 2744 2750 2746 +f 2747 2749 2750 +f 2747 2742 2748 +f 2751 2748 2742 +f 2744 2730 2747 +f 2742 2747 2730 +f 2744 2747 2750 +f 2752 2753 2751 +f 2711 2754 2752 +f 2751 2708 2752 +f 2709 2752 2708 +f 2742 2707 2751 +f 2708 2751 2707 +f 2707 2742 2729 +f 2752 2709 2711 +f 2753 2752 2754 +f 2748 2755 2749 +f 2756 2757 2754 +f 2757 2753 2754 +f 2748 2753 2755 +f 2753 2757 2755 +f 2748 2751 2753 +f 2758 2759 2760 +f 2683 2670 2761 +f 2761 2760 2683 +f 2759 2758 2762 +f 2762 2763 2759 +f 2760 2761 2758 +f 2761 2670 2669 +f 2762 2758 2764 +f 2758 2765 2764 +f 2758 2761 2765 +f 2761 2766 2765 +f 2766 2761 2669 +f 2765 2766 2764 +f 2766 2669 2764 +f 2764 2669 2767 +f 2768 2767 2671 +f 2669 2671 2767 +f 2769 2770 2771 +f 2771 2770 2768 +f 2772 2773 2769 +f 2770 2769 2773 +f 2771 2768 2673 +f 2671 2673 2768 +f 2770 2774 2768 +f 2775 2776 2777 +f 2773 2778 2770 +f 2775 2777 2779 +f 2776 2775 2780 +f 2781 2780 2775 +f 2778 2773 2781 +f 2780 2781 2773 +f 2782 2769 2771 +f 2772 2783 2784 +f 2783 2772 2785 +f 2782 2771 2678 +f 2673 2678 2771 +f 2785 2769 2782 +f 2769 2785 2772 +f 2786 2785 2787 +f 2787 2785 2782 +f 2788 2783 2786 +f 2787 2782 2697 +f 2678 2697 2782 +f 2785 2786 2783 +f 2789 2776 2784 +f 2776 2789 2790 +f 2773 2772 2780 +f 2784 2780 2772 +f 2780 2784 2776 +f 2776 2790 2777 +f 2784 2791 2789 +f 2789 2792 2790 +f 2791 2784 2783 +f 2793 2789 2791 +f 2789 2793 2792 +f 2791 2794 2793 +f 2795 2793 2794 +f 2793 2795 2796 +f 2793 2796 2792 +f 2794 2791 2788 +f 2783 2788 2791 +f 2759 2797 2798 +f 2798 2680 2760 +f 2680 2683 2760 +f 2799 2797 2763 +f 2797 2759 2763 +f 2760 2759 2798 +f 2681 2680 2800 +f 2798 2800 2680 +f 2800 2798 2801 +f 2797 2801 2798 +f 2801 2797 2802 +f 2799 2802 2797 +f 2802 2799 2803 +f 2774 2770 2778 +f 2768 2774 2767 +f 2762 2764 2804 +f 2767 2805 2764 +f 2799 2763 2806 +f 2763 2762 2804 +f 2804 2806 2763 +f 2807 2808 2809 +f 2809 2810 2807 +f 2806 2804 2808 +f 2808 2807 2806 +f 2810 2809 2811 +f 2812 2813 2814 +f 2812 2815 2811 +f 2813 2816 2805 +f 2805 2814 2813 +f 2814 2815 2812 +f 2814 2805 2811 +f 2811 2815 2814 +f 2816 2813 2808 +f 2804 2816 2808 +f 2812 2811 2809 +f 2808 2813 2809 +f 2813 2812 2809 +f 2811 2774 2778 +f 2767 2774 2805 +f 2811 2805 2774 +f 2811 2778 2817 +f 2764 2816 2804 +f 2816 2764 2805 +f 2778 2818 2817 +f 2781 2819 2818 +f 2781 2818 2778 +f 2775 2819 2781 +f 2775 2779 2820 +f 2820 2819 2775 +f 2819 2821 2818 +f 2821 2822 2818 +f 2819 2820 2821 +f 2823 2821 2820 +f 2810 2822 2824 +f 2825 2810 2824 +f 2818 2810 2817 +f 2817 2810 2811 +f 2810 2818 2822 +f 2826 2827 2828 +f 2827 2829 2830 +f 2827 2830 2828 +f 2828 2830 2831 +f 2832 2828 2831 +f 2833 2831 2834 +f 2803 2835 2830 +f 2835 2803 2799 +f 2831 2830 2834 +f 2836 2837 2838 +f 2838 2807 2836 +f 2825 2836 2810 +f 2835 2799 2806 +f 2807 2838 2835 +f 2806 2807 2835 +f 2807 2810 2836 +f 2839 2834 2840 +f 2838 2830 2835 +f 2841 2840 2837 +f 2837 2836 2842 +f 2840 2834 2838 +f 2834 2830 2838 +f 2837 2840 2838 +f 2843 2844 2837 +f 2824 2845 2825 +f 2825 2845 2846 +f 2846 2843 2842 +f 2847 2844 2837 +f 2841 2837 2844 +f 2839 2840 2848 +f 2846 2836 2825 +f 2840 2841 2849 +f 2837 2842 2843 +f 2842 2836 2846 +f 2810 2847 2837 +f 2837 2844 2831 +f 2850 2786 2787 +f 2850 2787 2701 +f 2697 2701 2787 +f 2851 2788 2852 +f 2852 2786 2850 +f 2786 2852 2788 +f 2851 2853 2854 +f 2855 2854 2853 +f 2856 2850 2704 +f 2701 2704 2850 +f 2857 2852 2856 +f 2856 2852 2850 +f 2853 2851 2857 +f 2852 2857 2851 +f 2788 2851 2794 +f 2794 2854 2795 +f 2854 2794 2851 +f 2795 2858 2796 +f 2859 2795 2854 +f 2859 2860 2858 +f 2795 2859 2858 +f 2854 2855 2859 +f 2861 2862 2863 +f 2863 2855 2864 +f 2862 2859 2855 +f 2862 2861 2865 +f 2862 2865 2860 +f 2859 2862 2860 +f 2853 2864 2855 +f 2855 2863 2862 +f 2866 2867 2826 +f 2868 2869 2832 +f 2870 2871 2829 +f 2872 2870 2873 +f 2867 2872 2827 +f 2869 2866 2828 +f 2848 2874 2839 +f 2874 2875 2834 +f 2876 2868 2831 +f 2875 2876 2833 +f 2844 2849 2841 +f 2849 2848 2840 +f 2844 2868 2831 +f 2828 2832 2869 +f 2834 2839 2874 +f 2831 2833 2876 +f 2833 2834 2875 +f 2832 2831 2868 +f 2877 2878 2879 +f 2880 2877 2881 +f 2882 2883 2884 +f 2885 2880 2886 +f 2887 2888 2889 +f 2883 2887 2890 +f 2878 2882 2891 +f 2887 2890 2892 +f 2827 2826 2867 +f 2873 2827 2872 +f 2888 2829 2871 +f 2829 2873 2870 +f 2826 2828 2866 +f 2831 2868 2887 +f 2879 2881 2877 +f 2889 2890 2887 +f 2891 2879 2878 +f 2884 2891 2882 +f 2890 2884 2883 +f 2871 2889 2888 +f 2868 2890 2887 +f 2893 2894 2895 +f 2895 2896 2893 +f 2710 2689 2897 +f 2898 2897 2689 +f 2897 2898 2899 +f 2900 2899 2898 +f 2899 2900 2896 +f 2901 2896 2900 +f 2896 2895 2899 +f 2902 2893 2896 +f 2896 2901 2902 +f 2800 2898 2681 +f 2898 2800 2900 +f 2801 2900 2800 +f 2900 2801 2901 +f 2802 2901 2801 +f 2689 2681 2898 +f 2901 2802 2903 +f 2803 2903 2802 +f 2873 2829 2827 +f 2830 2904 2803 +f 2829 2887 2830 +f 2887 2904 2830 +f 2903 2803 2904 +f 2883 2882 2887 +f 2888 2887 2829 +f 2878 2877 2882 +f 2905 2892 2885 +f 2892 2904 2885 +f 2885 2904 2877 +f 2877 2904 2882 +f 2882 2904 2887 +f 2880 2885 2877 +f 2904 2906 2903 +f 2903 2902 2901 +f 2892 2907 2904 +f 2908 2909 2907 +f 2902 2903 2906 +f 2910 2907 2892 +f 2909 2906 2907 +f 2907 2906 2904 +f 2909 2911 2906 +f 2906 2911 2912 +f 2913 2912 2911 +f 2914 2911 2909 +f 2912 2915 2906 +f 2893 2902 2916 +f 2906 2916 2902 +f 2915 2917 2906 +f 2917 2916 2906 +f 2918 2919 2917 +f 2920 2917 2915 +f 2921 2915 2912 +f 2738 2740 2922 +f 2923 2924 2925 +f 2922 2925 2924 +f 2924 2923 2926 +f 2922 2927 2738 +f 2741 2738 2927 +f 2924 2928 2922 +f 2927 2922 2928 +f 2926 2929 2924 +f 2928 2924 2929 +f 2930 2931 2932 +f 2932 2933 2934 +f 2737 2745 2934 +f 2934 2935 2737 +f 2739 2737 2935 +f 2936 2926 2923 +f 2925 2922 2740 +f 2740 2739 2925 +f 2925 2937 2923 +f 2923 2938 2936 +f 2939 2739 2935 +f 2937 2938 2923 +f 2739 2937 2925 +f 2938 2939 2936 +f 2938 2937 2939 +f 2937 2739 2939 +f 2940 2941 2942 +f 2943 2944 2945 +f 2942 2945 2944 +f 2719 2716 2940 +f 2940 2946 2719 +f 2941 2927 2945 +f 2945 2928 2943 +f 2941 2940 2716 +f 2945 2942 2941 +f 2716 2741 2941 +f 2926 2936 2947 +f 2927 2941 2741 +f 2929 2943 2928 +f 2928 2945 2927 +f 2948 2939 2949 +f 2947 2950 2926 +f 2929 2926 2950 +f 2951 2947 2936 +f 2939 2948 2951 +f 2934 2933 2935 +f 2933 2932 2931 +f 2936 2939 2951 +f 2931 2930 2952 +f 2953 2954 2952 +f 2955 2933 2931 +f 2952 2956 2931 +f 2935 2933 2949 +f 2935 2949 2939 +f 2955 2931 2957 +f 2955 2949 2933 +f 2931 2956 2957 +f 2952 2954 2956 +f 2958 2857 2856 +f 2704 2723 2856 +f 2857 2959 2853 +f 2958 2856 2723 +f 2959 2857 2958 +f 2864 2853 2959 +f 2960 2959 2958 +f 2864 2961 2863 +f 2961 2864 2962 +f 2959 2962 2864 +f 2960 2725 2963 +f 2960 2958 2725 +f 2723 2725 2958 +f 2964 2965 2725 +f 2725 2722 2964 +f 2966 2967 2965 +f 2965 2964 2966 +f 2968 2963 2967 +f 2967 2966 2968 +f 2725 2965 2963 +f 2965 2967 2963 +f 2968 2966 2969 +f 2721 2946 2964 +f 2964 2946 2966 +f 2946 2970 2966 +f 2966 2970 2969 +f 2722 2721 2964 +f 2970 2942 2969 +f 2944 2969 2942 +f 2944 2943 2971 +f 2943 2929 2972 +f 2721 2719 2946 +f 2942 2970 2940 +f 2946 2940 2970 +f 2973 2974 2975 +f 2947 2951 2976 +f 2974 2976 2975 +f 2977 2975 2978 +f 2975 2979 2978 +f 2950 2972 2929 +f 2979 2976 2951 +f 2976 2979 2975 +f 2979 2980 2978 +f 2980 2981 2978 +f 2978 2981 2955 +f 2951 2948 2979 +f 2981 2980 2982 +f 2980 2948 2949 +f 2948 2980 2979 +f 2983 2863 2961 +f 2861 2984 2985 +f 2863 2983 2861 +f 2984 2861 2983 +f 2861 2985 2865 +f 2971 2986 2944 +f 2972 2971 2943 +f 2987 2988 2972 +f 2950 2947 2989 +f 2989 2976 2990 +f 2991 2990 2992 +f 2993 2994 2990 +f 2995 2996 2994 +f 2996 2989 2994 +f 2994 2989 2990 +f 2992 2976 2997 +f 2997 2976 2998 +f 2974 2998 2976 +f 2990 2976 2992 +f 2976 2989 2947 +f 2999 2998 2974 +f 3000 2997 2998 +f 3001 2992 2997 +f 2962 3002 2961 +f 2962 2960 3002 +f 2968 2969 3003 +f 2969 2944 2986 +f 2962 2959 2960 +f 2963 2968 3003 +f 2963 3004 2960 +f 2824 2961 3002 +f 3005 3002 2960 +f 3003 3004 2963 +f 2961 2824 2983 +f 3005 2960 3004 +f 2969 2986 3003 +f 2983 2822 2984 +f 2821 2984 2822 +f 2985 2984 2821 +f 2821 2823 2985 +f 2822 2983 2824 +f 2824 3002 2847 +f 2847 3002 3006 +f 2847 2845 2824 +f 3005 3007 3002 +f 3002 3007 3008 +f 3008 3006 3002 +f 3008 3007 3009 +f 3009 3006 3008 +f 3007 3005 3004 +f 3010 3009 3004 +f 3004 3009 3007 +f 3004 3003 3010 +f 2847 3006 3009 +f 3009 3010 2847 +f 3011 2846 2847 +f 2847 2846 2845 +f 3003 2986 3010 +f 2986 3011 3010 +f 3010 3011 2847 +f 2986 2971 3011 +f 2848 2849 2844 +f 2844 2843 2846 +f 3011 2875 2848 +f 2848 2844 3011 +f 3011 2844 2846 +f 2868 2875 2988 +f 2988 2875 3011 +f 2868 2876 2875 +f 2869 2868 2866 +f 2988 2866 2868 +f 2988 3011 2971 +f 2875 2874 2848 +f 2881 2987 3012 +f 2886 2881 3012 +f 2987 2989 3013 +f 3014 3013 3015 +f 2972 2950 2987 +f 3016 3012 3013 +f 3013 3012 2987 +f 3017 3018 2996 +f 2989 2987 2950 +f 3013 2989 3015 +f 3019 3020 3018 +f 3020 2989 3018 +f 3015 2989 3020 +f 3018 2989 2996 +f 3021 3015 3020 +f 2884 2890 2891 +f 2889 2871 2890 +f 2971 2972 2988 +f 2988 2987 2871 +f 2871 2987 2890 +f 2870 2872 2871 +f 2871 2872 2988 +f 2866 2988 2872 +f 2867 2866 2872 +f 2955 2957 2978 +f 2978 2957 2956 +f 2981 3022 2955 +f 2982 3022 2981 +f 2955 3022 2982 +f 2949 2982 2980 +f 2982 2949 2955 +f 2890 2987 2891 +f 2891 2987 2881 +f 2879 2891 2881 +f 3016 3013 2892 +f 2886 3012 2885 +f 2881 2886 2880 +f 3012 3016 2905 +f 2890 3013 2892 +f 2910 2892 3013 +f 2908 2907 3015 +f 2909 2908 3021 +f 2892 2905 3016 +f 2907 2910 3014 +f 2905 2885 3012 +f 2911 2892 3018 +f 3014 3015 2907 +f 3019 3018 2911 +f 3013 3014 2910 +f 3015 3021 2908 +f 3020 3019 2914 +f 3021 3020 2909 +f 2892 3013 3018 +f 3023 2930 3024 +f 2932 3024 2930 +f 3024 2932 3025 +f 3025 2932 2934 +f 2745 2746 3025 +f 3025 2934 2745 +f 3026 3023 3027 +f 3024 3027 3023 +f 3023 3026 3028 +f 3029 3028 3026 +f 3030 3025 2746 +f 3027 3024 3030 +f 3030 3024 3025 +f 2952 3031 2953 +f 3031 2952 2930 +f 2953 3032 3033 +f 2953 3033 3034 +f 3032 2953 3031 +f 3031 3028 3032 +f 2930 3023 3031 +f 3028 3029 3035 +f 3036 3035 3029 +f 3035 3037 3038 +f 3032 3035 3038 +f 3035 3032 3028 +f 3032 3038 3033 +f 3028 3031 3023 +f 3039 3026 3040 +f 3040 3027 3041 +f 3041 3027 3030 +f 3041 3030 2750 +f 2746 2750 3030 +f 3027 3040 3026 +f 3042 3039 3043 +f 3044 3041 2749 +f 2750 2749 3041 +f 3043 3040 3044 +f 3044 3040 3041 +f 3040 3043 3039 +f 3029 3045 3036 +f 3046 3036 3045 +f 3036 3047 3037 +f 3026 3039 3029 +f 3045 3029 3039 +f 3035 3036 3037 +f 3036 3046 3047 +f 3039 3042 3045 +f 3048 3045 3042 +f 3045 3048 3046 +f 3049 3046 3048 +f 3046 3050 3047 +f 3046 3049 3050 +f 2899 3051 2897 +f 2711 2710 3052 +f 2897 3052 2710 +f 3051 2899 2895 +f 3052 2897 3051 +f 3052 3053 2711 +f 3051 3054 3052 +f 2895 3055 3051 +f 3053 3052 3054 +f 2754 2711 3053 +f 3055 2895 2894 +f 3054 3051 3055 +f 3056 3044 2755 +f 2749 2755 3044 +f 3056 3043 3044 +f 3057 3058 3059 +f 3059 3056 2757 +f 3060 3057 3059 +f 2755 2757 3056 +f 3061 3062 3063 +f 3057 3063 3062 +f 3062 3061 3064 +f 3064 3065 3066 +f 3065 3064 3061 +f 3067 3066 3065 +f 3066 3067 3068 +f 3048 3069 3049 +f 3070 3049 3069 +f 3049 3070 3071 +f 3049 3071 3050 +f 3069 3048 3072 +f 3062 3072 3057 +f 3058 3057 3072 +f 3072 3042 3058 +f 3043 3058 3042 +f 3059 3058 3056 +f 3042 3072 3048 +f 3058 3043 3056 +f 3066 3070 3064 +f 3066 3068 3073 +f 3070 3066 3073 +f 3070 3073 3071 +f 3064 3069 3062 +f 3072 3062 3069 +f 3069 3064 3070 +f 3054 3055 3074 +f 3075 3053 3076 +f 3074 3055 2894 +f 3053 3054 3076 +f 2756 2754 3075 +f 2754 3053 3075 +f 3076 3054 3074 +f 3060 2756 3074 +f 3074 3077 3060 +f 3060 3059 2756 +f 3075 3076 3074 +f 3063 3057 3060 +f 2757 2756 3059 +f 2756 3075 3074 +f 3017 2996 2912 +f 2915 2994 2993 +f 2996 2995 2921 +f 3018 3017 2913 +f 2995 2994 2915 +f 3018 2990 2911 +f 2913 2911 3018 +f 2915 2921 2995 +f 2912 2913 3017 +f 2911 2914 3019 +f 2993 2920 2915 +f 2921 2912 2996 +f 2914 2909 3020 +f 2990 2917 2911 +f 3001 2997 3078 +f 2991 2992 2919 +f 2992 3001 3079 +f 2993 2990 2917 +f 2990 2991 2918 +f 2917 2990 2998 +f 2919 2918 2991 +f 3079 2919 2992 +f 3080 3078 2997 +f 2917 2920 2993 +f 3081 3080 3000 +f 2918 2917 2990 +f 3078 3079 3001 +f 3081 2917 2998 +f 3082 2978 2956 +f 2974 2973 3083 +f 2977 2978 3082 +f 2975 2977 3084 +f 2973 2975 3085 +f 2999 2974 3086 +f 3000 2998 3081 +f 2997 3000 3080 +f 2998 2978 3081 +f 2998 2999 3087 +f 3086 3087 2999 +f 3085 3083 2973 +f 3087 3081 2998 +f 3083 3086 2974 +f 3084 3085 2975 +f 3082 3084 2977 +f 2978 3082 3081 +f 3080 3081 3078 +f 3086 2916 3081 +f 3079 3078 2919 +f 3081 2916 3078 +f 3078 2916 2919 +f 2919 2916 2917 +f 2916 3088 2893 +f 2894 2893 3088 +f 3084 3082 3085 +f 3087 3086 3081 +f 3082 3088 3085 +f 3085 3088 3086 +f 3088 2916 3086 +f 3083 3085 3086 +f 2956 3089 3082 +f 3089 3090 3082 +f 3082 3090 3091 +f 2894 3088 3077 +f 3082 3092 3088 +f 3093 3089 2956 +f 2953 3034 3094 +f 3094 2954 2953 +f 3095 3093 3094 +f 2954 3093 2956 +f 2954 3094 3093 +f 3093 3067 3089 +f 3093 3095 3068 +f 3096 3091 3060 +f 3089 3065 3090 +f 3077 3074 2894 +f 3090 3061 3091 +f 3096 3060 3077 +f 3092 3097 3088 +f 3088 3097 3077 +f 3091 3092 3082 +f 3063 3091 3061 +f 3068 3067 3093 +f 3065 3089 3067 +f 3061 3090 3065 +f 3063 3060 3091 +f 3091 3098 3099 +f 3098 3096 3077 +f 3077 3097 3098 +f 3097 3092 3099 +f 3099 3092 3091 +f 3096 3098 3091 +f 3099 3098 3097 +f 3100 3101 3102 +f 3100 3103 3101 +f 3104 3103 3100 +f 3104 3105 3103 +f 3106 3105 3104 +f 3106 3107 3105 +f 3101 3103 3108 +f 3109 3110 3107 +f 3110 3111 3105 +f 3103 3105 3111 +f 3111 3108 3103 +f 3108 3112 3101 +f 3108 3111 3113 +f 3113 3114 3108 +f 3112 3108 3114 +f 3114 3115 3112 +f 3116 3117 3113 +f 3118 3113 3117 +f 3118 3114 3113 +f 3115 3118 3119 +f 3115 3114 3118 +f 3111 3110 3120 +f 3120 3113 3111 +f 3121 3122 3109 +f 3110 3109 3122 +f 3122 3120 3110 +f 3120 3123 3116 +f 3120 3122 3123 +f 3116 3113 3120 +f 3122 3121 3124 +f 3122 3124 3123 +f 3125 3126 3127 +f 3107 3125 3109 +f 3128 3125 3107 +f 3128 3107 3106 +f 3105 3107 3110 +f 3127 3129 3130 +f 3131 3129 3132 +f 3126 3132 3129 +f 3129 3127 3126 +f 3109 3127 3121 +f 3127 3109 3125 +f 3133 3130 3129 +f 3130 3121 3127 +f 3130 3134 3135 +f 3121 3135 3124 +f 3121 3130 3135 +f 3132 3136 3131 +f 3136 3137 3138 +f 3138 3131 3136 +f 3139 3132 3140 +f 3139 3136 3132 +f 3137 3136 3139 +f 3131 3138 3141 +f 3130 3133 3134 +f 3129 3131 3133 +f 3134 3133 3141 +f 3141 3133 3131 +f 3142 3143 3144 +f 3143 3142 3145 +f 3145 3146 3143 +f 3147 3148 3146 +f 3149 3150 3148 +f 3151 3152 3150 +f 3153 3126 3125 +f 3140 3126 3153 +f 3140 3132 3126 +f 3154 3155 3152 +f 3156 3157 3155 +f 3153 3125 3128 +f 3158 3159 3160 +f 3161 3159 3158 +f 3161 3162 3159 +f 3146 3145 3147 +f 3148 3147 3149 +f 3158 3160 3163 +f 3150 3149 3151 +f 3152 3151 3154 +f 3157 3156 3164 +f 3164 3165 3157 +f 3165 3164 3166 +f 3155 3154 3156 +f 3167 3160 3168 +f 3168 3169 3167 +f 3170 3167 3169 +f 3163 3167 3171 +f 3163 3160 3167 +f 3160 3159 3172 +f 3173 3174 3175 +f 3175 3176 3173 +f 3177 3173 3176 +f 3178 3176 3175 +f 3179 3176 3178 +f 3180 3181 3174 +f 3174 3173 3180 +f 3162 3180 3173 +f 3181 3180 3182 +f 3182 3162 3161 +f 3182 3180 3162 +f 3176 3179 3177 +f 3179 3178 3183 +f 3179 3184 3172 +f 3184 3183 3185 +f 3184 3179 3183 +f 3172 3168 3160 +f 3173 3177 3162 +f 3159 3162 3177 +f 3172 3177 3179 +f 3177 3172 3159 +f 3168 3172 3184 +f 3186 3185 3187 +f 3186 3184 3185 +f 3188 3187 3189 +f 3188 3186 3187 +f 3189 3190 3188 +f 3188 3190 3191 +f 3186 3188 3169 +f 3191 3169 3188 +f 3184 3186 3168 +f 3169 3168 3186 +f 3189 3192 3190 +f 3193 3190 3192 +f 3193 3194 3190 +f 3195 3193 3196 +f 3195 3194 3193 +f 3197 3191 3190 +f 3194 3195 3198 +f 3190 3194 3197 +f 3198 3197 3194 +f 3171 3167 3170 +f 3199 3200 3197 +f 3201 3199 3202 +f 3203 3170 3200 +f 3201 3200 3199 +f 3203 3200 3201 +f 3171 3170 3203 +f 3197 3198 3199 +f 3169 3191 3170 +f 3200 3170 3191 +f 3191 3197 3200 +f 3204 3205 3206 +f 3204 3207 3205 +f 3204 3102 3207 +f 3205 3207 3208 +f 3102 3208 3207 +f 3209 3102 3204 +f 3209 3100 3102 +f 3210 3211 3212 +f 3213 3214 3215 +f 3209 3214 3213 +f 3215 3216 3210 +f 3216 3215 3214 +f 3217 3212 3211 +f 3211 3210 3216 +f 3218 3106 3104 +f 3104 3213 3218 +f 3213 3104 3100 +f 3218 3215 3219 +f 3213 3215 3218 +f 3210 3219 3215 +f 3220 3143 3146 +f 3221 3222 3220 +f 3146 3221 3220 +f 3221 3146 3148 +f 3223 3224 3225 +f 3226 3225 3227 +f 3225 3228 3229 +f 3229 3227 3225 +f 3225 3226 3223 +f 3230 3231 3144 +f 3230 3232 3231 +f 3233 3229 3228 +f 3230 3144 3233 +f 3144 3234 3233 +f 3234 3144 3143 +f 3235 3236 3237 +f 3234 3229 3233 +f 3100 3209 3213 +f 3143 3220 3234 +f 3212 3238 3210 +f 3239 3240 3217 +f 3239 3235 3237 +f 3212 3217 3240 +f 3239 3241 3240 +f 3235 3239 3217 +f 3224 3223 3242 +f 3228 3225 3224 +f 3223 3243 3237 +f 3237 3242 3223 +f 3239 3237 3243 +f 3241 3239 3243 +f 3242 3237 3236 +f 3234 3244 3229 +f 3227 3229 3244 +f 3244 3245 3227 +f 3245 3244 3222 +f 3220 3244 3234 +f 3220 3222 3244 +f 3241 3243 3246 +f 3247 3227 3245 +f 3227 3247 3226 +f 3226 3246 3243 +f 3243 3223 3226 +f 3246 3226 3247 +f 3204 3206 3248 +f 3205 3208 3206 +f 3102 3101 3208 +f 3249 3204 3248 +f 3250 3216 3251 +f 3216 3214 3251 +f 3249 3252 3204 +f 3253 3224 3254 +f 3224 3253 3228 +f 3204 3255 3209 +f 3255 3204 3252 +f 3253 3233 3228 +f 3233 3253 3256 +f 3257 3233 3256 +f 3233 3257 3230 +f 3258 3251 3259 +f 3251 3260 3259 +f 3258 3261 3262 +f 3258 3259 3261 +f 3263 3261 3259 +f 3262 3252 3249 +f 3262 3261 3252 +f 3259 3260 3263 +f 3255 3252 3261 +f 3261 3263 3255 +f 3211 3264 3217 +f 3250 3265 3216 +f 3265 3264 3211 +f 3266 3236 3235 +f 3255 3214 3209 +f 3267 3235 3217 +f 3214 3255 3251 +f 3216 3265 3211 +f 3268 3256 3253 +f 3253 3269 3268 +f 3270 3268 3269 +f 3269 3271 3270 +f 3271 3269 3253 +f 3257 3256 3268 +f 3268 3270 3257 +f 3272 3236 3266 +f 3236 3272 3242 +f 3266 3273 3274 +f 3254 3242 3272 +f 3242 3254 3224 +f 3230 3275 3276 +f 3276 3232 3230 +f 3231 3277 3142 +f 3230 3278 3275 +f 3142 3144 3231 +f 3232 3276 3277 +f 3277 3231 3232 +f 3270 3279 3278 +f 3280 3281 3254 +f 3271 3282 3279 +f 3281 3282 3253 +f 3271 3253 3282 +f 3279 3270 3271 +f 3278 3257 3270 +f 3278 3230 3257 +f 3253 3254 3281 +f 3254 3272 3280 +f 3272 3266 3283 +f 3284 3285 3248 +f 3206 3208 3286 +f 3286 3284 3206 +f 3208 3101 3112 +f 3112 3286 3208 +f 3248 3206 3284 +f 3249 3248 3285 +f 3285 3287 3249 +f 3258 3262 3288 +f 3288 3289 3258 +f 3262 3249 3287 +f 3287 3288 3262 +f 3251 3258 3289 +f 3290 3265 3289 +f 3250 3289 3265 +f 3251 3289 3250 +f 3285 3284 3291 +f 3291 3292 3285 +f 3284 3286 3293 +f 3293 3291 3284 +f 3286 3112 3115 +f 3115 3293 3286 +f 3294 3295 3296 +f 3297 3298 3299 +f 3299 3300 3294 +f 3300 3299 3298 +f 3301 3298 3297 +f 3119 3301 3297 +f 3301 3119 3118 +f 3293 3115 3119 +f 3291 3293 3302 +f 3292 3291 3303 +f 3293 3119 3302 +f 3291 3302 3303 +f 3292 3303 3304 +f 3304 3302 3119 +f 3304 3303 3302 +f 3304 3119 3297 +f 3118 3305 3301 +f 3305 3118 3117 +f 3117 3306 3305 +f 3306 3117 3116 +f 3307 3308 3309 +f 3310 3307 3309 +f 3295 3294 3300 +f 3309 3296 3295 +f 3296 3309 3308 +f 3295 3311 3309 +f 3312 3310 3311 +f 3311 3295 3313 +f 3310 3309 3311 +f 3300 3313 3295 +f 3313 3300 3314 +f 3315 3316 3317 +f 3317 3318 3315 +f 3318 3317 3319 +f 3306 3317 3305 +f 3306 3319 3317 +f 3320 3319 3306 +f 3116 3320 3306 +f 3320 3116 3123 +f 3316 3315 3314 +f 3301 3316 3298 +f 3305 3316 3301 +f 3305 3317 3316 +f 3298 3314 3300 +f 3314 3298 3316 +f 3321 3312 3322 +f 3313 3322 3311 +f 3322 3313 3323 +f 3312 3311 3322 +f 3323 3314 3315 +f 3314 3323 3313 +f 3321 3322 3324 +f 3323 3324 3322 +f 3325 3321 3324 +f 3326 3315 3318 +f 3315 3326 3323 +f 3324 3323 3326 +f 3327 3325 3328 +f 3329 3318 3330 +f 3325 3324 3328 +f 3328 3326 3329 +f 3326 3328 3324 +f 3318 3329 3326 +f 3331 3332 3333 +f 3331 3333 3334 +f 3334 3289 3331 +f 3334 3290 3289 +f 3335 3332 3336 +f 3332 3335 3333 +f 3289 3288 3337 +f 3337 3338 3289 +f 3288 3287 3339 +f 3339 3337 3288 +f 3287 3285 3292 +f 3292 3339 3287 +f 3289 3338 3340 +f 3341 3294 3340 +f 3339 3304 3342 +f 3342 3297 3343 +f 3297 3341 3343 +f 3296 3340 3294 +f 3341 3297 3299 +f 3339 3292 3304 +f 3294 3341 3299 +f 3297 3342 3304 +f 3338 3337 3344 +f 3341 3338 3345 +f 3344 3339 3342 +f 3344 3337 3339 +f 3338 3344 3345 +f 3338 3341 3340 +f 3341 3346 3343 +f 3342 3343 3346 +f 3347 3346 3341 +f 3344 3346 3347 +f 3346 3344 3342 +f 3347 3345 3344 +f 3341 3345 3347 +f 3307 3336 3332 +f 3308 3331 3296 +f 3331 3308 3332 +f 3340 3296 3331 +f 3340 3331 3289 +f 3332 3308 3307 +f 3235 3267 3266 +f 3251 3255 3263 +f 3263 3260 3251 +f 3265 3348 3264 +f 3348 3267 3264 +f 3217 3264 3267 +f 3247 3349 3246 +f 3350 3241 3246 +f 3350 3246 3349 +f 3351 3350 3349 +f 3352 3351 3353 +f 3351 3352 3354 +f 3351 3354 3355 +f 3350 3351 3355 +f 3356 3219 3357 +f 3218 3219 3356 +f 3219 3210 3238 +f 3238 3357 3219 +f 3356 3128 3106 +f 3106 3218 3356 +f 3357 3238 3358 +f 3359 3357 3360 +f 3355 3361 3362 +f 3356 3357 3359 +f 3362 3363 3358 +f 3361 3355 3354 +f 3363 3362 3361 +f 3360 3358 3363 +f 3358 3360 3357 +f 3240 3364 3212 +f 3364 3240 3365 +f 3364 3358 3238 +f 3358 3364 3362 +f 3365 3362 3364 +f 3238 3212 3364 +f 3350 3355 3365 +f 3241 3350 3365 +f 3362 3365 3355 +f 3241 3365 3240 +f 3354 3366 3361 +f 3363 3367 3360 +f 3368 3361 3366 +f 3361 3368 3363 +f 3369 3367 3370 +f 3366 3371 3368 +f 3368 3372 3367 +f 3369 3360 3367 +f 3367 3363 3368 +f 3359 3360 3369 +f 3221 3373 3222 +f 3374 3373 3221 +f 3374 3375 3373 +f 3376 3375 3374 +f 3128 3356 3359 +f 3148 3374 3221 +f 3374 3148 3150 +f 3150 3376 3374 +f 3369 3140 3153 +f 3153 3359 3369 +f 3359 3153 3128 +f 3376 3377 3375 +f 3376 3150 3152 +f 3152 3378 3376 +f 3378 3152 3155 +f 3245 3379 3247 +f 3222 3380 3245 +f 3380 3222 3373 +f 3349 3247 3379 +f 3379 3245 3380 +f 3373 3381 3380 +f 3381 3373 3375 +f 3353 3379 3382 +f 3382 3380 3381 +f 3380 3382 3379 +f 3351 3349 3353 +f 3379 3353 3349 +f 3352 3353 3383 +f 3382 3383 3353 +f 3383 3382 3384 +f 3352 3385 3386 +f 3385 3387 3388 +f 3352 3386 3354 +f 3366 3354 3386 +f 3386 3389 3366 +f 3389 3386 3388 +f 3385 3388 3386 +f 3378 3390 3377 +f 3384 3381 3391 +f 3392 3377 3390 +f 3377 3392 3391 +f 3378 3377 3376 +f 3381 3384 3382 +f 3375 3391 3381 +f 3391 3375 3377 +f 3393 3391 3392 +f 3391 3393 3384 +f 3385 3352 3383 +f 3385 3383 3394 +f 3387 3385 3394 +f 3393 3395 3394 +f 3394 3384 3393 +f 3384 3394 3383 +f 3370 3372 3396 +f 3370 3367 3372 +f 3139 3370 3396 +f 3370 3139 3140 +f 3140 3369 3370 +f 3397 3398 3399 +f 3400 3399 3398 +f 3399 3400 3401 +f 3402 3403 3397 +f 3398 3397 3403 +f 3404 3405 3402 +f 3403 3402 3405 +f 3389 3403 3371 +f 3371 3366 3389 +f 3396 3405 3404 +f 3405 3371 3403 +f 3371 3405 3372 +f 3372 3368 3371 +f 3396 3372 3405 +f 3403 3389 3398 +f 3388 3398 3389 +f 3398 3388 3400 +f 3406 3401 3400 +f 3387 3406 3400 +f 3387 3400 3388 +f 3407 3124 3135 +f 3135 3408 3407 +f 3408 3135 3134 +f 3319 3330 3318 +f 3320 3409 3319 +f 3410 3409 3320 +f 3123 3410 3320 +f 3410 3123 3124 +f 3330 3319 3409 +f 3407 3411 3410 +f 3124 3407 3410 +f 3410 3411 3409 +f 3409 3412 3330 +f 3412 3409 3411 +f 3330 3413 3329 +f 3414 3329 3413 +f 3329 3414 3328 +f 3413 3330 3412 +f 3327 3328 3414 +f 3415 3327 3414 +f 3416 3417 3418 +f 3418 3413 3416 +f 3413 3418 3414 +f 3416 3412 3419 +f 3412 3416 3413 +f 3420 3415 3418 +f 3415 3414 3418 +f 3420 3418 3417 +f 3134 3421 3408 +f 3411 3419 3412 +f 3419 3411 3422 +f 3407 3422 3411 +f 3408 3422 3407 +f 3408 3423 3422 +f 3424 3422 3423 +f 3417 3416 3425 +f 3425 3419 3424 +f 3419 3425 3416 +f 3426 3420 3417 +f 3422 3424 3419 +f 3421 3423 3408 +f 3427 3428 3137 +f 3427 3429 3428 +f 3428 3429 3430 +f 3430 3431 3428 +f 3431 3138 3137 +f 3429 3427 3432 +f 3432 3430 3429 +f 3137 3428 3431 +f 3430 3432 3433 +f 3433 3434 3430 +f 3138 3431 3435 +f 3431 3430 3434 +f 3434 3435 3431 +f 3433 3141 3435 +f 3435 3141 3138 +f 3433 3435 3434 +f 3421 3134 3141 +f 3433 3436 3421 +f 3421 3141 3433 +f 3437 3432 3427 +f 3438 3433 3432 +f 3427 3137 3404 +f 3404 3439 3427 +f 3440 3406 3395 +f 3397 3441 3402 +f 3401 3290 3399 +f 3348 3401 3406 +f 3399 3442 3397 +f 3441 3404 3402 +f 3439 3404 3443 +f 3406 3440 3348 +f 3441 3397 3442 +f 3442 3399 3290 +f 3290 3401 3348 +f 3404 3441 3443 +f 3439 3443 3444 +f 3444 3445 3439 +f 3446 3447 3445 +f 3441 3447 3446 +f 3445 3444 3446 +f 3441 3444 3443 +f 3446 3444 3441 +f 3423 3436 3448 +f 3432 3437 3438 +f 3436 3423 3421 +f 3427 3439 3437 +f 3436 3433 3438 +f 3334 3333 3425 +f 3425 3333 3417 +f 3335 3426 3417 +f 3417 3333 3335 +f 3424 3423 3448 +f 3424 3334 3425 +f 3449 3424 3448 +f 3449 3334 3424 +f 3449 3442 3290 +f 3441 3442 3449 +f 3439 3445 3450 +f 3450 3437 3439 +f 3449 3290 3334 +f 3441 3448 3451 +f 3441 3449 3448 +f 3451 3447 3441 +f 3445 3447 3451 +f 3451 3450 3445 +f 3452 3453 3448 +f 3454 3452 3436 +f 3448 3436 3452 +f 3448 3453 3455 +f 3452 3454 3455 +f 3455 3453 3452 +f 3455 3454 3450 +f 3437 3450 3454 +f 3448 3455 3451 +f 3450 3451 3455 +f 3454 3438 3437 +f 3436 3438 3454 +f 3456 3457 3458 +f 3456 3165 3457 +f 3396 3137 3139 +f 3157 3459 3460 +f 3459 3157 3165 +f 3155 3460 3378 +f 3460 3155 3157 +f 3460 3390 3378 +f 3460 3461 3390 +f 3390 3462 3392 +f 3462 3390 3461 +f 3406 3387 3395 +f 3395 3393 3463 +f 3463 3392 3462 +f 3392 3463 3393 +f 3387 3394 3395 +f 3267 3348 3440 +f 3273 3464 3465 +f 3464 3466 3467 +f 3266 3267 3273 +f 3265 3290 3348 +f 3440 3273 3267 +f 3137 3396 3404 +f 3456 3468 3459 +f 3459 3461 3460 +f 3459 3165 3456 +f 3462 3464 3463 +f 3462 3461 3469 +f 3463 3273 3395 +f 3468 3456 3470 +f 3468 3461 3459 +f 3466 3462 3469 +f 3461 3468 3469 +f 3466 3464 3462 +f 3464 3273 3463 +f 3395 3273 3440 +f 3469 3471 3472 +f 3469 3468 3471 +f 3469 3472 3473 +f 3468 3470 3474 +f 3473 3472 3471 +f 3474 3471 3468 +f 3471 3474 3473 +f 3475 3181 3182 +f 3145 3476 3477 +f 3476 3145 3142 +f 3275 3277 3276 +f 3275 3142 3277 +f 3477 3478 3479 +f 3477 3480 3478 +f 3147 3477 3479 +f 3477 3147 3145 +f 3481 3482 3483 +f 3478 3484 3485 +f 3484 3478 3480 +f 3483 3486 3487 +f 3487 3485 3484 +f 3485 3487 3486 +f 3482 3488 3483 +f 3465 3489 3274 +f 3490 3491 3492 +f 3283 3280 3272 +f 3489 3492 3493 +f 3274 3283 3266 +f 3493 3274 3489 +f 3476 3480 3477 +f 3275 3494 3476 +f 3476 3142 3275 +f 3181 3475 3495 +f 3487 3274 3483 +f 3494 3275 3278 +f 3484 3480 3281 +f 3484 3283 3487 +f 3494 3480 3476 +f 3483 3274 3493 +f 3283 3274 3487 +f 3280 3484 3281 +f 3280 3283 3484 +f 3480 3494 3281 +f 3494 3278 3279 +f 3281 3496 3282 +f 3279 3497 3494 +f 3281 3494 3497 +f 3497 3279 3282 +f 3282 3496 3497 +f 3281 3497 3496 +f 3498 3499 3500 +f 3501 3502 3503 +f 3503 3498 3501 +f 3502 3500 3181 +f 3502 3501 3500 +f 3181 3500 3499 +f 3499 3174 3181 +f 3500 3501 3498 +f 3498 3503 3504 +f 3504 3505 3498 +f 3174 3499 3506 +f 3499 3498 3505 +f 3505 3506 3499 +f 3506 3175 3174 +f 3504 3506 3505 +f 3504 3175 3506 +f 3507 3178 3175 +f 3178 3507 3508 +f 3508 3509 3510 +f 3511 3512 3513 +f 3512 3511 3509 +f 3508 3514 3509 +f 3510 3509 3511 +f 3507 3514 3508 +f 3515 3509 3514 +f 3516 3517 3518 +f 3518 3512 3515 +f 3512 3518 3517 +f 3519 3520 3516 +f 3509 3515 3512 +f 3510 3185 3183 +f 3183 3508 3510 +f 3508 3183 3178 +f 3185 3510 3521 +f 3522 3513 3523 +f 3513 3522 3511 +f 3521 3511 3522 +f 3510 3511 3521 +f 3521 3187 3185 +f 3187 3521 3524 +f 3524 3522 3525 +f 3521 3522 3524 +f 3525 3523 3526 +f 3523 3525 3522 +f 3527 3528 3529 +f 3530 3529 3528 +f 3520 3530 3528 +f 3520 3528 3516 +f 3517 3516 3528 +f 3517 3513 3512 +f 3513 3517 3527 +f 3528 3527 3517 +f 3527 3523 3513 +f 3523 3527 3531 +f 3529 3531 3527 +f 3531 3529 3532 +f 3533 3532 3529 +f 3530 3533 3529 +f 3182 3534 3475 +f 3534 3182 3161 +f 3161 3535 3534 +f 3534 3536 3475 +f 3534 3537 3536 +f 3495 3538 3539 +f 3540 3539 3538 +f 3541 3542 3543 +f 3542 3541 3544 +f 3543 3545 3540 +f 3545 3543 3542 +f 3539 3540 3545 +f 3536 3546 3547 +f 3547 3548 3549 +f 3475 3538 3495 +f 3475 3536 3538 +f 3538 3547 3540 +f 3547 3538 3536 +f 3549 3540 3547 +f 3482 3481 3541 +f 3482 3541 3550 +f 3550 3543 3549 +f 3543 3550 3541 +f 3540 3549 3543 +f 3481 3544 3541 +f 3507 3175 3504 +f 3504 3551 3507 +f 3552 3504 3503 +f 3553 3503 3502 +f 3502 3181 3495 +f 3495 3554 3502 +f 3555 3556 3518 +f 3518 3556 3516 +f 3557 3519 3516 +f 3516 3556 3557 +f 3515 3514 3558 +f 3515 3555 3518 +f 3559 3515 3558 +f 3559 3555 3515 +f 3558 3560 3561 +f 3553 3562 3563 +f 3563 3552 3553 +f 3551 3552 3563 +f 3562 3561 3560 +f 3560 3563 3562 +f 3564 3565 3559 +f 3559 3565 3491 +f 3566 3563 3560 +f 3560 3567 3566 +f 3566 3567 3558 +f 3563 3566 3551 +f 3558 3551 3566 +f 3558 3567 3560 +f 3554 3568 3562 +f 3559 3491 3555 +f 3562 3553 3554 +f 3561 3569 3564 +f 3558 3561 3564 +f 3568 3569 3561 +f 3561 3562 3568 +f 3564 3559 3558 +f 3551 3504 3552 +f 3503 3553 3552 +f 3502 3554 3553 +f 3551 3514 3507 +f 3514 3551 3558 +f 3564 3570 3571 +f 3572 3569 3568 +f 3554 3571 3570 +f 3572 3570 3564 +f 3568 3570 3572 +f 3570 3568 3554 +f 3564 3569 3572 +f 3565 3542 3491 +f 3554 3495 3571 +f 3495 3564 3571 +f 3481 3493 3492 +f 3564 3545 3565 +f 3491 3544 3492 +f 3542 3565 3545 +f 3493 3481 3483 +f 3544 3491 3542 +f 3492 3544 3481 +f 3564 3495 3539 +f 3545 3564 3539 +f 3573 3574 3575 +f 3575 3576 3573 +f 3577 3489 3465 +f 3490 3492 3578 +f 3492 3489 3578 +f 3579 3578 3489 +f 3580 3581 3582 +f 3580 3583 3581 +f 3584 3583 3580 +f 3163 3585 3586 +f 3154 3580 3582 +f 3580 3154 3151 +f 3151 3584 3580 +f 3582 3581 3587 +f 3584 3588 3583 +f 3584 3151 3149 +f 3149 3479 3584 +f 3479 3149 3147 +f 3535 3161 3158 +f 3158 3586 3535 +f 3586 3158 3163 +f 3548 3589 3590 +f 3590 3549 3548 +f 3549 3590 3550 +f 3591 3482 3550 +f 3591 3550 3590 +f 3592 3591 3590 +f 3592 3590 3589 +f 3479 3478 3588 +f 3479 3588 3584 +f 3583 3593 3594 +f 3593 3583 3588 +f 3588 3485 3593 +f 3485 3588 3478 +f 3595 3594 3593 +f 3594 3595 3596 +f 3486 3483 3488 +f 3488 3595 3486 +f 3595 3488 3597 +f 3591 3592 3597 +f 3591 3597 3488 +f 3482 3591 3488 +f 3486 3593 3485 +f 3593 3486 3595 +f 3598 3599 3600 +f 3601 3600 3599 +f 3599 3598 3602 +f 3587 3602 3598 +f 3602 3587 3581 +f 3581 3594 3602 +f 3594 3581 3583 +f 3603 3599 3596 +f 3596 3602 3594 +f 3602 3596 3599 +f 3597 3596 3595 +f 3596 3597 3603 +f 3599 3603 3601 +f 3604 3601 3603 +f 3592 3603 3597 +f 3586 3605 3535 +f 3546 3536 3537 +f 3537 3606 3546 +f 3548 3547 3546 +f 3535 3537 3534 +f 3535 3605 3537 +f 3607 3546 3606 +f 3546 3607 3548 +f 3589 3548 3607 +f 3606 3537 3605 +f 3592 3604 3603 +f 3604 3589 3608 +f 3609 3604 3608 +f 3600 3601 3610 +f 3609 3611 3610 +f 3604 3592 3589 +f 3609 3610 3601 +f 3604 3609 3601 +f 3585 3163 3171 +f 3171 3612 3585 +f 3585 3613 3614 +f 3612 3613 3585 +f 3614 3615 3616 +f 3615 3614 3613 +f 3613 3617 3615 +f 3608 3607 3618 +f 3586 3614 3605 +f 3585 3614 3586 +f 3607 3608 3589 +f 3618 3606 3616 +f 3606 3618 3607 +f 3605 3616 3606 +f 3616 3605 3614 +f 3611 3609 3619 +f 3611 3619 3620 +f 3618 3619 3608 +f 3609 3608 3619 +f 3620 3621 3622 +f 3621 3620 3619 +f 3619 3618 3621 +f 3615 3622 3621 +f 3621 3616 3615 +f 3616 3621 3618 +f 3532 3623 3531 +f 3623 3532 3624 +f 3625 3624 3532 +f 3533 3625 3532 +f 3531 3526 3523 +f 3526 3531 3623 +f 3626 3627 3624 +f 3625 3626 3624 +f 3623 3628 3526 +f 3628 3623 3629 +f 3624 3629 3623 +f 3629 3624 3627 +f 3526 3630 3525 +f 3524 3189 3187 +f 3189 3524 3631 +f 3631 3630 3632 +f 3631 3525 3630 +f 3524 3525 3631 +f 3628 3633 3630 +f 3630 3526 3628 +f 3629 3634 3628 +f 3627 3635 3629 +f 3634 3629 3635 +f 3626 3636 3627 +f 3635 3627 3637 +f 3636 3637 3627 +f 3634 3638 3633 +f 3633 3628 3634 +f 3638 3634 3639 +f 3640 3633 3638 +f 3632 3633 3640 +f 3632 3630 3633 +f 3641 3642 3643 +f 3642 3198 3195 +f 3644 3645 3646 +f 3643 3644 3641 +f 3195 3643 3642 +f 3646 3641 3644 +f 3192 3631 3632 +f 3631 3192 3189 +f 3193 3632 3640 +f 3632 3193 3192 +f 3639 3635 3647 +f 3637 3647 3635 +f 3647 3637 3648 +f 3636 3649 3637 +f 3635 3639 3634 +f 3649 3648 3637 +f 3648 3650 3647 +f 3650 3648 3651 +f 3652 3651 3648 +f 3649 3652 3648 +f 3647 3653 3639 +f 3654 3638 3655 +f 3640 3638 3654 +f 3196 3640 3654 +f 3640 3196 3193 +f 3655 3639 3653 +f 3639 3655 3638 +f 3653 3647 3650 +f 3643 3195 3196 +f 3645 3644 3656 +f 3644 3643 3657 +f 3644 3657 3656 +f 3658 3657 3196 +f 3658 3196 3654 +f 3645 3656 3658 +f 3658 3656 3657 +f 3643 3196 3657 +f 3659 3557 3556 +f 3660 3555 3491 +f 3660 3661 3555 +f 3661 3556 3555 +f 3661 3659 3556 +f 3557 3659 3662 +f 3660 3663 3664 +f 3665 3664 3663 +f 3663 3666 3665 +f 3664 3667 3660 +f 3666 3646 3645 +f 3668 3660 3667 +f 3645 3665 3666 +f 3650 3668 3653 +f 3669 3654 3655 +f 3653 3669 3655 +f 3665 3645 3658 +f 3654 3670 3658 +f 3661 3651 3659 +f 3659 3651 3652 +f 3652 3662 3659 +f 3668 3650 3661 +f 3660 3668 3661 +f 3651 3661 3650 +f 3665 3658 3670 +f 3669 3653 3668 +f 3670 3654 3671 +f 3654 3669 3671 +f 3672 3673 3674 +f 3670 3671 3673 +f 3674 3675 3672 +f 3669 3673 3671 +f 3669 3675 3674 +f 3674 3673 3669 +f 3673 3672 3670 +f 3669 3667 3675 +f 3667 3672 3675 +f 3667 3664 3672 +f 3672 3664 3665 +f 3667 3669 3668 +f 3672 3665 3670 +f 3491 3490 3660 +f 3676 3660 3490 +f 3575 3660 3676 +f 3575 3677 3660 +f 3678 3679 3642 +f 3642 3641 3678 +f 3679 3199 3198 +f 3198 3642 3679 +f 3680 3678 3641 +f 3681 3680 3646 +f 3641 3646 3680 +f 3677 3682 3663 +f 3663 3660 3677 +f 3646 3666 3681 +f 3682 3681 3666 +f 3666 3663 3682 +f 3683 3684 3685 +f 3684 3683 3467 +f 3465 3274 3273 +f 3686 3467 3683 +f 3467 3686 3465 +f 3687 3456 3458 +f 3458 3688 3687 +f 3688 3458 3457 +f 3457 3689 3688 +f 3165 3166 3689 +f 3687 3470 3456 +f 3689 3457 3165 +f 3466 3469 3684 +f 3469 3473 3690 +f 3691 3473 3474 +f 3692 3474 3470 +f 3691 3690 3473 +f 3470 3687 3693 +f 3474 3692 3691 +f 3684 3467 3466 +f 3470 3693 3692 +f 3690 3684 3469 +f 3467 3465 3464 +f 3156 3582 3694 +f 3582 3156 3154 +f 3582 3587 3694 +f 3612 3695 3613 +f 3696 3695 3612 +f 3694 3164 3156 +f 3612 3171 3203 +f 3203 3696 3612 +f 3696 3203 3201 +f 3617 3613 3695 +f 3697 3577 3698 +f 3697 3620 3579 +f 3577 3697 3579 +f 3622 3615 3617 +f 3622 3579 3620 +f 3697 3611 3620 +f 3697 3698 3699 +f 3611 3697 3699 +f 3685 3700 3683 +f 3700 3699 3698 +f 3698 3683 3700 +f 3683 3698 3686 +f 3701 3702 3685 +f 3694 3703 3704 +f 3694 3587 3703 +f 3705 3706 3703 +f 3703 3598 3705 +f 3598 3703 3587 +f 3704 3703 3706 +f 3699 3700 3707 +f 3600 3705 3598 +f 3705 3600 3707 +f 3611 3699 3610 +f 3610 3707 3600 +f 3707 3610 3699 +f 3706 3705 3702 +f 3702 3701 3706 +f 3700 3685 3702 +f 3707 3702 3705 +f 3702 3707 3700 +f 3164 3694 3704 +f 3201 3708 3696 +f 3704 3706 3709 +f 3577 3686 3698 +f 3166 3704 3709 +f 3704 3166 3164 +f 3687 3689 3166 +f 3687 3166 3709 +f 3687 3688 3689 +f 3709 3706 3701 +f 3710 3695 3711 +f 3696 3711 3695 +f 3708 3711 3696 +f 3579 3622 3712 +f 3712 3617 3710 +f 3617 3712 3622 +f 3695 3710 3617 +f 3202 3679 3713 +f 3714 3713 3679 +f 3715 3713 3714 +f 3715 3202 3713 +f 3715 3714 3678 +f 3708 3202 3715 +f 3708 3201 3202 +f 3681 3715 3680 +f 3715 3678 3680 +f 3714 3679 3678 +f 3202 3199 3679 +f 3715 3576 3708 +f 3576 3715 3716 +f 3693 3709 3717 +f 3690 3685 3684 +f 3685 3690 3701 +f 3690 3709 3701 +f 3709 3690 3717 +f 3709 3693 3687 +f 3710 3711 3575 +f 3676 3710 3575 +f 3681 3716 3715 +f 3677 3718 3719 +f 3682 3716 3681 +f 3677 3575 3718 +f 3575 3574 3718 +f 3677 3719 3682 +f 3573 3719 3718 +f 3682 3719 3716 +f 3489 3577 3579 +f 3710 3490 3712 +f 3465 3686 3577 +f 3490 3578 3712 +f 3576 3711 3708 +f 3711 3576 3575 +f 3712 3578 3579 +f 3676 3490 3710 +f 3720 3692 3693 +f 3721 3691 3692 +f 3691 3721 3690 +f 3693 3717 3720 +f 3720 3717 3690 +f 3690 3721 3720 +f 3692 3720 3721 +f 3719 3573 3576 +f 3718 3574 3573 +f 3576 3716 3719 +f 3722 3723 3724 +f 3724 3725 3722 +f 3725 3724 3726 +f 3726 3727 3725 +f 3727 3726 3728 +f 3728 3729 3727 +f 3729 3728 3730 +f 3730 3731 3729 +f 3723 3732 3724 +f 3732 3723 3733 +f 3724 3734 3726 +f 3734 3724 3732 +f 3726 3735 3728 +f 3735 3726 3734 +f 3728 3736 3730 +f 3736 3728 3735 +f 3737 3738 3739 +f 3738 3737 3740 +f 3739 3741 3742 +f 3741 3739 3738 +f 3734 3739 3735 +f 3739 3734 3737 +f 3735 3742 3736 +f 3742 3735 3739 +f 3733 3743 3732 +f 3743 3733 3744 +f 3732 3737 3734 +f 3737 3732 3743 +f 3744 3745 3743 +f 3745 3744 3746 +f 3743 3740 3737 +f 3740 3743 3745 +f 3747 3748 3749 +f 3749 3750 3747 +f 3750 3749 3751 +f 3751 3752 3750 +f 3752 3751 3753 +f 3753 3754 3752 +f 3754 3753 3723 +f 3723 3722 3754 +f 3748 3755 3749 +f 3755 3748 3756 +f 3749 3757 3751 +f 3757 3749 3755 +f 3751 3758 3753 +f 3758 3751 3757 +f 3753 3733 3723 +f 3733 3753 3758 +f 3757 3759 3758 +f 3759 3757 3760 +f 3758 3744 3733 +f 3744 3758 3759 +f 3760 3761 3759 +f 3761 3760 3762 +f 3759 3746 3744 +f 3746 3759 3761 +f 3763 3764 3765 +f 3764 3763 3766 +f 3765 3762 3760 +f 3762 3765 3764 +f 3756 3765 3755 +f 3765 3756 3763 +f 3755 3760 3757 +f 3760 3755 3765 +f 3767 3768 3769 +f 3768 3767 3770 +f 3769 3771 3772 +f 3771 3769 3768 +f 3740 3769 3738 +f 3769 3740 3767 +f 3738 3772 3741 +f 3772 3738 3769 +f 3746 3773 3745 +f 3773 3746 3774 +f 3745 3767 3740 +f 3767 3745 3773 +f 3774 3775 3773 +f 3775 3774 3776 +f 3773 3770 3767 +f 3770 3773 3775 +f 3777 3770 3778 +f 3777 3768 3770 +f 3779 3768 3777 +f 3779 3771 3768 +f 3780 3777 3778 +f 3777 3780 3781 +f 3781 3779 3777 +f 3779 3781 3782 +f 3783 3784 3785 +f 3784 3783 3786 +f 3786 3778 3784 +f 3778 3786 3780 +f 3784 3776 3785 +f 3784 3775 3776 +f 3770 3784 3778 +f 3770 3775 3784 +f 3787 3788 3789 +f 3788 3787 3790 +f 3789 3776 3774 +f 3776 3789 3788 +f 3762 3789 3761 +f 3789 3762 3787 +f 3761 3774 3746 +f 3774 3761 3789 +f 3766 3791 3764 +f 3791 3766 3792 +f 3764 3787 3762 +f 3787 3764 3791 +f 3792 3793 3791 +f 3793 3792 3794 +f 3791 3790 3787 +f 3790 3791 3793 +f 3795 3796 3797 +f 3796 3795 3798 +f 3798 3785 3796 +f 3785 3798 3783 +f 3788 3797 3796 +f 3788 3790 3797 +f 3785 3788 3796 +f 3785 3776 3788 +f 3794 3799 3793 +f 3794 3800 3799 +f 3793 3797 3790 +f 3793 3799 3797 +f 3801 3799 3800 +f 3799 3801 3802 +f 3802 3797 3799 +f 3797 3802 3795 +f 3803 3804 3805 +f 3804 3803 3806 +f 3807 3805 3808 +f 3805 3807 3803 +f 3806 3730 3804 +f 3730 3806 3731 +f 3809 3810 3811 +f 3812 3813 3814 +f 3812 3815 3813 +f 3811 3816 3813 +f 3727 3812 3814 +f 3812 3727 3729 +f 3807 3806 3803 +f 3807 3731 3806 +f 3817 3813 3815 +f 3818 3815 3812 +f 3818 3731 3807 +f 3729 3818 3812 +f 3818 3729 3731 +f 3819 3820 3821 +f 3813 3817 3811 +f 3822 3819 3821 +f 3811 3823 3809 +f 3809 3821 3820 +f 3821 3809 3823 +f 3823 3811 3817 +f 3824 3810 3825 +f 3810 3824 3816 +f 3826 3816 3824 +f 3814 3816 3826 +f 3814 3813 3816 +f 3725 3814 3826 +f 3814 3725 3727 +f 3826 3824 3827 +f 3828 3825 3829 +f 3825 3828 3824 +f 3722 3826 3827 +f 3826 3722 3725 +f 3830 3829 3825 +f 3827 3824 3828 +f 3831 3832 3833 +f 3825 3834 3830 +f 3829 3830 3835 +f 3836 3837 3832 +f 3831 3836 3832 +f 3830 3833 3832 +f 3832 3835 3830 +f 3835 3832 3837 +f 3838 3831 3833 +f 3838 3833 3820 +f 3819 3838 3820 +f 3820 3834 3809 +f 3834 3820 3833 +f 3834 3825 3810 +f 3816 3811 3810 +f 3810 3809 3834 +f 3833 3830 3834 +f 3839 3840 3841 +f 3842 3840 3839 +f 3840 3842 3843 +f 3844 3815 3818 +f 3845 3822 3821 +f 3823 3846 3821 +f 3817 3815 3847 +f 3807 3844 3818 +f 3848 3846 3823 +f 3815 3844 3847 +f 3821 3846 3845 +f 3849 3817 3847 +f 3844 3807 3850 +f 3849 3848 3817 +f 3817 3848 3823 +f 3807 3808 3851 +f 3851 3850 3807 +f 3852 3843 3847 +f 3853 3852 3854 +f 3851 3854 3850 +f 3854 3851 3853 +f 3852 3853 3843 +f 3847 3843 3849 +f 3847 3855 3852 +f 3852 3855 3856 +f 3844 3850 3854 +f 3854 3856 3844 +f 3842 3845 3846 +f 3856 3855 3847 +f 3847 3844 3856 +f 3856 3854 3852 +f 3846 3843 3842 +f 3849 3843 3848 +f 3848 3843 3846 +f 3730 3857 3804 +f 3857 3730 3736 +f 3804 3858 3805 +f 3858 3804 3857 +f 3805 3859 3808 +f 3859 3805 3858 +f 3736 3860 3857 +f 3857 3861 3858 +f 3858 3862 3859 +f 3861 3857 3860 +f 3863 3859 3862 +f 3860 3736 3742 +f 3862 3858 3861 +f 3859 3863 3864 +f 3808 3864 3851 +f 3864 3808 3859 +f 3865 3853 3866 +f 3865 3866 3867 +f 3843 3853 3865 +f 3843 3865 3868 +f 3865 3867 3869 +f 3867 3870 3871 +f 3851 3866 3853 +f 3866 3851 3864 +f 3866 3870 3867 +f 3870 3872 3873 +f 3872 3874 3875 +f 3874 3876 3877 +f 3872 3866 3874 +f 3870 3866 3872 +f 3874 3866 3876 +f 3864 3878 3866 +f 3876 3879 3880 +f 3879 3881 3882 +f 3881 3878 3883 +f 3879 3878 3881 +f 3876 3878 3879 +f 3876 3866 3878 +f 3881 3883 3884 +f 3878 3864 3863 +f 3885 3862 3886 +f 3742 3887 3860 +f 3887 3742 3741 +f 3860 3888 3861 +f 3888 3860 3887 +f 3861 3886 3862 +f 3886 3861 3888 +f 3883 3878 3889 +f 3862 3885 3863 +f 3863 3890 3878 +f 3891 3878 3890 +f 3892 3878 3891 +f 3889 3878 3892 +f 3883 3889 3893 +f 3889 3892 3894 +f 3890 3863 3885 +f 3892 3891 3895 +f 3872 3896 3897 +f 3876 3898 3899 +f 3875 3900 3896 +f 3877 3899 3901 +f 3880 3902 3898 +f 3874 3901 3900 +f 3903 3865 3869 +f 3904 3867 3871 +f 3905 3871 3870 +f 3906 3869 3867 +f 3907 3868 3865 +f 3840 3843 3868 +f 3843 3872 3840 +f 3868 3907 3840 +f 3869 3906 3903 +f 3871 3905 3904 +f 3873 3897 3908 +f 3870 3908 3905 +f 3867 3904 3906 +f 3865 3903 3907 +f 3896 3840 3872 +f 3908 3870 3873 +f 3896 3872 3875 +f 3901 3874 3877 +f 3899 3877 3876 +f 3900 3875 3874 +f 3872 3883 3896 +f 3897 3873 3872 +f 3893 3909 3910 +f 3883 3910 3911 +f 3881 3912 3913 +f 3884 3911 3912 +f 3882 3913 3914 +f 3910 3896 3883 +f 3914 3902 3880 +f 3911 3884 3883 +f 3913 3882 3881 +f 3880 3879 3914 +f 3898 3876 3880 +f 3914 3879 3882 +f 3912 3881 3884 +f 3910 3883 3893 +f 3915 3892 3895 +f 3916 3891 3917 +f 3918 3895 3891 +f 3919 3894 3892 +f 3883 3920 3910 +f 3909 3893 3889 +f 3921 3889 3894 +f 3891 3916 3918 +f 3895 3918 3915 +f 3922 3910 3920 +f 3917 3923 3916 +f 3894 3919 3921 +f 3892 3915 3919 +f 3889 3921 3909 +f 3835 3924 3829 +f 3925 3828 3926 +f 3827 3828 3925 +f 3926 3829 3924 +f 3829 3926 3828 +f 3754 3827 3925 +f 3827 3754 3722 +f 3924 3927 3926 +f 3928 3926 3927 +f 3925 3926 3928 +f 3752 3925 3928 +f 3925 3752 3754 +f 3929 3930 3927 +f 3927 3924 3929 +f 3924 3835 3931 +f 3932 3933 3934 +f 3932 3934 3837 +f 3836 3932 3837 +f 3931 3929 3924 +f 3837 3931 3835 +f 3931 3837 3934 +f 3934 3935 3931 +f 3933 3936 3937 +f 3929 3931 3935 +f 3935 3934 3937 +f 3933 3937 3934 +f 3935 3938 3929 +f 3938 3935 3939 +f 3936 3940 3937 +f 3937 3939 3935 +f 3939 3937 3940 +f 3941 3942 3943 +f 3942 3941 3944 +f 3945 3943 3946 +f 3943 3945 3941 +f 3944 3747 3942 +f 3747 3944 3748 +f 3930 3929 3938 +f 3947 3927 3930 +f 3750 3928 3947 +f 3928 3750 3752 +f 3928 3927 3947 +f 3947 3747 3750 +f 3948 3938 3949 +f 3946 3942 3747 +f 3950 3930 3948 +f 3947 3930 3950 +f 3946 3943 3942 +f 3946 3747 3950 +f 3747 3947 3950 +f 3939 3949 3938 +f 3949 3939 3951 +f 3938 3948 3930 +f 3952 3953 3940 +f 3936 3952 3940 +f 3940 3951 3939 +f 3951 3940 3953 +f 3954 3955 3956 +f 3845 3842 3957 +f 3954 3958 3959 +f 3956 3958 3954 +f 3960 3958 3956 +f 3956 3955 3960 +f 3961 3959 3958 +f 3958 3960 3961 +f 3952 3957 3842 +f 3961 3950 3959 +f 3950 3954 3959 +f 3954 3949 3841 +f 3839 3953 3842 +f 3953 3839 3951 +f 3954 3950 3948 +f 3842 3953 3952 +f 3950 3961 3946 +f 3949 3954 3948 +f 3961 3962 3945 +f 3945 3946 3961 +f 3951 3841 3949 +f 3841 3951 3839 +f 3841 3840 3954 +f 3840 3960 3955 +f 3963 3961 3960 +f 3961 3963 3962 +f 3954 3840 3955 +f 3960 3840 3963 +f 3964 3962 3965 +f 3965 3966 3964 +f 3962 3964 3945 +f 3964 3967 3968 +f 3969 3941 3968 +f 3945 3968 3941 +f 3944 3756 3748 +f 3968 3945 3964 +f 3756 3944 3969 +f 3969 3763 3756 +f 3968 3970 3969 +f 3941 3969 3944 +f 3970 3766 3763 +f 3763 3969 3970 +f 3966 3971 3967 +f 3972 3967 3971 +f 3766 3970 3972 +f 3970 3968 3967 +f 3967 3964 3966 +f 3967 3972 3970 +f 3903 3963 3840 +f 3904 3963 3903 +f 3903 3840 3907 +f 3904 3903 3906 +f 3908 3904 3905 +f 3963 3965 3962 +f 3965 3963 3973 +f 3904 3973 3963 +f 3896 3973 3908 +f 3898 3901 3899 +f 3898 3973 3901 +f 3901 3896 3900 +f 3973 3974 3965 +f 3896 3908 3897 +f 3901 3973 3896 +f 3908 3973 3904 +f 3974 3973 3975 +f 3914 3898 3902 +f 3975 3898 3914 +f 3975 3973 3898 +f 3912 3914 3913 +f 3966 3965 3974 +f 3974 3976 3966 +f 3971 3966 3976 +f 3921 3910 3909 +f 3912 3910 3975 +f 3975 3914 3912 +f 3975 3910 3921 +f 3910 3912 3911 +f 3976 3974 3977 +f 3978 3915 3916 +f 3975 3977 3974 +f 3977 3975 3978 +f 3915 3921 3919 +f 3915 3975 3921 +f 3916 3915 3918 +f 3978 3975 3915 +f 3979 3980 3981 +f 3980 3979 3982 +f 3772 3983 3984 +f 3983 3772 3771 +f 3984 3982 3979 +f 3982 3984 3983 +f 3741 3984 3887 +f 3984 3741 3772 +f 3887 3979 3888 +f 3979 3887 3984 +f 3888 3981 3886 +f 3981 3888 3979 +f 3890 3985 3891 +f 3891 3985 3917 +f 3886 3986 3885 +f 3986 3886 3981 +f 3987 3988 3989 +f 3990 3989 3988 +f 3989 3990 3991 +f 3991 3990 3992 +f 3990 3988 3993 +f 3988 3987 3994 +f 3991 3890 3989 +f 3989 3885 3986 +f 3985 3890 3920 +f 3920 3890 3995 +f 3985 3920 3996 +f 3920 3995 3997 +f 3995 3890 3998 +f 3998 3890 3991 +f 3995 3998 3999 +f 3998 3991 4000 +f 3885 3989 3890 +f 3981 4001 3986 +f 4001 3981 3980 +f 3980 4002 4003 +f 4004 3980 4003 +f 3986 4005 3989 +f 3989 4006 3987 +f 4005 4006 3989 +f 4007 4006 4005 +f 4006 4008 3987 +f 4007 4009 4006 +f 4010 4011 4007 +f 4005 3986 4001 +f 4005 4010 4007 +f 4012 4013 4010 +f 4012 4010 4005 +f 4014 4012 4015 +f 4015 4012 4005 +f 4016 4001 4004 +f 4016 4005 4001 +f 4015 4005 4016 +f 4017 3999 3998 +f 4018 3997 3995 +f 4019 3985 3996 +f 4020 3996 3920 +f 3923 3917 3985 +f 3922 3920 3997 +f 4021 3995 3999 +f 3997 4018 3922 +f 3999 4017 4021 +f 3920 3922 4020 +f 3998 4022 4017 +f 3995 4021 4018 +f 3996 4020 4019 +f 4023 3992 3990 +f 4024 4000 3991 +f 4022 3998 4000 +f 4025 3991 3992 +f 4026 3990 3993 +f 4027 3993 3988 +f 3920 3988 3922 +f 3992 4023 4025 +f 4000 4024 4022 +f 3988 4028 4027 +f 3993 4027 4026 +f 3990 4026 4023 +f 3991 4025 4024 +f 4028 3922 3988 +f 4008 4029 4030 +f 4006 4031 4029 +f 3987 4030 4032 +f 4007 4033 4034 +f 3994 4032 4028 +f 4009 4034 4031 +f 4034 4009 4007 +f 4031 4006 4009 +f 4028 3988 3994 +f 4030 3987 4008 +f 4029 4008 4006 +f 4032 3994 3987 +f 3988 4012 4028 +f 4012 4014 4035 +f 4036 4010 4013 +f 4037 4013 4012 +f 4033 4007 4011 +f 4012 4015 4038 +f 4039 4011 4010 +f 4012 4040 4037 +f 4011 4039 4033 +f 4013 4037 4036 +f 4010 4036 4039 +f 4035 4040 4012 +f 4038 4040 4012 +f 4040 4028 4012 +f 3980 3982 4002 +f 4041 3771 3779 +f 4041 3983 3771 +f 4002 3983 4041 +f 4002 3982 3983 +f 4042 4002 4041 +f 4041 4043 4042 +f 4044 4003 4002 +f 4002 4042 4044 +f 4043 4041 3779 +f 3779 3782 4043 +f 4045 4046 4047 +f 4048 4049 4045 +f 4049 4048 4050 +f 4051 4047 4046 +f 4047 4051 4052 +f 4053 4052 4051 +f 4054 4053 4051 +f 4046 4045 4049 +f 4044 4042 4043 +f 4055 4050 4048 +f 4044 3782 4055 +f 3782 4056 4055 +f 4056 3782 3781 +f 4044 4043 3782 +f 4050 4057 4049 +f 4056 4050 4055 +f 4056 4058 4050 +f 4059 4058 4056 +f 3781 4059 4056 +f 4059 3781 3780 +f 4059 4060 4058 +f 4061 4060 4059 +f 4057 4050 4058 +f 4058 4062 4057 +f 4062 4058 4060 +f 3780 4061 4059 +f 4061 3780 3786 +f 4063 4062 4064 +f 4065 4066 4061 +f 4061 4066 4060 +f 3786 4065 4061 +f 4065 3786 3783 +f 4060 4064 4062 +f 4064 4060 4066 +f 4067 4068 4069 +f 4069 4070 4067 +f 4068 4067 4063 +f 4067 4057 4062 +f 4071 4072 4069 +f 4071 4069 4068 +f 4073 4071 4068 +f 4062 4063 4067 +f 4070 4049 4057 +f 4074 4046 4070 +f 4046 4074 4051 +f 4049 4070 4046 +f 4054 4051 4074 +f 4057 4067 4070 +f 4070 4069 4074 +f 4072 4054 4074 +f 4072 4074 4069 +f 4075 4076 4035 +f 4077 4075 4035 +f 4035 4014 4077 +f 4078 4077 4014 +f 4078 4079 4077 +f 4080 4078 4014 +f 4081 4080 4014 +f 4004 4001 3980 +f 4082 4081 4014 +f 4083 4082 4015 +f 4014 4015 4082 +f 4004 4084 4085 +f 4085 4016 4004 +f 4016 4085 4083 +f 4083 4015 4016 +f 4044 4004 4003 +f 4055 4084 4044 +f 4052 4080 4047 +f 4078 4052 4053 +f 4045 4082 4048 +f 4082 4055 4048 +f 4047 4081 4045 +f 4053 4079 4078 +f 4004 4044 4084 +f 4084 4055 4086 +f 4080 4052 4078 +f 4055 4082 4086 +f 4082 4045 4081 +f 4081 4047 4080 +f 4087 4086 4082 +f 4085 4087 4088 +f 4087 4085 4084 +f 4083 4088 4082 +f 4082 4088 4087 +f 4088 4083 4085 +f 4084 4086 4087 +f 4019 3978 3916 +f 3985 4019 3923 +f 4089 3977 4090 +f 4019 3916 3923 +f 3972 3792 3766 +f 3792 3972 4091 +f 3971 4091 3972 +f 4091 3971 4092 +f 3976 4092 3971 +f 4092 3976 4089 +f 3977 4089 3976 +f 4093 4089 4094 +f 4091 3794 3792 +f 3794 4091 4095 +f 4092 4095 4091 +f 4095 4092 4093 +f 4089 4093 4092 +f 4028 4096 4026 +f 4030 4096 4028 +f 4026 4025 4023 +f 4028 4026 4027 +f 4030 4028 4032 +f 4090 3978 4096 +f 4096 4022 4025 +f 4096 4025 4026 +f 4021 3978 3922 +f 3922 4019 4020 +f 4021 3922 4018 +f 3922 3978 4019 +f 3978 4090 3977 +f 4022 4021 4017 +f 4025 4022 4024 +f 4022 3978 4021 +f 4096 3978 4022 +f 4097 4098 4094 +f 4090 4094 4089 +f 4094 4090 4099 +f 4096 4099 4090 +f 4031 4096 4030 +f 4100 4031 4033 +f 4100 4096 4031 +f 4031 4030 4029 +f 4033 4031 4034 +f 4036 4033 4039 +f 4099 4096 4100 +f 4099 4100 4101 +f 4099 4101 4097 +f 4037 4040 4036 +f 4100 4033 4036 +f 4100 4036 4040 +f 4040 4101 4100 +f 4038 4040 4035 +f 4040 4038 4101 +f 4065 4102 4066 +f 4103 4102 4065 +f 4104 4064 4105 +f 3783 4103 4065 +f 4103 3783 3798 +f 4066 4105 4064 +f 4105 4066 4102 +f 4106 4107 4103 +f 3798 4106 4103 +f 4106 3798 3795 +f 4106 4108 4107 +f 4103 4107 4102 +f 4102 4109 4105 +f 4109 4102 4107 +f 4104 4110 4111 +f 4111 4063 4104 +f 4063 4111 4068 +f 4064 4104 4063 +f 4105 4112 4104 +f 4073 4068 4111 +f 4113 4073 4111 +f 4113 4111 4110 +f 4112 4114 4110 +f 4114 4112 4115 +f 4107 4116 4109 +f 4110 4104 4112 +f 4109 4115 4112 +f 4112 4105 4109 +f 4117 4113 4110 +f 4117 4110 4114 +f 4118 4117 4114 +f 4098 4119 4093 +f 4095 4119 4120 +f 4095 4093 4119 +f 4098 4093 4094 +f 4120 3794 4095 +f 4120 3800 3794 +f 4098 4121 4122 +f 4122 4119 4098 +f 4120 4123 3801 +f 3801 3800 4120 +f 4119 4122 4123 +f 4123 4120 4119 +f 4108 4124 4116 +f 4124 4108 4125 +f 4116 4126 4115 +f 4115 4127 4114 +f 4127 4115 4126 +f 4118 4114 4127 +f 4128 4118 4127 +f 4126 4116 4124 +f 3795 4129 4106 +f 4129 3795 3802 +f 3802 4130 4129 +f 4116 4107 4108 +f 4129 4108 4106 +f 4129 4125 4108 +f 4115 4109 4116 +f 4121 4123 4122 +f 4130 4125 4129 +f 4130 3802 3801 +f 4121 3801 4123 +f 4130 3801 4121 +f 4097 4094 4099 +f 4038 4035 4131 +f 4038 4131 4132 +f 4101 4132 4133 +f 4076 4134 4035 +f 4132 4101 4038 +f 4133 4097 4101 +f 4134 4131 4035 +f 4135 4132 4131 +f 4134 4135 4136 +f 4131 4136 4135 +f 4137 4133 4132 +f 4134 4137 4135 +f 4132 4135 4137 +f 4138 4077 4079 +f 4134 4136 4131 +f 4133 4098 4097 +f 4075 4077 4126 +f 4125 4137 4134 +f 4137 4121 4133 +f 4076 4124 4134 +f 4076 4075 4124 +f 4127 4077 4138 +f 4098 4133 4121 +f 4138 4128 4127 +f 4137 4125 4130 +f 4121 4137 4130 +f 4124 4125 4134 +f 4124 4075 4126 +f 4126 4077 4127 +f 4139 4140 4141 +f 4140 4142 4143 +f 4141 4144 4139 +f 4143 4145 4141 +f 4143 4141 4140 +f 4146 4147 4148 +f 4147 4149 4148 +f 4147 4144 4141 +f 4142 4146 4143 +f 4142 4144 4146 +f 4144 4147 4146 +f 4150 4141 4145 +f 4148 4151 4143 +f 4145 4143 4151 +f 4152 4149 4141 +f 4141 4150 4152 +f 4148 4143 4146 +f 4141 4149 4147 +f 4148 4149 4151 +f 4149 4152 4151 +f 4151 4152 4145 +f 4152 4150 4145 +f 4153 4154 4155 +f 4154 4156 4155 +f 4155 4156 4142 +f 4156 4144 4142 +f 4155 4142 4157 +f 4144 4156 4158 +f 4159 4160 4153 +f 4160 4154 4153 +f 4155 4157 4153 +f 4161 4162 4159 +f 4158 4156 4154 +f 4162 4160 4159 +f 4159 4157 4161 +f 4159 4153 4157 +f 4161 4157 4162 +f 4158 4160 4162 +f 4154 4160 4158 +f 4158 4162 4157 +f 4139 4157 4140 +f 4139 4158 4157 +f 4139 4144 4158 +f 4157 4142 4140 +f 4163 4164 4165 +f 4165 4166 4163 +f 4167 4166 4165 +f 4166 4168 4169 +f 4170 4171 4172 +f 4171 4173 4167 +f 4167 4172 4171 +f 4169 4163 4166 +f 4173 4168 4166 +f 4166 4167 4173 +f 4174 4172 4175 +f 4175 4176 4174 +f 4172 4174 4170 +f 4172 4167 4177 +f 4177 4175 4172 +f 4165 4177 4167 +f 4178 4175 4177 +f 4179 4176 4175 +f 4175 4178 4179 +f 4165 4180 4181 +f 4181 4177 4165 +f 4177 4181 4178 +f 4182 4183 4163 +f 4163 4169 4182 +f 4183 4184 4185 +f 4185 4186 4183 +f 4182 4187 4184 +f 4184 4183 4182 +f 4183 4186 4164 +f 4164 4163 4183 +f 4164 4186 4188 +f 4188 4189 4164 +f 4190 4188 4186 +f 4186 4185 4190 +f 4165 4164 4189 +f 4189 4180 4165 +f 4179 4178 4191 +f 4191 4192 4179 +f 4178 4181 4193 +f 4193 4191 4178 +f 4181 4180 4194 +f 4194 4193 4181 +f 4194 4180 4189 +f 4195 4189 4188 +f 4196 4197 4198 +f 4188 4190 4197 +f 4197 4196 4188 +f 4199 4195 4196 +f 4189 4195 4194 +f 4188 4196 4195 +f 4196 4200 4199 +f 4200 4198 4201 +f 4199 4202 4203 +f 4198 4200 4196 +f 4195 4199 4204 +f 4200 4205 4202 +f 4201 4205 4200 +f 4202 4199 4200 +f 4206 4207 4204 +f 4207 4206 4208 +f 4209 4210 4211 +f 4208 4211 4207 +f 4203 4204 4199 +f 4211 4208 4209 +f 4204 4203 4206 +f 4204 4207 4193 +f 4207 4211 4191 +f 4211 4210 4192 +f 4192 4191 4211 +f 4191 4193 4207 +f 4204 4194 4195 +f 4193 4194 4204 +f 4182 4212 4213 +f 4212 4214 4215 +f 4216 4213 4212 +f 4212 4182 4169 +f 4169 4214 4212 +f 4214 4169 4168 +f 4213 4187 4182 +f 4168 4217 4214 +f 4218 4219 4171 +f 4171 4170 4218 +f 4219 4220 4173 +f 4173 4171 4219 +f 4220 4217 4168 +f 4168 4173 4220 +f 4214 4217 4221 +f 4221 4217 4220 +f 4222 4220 4219 +f 4223 4219 4218 +f 4224 4225 4226 +f 4221 4215 4214 +f 4226 4216 4224 +f 4227 4225 4224 +f 4224 4215 4227 +f 4227 4215 4221 +f 4215 4224 4212 +f 4212 4224 4216 +f 4219 4223 4222 +f 4218 4228 4223 +f 4229 4221 4222 +f 4220 4222 4221 +f 4222 4230 4229 +f 4231 4223 4228 +f 4221 4229 4227 +f 4228 4232 4231 +f 4230 4222 4223 +f 4223 4231 4230 +f 4233 4234 4231 +f 4231 4232 4233 +f 4235 4236 4229 +f 4229 4230 4235 +f 4237 4227 4229 +f 4229 4236 4237 +f 4234 4235 4230 +f 4230 4231 4234 +f 4227 4237 4238 +f 4225 4238 4239 +f 4240 4238 4237 +f 4241 4239 4238 +f 4239 4226 4225 +f 4238 4225 4227 +f 4240 4242 4243 +f 4243 4241 4240 +f 4242 4240 4244 +f 4245 4244 4246 +f 4244 4245 4242 +f 4237 4244 4240 +f 4238 4240 4241 +f 4247 4248 4249 +f 4250 4251 4248 +f 4252 4246 4253 +f 4253 4249 4252 +f 4249 4253 4247 +f 4246 4252 4245 +f 4248 4247 4250 +f 4247 4234 4233 +f 4253 4235 4234 +f 4244 4237 4236 +f 4246 4236 4235 +f 4235 4253 4246 +f 4233 4250 4247 +f 4236 4246 4244 +f 4234 4247 4253 +f 4254 4255 4256 +f 4256 4257 4254 +f 4258 4259 4260 +f 4261 4262 4263 +f 4254 4257 4264 +f 4261 4263 4265 +f 4266 4263 4267 +f 4254 4264 4261 +f 4263 4266 4265 +f 4266 4267 4268 +f 4267 4269 4268 +f 4254 4261 4265 +f 4270 4271 4257 +f 4270 4272 4271 +f 4270 4273 4272 +f 4270 4257 4256 +f 4256 4274 4270 +f 4275 4276 4277 +f 4273 4278 4279 +f 4279 4272 4273 +f 4278 4277 4276 +f 4276 4279 4278 +f 4280 4281 4282 +f 4268 4269 4280 +f 4283 4282 4281 +f 4281 4284 4283 +f 4268 4280 4282 +f 4285 4286 4283 +f 4287 4286 4285 +f 4285 4288 4287 +f 4289 4290 4288 +f 4285 4283 4284 +f 4285 4284 4291 +f 4288 4285 4289 +f 4285 4292 4293 +f 4285 4291 4292 +f 4294 4292 4295 +f 4294 4295 4296 +f 4296 4297 4298 +f 4299 4298 4297 +f 4285 4293 4289 +f 4294 4293 4292 +f 4298 4294 4296 +f 4277 4300 4275 +f 4300 4301 4275 +f 4300 4302 4301 +f 4300 4303 4302 +f 4304 4305 4306 +f 4305 4307 4303 +f 4307 4302 4303 +f 4305 4304 4307 +f 4308 4309 4310 +f 4304 4306 4308 +f 4308 4311 4309 +f 4308 4312 4311 +f 4304 4308 4313 +f 4308 4310 4313 +f 4314 4315 4316 +f 4314 4317 4315 +f 4308 4314 4312 +f 4308 4318 4314 +f 4314 4316 4312 +f 4319 4320 4321 +f 4322 4321 4320 +f 4322 4317 4321 +f 4322 4315 4317 +f 4323 4319 4321 +f 4324 4319 4325 +f 4319 4323 4325 +f 4324 4326 4319 +f 4325 4327 4324 +f 4325 4328 4327 +f 4325 4329 4328 +f 4330 4331 4332 +f 4331 4333 4332 +f 4334 4328 4329 +f 4334 4329 4331 +f 4330 4335 4331 +f 4335 4334 4331 +f 4335 4336 4334 +f 4337 4338 4339 +f 4335 4339 4338 +f 4335 4338 4336 +f 4339 4299 4337 +f 4340 4341 4335 +f 4335 4330 4340 +f 4297 4337 4299 +f 4335 4341 4339 +f 4342 4343 4344 +f 4344 4345 4342 +f 4344 4343 4346 +f 4346 4347 4344 +f 4344 4348 4345 +f 4348 4344 4347 +f 4347 4349 4348 +f 4342 4345 4185 +f 4349 4350 4351 +f 4351 4348 4349 +f 4348 4351 4352 +f 4352 4345 4348 +f 4350 4353 4351 +f 4185 4345 4352 +f 4352 4190 4185 +f 4354 4355 4356 +f 4356 4357 4354 +f 4357 4358 4359 +f 4358 4349 4347 +f 4356 4360 4358 +f 4358 4357 4356 +f 4358 4360 4350 +f 4350 4349 4358 +f 4360 4361 4353 +f 4362 4361 4360 +f 4356 4357 4362 +f 4353 4350 4360 +f 4356 4362 4360 +f 4359 4363 4357 +f 4364 4365 4363 +f 4363 4359 4364 +f 4346 4364 4359 +f 4359 4347 4346 +f 4347 4359 4358 +f 4357 4363 4366 +f 4366 4354 4357 +f 4367 4368 4365 +f 4363 4365 4368 +f 4368 4366 4363 +f 4366 4368 4369 +f 4369 4370 4366 +f 4371 4372 4343 +f 4343 4342 4371 +f 4373 4372 4371 +f 4374 4375 4371 +f 4184 4342 4185 +f 4184 4371 4342 +f 4371 4184 4187 +f 4372 4373 4376 +f 4376 4377 4372 +f 4378 4373 4375 +f 4379 4375 4374 +f 4375 4373 4371 +f 4343 4372 4377 +f 4377 4346 4343 +f 4377 4376 4380 +f 4381 4376 4373 +f 4382 4380 4383 +f 4376 4381 4384 +f 4384 4380 4376 +f 4375 4379 4378 +f 4378 4381 4373 +f 4384 4381 4378 +f 4385 4378 4379 +f 4346 4377 4382 +f 4382 4364 4346 +f 4386 4364 4382 +f 4380 4382 4377 +f 4387 4383 4388 +f 4382 4387 4386 +f 4388 4389 4387 +f 4386 4387 4389 +f 4383 4387 4382 +f 4379 4390 4385 +f 4391 4384 4385 +f 4392 4385 4390 +f 4378 4385 4384 +f 4380 4384 4391 +f 4390 4393 4392 +f 4385 4392 4391 +f 4391 4383 4380 +f 4392 4393 4394 +f 4391 4392 4395 +f 4386 4365 4364 +f 4255 4254 4396 +f 4365 4386 4367 +f 4256 4255 4397 +f 4398 4397 4399 +f 4400 4399 4401 +f 4355 4402 4362 +f 4279 4276 4403 +f 4362 4356 4355 +f 4277 4278 4404 +f 4405 4404 4406 +f 4407 4406 4404 +f 4406 4408 4405 +f 4404 4405 4277 +f 4404 4409 4407 +f 4409 4404 4278 +f 4278 4273 4409 +f 4410 4409 4273 +f 4273 4270 4410 +f 4411 4412 4413 +f 4409 4410 4413 +f 4414 4413 4410 +f 4415 4410 4270 +f 4416 4417 4355 +f 4418 4417 4419 +f 4419 4403 4418 +f 4403 4419 4279 +f 4420 4419 4417 +f 4402 4355 4417 +f 4417 4418 4402 +f 4421 4422 4257 +f 4271 4272 4420 +f 4257 4271 4421 +f 4420 4421 4271 +f 4264 4257 4422 +f 4422 4421 4370 +f 4354 4366 4370 +f 4355 4354 4416 +f 4272 4279 4419 +f 4370 4416 4354 +f 4416 4370 4421 +f 4419 4420 4272 +f 4421 4420 4416 +f 4417 4416 4420 +f 4423 4424 4425 +f 4425 4274 4423 +f 4414 4415 4426 +f 4410 4415 4414 +f 4415 4274 4425 +f 4270 4274 4415 +f 4426 4415 4425 +f 4427 4425 4424 +f 4424 4400 4427 +f 4428 4425 4427 +f 4398 4423 4274 +f 4274 4256 4398 +f 4400 4424 4423 +f 4423 4398 4400 +f 4429 4430 4431 +f 4426 4432 4414 +f 4411 4433 4430 +f 4434 4431 3957 +f 4432 4433 4411 +f 4435 4436 4437 +f 4426 4428 4438 +f 4438 4437 4426 +f 4432 4436 4435 +f 4432 4426 4437 +f 4437 4438 4435 +f 4437 4436 4432 +f 4411 4414 4432 +f 4430 4429 4411 +f 4431 4434 4429 +f 3957 3952 4434 +f 4425 4428 4426 +f 4427 4439 4428 +f 4440 4441 4430 +f 4428 4439 4442 +f 4438 4442 4440 +f 4441 4440 4443 +f 4440 4433 4432 +f 4432 4435 4440 +f 4440 4435 4438 +f 4442 4438 4428 +f 4430 4433 4440 +f 4440 4258 4443 +f 4444 4445 4258 +f 4258 4445 4443 +f 4258 4446 4444 +f 4447 4444 4446 +f 4397 4255 4401 +f 4401 4399 4397 +f 4401 4255 4448 +f 4396 4448 4255 +f 4449 4401 4448 +f 4450 4451 4448 +f 4399 4400 4398 +f 4401 4427 4400 +f 4439 4427 4401 +f 4397 4398 4256 +f 4401 4449 4439 +f 4448 4452 4449 +f 4453 4454 4455 +f 4455 4456 4453 +f 4454 4453 4457 +f 4448 4451 4457 +f 4457 4452 4448 +f 4457 4451 4454 +f 4258 4440 4458 +f 4459 4446 4460 +f 4260 4460 4446 +f 4458 4440 4442 +f 4461 4442 4439 +f 4446 4258 4260 +f 4457 4462 4452 +f 4463 4462 4457 +f 4461 4462 4463 +f 4463 4458 4461 +f 4449 4452 4462 +f 4462 4461 4449 +f 4457 4458 4463 +f 4260 4458 4457 +f 4260 4457 4453 +f 4260 4453 4456 +f 4258 4458 4260 +f 4442 4461 4458 +f 4439 4449 4461 +f 4413 4414 4411 +f 4412 4411 4429 +f 4434 3952 3936 +f 4429 4464 4412 +f 4464 4429 4434 +f 4465 3936 3933 +f 3936 4465 4434 +f 3933 4466 4465 +f 4434 4465 4464 +f 4467 4464 4465 +f 4465 4466 4467 +f 4407 4413 4412 +f 4468 4469 4467 +f 4469 4412 4464 +f 4464 4467 4469 +f 4413 4407 4409 +f 4406 4407 4469 +f 4412 4469 4407 +f 4467 4470 4468 +f 3932 4471 4466 +f 4466 3933 3932 +f 4466 4471 4470 +f 4469 4468 4406 +f 4470 4467 4466 +f 4408 4406 4468 +f 4468 4472 4408 +f 4473 4470 4471 +f 4471 4474 4473 +f 3836 4474 4471 +f 4472 4468 4470 +f 4470 4473 4472 +f 4471 3932 3836 +f 3957 4431 4475 +f 4431 4476 4477 +f 4430 4476 4431 +f 4441 4476 4430 +f 4475 3845 3957 +f 4477 4475 4431 +f 4478 4479 4480 +f 4476 4480 4477 +f 4481 4477 4480 +f 4443 4482 4441 +f 4483 4482 4484 +f 4484 4485 4486 +f 4482 4443 4445 +f 4445 4484 4482 +f 4480 4476 4478 +f 4482 4483 4478 +f 4478 4441 4482 +f 4479 4478 4483 +f 4441 4478 4476 +f 4485 4484 4445 +f 4445 4444 4485 +f 4487 4488 4447 +f 4444 4447 4488 +f 4488 4485 4444 +f 4489 4483 4490 +f 4484 4490 4483 +f 4490 4486 4491 +f 4486 4490 4484 +f 4395 4394 4492 +f 4394 4395 4392 +f 4492 4493 4395 +f 4494 4395 4493 +f 4493 4495 4494 +f 4496 4497 4498 +f 4496 4396 4254 +f 4254 4265 4496 +f 4497 4496 4265 +f 4265 4266 4497 +f 4499 4500 4450 +f 4501 4450 4396 +f 4448 4396 4450 +f 4454 4500 4502 +f 4451 4450 4500 +f 4396 4496 4501 +f 4498 4501 4496 +f 4450 4501 4499 +f 4503 4499 4501 +f 4500 4499 4504 +f 4505 4504 4499 +f 4506 4498 4497 +f 4497 4507 4506 +f 4498 4506 4508 +f 4507 4497 4266 +f 4266 4268 4507 +f 4504 4505 4509 +f 4510 4511 4509 +f 4509 4512 4510 +f 4499 4503 4505 +f 4501 4498 4503 +f 4513 4514 4512 +f 4505 4515 4513 +f 4513 4509 4505 +f 4512 4509 4513 +f 4503 4508 4515 +f 4515 4505 4503 +f 4508 4503 4498 +f 4455 4502 4516 +f 4516 4517 4455 +f 4500 4454 4451 +f 4518 4517 4516 +f 4502 4455 4454 +f 4516 4519 4518 +f 4519 4516 4511 +f 4511 4510 4519 +f 4502 4504 4511 +f 4504 4502 4500 +f 4511 4516 4502 +f 4509 4511 4504 +f 4456 4455 4517 +f 4517 4520 4456 +f 4520 4517 4518 +f 4518 4521 4520 +f 4260 4456 4520 +f 4520 4460 4260 +f 4522 4460 4520 +f 4523 4524 4520 +f 4520 4525 4523 +f 4526 4525 4520 +f 4527 4528 4529 +f 4529 4525 4527 +f 4523 4525 4529 +f 4530 4526 4520 +f 4531 4520 4532 +f 4520 4524 4532 +f 4520 4521 4530 +f 4520 4533 4522 +f 4534 4533 4520 +f 4520 4535 4534 +f 4531 4535 4520 +f 4459 4536 4537 +f 4446 4459 4447 +f 4536 4459 4522 +f 4460 4522 4459 +f 4522 4533 4536 +f 4538 4536 4533 +f 4539 4538 4534 +f 4534 4535 4539 +f 4536 4538 4540 +f 4538 4539 4541 +f 4533 4534 4538 +f 4542 4369 4368 +f 4389 4388 4543 +f 4543 4544 4389 +f 4389 4367 4386 +f 4367 4389 4544 +f 4544 4542 4367 +f 4368 4367 4542 +f 4422 4545 4264 +f 4261 4264 4545 +f 4370 4369 4422 +f 4546 4545 4542 +f 4369 4542 4545 +f 4545 4422 4369 +f 4547 4546 4544 +f 4546 4547 4262 +f 4262 4261 4546 +f 4545 4546 4261 +f 4544 4543 4547 +f 4542 4544 4546 +f 4495 4543 4388 +f 4494 4388 4383 +f 4388 4494 4495 +f 4383 4391 4494 +f 4395 4494 4391 +f 4548 4547 4543 +f 4263 4262 4547 +f 4267 4263 4548 +f 4549 4548 4495 +f 4543 4495 4548 +f 4547 4548 4263 +f 4493 4492 4550 +f 4495 4493 4549 +f 4549 4550 4269 +f 4550 4549 4493 +f 4269 4267 4549 +f 4548 4549 4267 +f 4447 4537 4487 +f 4551 4487 4537 +f 4537 4540 4551 +f 4552 4551 4540 +f 4540 4541 4552 +f 4537 4447 4459 +f 4541 4540 4538 +f 4540 4537 4536 +f 4485 4488 4553 +f 4554 4553 4488 +f 4553 4486 4485 +f 4486 4553 4555 +f 4556 4554 4487 +f 4551 4552 4557 +f 4487 4551 4556 +f 4556 4557 4558 +f 4488 4487 4554 +f 4557 4556 4551 +f 4187 4559 4371 +f 4560 4559 4187 +f 4187 4213 4560 +f 4371 4559 4374 +f 4561 4374 4559 +f 4559 4560 4561 +f 4374 4562 4379 +f 4563 4562 4374 +f 4374 4561 4564 +f 4564 4563 4374 +f 4563 4564 4565 +f 4566 4567 4568 +f 4563 4568 4567 +f 4567 4562 4563 +f 4562 4567 4390 +f 4390 4379 4562 +f 4568 4569 4566 +f 4567 4566 4393 +f 4393 4390 4567 +f 4566 4570 4571 +f 4566 4569 4570 +f 4571 4572 4566 +f 4393 4566 4572 +f 4565 4568 4563 +f 4570 4569 4573 +f 4573 4569 4574 +f 4574 4575 4573 +f 4571 4570 4573 +f 4574 4569 4568 +f 4568 4565 4574 +f 4573 4575 4576 +f 4576 4577 4573 +f 4571 4573 4577 +f 4575 4578 4579 +f 4579 4576 4575 +f 4571 4577 4580 +f 4581 4560 4213 +f 4582 4564 4561 +f 4564 4582 4583 +f 4561 4584 4582 +f 4584 4561 4560 +f 4560 4581 4584 +f 4585 4581 4216 +f 4584 4581 4585 +f 4213 4216 4581 +f 4582 4586 4587 +f 4588 4586 4582 +f 4582 4584 4588 +f 4583 4587 4589 +f 4587 4583 4582 +f 4590 4589 4591 +f 4574 4589 4590 +f 4589 4592 4583 +f 4578 4593 4594 +f 4595 4596 4590 +f 4596 4594 4593 +f 4593 4590 4596 +f 4590 4591 4595 +f 4583 4565 4564 +f 4574 4590 4593 +f 4574 4592 4589 +f 4578 4575 4574 +f 4578 4574 4593 +f 4592 4574 4565 +f 4565 4583 4592 +f 4597 4506 4507 +f 4507 4598 4597 +f 4598 4507 4268 +f 4268 4282 4598 +f 4599 4598 4282 +f 4506 4597 4600 +f 4601 4600 4597 +f 4602 4603 4600 +f 4604 4597 4598 +f 4600 4508 4506 +f 4508 4600 4603 +f 4603 4515 4508 +f 4600 4601 4602 +f 4605 4604 4599 +f 4282 4283 4599 +f 4597 4604 4601 +f 4606 4601 4604 +f 4598 4599 4604 +f 4607 4608 4286 +f 4604 4605 4606 +f 4599 4608 4605 +f 4608 4599 4283 +f 4283 4286 4608 +f 4607 4287 4609 +f 4610 4611 4609 +f 4609 4287 4610 +f 4286 4287 4607 +f 4612 4610 4287 +f 4613 4611 4610 +f 4614 4609 4611 +f 4615 4616 4284 +f 4284 4281 4615 +f 4617 4580 4616 +f 4618 4616 4580 +f 4616 4618 4291 +f 4291 4284 4616 +f 4572 4394 4393 +f 4580 4617 4571 +f 4572 4571 4617 +f 4617 4619 4572 +f 4394 4572 4619 +f 4619 4492 4394 +f 4619 4617 4615 +f 4281 4280 4620 +f 4550 4620 4280 +f 4280 4269 4550 +f 4620 4550 4492 +f 4492 4619 4620 +f 4616 4615 4617 +f 4620 4615 4281 +f 4615 4620 4619 +f 4621 4622 4603 +f 4515 4603 4622 +f 4622 4513 4515 +f 4514 4513 4622 +f 4622 4623 4514 +f 4623 4622 4621 +f 4624 4621 4602 +f 4603 4602 4621 +f 4621 4625 4623 +f 4625 4621 4624 +f 4624 4626 4625 +f 4627 4602 4601 +f 4602 4627 4624 +f 4628 4629 4626 +f 4601 4606 4627 +f 4630 4627 4606 +f 4627 4630 4628 +f 4628 4624 4627 +f 4626 4624 4628 +f 4606 4631 4630 +f 4632 4605 4608 +f 4608 4607 4632 +f 4631 4606 4605 +f 4605 4632 4631 +f 4633 4630 4631 +f 4631 4632 4634 +f 4530 4629 4628 +f 4632 4607 4635 +f 4526 4628 4630 +f 4628 4526 4530 +f 4634 4636 4631 +f 4631 4636 4633 +f 4630 4633 4526 +f 4635 4634 4632 +f 4637 4526 4633 +f 4633 4636 4637 +f 4527 4525 4526 +f 4526 4637 4527 +f 4638 4636 4634 +f 4634 4639 4640 +f 4640 4639 4641 +f 4641 4639 4634 +f 4634 4635 4641 +f 4642 4641 4635 +f 4643 4644 4524 +f 4524 4523 4643 +f 4529 4643 4523 +f 4645 4529 4528 +f 4643 4529 4645 +f 4641 4642 4640 +f 4646 4642 4647 +f 4647 4609 4614 +f 4648 4640 4642 +f 4635 4647 4642 +f 4635 4607 4609 +f 4609 4647 4635 +f 4649 4634 4640 +f 4638 4634 4649 +f 4528 4527 4637 +f 4637 4638 4528 +f 4636 4638 4637 +f 4535 4531 4650 +f 4531 4532 4651 +f 4644 4651 4532 +f 4532 4524 4644 +f 4651 4650 4531 +f 4539 4650 4652 +f 4650 4651 4653 +f 4650 4539 4535 +f 4654 4655 4644 +f 4645 4654 4643 +f 4644 4643 4654 +f 4655 4653 4651 +f 4651 4644 4655 +f 4652 4541 4539 +f 4652 4653 4656 +f 4653 4652 4650 +f 4541 4652 4657 +f 4658 4659 4655 +f 4659 4656 4653 +f 4653 4655 4659 +f 4660 4658 4654 +f 4655 4654 4658 +f 4528 4661 4645 +f 4638 4649 4662 +f 4661 4528 4638 +f 4638 4662 4661 +f 4649 4663 4664 +f 4649 4665 4663 +f 4662 4664 4666 +f 4660 4645 4661 +f 4661 4666 4660 +f 4654 4645 4660 +f 4666 4661 4662 +f 4664 4662 4649 +f 4667 4613 4612 +f 4668 4612 4288 +f 4287 4288 4612 +f 4669 4614 4613 +f 4610 4612 4613 +f 4611 4613 4614 +f 4613 4667 4669 +f 4612 4668 4667 +f 4669 4290 4670 +f 4671 4670 4290 +f 4290 4289 4671 +f 4288 4290 4668 +f 4668 4290 4669 +f 4669 4667 4668 +f 4614 4672 4647 +f 4672 4614 4669 +f 4673 4648 4646 +f 4642 4646 4648 +f 4674 4646 4672 +f 4665 4648 4673 +f 4649 4640 4648 +f 4647 4672 4646 +f 4665 4649 4648 +f 4675 4676 4677 +f 4670 4678 4679 +f 4669 4679 4672 +f 4676 4675 4680 +f 4680 4678 4670 +f 4675 4681 4665 +f 4665 4680 4675 +f 4665 4673 4680 +f 4672 4679 4674 +f 4646 4674 4673 +f 4673 4682 4680 +f 4677 4681 4675 +f 4683 4674 4679 +f 4682 4673 4674 +f 4679 4678 4683 +f 4683 4678 4680 +f 4680 4682 4683 +f 4674 4683 4682 +f 4670 4684 4680 +f 4685 4684 4670 +f 4686 4676 4684 +f 4679 4669 4670 +f 4680 4684 4676 +f 4687 4688 4689 +f 4690 4691 4689 +f 4689 4691 4687 +f 4576 4579 4692 +f 4594 4579 4578 +f 4579 4594 4693 +f 4692 4693 4694 +f 4693 4692 4579 +f 4695 4692 4696 +f 4577 4576 4695 +f 4695 4580 4577 +f 4692 4695 4576 +f 4580 4695 4618 +f 4696 4694 4295 +f 4696 4618 4695 +f 4292 4291 4618 +f 4295 4292 4696 +f 4694 4696 4692 +f 4618 4696 4292 +f 4697 4698 4699 +f 4699 4700 4697 +f 4594 4596 4697 +f 4596 4595 4698 +f 4693 4697 4700 +f 4698 4697 4596 +f 4697 4693 4594 +f 4700 4699 4297 +f 4700 4694 4693 +f 4296 4295 4694 +f 4694 4700 4296 +f 4657 4656 4701 +f 4656 4657 4652 +f 4552 4657 4702 +f 4703 4704 4659 +f 4704 4701 4656 +f 4656 4659 4704 +f 4657 4552 4541 +f 4701 4702 4657 +f 4705 4706 4702 +f 4707 4705 4701 +f 4702 4701 4705 +f 4702 4557 4552 +f 4701 4704 4707 +f 4557 4702 4706 +f 4666 4708 4709 +f 4708 4666 4664 +f 4663 4710 4711 +f 4709 4660 4666 +f 4711 4664 4663 +f 4658 4660 4709 +f 4664 4711 4708 +f 4710 4663 4665 +f 4704 4703 4690 +f 4691 4709 4708 +f 4659 4658 4703 +f 4709 4703 4658 +f 4703 4709 4691 +f 4691 4690 4703 +f 4690 4707 4704 +f 4708 4687 4691 +f 4712 4711 4710 +f 4711 4712 4687 +f 4687 4708 4711 +f 4688 4687 4712 +f 4297 4296 4700 +f 4713 4714 4715 +f 4716 4713 4717 +f 4715 4717 4713 +f 4716 4686 4685 +f 4713 4716 4718 +f 4714 4713 4719 +f 4720 4671 4289 +f 4289 4293 4720 +f 4721 4720 4293 +f 4293 4294 4721 +f 4718 4685 4671 +f 4685 4718 4716 +f 4671 4720 4718 +f 4719 4718 4720 +f 4677 4722 4723 +f 4722 4677 4676 +f 4670 4671 4685 +f 4723 4724 4677 +f 4676 4686 4722 +f 4725 4724 4723 +f 4684 4685 4686 +f 4726 4723 4722 +f 4686 4716 4727 +f 4728 4723 4726 +f 4722 4727 4726 +f 4723 4728 4725 +f 4727 4722 4686 +f 4720 4721 4719 +f 4729 4721 4294 +f 4730 4719 4721 +f 4294 4298 4729 +f 4719 4730 4714 +f 4718 4719 4713 +f 4721 4729 4730 +f 4727 4717 4731 +f 4731 4726 4727 +f 4732 4726 4731 +f 4717 4727 4716 +f 4726 4732 4728 +f 4731 4733 4732 +f 4734 4735 4733 +f 4733 4731 4734 +f 4734 4731 4717 +f 4717 4715 4734 +f 4710 4736 4712 +f 4688 4712 4736 +f 4736 4737 4688 +f 4737 4736 4738 +f 4738 4739 4737 +f 4665 4740 4738 +f 4738 4741 4665 +f 4665 4741 4710 +f 4681 4740 4665 +f 4739 4738 4740 +f 4736 4710 4741 +f 4741 4738 4736 +f 4681 4742 4740 +f 4733 4735 4743 +f 4728 4743 4725 +f 4744 4724 4725 +f 4728 4732 4743 +f 4681 4677 4724 +f 4733 4743 4732 +f 4745 4744 4743 +f 4743 4735 4746 +f 4681 4744 4745 +f 4724 4744 4681 +f 4725 4743 4744 +f 4747 4748 4749 +f 4351 4750 4352 +f 4190 4352 4751 +f 4751 4197 4190 +f 4750 4751 4352 +f 4750 4749 4751 +f 4749 4748 4751 +f 4351 4752 4753 +f 4753 4750 4351 +f 4353 4752 4351 +f 4750 4753 4754 +f 4755 4754 4753 +f 4756 4757 4754 +f 4747 4757 4756 +f 4754 4749 4750 +f 4754 4757 4749 +f 4749 4757 4747 +f 4753 4758 4755 +f 4755 4758 4759 +f 4353 4361 4760 +f 4760 4752 4353 +f 4752 4760 4758 +f 4758 4753 4752 +f 4362 4761 4760 +f 4761 4362 4402 +f 4402 4762 4761 +f 4760 4361 4362 +f 4763 4759 4764 +f 4761 4759 4758 +f 4759 4761 4762 +f 4762 4764 4759 +f 4758 4760 4761 +f 4756 4755 4763 +f 4756 4765 4747 +f 4765 4756 4766 +f 4766 4767 4765 +f 4754 4755 4756 +f 4759 4763 4755 +f 4766 4763 4768 +f 4767 4766 4769 +f 4770 4767 4771 +f 4763 4766 4756 +f 4764 4768 4763 +f 4197 4751 4772 +f 4772 4198 4197 +f 4751 4748 4773 +f 4773 4772 4751 +f 4774 4775 4747 +f 4748 4747 4775 +f 4775 4773 4748 +f 4773 4776 4777 +f 4776 4773 4775 +f 4775 4778 4776 +f 4779 4778 4775 +f 4777 4772 4773 +f 4198 4772 4777 +f 4777 4201 4198 +f 4765 4767 4780 +f 4780 4774 4765 +f 4747 4765 4774 +f 4770 4781 4780 +f 4770 4780 4767 +f 4775 4774 4779 +f 4782 4783 4780 +f 4774 4780 4783 +f 4783 4779 4774 +f 4784 4785 4780 +f 4785 4784 4786 +f 4780 4781 4784 +f 4787 4785 4788 +f 4788 4785 4786 +f 4780 4785 4782 +f 4785 4787 4789 +f 4789 4782 4785 +f 4790 4791 4792 +f 4408 4792 4791 +f 4791 4405 4408 +f 4303 4300 4791 +f 4405 4791 4300 +f 4300 4277 4405 +f 4793 4792 4794 +f 4795 4793 4796 +f 4794 4796 4793 +f 4797 4796 4798 +f 4799 4798 4796 +f 4800 4790 4793 +f 4792 4793 4790 +f 4792 4408 4472 +f 4799 4473 4474 +f 4474 4801 4799 +f 3831 4801 4474 +f 4474 3836 3831 +f 4794 4472 4473 +f 4473 4799 4794 +f 4798 4799 4801 +f 4796 4794 4799 +f 4801 3831 3838 +f 4472 4794 4792 +f 4791 4790 4303 +f 4306 4305 4800 +f 4790 4800 4305 +f 4305 4303 4790 +f 4802 4795 4797 +f 4796 4797 4795 +f 4803 4797 4804 +f 4805 4800 4795 +f 4793 4795 4800 +f 4762 4402 4418 +f 4418 4806 4762 +f 4764 4762 4806 +f 4806 4807 4764 +f 4806 4418 4403 +f 4403 4808 4806 +f 4807 4806 4808 +f 4808 4809 4807 +f 4809 4808 4275 +f 4275 4301 4809 +f 4808 4403 4276 +f 4276 4275 4808 +f 4810 4811 4769 +f 4768 4764 4807 +f 4807 4810 4768 +f 4810 4807 4809 +f 4769 4768 4810 +f 4811 4810 4812 +f 4809 4812 4810 +f 4301 4302 4812 +f 4812 4809 4301 +f 4798 4804 4797 +f 4801 4813 4798 +f 4813 3838 3819 +f 4804 4798 4813 +f 4813 4814 4804 +f 3838 4813 4801 +f 3819 4814 4813 +f 4815 4804 4814 +f 4814 4816 4815 +f 3822 4816 4814 +f 4814 3819 3822 +f 4797 4803 4802 +f 4804 4815 4803 +f 4795 4802 4805 +f 4817 4306 4818 +f 4818 4819 4817 +f 4818 4306 4805 +f 4800 4805 4306 +f 4820 4818 4805 +f 4805 4802 4821 +f 4821 4822 4805 +f 4818 4820 4823 +f 4815 4824 4825 +f 4803 4825 4821 +f 4821 4802 4803 +f 4805 4822 4820 +f 4825 4803 4815 +f 4477 4826 4475 +f 3822 3845 4475 +f 4477 4481 4826 +f 4475 4816 3822 +f 4824 4826 4827 +f 4824 4815 4816 +f 4826 4824 4475 +f 4816 4475 4824 +f 4821 4828 4829 +f 4830 4831 4820 +f 4821 4830 4822 +f 4831 4830 4829 +f 4829 4828 4831 +f 4820 4822 4830 +f 4829 4830 4821 +f 4827 4825 4824 +f 4832 4831 4828 +f 4823 4820 4831 +f 4833 4828 4821 +f 4821 4825 4833 +f 4827 4833 4825 +f 4826 4834 4827 +f 4828 4833 4832 +f 4831 4832 4823 +f 4835 4836 4837 +f 4837 4836 4838 +f 4838 4489 4837 +f 4479 4839 4481 +f 4481 4480 4479 +f 4834 4826 4481 +f 4489 4838 4839 +f 4839 4479 4489 +f 4818 4840 4841 +f 4842 4843 4844 +f 4843 4842 4845 +f 4839 4845 4842 +f 4823 4840 4818 +f 4845 4839 4838 +f 4817 4846 4308 +f 4819 4841 4846 +f 4834 4844 4833 +f 4833 4827 4834 +f 4844 4834 4842 +f 4481 4842 4834 +f 4842 4481 4839 +f 4781 4770 4847 +f 4768 4769 4766 +f 4771 4847 4770 +f 4769 4771 4767 +f 4771 4769 4811 +f 4811 4848 4771 +f 4847 4771 4848 +f 4848 4849 4850 +f 4849 4851 4850 +f 4851 4852 4850 +f 4852 4851 4309 +f 4309 4311 4852 +f 4311 4312 4852 +f 4312 4316 4852 +f 4853 4812 4302 +f 4853 4849 4848 +f 4853 4854 4848 +f 4302 4307 4853 +f 4849 4853 4307 +f 4812 4853 4811 +f 4848 4811 4853 +f 4307 4313 4849 +f 4313 4310 4849 +f 4851 4849 4310 +f 4310 4309 4851 +f 4855 4856 4857 +f 4857 4858 4855 +f 4859 4855 4858 +f 4858 4857 4860 +f 4861 4858 4862 +f 4863 4864 4865 +f 4856 4863 4865 +f 4865 4857 4856 +f 4866 4867 4850 +f 4848 4850 4847 +f 4868 4866 4864 +f 4850 4864 4866 +f 4867 4847 4850 +f 4850 4848 4854 +f 4869 4870 4864 +f 4869 4850 4852 +f 4870 4865 4864 +f 4864 4850 4869 +f 4491 4837 4490 +f 4837 4491 4835 +f 4555 4491 4486 +f 4490 4837 4489 +f 4483 4489 4479 +f 4871 4555 4553 +f 4860 4872 4555 +f 4553 4554 4871 +f 4872 4835 4491 +f 4555 4871 4860 +f 4491 4555 4872 +f 4554 4556 4873 +f 4558 4873 4556 +f 4862 4860 4871 +f 4873 4871 4554 +f 4871 4873 4862 +f 4874 4862 4873 +f 4873 4558 4874 +f 4865 4870 4835 +f 4857 4865 4872 +f 4860 4862 4858 +f 4862 4874 4861 +f 4835 4872 4865 +f 4872 4860 4857 +f 4869 4852 4316 +f 4875 4316 4876 +f 4870 4869 4836 +f 4316 4875 4869 +f 4875 4838 4836 +f 4836 4835 4870 +f 4838 4875 4845 +f 4836 4869 4875 +f 4845 4876 4843 +f 4876 4845 4875 +f 4846 4817 4819 +f 4841 4819 4818 +f 4308 4306 4817 +f 4846 4877 4318 +f 4318 4308 4846 +f 4841 4878 4877 +f 4877 4846 4841 +f 4840 4879 4878 +f 4878 4841 4840 +f 4880 4315 4322 +f 4319 4881 4882 +f 4882 4320 4319 +f 4320 4882 4883 +f 4883 4322 4320 +f 4866 4868 4784 +f 4867 4866 4781 +f 4847 4867 4781 +f 4784 4781 4866 +f 4788 4786 4884 +f 4786 4784 4868 +f 4868 4884 4786 +f 4884 4868 4863 +f 4885 4886 4856 +f 4856 4855 4885 +f 4887 4885 4855 +f 4864 4863 4868 +f 4886 4884 4863 +f 4863 4856 4886 +f 4888 4788 4886 +f 4886 4885 4888 +f 4889 4888 4885 +f 4884 4886 4788 +f 4788 4888 4787 +f 4890 4891 4892 +f 4893 4894 4895 +f 4892 4895 4890 +f 4891 4890 4314 +f 4323 4321 4896 +f 4897 4896 4321 +f 4321 4317 4897 +f 4890 4897 4317 +f 4317 4314 4890 +f 4898 4896 4899 +f 4900 4899 4896 +f 4894 4901 4900 +f 4900 4895 4894 +f 4896 4897 4900 +f 4895 4900 4897 +f 4897 4890 4895 +f 4902 4903 4901 +f 4901 4894 4902 +f 4904 4905 4906 +f 4899 4900 4901 +f 4907 4903 4904 +f 4901 4908 4899 +f 4908 4901 4903 +f 4903 4907 4908 +f 4904 4909 4907 +f 4910 4899 4908 +f 4911 4907 4909 +f 4906 4909 4904 +f 4909 4906 4912 +f 4913 4908 4907 +f 4908 4913 4910 +f 4912 4914 4909 +f 4907 4911 4913 +f 4909 4914 4911 +f 4915 4916 4917 +f 4918 4883 4882 +f 4919 4920 4916 +f 4880 4322 4883 +f 4881 4916 4920 +f 4882 4920 4918 +f 4921 4918 4920 +f 4920 4882 4881 +f 4880 4922 4843 +f 4922 4883 4918 +f 4922 4880 4883 +f 4843 4876 4880 +f 4315 4880 4876 +f 4876 4316 4315 +f 4923 4922 4918 +f 4891 4318 4879 +f 4314 4318 4891 +f 4924 4891 4879 +f 4925 4879 4840 +f 4877 4878 4879 +f 4879 4318 4877 +f 4895 4892 4893 +f 4926 4927 4928 +f 4894 4893 4929 +f 4929 4902 4894 +f 4902 4929 4926 +f 4928 4930 4926 +f 4905 4904 4930 +f 4930 4928 4905 +f 4926 4930 4902 +f 4903 4902 4930 +f 4930 4904 4903 +f 4924 4931 4892 +f 4879 4925 4924 +f 4931 4932 4893 +f 4893 4892 4931 +f 4893 4932 4933 +f 4933 4929 4893 +f 4892 4891 4924 +f 4934 4921 4933 +f 4833 4935 4936 +f 4923 4934 4833 +f 4936 4832 4833 +f 4935 4833 4934 +f 4925 4823 4832 +f 4832 4936 4925 +f 4932 4931 4934 +f 4934 4931 4935 +f 4934 4933 4932 +f 4931 4937 4938 +f 4931 4924 4937 +f 4937 4936 4935 +f 4935 4938 4937 +f 4924 4925 4936 +f 4936 4937 4924 +f 4931 4938 4935 +f 4922 4923 4844 +f 4844 4843 4922 +f 4934 4923 4921 +f 4840 4823 4925 +f 4833 4844 4923 +f 4923 4918 4921 +f 4939 4915 4940 +f 4920 4919 4921 +f 4933 4921 4919 +f 4940 4941 4939 +f 4942 4919 4915 +f 4916 4915 4919 +f 4912 4906 4943 +f 4905 4943 4906 +f 4942 4926 4929 +f 4944 4943 4928 +f 4905 4928 4943 +f 4943 4927 4926 +f 4915 4939 4942 +f 4929 4933 4942 +f 4945 4943 4942 +f 4919 4942 4933 +f 4926 4942 4943 +f 4945 4912 4943 +f 4946 4945 4947 +f 4946 4948 4945 +f 4949 4945 4948 +f 4949 4912 4945 +f 4950 4951 4952 +f 4952 4585 4226 +f 4226 4239 4952 +f 4953 4952 4239 +f 4951 4588 4585 +f 4585 4588 4584 +f 4585 4952 4951 +f 4216 4226 4585 +f 4951 4950 4954 +f 4952 4953 4950 +f 4955 4953 4241 +f 4241 4243 4955 +f 4950 4956 4957 +f 4956 4950 4953 +f 4953 4955 4956 +f 4239 4241 4953 +f 4958 4959 4960 +f 4961 4962 4960 +f 4960 4959 4961 +f 4957 4954 4950 +f 4963 4964 4954 +f 4965 4954 4964 +f 4954 4957 4963 +f 4966 4964 4967 +f 4968 4967 4964 +f 4964 4963 4968 +f 4588 4951 4965 +f 4965 4586 4588 +f 4966 4587 4586 +f 4586 4965 4966 +f 4964 4966 4965 +f 4587 4966 4969 +f 4954 4965 4951 +f 4970 4971 4972 +f 4591 4972 4971 +f 4971 4595 4591 +f 4972 4973 4970 +f 4589 4972 4591 +f 4962 4972 4589 +f 4699 4974 4337 +f 4698 4975 4974 +f 4974 4699 4698 +f 4595 4971 4975 +f 4975 4698 4595 +f 4975 4976 4977 +f 4977 4974 4975 +f 4971 4970 4976 +f 4976 4975 4971 +f 4978 4977 4976 +f 4979 4976 4980 +f 4974 4977 4338 +f 4981 4982 4983 +f 4984 4982 4707 +f 4980 4985 4986 +f 4986 4983 4982 +f 4982 4984 4986 +f 4979 4986 4984 +f 4986 4979 4980 +f 4976 4979 4978 +f 4338 4337 4974 +f 4338 4987 4988 +f 4977 4978 4987 +f 4987 4338 4977 +f 4707 4690 4984 +f 4982 4981 4705 +f 4705 4707 4982 +f 4981 4989 4706 +f 4706 4705 4981 +f 4706 4558 4557 +f 4989 4874 4558 +f 4558 4706 4989 +f 4689 4688 4978 +f 4978 4979 4689 +f 4984 4689 4979 +f 4689 4984 4690 +f 4858 4861 4859 +f 4990 4859 4861 +f 4861 4991 4990 +f 4992 4983 4993 +f 4993 4990 4992 +f 4991 4992 4990 +f 4991 4861 4874 +f 4874 4989 4991 +f 4983 4992 4981 +f 4992 4991 4989 +f 4989 4981 4992 +f 4976 4980 4994 +f 4336 4995 4996 +f 4338 4997 4995 +f 4336 4338 4977 +f 4995 4336 4338 +f 4994 4977 4976 +f 4977 4994 4336 +f 4996 4334 4336 +f 4998 4962 4961 +f 4998 4961 4999 +f 4999 5000 4998 +f 4961 4959 5001 +f 5001 4999 4961 +f 4962 4998 5002 +f 5000 4999 5003 +f 5003 4985 5000 +f 4985 5003 4986 +f 4983 4986 5003 +f 5003 4993 4983 +f 5004 5000 4985 +f 4985 4980 5004 +f 4993 5003 4999 +f 5005 4970 4973 +f 5004 5005 5006 +f 4970 5005 4980 +f 4980 4976 4970 +f 5005 5004 4980 +f 4973 5006 5005 +f 4973 4962 5006 +f 5002 4998 5000 +f 5002 5006 4962 +f 5000 5004 5002 +f 5006 5002 5004 +f 4855 4859 4887 +f 4859 4990 5007 +f 4319 5008 4881 +f 5008 4324 5009 +f 5008 4319 4324 +f 5007 4887 4859 +f 5001 5007 4990 +f 4990 4993 5001 +f 4999 5001 4993 +f 5007 5001 4959 +f 4887 5007 4958 +f 4959 4958 5007 +f 4958 4960 4889 +f 4885 4887 4889 +f 4958 4889 4887 +f 4328 5010 5011 +f 5011 4327 4328 +f 5009 4324 4327 +f 5010 4334 4996 +f 4327 5011 5009 +f 5010 4328 4334 +f 5012 5009 5011 +f 4917 5009 5012 +f 5008 4917 4916 +f 4916 4881 5008 +f 5012 5011 5010 +f 4917 5008 5009 +f 4787 4888 4889 +f 4889 4960 4787 +f 4325 4323 4898 +f 4789 4787 4960 +f 4960 5013 4789 +f 4896 4898 4323 +f 4910 5014 5015 +f 5015 4898 4910 +f 4899 4910 4898 +f 4898 5015 4325 +f 5014 4910 4913 +f 4949 5016 4914 +f 4914 4912 4949 +f 4911 5017 5018 +f 4913 5018 5014 +f 5018 4913 4911 +f 5017 4911 4914 +f 4914 5016 5017 +f 5019 5020 4331 +f 4331 4329 5019 +f 5015 5019 4329 +f 5014 5021 5019 +f 4329 4325 5015 +f 5019 5015 5014 +f 5017 5022 5023 +f 5023 5018 5017 +f 5022 5017 5016 +f 5018 5023 5021 +f 5021 5014 5018 +f 5024 5023 5022 +f 5025 5026 5027 +f 5028 5029 5026 +f 5010 5030 5012 +f 5030 5010 4996 +f 5030 4996 4995 +f 5031 5030 5032 +f 4332 5027 4330 +f 4946 5033 5034 +f 5034 5033 5035 +f 5034 4948 4946 +f 4948 5034 5016 +f 5016 4949 4948 +f 5016 5034 5022 +f 5035 5022 5034 +f 5036 5037 5024 +f 5023 5024 5037 +f 5037 5021 5023 +f 5038 5020 5037 +f 5021 5037 5020 +f 5020 5019 5021 +f 5022 5035 5024 +f 5039 5024 5035 +f 5035 5040 5039 +f 5040 5035 5033 +f 5033 4946 4947 +f 5033 5041 5040 +f 4947 5041 5033 +f 5042 5012 5030 +f 4940 4917 5012 +f 5043 5042 5030 +f 4940 5012 5042 +f 5031 5044 5045 +f 5045 5043 5031 +f 5042 5043 4940 +f 4917 4940 4915 +f 5030 5031 5043 +f 5046 5045 5044 +f 5045 5046 4939 +f 5043 5045 4941 +f 4941 4940 5043 +f 4942 5046 4945 +f 4942 4939 5046 +f 4939 4941 5045 +f 4988 4997 4338 +f 4688 4987 4978 +f 4337 4297 4699 +f 4997 5047 5032 +f 5032 4995 4997 +f 4997 4988 5048 +f 5048 5047 4997 +f 4995 5032 5030 +f 4987 4688 4737 +f 4737 4988 4987 +f 4740 4742 5049 +f 5049 5050 4740 +f 4740 5050 4739 +f 5048 4739 5050 +f 5050 5049 5048 +f 4988 4737 4739 +f 4739 5048 4988 +f 5047 5048 5049 +f 5051 5032 5047 +f 5052 5051 5053 +f 5047 5053 5051 +f 4298 4299 5054 +f 4299 4339 5055 +f 4969 4589 4587 +f 5054 4729 4298 +f 5056 5055 4339 +f 5055 5054 4299 +f 5057 5058 5054 +f 5058 4730 4729 +f 4729 5054 5058 +f 5054 5055 5057 +f 4730 5058 5059 +f 5060 5061 5062 +f 5063 5064 5065 +f 5061 5060 5066 +f 5066 5067 5068 +f 5062 5065 5060 +f 5069 4734 4715 +f 5070 5059 5058 +f 4714 5059 5071 +f 5071 4715 4714 +f 5072 5071 5059 +f 4715 5071 5069 +f 5059 4714 4730 +f 5059 5070 5072 +f 5070 5062 5061 +f 5061 5072 5070 +f 5063 5057 5055 +f 5055 5056 5063 +f 5058 5057 5070 +f 5062 5070 5057 +f 5057 5063 5062 +f 5053 5049 4742 +f 4742 5073 5053 +f 5049 5053 5047 +f 5053 5073 5052 +f 5074 5073 4742 +f 5066 5075 5061 +f 5072 5061 5075 +f 5075 5068 5076 +f 5068 5075 5066 +f 5076 4746 5077 +f 5076 5078 5075 +f 5075 5078 5072 +f 4735 5077 4746 +f 4735 4734 5069 +f 5069 5077 4735 +f 5077 5069 5078 +f 5078 5069 5071 +f 5078 5076 5077 +f 5071 5072 5078 +f 5076 5068 4746 +f 4745 5066 5060 +f 4746 5067 5066 +f 5067 4746 5068 +f 5060 5074 4745 +f 5066 4745 4746 +f 4743 4746 4745 +f 5074 4742 4681 +f 5079 5073 5074 +f 4681 4745 5074 +f 4973 4972 4962 +f 4960 4962 4969 +f 4589 4969 4962 +f 4339 4341 5056 +f 4967 4969 4966 +f 5013 4960 4967 +f 4333 4331 5020 +f 4967 4968 5013 +f 4969 4967 4960 +f 5080 5028 5038 +f 5037 5036 5038 +f 5020 5038 4333 +f 5028 5025 4332 +f 4332 4333 5028 +f 5028 4333 5038 +f 5026 5025 5028 +f 5027 4332 5025 +f 5024 5039 5036 +f 5052 5081 5044 +f 5051 5052 5044 +f 5032 5051 5031 +f 5082 5083 5084 +f 5085 5052 5083 +f 5044 5031 5051 +f 5081 5039 5040 +f 5038 5036 5086 +f 5086 5036 5039 +f 5087 5040 5041 +f 5046 5041 4947 +f 5039 5081 5086 +f 5038 5088 5080 +f 5041 5046 5087 +f 5040 5087 5081 +f 4947 4945 5046 +f 5086 5088 5038 +f 5052 5085 5086 +f 5083 5082 5085 +f 5052 5086 5081 +f 5084 5080 5082 +f 5044 5087 5046 +f 5044 5081 5087 +f 5089 5088 5086 +f 5080 5088 5089 +f 5090 5085 5082 +f 5082 5089 5090 +f 5089 5082 5080 +f 5086 5090 5089 +f 5085 5090 5086 +f 5091 5056 4341 +f 5064 5063 5056 +f 5065 5062 5063 +f 5056 5091 5064 +f 5091 4340 5092 +f 4341 4340 5091 +f 5092 4340 5093 +f 5093 5094 5092 +f 5095 5091 5092 +f 5065 5079 5074 +f 5074 5060 5065 +f 5065 5064 5096 +f 5092 5097 5095 +f 5064 5091 5095 +f 5095 5096 5064 +f 5096 5079 5065 +f 5052 5098 5099 +f 5097 5084 5083 +f 5083 5099 5097 +f 5099 5083 5052 +f 5096 5098 5052 +f 5079 5052 5073 +f 5052 5079 5096 +f 5095 5097 5099 +f 5096 5100 5098 +f 5099 5101 5095 +f 5101 5099 5098 +f 5096 5095 5101 +f 5098 5100 5101 +f 5096 5101 5100 +f 5093 5027 5026 +f 5027 5093 4340 +f 5029 5092 5094 +f 5097 5092 5029 +f 5094 5026 5029 +f 4340 4330 5027 +f 5084 5029 5028 +f 5026 5094 5093 +f 5028 5080 5084 +f 5029 5084 5097 diff --git a/resources/dungeon_all_tiles_navmesh.bin b/resources/dungeon_all_tiles_navmesh.bin new file mode 100644 index 0000000000000000000000000000000000000000..6f538e6cca97fa4050e4e63ee9f3b2dd0ed40e24 GIT binary patch literal 36228 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 BH&p-t literal 0 HcmV?d00001 diff --git a/resources/dungeon_all_tiles_navmesh_32bit.bin b/resources/dungeon_all_tiles_navmesh_32bit.bin new file mode 100644 index 0000000000000000000000000000000000000000..f20ef97104e8d19091b241b679e70fe915d871c3 GIT binary patch literal 32460 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 literal 0 HcmV?d00001 diff --git a/resources/dungeon_all_tiles_tilecache.bin b/resources/dungeon_all_tiles_tilecache.bin new file mode 100644 index 0000000000000000000000000000000000000000..ba9d7c33702b0d8becb0f8cea4f9914153520bc2 GIT binary patch literal 18767 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%0D=-PBu&{ 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 eAbeB{w-zz literal 0 HcmV?d00001 diff --git a/resources/test_boundstree.zip b/resources/test_boundstree.zip new file mode 100644 index 0000000000000000000000000000000000000000..9099dc5ae47dbbdf4bc20bfbb0c00280db027530 GIT binary patch literal 218183 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

ibl^~_87s(7yISmuk+{4=M}R=8 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 literal 0 HcmV?d00001 diff --git a/resources/test_tiles.voxels b/resources/test_tiles.voxels new file mode 100644 index 0000000000000000000000000000000000000000..03924e64561a10c35a77670f4d487997c9d8d1e0 GIT binary patch literal 11162953 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 HE`k37uM5J6 literal 0 HcmV?d00001 diff --git a/src/DotRecast.Core/ArrayUtils.cs b/src/DotRecast.Core/ArrayUtils.cs new file mode 100644 index 0000000..ac9d224 --- /dev/null +++ b/src/DotRecast.Core/ArrayUtils.cs @@ -0,0 +1,41 @@ +using System; + +namespace DotRecast.Core; + +public static class ArrayUtils +{ + public static T[] CopyOf(T[] source, int startIdx, int length) + { + var deatArr = new T[length]; + for (int i = 0; i < length; ++i) + { + deatArr[i] = source[startIdx + i]; + } + + return deatArr; + } + + public static T[] CopyOf(T[] source, int length) + { + var deatArr = new T[length]; + var count = Math.Max(0, Math.Min(source.Length, length)); + for (int i = 0; i < count; ++i) + { + deatArr[i] = source[i]; + } + + return deatArr; + } + + public static T[][] Of(int len1, int len2) + { + var temp = new T[len1][]; + + for (int i = 0; i < len1; ++i) + { + temp[i] = new T[len2]; + } + + return temp; + } +} \ No newline at end of file diff --git a/src/DotRecast.Core/AtomicBoolean.cs b/src/DotRecast.Core/AtomicBoolean.cs new file mode 100644 index 0000000..19724e5 --- /dev/null +++ b/src/DotRecast.Core/AtomicBoolean.cs @@ -0,0 +1,18 @@ +using System.Threading; + +namespace DotRecast.Core; + +public class AtomicBoolean +{ + private volatile int _location; + + public bool set(bool v) + { + return 0 != Interlocked.Exchange(ref _location, v ? 1 : 0); + } + + public bool get() + { + return 0 != _location; + } +} \ No newline at end of file diff --git a/src/DotRecast.Core/AtomicFloat.cs b/src/DotRecast.Core/AtomicFloat.cs new file mode 100644 index 0000000..101a1bb --- /dev/null +++ b/src/DotRecast.Core/AtomicFloat.cs @@ -0,0 +1,28 @@ +using System.Threading; + +namespace DotRecast.Core; + +public class AtomicFloat +{ + private volatile float _location; + + public AtomicFloat(float location) + { + _location = location; + } + + public float Get() + { + return _location; + } + + public float Exchange(float exchange) + { + return Interlocked.Exchange(ref _location, exchange); + } + + public float CompareExchange(float value, float comparand) + { + return Interlocked.CompareExchange(ref _location, value, comparand); + } +} \ No newline at end of file diff --git a/src/DotRecast.Core/AtomicInteger.cs b/src/DotRecast.Core/AtomicInteger.cs new file mode 100644 index 0000000..0f09510 --- /dev/null +++ b/src/DotRecast.Core/AtomicInteger.cs @@ -0,0 +1,66 @@ +using System.Threading; + +namespace DotRecast.Core; + +public class AtomicInteger +{ + private volatile int _location; + + public AtomicInteger() : this(0) + { + + } + + public AtomicInteger(int location) + { + _location = location; + } + + public int IncrementAndGet() + { + return Interlocked.Increment(ref _location); + } + + public int GetAndIncrement() + { + var next = Interlocked.Increment(ref _location); + return next - 1; + } + + + public int DecrementAndGet() + { + return Interlocked.Decrement(ref _location); + } + + public int Read() + { + return _location; + } + + public int GetSoft() + { + return _location; + } + + public int Exchange(int exchange) + { + return Interlocked.Exchange(ref _location, exchange); + } + + public int Decrease(int value) + { + return Interlocked.Add(ref _location, -value); + } + + public int CompareExchange(int value, int comparand) + { + return Interlocked.CompareExchange(ref _location, value, comparand); + } + + public int Add(int value) + { + return Interlocked.Add(ref _location, value); + } + +} \ No newline at end of file diff --git a/src/DotRecast.Core/AtomicLong.cs b/src/DotRecast.Core/AtomicLong.cs new file mode 100644 index 0000000..b3103e1 --- /dev/null +++ b/src/DotRecast.Core/AtomicLong.cs @@ -0,0 +1,52 @@ +using System.Threading; + +namespace DotRecast.Core; + +public class AtomicLong +{ + private long _location; + + public AtomicLong() : this(0) + { + } + + public AtomicLong(long location) + { + _location = location; + } + + public long IncrementAndGet() + { + return Interlocked.Increment(ref _location); + } + + public long DecrementAndGet() + { + return Interlocked.Decrement(ref _location); + } + + public long Read() + { + return Interlocked.Read(ref _location); + } + + public long Exchange(long exchange) + { + return Interlocked.Exchange(ref _location, exchange); + } + + public long Decrease(long value) + { + return Interlocked.Add(ref _location, -value); + } + + public long CompareExchange(long value, long comparand) + { + return Interlocked.CompareExchange(ref _location, value, comparand); + } + + public long AddAndGet(long value) + { + return Interlocked.Add(ref _location, value); + } +} \ No newline at end of file diff --git a/src/DotRecast.Core/ByteBuffer.cs b/src/DotRecast.Core/ByteBuffer.cs new file mode 100644 index 0000000..96dc709 --- /dev/null +++ b/src/DotRecast.Core/ByteBuffer.cs @@ -0,0 +1,128 @@ +using System; +using System.Buffers.Binary; + +namespace DotRecast.Core; + +public class ByteBuffer +{ + private ByteOrder _order; + private byte[] _bytes; + private int _position; + + public ByteBuffer(byte[] bytes) + { + _order = BitConverter.IsLittleEndian + ? ByteOrder.LITTLE_ENDIAN + : ByteOrder.BIG_ENDIAN; + + _bytes = bytes; + _position = 0; + } + + public ByteOrder order() + { + return _order; + } + + public void order(ByteOrder order) + { + _order = order; + } + + public int limit() { + return _bytes.Length - _position; + } + + public int remaining() { + int rem = limit(); + return rem > 0 ? rem : 0; + } + + + public void position(int pos) + { + _position = pos; + } + + public int position() + { + return _position; + } + + public Span ReadBytes(int length) + { + var nextPos = _position + length; + (nextPos, _position) = (_position, nextPos); + + return _bytes.AsSpan(nextPos, length); + } + + public byte get() + { + var span = ReadBytes(1); + return span[0]; + } + + public short getShort() + { + var span = ReadBytes(2); + if (_order == ByteOrder.BIG_ENDIAN) + { + return BinaryPrimitives.ReadInt16BigEndian(span); + } + else + { + return BinaryPrimitives.ReadInt16LittleEndian(span); + } + } + + + public int getInt() + { + var span = ReadBytes(4); + if (_order == ByteOrder.BIG_ENDIAN) + { + return BinaryPrimitives.ReadInt32BigEndian(span); + } + else + { + return BinaryPrimitives.ReadInt32LittleEndian(span); + } + } + + public float getFloat() + { + var span = ReadBytes(4); + if (_order == ByteOrder.BIG_ENDIAN) + { + return BinaryPrimitives.ReadSingleBigEndian(span); + } + else + { + return BinaryPrimitives.ReadSingleLittleEndian(span); + } + } + + public long getLong() + { + var span = ReadBytes(8); + if (_order == ByteOrder.BIG_ENDIAN) + { + return BinaryPrimitives.ReadInt64BigEndian(span); + } + else + { + return BinaryPrimitives.ReadInt64LittleEndian(span); + } + } + + public void putFloat(float v) + { + // ? + } + + public void putInt(int v) + { + // ? + } +} \ No newline at end of file diff --git a/src/DotRecast.Core/ByteOrder.cs b/src/DotRecast.Core/ByteOrder.cs new file mode 100644 index 0000000..4526da6 --- /dev/null +++ b/src/DotRecast.Core/ByteOrder.cs @@ -0,0 +1,8 @@ +namespace DotRecast.Core; + +public enum ByteOrder +{ + ///

Default on most Windows systems + LITTLE_ENDIAN, + BIG_ENDIAN, +} \ No newline at end of file diff --git a/src/DotRecast.Core/CollectionExtensions.cs b/src/DotRecast.Core/CollectionExtensions.cs new file mode 100644 index 0000000..904c4e9 --- /dev/null +++ b/src/DotRecast.Core/CollectionExtensions.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; + +namespace DotRecast.Core; + +public static class CollectionExtensions +{ + public static void forEach(this IEnumerable collection, Action action) + { + foreach (var item in collection) + { + action.Invoke(item); + } + } + + public static void Shuffle(this IList list) + { + Random random = new Random(); + int n = list.Count; + while (n > 1) + { + n--; + int k = random.Next(n + 1); + T value = list[k]; + list[k] = list[n]; + list[n] = value; + } + } +} \ No newline at end of file diff --git a/src/DotRecast.Core/ConvexUtils.cs b/src/DotRecast.Core/ConvexUtils.cs new file mode 100644 index 0000000..7b9f5c3 --- /dev/null +++ b/src/DotRecast.Core/ConvexUtils.cs @@ -0,0 +1,85 @@ +/* +recast4j copyright (c) 2021 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.Collections.Generic; + +namespace DotRecast.Core; + +public static class ConvexUtils { + + // Calculates convex hull on xz-plane of points on 'pts', + // stores the indices of the resulting hull in 'out' and + // returns number of points on hull. + public static List convexhull(List pts) { + int npts = pts.Count / 3; + List @out = new(); + // Find lower-leftmost point. + int hull = 0; + for (int i = 1; i < npts; ++i) { + float[] a = new float[] { pts[i * 3], pts[i * 3 + 1], pts[i * 3 + 2] }; + float[] b = new float[] { pts[hull * 3], pts[hull * 3 + 1], pts[hull * 3 + 2] }; + if (cmppt(a, b)) { + hull = i; + } + } + // Gift wrap hull. + int endpt = 0; + do { + @out.Add(hull); + endpt = 0; + for (int j = 1; j < npts; ++j) { + float[] a = new float[] { pts[hull * 3], pts[hull * 3 + 1], pts[hull * 3 + 2] }; + float[] b = new float[] { pts[endpt * 3], pts[endpt * 3 + 1], pts[endpt * 3 + 2] }; + float[] c = new float[] { pts[j * 3], pts[j * 3 + 1], pts[j * 3 + 2] }; + if (hull == endpt || left(a, b, c)) { + endpt = j; + } + } + hull = endpt; + } while (endpt != @out[0]); + + return @out; + } + + // Returns true if 'a' is more lower-left than 'b'. + private static bool cmppt(float[] a, float[] b) { + if (a[0] < b[0]) { + return true; + } + if (a[0] > b[0]) { + return false; + } + if (a[2] < b[2]) { + return true; + } + if (a[2] > b[2]) { + return false; + } + return false; + } + + // Returns true if 'c' is left of line 'a'-'b'. + private static bool left(float[] a, float[] b, float[] c) { + float u1 = b[0] - a[0]; + float v1 = b[2] - a[2]; + float u2 = c[0] - a[0]; + float v2 = c[2] - a[2]; + return u1 * v2 - v1 * u2 < 0; + } + +} diff --git a/src/DotRecast.Core/DemoMath.cs b/src/DotRecast.Core/DemoMath.cs new file mode 100644 index 0000000..5967903 --- /dev/null +++ b/src/DotRecast.Core/DemoMath.cs @@ -0,0 +1,78 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; + +namespace DotRecast.Core; + +public class DemoMath { + public static float vDistSqr(float[] v1, float[] v2, int i) { + float dx = v2[i] - v1[0]; + float dy = v2[i + 1] - v1[1]; + float dz = v2[i + 2] - v1[2]; + return dx * dx + dy * dy + dz * dz; + } + + public static float[] vCross(float[] v1, float[] v2) { + float[] dest = new float[3]; + dest[0] = v1[1] * v2[2] - v1[2] * v2[1]; + dest[1] = v1[2] * v2[0] - v1[0] * v2[2]; + dest[2] = v1[0] * v2[1] - v1[1] * v2[0]; + return dest; + } + + public static float vDot(float[] v1, float[] v2) { + return v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2]; + } + + public static float sqr(float f) { + return f * f; + } + + public static float getPathLen(float[] path, int npath) { + float totd = 0; + for (int i = 0; i < npath - 1; ++i) { + totd += (float)Math.Sqrt(vDistSqr(path, i * 3, (i + 1) * 3)); + } + return totd; + } + + public static float vDistSqr(float[] v, int i, int j) { + float dx = v[i] - v[j]; + float dy = v[i + 1] - v[j + 1]; + float dz = v[i + 2] - v[j + 2]; + return dx * dx + dy * dy + dz * dz; + } + + public static float step(float threshold, float v) { + return v < threshold ? 0.0f : 1.0f; + } + + public static int clamp(int v, int min, int max) { + return Math.Max(Math.Min(v, max), min); + } + + public static float clamp(float v, float min, float max) { + return Math.Max(Math.Min(v, max), min); + } + + public static float lerp(float f, float g, float u) { + return u * g + (1f - u) * f; + } +} diff --git a/src/DotRecast.Core/DotRecast.Core.csproj b/src/DotRecast.Core/DotRecast.Core.csproj new file mode 100644 index 0000000..57bdec1 --- /dev/null +++ b/src/DotRecast.Core/DotRecast.Core.csproj @@ -0,0 +1,9 @@ + + + + net7.0 + + + + + diff --git a/src/DotRecast.Core/Loader.cs b/src/DotRecast.Core/Loader.cs new file mode 100644 index 0000000..2d40ccd --- /dev/null +++ b/src/DotRecast.Core/Loader.cs @@ -0,0 +1,32 @@ +using System.IO; + +namespace DotRecast.Core; + +public static class Loader +{ + public static byte[] ToBytes(string filename) + { + var filepath = ToRPath(filename); + using var fs = new FileStream(filepath, FileMode.Open); + byte[] buffer = new byte[fs.Length]; + fs.Read(buffer, 0, buffer.Length); + + return buffer; + } + + public static string ToRPath(string filename) + { + string filePath = Path.Combine("resources", filename); + for (int i = 0; i < 10; ++i) + { + if (File.Exists(filePath)) + { + return Path.GetFullPath(filePath); + } + + filePath = Path.Combine("..", filePath); + } + + return filename; + } +} \ No newline at end of file diff --git a/src/DotRecast.Core/NodeQueue.cs b/src/DotRecast.Core/NodeQueue.cs new file mode 100644 index 0000000..526e058 --- /dev/null +++ b/src/DotRecast.Core/NodeQueue.cs @@ -0,0 +1,72 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; + +namespace DotRecast.Core; + +using System.Collections.Generic; + +public class OrderedQueue +{ + + private readonly List _items; + private readonly Comparison _comparison; + + public OrderedQueue(Comparison comparison) + { + _items = new(); + _comparison = comparison; + } + + public int count() + { + return _items.Count; + } + + public void clear() { + _items.Clear(); + } + + public T top() + { + return _items[0]; + } + + public T Dequeue() + { + var node = top(); + _items.Remove(node); + return node; + } + + public void Enqueue(T item) { + _items.Add(item); + _items.Sort(_comparison); + } + + public void Remove(T item) { + _items.Remove(item); + } + + public bool isEmpty() + { + return 0 == _items.Count; + } +} diff --git a/src/DotRecast.Detour.Crowd/Crowd.cs b/src/DotRecast.Detour.Crowd/Crowd.cs new file mode 100644 index 0000000..912eb20 --- /dev/null +++ b/src/DotRecast.Detour.Crowd/Crowd.cs @@ -0,0 +1,1171 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using DotRecast.Core; +using DotRecast.Detour.Crowd.Tracking; + +namespace DotRecast.Detour.Crowd; + +using static DetourCommon; + +/** + * Members in this module implement local steering and dynamic avoidance features. + * + * The crowd is the big beast of the navigation features. It not only handles a lot of the path management for you, but + * also local steering and dynamic avoidance between members of the crowd. I.e. It can keep your agents from running + * into each other. + * + * Main class: Crowd + * + * The #dtNavMeshQuery and #dtPathCorridor classes provide perfectly good, easy to use path planning features. But in + * the end they only give you points that your navigation client should be moving toward. When it comes to deciding + * things like agent velocity and steering to avoid other agents, that is up to you to implement. Unless, of course, you + * decide to use Crowd. + * + * Basically, you add an agent to the crowd, providing various configuration settings such as maximum speed and + * acceleration. You also provide a local target to move toward. The crowd manager then provides, with every update, the + * new agent position and velocity for the frame. The movement will be constrained to the navigation mesh, and steering + * will be applied to ensure agents managed by the crowd do not collide with each other. + * + * This is very powerful feature set. But it comes with limitations. + * + * The biggest limitation is that you must give control of the agent's position completely over to the crowd manager. + * You can update things like maximum speed and acceleration. But in order for the crowd manager to do its thing, it + * can't allow you to constantly be giving it overrides to position and velocity. So you give up direct control of the + * agent's movement. It belongs to the crowd. + * + * The second biggest limitation revolves around the fact that the crowd manager deals with local planning. So the + * agent's target should never be more than 256 polygons away from its current position. If it is, you risk your agent + * failing to reach its target. So you may still need to do long distance planning and provide the crowd manager with + * intermediate targets. + * + * Other significant limitations: + * + * - All agents using the crowd manager will use the same #dtQueryFilter. - Crowd management is relatively expensive. + * The maximum agents under crowd management at any one time is between 20 and 30. A good place to start is a maximum of + * 25 agents for 0.5ms per frame. + * + * @note This is a summary list of members. Use the index or search feature to find minor members. + * + * @struct dtCrowdAgentParams + * @see CrowdAgent, Crowd::addAgent(), Crowd::updateAgentParameters() + * + * @var dtCrowdAgentParams::obstacleAvoidanceType + * @par + * + * #dtCrowd permits agents to use different avoidance configurations. This value is the index of the + * #dtObstacleAvoidanceParams within the crowd. + * + * @see dtObstacleAvoidanceParams, dtCrowd::setObstacleAvoidanceParams(), dtCrowd::getObstacleAvoidanceParams() + * + * @var dtCrowdAgentParams::collisionQueryRange + * @par + * + * Collision elements include other agents and navigation mesh boundaries. + * + * This value is often based on the agent radius and/or maximum speed. E.g. radius * 8 + * + * @var dtCrowdAgentParams::pathOptimizationRange + * @par + * + * Only applicalbe if #updateFlags includes the #DT_CROWD_OPTIMIZE_VIS flag. + * + * This value is often based on the agent radius. E.g. radius * 30 + * + * @see dtPathCorridor::optimizePathVisibility() + * + * @var dtCrowdAgentParams::separationWeight + * @par + * + * A higher value will result in agents trying to stay farther away from each other at the cost of more difficult + * steering in tight spaces. + * + */ +/** + * This is the core class of the refs crowd module. See the refs crowd documentation for a summary of the crowd + * features. A common method for setting up the crowd is as follows: -# Allocate the crowd -# Set the avoidance + * configurations using #setObstacleAvoidanceParams(). -# Add agents using #addAgent() and make an initial movement + * request using #requestMoveTarget(). A common process for managing the crowd is as follows: -# Call #update() to allow + * the crowd to manage its agents. -# Retrieve agent information using #getActiveAgents(). -# Make movement requests + * using #requestMoveTarget() when movement goal changes. -# Repeat every frame. Some agent configuration settings can + * be updated using #updateAgentParameters(). But the crowd owns the agent position. So it is not possible to update an + * active agent's position. If agent position must be fed back into the crowd, the agent must be removed and re-added. + * Notes: - Path related information is available for newly added agents only after an #update() has been performed. - + * Agent objects are kept in a pool and re-used. So it is important when using agent objects to check the value of + * #dtCrowdAgent::active to determine if the agent is actually in use or not. - This class is meant to provide 'local' + * movement. There is a limit of 256 polygons in the path corridor. So it is not meant to provide automatic pathfinding + * services over long distances. + * + * @see dtAllocCrowd(), dtFreeCrowd(), init(), dtCrowdAgent + */ +public class Crowd { + + /// The maximum number of corners a crowd agent will look ahead in the path. + /// This value is used for sizing the crowd agent corner buffers. + /// Due to the behavior of the crowd manager, the actual number of useful + /// corners will be one less than this number. + /// @ingroup crowd + public const int DT_CROWDAGENT_MAX_CORNERS = 4; + + /// The maximum number of crowd avoidance configurations supported by the + /// crowd manager. + /// @ingroup crowd + /// @see dtObstacleAvoidanceParams, dtCrowd::setObstacleAvoidanceParams(), dtCrowd::getObstacleAvoidanceParams(), + /// dtCrowdAgentParams::obstacleAvoidanceType + public const int DT_CROWD_MAX_OBSTAVOIDANCE_PARAMS = 8; + + /// The maximum number of query filter types supported by the crowd manager. + /// @ingroup crowd + /// @see dtQueryFilter, dtCrowd::getFilter() dtCrowd::getEditableFilter(), + /// dtCrowdAgentParams::queryFilterType + public const int DT_CROWD_MAX_QUERY_FILTER_TYPE = 16; + + private readonly AtomicInteger agentId = new AtomicInteger(); + private readonly HashSet m_agents; + private readonly PathQueue m_pathq; + private readonly ObstacleAvoidanceQuery.ObstacleAvoidanceParams[] m_obstacleQueryParams = new ObstacleAvoidanceQuery.ObstacleAvoidanceParams[DT_CROWD_MAX_OBSTAVOIDANCE_PARAMS]; + private readonly ObstacleAvoidanceQuery m_obstacleQuery; + private ProximityGrid m_grid; + private readonly float[] m_ext = new float[3]; + private readonly QueryFilter[] m_filters = new QueryFilter[DT_CROWD_MAX_QUERY_FILTER_TYPE]; + private NavMeshQuery navQuery; + private NavMesh navMesh; + private readonly CrowdConfig _config; + private readonly CrowdTelemetry _telemetry = new CrowdTelemetry(); + int m_velocitySampleCount; + + public Crowd(CrowdConfig config, NavMesh nav) : + this(config, nav, i => new DefaultQueryFilter()) + { + } + + public Crowd(CrowdConfig config, NavMesh nav, Func queryFilterFactory) { + + _config = config; + vSet(m_ext, config.maxAgentRadius * 2.0f, config.maxAgentRadius * 1.5f, config.maxAgentRadius * 2.0f); + + m_obstacleQuery = new ObstacleAvoidanceQuery(config.maxObstacleAvoidanceCircles, config.maxObstacleAvoidanceSegments); + + for (int i = 0; i < DT_CROWD_MAX_QUERY_FILTER_TYPE; i++) { + m_filters[i] = queryFilterFactory.Invoke(i); + } + // Init obstacle query option. + for (int i = 0; i < DT_CROWD_MAX_OBSTAVOIDANCE_PARAMS; ++i) { + m_obstacleQueryParams[i] = new ObstacleAvoidanceQuery.ObstacleAvoidanceParams(); + } + + // Allocate temp buffer for merging paths. + m_pathq = new PathQueue(config); + m_agents = new(); + + // The navQuery is mostly used for local searches, no need for large node pool. + navMesh = nav; + navQuery = new NavMeshQuery(nav); + } + + public void setNavMesh(NavMesh nav) { + navMesh = nav; + navQuery = new NavMeshQuery(nav); + } + + /// Sets the shared avoidance configuration for the specified index. + /// @param[in] idx The index. [Limits: 0 <= value < + /// #DT_CROWD_MAX_OBSTAVOIDANCE_PARAMS] + /// @param[in] option The new configuration. + public void setObstacleAvoidanceParams(int idx, ObstacleAvoidanceQuery.ObstacleAvoidanceParams option) { + if (idx >= 0 && idx < DT_CROWD_MAX_OBSTAVOIDANCE_PARAMS) { + m_obstacleQueryParams[idx] = new ObstacleAvoidanceQuery.ObstacleAvoidanceParams(option); + } + } + + /// Gets the shared avoidance configuration for the specified index. + /// @param[in] idx The index of the configuration to retreive. + /// [Limits: 0 <= value < #DT_CROWD_MAX_OBSTAVOIDANCE_PARAMS] + /// @return The requested configuration. + public ObstacleAvoidanceQuery.ObstacleAvoidanceParams getObstacleAvoidanceParams(int idx) { + if (idx >= 0 && idx < DT_CROWD_MAX_OBSTAVOIDANCE_PARAMS) { + return m_obstacleQueryParams[idx]; + } + return null; + } + + /// Updates the specified agent's configuration. + /// @param[in] idx The agent index. [Limits: 0 <= value < #getAgentCount()] + /// @param[in] params The new agent configuration. + public void updateAgentParameters(CrowdAgent agent, CrowdAgentParams option) { + agent.option = option; + } + + /** + * Adds a new agent to the crowd. + * + * @param pos + * The requested position of the agent. [(x, y, z)] + * @param params + * The configutation of the agent. + * @return The newly created agent object + */ + public CrowdAgent addAgent(float[] pos, CrowdAgentParams option) { + CrowdAgent ag = new CrowdAgent(agentId.GetAndIncrement()); + m_agents.Add(ag); + updateAgentParameters(ag, option); + + // Find nearest position on navmesh and place the agent there. + Result nearestPoly = navQuery.findNearestPoly(pos, m_ext, m_filters[ag.option.queryFilterType]); + + float[] nearest = nearestPoly.succeeded() ? nearestPoly.result.getNearestPos() : pos; + long refs = nearestPoly.succeeded() ? nearestPoly.result.getNearestRef() : 0L; + ag.corridor.reset(refs, nearest); + ag.boundary.reset(); + ag.partial = false; + + ag.topologyOptTime = 0; + ag.targetReplanTime = 0; + + vSet(ag.dvel, 0, 0, 0); + vSet(ag.nvel, 0, 0, 0); + vSet(ag.vel, 0, 0, 0); + vCopy(ag.npos, nearest); + + ag.desiredSpeed = 0; + + if (refs != 0) { + ag.state = CrowdAgent.CrowdAgentState.DT_CROWDAGENT_STATE_WALKING; + } else { + ag.state = CrowdAgent.CrowdAgentState.DT_CROWDAGENT_STATE_INVALID; + } + + ag.targetState = CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_NONE; + + return ag; + } + + /** + * Removes the agent from the crowd. + * + * @param agent + * Agent to be removed + */ + public void removeAgent(CrowdAgent agent) { + m_agents.Remove(agent); + } + + private bool requestMoveTargetReplan(CrowdAgent ag, long refs, float[] pos) { + ag.setTarget(refs, pos); + ag.targetReplan = true; + return true; + } + + /// Submits a new move request for the specified agent. + /// @param[in] idx The agent index. [Limits: 0 <= value < #getAgentCount()] + /// @param[in] ref The position's polygon reference. + /// @param[in] pos The position within the polygon. [(x, y, z)] + /// @return True if the request was successfully submitted. + /// + /// This method is used when a new target is set. + /// + /// The position will be constrained to the surface of the navigation mesh. + /// + /// The request will be processed during the next #update(). + public bool requestMoveTarget(CrowdAgent agent, long refs, float[] pos) { + if (refs == 0) { + return false; + } + + // Initialize request. + agent.setTarget(refs, pos); + agent.targetReplan = false; + return true; + } + + /// Submits a new move request for the specified agent. + /// @param[in] idx The agent index. [Limits: 0 <= value < #getAgentCount()] + /// @param[in] vel The movement velocity. [(x, y, z)] + /// @return True if the request was successfully submitted. + public bool requestMoveVelocity(CrowdAgent agent, float[] vel) { + // Initialize request. + agent.targetRef = 0; + vCopy(agent.targetPos, vel); + agent.targetPathQueryResult = null; + agent.targetReplan = false; + agent.targetState = CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_VELOCITY; + + return true; + } + + /// Resets any request for the specified agent. + /// @param[in] idx The agent index. [Limits: 0 <= value < #getAgentCount()] + /// @return True if the request was successfully reseted. + public bool resetMoveTarget(CrowdAgent agent) { + // Initialize request. + agent.targetRef = 0; + vSet(agent.targetPos, 0, 0, 0); + vSet(agent.dvel, 0, 0, 0); + agent.targetPathQueryResult = null; + agent.targetReplan = false; + agent.targetState = CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_NONE; + return true; + } + + /** + * Gets the active agents int the agent pool. + * + * @return List of active agents + */ + public List getActiveAgents() { + return new(m_agents); + } + + public float[] getQueryExtents() { + return m_ext; + } + + public QueryFilter getFilter(int i) { + return i >= 0 && i < DT_CROWD_MAX_QUERY_FILTER_TYPE ? m_filters[i] : null; + } + + public ProximityGrid getGrid() { + return m_grid; + } + + public PathQueue getPathQueue() { + return m_pathq; + } + + public CrowdTelemetry telemetry() { + return _telemetry; + } + + public CrowdConfig config() { + return _config; + } + + public CrowdTelemetry update(float dt, CrowdAgentDebugInfo debug) { + m_velocitySampleCount = 0; + + _telemetry.start(); + + ICollection agents = getActiveAgents(); + + // Check that all agents still have valid paths. + checkPathValidity(agents, dt); + + // Update async move request and path finder. + updateMoveRequest(agents, dt); + + // Optimize path topology. + updateTopologyOptimization(agents, dt); + + // Register agents to proximity grid. + buildProximityGrid(agents); + + // Get nearby navmesh segments and agents to collide with. + buildNeighbours(agents); + + // Find next corner to steer to. + findCorners(agents, debug); + + // Trigger off-mesh connections (depends on corners). + triggerOffMeshConnections(agents); + + // Calculate steering. + calculateSteering(agents); + + // Velocity planning. + planVelocity(debug, agents); + + // Integrate. + integrate(dt, agents); + + // Handle collisions. + handleCollisions(agents); + + moveAgents(agents); + + // Update agents using off-mesh connection. + updateOffMeshConnections(agents, dt); + return _telemetry; + } + + + private void checkPathValidity(ICollection agents, float dt) { + _telemetry.start("checkPathValidity"); + + foreach (CrowdAgent ag in agents) { + + if (ag.state != CrowdAgent.CrowdAgentState.DT_CROWDAGENT_STATE_WALKING) { + continue; + } + + ag.targetReplanTime += dt; + + bool replan = false; + + // First check that the current location is valid. + float[] agentPos = new float[3]; + long agentRef = ag.corridor.getFirstPoly(); + vCopy(agentPos, ag.npos); + if (!navQuery.isValidPolyRef(agentRef, m_filters[ag.option.queryFilterType])) { + // Current location is not valid, try to reposition. + // TODO: this can snap agents, how to handle that? + Result nearestPoly = navQuery.findNearestPoly(ag.npos, m_ext, + m_filters[ag.option.queryFilterType]); + agentRef = nearestPoly.succeeded() ? nearestPoly.result.getNearestRef() : 0L; + if (nearestPoly.succeeded()) { + vCopy(agentPos, nearestPoly.result.getNearestPos()); + } + + if (agentRef == 0) { + // Could not find location in navmesh, set state to invalid. + ag.corridor.reset(0, agentPos); + ag.partial = false; + ag.boundary.reset(); + ag.state = CrowdAgent.CrowdAgentState.DT_CROWDAGENT_STATE_INVALID; + continue; + } + + // Make sure the first polygon is valid, but leave other valid + // polygons in the path so that replanner can adjust the path + // better. + ag.corridor.fixPathStart(agentRef, agentPos); + // ag.corridor.trimInvalidPath(agentRef, agentPos, m_navquery, + // &m_filter); + ag.boundary.reset(); + vCopy(ag.npos, agentPos); + + replan = true; + } + + // If the agent does not have move target or is controlled by + // velocity, no need to recover the target nor replan. + if (ag.targetState == CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_NONE + || ag.targetState == CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_VELOCITY) { + continue; + } + + // Try to recover move request position. + if (ag.targetState != CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_NONE + && ag.targetState != CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_FAILED) { + if (!navQuery.isValidPolyRef(ag.targetRef, m_filters[ag.option.queryFilterType])) { + // Current target is not valid, try to reposition. + Result fnp = navQuery.findNearestPoly(ag.targetPos, m_ext, + m_filters[ag.option.queryFilterType]); + ag.targetRef = fnp.succeeded() ? fnp.result.getNearestRef() : 0L; + if (fnp.succeeded()) { + vCopy(ag.targetPos, fnp.result.getNearestPos()); + } + replan = true; + } + if (ag.targetRef == 0) { + // Failed to reposition target, fail moverequest. + ag.corridor.reset(agentRef, agentPos); + ag.partial = false; + ag.targetState = CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_NONE; + } + } + + // If nearby corridor is not valid, replan. + if (!ag.corridor.isValid(_config.checkLookAhead, navQuery, m_filters[ag.option.queryFilterType])) { + // Fix current path. + // ag.corridor.trimInvalidPath(agentRef, agentPos, m_navquery, + // &m_filter); + // ag.boundary.reset(); + replan = true; + } + + // If the end of the path is near and it is not the requested + // location, replan. + if (ag.targetState == CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_VALID) { + if (ag.targetReplanTime > _config.targetReplanDelay && ag.corridor.getPathCount() < _config.checkLookAhead + && ag.corridor.getLastPoly() != ag.targetRef) { + replan = true; + } + } + + // Try to replan path to goal. + if (replan) { + if (ag.targetState != CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_NONE) { + requestMoveTargetReplan(ag, ag.targetRef, ag.targetPos); + } + } + } + _telemetry.stop("checkPathValidity"); + } + + private void updateMoveRequest(ICollection agents, float dt) { + _telemetry.start("updateMoveRequest"); + + OrderedQueue queue = new((a1, a2) => a2.targetReplanTime.CompareTo(a1.targetReplanTime)); + + // Fire off new requests. + foreach (CrowdAgent ag in agents) { + if (ag.state == CrowdAgent.CrowdAgentState.DT_CROWDAGENT_STATE_INVALID) { + continue; + } + if (ag.targetState == CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_NONE + || ag.targetState == CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_VELOCITY) { + continue; + } + + if (ag.targetState == CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_REQUESTING) { + List path = ag.corridor.getPath(); + if (0 == path.Count) { + throw new ArgumentException("Empty path"); + } + // Quick search towards the goal. + navQuery.initSlicedFindPath(path[0], ag.targetRef, ag.npos, ag.targetPos, + m_filters[ag.option.queryFilterType], 0); + navQuery.updateSlicedFindPath(_config.maxTargetFindPathIterations); + Result> pathFound; + if (ag.targetReplan) // && npath > 10) + { + // Try to use existing steady path during replan if + // possible. + pathFound = navQuery.finalizeSlicedFindPathPartial(path); + } else { + // Try to move towards target when goal changes. + pathFound = navQuery.finalizeSlicedFindPath(); + } + List reqPath = pathFound.result; + float[] reqPos = new float[3]; + if (pathFound.succeeded() && reqPath.Count > 0) { + // In progress or succeed. + if (reqPath[reqPath.Count - 1] != ag.targetRef) { + // Partial path, constrain target position inside the + // last polygon. + Result cr = navQuery.closestPointOnPoly(reqPath[reqPath.Count - 1], + ag.targetPos); + if (cr.succeeded()) { + reqPos = cr.result.getClosest(); + } else { + reqPath = new(); + } + } else { + vCopy(reqPos, ag.targetPos); + } + } else { + // Could not find path, start the request from current + // location. + vCopy(reqPos, ag.npos); + reqPath = new(); + reqPath.Add(path[0]); + } + + ag.corridor.setCorridor(reqPos, reqPath); + ag.boundary.reset(); + ag.partial = false; + + if (reqPath[reqPath.Count - 1] == ag.targetRef) { + ag.targetState = CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_VALID; + ag.targetReplanTime = 0; + } else { + // The path is longer or potentially unreachable, full plan. + ag.targetState = CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_WAITING_FOR_QUEUE; + } + ag.targetReplanWaitTime = 0; + } + + if (ag.targetState == CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_WAITING_FOR_QUEUE) { + queue.Enqueue(ag); + } + } + + while (!queue.isEmpty()) { + CrowdAgent ag = queue.Dequeue(); + ag.targetPathQueryResult = m_pathq.request(ag.corridor.getLastPoly(), ag.targetRef, ag.corridor.getTarget(), + ag.targetPos, m_filters[ag.option.queryFilterType]); + if (ag.targetPathQueryResult != null) { + ag.targetState = CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_WAITING_FOR_PATH; + } else { + _telemetry.recordMaxTimeToEnqueueRequest(ag.targetReplanWaitTime); + ag.targetReplanWaitTime += dt; + } + } + + // Update requests. + _telemetry.start("pathQueueUpdate"); + m_pathq.update(navMesh); + _telemetry.stop("pathQueueUpdate"); + + // Process path results. + foreach (CrowdAgent ag in agents) { + if (ag.targetState == CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_NONE + || ag.targetState == CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_VELOCITY) { + continue; + } + + if (ag.targetState == CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_WAITING_FOR_PATH) { + // _telemetry.recordPathWaitTime(ag.targetReplanTime); + // Poll path queue. + Status status = ag.targetPathQueryResult.status; + if (status != null && status.isFailed()) { + // Path find failed, retry if the target location is still + // valid. + ag.targetPathQueryResult = null; + if (ag.targetRef != 0) { + ag.targetState = CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_REQUESTING; + } else { + ag.targetState = CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_FAILED; + } + ag.targetReplanTime = 0; + } else if (status != null && status.isSuccess()) { + List path = ag.corridor.getPath(); + if (0 == path.Count) { + throw new ArgumentException("Empty path"); + } + + // Apply results. + float[] targetPos = ag.targetPos; + + bool valid = true; + List res = ag.targetPathQueryResult.path; + if (status.isFailed() || 0 == res.Count) { + valid = false; + } + + if (status.isPartial()) { + ag.partial = true; + } else { + ag.partial = false; + } + + // Merge result and existing path. + // The agent might have moved whilst the request is + // being processed, so the path may have changed. + // We assume that the end of the path is at the same + // location + // where the request was issued. + + // The last ref in the old path should be the same as + // the location where the request was issued.. + if (valid && path[path.Count - 1] != res[0]) { + valid = false; + } + + if (valid) { + // Put the old path infront of the old path. + if (path.Count > 1) { + path.RemoveAt(path.Count - 1); + path.AddRange(res); + res = path; + // Remove trackbacks + for (int j = 1; j < res.Count - 1; ++j) { + if (j - 1 >= 0 && j + 1 < res.Count) { + if (res[j - 1] == res[j + 1]) { + res.RemoveAt(j + 1); + res.RemoveAt(j); + j -= 2; + } + } + } + } + + // Check for partial path. + if (res[res.Count - 1] != ag.targetRef) { + // Partial path, constrain target position inside + // the last polygon. + Result cr = navQuery.closestPointOnPoly(res[res.Count - 1], targetPos); + if (cr.succeeded()) { + targetPos = cr.result.getClosest(); + } else { + valid = false; + } + } + } + + if (valid) { + // Set current corridor. + ag.corridor.setCorridor(targetPos, res); + // Force to update boundary. + ag.boundary.reset(); + ag.targetState = CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_VALID; + } else { + // Something went wrong. + ag.targetState = CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_FAILED; + } + + ag.targetReplanTime = 0; + } + _telemetry.recordMaxTimeToFindPath(ag.targetReplanWaitTime); + ag.targetReplanWaitTime += dt; + } + } + _telemetry.stop("updateMoveRequest"); + } + + private void updateTopologyOptimization(ICollection agents, float dt) { + _telemetry.start("updateTopologyOptimization"); + + OrderedQueue queue = new((a1, a2) => a2.topologyOptTime.CompareTo(a1.topologyOptTime)); + + foreach (CrowdAgent ag in agents) { + if (ag.state != CrowdAgent.CrowdAgentState.DT_CROWDAGENT_STATE_WALKING) { + continue; + } + if (ag.targetState == CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_NONE + || ag.targetState == CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_VELOCITY) { + continue; + } + if ((ag.option.updateFlags & CrowdAgentParams.DT_CROWD_OPTIMIZE_TOPO) == 0) { + continue; + } + ag.topologyOptTime += dt; + if (ag.topologyOptTime >= _config.topologyOptimizationTimeThreshold) { + queue.Enqueue(ag); + } + } + + while (!queue.isEmpty()) { + CrowdAgent ag = queue.Dequeue(); + ag.corridor.optimizePathTopology(navQuery, m_filters[ag.option.queryFilterType], _config.maxTopologyOptimizationIterations); + ag.topologyOptTime = 0; + } + _telemetry.stop("updateTopologyOptimization"); + + } + + private void buildProximityGrid(ICollection agents) { + _telemetry.start("buildProximityGrid"); + m_grid = new ProximityGrid(_config.maxAgentRadius * 3); + foreach (CrowdAgent ag in agents) { + float[] p = ag.npos; + float r = ag.option.radius; + m_grid.addItem(ag, p[0] - r, p[2] - r, p[0] + r, p[2] + r); + } + _telemetry.stop("buildProximityGrid"); + } + + private void buildNeighbours(ICollection agents) { + _telemetry.start("buildNeighbours"); + foreach (CrowdAgent ag in agents) { + if (ag.state != CrowdAgent.CrowdAgentState.DT_CROWDAGENT_STATE_WALKING) { + continue; + } + + // Update the collision boundary after certain distance has been passed or + // if it has become invalid. + float updateThr = ag.option.collisionQueryRange * 0.25f; + if (vDist2DSqr(ag.npos, ag.boundary.getCenter()) > sqr(updateThr) + || !ag.boundary.isValid(navQuery, m_filters[ag.option.queryFilterType])) { + ag.boundary.update(ag.corridor.getFirstPoly(), ag.npos, ag.option.collisionQueryRange, navQuery, + m_filters[ag.option.queryFilterType]); + } + // Query neighbour agents + ag.neis = getNeighbours(ag.npos, ag.option.height, ag.option.collisionQueryRange, ag, m_grid); + } + _telemetry.stop("buildNeighbours"); + } + + private List getNeighbours(float[] pos, float height, float range, CrowdAgent skip, ProximityGrid grid) { + + List result = new(); + HashSet proxAgents = grid.queryItems(pos[0] - range, pos[2] - range, pos[0] + range, pos[2] + range); + + foreach (CrowdAgent ag in proxAgents) { + + if (ag == skip) { + continue; + } + + // Check for overlap. + float[] diff = vSub(pos, ag.npos); + if (Math.Abs(diff[1]) >= (height + ag.option.height) / 2.0f) { + continue; + } + diff[1] = 0; + float distSqr = vLenSqr(diff); + if (distSqr > sqr(range)) { + continue; + } + + result.Add(new CrowdNeighbour(ag, distSqr)); + } + result.Sort((o1, o2) => o1.dist.CompareTo(o2.dist)); + return result; + + } + + private void findCorners(ICollection agents, CrowdAgentDebugInfo debug) { + _telemetry.start("findCorners"); + CrowdAgent debugAgent = debug != null ? debug.agent : null; + foreach (CrowdAgent ag in agents) { + + if (ag.state != CrowdAgent.CrowdAgentState.DT_CROWDAGENT_STATE_WALKING) { + continue; + } + if (ag.targetState == CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_NONE + || ag.targetState == CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_VELOCITY) { + continue; + } + + // Find corners for steering + ag.corners = ag.corridor.findCorners(DT_CROWDAGENT_MAX_CORNERS, navQuery, m_filters[ag.option.queryFilterType]); + + // Check to see if the corner after the next corner is directly visible, + // and short cut to there. + if ((ag.option.updateFlags & CrowdAgentParams.DT_CROWD_OPTIMIZE_VIS) != 0 && ag.corners.Count > 0) { + float[] target = ag.corners[Math.Min(1, ag.corners.Count - 1)].getPos(); + ag.corridor.optimizePathVisibility(target, ag.option.pathOptimizationRange, navQuery, + m_filters[ag.option.queryFilterType]); + + // Copy data for debug purposes. + if (debugAgent == ag) { + vCopy(debug.optStart, ag.corridor.getPos()); + vCopy(debug.optEnd, target); + } + } else { + // Copy data for debug purposes. + if (debugAgent == ag) { + vSet(debug.optStart, 0, 0, 0); + vSet(debug.optEnd, 0, 0, 0); + } + } + } + _telemetry.stop("findCorners"); + } + + private void triggerOffMeshConnections(ICollection agents) { + _telemetry.start("triggerOffMeshConnections"); + foreach (CrowdAgent ag in agents) { + + if (ag.state != CrowdAgent.CrowdAgentState.DT_CROWDAGENT_STATE_WALKING) { + continue; + } + if (ag.targetState == CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_NONE + || ag.targetState == CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_VELOCITY) { + continue; + } + + // Check + float triggerRadius = ag.option.radius * 2.25f; + if (ag.overOffmeshConnection(triggerRadius)) { + // Prepare to off-mesh connection. + CrowdAgentAnimation anim = ag.animation; + + // Adjust the path over the off-mesh connection. + long[] refs = new long[2]; + if (ag.corridor.moveOverOffmeshConnection(ag.corners[ag.corners.Count - 1].getRef(), refs, anim.startPos, + anim.endPos, navQuery)) { + vCopy(anim.initPos, ag.npos); + anim.polyRef = refs[1]; + anim.active = true; + anim.t = 0.0f; + anim.tmax = (vDist2D(anim.startPos, anim.endPos) / ag.option.maxSpeed) * 0.5f; + + ag.state = CrowdAgent.CrowdAgentState.DT_CROWDAGENT_STATE_OFFMESH; + ag.corners.Clear(); + ag.neis.Clear(); + continue; + } else { + // Path validity check will ensure that bad/blocked connections will be replanned. + } + } + } + _telemetry.stop("triggerOffMeshConnections"); + } + + private void calculateSteering(ICollection agents) { + _telemetry.start("calculateSteering"); + foreach (CrowdAgent ag in agents) { + + if (ag.state != CrowdAgent.CrowdAgentState.DT_CROWDAGENT_STATE_WALKING) { + continue; + } + if (ag.targetState == CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_NONE) { + continue; + } + + float[] dvel = new float[3]; + + if (ag.targetState == CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_VELOCITY) { + vCopy(dvel, ag.targetPos); + ag.desiredSpeed = vLen(ag.targetPos); + } else { + // Calculate steering direction. + if ((ag.option.updateFlags & CrowdAgentParams.DT_CROWD_ANTICIPATE_TURNS) != 0) { + dvel = ag.calcSmoothSteerDirection(); + } else { + dvel = ag.calcStraightSteerDirection(); + } + // Calculate speed scale, which tells the agent to slowdown at the end of the path. + float slowDownRadius = ag.option.radius * 2; // TODO: make less hacky. + float speedScale = ag.getDistanceToGoal(slowDownRadius) / slowDownRadius; + + ag.desiredSpeed = ag.option.maxSpeed; + dvel = vScale(dvel, ag.desiredSpeed * speedScale); + } + + // Separation + if ((ag.option.updateFlags & CrowdAgentParams.DT_CROWD_SEPARATION) != 0) { + float separationDist = ag.option.collisionQueryRange; + float invSeparationDist = 1.0f / separationDist; + float separationWeight = ag.option.separationWeight; + + float w = 0; + float[] disp = new float[3]; + + for (int j = 0; j < ag.neis.Count; ++j) { + CrowdAgent nei = ag.neis[j].agent; + + float[] diff = vSub(ag.npos, nei.npos); + diff[1] = 0; + + float distSqr = vLenSqr(diff); + if (distSqr < 0.00001f) { + continue; + } + if (distSqr > sqr(separationDist)) { + continue; + } + float dist = (float) Math.Sqrt(distSqr); + float weight = separationWeight * (1.0f - sqr(dist * invSeparationDist)); + + disp = vMad(disp, diff, weight / dist); + w += 1.0f; + } + + if (w > 0.0001f) { + // Adjust desired velocity. + dvel = vMad(dvel, disp, 1.0f / w); + // Clamp desired velocity to desired speed. + float speedSqr = vLenSqr(dvel); + float desiredSqr = sqr(ag.desiredSpeed); + if (speedSqr > desiredSqr) { + dvel = vScale(dvel, desiredSqr / speedSqr); + } + } + } + + // Set the desired velocity. + vCopy(ag.dvel, dvel); + } + _telemetry.stop("calculateSteering"); + } + + private void planVelocity(CrowdAgentDebugInfo debug, ICollection agents) { + _telemetry.start("planVelocity"); + CrowdAgent debugAgent = debug != null ? debug.agent : null; + foreach (CrowdAgent ag in agents) { + + if (ag.state != CrowdAgent.CrowdAgentState.DT_CROWDAGENT_STATE_WALKING) { + continue; + } + + if ((ag.option.updateFlags & CrowdAgentParams.DT_CROWD_OBSTACLE_AVOIDANCE) != 0) { + m_obstacleQuery.reset(); + + // Add neighbours as obstacles. + for (int j = 0; j < ag.neis.Count; ++j) { + CrowdAgent nei = ag.neis[j].agent; + m_obstacleQuery.addCircle(nei.npos, nei.option.radius, nei.vel, nei.dvel); + } + + // Append neighbour segments as obstacles. + for (int j = 0; j < ag.boundary.getSegmentCount(); ++j) { + float[] s = ag.boundary.getSegment(j); + float[] s3 = new float[3]; + Array.Copy(s, 3, s3, 0, 3); + if (triArea2D(ag.npos, s, s3) < 0.0f) { + continue; + } + m_obstacleQuery.addSegment(s, s3); + } + + ObstacleAvoidanceDebugData vod = null; + if (debugAgent == ag) { + vod = debug.vod; + } + + // Sample new safe velocity. + bool adaptive = true; + int ns = 0; + + ObstacleAvoidanceQuery.ObstacleAvoidanceParams option = m_obstacleQueryParams[ag.option.obstacleAvoidanceType]; + + if (adaptive) { + Tuple nsnvel = m_obstacleQuery.sampleVelocityAdaptive(ag.npos, ag.option.radius, + ag.desiredSpeed, ag.vel, ag.dvel, option, vod); + ns = nsnvel.Item1; + ag.nvel = nsnvel.Item2; + } else { + Tuple nsnvel = m_obstacleQuery.sampleVelocityGrid(ag.npos, ag.option.radius, + ag.desiredSpeed, ag.vel, ag.dvel, option, vod); + ns = nsnvel.Item1; + ag.nvel = nsnvel.Item2; + } + m_velocitySampleCount += ns; + } else { + // If not using velocity planning, new velocity is directly the desired velocity. + vCopy(ag.nvel, ag.dvel); + } + } + _telemetry.stop("planVelocity"); + } + + private void integrate(float dt, ICollection agents) { + _telemetry.start("integrate"); + foreach (CrowdAgent ag in agents) { + if (ag.state != CrowdAgent.CrowdAgentState.DT_CROWDAGENT_STATE_WALKING) { + continue; + } + ag.integrate(dt); + } + _telemetry.stop("integrate"); + } + + private void handleCollisions(ICollection agents) { + _telemetry.start("handleCollisions"); + for (int iter = 0; iter < 4; ++iter) { + foreach (CrowdAgent ag in agents) { + long idx0 = ag.idx; + if (ag.state != CrowdAgent.CrowdAgentState.DT_CROWDAGENT_STATE_WALKING) { + continue; + } + + vSet(ag.disp, 0, 0, 0); + + float w = 0; + + for (int j = 0; j < ag.neis.Count; ++j) { + CrowdAgent nei = ag.neis[j].agent; + long idx1 = nei.idx; + float[] diff = vSub(ag.npos, nei.npos); + diff[1] = 0; + + float dist = vLenSqr(diff); + if (dist > sqr(ag.option.radius + nei.option.radius)) { + continue; + } + dist = (float) Math.Sqrt(dist); + float pen = (ag.option.radius + nei.option.radius) - dist; + if (dist < 0.0001f) { + // Agents on top of each other, try to choose diverging separation directions. + if (idx0 > idx1) { + vSet(diff, -ag.dvel[2], 0, ag.dvel[0]); + } else { + vSet(diff, ag.dvel[2], 0, -ag.dvel[0]); + } + pen = 0.01f; + } else { + pen = (1.0f / dist) * (pen * 0.5f) * _config.collisionResolveFactor; + } + + ag.disp = vMad(ag.disp, diff, pen); + + w += 1.0f; + } + + if (w > 0.0001f) { + float iw = 1.0f / w; + ag.disp = vScale(ag.disp, iw); + } + } + + foreach (CrowdAgent ag in agents) { + if (ag.state != CrowdAgent.CrowdAgentState.DT_CROWDAGENT_STATE_WALKING) { + continue; + } + + ag.npos = vAdd(ag.npos, ag.disp); + } + } + + _telemetry.stop("handleCollisions"); + } + + private void moveAgents(ICollection agents) { + _telemetry.start("moveAgents"); + foreach (CrowdAgent ag in agents) { + if (ag.state != CrowdAgent.CrowdAgentState.DT_CROWDAGENT_STATE_WALKING) { + continue; + } + + // Move along navmesh. + ag.corridor.movePosition(ag.npos, navQuery, m_filters[ag.option.queryFilterType]); + // Get valid constrained position back. + vCopy(ag.npos, ag.corridor.getPos()); + + // If not using path, truncate the corridor to just one poly. + if (ag.targetState == CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_NONE + || ag.targetState == CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_VELOCITY) { + ag.corridor.reset(ag.corridor.getFirstPoly(), ag.npos); + ag.partial = false; + } + + } + _telemetry.stop("moveAgents"); + } + + private void updateOffMeshConnections(ICollection agents, float dt) { + _telemetry.start("updateOffMeshConnections"); + foreach (CrowdAgent ag in agents) { + CrowdAgentAnimation anim = ag.animation; + if (!anim.active) { + continue; + } + + anim.t += dt; + if (anim.t > anim.tmax) { + // Reset animation + anim.active = false; + // Prepare agent for walking. + ag.state = CrowdAgent.CrowdAgentState.DT_CROWDAGENT_STATE_WALKING; + continue; + } + + // Update position + float ta = anim.tmax * 0.15f; + float tb = anim.tmax; + if (anim.t < ta) { + float u = tween(anim.t, 0.0f, ta); + ag.npos = vLerp(anim.initPos, anim.startPos, u); + } else { + float u = tween(anim.t, ta, tb); + ag.npos = vLerp(anim.startPos, anim.endPos, u); + } + + // Update velocity. + vSet(ag.vel, 0, 0, 0); + vSet(ag.dvel, 0, 0, 0); + } + _telemetry.stop("updateOffMeshConnections"); + } + + private float tween(float t, float t0, float t1) { + return clamp((t - t0) / (t1 - t0), 0.0f, 1.0f); + } + + /// Provides neighbor data for agents managed by the crowd. + /// @ingroup crowd + /// @see dtCrowdAgent::neis, dtCrowd + public class CrowdNeighbour { + public readonly CrowdAgent agent; /// < The index of the neighbor in the crowd. + public readonly float dist; /// < The distance between the current agent and the neighbor. + + public CrowdNeighbour(CrowdAgent agent, float dist) { + this.agent = agent; + this.dist = dist; + } + }; + +} diff --git a/src/DotRecast.Detour.Crowd/CrowdAgent.cs b/src/DotRecast.Detour.Crowd/CrowdAgent.cs new file mode 100644 index 0000000..f5edefa --- /dev/null +++ b/src/DotRecast.Detour.Crowd/CrowdAgent.cs @@ -0,0 +1,191 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.Collections.Generic; + +namespace DotRecast.Detour.Crowd; + +using static DetourCommon; + +/// Represents an agent managed by a #dtCrowd object. +/// @ingroup crowd +public class CrowdAgent { + + /// The type of navigation mesh polygon the agent is currently traversing. + /// @ingroup crowd + public enum CrowdAgentState { + DT_CROWDAGENT_STATE_INVALID, /// < The agent is not in a valid state. + DT_CROWDAGENT_STATE_WALKING, /// < The agent is traversing a normal navigation mesh polygon. + DT_CROWDAGENT_STATE_OFFMESH, /// < The agent is traversing an off-mesh connection. + }; + + public enum MoveRequestState { + DT_CROWDAGENT_TARGET_NONE, + DT_CROWDAGENT_TARGET_FAILED, + DT_CROWDAGENT_TARGET_VALID, + DT_CROWDAGENT_TARGET_REQUESTING, + DT_CROWDAGENT_TARGET_WAITING_FOR_QUEUE, + DT_CROWDAGENT_TARGET_WAITING_FOR_PATH, + DT_CROWDAGENT_TARGET_VELOCITY, + }; + + public readonly long idx; + /// The type of mesh polygon the agent is traversing. (See: #CrowdAgentState) + public CrowdAgentState state; + /// True if the agent has valid path (targetState == DT_CROWDAGENT_TARGET_VALID) and the path does not lead to the + /// requested position, else false. + public bool partial; + /// The path corridor the agent is using. + public PathCorridor corridor; + /// The local boundary data for the agent. + public LocalBoundary boundary; + /// Time since the agent's path corridor was optimized. + public float topologyOptTime; + /// The known neighbors of the agent. + public List neis = new(); + /// The desired speed. + public float desiredSpeed; + + public float[] npos = new float[3]; /// < The current agent position. [(x, y, z)] + public float[] disp = new float[3]; /// < A temporary value used to accumulate agent displacement during iterative + /// collision resolution. [(x, y, z)] + public float[] dvel = new float[3]; /// < The desired velocity of the agent. Based on the current path, calculated + /// from + /// scratch each frame. [(x, y, z)] + public float[] nvel = new float[3]; /// < The desired velocity adjusted by obstacle avoidance, calculated from scratch each + /// frame. [(x, y, z)] + public float[] vel = new float[3]; /// < The actual velocity of the agent. The change from nvel -> vel is + /// constrained by max acceleration. [(x, y, z)] + + /// The agent's configuration parameters. + public CrowdAgentParams option; + /// The local path corridor corners for the agent. + public List corners = new(); + + public MoveRequestState targetState; /// < State of the movement request. + public long targetRef; /// < Target polyref of the movement request. + public float[] targetPos = new float[3]; /// < Target position of the movement request (or velocity in case of + /// DT_CROWDAGENT_TARGET_VELOCITY). + public PathQueryResult targetPathQueryResult; /// < Path finder query + public bool targetReplan; /// < Flag indicating that the current path is being replanned. + public float targetReplanTime; ///
jfastlz + * library written by William Kinney. + */ +public class FastLz { + + private static readonly int MAX_DISTANCE = 8191; + private static readonly int MAX_FARDISTANCE = 65535 + MAX_DISTANCE - 1; + + private static readonly int HASH_LOG = 13; + private static readonly int HASH_SIZE = 1 << HASH_LOG; // 8192 + private static readonly int HASH_MASK = HASH_SIZE - 1; + + private static readonly int MAX_COPY = 32; + private static readonly int MAX_LEN = 256 + 8; + + private static readonly int MIN_RECOMENDED_LENGTH_FOR_LEVEL_2 = 1024 * 64; + + static readonly int MAGIC_NUMBER = 'F' << 16 | 'L' << 8 | 'Z'; + + static readonly byte BLOCK_TYPE_NON_COMPRESSED = 0x00; + static readonly byte BLOCK_TYPE_COMPRESSED = 0x01; + static readonly byte BLOCK_WITHOUT_CHECKSUM = 0x00; + static readonly byte BLOCK_WITH_CHECKSUM = 0x10; + + static readonly int OPTIONS_OFFSET = 3; + static readonly int CHECKSUM_OFFSET = 4; + + static readonly int MAX_CHUNK_LENGTH = 0xFFFF; + + /** + * Do not call {@link #compress(byte[], int, int, byte[], int, int)} for input buffers + * which length less than this value. + */ + static readonly int MIN_LENGTH_TO_COMPRESSION = 32; + + /** + * In this case {@link #compress(byte[], int, int, byte[], int, int)} will choose level + * automatically depending on the length of the input buffer. If length less than + * {@link #MIN_RECOMENDED_LENGTH_FOR_LEVEL_2} {@link #LEVEL_1} will be choosen, + * otherwise {@link #LEVEL_2}. + */ + static readonly int LEVEL_AUTO = 0; + + /** + * Level 1 is the fastest compression and generally useful for short data. + */ + static readonly int LEVEL_1 = 1; + + /** + * Level 2 is slightly slower but it gives better compression ratio. + */ + static readonly int LEVEL_2 = 2; + + /** + * The output buffer must be at least 6% larger than the input buffer and can not be smaller than 66 bytes. + * @param inputLength length of input buffer + * @return Maximum output buffer length + */ + public static int calculateOutputBufferLength(int inputLength) { + int tempOutputLength = (int) (inputLength * 1.06); + return Math.Max(tempOutputLength, 66); + } + + /** + * Compress a block of data in the input buffer and returns the size of compressed block. + * The size of input buffer is specified by length. The minimum input buffer size is 32. + * + * If the input is not compressible, the return value might be larger than length (input buffer size). + */ + public static int compress(byte[] input, int inOffset, int inLength, + byte[] output, int outOffset, int proposedLevel) { + int level; + if (proposedLevel == LEVEL_AUTO) { + level = inLength < MIN_RECOMENDED_LENGTH_FOR_LEVEL_2 ? LEVEL_1 : LEVEL_2; + } else { + level = proposedLevel; + } + + int ip = 0; + int ipBound = ip + inLength - 2; + int ipLimit = ip + inLength - 12; + + int op = 0; + + // const flzuint8* htab[HASH_SIZE]; + int[] htab = new int[HASH_SIZE]; + // const flzuint8** hslot; + int hslot; + // flzuint32 hval; + // int OK b/c address starting from 0 + int hval; + // flzuint32 copy; + // int OK b/c address starting from 0 + int copy; + + /* sanity check */ + if (inLength < 4) { + if (inLength != 0) { + // *op++ = length-1; + output[outOffset + op++] = (byte) (inLength - 1); + ipBound++; + while (ip <= ipBound) { + output[outOffset + op++] = input[inOffset + ip++]; + } + return inLength + 1; + } + // else + return 0; + } + + /* initializes hash table */ + // for (hslot = htab; hslot < htab + HASH_SIZE; hslot++) + for (hslot = 0; hslot < HASH_SIZE; hslot++) { + //*hslot = ip; + htab[hslot] = ip; + } + + /* we start with literal copy */ + copy = 2; + output[outOffset + op++] = (byte)(MAX_COPY - 1); + output[outOffset + op++] = input[inOffset + ip++]; + output[outOffset + op++] = input[inOffset + ip++]; + + /* main loop */ + while (ip < ipLimit) { + int refs = 0; + + long distance = 0; + + /* minimum match length */ + // flzuint32 len = 3; + // int OK b/c len is 0 and octal based + int len = 3; + + /* comparison starting-point */ + int anchor = ip; + + bool matchLabel = false; + + /* check for a run */ + if (level == LEVEL_2) { + //if(ip[0] == ip[-1] && FASTLZ_READU16(ip-1)==FASTLZ_READU16(ip+1)) + if (input[inOffset + ip] == input[inOffset + ip - 1] && + readU16(input, inOffset + ip - 1) == readU16(input, inOffset + ip + 1)) { + distance = 1; + ip += 3; + refs = anchor - 1 + 3; + + /* + * goto match; + */ + matchLabel = true; + } + } + if (!matchLabel) { + /* find potential match */ + // HASH_FUNCTION(hval,ip); + hval = hashFunction(input, inOffset + ip); + // hslot = htab + hval; + hslot = hval; + // refs = htab[hval]; + refs = htab[hval]; + + /* calculate distance to the match */ + distance = anchor - refs; + + /* update hash table */ + //*hslot = anchor; + htab[hslot] = anchor; + + /* is this a match? check the first 3 bytes */ + if (distance == 0 + || (level == LEVEL_1 ? distance >= MAX_DISTANCE : distance >= MAX_FARDISTANCE) + || input[inOffset + refs++] != input[inOffset + ip++] + || input[inOffset + refs++] != input[inOffset + ip++] + || input[inOffset + refs++] != input[inOffset + ip++]) { + /* + * goto literal; + */ + output[outOffset + op++] = input[inOffset + anchor++]; + ip = anchor; + copy++; + if (copy == MAX_COPY) { + copy = 0; + output[outOffset + op++] = (byte)(MAX_COPY - 1); + } + continue; + } + + if (level == LEVEL_2) { + /* far, needs at least 5-byte match */ + if (distance >= MAX_DISTANCE) { + if (input[inOffset + ip++] != input[inOffset + refs++] + || input[inOffset + ip++] != input[inOffset + refs++]) { + /* + * goto literal; + */ + output[outOffset + op++] = input[inOffset + anchor++]; + ip = anchor; + copy++; + if (copy == MAX_COPY) { + copy = 0; + output[outOffset + op++] = (byte)(MAX_COPY - 1); + } + continue; + } + len += 2; + } + } + } // end if(!matchLabel) + /* + * match: + */ + /* last matched byte */ + ip = anchor + len; + + /* distance is biased */ + distance--; + + if (distance == 0) { + /* zero distance means a run */ + //flzuint8 x = ip[-1]; + byte x = input[inOffset + ip - 1]; + while (ip < ipBound) { + if (input[inOffset + refs++] != x) { + break; + } else { + ip++; + } + } + } else { + for (;;) { + /* safe because the outer check against ip limit */ + if (input[inOffset + refs++] != input[inOffset + ip++]) { + break; + } + if (input[inOffset + refs++] != input[inOffset + ip++]) { + break; + } + if (input[inOffset + refs++] != input[inOffset + ip++]) { + break; + } + if (input[inOffset + refs++] != input[inOffset + ip++]) { + break; + } + if (input[inOffset + refs++] != input[inOffset + ip++]) { + break; + } + if (input[inOffset + refs++] != input[inOffset + ip++]) { + break; + } + if (input[inOffset + refs++] != input[inOffset + ip++]) { + break; + } + if (input[inOffset + refs++] != input[inOffset + ip++]) { + break; + } + while (ip < ipBound) { + if (input[inOffset + refs++] != input[inOffset + ip++]) { + break; + } + } + break; + } + } + + /* if we have copied something, adjust the copy count */ + if (copy != 0) { + /* copy is biased, '0' means 1 byte copy */ + // *(op-copy-1) = copy-1; + output[outOffset + op - copy - 1] = (byte) (copy - 1); + } else { + /* back, to overwrite the copy count */ + op--; + } + + /* reset literal counter */ + copy = 0; + + /* length is biased, '1' means a match of 3 bytes */ + ip -= 3; + len = ip - anchor; + + /* encode the match */ + if (level == LEVEL_2) { + if (distance < MAX_DISTANCE) { + if (len < 7) { + output[outOffset + op++] = (byte) ((len << 5) + (distance >>> 8)); + output[outOffset + op++] = (byte) (distance & 255); + } else { + output[outOffset + op++] = (byte) ((7 << 5) + (distance >>> 8)); + for (len -= 7; len >= 255; len -= 255) { + output[outOffset + op++] = (byte) 255; + } + output[outOffset + op++] = (byte) len; + output[outOffset + op++] = (byte) (distance & 255); + } + } else { + /* far away, but not yet in the another galaxy... */ + if (len < 7) { + distance -= MAX_DISTANCE; + output[outOffset + op++] = (byte) ((len << 5) + 31); + output[outOffset + op++] = (byte) 255; + output[outOffset + op++] = (byte) (distance >>> 8); + output[outOffset + op++] = (byte) (distance & 255); + } else { + distance -= MAX_DISTANCE; + output[outOffset + op++] = (byte) ((7 << 5) + 31); + for (len -= 7; len >= 255; len -= 255) { + output[outOffset + op++] = (byte) 255; + } + output[outOffset + op++] = (byte) len; + output[outOffset + op++] = (byte) 255; + output[outOffset + op++] = (byte) (distance >>> 8); + output[outOffset + op++] = (byte) (distance & 255); + } + } + } else { + if (len > MAX_LEN - 2) { + while (len > MAX_LEN - 2) { + output[outOffset + op++] = (byte) ((7 << 5) + (distance >>> 8)); + output[outOffset + op++] = (byte) (MAX_LEN - 2 - 7 - 2); + output[outOffset + op++] = (byte) (distance & 255); + len -= MAX_LEN - 2; + } + } + + if (len < 7) { + output[outOffset + op++] = (byte) ((len << 5) + (distance >>> 8)); + output[outOffset + op++] = (byte) (distance & 255); + } else { + output[outOffset + op++] = (byte) ((7 << 5) + (distance >>> 8)); + output[outOffset + op++] = (byte) (len - 7); + output[outOffset + op++] = (byte) (distance & 255); + } + } + + /* update the hash at match boundary */ + //HASH_FUNCTION(hval,ip); + hval = hashFunction(input, inOffset + ip); + htab[hval] = ip++; + + //HASH_FUNCTION(hval,ip); + hval = hashFunction(input, inOffset + ip); + htab[hval] = ip++; + + /* assuming literal copy */ + output[outOffset + op++] = (byte)(MAX_COPY - 1); + + continue; + + // Moved to be inline, with a 'continue' + /* + * literal: + * + output[outOffset + op++] = input[inOffset + anchor++]; + ip = anchor; + copy++; + if(copy == MAX_COPY){ + copy = 0; + output[outOffset + op++] = MAX_COPY-1; + } + */ + } + + /* left-over as literal copy */ + ipBound++; + while (ip <= ipBound) { + output[outOffset + op++] = input[inOffset + ip++]; + copy++; + if (copy == MAX_COPY) { + copy = 0; + output[outOffset + op++] = (byte)(MAX_COPY - 1); + } + } + + /* if we have copied something, adjust the copy length */ + if (copy != 0) { + //*(op-copy-1) = copy-1; + output[outOffset + op - copy - 1] = (byte) (copy - 1); + } else { + op--; + } + + if (level == LEVEL_2) { + /* marker for fastlz2 */ + output[outOffset] |= 1 << 5; + } + + return op; + } + + /** + * Decompress a block of compressed data and returns the size of the decompressed block. + * If error occurs, e.g. the compressed data is corrupted or the output buffer is not large + * enough, then 0 (zero) will be returned instead. + * + * Decompression is memory safe and guaranteed not to write the output buffer + * more than what is specified in outLength. + */ + public static int decompress(byte[] input, int inOffset, int inLength, + byte[] output, int outOffset, int outLength) { + //int level = ((*(const flzuint8*)input) >> 5) + 1; + int level = (input[inOffset] >> 5) + 1; + if (level != LEVEL_1 && level != LEVEL_2) { + throw new Exception($"invalid level: {level} (expected: {LEVEL_1} or {LEVEL_2})"); + } + + // const flzuint8* ip = (const flzuint8*) input; + int ip = 0; + // flzuint8* op = (flzuint8*) output; + int op = 0; + // flzuint32 ctrl = (*ip++) & 31; + long ctrl = input[inOffset + ip++] & 31; + + int loop = 1; + do { + // const flzuint8* refs = op; + int refs = op; + // flzuint32 len = ctrl >> 5; + long len = ctrl >> 5; + // flzuint32 ofs = (ctrl & 31) << 8; + long ofs = (ctrl & 31) << 8; + + if (ctrl >= 32) { + len--; + // refs -= ofs; + refs -= (int)ofs; + + int code; + if (len == 6) { + if (level == LEVEL_1) { + // len += *ip++; + len += input[inOffset + ip++] & 0xFF; + } else { + do { + code = input[inOffset + ip++] & 0xFF; + len += code; + } while (code == 255); + } + } + if (level == LEVEL_1) { + // refs -= *ip++; + refs -= input[inOffset + ip++] & 0xFF; + } else { + code = input[inOffset + ip++] & 0xFF; + refs -= code; + + /* match from 16-bit distance */ + // if(FASTLZ_UNEXPECT_CONDITIONAL(code==255)) + // if(FASTLZ_EXPECT_CONDITIONAL(ofs==(31 << 8))) + if (code == 255 && ofs == 31 << 8) { + ofs = (input[inOffset + ip++] & 0xFF) << 8; + ofs += input[inOffset + ip++] & 0xFF; + + refs = (int) (op - ofs - MAX_DISTANCE); + } + } + + // if the output index + length of block(?) + 3(?) is over the output limit? + if (op + len + 3 > outLength) { + return 0; + } + + // if (FASTLZ_UNEXPECT_CONDITIONAL(refs-1 < (flzuint8 *)output)) + // if the address space of refs-1 is < the address of output? + // if we are still at the beginning of the output address? + if (refs - 1 < 0) { + return 0; + } + + if (ip < inLength) { + ctrl = input[inOffset + ip++] & 0xFF; + } else { + loop = 0; + } + + if (refs == op) { + /* optimize copy for a run */ + // flzuint8 b = refs[-1]; + byte b = output[outOffset + refs - 1]; + output[outOffset + op++] = b; + output[outOffset + op++] = b; + output[outOffset + op++] = b; + while (len != 0) { + output[outOffset + op++] = b; + --len; + } + } else { + /* copy from reference */ + refs--; + + // *op++ = *refs++; + output[outOffset + op++] = output[outOffset + refs++]; + output[outOffset + op++] = output[outOffset + refs++]; + output[outOffset + op++] = output[outOffset + refs++]; + + while (len != 0) { + output[outOffset + op++] = output[outOffset + refs++]; + --len; + } + } + } else { + ctrl++; + + if (op + ctrl > outLength) { + return 0; + } + if (ip + ctrl > inLength) { + return 0; + } + + //*op++ = *ip++; + output[outOffset + op++] = input[inOffset + ip++]; + + for (--ctrl; ctrl != 0; ctrl--) { + // *op++ = *ip++; + output[outOffset + op++] = input[inOffset + ip++]; + } + + loop = ip < inLength ? 1 : 0; + if (loop != 0) { + // ctrl = *ip++; + ctrl = input[inOffset + ip++] & 0xFF; + } + } + + // while(FASTLZ_EXPECT_CONDITIONAL(loop)); + } while (loop != 0); + + // return op - (flzuint8*)output; + return op; + } + + private static int hashFunction(byte[] p, int offset) { + int v = readU16(p, offset); + v ^= readU16(p, offset + 1) ^ v >> 16 - HASH_LOG; + v &= HASH_MASK; + return v; + } + + private static int readU16(byte[] data, int offset) { + if (offset + 1 >= data.Length) { + return data[offset] & 0xff; + } + return (data[offset + 1] & 0xff) << 8 | data[offset] & 0xff; + } + + private FastLz() { } +} \ No newline at end of file diff --git a/src/DotRecast.Detour.TileCache/Io/Compress/FastLzTileCacheCompressor.cs b/src/DotRecast.Detour.TileCache/Io/Compress/FastLzTileCacheCompressor.cs new file mode 100644 index 0000000..c5e522f --- /dev/null +++ b/src/DotRecast.Detour.TileCache/Io/Compress/FastLzTileCacheCompressor.cs @@ -0,0 +1,39 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using DotRecast.Core; +using K4os.Compression.LZ4; + +namespace DotRecast.Detour.TileCache.Io.Compress; + +public class FastLzTileCacheCompressor : TileCacheCompressor { + + public byte[] decompress(byte[] buf, int offset, int len, int outputlen) { + byte[] output = new byte[outputlen]; + FastLz.decompress(buf, offset, len, output, 0, outputlen); + return output; + } + + public byte[] compress(byte[] buf) { + byte[] output = new byte[FastLz.calculateOutputBufferLength(buf.Length)]; + int len = FastLz.compress(buf, 0, buf.Length, output, 0, output.Length); + return ArrayUtils.CopyOf(output, len); + } + +} diff --git a/src/DotRecast.Detour.TileCache/Io/Compress/LZ4TileCacheCompressor.cs b/src/DotRecast.Detour.TileCache/Io/Compress/LZ4TileCacheCompressor.cs new file mode 100644 index 0000000..7e9e861 --- /dev/null +++ b/src/DotRecast.Detour.TileCache/Io/Compress/LZ4TileCacheCompressor.cs @@ -0,0 +1,34 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using K4os.Compression.LZ4; + +namespace DotRecast.Detour.TileCache.Io.Compress; + +public class LZ4TileCacheCompressor : TileCacheCompressor { + + public byte[] decompress(byte[] buf, int offset, int len, int outputlen) { + return LZ4Pickler.Unpickle(buf, offset, len); + } + + public byte[] compress(byte[] buf) { + return LZ4Pickler.Pickle(buf); + } + +} diff --git a/src/DotRecast.Detour.TileCache/Io/Compress/TileCacheCompressorFactory.cs b/src/DotRecast.Detour.TileCache/Io/Compress/TileCacheCompressorFactory.cs new file mode 100644 index 0000000..0812fc4 --- /dev/null +++ b/src/DotRecast.Detour.TileCache/Io/Compress/TileCacheCompressorFactory.cs @@ -0,0 +1,28 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Detour.TileCache.Io.Compress; + + + +public class TileCacheCompressorFactory { + + public static TileCacheCompressor get(bool cCompatibility) { + return cCompatibility ? new FastLzTileCacheCompressor() : new LZ4TileCacheCompressor(); + } +} diff --git a/src/DotRecast.Detour.TileCache/Io/TileCacheLayerHeaderReader.cs b/src/DotRecast.Detour.TileCache/Io/TileCacheLayerHeaderReader.cs new file mode 100644 index 0000000..38da18e --- /dev/null +++ b/src/DotRecast.Detour.TileCache/Io/TileCacheLayerHeaderReader.cs @@ -0,0 +1,60 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.IO; +using DotRecast.Core; + +namespace DotRecast.Detour.TileCache.Io; + +public class TileCacheLayerHeaderReader { + + public TileCacheLayerHeader read(ByteBuffer data, bool cCompatibility) { + TileCacheLayerHeader header = new TileCacheLayerHeader(); + header.magic = data.getInt(); + header.version = data.getInt(); + + if (header.magic != TileCacheLayerHeader.DT_TILECACHE_MAGIC) + throw new IOException("Invalid magic"); + if (header.version != TileCacheLayerHeader.DT_TILECACHE_VERSION) + throw new IOException("Invalid version"); + + header.tx = data.getInt(); + header.ty = data.getInt(); + header.tlayer = data.getInt(); + for (int j = 0; j < 3; j++) { + header.bmin[j] = data.getFloat(); + } + for (int j = 0; j < 3; j++) { + header.bmax[j] = data.getFloat(); + } + header.hmin = data.getShort() & 0xFFFF; + header.hmax = data.getShort() & 0xFFFF; + header.width = data.get() & 0xFF; + header.height = data.get() & 0xFF; + header.minx = data.get() & 0xFF; + header.maxx = data.get() & 0xFF; + header.miny = data.get() & 0xFF; + header.maxy = data.get() & 0xFF; + if (cCompatibility) { + data.getShort(); // C struct padding + } + return header; + } + +} diff --git a/src/DotRecast.Detour.TileCache/Io/TileCacheLayerHeaderWriter.cs b/src/DotRecast.Detour.TileCache/Io/TileCacheLayerHeaderWriter.cs new file mode 100644 index 0000000..d48577e --- /dev/null +++ b/src/DotRecast.Detour.TileCache/Io/TileCacheLayerHeaderWriter.cs @@ -0,0 +1,53 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.IO; +using DotRecast.Core; +using DotRecast.Detour.Io; + +namespace DotRecast.Detour.TileCache.Io; + +public class TileCacheLayerHeaderWriter : DetourWriter { + + public void write(BinaryWriter stream, TileCacheLayerHeader header, ByteOrder order, bool cCompatibility) { + write(stream, header.magic, order); + write(stream, header.version, order); + write(stream, header.tx, order); + write(stream, header.ty, order); + write(stream, header.tlayer, order); + for (int j = 0; j < 3; j++) { + write(stream, header.bmin[j], order); + } + for (int j = 0; j < 3; j++) { + write(stream, header.bmax[j], order); + } + write(stream, (short) header.hmin, order); + write(stream, (short) header.hmax, order); + write(stream, (byte) header.width); + write(stream, (byte) header.height); + write(stream, (byte) header.minx); + write(stream, (byte) header.maxx); + write(stream, (byte) header.miny); + write(stream, (byte) header.maxy); + if (cCompatibility) { + write(stream, (short) 0, order); // C struct padding + } + } + +} diff --git a/src/DotRecast.Detour.TileCache/Io/TileCacheReader.cs b/src/DotRecast.Detour.TileCache/Io/TileCacheReader.cs new file mode 100644 index 0000000..a8e7120 --- /dev/null +++ b/src/DotRecast.Detour.TileCache/Io/TileCacheReader.cs @@ -0,0 +1,94 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.IO; +using DotRecast.Core; +using DotRecast.Detour.Io; +using DotRecast.Detour.TileCache.Io.Compress; + +namespace DotRecast.Detour.TileCache.Io; + +public class TileCacheReader { + + private readonly NavMeshParamReader paramReader = new NavMeshParamReader(); + + public TileCache read(BinaryReader @is, int maxVertPerPoly, TileCacheMeshProcess meshProcessor) { + ByteBuffer bb = IOUtils.toByteBuffer(@is); + return read(bb, maxVertPerPoly, meshProcessor); + } + + public TileCache read(ByteBuffer bb, int maxVertPerPoly, TileCacheMeshProcess meshProcessor) { + TileCacheSetHeader header = new TileCacheSetHeader(); + header.magic = bb.getInt(); + if (header.magic != TileCacheSetHeader.TILECACHESET_MAGIC) { + header.magic = IOUtils.swapEndianness(header.magic); + if (header.magic != TileCacheSetHeader.TILECACHESET_MAGIC) { + throw new IOException("Invalid magic"); + } + bb.order(bb.order() == ByteOrder.BIG_ENDIAN ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN); + } + header.version = bb.getInt(); + if (header.version != TileCacheSetHeader.TILECACHESET_VERSION) { + if (header.version != TileCacheSetHeader.TILECACHESET_VERSION_RECAST4J) { + throw new IOException("Invalid version"); + } + } + bool cCompatibility = header.version == TileCacheSetHeader.TILECACHESET_VERSION; + header.numTiles = bb.getInt(); + header.meshParams = paramReader.read(bb); + header.cacheParams = readCacheParams(bb, cCompatibility); + NavMesh mesh = new NavMesh(header.meshParams, maxVertPerPoly); + TileCacheCompressor compressor = TileCacheCompressorFactory.get(cCompatibility); + TileCache tc = new TileCache(header.cacheParams, new TileCacheStorageParams(bb.order(), cCompatibility), mesh, + compressor, meshProcessor); + // Read tiles. + for (int i = 0; i < header.numTiles; ++i) { + long tileRef = bb.getInt(); + int dataSize = bb.getInt(); + if (tileRef == 0 || dataSize == 0) { + break; + } + + byte[] data = bb.ReadBytes(dataSize).ToArray(); + long tile = tc.addTile(data, 0); + if (tile != 0) { + tc.buildNavMeshTile(tile); + } + } + return tc; + } + + private TileCacheParams readCacheParams(ByteBuffer bb, bool cCompatibility) { + TileCacheParams option = new TileCacheParams(); + for (int i = 0; i < 3; i++) { + option.orig[i] = bb.getFloat(); + } + option.cs = bb.getFloat(); + option.ch = bb.getFloat(); + option.width = bb.getInt(); + option.height = bb.getInt(); + option.walkableHeight = bb.getFloat(); + option.walkableRadius = bb.getFloat(); + option.walkableClimb = bb.getFloat(); + option.maxSimplificationError = bb.getFloat(); + option.maxTiles = bb.getInt(); + option.maxObstacles = bb.getInt(); + return option; + } +} diff --git a/src/DotRecast.Detour.TileCache/Io/TileCacheSetHeader.cs b/src/DotRecast.Detour.TileCache/Io/TileCacheSetHeader.cs new file mode 100644 index 0000000..1bd9203 --- /dev/null +++ b/src/DotRecast.Detour.TileCache/Io/TileCacheSetHeader.cs @@ -0,0 +1,33 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Detour.TileCache.Io; + +public class TileCacheSetHeader { + + public const int TILECACHESET_MAGIC = 'T' << 24 | 'S' << 16 | 'E' << 8 | 'T'; // 'TSET'; + public const int TILECACHESET_VERSION = 1; + public const int TILECACHESET_VERSION_RECAST4J = 0x8801; + + public int magic; + public int version; + public int numTiles; + public NavMeshParams meshParams = new NavMeshParams(); + public TileCacheParams cacheParams = new TileCacheParams(); + +} diff --git a/src/DotRecast.Detour.TileCache/Io/TileCacheWriter.cs b/src/DotRecast.Detour.TileCache/Io/TileCacheWriter.cs new file mode 100644 index 0000000..11ea07d --- /dev/null +++ b/src/DotRecast.Detour.TileCache/Io/TileCacheWriter.cs @@ -0,0 +1,74 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.IO; +using DotRecast.Core; +using DotRecast.Detour.Io; + +namespace DotRecast.Detour.TileCache.Io; + +public class TileCacheWriter : DetourWriter { + + private readonly NavMeshParamWriter paramWriter = new NavMeshParamWriter(); + private readonly TileCacheBuilder builder = new TileCacheBuilder(); + + public void write(BinaryWriter stream, TileCache cache, ByteOrder order, bool cCompatibility) { + write(stream, TileCacheSetHeader.TILECACHESET_MAGIC, order); + write(stream, cCompatibility ? TileCacheSetHeader.TILECACHESET_VERSION + : TileCacheSetHeader.TILECACHESET_VERSION_RECAST4J, order); + int numTiles = 0; + for (int i = 0; i < cache.getTileCount(); ++i) { + CompressedTile tile = cache.getTile(i); + if (tile == null || tile.data == null) + continue; + numTiles++; + } + write(stream, numTiles, order); + paramWriter.write(stream, cache.getNavMesh().getParams(), order); + writeCacheParams(stream, cache.getParams(), order); + for (int i = 0; i < cache.getTileCount(); i++) { + CompressedTile tile = cache.getTile(i); + if (tile == null || tile.data == null) + continue; + write(stream, (int) cache.getTileRef(tile), order); + byte[] data = tile.data; + TileCacheLayer layer = cache.decompressTile(tile); + data = builder.compressTileCacheLayer(layer, order, cCompatibility); + write(stream, data.Length, order); + stream.Write(data); + } + } + + private void writeCacheParams(BinaryWriter stream, TileCacheParams option, ByteOrder order) { + for (int i = 0; i < 3; i++) { + write(stream, option.orig[i], order); + } + write(stream, option.cs, order); + write(stream, option.ch, order); + write(stream, option.width, order); + write(stream, option.height, order); + write(stream, option.walkableHeight, order); + write(stream, option.walkableRadius, order); + write(stream, option.walkableClimb, order); + write(stream, option.maxSimplificationError, order); + write(stream, option.maxTiles, order); + write(stream, option.maxObstacles, order); + } + +} diff --git a/src/DotRecast.Detour.TileCache/ObstacleRequest.cs b/src/DotRecast.Detour.TileCache/ObstacleRequest.cs new file mode 100644 index 0000000..3e84faa --- /dev/null +++ b/src/DotRecast.Detour.TileCache/ObstacleRequest.cs @@ -0,0 +1,24 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Detour.TileCache; + +public class ObstacleRequest { + public ObstacleRequestAction action; + public long refs; +} diff --git a/src/DotRecast.Detour.TileCache/ObstacleRequestAction.cs b/src/DotRecast.Detour.TileCache/ObstacleRequestAction.cs new file mode 100644 index 0000000..e838b80 --- /dev/null +++ b/src/DotRecast.Detour.TileCache/ObstacleRequestAction.cs @@ -0,0 +1,23 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Detour.TileCache; + +public enum ObstacleRequestAction { + REQUEST_ADD, REQUEST_REMOVE +} diff --git a/src/DotRecast.Detour.TileCache/ObstacleState.cs b/src/DotRecast.Detour.TileCache/ObstacleState.cs new file mode 100644 index 0000000..961c20c --- /dev/null +++ b/src/DotRecast.Detour.TileCache/ObstacleState.cs @@ -0,0 +1,24 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Detour.TileCache; + +public enum ObstacleState { + + DT_OBSTACLE_EMPTY, DT_OBSTACLE_PROCESSING, DT_OBSTACLE_PROCESSED, DT_OBSTACLE_REMOVING +} diff --git a/src/DotRecast.Detour.TileCache/TileCache.cs b/src/DotRecast.Detour.TileCache/TileCache.cs new file mode 100644 index 0000000..9e7b7b1 --- /dev/null +++ b/src/DotRecast.Detour.TileCache/TileCache.cs @@ -0,0 +1,604 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.Collections.Generic; +using DotRecast.Core; +using DotRecast.Detour.TileCache.Io; + +using static DotRecast.Detour.DetourCommon; + +namespace DotRecast.Detour.TileCache; + +public class TileCache { + + int m_tileLutSize; /// < Tile hash lookup size (must be pot). + int m_tileLutMask; /// < Tile hash lookup mask. + + private readonly CompressedTile[] m_posLookup; /// < Tile hash lookup. + private CompressedTile m_nextFreeTile; /// < Freelist of tiles. + private readonly CompressedTile[] m_tiles; /// < List of tiles. // TODO: (PP) replace with list + + private readonly int m_saltBits; /// < Number of salt bits in the tile ID. + private readonly int m_tileBits; /// < Number of tile bits in the tile ID. + + private readonly NavMesh m_navmesh; + private readonly TileCacheParams m_params; + private readonly TileCacheStorageParams m_storageParams; + + private readonly TileCacheCompressor m_tcomp; + private readonly TileCacheMeshProcess m_tmproc; + + private readonly List m_obstacles = new(); + private TileCacheObstacle m_nextFreeObstacle; + + private readonly List m_reqs = new(); + private readonly List m_update = new(); + + private readonly TileCacheBuilder builder = new TileCacheBuilder(); + private readonly TileCacheLayerHeaderReader tileReader = new TileCacheLayerHeaderReader(); + + private bool contains(List a, long v) { + return a.Contains(v); + } + + /// Encodes a tile id. + private long encodeTileId(int salt, int it) { + return ((long) salt << m_tileBits) | it; + } + + /// Decodes a tile salt. + private int decodeTileIdSalt(long refs) { + long saltMask = (1L << m_saltBits) - 1; + return (int) ((refs >> m_tileBits) & saltMask); + } + + /// Decodes a tile id. + private int decodeTileIdTile(long refs) { + long tileMask = (1L << m_tileBits) - 1; + return (int) (refs & tileMask); + } + + /// Encodes an obstacle id. + private long encodeObstacleId(int salt, int it) { + return ((long) salt << 16) | it; + } + + /// Decodes an obstacle salt. + private int decodeObstacleIdSalt(long refs) { + long saltMask = ((long) 1 << 16) - 1; + return (int) ((refs >> 16) & saltMask); + } + + /// Decodes an obstacle id. + private int decodeObstacleIdObstacle(long refs) { + long tileMask = ((long) 1 << 16) - 1; + return (int) (refs & tileMask); + } + + public TileCache(TileCacheParams option, TileCacheStorageParams storageParams, NavMesh navmesh, + TileCacheCompressor tcomp, TileCacheMeshProcess tmprocs) { + m_params = option; + m_storageParams = storageParams; + m_navmesh = navmesh; + m_tcomp = tcomp; + m_tmproc = tmprocs; + + m_tileLutSize = nextPow2(m_params.maxTiles / 4); + if (m_tileLutSize == 0) { + m_tileLutSize = 1; + } + m_tileLutMask = m_tileLutSize - 1; + m_tiles = new CompressedTile[m_params.maxTiles]; + m_posLookup = new CompressedTile[m_tileLutSize]; + for (int i = m_params.maxTiles - 1; i >= 0; --i) { + m_tiles[i] = new CompressedTile(i); + m_tiles[i].next = m_nextFreeTile; + m_nextFreeTile = m_tiles[i]; + } + m_tileBits = ilog2(nextPow2(m_params.maxTiles)); + m_saltBits = Math.Min(31, 32 - m_tileBits); + if (m_saltBits < 10) { + throw new Exception("Too few salt bits: " + m_saltBits); + } + } + + public CompressedTile getTileByRef(long refs) { + if (refs == 0) { + return null; + } + int tileIndex = decodeTileIdTile(refs); + int tileSalt = decodeTileIdSalt(refs); + if (tileIndex >= m_params.maxTiles) { + return null; + } + CompressedTile tile = m_tiles[tileIndex]; + if (tile.salt != tileSalt) { + return null; + } + return tile; + } + + public List getTilesAt(int tx, int ty) { + List tiles = new(); + + // Find tile based on hash. + int h = NavMesh.computeTileHash(tx, ty, m_tileLutMask); + CompressedTile tile = m_posLookup[h]; + while (tile != null) { + if (tile.header != null && tile.header.tx == tx && tile.header.ty == ty) { + tiles.Add(getTileRef(tile)); + } + tile = tile.next; + } + + return tiles; + } + + CompressedTile getTileAt(int tx, int ty, int tlayer) { + // Find tile based on hash. + int h = NavMesh.computeTileHash(tx, ty, m_tileLutMask); + CompressedTile tile = m_posLookup[h]; + while (tile != null) { + if (tile.header != null && tile.header.tx == tx && tile.header.ty == ty && tile.header.tlayer == tlayer) { + return tile; + } + tile = tile.next; + } + return null; + } + + public long getTileRef(CompressedTile tile) { + if (tile == null) { + return 0; + } + int it = tile.index; + return encodeTileId(tile.salt, it); + } + + public long getObstacleRef(TileCacheObstacle ob) { + if (ob == null) { + return 0; + } + int idx = ob.index; + return encodeObstacleId(ob.salt, idx); + } + + public TileCacheObstacle getObstacleByRef(long refs) { + if (refs == 0) { + return null; + } + int idx = decodeObstacleIdObstacle(refs); + if (idx >= m_obstacles.Count) { + return null; + } + + TileCacheObstacle ob = m_obstacles[idx]; + int salt = decodeObstacleIdSalt(refs); + if (ob.salt != salt) { + return null; + } + return ob; + } + + public long addTile(byte[] data, int flags) { + // Make sure the data is in right format. + ByteBuffer buf = new ByteBuffer(data); + buf.order(m_storageParams.byteOrder); + TileCacheLayerHeader header = tileReader.read(buf, m_storageParams.cCompatibility); + // Make sure the location is free. + if (getTileAt(header.tx, header.ty, header.tlayer) != null) { + return 0; + } + // Allocate a tile. + CompressedTile tile = null; + if (m_nextFreeTile != null) { + tile = m_nextFreeTile; + m_nextFreeTile = tile.next; + tile.next = null; + } + + // Make sure we could allocate a tile. + if (tile == null) { + throw new Exception("Out of storage"); + } + + // Insert tile into the position lut. + int h = NavMesh.computeTileHash(header.tx, header.ty, m_tileLutMask); + tile.next = m_posLookup[h]; + m_posLookup[h] = tile; + + // Init tile. + tile.header = header; + tile.data = data; + tile.compressed = align4(buf.position()); + tile.flags = flags; + + return getTileRef(tile); + } + + private int align4(int i) { + return (i + 3) & (~3); + } + + public void removeTile(long refs) { + if (refs == 0) { + throw new Exception("Invalid tile ref"); + } + int tileIndex = decodeTileIdTile(refs); + int tileSalt = decodeTileIdSalt(refs); + if (tileIndex >= m_params.maxTiles) { + throw new Exception("Invalid tile index"); + } + CompressedTile tile = m_tiles[tileIndex]; + if (tile.salt != tileSalt) { + throw new Exception("Invalid tile salt"); + } + + // Remove tile from hash lookup. + int h = NavMesh.computeTileHash(tile.header.tx, tile.header.ty, m_tileLutMask); + CompressedTile prev = null; + CompressedTile cur = m_posLookup[h]; + while (cur != null) { + if (cur == tile) { + if (prev != null) { + prev.next = cur.next; + } else { + m_posLookup[h] = cur.next; + } + break; + } + prev = cur; + cur = cur.next; + } + + tile.header = null; + tile.data = null; + tile.compressed = 0; + tile.flags = 0; + + // Update salt, salt should never be zero. + tile.salt = (tile.salt + 1) & ((1 << m_saltBits) - 1); + if (tile.salt == 0) { + tile.salt++; + } + + // Add to free list. + tile.next = m_nextFreeTile; + m_nextFreeTile = tile; + + } + + // Cylinder obstacle + public long addObstacle(float[] pos, float radius, float height) { + TileCacheObstacle ob = allocObstacle(); + ob.type = TileCacheObstacle.TileCacheObstacleType.CYLINDER; + + vCopy(ob.pos, pos); + ob.radius = radius; + ob.height = height; + + return addObstacleRequest(ob).refs; + } + + // Aabb obstacle + public long addBoxObstacle(float[] bmin, float[] bmax) { + TileCacheObstacle ob = allocObstacle(); + ob.type = TileCacheObstacle.TileCacheObstacleType.BOX; + + vCopy(ob.bmin, bmin); + vCopy(ob.bmax, bmax); + + return addObstacleRequest(ob).refs; + } + + // Box obstacle: can be rotated in Y + public long addBoxObstacle(float[] center, float[] extents, float yRadians) { + TileCacheObstacle ob = allocObstacle(); + ob.type = TileCacheObstacle.TileCacheObstacleType.ORIENTED_BOX; + vCopy(ob.center, center); + vCopy(ob.extents, extents); + float coshalf = (float) Math.Cos(0.5f * yRadians); + float sinhalf = (float) Math.Sin(-0.5f * yRadians); + ob.rotAux[0] = coshalf * sinhalf; + ob.rotAux[1] = coshalf * coshalf - 0.5f; + return addObstacleRequest(ob).refs; + } + + private ObstacleRequest addObstacleRequest(TileCacheObstacle ob) { + ObstacleRequest req = new ObstacleRequest(); + req.action = ObstacleRequestAction.REQUEST_ADD; + req.refs = getObstacleRef(ob); + m_reqs.Add(req); + return req; + } + + public void removeObstacle(long refs) { + if (refs == 0) { + return; + } + + ObstacleRequest req = new ObstacleRequest(); + req.action = ObstacleRequestAction.REQUEST_REMOVE; + req.refs = refs; + m_reqs.Add(req); + } + + private TileCacheObstacle allocObstacle() { + TileCacheObstacle o = m_nextFreeObstacle; + if (o == null) { + o = new TileCacheObstacle(m_obstacles.Count); + m_obstacles.Add(o); + } else { + m_nextFreeObstacle = o.next; + } + o.state = ObstacleState.DT_OBSTACLE_PROCESSING; + o.touched.Clear(); + o.pending.Clear(); + o.next = null; + return o; + } + + List queryTiles(float[] bmin, float[] bmax) { + List results = new(); + float tw = m_params.width * m_params.cs; + float th = m_params.height * m_params.cs; + int tx0 = (int) Math.Floor((bmin[0] - m_params.orig[0]) / tw); + int tx1 = (int) Math.Floor((bmax[0] - m_params.orig[0]) / tw); + int ty0 = (int) Math.Floor((bmin[2] - m_params.orig[2]) / th); + int ty1 = (int) Math.Floor((bmax[2] - m_params.orig[2]) / th); + for (int ty = ty0; ty <= ty1; ++ty) { + for (int tx = tx0; tx <= tx1; ++tx) { + List tiles = getTilesAt(tx, ty); + foreach (long i in tiles) { + CompressedTile tile = m_tiles[decodeTileIdTile(i)]; + float[] tbmin = new float[3]; + float[] tbmax = new float[3]; + calcTightTileBounds(tile.header, tbmin, tbmax); + if (overlapBounds(bmin, bmax, tbmin, tbmax)) { + results.Add(i); + } + } + } + } + return results; + } + + /** + * Updates the tile cache by rebuilding tiles touched by unfinished obstacle requests. + * + * @return Returns true if the tile cache is fully up to date with obstacle requests and tile rebuilds. If the tile + * cache is up to date another (immediate) call to update will have no effect; otherwise another call will + * continue processing obstacle requests and tile rebuilds. + */ + public bool update() { + if (0 == m_update.Count) { + // Process requests. + foreach (ObstacleRequest req in m_reqs) { + int idx = decodeObstacleIdObstacle(req.refs); + if (idx >= m_obstacles.Count) { + continue; + } + TileCacheObstacle ob = m_obstacles[idx]; + int salt = decodeObstacleIdSalt(req.refs); + if (ob.salt != salt) { + continue; + } + + if (req.action == ObstacleRequestAction.REQUEST_ADD) { + // Find touched tiles. + float[] bmin = new float[3]; + float[] bmax = new float[3]; + getObstacleBounds(ob, bmin, bmax); + ob.touched = queryTiles(bmin, bmax); + // Add tiles to update list. + ob.pending.Clear(); + foreach (long j in ob.touched) { + if (!contains(m_update, j)) { + m_update.Add(j); + } + ob.pending.Add(j); + } + } else if (req.action == ObstacleRequestAction.REQUEST_REMOVE) { + // Prepare to remove obstacle. + ob.state = ObstacleState.DT_OBSTACLE_REMOVING; + // Add tiles to update list. + ob.pending.Clear(); + foreach (long j in ob.touched) { + if (!contains(m_update, j)) { + m_update.Add(j); + } + ob.pending.Add(j); + } + } + } + + m_reqs.Clear(); + } + + // Process updates + if (0 < m_update.Count) { + long refs = m_update[0]; + m_update.RemoveAt(0); + // Build mesh + buildNavMeshTile(refs); + + // Update obstacle states. + for (int i = 0; i < m_obstacles.Count; ++i) { + TileCacheObstacle ob = m_obstacles[i]; + if (ob.state == ObstacleState.DT_OBSTACLE_PROCESSING + || ob.state == ObstacleState.DT_OBSTACLE_REMOVING) { + // Remove handled tile from pending list. + ob.pending.Remove(refs); + + // If all pending tiles processed, change state. + if (0 == ob.pending.Count) { + if (ob.state == ObstacleState.DT_OBSTACLE_PROCESSING) { + ob.state = ObstacleState.DT_OBSTACLE_PROCESSED; + } else if (ob.state == ObstacleState.DT_OBSTACLE_REMOVING) { + ob.state = ObstacleState.DT_OBSTACLE_EMPTY; + // Update salt, salt should never be zero. + ob.salt = (ob.salt + 1) & ((1 << 16) - 1); + if (ob.salt == 0) { + ob.salt++; + } + // Return obstacle to free list. + ob.next = m_nextFreeObstacle; + m_nextFreeObstacle = ob; + } + } + } + } + } + + return 0 == m_update.Count && 0 == m_reqs.Count; + } + + public void buildNavMeshTile(long refs) { + int idx = decodeTileIdTile(refs); + if (idx > m_params.maxTiles) { + throw new Exception("Invalid tile index"); + } + CompressedTile tile = m_tiles[idx]; + int salt = decodeTileIdSalt(refs); + if (tile.salt != salt) { + throw new Exception("Invalid tile salt"); + } + int walkableClimbVx = (int) (m_params.walkableClimb / m_params.ch); + + // Decompress tile layer data. + TileCacheLayer layer = decompressTile(tile); + + // Rasterize obstacles. + for (int i = 0; i < m_obstacles.Count; ++i) { + TileCacheObstacle ob = m_obstacles[i]; + if (ob.state == ObstacleState.DT_OBSTACLE_EMPTY || ob.state == ObstacleState.DT_OBSTACLE_REMOVING) { + continue; + } + if (contains(ob.touched, refs)) { + if (ob.type == TileCacheObstacle.TileCacheObstacleType.CYLINDER) { + builder.markCylinderArea(layer, tile.header.bmin, m_params.cs, m_params.ch, ob.pos, ob.radius, + ob.height, 0); + } else if (ob.type == TileCacheObstacle.TileCacheObstacleType.BOX) { + builder.markBoxArea(layer, tile.header.bmin, m_params.cs, m_params.ch, ob.bmin, ob.bmax, 0); + } else if (ob.type == TileCacheObstacle.TileCacheObstacleType.ORIENTED_BOX) { + builder.markBoxArea(layer, tile.header.bmin, m_params.cs, m_params.ch, ob.center, ob.extents, + ob.rotAux, 0); + } + } + } + // Build navmesh + builder.buildTileCacheRegions(layer, walkableClimbVx); + TileCacheContourSet lcset = builder.buildTileCacheContours(layer, walkableClimbVx, + m_params.maxSimplificationError); + TileCachePolyMesh polyMesh = builder.buildTileCachePolyMesh(lcset, m_navmesh.getMaxVertsPerPoly()); + // Early out if the mesh tile is empty. + if (polyMesh.npolys == 0) { + m_navmesh.removeTile(m_navmesh.getTileRefAt(tile.header.tx, tile.header.ty, tile.header.tlayer)); + return; + } + NavMeshDataCreateParams option = new NavMeshDataCreateParams(); + option.verts = polyMesh.verts; + option.vertCount = polyMesh.nverts; + option.polys = polyMesh.polys; + option.polyAreas = polyMesh.areas; + option.polyFlags = polyMesh.flags; + option.polyCount = polyMesh.npolys; + option.nvp = m_navmesh.getMaxVertsPerPoly(); + option.walkableHeight = m_params.walkableHeight; + option.walkableRadius = m_params.walkableRadius; + option.walkableClimb = m_params.walkableClimb; + option.tileX = tile.header.tx; + option.tileZ = tile.header.ty; + option.tileLayer = tile.header.tlayer; + option.cs = m_params.cs; + option.ch = m_params.ch; + option.buildBvTree = false; + option.bmin = tile.header.bmin; + option.bmax = tile.header.bmax; + if (m_tmproc != null) { + m_tmproc.process(option); + } + MeshData meshData = NavMeshBuilder.createNavMeshData(option); + // Remove existing tile. + m_navmesh.removeTile(m_navmesh.getTileRefAt(tile.header.tx, tile.header.ty, tile.header.tlayer)); + // Add new tile, or leave the location empty. if (navData) { // Let the + if (meshData != null) { + m_navmesh.addTile(meshData, 0, 0); + } + } + + public TileCacheLayer decompressTile(CompressedTile tile) { + TileCacheLayer layer = builder.decompressTileCacheLayer(m_tcomp, tile.data, m_storageParams.byteOrder, + m_storageParams.cCompatibility); + return layer; + } + + void calcTightTileBounds(TileCacheLayerHeader header, float[] bmin, float[] bmax) { + float cs = m_params.cs; + bmin[0] = header.bmin[0] + header.minx * cs; + bmin[1] = header.bmin[1]; + bmin[2] = header.bmin[2] + header.miny * cs; + bmax[0] = header.bmin[0] + (header.maxx + 1) * cs; + bmax[1] = header.bmax[1]; + bmax[2] = header.bmin[2] + (header.maxy + 1) * cs; + } + + void getObstacleBounds(TileCacheObstacle ob, float[] bmin, float[] bmax) { + if (ob.type == TileCacheObstacle.TileCacheObstacleType.CYLINDER) { + bmin[0] = ob.pos[0] - ob.radius; + bmin[1] = ob.pos[1]; + bmin[2] = ob.pos[2] - ob.radius; + bmax[0] = ob.pos[0] + ob.radius; + bmax[1] = ob.pos[1] + ob.height; + bmax[2] = ob.pos[2] + ob.radius; + } else if (ob.type == TileCacheObstacle.TileCacheObstacleType.BOX) { + vCopy(bmin, ob.bmin); + vCopy(bmax, ob.bmax); + } else if (ob.type == TileCacheObstacle.TileCacheObstacleType.ORIENTED_BOX) { + float maxr = 1.41f * Math.Max(ob.extents[0], ob.extents[2]); + bmin[0] = ob.center[0] - maxr; + bmax[0] = ob.center[0] + maxr; + bmin[1] = ob.center[1] - ob.extents[1]; + bmax[1] = ob.center[1] + ob.extents[1]; + bmin[2] = ob.center[2] - maxr; + bmax[2] = ob.center[2] + maxr; + } + } + + public TileCacheParams getParams() { + return m_params; + } + + public TileCacheCompressor getCompressor() { + return m_tcomp; + } + + public int getTileCount() { + return m_params.maxTiles; + } + + public CompressedTile getTile(int i) { + return m_tiles[i]; + } + + public NavMesh getNavMesh() { + return m_navmesh; + } +} diff --git a/src/DotRecast.Detour.TileCache/TileCacheBuilder.cs b/src/DotRecast.Detour.TileCache/TileCacheBuilder.cs new file mode 100644 index 0000000..4384cf3 --- /dev/null +++ b/src/DotRecast.Detour.TileCache/TileCacheBuilder.cs @@ -0,0 +1,1842 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.Collections.Generic; +using System.IO; +using DotRecast.Core; +using DotRecast.Detour.TileCache.Io; +using DotRecast.Detour.TileCache.Io.Compress; +using static DotRecast.Detour.DetourCommon; + +namespace DotRecast.Detour.TileCache; + +public class TileCacheBuilder { + + const int DT_TILECACHE_NULL_AREA = 0; + const int DT_TILECACHE_WALKABLE_AREA = 63; + const int DT_TILECACHE_NULL_IDX = 0xffff; + + public class LayerSweepSpan { + public int ns; // number samples + public int id; // region id + public int nei; // neighbour id + }; + + public class LayerMonotoneRegion { + public int area; + public List neis = new(16); + public int regId; + public int areaId; + }; + + public class TempContour { + public List verts; + public int nverts; + public List poly; + + public TempContour() { + verts = new(); + nverts = 0; + poly = new(); + } + + public int npoly() { + return poly.Count; + } + + public void clear() { + nverts = 0; + verts.Clear(); + } + }; + + public class Edge { + public int[] vert = new int[2]; + public int[] polyEdge = new int[2]; + public int[] poly = new int[2]; + }; + + private readonly TileCacheLayerHeaderReader reader = new TileCacheLayerHeaderReader(); + + public void buildTileCacheRegions(TileCacheLayer layer, int walkableClimb) { + + int w = layer.header.width; + int h = layer.header.height; + + Array.Fill(layer.regs, (short) 0x00FF); + int nsweeps = w; + LayerSweepSpan[] sweeps = new LayerSweepSpan[nsweeps]; + for (int i = 0; i < sweeps.Length; i++) { + sweeps[i] = new LayerSweepSpan(); + } + // Partition walkable area into monotone regions. + int[] prevCount = new int[256]; + int regId = 0; + + for (int y = 0; y < h; ++y) { + if (regId > 0) { + Array.Fill(prevCount, 0, 0, regId); + } + // memset(prevCount,0,sizeof(char)*regId); + int sweepId = 0; + + for (int x = 0; x < w; ++x) { + int idx = x + y * w; + if (layer.areas[idx] == DT_TILECACHE_NULL_AREA) + continue; + + int sid = 0xff; + + // -x + int xidx = (x - 1) + y * w; + if (x > 0 && isConnected(layer, idx, xidx, walkableClimb)) { + if (layer.regs[xidx] != 0xff) + sid = layer.regs[xidx]; + } + + if (sid == 0xff) { + sid = sweepId++; + sweeps[sid].nei = 0xff; + sweeps[sid].ns = 0; + } + + // -y + int yidx = x + (y - 1) * w; + if (y > 0 && isConnected(layer, idx, yidx, walkableClimb)) { + int nr = layer.regs[yidx]; + if (nr != 0xff) { + // Set neighbour when first valid neighbour is + // encoutered. + if (sweeps[sid].ns == 0) + sweeps[sid].nei = nr; + + if (sweeps[sid].nei == nr) { + // Update existing neighbour + sweeps[sid].ns++; + prevCount[nr]++; + } else { + // This is hit if there is nore than one neighbour. + // Invalidate the neighbour. + sweeps[sid].nei = 0xff; + } + } + } + + layer.regs[idx] = (byte) sid; + } + + // Create unique ID. + for (int i = 0; i < sweepId; ++i) { + // If the neighbour is set and there is only one continuous + // connection to it, + // the sweep will be merged with the previous one, else new + // region is created. + if (sweeps[i].nei != 0xff && prevCount[sweeps[i].nei] == sweeps[i].ns) { + sweeps[i].id = sweeps[i].nei; + } else { + if (regId == 255) { + // Region ID's overflow. + throw new Exception("Buffer too small"); + } + sweeps[i].id = regId++; + } + } + + // Remap local sweep ids to region ids. + for (int x = 0; x < w; ++x) { + int idx = x + y * w; + if (layer.regs[idx] != 0xff) + layer.regs[idx] = (short) sweeps[layer.regs[idx]].id; + } + } + + // Allocate and init layer regions. + int nregs = regId; + LayerMonotoneRegion[] regs = new LayerMonotoneRegion[nregs]; + + for (int i = 0; i < nregs; ++i) { + regs[i] = new LayerMonotoneRegion(); + regs[i].regId = 0xff; + } + + // Find region neighbours. + for (int y = 0; y < h; ++y) { + for (int x = 0; x < w; ++x) { + int idx = x + y * w; + int ri = layer.regs[idx]; + if (ri == 0xff) + continue; + + // Update area. + regs[ri].area++; + regs[ri].areaId = layer.areas[idx]; + + // Update neighbours + int ymi = x + (y - 1) * w; + if (y > 0 && isConnected(layer, idx, ymi, walkableClimb)) { + int rai = layer.regs[ymi]; + if (rai != 0xff && rai != ri) { + addUniqueLast(regs[ri].neis, rai); + addUniqueLast(regs[rai].neis, ri); + } + } + } + } + + for (int i = 0; i < nregs; ++i) + regs[i].regId = i; + + for (int i = 0; i < nregs; ++i) { + LayerMonotoneRegion reg = regs[i]; + + int merge = -1; + int mergea = 0; + foreach (int nei in reg.neis) { + LayerMonotoneRegion regn = regs[nei]; + if (reg.regId == regn.regId) + continue; + if (reg.areaId != regn.areaId) + continue; + if (regn.area > mergea) { + if (canMerge(reg.regId, regn.regId, regs, nregs)) { + mergea = regn.area; + merge = nei; + } + } + } + if (merge != -1) { + int oldId = reg.regId; + int newId = regs[merge].regId; + for (int j = 0; j < nregs; ++j) + if (regs[j].regId == oldId) + regs[j].regId = newId; + } + } + + // Compact ids. + int[] remap = new int[256]; + // Find number of unique regions. + regId = 0; + for (int i = 0; i < nregs; ++i) + remap[regs[i].regId] = 1; + for (int i = 0; i < 256; ++i) + if (remap[i] != 0) + remap[i] = regId++; + // Remap ids. + for (int i = 0; i < nregs; ++i) + regs[i].regId = remap[regs[i].regId]; + + layer.regCount = regId; + + for (int i = 0; i < w * h; ++i) { + if (layer.regs[i] != 0xff) + layer.regs[i] = (short) regs[layer.regs[i]].regId; + } + + } + + void addUniqueLast(List a, int v) { + int n = a.Count; + if (n > 0 && a[n - 1] == v) + return; + a.Add(v); + } + + bool isConnected(TileCacheLayer layer, int ia, int ib, int walkableClimb) { + if (layer.areas[ia] != layer.areas[ib]) + return false; + if (Math.Abs(layer.heights[ia] - layer.heights[ib]) > walkableClimb) + return false; + return true; + } + + bool canMerge(int oldRegId, int newRegId, LayerMonotoneRegion[] regs, int nregs) { + int count = 0; + for (int i = 0; i < nregs; ++i) { + LayerMonotoneRegion reg = regs[i]; + if (reg.regId != oldRegId) + continue; + foreach (int nei in reg.neis) { + if (regs[nei].regId == newRegId) + count++; + } + } + return count == 1; + } + + private void appendVertex(TempContour cont, int x, int y, int z, int r) { + // Try to merge with existing segments. + if (cont.nverts > 1) { + int pa = (cont.nverts - 2) * 4; + int pb = (cont.nverts - 1) * 4; + if (cont.verts[pb + 3] == r) { + if (cont.verts[pa] == cont.verts[pb] && cont.verts[pb] == x) { + // The verts are aligned aling x-axis, update z. + cont.verts[pb + 1] = y; + cont.verts[pb + 2] = z; + return; + } else if (cont.verts[pa + 2] == cont.verts[pb + 2] + && cont.verts[pb + 2] == z) { + // The verts are aligned aling z-axis, update x. + cont.verts[pb] = x; + cont.verts[pb + 1] = y; + return; + } + } + } + cont.verts.Add(x); + cont.verts.Add(y); + cont.verts.Add(z); + cont.verts.Add(r); + cont.nverts++; + } + + private int getNeighbourReg(TileCacheLayer layer, int ax, int ay, int dir) { + int w = layer.header.width; + int ia = ax + ay * w; + + int con = layer.cons[ia] & 0xf; + int portal = layer.cons[ia] >> 4; + int mask = 1 << dir; + + if ((con & mask) == 0) { + // No connection, return portal or hard edge. + if ((portal & mask) != 0) + return 0xf8 + dir; + return 0xff; + } + + int bx = ax + getDirOffsetX(dir); + int by = ay + getDirOffsetY(dir); + int ib = bx + by * w; + return layer.regs[ib]; + } + + private int getDirOffsetX(int dir) { + int[] offset = new int[] { -1, 0, 1, 0, }; + return offset[dir & 0x03]; + } + + private int getDirOffsetY(int dir) { + int[] offset = new int[] { 0, 1, 0, -1 }; + return offset[dir & 0x03]; + } + + private void walkContour(TileCacheLayer layer, int x, int y, TempContour cont) { + int w = layer.header.width; + int h = layer.header.height; + + cont.clear(); + + int startX = x; + int startY = y; + int startDir = -1; + + for (int i = 0; i < 4; ++i) { + int ndir = (i + 3) & 3; + int rn = getNeighbourReg(layer, x, y, ndir); + if (rn != layer.regs[x + y * w]) { + startDir = ndir; + break; + } + } + if (startDir == -1) + return; + + int dir = startDir; + int maxIter = w * h; + int iter = 0; + while (iter < maxIter) { + int rn = getNeighbourReg(layer, x, y, dir); + + int nx = x; + int ny = y; + int ndir = dir; + + if (rn != layer.regs[x + y * w]) { + // Solid edge. + int px = x; + int pz = y; + switch (dir) { + case 0: + pz++; + break; + case 1: + px++; + pz++; + break; + case 2: + px++; + break; + } + + // Try to merge with previous vertex. + appendVertex(cont, px, layer.heights[x + y * w], pz, rn); + ndir = (dir + 1) & 0x3; // Rotate CW + } else { + // Move to next. + nx = x + getDirOffsetX(dir); + ny = y + getDirOffsetY(dir); + ndir = (dir + 3) & 0x3; // Rotate CCW + } + + if (iter > 0 && x == startX && y == startY && dir == startDir) + break; + + x = nx; + y = ny; + dir = ndir; + + iter++; + } + + // Remove last vertex if it is duplicate of the first one. + int pa = (cont.nverts - 1) * 4; + int pb = 0; + if (cont.verts[pa] == cont.verts[pb] + && cont.verts[pa + 2] == cont.verts[pb + 2]) + cont.nverts--; + + } + + private float distancePtSeg(int x, int z, int px, int pz, int qx, int qz) { + float pqx = qx - px; + float pqz = qz - pz; + float dx = x - px; + float dz = z - pz; + float d = pqx * pqx + pqz * pqz; + float t = pqx * dx + pqz * dz; + if (d > 0) + t /= d; + if (t < 0) + t = 0; + else if (t > 1) + t = 1; + + dx = px + t * pqx - x; + dz = pz + t * pqz - z; + + return dx * dx + dz * dz; + } + + private void simplifyContour(TempContour cont, float maxError) { + cont.poly.Clear(); + + for (int i = 0; i < cont.nverts; ++i) { + int j = (i + 1) % cont.nverts; + // Check for start of a wall segment. + int ra = j * 4 + 3; + int rb = i * 4 + 3; + if (cont.verts[ra] != cont.verts[rb]) + cont.poly.Add(i); + } + if (cont.npoly() < 2) { + // If there is no transitions at all, + // create some initial points for the simplification process. + // Find lower-left and upper-right vertices of the contour. + int llx = cont.verts[0]; + int llz = cont.verts[2]; + int lli = 0; + int urx = cont.verts[0]; + int urz = cont.verts[2]; + int uri = 0; + for (int i = 1; i < cont.nverts; ++i) { + int x = cont.verts[i * 4 + 0]; + int z = cont.verts[i * 4 + 2]; + if (x < llx || (x == llx && z < llz)) { + llx = x; + llz = z; + lli = i; + } + if (x > urx || (x == urx && z > urz)) { + urx = x; + urz = z; + uri = i; + } + } + cont.poly.Clear(); + cont.poly.Add(lli); + cont.poly.Add(uri); + } + + // Add points until all raw points are within + // error tolerance to the simplified shape. + for (int i = 0; i < cont.npoly();) { + int ii = (i + 1) % cont.npoly(); + + int ai = cont.poly[i]; + int ax = cont.verts[ai * 4]; + int az = cont.verts[ai * 4 + 2]; + + int bi = cont.poly[ii]; + int bx = cont.verts[bi * 4]; + int bz = cont.verts[bi * 4 + 2]; + + // Find maximum deviation from the segment. + float maxd = 0; + int maxi = -1; + int ci, cinc, endi; + + // Traverse the segment in lexilogical order so that the + // max deviation is calculated similarly when traversing + // opposite segments. + if (bx > ax || (bx == ax && bz > az)) { + cinc = 1; + ci = (ai + cinc) % cont.nverts; + endi = bi; + } else { + cinc = cont.nverts - 1; + ci = (bi + cinc) % cont.nverts; + endi = ai; + } + + // Tessellate only outer edges or edges between areas. + while (ci != endi) { + float d = distancePtSeg(cont.verts[ci * 4], cont.verts[ci * 4 + 2], ax, az, bx, bz); + if (d > maxd) { + maxd = d; + maxi = ci; + } + ci = (ci + cinc) % cont.nverts; + } + + // If the max deviation is larger than accepted error, + // add new point, else continue to next segment. + if (maxi != -1 && maxd > (maxError * maxError)) { + cont.poly.Insert(i + 1, maxi); + } else { + ++i; + } + } + + // Remap vertices + int start = 0; + for (int i = 1; i < cont.npoly(); ++i) + if (cont.poly[i] < cont.poly[start]) + start = i; + + cont.nverts = 0; + for (int i = 0; i < cont.npoly(); ++i) { + int j = (start + i) % cont.npoly(); + int src = cont.poly[j] * 4; + int dst = cont.nverts * 4; + cont.verts[dst] = cont.verts[src]; + cont.verts[dst + 1] = cont.verts[src + 1]; + cont.verts[dst + 2] = cont.verts[src + 2]; + cont.verts[dst + 3] = cont.verts[src + 3]; + cont.nverts++; + } + } + + static Tuple getCornerHeight(TileCacheLayer layer, int x, int y, int z, int walkableClimb) { + int w = layer.header.width; + int h = layer.header.height; + + int n = 0; + + int portal = 0xf; + int height = 0; + int preg = 0xff; + bool allSameReg = true; + + for (int dz = -1; dz <= 0; ++dz) { + for (int dx = -1; dx <= 0; ++dx) { + int px = x + dx; + int pz = z + dz; + if (px >= 0 && pz >= 0 && px < w && pz < h) { + int idx = px + pz * w; + int lh = layer.heights[idx]; + if (Math.Abs(lh - y) <= walkableClimb && layer.areas[idx] != DT_TILECACHE_NULL_AREA) { + height = Math.Max(height, (char) lh); + portal &= (layer.cons[idx] >> 4); + if (preg != 0xff && preg != layer.regs[idx]) + allSameReg = false; + preg = layer.regs[idx]; + n++; + } + } + } + } + + int portalCount = 0; + for (int dir = 0; dir < 4; ++dir) + if ((portal & (1 << dir)) != 0) + portalCount++; + + bool shouldRemove = false; + if (n > 1 && portalCount == 1 && allSameReg) { + shouldRemove = true; + } + + return Tuple.Create(height, shouldRemove); + } + + // TODO: move this somewhere else, once the layer meshing is done. + public TileCacheContourSet buildTileCacheContours(TileCacheLayer layer, int walkableClimb, float maxError) { + int w = layer.header.width; + int h = layer.header.height; + + TileCacheContourSet lcset = new TileCacheContourSet(); + lcset.nconts = layer.regCount; + lcset.conts = new TileCacheContour[lcset.nconts]; + for (int i = 0; i < lcset.nconts; i++) { + lcset.conts[i] = new TileCacheContour(); + } + + // Allocate temp buffer for contour tracing. + TempContour temp = new TempContour(); + + // Find contours. + for (int y = 0; y < h; ++y) { + for (int x = 0; x < w; ++x) { + int idx = x + y * w; + int ri = layer.regs[idx]; + if (ri == 0xff) + continue; + + TileCacheContour cont = lcset.conts[ri]; + + if (cont.nverts > 0) + continue; + + cont.reg = ri; + cont.area = layer.areas[idx]; + + walkContour(layer, x, y, temp); + + simplifyContour(temp, maxError); + + // Store contour. + cont.nverts = temp.nverts; + if (cont.nverts > 0) { + cont.verts = new int[4 * temp.nverts]; + + for (int i = 0, j = temp.nverts - 1; i < temp.nverts; j = i++) { + int dst = j * 4; + int v = j * 4; + int vn = i * 4; + int nei = temp.verts[vn + 3]; // The neighbour reg + // is + // stored at segment + // vertex of a + // segment. + Tuple res = getCornerHeight(layer, temp.verts[v], temp.verts[v + 1], + temp.verts[v + 2], walkableClimb); + int lh = res.Item1; + bool shouldRemove = res.Item2; + cont.verts[dst + 0] = temp.verts[v]; + cont.verts[dst + 1] = lh; + cont.verts[dst + 2] = temp.verts[v + 2]; + + // Store portal direction and remove status to the + // fourth component. + cont.verts[dst + 3] = 0x0f; + if (nei != 0xff && nei >= 0xf8) + cont.verts[dst + 3] = nei - 0xf8; + if (shouldRemove) + cont.verts[dst + 3] |= 0x80; + } + } + } + } + return lcset; + + } + + const uint VERTEX_BUCKET_COUNT2 = (1 << 8); + + private int computeVertexHash2(int x, int y, int z) { + uint h1 = 0x8da6b343; // Large multiplicative constants; + uint h2 = 0xd8163841; // here arbitrarily chosen primes + uint h3 = 0xcb1ab31f; + uint n = h1 * (uint)x + h2 * (uint)y + h3 * (uint)z; + return (int)(n & (VERTEX_BUCKET_COUNT2 - 1)); + } + + private int addVertex(int x, int y, int z, int[] verts, int[] firstVert, int[] nextVert, int nv) { + int bucket = computeVertexHash2(x, 0, z); + int i = firstVert[bucket]; + while (i != DT_TILECACHE_NULL_IDX) { + int tv = i * 3; + if (verts[tv] == x && verts[tv + 2] == z && (Math.Abs(verts[tv + 1] - y) <= 2)) + return i; + i = nextVert[i]; // next + } + + // Could not find, create new. + i = nv; + int v = i * 3; + verts[v] = x; + verts[v + 1] = y; + verts[v + 2] = z; + nextVert[i] = firstVert[bucket]; + firstVert[bucket] = i; + return i; + } + + private void buildMeshAdjacency(int[] polys, int npolys, int[] verts, int nverts, TileCacheContourSet lcset, + int maxVertsPerPoly) { + // Based on code by Eric Lengyel from: + // http://www.terathon.com/code/edges.php + + int maxEdgeCount = npolys * maxVertsPerPoly; + + int[] firstEdge = new int[nverts + maxEdgeCount]; + int nextEdge = nverts; + int edgeCount = 0; + + Edge[] edges = new Edge[maxEdgeCount]; + for (int i = 0; i < maxEdgeCount; i++) { + edges[i] = new Edge(); + } + for (int i = 0; i < nverts; i++) + firstEdge[i] = DT_TILECACHE_NULL_IDX; + + for (int i = 0; i < npolys; ++i) { + int t = i * maxVertsPerPoly * 2; + for (int j = 0; j < maxVertsPerPoly; ++j) { + if (polys[t + j] == DT_TILECACHE_NULL_IDX) + break; + int v0 = polys[t + j]; + int v1 = (j + 1 >= maxVertsPerPoly || polys[t + j + 1] == DT_TILECACHE_NULL_IDX) ? polys[t] + : polys[t + j + 1]; + if (v0 < v1) { + Edge edge = edges[edgeCount]; + edge.vert[0] = v0; + edge.vert[1] = v1; + edge.poly[0] = i; + edge.polyEdge[0] = j; + edge.poly[1] = i; + edge.polyEdge[1] = 0xff; + // Insert edge + firstEdge[nextEdge + edgeCount] = firstEdge[v0]; + firstEdge[v0] = (short) edgeCount; + edgeCount++; + } + } + } + + for (int i = 0; i < npolys; ++i) { + int t = i * maxVertsPerPoly * 2; + for (int j = 0; j < maxVertsPerPoly; ++j) { + if (polys[t + j] == DT_TILECACHE_NULL_IDX) + break; + int v0 = polys[t + j]; + int v1 = (j + 1 >= maxVertsPerPoly || polys[t + j + 1] == DT_TILECACHE_NULL_IDX) ? polys[t] + : polys[t + j + 1]; + if (v0 > v1) { + bool found = false; + for (int e = firstEdge[v1]; e != DT_TILECACHE_NULL_IDX; e = firstEdge[nextEdge + e]) { + Edge edge = edges[e]; + if (edge.vert[1] == v0 && edge.poly[0] == edge.poly[1]) { + edge.poly[1] = i; + edge.polyEdge[1] = j; + found = true; + break; + } + } + if (!found) { + // Matching edge not found, it is an open edge, add it. + Edge edge = edges[edgeCount]; + edge.vert[0] = v1; + edge.vert[1] = v0; + edge.poly[0] = (short) i; + edge.polyEdge[0] = (short) j; + edge.poly[1] = (short) i; + edge.polyEdge[1] = 0xff; + // Insert edge + firstEdge[nextEdge + edgeCount] = firstEdge[v1]; + firstEdge[v1] = (short) edgeCount; + edgeCount++; + } + } + } + } + + // Mark portal edges. + for (int i = 0; i < lcset.nconts; ++i) { + TileCacheContour cont = lcset.conts[i]; + if (cont.nverts < 3) + continue; + + for (int j = 0, k = cont.nverts - 1; j < cont.nverts; k = j++) { + int va = k * 4; + int vb = j * 4; + int dir = cont.verts[va + 3] & 0xf; + if (dir == 0xf) + continue; + + if (dir == 0 || dir == 2) { + // Find matching vertical edge + int x = cont.verts[va]; + int zmin = cont.verts[va + 2]; + int zmax = cont.verts[vb + 2]; + if (zmin > zmax) { + int tmp = zmin; + zmin = zmax; + zmax = tmp; + } + + for (int m = 0; m < edgeCount; ++m) { + Edge e = edges[m]; + // Skip connected edges. + if (e.poly[0] != e.poly[1]) + continue; + int eva = e.vert[0] * 3; + int evb = e.vert[1] * 3; + if (verts[eva] == x && verts[evb] == x) { + int ezmin = verts[eva + 2]; + int ezmax = verts[evb + 2]; + if (ezmin > ezmax) { + int tmp = ezmin; + ezmin = ezmax; + ezmax = tmp; + } + if (overlapRangeExl(zmin, zmax, ezmin, ezmax)) { + // Reuse the other polyedge to store dir. + e.polyEdge[1] = dir; + } + } + } + } else { + // Find matching vertical edge + int z = cont.verts[va + 2]; + int xmin = cont.verts[va]; + int xmax = cont.verts[vb]; + if (xmin > xmax) { + int tmp = xmin; + xmin = xmax; + xmax = tmp; + } + for (int m = 0; m < edgeCount; ++m) { + Edge e = edges[m]; + // Skip connected edges. + if (e.poly[0] != e.poly[1]) + continue; + int eva = e.vert[0] * 3; + int evb = e.vert[1] * 3; + if (verts[eva + 2] == z && verts[evb + 2] == z) { + int exmin = verts[eva]; + int exmax = verts[evb]; + if (exmin > exmax) { + int tmp = exmin; + exmin = exmax; + exmax = tmp; + } + if (overlapRangeExl(xmin, xmax, exmin, exmax)) { + // Reuse the other polyedge to store dir. + e.polyEdge[1] = dir; + } + } + } + } + } + } + + // Store adjacency + for (int i = 0; i < edgeCount; ++i) { + Edge e = edges[i]; + if (e.poly[0] != e.poly[1]) { + int p0 = e.poly[0] * maxVertsPerPoly * 2; + int p1 = e.poly[1] * maxVertsPerPoly * 2; + polys[p0 + maxVertsPerPoly + e.polyEdge[0]] = e.poly[1]; + polys[p1 + maxVertsPerPoly + e.polyEdge[1]] = e.poly[0]; + } else if (e.polyEdge[1] != 0xff) { + int p0 = e.poly[0] * maxVertsPerPoly * 2; + polys[p0 + maxVertsPerPoly + e.polyEdge[0]] = 0x8000 | (short) e.polyEdge[1]; + } + + } + } + + private bool overlapRangeExl(int amin, int amax, int bmin, int bmax) { + return (amin >= bmax || amax <= bmin) ? false : true; + } + + private int prev(int i, int n) { + return i - 1 >= 0 ? i - 1 : n - 1; + } + + private int next(int i, int n) { + return i + 1 < n ? i + 1 : 0; + } + + private int area2(int[] verts, int a, int b, int c) { + return (verts[b] - verts[a]) * (verts[c + 2] - verts[a + 2]) + - (verts[c] - verts[a]) * (verts[b + 2] - verts[a + 2]); + } + + // Returns true iff c is strictly to the left of the directed + // line through a to b. + private bool left(int[] verts, int a, int b, int c) { + return area2(verts, a, b, c) < 0; + } + + private bool leftOn(int[] verts, int a, int b, int c) { + return area2(verts, a, b, c) <= 0; + } + + private bool collinear(int[] verts, int a, int b, int c) { + return area2(verts, a, b, c) == 0; + } + + // Returns true iff ab properly intersects cd: they share + // a point interior to both segments. The properness of the + // intersection is ensured by using strict leftness. + private bool intersectProp(int[] verts, int a, int b, int c, int d) { + // Eliminate improper cases. + if (collinear(verts, a, b, c) || collinear(verts, a, b, d) || collinear(verts, c, d, a) + || collinear(verts, c, d, b)) + return false; + + return (left(verts, a, b, c) ^ left(verts, a, b, d)) && (left(verts, c, d, a) ^ left(verts, c, d, b)); + } + + // Returns T iff (a,b,c) are collinear and point c lies + // on the closed segement ab. + private bool between(int[] verts, int a, int b, int c) { + if (!collinear(verts, a, b, c)) + return false; + // If ab not vertical, check betweenness on x; else on y. + if (verts[a] != verts[b]) + return ((verts[a] <= verts[c]) && (verts[c] <= verts[b])) + || ((verts[a] >= verts[c]) && (verts[c] >= verts[b])); + else + return ((verts[a + 2] <= verts[c + 2]) && (verts[c + 2] <= verts[b + 2])) + || ((verts[a + 2] >= verts[c + 2]) && (verts[c + 2] >= verts[b + 2])); + } + + // Returns true iff segments ab and cd intersect, properly or improperly. + private bool intersect(int[] verts, int a, int b, int c, int d) { + if (intersectProp(verts, a, b, c, d)) + return true; + else if (between(verts, a, b, c) || between(verts, a, b, d) || between(verts, c, d, a) + || between(verts, c, d, b)) + return true; + else + return false; + } + + private bool vequal(int[] verts, int a, int b) { + return verts[a] == verts[b] && verts[a + 2] == verts[b + 2]; + } + + // Returns T iff (v_i, v_j) is a proper internal *or* external + // diagonal of P, *ignoring edges incident to v_i and v_j*. + private bool diagonalie(int i, int j, int n, int[] verts, int[] indices) { + int d0 = (indices[i] & 0x7fff) * 4; + int d1 = (indices[j] & 0x7fff) * 4; + + // For each edge (k,k+1) of P + for (int k = 0; k < n; k++) { + int k1 = next(k, n); + // Skip edges incident to i or j + if (!((k == i) || (k1 == i) || (k == j) || (k1 == j))) { + int p0 = (indices[k] & 0x7fff) * 4; + int p1 = (indices[k1] & 0x7fff) * 4; + + if (vequal(verts, d0, p0) || vequal(verts, d1, p0) || vequal(verts, d0, p1) || vequal(verts, d1, p1)) + continue; + + if (intersect(verts, d0, d1, p0, p1)) + return false; + } + } + return true; + } + + // Returns true iff the diagonal (i,j) is strictly internal to the + // polygon P in the neighborhood of the i endpoint. + private bool inCone(int i, int j, int n, int[] verts, int[] indices) { + int pi = (indices[i] & 0x7fff) * 4; + int pj = (indices[j] & 0x7fff) * 4; + int pi1 = (indices[next(i, n)] & 0x7fff) * 4; + int pin1 = (indices[prev(i, n)] & 0x7fff) * 4; + + // If P[i] is a convex vertex [ i+1 left or on (i-1,i) ]. + if (leftOn(verts, pin1, pi, pi1)) + return left(verts, pi, pj, pin1) && left(verts, pj, pi, pi1); + // Assume (i-1,i,i+1) not collinear. + // else P[i] is reflex. + return !(leftOn(verts, pi, pj, pi1) && leftOn(verts, pj, pi, pin1)); + } + + // Returns T iff (v_i, v_j) is a proper internal + // diagonal of P. + private bool diagonal(int i, int j, int n, int[] verts, int[] indices) { + return inCone(i, j, n, verts, indices) && diagonalie(i, j, n, verts, indices); + } + + private int triangulate(int n, int[] verts, int[] indices, int[] tris) { + int ntris = 0; + int dst = 0;// tris; + // The last bit of the index is used to indicate if the vertex can be + // removed. + for (int i = 0; i < n; i++) { + int i1 = next(i, n); + int i2 = next(i1, n); + if (diagonal(i, i2, n, verts, indices)) + indices[i1] |= 0x8000; + } + + while (n > 3) { + int minLen = -1; + int mini = -1; + for (int mi = 0; mi < n; mi++) { + int mi1 = next(mi, n); + if ((indices[mi1] & 0x8000) != 0) { + int p0 = (indices[mi] & 0x7fff) * 4; + int p2 = (indices[next(mi1, n)] & 0x7fff) * 4; + + int dx = verts[p2] - verts[p0]; + int dz = verts[p2 + 2] - verts[p0 + 2]; + int len = dx * dx + dz * dz; + if (minLen < 0 || len < minLen) { + minLen = len; + mini = mi; + } + } + } + + if (mini == -1) { + // Should not happen. + /* + * printf("mini == -1 ntris=%d n=%d\n", ntris, n); for (int i = 0; i < n; i++) { printf("%d ", + * indices[i] & 0x0fffffff); } printf("\n"); + */ + return -ntris; + } + + int i = mini; + int i1 = next(i, n); + int i2 = next(i1, n); + + tris[dst++] = indices[i] & 0x7fff; + tris[dst++] = indices[i1] & 0x7fff; + tris[dst++] = indices[i2] & 0x7fff; + ntris++; + + // Removes P[i1] by copying P[i+1]...P[n-1] left one index. + n--; + for (int k = i1; k < n; k++) + indices[k] = indices[k + 1]; + + if (i1 >= n) + i1 = 0; + i = prev(i1, n); + // Update diagonal flags. + if (diagonal(prev(i, n), i1, n, verts, indices)) + indices[i] |= 0x8000; + else + indices[i] &= 0x7fff; + + if (diagonal(i, next(i1, n), n, verts, indices)) + indices[i1] |= 0x8000; + else + indices[i1] &= 0x7fff; + } + + // Append the remaining triangle. + tris[dst++] = indices[0] & 0x7fff; + tris[dst++] = indices[1] & 0x7fff; + tris[dst++] = indices[2] & 0x7fff; + ntris++; + + return ntris; + } + + private int countPolyVerts(int[] polys, int p, int maxVertsPerPoly) { + for (int i = 0; i < maxVertsPerPoly; ++i) + if (polys[p + i] == DT_TILECACHE_NULL_IDX) + return i; + return maxVertsPerPoly; + } + + private bool uleft(int[] verts, int a, int b, int c) { + return (verts[b] - verts[a]) * (verts[c + 2] - verts[a + 2]) + - (verts[c] - verts[a]) * (verts[b + 2] - verts[a + 2]) < 0; + } + + private int[] getPolyMergeValue(int[] polys, int pa, int pb, int[] verts, int maxVertsPerPoly) { + int na = countPolyVerts(polys, pa, maxVertsPerPoly); + int nb = countPolyVerts(polys, pb, maxVertsPerPoly); + + // If the merged polygon would be too big, do not merge. + if (na + nb - 2 > maxVertsPerPoly) + return new int[] { -1, 0, 0 }; + + // Check if the polygons share an edge. + int ea = -1; + int eb = -1; + + for (int i = 0; i < na; ++i) { + int va0 = polys[pa + i]; + int va1 = polys[pa + (i + 1) % na]; + if (va0 > va1) { + int tmp = va0; + va0 = va1; + va1 = tmp; + } + for (int j = 0; j < nb; ++j) { + int vb0 = polys[pb + j]; + int vb1 = polys[pb + (j + 1) % nb]; + if (vb0 > vb1) { + int tmp = vb0; + vb0 = vb1; + vb1 = tmp; + } + if (va0 == vb0 && va1 == vb1) { + ea = i; + eb = j; + break; + } + } + } + + // No common edge, cannot merge. + if (ea == -1 || eb == -1) + return new int[] { -1, ea, eb }; + + // Check to see if the merged polygon would be convex. + int va, vb, vc; + + va = polys[pa + (ea + na - 1) % na]; + vb = polys[pa + ea]; + vc = polys[pb + (eb + 2) % nb]; + if (!uleft(verts, va * 3, vb * 3, vc * 3)) + return new int[] { -1, ea, eb }; + + va = polys[pb + (eb + nb - 1) % nb]; + vb = polys[pb + eb]; + vc = polys[pa + (ea + 2) % na]; + if (!uleft(verts, va * 3, vb * 3, vc * 3)) + return new int[] { -1, ea, eb }; + + va = polys[pa + ea]; + vb = polys[pa + (ea + 1) % na]; + + int dx = verts[va * 3 + 0] - verts[vb * 3 + 0]; + int dy = verts[va * 3 + 2] - verts[vb * 3 + 2]; + + return new int[] { dx * dx + dy * dy, ea, eb }; + } + + private void mergePolys(int[] polys, int pa, int pb, int ea, int eb, int maxVertsPerPoly) { + int[] tmp = new int[maxVertsPerPoly * 2]; + + int na = countPolyVerts(polys, pa, maxVertsPerPoly); + int nb = countPolyVerts(polys, pb, maxVertsPerPoly); + + // Merge polygons. + Array.Fill(tmp, DT_TILECACHE_NULL_IDX); + int n = 0; + // Add pa + for (int i = 0; i < na - 1; ++i) + tmp[n++] = polys[pa + (ea + 1 + i) % na]; + // Add pb + for (int i = 0; i < nb - 1; ++i) + tmp[n++] = polys[pb + (eb + 1 + i) % nb]; + Array.Copy(tmp, 0, polys, pa, maxVertsPerPoly); + } + + private int pushFront(int v, List arr) { + arr.Insert(0, v); + return arr.Count; + } + + private int pushBack(int v, List arr) { + arr.Add(v); + return arr.Count; + } + + private bool canRemoveVertex(TileCachePolyMesh mesh, int rem) { + // Count number of polygons to remove. + int maxVertsPerPoly = mesh.nvp; + int numRemainingEdges = 0; + for (int i = 0; i < mesh.npolys; ++i) { + int p = i * mesh.nvp * 2; + int nv = countPolyVerts(mesh.polys, p, maxVertsPerPoly); + int numRemoved = 0; + int numVerts = 0; + for (int j = 0; j < nv; ++j) { + if (mesh.polys[p + j] == rem) { + numRemoved++; + } + numVerts++; + } + if (numRemoved != 0) { + numRemainingEdges += numVerts - (numRemoved + 1); + } + } + + // There would be too few edges remaining to create a polygon. + // This can happen for example when a tip of a triangle is marked + // as deletion, but there are no other polys that share the vertex. + // In this case, the vertex should not be removed. + if (numRemainingEdges <= 2) + return false; + + // Find edges which share the removed vertex. + List edges = new(); + int nedges = 0; + + for (int i = 0; i < mesh.npolys; ++i) { + int p = i * mesh.nvp * 2; + int nv = countPolyVerts(mesh.polys, p, maxVertsPerPoly); + + // Collect edges which touches the removed vertex. + for (int j = 0, k = nv - 1; j < nv; k = j++) { + if (mesh.polys[p + j] == rem || mesh.polys[p + k] == rem) { + // Arrange edge so that a=rem. + int a = mesh.polys[p + j], b = mesh.polys[p + k]; + if (b == rem) { + int tmp = a; + a = b; + b = tmp; + } + + // Check if the edge exists + bool exists = false; + for (int m = 0; m < nedges; ++m) { + int e = m * 3; + if (edges[e + 1] == b) { + // Exists, increment vertex share count. + edges[e + 2] = edges[e + 2] + 1; + exists = true; + } + } + // Add new edge. + if (!exists) { + edges.Add(a); + edges.Add(b); + edges.Add(1); + nedges++; + } + } + } + } + + // There should be no more than 2 open edges. + // This catches the case that two non-adjacent polygons + // share the removed vertex. In that case, do not remove the vertex. + int numOpenEdges = 0; + for (int i = 0; i < nedges; ++i) { + if (edges[i * 3 + 2] < 2) + numOpenEdges++; + } + if (numOpenEdges > 2) + return false; + + return true; + } + + private void removeVertex(TileCachePolyMesh mesh, int rem, int maxTris) { + // Count number of polygons to remove. + int maxVertsPerPoly = mesh.nvp; + int numRemovedVerts = 0; + for (int i = 0; i < mesh.npolys; ++i) { + int p = i * maxVertsPerPoly * 2; + int nv = countPolyVerts(mesh.polys, p, maxVertsPerPoly); + for (int j = 0; j < nv; ++j) { + if (mesh.polys[p + j] == rem) + numRemovedVerts++; + } + } + + int nedges = 0; + List edges = new(); + int nhole = 0; + List hole = new(); + List harea = new(); + + for (int i = 0; i < mesh.npolys; ++i) { + int p = i * maxVertsPerPoly * 2; + int nv = countPolyVerts(mesh.polys, p, maxVertsPerPoly); + bool hasRem = false; + for (int j = 0; j < nv; ++j) + if (mesh.polys[p + j] == rem) + hasRem = true; + if (hasRem) { + // Collect edges which does not touch the removed vertex. + for (int j = 0, k = nv - 1; j < nv; k = j++) { + if (mesh.polys[p + j] != rem && mesh.polys[p + k] != rem) { + edges.Add(mesh.polys[p + k]); + edges.Add(mesh.polys[p + j]); + edges.Add(mesh.areas[i]); + nedges++; + } + } + // Remove the polygon. + int p2 = (mesh.npolys - 1) * maxVertsPerPoly * 2; + Array.Copy(mesh.polys, p2, mesh.polys, p, maxVertsPerPoly); + Array.Fill(mesh.polys, DT_TILECACHE_NULL_IDX, p + maxVertsPerPoly, maxVertsPerPoly); + mesh.areas[i] = mesh.areas[mesh.npolys - 1]; + mesh.npolys--; + --i; + } + } + + // Remove vertex. + for (int i = rem; i < mesh.nverts; ++i) { + mesh.verts[i * 3 + 0] = mesh.verts[(i + 1) * 3 + 0]; + mesh.verts[i * 3 + 1] = mesh.verts[(i + 1) * 3 + 1]; + mesh.verts[i * 3 + 2] = mesh.verts[(i + 1) * 3 + 2]; + } + mesh.nverts--; + + // Adjust indices to match the removed vertex layout. + for (int i = 0; i < mesh.npolys; ++i) { + int p = i * maxVertsPerPoly * 2; + int nv = countPolyVerts(mesh.polys, p, maxVertsPerPoly); + for (int j = 0; j < nv; ++j) + if (mesh.polys[p + j] > rem) + mesh.polys[p + j]--; + } + for (int i = 0; i < nedges; ++i) { + if (edges[i * 3] > rem) + edges[i * 3] = edges[i * 3] - 1; + if (edges[i * 3 + 1] > rem) + edges[i * 3 + 1] = edges[i * 3 + 1] - 1; + } + + if (nedges == 0) + return; + + // Start with one vertex, keep appending connected + // segments to the start and end of the hole. + nhole = pushBack(edges[0], hole); + pushBack(edges[2], harea); + + while (nedges != 0) { + bool match = false; + + for (int i = 0; i < nedges; ++i) { + int ea = edges[i * 3]; + int eb = edges[i * 3 + 1]; + int a = edges[i * 3 + 2]; + bool add = false; + if (hole[0] == eb) { + // The segment matches the beginning of the hole boundary. + nhole = pushFront(ea, hole); + pushFront(a, harea); + add = true; + } else if (hole[nhole - 1] == ea) { + // The segment matches the end of the hole boundary. + nhole = pushBack(eb, hole); + pushBack(a, harea); + add = true; + } + if (add) { + // The edge segment was added, remove it. + edges[i * 3] = edges[(nedges - 1) * 3]; + edges[i * 3 + 1] = edges[(nedges - 1) * 3] + 1; + edges[i * 3 + 2] = edges[(nedges - 1) * 3] + 2; + --nedges; + match = true; + --i; + } + } + + if (!match) + break; + } + + int[] tris = new int[nhole * 3]; + int[] tverts = new int[nhole * 4]; + int[] tpoly = new int[nhole]; + + // Generate temp vertex array for triangulation. + for (int i = 0; i < nhole; ++i) { + int pi = hole[i]; + tverts[i * 4 + 0] = mesh.verts[pi * 3 + 0]; + tverts[i * 4 + 1] = mesh.verts[pi * 3 + 1]; + tverts[i * 4 + 2] = mesh.verts[pi * 3 + 2]; + tverts[i * 4 + 3] = 0; + tpoly[i] = i; + } + + // Triangulate the hole. + int ntris = triangulate(nhole, tverts, tpoly, tris); + if (ntris < 0) { + // TODO: issue warning! + ntris = -ntris; + } + + int[] polys = new int[ntris * maxVertsPerPoly]; + int[] pareas = new int[ntris]; + + // Build initial polygons. + int npolys = 0; + Array.Fill(polys, DT_TILECACHE_NULL_IDX, 0, ntris * maxVertsPerPoly); + for (int j = 0; j < ntris; ++j) { + int t = j * 3; + if (tris[t] != tris[t + 1] && tris[t] != tris[t + 2] && tris[t + 1] != tris[t + 2]) { + polys[npolys * maxVertsPerPoly + 0] = hole[tris[t]]; + polys[npolys * maxVertsPerPoly + 1] = hole[tris[t + 1]]; + polys[npolys * maxVertsPerPoly + 2] = hole[tris[t + 2]]; + pareas[npolys] = harea[tris[t]]; + npolys++; + } + } + if (npolys == 0) + return; + + // Merge polygons. + if (maxVertsPerPoly > 3) { + for (;;) { + // Find best polygons to merge. + int bestMergeVal = 0; + int bestPa = 0, bestPb = 0, bestEa = 0, bestEb = 0; + + for (int j = 0; j < npolys - 1; ++j) { + int pj = j * maxVertsPerPoly; + for (int k = j + 1; k < npolys; ++k) { + int pk = k * maxVertsPerPoly; + int[] pm = getPolyMergeValue(polys, pj, pk, mesh.verts, maxVertsPerPoly); + int v = pm[0]; + int ea = pm[1]; + int eb = pm[2]; + if (v > bestMergeVal) { + bestMergeVal = v; + bestPa = j; + bestPb = k; + bestEa = ea; + bestEb = eb; + } + } + } + + if (bestMergeVal > 0) { + // Found best, merge. + int pa = bestPa * maxVertsPerPoly; + int pb = bestPb * maxVertsPerPoly; + mergePolys(polys, pa, pb, bestEa, bestEb, maxVertsPerPoly); + Array.Copy(polys, (npolys - 1) * maxVertsPerPoly, polys, pb, maxVertsPerPoly); + pareas[bestPb] = pareas[npolys - 1]; + npolys--; + } else { + // Could not merge any polygons, stop. + break; + } + } + } + + // Store polygons. + for (int i = 0; i < npolys; ++i) { + if (mesh.npolys >= maxTris) + break; + int p = mesh.npolys * maxVertsPerPoly * 2; + Array.Fill(mesh.polys, DT_TILECACHE_NULL_IDX, p, maxVertsPerPoly * 2); + for (int j = 0; j < maxVertsPerPoly; ++j) + mesh.polys[p + j] = polys[i * maxVertsPerPoly + j]; + mesh.areas[mesh.npolys] = pareas[i]; + mesh.npolys++; + if (mesh.npolys > maxTris) { + throw new Exception("Buffer too small"); + } + } + + } + + public TileCachePolyMesh buildTileCachePolyMesh(TileCacheContourSet lcset, int maxVertsPerPoly) { + + int maxVertices = 0; + int maxTris = 0; + int maxVertsPerCont = 0; + for (int i = 0; i < lcset.nconts; ++i) { + // Skip null contours. + if (lcset.conts[i].nverts < 3) + continue; + maxVertices += lcset.conts[i].nverts; + maxTris += lcset.conts[i].nverts - 2; + maxVertsPerCont = Math.Max(maxVertsPerCont, lcset.conts[i].nverts); + } + + // TODO: warn about too many vertices? + + TileCachePolyMesh mesh = new TileCachePolyMesh(maxVertsPerPoly); + + int[] vflags = new int[maxVertices]; + + mesh.verts = new int[maxVertices * 3]; + mesh.polys = new int[maxTris * maxVertsPerPoly * 2]; + mesh.areas = new int[maxTris]; + // Just allocate and clean the mesh flags array. The user is resposible + // for filling it. + mesh.flags = new int[maxTris]; + + mesh.nverts = 0; + mesh.npolys = 0; + + Array.Fill(mesh.polys, DT_TILECACHE_NULL_IDX); + + int[] firstVert = new int[VERTEX_BUCKET_COUNT2]; + for (int i = 0; i < VERTEX_BUCKET_COUNT2; ++i) + firstVert[i] = DT_TILECACHE_NULL_IDX; + + int[] nextVert = new int[maxVertices]; + int[] indices = new int[maxVertsPerCont]; + int[] tris = new int[maxVertsPerCont * 3]; + int[] polys = new int[maxVertsPerCont * maxVertsPerPoly]; + + for (int i = 0; i < lcset.nconts; ++i) { + TileCacheContour cont = lcset.conts[i]; + + // Skip null contours. + if (cont.nverts < 3) + continue; + + // Triangulate contour + for (int j = 0; j < cont.nverts; ++j) + indices[j] = j; + + int ntris = triangulate(cont.nverts, cont.verts, indices, tris); + if (ntris <= 0) { + // TODO: issue warning! + ntris = -ntris; + } + + // Add and merge vertices. + for (int j = 0; j < cont.nverts; ++j) { + int v = j * 4; + indices[j] = addVertex(cont.verts[v], cont.verts[v + 1], cont.verts[v + 2], mesh.verts, firstVert, + nextVert, mesh.nverts); + mesh.nverts = Math.Max(mesh.nverts, indices[j] + 1); + if ((cont.verts[v + 3] & 0x80) != 0) { + // This vertex should be removed. + vflags[indices[j]] = 1; + } + } + + // Build initial polygons. + int npolys = 0; + Array.Fill(polys, DT_TILECACHE_NULL_IDX); + for (int j = 0; j < ntris; ++j) { + int t = j * 3; + if (tris[t] != tris[t + 1] && tris[t] != tris[t + 2] && tris[t + 1] != tris[t + 2]) { + polys[npolys * maxVertsPerPoly + 0] = indices[tris[t]]; + polys[npolys * maxVertsPerPoly + 1] = indices[tris[t + 1]]; + polys[npolys * maxVertsPerPoly + 2] = indices[tris[t + 2]]; + npolys++; + } + } + if (npolys == 0) + continue; + + // Merge polygons. + if (maxVertsPerPoly > 3) { + for (;;) { + // Find best polygons to merge. + int bestMergeVal = 0; + int bestPa = 0, bestPb = 0, bestEa = 0, bestEb = 0; + + for (int j = 0; j < npolys - 1; ++j) { + int pj = j * maxVertsPerPoly; + for (int k = j + 1; k < npolys; ++k) { + int pk = k * maxVertsPerPoly; + int[] pm = getPolyMergeValue(polys, pj, pk, mesh.verts, maxVertsPerPoly); + int v = pm[0]; + int ea = pm[1]; + int eb = pm[2]; + if (v > bestMergeVal) { + bestMergeVal = v; + bestPa = j; + bestPb = k; + bestEa = ea; + bestEb = eb; + } + } + } + + if (bestMergeVal > 0) { + // Found best, merge. + int pa = bestPa * maxVertsPerPoly; + int pb = bestPb * maxVertsPerPoly; + mergePolys(polys, pa, pb, bestEa, bestEb, maxVertsPerPoly); + Array.Copy(polys, (npolys - 1) * maxVertsPerPoly, polys, pb, maxVertsPerPoly); + npolys--; + } else { + // Could not merge any polygons, stop. + break; + } + } + } + + // Store polygons. + for (int j = 0; j < npolys; ++j) { + int p = mesh.npolys * maxVertsPerPoly * 2; + int q = j * maxVertsPerPoly; + for (int k = 0; k < maxVertsPerPoly; ++k) + mesh.polys[p + k] = polys[q + k]; + mesh.areas[mesh.npolys] = cont.area; + mesh.npolys++; + if (mesh.npolys > maxTris) + throw new Exception("Buffer too small"); + } + } + + // Remove edge vertices. + for (int i = 0; i < mesh.nverts; ++i) { + if (vflags[i] != 0) { + if (!canRemoveVertex(mesh, i)) + continue; + removeVertex(mesh, i, maxTris); + // Remove vertex + // Note: mesh.nverts is already decremented inside + // removeVertex()! + for (int j = i; j < mesh.nverts; ++j) + vflags[j] = vflags[j + 1]; + --i; + } + } + + // Calculate adjacency. + buildMeshAdjacency(mesh.polys, mesh.npolys, mesh.verts, mesh.nverts, lcset, maxVertsPerPoly); + + return mesh; + } + + public void markCylinderArea(TileCacheLayer layer, float[] orig, float cs, float ch, float[] pos, float radius, + float height, int areaId) { + float[] bmin = new float[3]; + float[] bmax = new float[3]; + bmin[0] = pos[0] - radius; + bmin[1] = pos[1]; + bmin[2] = pos[2] - radius; + bmax[0] = pos[0] + radius; + bmax[1] = pos[1] + height; + bmax[2] = pos[2] + radius; + float r2 = sqr(radius / cs + 0.5f); + + int w = layer.header.width; + int h = layer.header.height; + float ics = 1.0f / cs; + float ich = 1.0f / ch; + + float px = (pos[0] - orig[0]) * ics; + float pz = (pos[2] - orig[2]) * ics; + + int minx = (int) Math.Floor((bmin[0] - orig[0]) * ics); + int miny = (int) Math.Floor((bmin[1] - orig[1]) * ich); + int minz = (int) Math.Floor((bmin[2] - orig[2]) * ics); + int maxx = (int) Math.Floor((bmax[0] - orig[0]) * ics); + int maxy = (int) Math.Floor((bmax[1] - orig[1]) * ich); + int maxz = (int) Math.Floor((bmax[2] - orig[2]) * ics); + + if (maxx < 0) + return; + if (minx >= w) + return; + if (maxz < 0) + return; + if (minz >= h) + return; + + if (minx < 0) + minx = 0; + if (maxx >= w) + maxx = w - 1; + if (minz < 0) + minz = 0; + if (maxz >= h) + maxz = h - 1; + + for (int z = minz; z <= maxz; ++z) { + for (int x = minx; x <= maxx; ++x) { + float dx = x + 0.5f - px; + float dz = z + 0.5f - pz; + if (dx * dx + dz * dz > r2) + continue; + int y = layer.heights[x + z * w]; + if (y < miny || y > maxy) + continue; + layer.areas[x + z * w] = (short) areaId; + } + } + } + + public void markBoxArea(TileCacheLayer layer, float[] orig, float cs, float ch, float[] bmin, float[] bmax, + int areaId) { + int w = layer.header.width; + int h = layer.header.height; + float ics = 1.0f / cs; + float ich = 1.0f / ch; + + int minx = (int) Math.Floor((bmin[0] - orig[0]) * ics); + int miny = (int) Math.Floor((bmin[1] - orig[1]) * ich); + int minz = (int) Math.Floor((bmin[2] - orig[2]) * ics); + int maxx = (int) Math.Floor((bmax[0] - orig[0]) * ics); + int maxy = (int) Math.Floor((bmax[1] - orig[1]) * ich); + int maxz = (int) Math.Floor((bmax[2] - orig[2]) * ics); + + if (maxx < 0) + return; + if (minx >= w) + return; + if (maxz < 0) + return; + if (minz >= h) + return; + + if (minx < 0) + minx = 0; + if (maxx >= w) + maxx = w - 1; + if (minz < 0) + minz = 0; + if (maxz >= h) + maxz = h - 1; + + for (int z = minz; z <= maxz; ++z) { + for (int x = minx; x <= maxx; ++x) { + int y = layer.heights[x + z * w]; + if (y < miny || y > maxy) + continue; + layer.areas[x + z * w] = (short) areaId; + } + } + + } + + public byte[] compressTileCacheLayer(TileCacheLayer layer, ByteOrder order, bool cCompatibility) { + using var ms = new MemoryStream(); + using var baos = new BinaryWriter(ms); + TileCacheLayerHeaderWriter hw = new TileCacheLayerHeaderWriter(); + try { + hw.write(baos, layer.header, order, cCompatibility); + int gridSize = layer.header.width * layer.header.height; + byte[] buffer = new byte[gridSize * 3]; + for (int i = 0; i < gridSize; i++) { + buffer[i] = (byte) layer.heights[i]; + buffer[gridSize + i] = (byte) layer.areas[i]; + buffer[gridSize * 2 + i] = (byte) layer.cons[i]; + } + baos.Write(TileCacheCompressorFactory.get(cCompatibility).compress(buffer)); + return ms.ToArray(); + } catch (IOException e) { + throw new Exception(e.Message, e); + } + } + + public byte[] compressTileCacheLayer(TileCacheLayerHeader header, int[] heights, int[] areas, int[] cons, + ByteOrder order, bool cCompatibility) { + using var ms = new MemoryStream(); + using var baos = new BinaryWriter(ms); + TileCacheLayerHeaderWriter hw = new TileCacheLayerHeaderWriter(); + try { + hw.write(baos, header, order, cCompatibility); + int gridSize = header.width * header.height; + byte[] buffer = new byte[gridSize * 3]; + for (int i = 0; i < gridSize; i++) { + buffer[i] = (byte) heights[i]; + buffer[gridSize + i] = (byte) areas[i]; + buffer[gridSize * 2 + i] = (byte) cons[i]; + } + baos.Write(TileCacheCompressorFactory.get(cCompatibility).compress(buffer)); + return ms.ToArray(); + } catch (IOException e) { + throw new Exception(e.Message, e); + } + } + + public TileCacheLayer decompressTileCacheLayer(TileCacheCompressor comp, byte[] compressed, ByteOrder order, + bool cCompatibility) { + ByteBuffer buf = new ByteBuffer(compressed); + buf.order(order); + TileCacheLayer layer = new TileCacheLayer(); + try { + layer.header = reader.read(buf, cCompatibility); + } catch (IOException e) { + throw new Exception(e.Message, e); + } + + int gridSize = layer.header.width * layer.header.height; + byte[] grids = comp.decompress(compressed, buf.position(), compressed.Length - buf.position(), gridSize * 3); + layer.heights = new short[gridSize]; + layer.areas = new short[gridSize]; + layer.cons = new short[gridSize]; + layer.regs = new short[gridSize]; + for (int i = 0; i < gridSize; i++) { + layer.heights[i] = (short) (grids[i] & 0xFF); + layer.areas[i] = (short) (grids[i + gridSize] & 0xFF); + layer.cons[i] = (short) (grids[i + gridSize * 2] & 0xFF); + } + return layer; + + } + + public void markBoxArea(TileCacheLayer layer, float[] orig, float cs, float ch, float[] center, float[] extents, + float[] rotAux, int areaId) { + int w = layer.header.width; + int h = layer.header.height; + float ics = 1.0f / cs; + float ich = 1.0f / ch; + + float cx = (center[0] - orig[0]) * ics; + float cz = (center[2] - orig[2]) * ics; + + float maxr = 1.41f * Math.Max(extents[0], extents[2]); + int minx = (int) Math.Floor(cx - maxr * ics); + int maxx = (int) Math.Floor(cx + maxr * ics); + int minz = (int) Math.Floor(cz - maxr * ics); + int maxz = (int) Math.Floor(cz + maxr * ics); + int miny = (int) Math.Floor((center[1] - extents[1] - orig[1]) * ich); + int maxy = (int) Math.Floor((center[1] + extents[1] - orig[1]) * ich); + + if (maxx < 0) + return; + if (minx >= w) + return; + if (maxz < 0) + return; + if (minz >= h) + return; + + if (minx < 0) + minx = 0; + if (maxx >= w) + maxx = w - 1; + if (minz < 0) + minz = 0; + if (maxz >= h) + maxz = h - 1; + + float xhalf = extents[0] * ics + 0.5f; + float zhalf = extents[2] * ics + 0.5f; + for (int z = minz; z <= maxz; ++z) { + for (int x = minx; x <= maxx; ++x) { + float x2 = 2.0f * (x - cx); + float z2 = 2.0f * (z - cz); + float xrot = rotAux[1] * x2 + rotAux[0] * z2; + if (xrot > xhalf || xrot < -xhalf) + continue; + float zrot = rotAux[1] * z2 - rotAux[0] * x2; + if (zrot > zhalf || zrot < -zhalf) + continue; + int y = layer.heights[x + z * w]; + if (y < miny || y > maxy) + continue; + layer.areas[x + z * w] = (short) areaId; + } + } + + } + +} diff --git a/src/DotRecast.Detour.TileCache/TileCacheCompressor.cs b/src/DotRecast.Detour.TileCache/TileCacheCompressor.cs new file mode 100644 index 0000000..ef4d880 --- /dev/null +++ b/src/DotRecast.Detour.TileCache/TileCacheCompressor.cs @@ -0,0 +1,26 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Detour.TileCache; + +public interface TileCacheCompressor { + + byte[] decompress(byte[] buf, int offset, int len, int outputlen); + + byte[] compress(byte[] buf); +} diff --git a/src/DotRecast.Detour.TileCache/TileCacheContour.cs b/src/DotRecast.Detour.TileCache/TileCacheContour.cs new file mode 100644 index 0000000..9b08dfc --- /dev/null +++ b/src/DotRecast.Detour.TileCache/TileCacheContour.cs @@ -0,0 +1,26 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Detour.TileCache; + +public class TileCacheContour { + public int nverts; + public int[] verts; + public int reg; + public int area; +} diff --git a/src/DotRecast.Detour.TileCache/TileCacheContourSet.cs b/src/DotRecast.Detour.TileCache/TileCacheContourSet.cs new file mode 100644 index 0000000..258e745 --- /dev/null +++ b/src/DotRecast.Detour.TileCache/TileCacheContourSet.cs @@ -0,0 +1,24 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Detour.TileCache; + +public class TileCacheContourSet { + public int nconts; + public TileCacheContour[] conts; +} diff --git a/src/DotRecast.Detour.TileCache/TileCacheLayer.cs b/src/DotRecast.Detour.TileCache/TileCacheLayer.cs new file mode 100644 index 0000000..0378f98 --- /dev/null +++ b/src/DotRecast.Detour.TileCache/TileCacheLayer.cs @@ -0,0 +1,28 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Detour.TileCache; + +public class TileCacheLayer { + public TileCacheLayerHeader header; + public int regCount; /// < Region count. + public short[] heights; // char + public short[] areas; // char + public short[] cons; // char + public short[] regs; // char +} diff --git a/src/DotRecast.Detour.TileCache/TileCacheLayerHeader.cs b/src/DotRecast.Detour.TileCache/TileCacheLayerHeader.cs new file mode 100644 index 0000000..c8b8b53 --- /dev/null +++ b/src/DotRecast.Detour.TileCache/TileCacheLayerHeader.cs @@ -0,0 +1,35 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Detour.TileCache; + +public class TileCacheLayerHeader { + + public const int DT_TILECACHE_MAGIC = 'D' << 24 | 'T' << 16 | 'L' << 8 | 'R'; /// < 'DTLR'; + public const int DT_TILECACHE_VERSION = 1; + + public int magic; /// < Data magic + public int version; /// < Data version + public int tx, ty, tlayer; + public float[] bmin = new float[3]; + public float[] bmax = new float[3]; + public int hmin, hmax; /// < Height min/max range + public int width, height; /// < Dimension of the layer. + public int minx, maxx, miny, maxy; /// < Usable sub-region. + +} diff --git a/src/DotRecast.Detour.TileCache/TileCacheMeshProcess.cs b/src/DotRecast.Detour.TileCache/TileCacheMeshProcess.cs new file mode 100644 index 0000000..d66e92a --- /dev/null +++ b/src/DotRecast.Detour.TileCache/TileCacheMeshProcess.cs @@ -0,0 +1,24 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Detour.TileCache; + +public interface TileCacheMeshProcess { + + void process(NavMeshDataCreateParams option); +} diff --git a/src/DotRecast.Detour.TileCache/TileCacheObstacle.cs b/src/DotRecast.Detour.TileCache/TileCacheObstacle.cs new file mode 100644 index 0000000..9de9eba --- /dev/null +++ b/src/DotRecast.Detour.TileCache/TileCacheObstacle.cs @@ -0,0 +1,50 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.Collections.Generic; + +namespace DotRecast.Detour.TileCache; + +public class TileCacheObstacle { + + public enum TileCacheObstacleType { + CYLINDER, BOX, ORIENTED_BOX + }; + + public readonly int index; + public TileCacheObstacleType type; + public readonly float[] pos = new float[3]; + public readonly float[] bmin = new float[3]; + public readonly float[] bmax = new float[3]; + public float radius, height; + public readonly float[] center = new float[3]; + public readonly float[] extents = new float[3]; + public readonly float[] rotAux = new float[2]; // { cos(0.5f*angle)*sin(-0.5f*angle); cos(0.5f*angle)*cos(0.5f*angle) - 0.5 } + public List touched = new(); + public readonly List pending = new(); + public int salt; + public ObstacleState state = ObstacleState.DT_OBSTACLE_EMPTY; + public TileCacheObstacle next; + + public TileCacheObstacle(int index) { + salt = 1; + this.index = index; + } + +} diff --git a/src/DotRecast.Detour.TileCache/TileCacheParams.cs b/src/DotRecast.Detour.TileCache/TileCacheParams.cs new file mode 100644 index 0000000..0681480 --- /dev/null +++ b/src/DotRecast.Detour.TileCache/TileCacheParams.cs @@ -0,0 +1,32 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Detour.TileCache; + +public class TileCacheParams { + public readonly float[] orig = new float[3]; + public float cs, ch; + public int width, height; + public float walkableHeight; + public float walkableRadius; + public float walkableClimb; + public float maxSimplificationError; + public int maxTiles; + public int maxObstacles; + +} diff --git a/src/DotRecast.Detour.TileCache/TileCachePolyMesh.cs b/src/DotRecast.Detour.TileCache/TileCachePolyMesh.cs new file mode 100644 index 0000000..3034bf5 --- /dev/null +++ b/src/DotRecast.Detour.TileCache/TileCachePolyMesh.cs @@ -0,0 +1,33 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Detour.TileCache; + +public class TileCachePolyMesh { + public int nvp; + public int nverts; /// < Number of vertices. + public int npolys; /// < Number of polygons. + public int[] verts; /// < Vertices of the mesh, 3 elements per vertex. + public int[] polys; /// < Polygons of the mesh, nvp*2 elements per polygon. + public int[] flags; /// < Per polygon flags. + public int[] areas; /// < Area ID of polygons. + + public TileCachePolyMesh(int nvp) { + this.nvp = nvp; + } +} diff --git a/src/DotRecast.Detour.TileCache/TileCacheStorageParams.cs b/src/DotRecast.Detour.TileCache/TileCacheStorageParams.cs new file mode 100644 index 0000000..77a7636 --- /dev/null +++ b/src/DotRecast.Detour.TileCache/TileCacheStorageParams.cs @@ -0,0 +1,34 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using DotRecast.Core; + +namespace DotRecast.Detour.TileCache; + +public class TileCacheStorageParams { + + public readonly ByteOrder byteOrder; + public readonly bool cCompatibility; + + public TileCacheStorageParams(ByteOrder byteOrder, bool cCompatibility) { + this.byteOrder = byteOrder; + this.cCompatibility = cCompatibility; + } + +} diff --git a/src/DotRecast.Detour/BVNode.cs b/src/DotRecast.Detour/BVNode.cs new file mode 100644 index 0000000..2a807f5 --- /dev/null +++ b/src/DotRecast.Detour/BVNode.cs @@ -0,0 +1,34 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Detour; + +/** + * Bounding volume node. + * + * @note This structure is rarely if ever used by the end user. + * @see MeshTile + */ +public class BVNode { + /** Minimum bounds of the node's AABB. [(x, y, z)] */ + public int[] bmin = new int[3]; + /** Maximum bounds of the node's AABB. [(x, y, z)] */ + public int[] bmax = new int[3]; + /** The node's index. (Negative for escape sequence.) */ + public int i; +} diff --git a/src/DotRecast.Detour/ClosestPointOnPolyResult.cs b/src/DotRecast.Detour/ClosestPointOnPolyResult.cs new file mode 100644 index 0000000..61fc153 --- /dev/null +++ b/src/DotRecast.Detour/ClosestPointOnPolyResult.cs @@ -0,0 +1,41 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Detour; + +public class ClosestPointOnPolyResult { + + private readonly bool posOverPoly; + private readonly float[] closest; + + public ClosestPointOnPolyResult(bool posOverPoly, float[] closest) { + this.posOverPoly = posOverPoly; + this.closest = closest; + } + + /** Returns true if the position is over the polygon. */ + public bool isPosOverPoly() { + return posOverPoly; + } + + /** Returns the closest point on the polygon. [(x, y, z)] */ + public float[] getClosest() { + return closest; + } + +} diff --git a/src/DotRecast.Detour/ConvexConvexIntersection.cs b/src/DotRecast.Detour/ConvexConvexIntersection.cs new file mode 100644 index 0000000..fc05351 --- /dev/null +++ b/src/DotRecast.Detour/ConvexConvexIntersection.cs @@ -0,0 +1,237 @@ +/* +recast4j copyright (c) 2021 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; + +namespace DotRecast.Detour; + +using static DetourCommon; + +/** + * Convex-convex intersection based on "Computational Geometry in C" by Joseph O'Rourke + */ +public static class ConvexConvexIntersection { + + private static readonly float EPSILON = 0.0001f; + + private enum InFlag { + Pin, Qin, Unknown, + } + + private enum Intersection { + None, Single, Overlap, + } + + public static float[] intersect(float[] p, float[] q) { + int n = p.Length / 3; + int m = q.Length / 3; + float[] inters = new float[Math.Max(m, n) * 3 * 3]; + int ii = 0; + /* Initialize variables. */ + float[] a = new float[3]; + float[] b = new float[3]; + float[] a1 = new float[3]; + float[] b1 = new float[3]; + + int aa = 0; + int ba = 0; + int ai = 0; + int bi = 0; + + InFlag f = InFlag.Unknown; + bool FirstPoint = true; + float[] ip = new float[3]; + float[] iq = new float[3]; + + do { + vCopy(a, p, 3 * (ai % n)); + vCopy(b, q, 3 * (bi % m)); + vCopy(a1, p, 3 * ((ai + n - 1) % n)); // prev a + vCopy(b1, q, 3 * ((bi + m - 1) % m)); // prev b + + float[] A = vSub(a, a1); + float[] B = vSub(b, b1); + + float cross = B[0] * A[2] - A[0] * B[2];// triArea2D({0, 0}, A, B); + float aHB = triArea2D(b1, b, a); + float bHA = triArea2D(a1, a, b); + if (Math.Abs(cross) < EPSILON) { + cross = 0f; + } + bool parallel = cross == 0f; + Intersection code = parallel ? parallelInt(a1, a, b1, b, ip, iq) : segSegInt(a1, a, b1, b, ip, iq); + + if (code == Intersection.Single) { + if (FirstPoint) { + FirstPoint = false; + aa = ba = 0; + } + ii = addVertex(inters, ii, ip); + f = inOut(f, aHB, bHA); + } + + /*-----Advance rules-----*/ + + /* Special case: A & B overlap and oppositely oriented. */ + if (code == Intersection.Overlap && vDot2D(A, B) < 0) { + ii = addVertex(inters, ii, ip); + ii = addVertex(inters, ii, iq); + break; + } + + /* Special case: A & B parallel and separated. */ + if (parallel && aHB < 0f && bHA < 0f) { + return null; + } + + /* Special case: A & B collinear. */ + else if (parallel && Math.Abs(aHB) < EPSILON && Math.Abs(bHA) < EPSILON) { + /* Advance but do not output point. */ + if (f == InFlag.Pin) { + ba++; + bi++; + } else { + aa++; + ai++; + } + } + + /* Generic cases. */ + else if (cross >= 0) { + if (bHA > 0) { + if (f == InFlag.Pin) { + ii = addVertex(inters, ii, a); + } + aa++; + ai++; + } else { + if (f == InFlag.Qin) { + ii = addVertex(inters, ii, b); + } + ba++; + bi++; + } + } else { + if (aHB > 0) { + if (f == InFlag.Qin) { + ii = addVertex(inters, ii, b); + } + ba++; + bi++; + } else { + if (f == InFlag.Pin) { + ii = addVertex(inters, ii, a); + } + aa++; + ai++; + } + } + /* Quit when both adv. indices have cycled, or one has cycled twice. */ + } while ((aa < n || ba < m) && aa < 2 * n && ba < 2 * m); + + /* Deal with special cases: not implemented. */ + if (f == InFlag.Unknown) { + return null; + } + + float[] copied = new float[ii]; + Array.Copy(inters, copied, ii); + return copied; + } + + private static int addVertex(float[] inters, int ii, float[] p) { + if (ii > 0) { + if (inters[ii - 3] == p[0] && inters[ii - 2] == p[1] && inters[ii - 1] == p[2]) { + return ii; + } + if (inters[0] == p[0] && inters[1] == p[1] && inters[2] == p[2]) { + return ii; + } + } + inters[ii] = p[0]; + inters[ii + 1] = p[1]; + inters[ii + 2] = p[2]; + return ii + 3; + } + + private static InFlag inOut(InFlag inflag, float aHB, float bHA) { + if (aHB > 0) { + return InFlag.Pin; + } else if (bHA > 0) { + return InFlag.Qin; + } + return inflag; + } + + private static Intersection segSegInt(float[] a, float[] b, float[] c, float[] d, float[] p, float[] q) { + var isec = intersectSegSeg2D(a, b, c, d); + if (null != isec) { + float s = isec.Item1; + float t = isec.Item2; + if (s >= 0.0f && s <= 1.0f && t >= 0.0f && t <= 1.0f) { + p[0] = a[0] + (b[0] - a[0]) * s; + p[1] = a[1] + (b[1] - a[1]) * s; + p[2] = a[2] + (b[2] - a[2]) * s; + return Intersection.Single; + } + } + return Intersection.None; + } + + private static Intersection parallelInt(float[] a, float[] b, float[] c, float[] d, float[] p, float[] q) { + if (between(a, b, c) && between(a, b, d)) { + vCopy(p, c); + vCopy(q, d); + return Intersection.Overlap; + } + if (between(c, d, a) && between(c, d, b)) { + vCopy(p, a); + vCopy(q, b); + return Intersection.Overlap; + } + if (between(a, b, c) && between(c, d, b)) { + vCopy(p, c); + vCopy(q, b); + return Intersection.Overlap; + } + if (between(a, b, c) && between(c, d, a)) { + vCopy(p, c); + vCopy(q, a); + return Intersection.Overlap; + } + if (between(a, b, d) && between(c, d, b)) { + vCopy(p, d); + vCopy(q, b); + return Intersection.Overlap; + } + if (between(a, b, d) && between(c, d, a)) { + vCopy(p, d); + vCopy(q, a); + return Intersection.Overlap; + } + return Intersection.None; + } + + private static bool between(float[] a, float[] b, float[] c) { + if (Math.Abs(a[0] - b[0]) > Math.Abs(a[2] - b[2])) { + return ((a[0] <= c[0]) && (c[0] <= b[0])) || ((a[0] >= c[0]) && (c[0] >= b[0])); + } else { + return ((a[2] <= c[2]) && (c[2] <= b[2])) || ((a[2] >= c[2]) && (c[2] >= b[2])); + } + } +} diff --git a/src/DotRecast.Detour/DefaultQueryFilter.cs b/src/DotRecast.Detour/DefaultQueryFilter.cs new file mode 100644 index 0000000..140cff0 --- /dev/null +++ b/src/DotRecast.Detour/DefaultQueryFilter.cs @@ -0,0 +1,101 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; + +namespace DotRecast.Detour; + +using static DetourCommon; + +/** + * The Default Implementation + * + * At construction: All area costs default to 1.0. All flags are included and none are excluded. + * + * If a polygon has both an include and an exclude flag, it will be excluded. + * + * The way filtering works, a navigation mesh polygon must have at least one flag set to ever be considered by a query. + * So a polygon with no flags will never be considered. + * + * Setting the include flags to 0 will result in all polygons being excluded. + * + * Custom Implementations + * + * Implement a custom query filter by overriding the virtual passFilter() and getCost() functions. If this is done, both + * functions should be as fast as possible. Use cached local copies of data rather than accessing your own objects where + * possible. + * + * Custom implementations do not need to adhere to the flags or cost logic used by the default implementation. + * + * In order for A* searches to work properly, the cost should be proportional to the travel distance. Implementing a + * cost modifier less than 1.0 is likely to lead to problems during pathfinding. + * + * @see NavMeshQuery + */ +public class DefaultQueryFilter : QueryFilter { + + private int m_excludeFlags; + private int m_includeFlags; + private readonly float[] m_areaCost = new float[NavMesh.DT_MAX_AREAS]; + + public DefaultQueryFilter() { + m_includeFlags = 0xffff; + m_excludeFlags = 0; + for (int i = 0; i < NavMesh.DT_MAX_AREAS; ++i) { + m_areaCost[i] = 1.0f; + } + } + + public DefaultQueryFilter(int includeFlags, int excludeFlags, float[] areaCost) { + m_includeFlags = includeFlags; + m_excludeFlags = excludeFlags; + for (int i = 0; i < Math.Min(NavMesh.DT_MAX_AREAS, areaCost.Length); ++i) { + m_areaCost[i] = areaCost[i]; + } + for (int i = areaCost.Length; i < NavMesh.DT_MAX_AREAS; ++i) { + m_areaCost[i] = 1.0f; + } + } + + public bool passFilter(long refs, MeshTile tile, Poly poly) { + return (poly.flags & m_includeFlags) != 0 && (poly.flags & m_excludeFlags) == 0; + } + + public float getCost(float[] pa, float[] pb, long prevRef, MeshTile prevTile, Poly prevPoly, long curRef, + MeshTile curTile, Poly curPoly, long nextRef, MeshTile nextTile, Poly nextPoly) { + return vDist(pa, pb) * m_areaCost[curPoly.getArea()]; + } + + public int getIncludeFlags() { + return m_includeFlags; + } + + public void setIncludeFlags(int flags) { + m_includeFlags = flags; + } + + public int getExcludeFlags() { + return m_excludeFlags; + } + + public void setExcludeFlags(int flags) { + m_excludeFlags = flags; + } + +} diff --git a/src/DotRecast.Detour/DefaultQueryHeuristic.cs b/src/DotRecast.Detour/DefaultQueryHeuristic.cs new file mode 100644 index 0000000..ef66e25 --- /dev/null +++ b/src/DotRecast.Detour/DefaultQueryHeuristic.cs @@ -0,0 +1,39 @@ +/* +recast4j copyright (c) 2021 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +namespace DotRecast.Detour; + +using static DetourCommon; + +public class DefaultQueryHeuristic : QueryHeuristic { + + private readonly float scale; + + public DefaultQueryHeuristic() : this(0.999f) + { + } + + public DefaultQueryHeuristic(float scale) { + this.scale = scale; + } + + public float getCost(float[] neighbourPos, float[] endPos) { + return vDist(neighbourPos, endPos) * scale; + } + +} diff --git a/src/DotRecast.Detour/DetourBuilder.cs b/src/DotRecast.Detour/DetourBuilder.cs new file mode 100644 index 0000000..41b5e55 --- /dev/null +++ b/src/DotRecast.Detour/DetourBuilder.cs @@ -0,0 +1,31 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Detour; + +public class DetourBuilder { + + public MeshData build(NavMeshDataCreateParams option, int tileX, int tileY) { + MeshData data = NavMeshBuilder.createNavMeshData(option); + if (data != null) { + data.header.x = tileX; + data.header.y = tileY; + } + return data; + } +} diff --git a/src/DotRecast.Detour/DetourCommon.cs b/src/DotRecast.Detour/DetourCommon.cs new file mode 100644 index 0000000..b7ab979 --- /dev/null +++ b/src/DotRecast.Detour/DetourCommon.cs @@ -0,0 +1,636 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; + +namespace DotRecast.Detour; + +public static class DetourCommon { + + public const float EPS = 1e-4f; + + /// Performs a scaled vector addition. (@p v1 + (@p v2 * @p s)) + /// @param[out] dest The result vector. [(x, y, z)] + /// @param[in] v1 The base vector. [(x, y, z)] + /// @param[in] v2 The vector to scale and add to @p v1. [(x, y, z)] + /// @param[in] s The amount to scale @p v2 by before adding to @p v1. + public static float[] vMad(float[] v1, float[] v2, float s) { + float[] dest = new float[3]; + dest[0] = v1[0] + v2[0] * s; + dest[1] = v1[1] + v2[1] * s; + dest[2] = v1[2] + v2[2] * s; + return dest; + } + + /// Performs a linear interpolation between two vectors. (@p v1 toward @p + /// v2) + /// @param[out] dest The result vector. [(x, y, x)] + /// @param[in] v1 The starting vector. + /// @param[in] v2 The destination vector. + /// @param[in] t The interpolation factor. [Limits: 0 <= value <= 1.0] + public static float[] vLerp(float[] verts, int v1, int v2, float t) { + float[] dest = new float[3]; + dest[0] = verts[v1 + 0] + (verts[v2 + 0] - verts[v1 + 0]) * t; + dest[1] = verts[v1 + 1] + (verts[v2 + 1] - verts[v1 + 1]) * t; + dest[2] = verts[v1 + 2] + (verts[v2 + 2] - verts[v1 + 2]) * t; + return dest; + } + + public static float[] vLerp(float[] v1, float[] v2, float t) { + float[] dest = new float[3]; + dest[0] = v1[0] + (v2[0] - v1[0]) * t; + dest[1] = v1[1] + (v2[1] - v1[1]) * t; + dest[2] = v1[2] + (v2[2] - v1[2]) * t; + return dest; + } + + public static float[] vSub(VectorPtr v1, VectorPtr v2) { + float[] dest = new float[3]; + dest[0] = v1.get(0) - v2.get(0); + dest[1] = v1.get(1) - v2.get(1); + dest[2] = v1.get(2) - v2.get(2); + return dest; + } + + public static float[] vSub(float[] v1, float[] v2) { + float[] dest = new float[3]; + dest[0] = v1[0] - v2[0]; + dest[1] = v1[1] - v2[1]; + dest[2] = v1[2] - v2[2]; + return dest; + } + + public static float[] vAdd(float[] v1, float[] v2) { + float[] dest = new float[3]; + dest[0] = v1[0] + v2[0]; + dest[1] = v1[1] + v2[1]; + dest[2] = v1[2] + v2[2]; + return dest; + } + + public static float[] vCopy(float[] @in) { + float[] @out = new float[3]; + @out[0] = @in[0]; + @out[1] = @in[1]; + @out[2] = @in[2]; + return @out; + } + + public static void vSet(float[] @out, float a, float b, float c) { + @out[0] = a; + @out[1] = b; + @out[2] = c; + } + + public static void vCopy(float[] @out, float[] @in) { + @out[0] = @in[0]; + @out[1] = @in[1]; + @out[2] = @in[2]; + } + + public static void vCopy(float[] @out, float[] @in, int i) { + @out[0] = @in[i]; + @out[1] = @in[i + 1]; + @out[2] = @in[i + 2]; + } + + public static void vMin(float[] @out, float[] @in, int i) { + @out[0] = Math.Min(@out[0], @in[i]); + @out[1] = Math.Min(@out[1], @in[i + 1]); + @out[2] = Math.Min(@out[2], @in[i + 2]); + } + + public static void vMax(float[] @out, float[] @in, int i) { + @out[0] = Math.Max(@out[0], @in[i]); + @out[1] = Math.Max(@out[1], @in[i + 1]); + @out[2] = Math.Max(@out[2], @in[i + 2]); + } + + /// Returns the distance between two points. + /// @param[in] v1 A point. [(x, y, z)] + /// @param[in] v2 A point. [(x, y, z)] + /// @return The distance between the two points. + public static float vDist(float[] v1, float[] v2) { + float dx = v2[0] - v1[0]; + float dy = v2[1] - v1[1]; + float dz = v2[2] - v1[2]; + return (float) Math.Sqrt(dx * dx + dy * dy + dz * dz); + } + + /// Returns the distance between two points. + /// @param[in] v1 A point. [(x, y, z)] + /// @param[in] v2 A point. [(x, y, z)] + /// @return The distance between the two points. + public static float vDistSqr(float[] v1, float[] v2) { + float dx = v2[0] - v1[0]; + float dy = v2[1] - v1[1]; + float dz = v2[2] - v1[2]; + return dx * dx + dy * dy + dz * dz; + } + + public static float sqr(float a) { + return a * a; + } + + /// Derives the square of the scalar length of the vector. (len * len) + /// @param[in] v The vector. [(x, y, z)] + /// @return The square of the scalar length of the vector. + public static float vLenSqr(float[] v) { + return v[0] * v[0] + v[1] * v[1] + v[2] * v[2]; + } + + public static float vLen(float[] v) { + return (float) Math.Sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); + } + + public static float vDist(float[] v1, float[] verts, int i) { + float dx = verts[i] - v1[0]; + float dy = verts[i + 1] - v1[1]; + float dz = verts[i + 2] - v1[2]; + return (float) Math.Sqrt(dx * dx + dy * dy + dz * dz); + } + + public static float clamp(float v, float min, float max) { + return Math.Max(Math.Min(v, max), min); + } + + public static int clamp(int v, int min, int max) { + return Math.Max(Math.Min(v, max), min); + } + + /// Derives the distance between the specified points on the xz-plane. + /// @param[in] v1 A point. [(x, y, z)] + /// @param[in] v2 A point. [(x, y, z)] + /// @return The distance between the point on the xz-plane. + /// + /// The vectors are projected onto the xz-plane, so the y-values are + /// ignored. + public static float vDist2D(float[] v1, float[] v2) { + float dx = v2[0] - v1[0]; + float dz = v2[2] - v1[2]; + return (float) Math.Sqrt(dx * dx + dz * dz); + } + + public static float vDist2DSqr(float[] v1, float[] v2) { + float dx = v2[0] - v1[0]; + float dz = v2[2] - v1[2]; + return dx * dx + dz * dz; + } + + public static float vDist2DSqr(float[] p, float[] verts, int i) { + float dx = verts[i] - p[0]; + float dz = verts[i + 2] - p[2]; + return dx * dx + dz * dz; + } + + /// Normalizes the vector. + /// @param[in,out] v The vector to normalize. [(x, y, z)] + public static void vNormalize(float[] v) { + float d = (float) (1.0f / Math.Sqrt(sqr(v[0]) + sqr(v[1]) + sqr(v[2]))); + if (d != 0) { + v[0] *= d; + v[1] *= d; + v[2] *= d; + } + } + + private static readonly float EQUAL_THRESHOLD = sqr(1.0f / 16384.0f); + + /// Performs a 'sloppy' colocation check of the specified points. + /// @param[in] p0 A point. [(x, y, z)] + /// @param[in] p1 A point. [(x, y, z)] + /// @return True if the points are considered to be at the same location. + /// + /// Basically, this function will return true if the specified points are + /// close enough to eachother to be considered colocated. + public static bool vEqual(float[] p0, float[] p1) { + return vEqual(p0, p1, EQUAL_THRESHOLD); + } + + public static bool vEqual(float[] p0, float[] p1, float thresholdSqr) { + float d = vDistSqr(p0, p1); + return d < thresholdSqr; + } + + /// Derives the dot product of two vectors on the xz-plane. (@p u . @p v) + /// @param[in] u A vector [(x, y, z)] + /// @param[in] v A vector [(x, y, z)] + /// @return The dot product on the xz-plane. + /// + /// The vectors are projected onto the xz-plane, so the y-values are + /// ignored. + public static float vDot2D(float[] u, float[] v) { + return u[0] * v[0] + u[2] * v[2]; + } + + public static float vDot2D(float[] u, float[] v, int vi) { + return u[0] * v[vi] + u[2] * v[vi + 2]; + } + + /// Derives the xz-plane 2D perp product of the two vectors. (uz*vx - ux*vz) + /// @param[in] u The LHV vector [(x, y, z)] + /// @param[in] v The RHV vector [(x, y, z)] + /// @return The dot product on the xz-plane. + /// + /// The vectors are projected onto the xz-plane, so the y-values are + /// ignored. + public static float vPerp2D(float[] u, float[] v) { + return u[2] * v[0] - u[0] * v[2]; + } + + /// @} + /// @name Computational geometry helper functions. + /// @{ + + /// Derives the signed xz-plane area of the triangle ABC, or the + /// relationship of line AB to point C. + /// @param[in] a Vertex A. [(x, y, z)] + /// @param[in] b Vertex B. [(x, y, z)] + /// @param[in] c Vertex C. [(x, y, z)] + /// @return The signed xz-plane area of the triangle. + public static float triArea2D(float[] verts, int a, int b, int c) { + float abx = verts[b] - verts[a]; + float abz = verts[b + 2] - verts[a + 2]; + float acx = verts[c] - verts[a]; + float acz = verts[c + 2] - verts[a + 2]; + return acx * abz - abx * acz; + } + + public static float triArea2D(float[] a, float[] b, float[] c) { + float abx = b[0] - a[0]; + float abz = b[2] - a[2]; + float acx = c[0] - a[0]; + float acz = c[2] - a[2]; + return acx * abz - abx * acz; + } + + /// Determines if two axis-aligned bounding boxes overlap. + /// @param[in] amin Minimum bounds of box A. [(x, y, z)] + /// @param[in] amax Maximum bounds of box A. [(x, y, z)] + /// @param[in] bmin Minimum bounds of box B. [(x, y, z)] + /// @param[in] bmax Maximum bounds of box B. [(x, y, z)] + /// @return True if the two AABB's overlap. + /// @see dtOverlapBounds + public static bool overlapQuantBounds(int[] amin, int[] amax, int[] bmin, int[] bmax) { + bool overlap = true; + overlap = (amin[0] > bmax[0] || amax[0] < bmin[0]) ? false : overlap; + overlap = (amin[1] > bmax[1] || amax[1] < bmin[1]) ? false : overlap; + overlap = (amin[2] > bmax[2] || amax[2] < bmin[2]) ? false : overlap; + return overlap; + } + + /// Determines if two axis-aligned bounding boxes overlap. + /// @param[in] amin Minimum bounds of box A. [(x, y, z)] + /// @param[in] amax Maximum bounds of box A. [(x, y, z)] + /// @param[in] bmin Minimum bounds of box B. [(x, y, z)] + /// @param[in] bmax Maximum bounds of box B. [(x, y, z)] + /// @return True if the two AABB's overlap. + /// @see dtOverlapQuantBounds + public static bool overlapBounds(float[] amin, float[] amax, float[] bmin, float[] bmax) { + bool overlap = true; + overlap = (amin[0] > bmax[0] || amax[0] < bmin[0]) ? false : overlap; + overlap = (amin[1] > bmax[1] || amax[1] < bmin[1]) ? false : overlap; + overlap = (amin[2] > bmax[2] || amax[2] < bmin[2]) ? false : overlap; + return overlap; + } + + public static Tuple distancePtSegSqr2D(float[] pt, float[] p, float[] q) { + float pqx = q[0] - p[0]; + float pqz = q[2] - p[2]; + float dx = pt[0] - p[0]; + float dz = pt[2] - p[2]; + float d = pqx * pqx + pqz * pqz; + float t = pqx * dx + pqz * dz; + if (d > 0) { + t /= d; + } + if (t < 0) { + t = 0; + } else if (t > 1) { + t = 1; + } + dx = p[0] + t * pqx - pt[0]; + dz = p[2] + t * pqz - pt[2]; + return Tuple.Create(dx * dx + dz * dz, t); + } + + public static float? closestHeightPointTriangle(float[] p, float[] a, float[] b, float[] c) { + float[] v0 = vSub(c, a); + float[] v1 = vSub(b, a); + float[] v2 = vSub(p, a); + + // Compute scaled barycentric coordinates + float denom = v0[0] * v1[2] - v0[2] * v1[0]; + if (Math.Abs(denom) < EPS) { + return null; + } + + float u = v1[2] * v2[0] - v1[0] * v2[2]; + float v = v0[0] * v2[2] - v0[2] * v2[0]; + + if (denom < 0) { + denom = -denom; + u = -u; + v = -v; + } + + // If point lies inside the triangle, return interpolated ycoord. + if (u >= 0.0f && v >= 0.0f && (u + v) <= denom) { + float h = a[1] + (v0[1] * u + v1[1] * v) / denom; + return h; + } + + return null; + } + + /// @par + /// + /// All points are projected onto the xz-plane, so the y-values are ignored. + public static bool pointInPolygon(float[] pt, float[] verts, int nverts) { + // TODO: Replace pnpoly with triArea2D tests? + int i, j; + bool c = false; + for (i = 0, j = nverts - 1; i < nverts; j = i++) { + int vi = i * 3; + int vj = j * 3; + if (((verts[vi + 2] > pt[2]) != (verts[vj + 2] > pt[2])) && (pt[0] < (verts[vj + 0] - verts[vi + 0]) + * (pt[2] - verts[vi + 2]) / (verts[vj + 2] - verts[vi + 2]) + verts[vi + 0])) { + c = !c; + } + } + return c; + } + + public static bool distancePtPolyEdgesSqr(float[] pt, float[] verts, int nverts, float[] ed, float[] et) { + // TODO: Replace pnpoly with triArea2D tests? + int i, j; + bool c = false; + for (i = 0, j = nverts - 1; i < nverts; j = i++) { + int vi = i * 3; + int vj = j * 3; + if (((verts[vi + 2] > pt[2]) != (verts[vj + 2] > pt[2])) && (pt[0] < (verts[vj + 0] - verts[vi + 0]) + * (pt[2] - verts[vi + 2]) / (verts[vj + 2] - verts[vi + 2]) + verts[vi + 0])) { + c = !c; + } + Tuple edet = distancePtSegSqr2D(pt, verts, vj, vi); + ed[j] = edet.Item1; + et[j] = edet.Item2; + } + return c; + } + + public static float[] projectPoly(float[] axis, float[] poly, int npoly) { + float rmin, rmax; + rmin = rmax = vDot2D(axis, poly, 0); + for (int i = 1; i < npoly; ++i) { + float d = vDot2D(axis, poly, i * 3); + rmin = Math.Min(rmin, d); + rmax = Math.Max(rmax, d); + } + return new float[] { rmin, rmax }; + } + + public static bool overlapRange(float amin, float amax, float bmin, float bmax, float eps) { + return ((amin + eps) > bmax || (amax - eps) < bmin) ? false : true; + } + + static float eps = 1e-4f; + + /// @par + /// + /// All vertices are projected onto the xz-plane, so the y-values are ignored. + public static bool overlapPolyPoly2D(float[] polya, int npolya, float[] polyb, int npolyb) { + + for (int i = 0, j = npolya - 1; i < npolya; j = i++) { + int va = j * 3; + int vb = i * 3; + + float[] n = new float[] { polya[vb + 2] - polya[va + 2], 0, -(polya[vb + 0] - polya[va + 0]) }; + + float[] aminmax = projectPoly(n, polya, npolya); + float[] bminmax = projectPoly(n, polyb, npolyb); + if (!overlapRange(aminmax[0], aminmax[1], bminmax[0], bminmax[1], eps)) { + // Found separating axis + return false; + } + } + for (int i = 0, j = npolyb - 1; i < npolyb; j = i++) { + int va = j * 3; + int vb = i * 3; + + float[] n = new float[] { polyb[vb + 2] - polyb[va + 2], 0, -(polyb[vb + 0] - polyb[va + 0]) }; + + float[] aminmax = projectPoly(n, polya, npolya); + float[] bminmax = projectPoly(n, polyb, npolyb); + if (!overlapRange(aminmax[0], aminmax[1], bminmax[0], bminmax[1], eps)) { + // Found separating axis + return false; + } + } + return true; + } + + // Returns a random point in a convex polygon. + // Adapted from Graphics Gems article. + public static float[] randomPointInConvexPoly(float[] pts, int npts, float[] areas, float s, float t) { + // Calc triangle araes + float areasum = 0.0f; + for (int i = 2; i < npts; i++) { + areas[i] = triArea2D(pts, 0, (i - 1) * 3, i * 3); + areasum += Math.Max(0.001f, areas[i]); + } + // Find sub triangle weighted by area. + float thr = s * areasum; + float acc = 0.0f; + float u = 1.0f; + int tri = npts - 1; + for (int i = 2; i < npts; i++) { + float dacc = areas[i]; + if (thr >= acc && thr < (acc + dacc)) { + u = (thr - acc) / dacc; + tri = i; + break; + } + acc += dacc; + } + + float v = (float) Math.Sqrt(t); + + float a = 1 - v; + float b = (1 - u) * v; + float c = u * v; + int pa = 0; + int pb = (tri - 1) * 3; + int pc = tri * 3; + + return new float[] { a * pts[pa] + b * pts[pb] + c * pts[pc], + a * pts[pa + 1] + b * pts[pb + 1] + c * pts[pc + 1], + a * pts[pa + 2] + b * pts[pb + 2] + c * pts[pc + 2] }; + } + + public static int nextPow2(int v) { + v--; + v |= v >> 1; + v |= v >> 2; + v |= v >> 4; + v |= v >> 8; + v |= v >> 16; + v++; + return v; + } + + public static int ilog2(int v) { + int r; + int shift; + r = (v > 0xffff ? 1 : 0) << 4; + v >>= r; + shift = (v > 0xff ? 1 : 0) << 3; + v >>= shift; + r |= shift; + shift = (v > 0xf ? 1 : 0) << 2; + v >>= shift; + r |= shift; + shift = (v > 0x3 ? 1 : 0) << 1; + v >>= shift; + r |= shift; + r |= (v >> 1); + return r; + } + + public class IntersectResult { + public bool intersects; + public float tmin; + public float tmax = 1f; + public int segMin = -1; + public int segMax = -1; + } + + public static IntersectResult intersectSegmentPoly2D(float[] p0, float[] p1, float[] verts, int nverts) { + + IntersectResult result = new IntersectResult(); + float EPS = 0.00000001f; + float[] dir = vSub(p1, p0); + + VectorPtr p0v = new VectorPtr(p0); + for (int i = 0, j = nverts - 1; i < nverts; j = i++) { + VectorPtr vpj = new VectorPtr(verts, j * 3); + float[] edge = vSub(new VectorPtr(verts, i * 3), vpj); + float[] diff = vSub(p0v, vpj); + float n = vPerp2D(edge, diff); + float d = vPerp2D(dir, edge); + if (Math.Abs(d) < EPS) { + // S is nearly parallel to this edge + if (n < 0) { + return result; + } else { + continue; + } + } + float t = n / d; + if (d < 0) { + // segment S is entering across this edge + if (t > result.tmin) { + result.tmin = t; + result.segMin = j; + // S enters after leaving polygon + if (result.tmin > result.tmax) { + return result; + } + } + } else { + // segment S is leaving across this edge + if (t < result.tmax) { + result.tmax = t; + result.segMax = j; + // S leaves before entering polygon + if (result.tmax < result.tmin) { + return result; + } + } + } + } + result.intersects = true; + return result; + } + + public static Tuple distancePtSegSqr2D(float[] pt, float[] verts, int p, int q) { + float pqx = verts[q + 0] - verts[p + 0]; + float pqz = verts[q + 2] - verts[p + 2]; + float dx = pt[0] - verts[p + 0]; + float dz = pt[2] - verts[p + 2]; + float d = pqx * pqx + pqz * pqz; + float t = pqx * dx + pqz * dz; + if (d > 0) { + t /= d; + } + if (t < 0) { + t = 0; + } else if (t > 1) { + t = 1; + } + dx = verts[p + 0] + t * pqx - pt[0]; + dz = verts[p + 2] + t * pqz - pt[2]; + return Tuple.Create(dx * dx + dz * dz, t); + } + + public static int oppositeTile(int side) { + return (side + 4) & 0x7; + } + + public static float vperpXZ(float[] a, float[] b) { + return a[0] * b[2] - a[2] * b[0]; + } + + public static Tuple? intersectSegSeg2D(float[] ap, float[] aq, float[] bp, float[] bq) { + float[] u = vSub(aq, ap); + float[] v = vSub(bq, bp); + float[] w = vSub(ap, bp); + float d = vperpXZ(u, v); + if (Math.Abs(d) < 1e-6f) + { + return null; + } + float s = vperpXZ(v, w) / d; + float t = vperpXZ(u, w) / d; + return Tuple.Create(s, t); + } + + public static float[] vScale(float[] @in, float scale) { + float[] @out = new float[3]; + @out[0] = @in[0] * scale; + @out[1] = @in[1] * scale; + @out[2] = @in[2] * scale; + return @out; + } + + /// Checks that the specified vector's components are all finite. + /// @param[in] v A point. [(x, y, z)] + /// @return True if all of the point's components are finite, i.e. not NaN + /// or any of the infinities. + public static bool vIsFinite(float[] v) { + return float.IsFinite(v[0]) && float.IsFinite(v[1]) && float.IsFinite(v[2]); + } + + /// Checks that the specified vector's 2D components are finite. + /// @param[in] v A point. [(x, y, z)] + public static bool vIsFinite2D(float[] v) { + return float.IsFinite(v[0]) && float.IsFinite(v[2]); + } + +} diff --git a/src/DotRecast.Detour/DotRecast.Detour.csproj b/src/DotRecast.Detour/DotRecast.Detour.csproj new file mode 100644 index 0000000..561560c --- /dev/null +++ b/src/DotRecast.Detour/DotRecast.Detour.csproj @@ -0,0 +1,12 @@ + + + + net7.0 + + + + + + + + diff --git a/src/DotRecast.Detour/FindDistanceToWallResult.cs b/src/DotRecast.Detour/FindDistanceToWallResult.cs new file mode 100644 index 0000000..c366b42 --- /dev/null +++ b/src/DotRecast.Detour/FindDistanceToWallResult.cs @@ -0,0 +1,45 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Detour; + +//TODO: (PP) Add comments +public class FindDistanceToWallResult { + private readonly float distance; + private readonly float[] position; + private readonly float[] normal; + + public FindDistanceToWallResult(float distance, float[] position, float[] normal) { + this.distance = distance; + this.position = position; + this.normal = normal; + } + + public float getDistance() { + return distance; + } + + public float[] getPosition() { + return position; + } + + public float[] getNormal() { + return normal; + } + +} \ No newline at end of file diff --git a/src/DotRecast.Detour/FindLocalNeighbourhoodResult.cs b/src/DotRecast.Detour/FindLocalNeighbourhoodResult.cs new file mode 100644 index 0000000..8dacd07 --- /dev/null +++ b/src/DotRecast.Detour/FindLocalNeighbourhoodResult.cs @@ -0,0 +1,42 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.Collections.Generic; + +namespace DotRecast.Detour; + +//TODO: (PP) Add comments +public class FindLocalNeighbourhoodResult { + private readonly List refs; + private readonly List parentRefs; + + public FindLocalNeighbourhoodResult(List refs, List parentRefs) { + this.@refs = refs; + this.parentRefs = parentRefs; + } + + public List getRefs() { + return refs; + } + + public List getParentRefs() { + return parentRefs; + } + +} \ No newline at end of file diff --git a/src/DotRecast.Detour/FindNearestPolyQuery.cs b/src/DotRecast.Detour/FindNearestPolyQuery.cs new file mode 100644 index 0000000..c47f75a --- /dev/null +++ b/src/DotRecast.Detour/FindNearestPolyQuery.cs @@ -0,0 +1,53 @@ +using System; + +namespace DotRecast.Detour; + +using static DetourCommon; + +public class FindNearestPolyQuery : PolyQuery { + + private readonly NavMeshQuery query; + private readonly float[] center; + private long nearestRef; + private float[] nearestPt; + private bool overPoly; + private float nearestDistanceSqr; + + public FindNearestPolyQuery(NavMeshQuery query, float[] center) { + this.query = query; + this.center = center; + nearestDistanceSqr = float.MaxValue; + nearestPt = new float[] { center[0], center[1], center[2] }; + } + + public void process(MeshTile tile, Poly poly, long refs) { + // Find nearest polygon amongst the nearby polygons. + Result closest = query.closestPointOnPoly(refs, center); + bool posOverPoly = closest.result.isPosOverPoly(); + float[] closestPtPoly = closest.result.getClosest(); + + // If a point is directly over a polygon and closer than + // climb height, favor that instead of straight line nearest point. + float d = 0; + float[] diff = vSub(center, closestPtPoly); + if (posOverPoly) { + d = Math.Abs(diff[1]) - tile.data.header.walkableClimb; + d = d > 0 ? d * d : 0; + } else { + d = vLenSqr(diff); + } + + if (d < nearestDistanceSqr) { + nearestPt = closestPtPoly; + nearestDistanceSqr = d; + nearestRef = refs; + overPoly = posOverPoly; + } + + } + + public FindNearestPolyResult result() { + return new FindNearestPolyResult(nearestRef, nearestPt, overPoly); + } + +} diff --git a/src/DotRecast.Detour/FindNearestPolyResult.cs b/src/DotRecast.Detour/FindNearestPolyResult.cs new file mode 100644 index 0000000..47ae34d --- /dev/null +++ b/src/DotRecast.Detour/FindNearestPolyResult.cs @@ -0,0 +1,46 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Detour; + +public class FindNearestPolyResult { + private readonly long nearestRef; + private readonly float[] nearestPos; + private readonly bool overPoly; + + public FindNearestPolyResult(long nearestRef, float[] nearestPos, bool overPoly) { + this.nearestRef = nearestRef; + this.nearestPos = nearestPos; + this.overPoly = overPoly; + } + + /** Returns the reference id of the nearest polygon. 0 if no polygon is found. */ + public long getNearestRef() { + return nearestRef; + } + + /** Returns the nearest point on the polygon. [opt] [(x, y, z)]. Unchanged if no polygon is found. */ + public float[] getNearestPos() { + return nearestPos; + } + + public bool isOverPoly() { + return overPoly; + } + +} diff --git a/src/DotRecast.Detour/FindPolysAroundResult.cs b/src/DotRecast.Detour/FindPolysAroundResult.cs new file mode 100644 index 0000000..ab82b84 --- /dev/null +++ b/src/DotRecast.Detour/FindPolysAroundResult.cs @@ -0,0 +1,49 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.Collections.Generic; + +namespace DotRecast.Detour; + + +// TODO: (PP) Add comments +public class FindPolysAroundResult { + private readonly List refs; + private readonly List parentRefs; + private readonly List costs; + + public FindPolysAroundResult(List refs, List parentRefs, List costs) { + this.@refs = refs; + this.parentRefs = parentRefs; + this.costs = costs; + } + + public List getRefs() { + return refs; + } + + public List getParentRefs() { + return parentRefs; + } + + public List getCosts() { + return costs; + } + +} \ No newline at end of file diff --git a/src/DotRecast.Detour/FindRandomPointResult.cs b/src/DotRecast.Detour/FindRandomPointResult.cs new file mode 100644 index 0000000..d8408ad --- /dev/null +++ b/src/DotRecast.Detour/FindRandomPointResult.cs @@ -0,0 +1,41 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Detour; + +//TODO: (PP) Add comments +public class FindRandomPointResult { + private readonly long randomRef; + private readonly float[] randomPt; + + public FindRandomPointResult(long randomRef, float[] randomPt) { + this.randomRef = randomRef; + this.randomPt = randomPt; + } + + /// @param[out] randomRef The reference id of the random location. + public long getRandomRef() { + return randomRef; + } + + /// @param[out] randomPt The random location. + public float[] getRandomPt() { + return randomPt; + } + +} \ No newline at end of file diff --git a/src/DotRecast.Detour/GetPolyWallSegmentsResult.cs b/src/DotRecast.Detour/GetPolyWallSegmentsResult.cs new file mode 100644 index 0000000..8f98b36 --- /dev/null +++ b/src/DotRecast.Detour/GetPolyWallSegmentsResult.cs @@ -0,0 +1,43 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.Collections.Generic; + +namespace DotRecast.Detour; + + +public class GetPolyWallSegmentsResult { + + private readonly List segmentVerts; + private readonly List segmentRefs; + + public GetPolyWallSegmentsResult(List segmentVerts, List segmentRefs) { + this.segmentVerts = segmentVerts; + this.segmentRefs = segmentRefs; + } + + public List getSegmentVerts() { + return segmentVerts; + } + + public List getSegmentRefs() { + return segmentRefs; + } + +} diff --git a/src/DotRecast.Detour/Io/DetourWriter.cs b/src/DotRecast.Detour/Io/DetourWriter.cs new file mode 100644 index 0000000..6fb490b --- /dev/null +++ b/src/DotRecast.Detour/Io/DetourWriter.cs @@ -0,0 +1,82 @@ +/* +Recast4J Copyright (c) 2015 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.IO; +using DotRecast.Core; + +namespace DotRecast.Detour.Io; + +public abstract class DetourWriter { + + protected void write(BinaryWriter stream, float value, ByteOrder order) { + byte[] bytes = BitConverter.GetBytes(value); + int i = BitConverter.ToInt32(bytes, 0); + write(stream, i, order); + } + + protected void write(BinaryWriter stream, short value, ByteOrder order) { + if (order == ByteOrder.BIG_ENDIAN) { + stream.Write((byte)((value >> 8) & 0xFF)); + stream.Write((byte)(value & 0xFF)); + } else { + stream.Write((byte)(value & 0xFF)); + stream.Write((byte)((value >> 8) & 0xFF)); + } + } + + protected void write(BinaryWriter stream, long value, ByteOrder order) { + if (order == ByteOrder.BIG_ENDIAN) { + write(stream, (int) (value >>> 32), order); + write(stream, (int) (value & 0xFFFFFFFF), order); + } else { + write(stream, (int) (value & 0xFFFFFFFF), order); + write(stream, (int) (value >>> 32), order); + } + } + + protected void write(BinaryWriter stream, int value, ByteOrder order) { + if (order == ByteOrder.BIG_ENDIAN) { + stream.Write((byte)((value >> 24) & 0xFF)); + stream.Write((byte)((value >> 16) & 0xFF)); + stream.Write((byte)((value >> 8) & 0xFF)); + stream.Write((byte)(value & 0xFF)); + } else { + stream.Write((byte)(value & 0xFF)); + stream.Write((byte)((value >> 8) & 0xFF)); + stream.Write((byte)((value >> 16) & 0xFF)); + stream.Write((byte)((value >> 24) & 0xFF)); + } + } + + protected void write(BinaryWriter stream, bool @bool) { + write(stream, (byte) (@bool ? 1 : 0)); + } + + protected void write(BinaryWriter stream, byte value) { + stream.Write(value); + } + + protected void write(BinaryWriter stream, MemoryStream data) { + data.Position = 0; + byte[] buffer = new byte[data.Length]; + data.Read(buffer, 0, buffer.Length); + stream.Write(buffer); + } + +} diff --git a/src/DotRecast.Detour/Io/IOUtils.cs b/src/DotRecast.Detour/Io/IOUtils.cs new file mode 100644 index 0000000..76a65ad --- /dev/null +++ b/src/DotRecast.Detour/Io/IOUtils.cs @@ -0,0 +1,58 @@ +/* +Recast4J Copyright (c) 2015 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.IO; +using DotRecast.Core; + +namespace DotRecast.Detour.Io; + +public static class IOUtils { + + public static ByteBuffer toByteBuffer(BinaryReader @is, bool direct) { + byte[] data = toByteArray(@is); + if (direct) { + Array.Reverse(data); + } + + return new ByteBuffer(data); + } + + public static byte[] toByteArray(BinaryReader inputStream) { + using var baos = new MemoryStream(); + byte[] buffer = new byte[4096]; + int l; + while ((l = inputStream.Read(buffer)) > 0) { + baos.Write(buffer, 0, l); + } + + return baos.ToArray(); + } + + + public static ByteBuffer toByteBuffer(BinaryReader inputStream) + { + var bytes = toByteArray(inputStream); + return new ByteBuffer(bytes); + } + + public static int swapEndianness(int i) { + var s = ((i >>> 24) & 0xFF) | ((i>>8) & 0xFF00) | ((i<<8) & 0xFF0000) | ((i << 24) & 0xFF000000); + return (int)s; + } +} diff --git a/src/DotRecast.Detour/Io/MeshDataReader.cs b/src/DotRecast.Detour/Io/MeshDataReader.cs new file mode 100644 index 0000000..75654a3 --- /dev/null +++ b/src/DotRecast.Detour/Io/MeshDataReader.cs @@ -0,0 +1,201 @@ +/* +Recast4J Copyright (c) 2015 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.IO; +using DotRecast.Core; + +namespace DotRecast.Detour.Io; + +public class MeshDataReader { + + public const int DT_POLY_DETAIL_SIZE = 10; + + public MeshData read(BinaryReader stream, int maxVertPerPoly) { + ByteBuffer buf = IOUtils.toByteBuffer(stream); + return read(buf, maxVertPerPoly, false); + } + + public MeshData read(ByteBuffer buf, int maxVertPerPoly) { + return read(buf, maxVertPerPoly, false); + } + + public MeshData read32Bit(BinaryReader stream, int maxVertPerPoly) { + ByteBuffer buf = IOUtils.toByteBuffer(stream); + return read(buf, maxVertPerPoly, true); + } + + public MeshData read32Bit(ByteBuffer buf, int maxVertPerPoly) { + return read(buf, maxVertPerPoly, true); + } + + public MeshData read(ByteBuffer buf, int maxVertPerPoly, bool is32Bit) + { + MeshData data = new MeshData(); + MeshHeader header = new MeshHeader(); + data.header = header; + header.magic = buf.getInt(); + if (header.magic != MeshHeader.DT_NAVMESH_MAGIC) { + header.magic = IOUtils.swapEndianness(header.magic); + if (header.magic != MeshHeader.DT_NAVMESH_MAGIC) { + throw new IOException("Invalid magic"); + } + buf.order(buf.order() == ByteOrder.BIG_ENDIAN ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN); + } + header.version = buf.getInt(); + if (header.version != MeshHeader.DT_NAVMESH_VERSION) { + if (header.version < MeshHeader.DT_NAVMESH_VERSION_RECAST4J_FIRST + || header.version > MeshHeader.DT_NAVMESH_VERSION_RECAST4J_LAST) { + throw new IOException("Invalid version " + header.version); + } + } + bool cCompatibility = header.version == MeshHeader.DT_NAVMESH_VERSION; + header.x = buf.getInt(); + header.y = buf.getInt(); + header.layer = buf.getInt(); + header.userId = buf.getInt(); + header.polyCount = buf.getInt(); + header.vertCount = buf.getInt(); + header.maxLinkCount = buf.getInt(); + header.detailMeshCount = buf.getInt(); + header.detailVertCount = buf.getInt(); + header.detailTriCount = buf.getInt(); + header.bvNodeCount = buf.getInt(); + header.offMeshConCount = buf.getInt(); + header.offMeshBase = buf.getInt(); + header.walkableHeight = buf.getFloat(); + header.walkableRadius = buf.getFloat(); + header.walkableClimb = buf.getFloat(); + for (int j = 0; j < 3; j++) { + header.bmin[j] = buf.getFloat(); + } + for (int j = 0; j < 3; j++) { + header.bmax[j] = buf.getFloat(); + } + header.bvQuantFactor = buf.getFloat(); + data.verts = readVerts(buf, header.vertCount); + data.polys = readPolys(buf, header, maxVertPerPoly); + if (cCompatibility) { + buf.position(buf.position() + header.maxLinkCount * getSizeofLink(is32Bit)); + } + data.detailMeshes = readPolyDetails(buf, header, cCompatibility); + data.detailVerts = readVerts(buf, header.detailVertCount); + data.detailTris = readDTris(buf, header); + data.bvTree = readBVTree(buf, header); + data.offMeshCons = readOffMeshCons(buf, header); + return data; + } + + public const int LINK_SIZEOF = 16; + public const int LINK_SIZEOF32BIT = 12; + + public static int getSizeofLink(bool is32Bit) { + return is32Bit ? LINK_SIZEOF32BIT : LINK_SIZEOF; + } + + private float[] readVerts(ByteBuffer buf, int count) { + float[] verts = new float[count * 3]; + for (int i = 0; i < verts.Length; i++) { + verts[i] = buf.getFloat(); + } + return verts; + } + + private Poly[] readPolys(ByteBuffer buf, MeshHeader header, int maxVertPerPoly) { + Poly[] polys = new Poly[header.polyCount]; + for (int i = 0; i < polys.Length; i++) { + polys[i] = new Poly(i, maxVertPerPoly); + if (header.version < MeshHeader.DT_NAVMESH_VERSION_RECAST4J_NO_POLY_FIRSTLINK) { + buf.getInt(); // polys[i].firstLink + } + for (int j = 0; j < polys[i].verts.Length; j++) { + polys[i].verts[j] = buf.getShort() & 0xFFFF; + } + for (int j = 0; j < polys[i].neis.Length; j++) { + polys[i].neis[j] = buf.getShort() & 0xFFFF; + } + polys[i].flags = buf.getShort() & 0xFFFF; + polys[i].vertCount = buf.get() & 0xFF; + polys[i].areaAndtype = buf.get() & 0xFF; + } + return polys; + } + + private PolyDetail[] readPolyDetails(ByteBuffer buf, MeshHeader header, bool cCompatibility) { + PolyDetail[] polys = new PolyDetail[header.detailMeshCount]; + for (int i = 0; i < polys.Length; i++) { + polys[i] = new PolyDetail(); + polys[i].vertBase = buf.getInt(); + polys[i].triBase = buf.getInt(); + polys[i].vertCount = buf.get() & 0xFF; + polys[i].triCount = buf.get() & 0xFF; + if (cCompatibility) { + buf.getShort(); // C struct padding + } + } + return polys; + } + + private int[] readDTris(ByteBuffer buf, MeshHeader header) { + int[] tris = new int[4 * header.detailTriCount]; + for (int i = 0; i < tris.Length; i++) { + tris[i] = buf.get() & 0xFF; + } + return tris; + } + + private BVNode[] readBVTree(ByteBuffer buf, MeshHeader header) { + BVNode[] nodes = new BVNode[header.bvNodeCount]; + for (int i = 0; i < nodes.Length; i++) { + nodes[i] = new BVNode(); + if (header.version < MeshHeader.DT_NAVMESH_VERSION_RECAST4J_32BIT_BVTREE) { + for (int j = 0; j < 3; j++) { + nodes[i].bmin[j] = buf.getShort() & 0xFFFF; + } + for (int j = 0; j < 3; j++) { + nodes[i].bmax[j] = buf.getShort() & 0xFFFF; + } + } else { + for (int j = 0; j < 3; j++) { + nodes[i].bmin[j] = buf.getInt(); + } + for (int j = 0; j < 3; j++) { + nodes[i].bmax[j] = buf.getInt(); + } + } + nodes[i].i = buf.getInt(); + } + return nodes; + } + + private OffMeshConnection[] readOffMeshCons(ByteBuffer buf, MeshHeader header) { + OffMeshConnection[] cons = new OffMeshConnection[header.offMeshConCount]; + for (int i = 0; i < cons.Length; i++) { + cons[i] = new OffMeshConnection(); + for (int j = 0; j < 6; j++) { + cons[i].pos[j] = buf.getFloat(); + } + cons[i].rad = buf.getFloat(); + cons[i].poly = buf.getShort() & 0xFFFF; + cons[i].flags = buf.get() & 0xFF; + cons[i].side = buf.get() & 0xFF; + cons[i].userId = buf.getInt(); + } + return cons; + } + +} diff --git a/src/DotRecast.Detour/Io/MeshDataWriter.cs b/src/DotRecast.Detour/Io/MeshDataWriter.cs new file mode 100644 index 0000000..0416eee --- /dev/null +++ b/src/DotRecast.Detour/Io/MeshDataWriter.cs @@ -0,0 +1,142 @@ +/* +Recast4J Copyright (c) 2015 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.IO; +using DotRecast.Core; + +namespace DotRecast.Detour.Io; + +public class MeshDataWriter : DetourWriter { + + public void write(BinaryWriter stream, MeshData data, ByteOrder order, bool cCompatibility) { + MeshHeader header = data.header; + write(stream, header.magic, order); + write(stream, cCompatibility ? MeshHeader.DT_NAVMESH_VERSION : MeshHeader.DT_NAVMESH_VERSION_RECAST4J_LAST, order); + write(stream, header.x, order); + write(stream, header.y, order); + write(stream, header.layer, order); + write(stream, header.userId, order); + write(stream, header.polyCount, order); + write(stream, header.vertCount, order); + write(stream, header.maxLinkCount, order); + write(stream, header.detailMeshCount, order); + write(stream, header.detailVertCount, order); + write(stream, header.detailTriCount, order); + write(stream, header.bvNodeCount, order); + write(stream, header.offMeshConCount, order); + write(stream, header.offMeshBase, order); + write(stream, header.walkableHeight, order); + write(stream, header.walkableRadius, order); + write(stream, header.walkableClimb, order); + write(stream, header.bmin[0], order); + write(stream, header.bmin[1], order); + write(stream, header.bmin[2], order); + write(stream, header.bmax[0], order); + write(stream, header.bmax[1], order); + write(stream, header.bmax[2], order); + write(stream, header.bvQuantFactor, order); + writeVerts(stream, data.verts, header.vertCount, order); + writePolys(stream, data, order, cCompatibility); + if (cCompatibility) { + byte[] linkPlaceholder = new byte[header.maxLinkCount * MeshDataReader.getSizeofLink(false)]; + stream.Write(linkPlaceholder); + } + writePolyDetails(stream, data, order, cCompatibility); + writeVerts(stream, data.detailVerts, header.detailVertCount, order); + writeDTris(stream, data); + writeBVTree(stream, data, order, cCompatibility); + writeOffMeshCons(stream, data, order); + } + + private void writeVerts(BinaryWriter stream, float[] verts, int count, ByteOrder order) { + for (int i = 0; i < count * 3; i++) { + write(stream, verts[i], order); + } + } + + private void writePolys(BinaryWriter stream, MeshData data, ByteOrder order, bool cCompatibility) { + for (int i = 0; i < data.header.polyCount; i++) { + if (cCompatibility) { + write(stream, 0xFFFF, order); + } + for (int j = 0; j < data.polys[i].verts.Length; j++) { + write(stream, (short) data.polys[i].verts[j], order); + } + for (int j = 0; j < data.polys[i].neis.Length; j++) { + write(stream, (short) data.polys[i].neis[j], order); + } + write(stream, (short) data.polys[i].flags, order); + write(stream, (byte)data.polys[i].vertCount); + write(stream, (byte)data.polys[i].areaAndtype); + } + } + + private void writePolyDetails(BinaryWriter stream, MeshData data, ByteOrder order, bool cCompatibility) + { + for (int i = 0; i < data.header.detailMeshCount; i++) { + write(stream, data.detailMeshes[i].vertBase, order); + write(stream, data.detailMeshes[i].triBase, order); + write(stream, (byte)data.detailMeshes[i].vertCount); + write(stream, (byte)data.detailMeshes[i].triCount); + if (cCompatibility) { + write(stream, (short) 0, order); + } + } + } + + private void writeDTris(BinaryWriter stream, MeshData data) { + for (int i = 0; i < data.header.detailTriCount * 4; i++) { + write(stream, (byte)data.detailTris[i]); + } + } + + private void writeBVTree(BinaryWriter stream, MeshData data, ByteOrder order, bool cCompatibility) { + for (int i = 0; i < data.header.bvNodeCount; i++) { + if (cCompatibility) { + for (int j = 0; j < 3; j++) { + write(stream, (short) data.bvTree[i].bmin[j], order); + } + for (int j = 0; j < 3; j++) { + write(stream, (short) data.bvTree[i].bmax[j], order); + } + } else { + for (int j = 0; j < 3; j++) { + write(stream, data.bvTree[i].bmin[j], order); + } + for (int j = 0; j < 3; j++) { + write(stream, data.bvTree[i].bmax[j], order); + } + } + write(stream, data.bvTree[i].i, order); + } + } + + private void writeOffMeshCons(BinaryWriter stream, MeshData data, ByteOrder order) { + for (int i = 0; i < data.header.offMeshConCount; i++) { + for (int j = 0; j < 6; j++) { + write(stream, data.offMeshCons[i].pos[j], order); + } + write(stream, data.offMeshCons[i].rad, order); + write(stream, (short) data.offMeshCons[i].poly, order); + write(stream, (byte) data.offMeshCons[i].flags); + write(stream, (byte) data.offMeshCons[i].side); + write(stream, data.offMeshCons[i].userId, order); + } + } + +} diff --git a/src/DotRecast.Detour/Io/MeshSetReader.cs b/src/DotRecast.Detour/Io/MeshSetReader.cs new file mode 100644 index 0000000..1dc92d0 --- /dev/null +++ b/src/DotRecast.Detour/Io/MeshSetReader.cs @@ -0,0 +1,127 @@ +/* +Recast4J Copyright (c) 2015 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.IO; +using DotRecast.Core; + +namespace DotRecast.Detour.Io; + +using static DetourCommon; + + +public class MeshSetReader { + + private readonly MeshDataReader meshReader = new MeshDataReader(); + private readonly NavMeshParamReader paramReader = new NavMeshParamReader(); + + public NavMesh read(BinaryReader @is, int maxVertPerPoly) { + return read(IOUtils.toByteBuffer(@is), maxVertPerPoly, false); + } + + public NavMesh read(ByteBuffer bb, int maxVertPerPoly) { + return read(bb, maxVertPerPoly, false); + } + + public NavMesh read32Bit(BinaryReader @is, int maxVertPerPoly) { + return read(IOUtils.toByteBuffer(@is), maxVertPerPoly, true); + } + + public NavMesh read32Bit(ByteBuffer bb, int maxVertPerPoly) { + return read(bb, maxVertPerPoly, true); + } + + public NavMesh read(BinaryReader @is) { + return read(IOUtils.toByteBuffer(@is)); + } + + public NavMesh read(ByteBuffer bb) { + return read(bb, -1, false); + } + + NavMesh read(ByteBuffer bb, int maxVertPerPoly, bool is32Bit) { + NavMeshSetHeader header = readHeader(bb, maxVertPerPoly); + if (header.maxVertsPerPoly <= 0) { + throw new IOException("Invalid number of verts per poly " + header.maxVertsPerPoly); + } + bool cCompatibility = header.version == NavMeshSetHeader.NAVMESHSET_VERSION; + NavMesh mesh = new NavMesh(header.option, header.maxVertsPerPoly); + readTiles(bb, is32Bit, header, cCompatibility, mesh); + return mesh; + } + + private NavMeshSetHeader readHeader(ByteBuffer bb, int maxVertsPerPoly) { + NavMeshSetHeader header = new NavMeshSetHeader(); + header.magic = bb.getInt(); + if (header.magic != NavMeshSetHeader.NAVMESHSET_MAGIC) { + header.magic = IOUtils.swapEndianness(header.magic); + if (header.magic != NavMeshSetHeader.NAVMESHSET_MAGIC) { + throw new IOException("Invalid magic " + header.magic); + } + bb.order(bb.order() == ByteOrder.BIG_ENDIAN ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN); + } + header.version = bb.getInt(); + if (header.version != NavMeshSetHeader.NAVMESHSET_VERSION && header.version != NavMeshSetHeader.NAVMESHSET_VERSION_RECAST4J_1 + && header.version != NavMeshSetHeader.NAVMESHSET_VERSION_RECAST4J) { + throw new IOException("Invalid version " + header.version); + } + header.numTiles = bb.getInt(); + header.option = paramReader.read(bb); + header.maxVertsPerPoly = maxVertsPerPoly; + if (header.version == NavMeshSetHeader.NAVMESHSET_VERSION_RECAST4J) { + header.maxVertsPerPoly = bb.getInt(); + } + return header; + } + + private void readTiles(ByteBuffer bb, bool is32Bit, NavMeshSetHeader header, bool cCompatibility, NavMesh mesh) + { + // Read tiles. + for (int i = 0; i < header.numTiles; ++i) { + NavMeshTileHeader tileHeader = new NavMeshTileHeader(); + if (is32Bit) { + tileHeader.tileRef = convert32BitRef(bb.getInt(), header.option); + } else { + tileHeader.tileRef = bb.getLong(); + } + tileHeader.dataSize = bb.getInt(); + if (tileHeader.tileRef == 0 || tileHeader.dataSize == 0) { + break; + } + if (cCompatibility && !is32Bit) { + bb.getInt(); // C struct padding + } + MeshData data = meshReader.read(bb, mesh.getMaxVertsPerPoly(), is32Bit); + mesh.addTile(data, i, tileHeader.tileRef); + } + } + + private long convert32BitRef(int refs, NavMeshParams option) { + int m_tileBits = ilog2(nextPow2(option.maxTiles)); + int m_polyBits = ilog2(nextPow2(option.maxPolys)); + // Only allow 31 salt bits, since the salt mask is calculated using 32bit uint and it will overflow. + int m_saltBits = Math.Min(31, 32 - m_tileBits - m_polyBits); + int saltMask = (1 << m_saltBits) - 1; + int tileMask = (1 << m_tileBits) - 1; + int polyMask = (1 << m_polyBits) - 1; + int salt = ((refs >> (m_polyBits + m_tileBits)) & saltMask); + int it = ((refs >> m_polyBits) & tileMask); + int ip = refs & polyMask; + return NavMesh.encodePolyId(salt, it, ip); + } +} diff --git a/src/DotRecast.Detour/Io/MeshSetWriter.cs b/src/DotRecast.Detour/Io/MeshSetWriter.cs new file mode 100644 index 0000000..d0135d0 --- /dev/null +++ b/src/DotRecast.Detour/Io/MeshSetWriter.cs @@ -0,0 +1,78 @@ +/* +Recast4J Copyright (c) 2015-2018 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.IO; +using DotRecast.Core; + +namespace DotRecast.Detour.Io; + +public class MeshSetWriter : DetourWriter { + + private readonly MeshDataWriter writer = new MeshDataWriter(); + private readonly NavMeshParamWriter paramWriter = new NavMeshParamWriter(); + + public void write(BinaryWriter stream, NavMesh mesh, ByteOrder order, bool cCompatibility) { + writeHeader(stream, mesh, order, cCompatibility); + writeTiles(stream, mesh, order, cCompatibility); + } + + private void writeHeader(BinaryWriter stream, NavMesh mesh, ByteOrder order, bool cCompatibility) { + write(stream, NavMeshSetHeader.NAVMESHSET_MAGIC, order); + write(stream, cCompatibility ? NavMeshSetHeader.NAVMESHSET_VERSION : NavMeshSetHeader.NAVMESHSET_VERSION_RECAST4J, order); + int numTiles = 0; + for (int i = 0; i < mesh.getMaxTiles(); ++i) { + MeshTile tile = mesh.getTile(i); + if (tile == null || tile.data == null || tile.data.header == null) { + continue; + } + numTiles++; + } + write(stream, numTiles, order); + paramWriter.write(stream, mesh.getParams(), order); + if (!cCompatibility) { + write(stream, mesh.getMaxVertsPerPoly(), order); + } + } + + private void writeTiles(BinaryWriter stream, NavMesh mesh, ByteOrder order, bool cCompatibility) { + for (int i = 0; i < mesh.getMaxTiles(); ++i) { + MeshTile tile = mesh.getTile(i); + if (tile == null || tile.data == null || tile.data.header == null) { + continue; + } + + NavMeshTileHeader tileHeader = new NavMeshTileHeader(); + tileHeader.tileRef = mesh.getTileRef(tile); + using MemoryStream bb = new MemoryStream(); + using BinaryWriter baos = new BinaryWriter(bb); + writer.write(baos, tile.data, order, cCompatibility); + baos.Flush(); + baos.Close(); + + byte[] ba = bb.ToArray(); + tileHeader.dataSize = ba.Length; + write(stream, tileHeader.tileRef, order); + write(stream, tileHeader.dataSize, order); + if (cCompatibility) { + write(stream, 0, order); // C struct padding + } + stream.Write(ba); + } + } + +} diff --git a/src/DotRecast.Detour/Io/NavMeshParamReader.cs b/src/DotRecast.Detour/Io/NavMeshParamReader.cs new file mode 100644 index 0000000..54e39a9 --- /dev/null +++ b/src/DotRecast.Detour/Io/NavMeshParamReader.cs @@ -0,0 +1,19 @@ +using DotRecast.Core; + +namespace DotRecast.Detour.Io; + +public class NavMeshParamReader { + + public NavMeshParams read(ByteBuffer bb) { + NavMeshParams option = new NavMeshParams(); + option.orig[0] = bb.getFloat(); + option.orig[1] = bb.getFloat(); + option.orig[2] = bb.getFloat(); + option.tileWidth = bb.getFloat(); + option.tileHeight = bb.getFloat(); + option.maxTiles = bb.getInt(); + option.maxPolys = bb.getInt(); + return option; + } + +} diff --git a/src/DotRecast.Detour/Io/NavMeshParamWriter.cs b/src/DotRecast.Detour/Io/NavMeshParamWriter.cs new file mode 100644 index 0000000..8fac183 --- /dev/null +++ b/src/DotRecast.Detour/Io/NavMeshParamWriter.cs @@ -0,0 +1,18 @@ +using System.IO; +using DotRecast.Core; + +namespace DotRecast.Detour.Io; + +public class NavMeshParamWriter : DetourWriter { + + public void write(BinaryWriter stream, NavMeshParams option, ByteOrder order) { + write(stream, option.orig[0], order); + write(stream, option.orig[1], order); + write(stream, option.orig[2], order); + write(stream, option.tileWidth, order); + write(stream, option.tileHeight, order); + write(stream, option.maxTiles, order); + write(stream, option.maxPolys, order); + } + +} diff --git a/src/DotRecast.Detour/Io/NavMeshSetHeader.cs b/src/DotRecast.Detour/Io/NavMeshSetHeader.cs new file mode 100644 index 0000000..ab2fc44 --- /dev/null +++ b/src/DotRecast.Detour/Io/NavMeshSetHeader.cs @@ -0,0 +1,33 @@ +/* +Recast4J Copyright (c) 2015-2018 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Detour.Io; + +public class NavMeshSetHeader { + + public const int NAVMESHSET_MAGIC = 'M' << 24 | 'S' << 16 | 'E' << 8 | 'T'; // 'MSET'; + public const int NAVMESHSET_VERSION = 1; + public const int NAVMESHSET_VERSION_RECAST4J_1 = 0x8801; + public const int NAVMESHSET_VERSION_RECAST4J = 0x8802; + + public int magic; + public int version; + public int numTiles; + public NavMeshParams option = new NavMeshParams(); + public int maxVertsPerPoly; + +} diff --git a/src/DotRecast.Detour/Io/NavMeshTileHeader.cs b/src/DotRecast.Detour/Io/NavMeshTileHeader.cs new file mode 100644 index 0000000..3613c3f --- /dev/null +++ b/src/DotRecast.Detour/Io/NavMeshTileHeader.cs @@ -0,0 +1,6 @@ +namespace DotRecast.Detour.Io; + +public class NavMeshTileHeader { + public long tileRef; + public int dataSize; +} diff --git a/src/DotRecast.Detour/LegacyNavMeshQuery.cs b/src/DotRecast.Detour/LegacyNavMeshQuery.cs new file mode 100644 index 0000000..e11bc0a --- /dev/null +++ b/src/DotRecast.Detour/LegacyNavMeshQuery.cs @@ -0,0 +1,730 @@ +/* +recast4j copyright (c) 2021 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.Collections.Generic; + +namespace DotRecast.Detour; + +using static DetourCommon; + + +public class LegacyNavMeshQuery : NavMeshQuery { + + private static float H_SCALE = 0.999f; // Search heuristic scale. + + public LegacyNavMeshQuery(NavMesh nav) : base(nav) { + } + + public override Result> findPath(long startRef, long endRef, float[] startPos, float[] endPos, QueryFilter filter, + int options, float raycastLimit) { + return findPath(startRef, endRef, startPos, endPos, filter); + } + + public override Result> findPath(long startRef, long endRef, float[] startPos, float[] endPos, + QueryFilter filter) { + // Validate input + if (!m_nav.isValidPolyRef(startRef) || !m_nav.isValidPolyRef(endRef) || null == startPos + || !vIsFinite(startPos) || null == endPos || !vIsFinite(endPos) || null == filter) { + return Results.invalidParam>(); + } + + if (startRef == endRef) { + List singlePath = new(1); + singlePath.Add(startRef); + return Results.success(singlePath); + } + + m_nodePool.clear(); + m_openList.clear(); + + Node startNode = m_nodePool.getNode(startRef); + vCopy(startNode.pos, startPos); + startNode.pidx = 0; + startNode.cost = 0; + startNode.total = vDist(startPos, endPos) * H_SCALE; + startNode.id = startRef; + startNode.flags = Node.DT_NODE_OPEN; + m_openList.push(startNode); + + Node lastBestNode = startNode; + float lastBestNodeCost = startNode.total; + + Status status = Status.SUCCSESS; + + while (!m_openList.isEmpty()) { + // Remove node from open list and put it in closed list. + Node bestNode = m_openList.pop(); + bestNode.flags &= ~Node.DT_NODE_OPEN; + bestNode.flags |= Node.DT_NODE_CLOSED; + + // Reached the goal, stop searching. + if (bestNode.id == endRef) { + lastBestNode = bestNode; + break; + } + + // Get current poly and tile. + // The API input has been cheked already, skip checking internal data. + long bestRef = bestNode.id; + Tuple tileAndPoly = m_nav.getTileAndPolyByRefUnsafe(bestRef); + MeshTile bestTile = tileAndPoly.Item1; + Poly bestPoly = tileAndPoly.Item2; + + // Get parent poly and tile. + long parentRef = 0; + MeshTile parentTile = null; + Poly parentPoly = null; + if (bestNode.pidx != 0) { + parentRef = m_nodePool.getNodeAtIdx(bestNode.pidx).id; + } + if (parentRef != 0) { + tileAndPoly = m_nav.getTileAndPolyByRefUnsafe(parentRef); + parentTile = tileAndPoly.Item1; + parentPoly = tileAndPoly.Item2; + } + + for (int i = bestTile.polyLinks[bestPoly.index]; i != NavMesh.DT_NULL_LINK; i = bestTile.links[i].next) { + long neighbourRef = bestTile.links[i].refs; + + // Skip invalid ids and do not expand back to where we came from. + if (neighbourRef == 0 || neighbourRef == parentRef) { + continue; + } + + // Get neighbour poly and tile. + // The API input has been cheked already, skip checking internal data. + tileAndPoly = m_nav.getTileAndPolyByRefUnsafe(neighbourRef); + MeshTile neighbourTile = tileAndPoly.Item1; + Poly neighbourPoly = tileAndPoly.Item2; + + if (!filter.passFilter(neighbourRef, neighbourTile, neighbourPoly)) { + continue; + } + + // deal explicitly with crossing tile boundaries + int crossSide = 0; + if (bestTile.links[i].side != 0xff) { + crossSide = bestTile.links[i].side >> 1; + } + + // get the node + Node neighbourNode = m_nodePool.getNode(neighbourRef, crossSide); + + // If the node is visited the first time, calculate node position. + if (neighbourNode.flags == 0) { + Result midpod = getEdgeMidPoint(bestRef, bestPoly, bestTile, neighbourRef, neighbourPoly, + neighbourTile); + if (!midpod.failed()) { + neighbourNode.pos = midpod.result; + } + } + + // Calculate cost and heuristic. + float cost = 0; + float heuristic = 0; + + // Special case for last node. + if (neighbourRef == endRef) { + // Cost + float curCost = filter.getCost(bestNode.pos, neighbourNode.pos, parentRef, parentTile, parentPoly, + bestRef, bestTile, bestPoly, neighbourRef, neighbourTile, neighbourPoly); + float endCost = filter.getCost(neighbourNode.pos, endPos, bestRef, bestTile, bestPoly, neighbourRef, + neighbourTile, neighbourPoly, 0L, null, null); + + cost = bestNode.cost + curCost + endCost; + heuristic = 0; + } else { + // Cost + float curCost = filter.getCost(bestNode.pos, neighbourNode.pos, parentRef, parentTile, parentPoly, + bestRef, bestTile, bestPoly, neighbourRef, neighbourTile, neighbourPoly); + cost = bestNode.cost + curCost; + heuristic = vDist(neighbourNode.pos, endPos) * H_SCALE; + } + + float total = cost + heuristic; + + // The node is already in open list and the new result is worse, skip. + if ((neighbourNode.flags & Node.DT_NODE_OPEN) != 0 && total >= neighbourNode.total) { + continue; + } + // The node is already visited and process, and the new result is worse, skip. + if ((neighbourNode.flags & Node.DT_NODE_CLOSED) != 0 && total >= neighbourNode.total) { + continue; + } + + // Add or update the node. + neighbourNode.pidx = m_nodePool.getNodeIdx(bestNode); + neighbourNode.id = neighbourRef; + neighbourNode.flags = (neighbourNode.flags & ~Node.DT_NODE_CLOSED); + neighbourNode.cost = cost; + neighbourNode.total = total; + + if ((neighbourNode.flags & Node.DT_NODE_OPEN) != 0) { + // Already in open, update node location. + m_openList.modify(neighbourNode); + } else { + // Put the node in open list. + neighbourNode.flags |= Node.DT_NODE_OPEN; + m_openList.push(neighbourNode); + } + + // Update nearest node to target so far. + if (heuristic < lastBestNodeCost) { + lastBestNodeCost = heuristic; + lastBestNode = neighbourNode; + } + } + } + + List path = getPathToNode(lastBestNode); + + if (lastBestNode.id != endRef) { + status = Status.PARTIAL_RESULT; + } + + return Results.of(status, path); + } + + /** + * Updates an in-progress sliced path query. + * + * @param maxIter + * The maximum number of iterations to perform. + * @return The status flags for the query. + */ + public override Result updateSlicedFindPath(int maxIter) { + if (!m_query.status.isInProgress()) { + return Results.of(m_query.status, 0); + } + + // Make sure the request is still valid. + if (!m_nav.isValidPolyRef(m_query.startRef) || !m_nav.isValidPolyRef(m_query.endRef)) { + m_query.status = Status.FAILURE; + return Results.of(m_query.status, 0); + } + + int iter = 0; + while (iter < maxIter && !m_openList.isEmpty()) { + iter++; + + // Remove node from open list and put it in closed list. + Node bestNode = m_openList.pop(); + bestNode.flags &= ~Node.DT_NODE_OPEN; + bestNode.flags |= Node.DT_NODE_CLOSED; + + // Reached the goal, stop searching. + if (bestNode.id == m_query.endRef) { + m_query.lastBestNode = bestNode; + m_query.status = Status.SUCCSESS; + return Results.of(m_query.status, iter); + } + + // Get current poly and tile. + // The API input has been cheked already, skip checking internal + // data. + long bestRef = bestNode.id; + Result> tileAndPoly = m_nav.getTileAndPolyByRef(bestRef); + if (tileAndPoly.failed()) { + m_query.status = Status.FAILURE; + // The polygon has disappeared during the sliced query, fail. + return Results.of(m_query.status, iter); + } + MeshTile bestTile = tileAndPoly.result.Item1; + Poly bestPoly = tileAndPoly.result.Item2; + // Get parent and grand parent poly and tile. + long parentRef = 0, grandpaRef = 0; + MeshTile parentTile = null; + Poly parentPoly = null; + Node parentNode = null; + if (bestNode.pidx != 0) { + parentNode = m_nodePool.getNodeAtIdx(bestNode.pidx); + parentRef = parentNode.id; + if (parentNode.pidx != 0) { + grandpaRef = m_nodePool.getNodeAtIdx(parentNode.pidx).id; + } + } + if (parentRef != 0) { + bool invalidParent = false; + tileAndPoly = m_nav.getTileAndPolyByRef(parentRef); + invalidParent = tileAndPoly.failed(); + if (invalidParent || (grandpaRef != 0 && !m_nav.isValidPolyRef(grandpaRef))) { + // The polygon has disappeared during the sliced query, + // fail. + m_query.status = Status.FAILURE; + return Results.of(m_query.status, iter); + } + parentTile = tileAndPoly.result.Item1; + parentPoly = tileAndPoly.result.Item2; + } + + // decide whether to test raycast to previous nodes + bool tryLOS = false; + if ((m_query.options & DT_FINDPATH_ANY_ANGLE) != 0) { + if ((parentRef != 0) && (vDistSqr(parentNode.pos, bestNode.pos) < m_query.raycastLimitSqr)) { + tryLOS = true; + } + } + + for (int i = bestTile.polyLinks[bestPoly.index]; i != NavMesh.DT_NULL_LINK; i = bestTile.links[i].next) { + long neighbourRef = bestTile.links[i].refs; + + // Skip invalid ids and do not expand back to where we came + // from. + if (neighbourRef == 0 || neighbourRef == parentRef) { + continue; + } + + // Get neighbour poly and tile. + // The API input has been cheked already, skip checking internal + // data. + Tuple tileAndPolyUns = m_nav.getTileAndPolyByRefUnsafe(neighbourRef); + MeshTile neighbourTile = tileAndPolyUns.Item1; + Poly neighbourPoly = tileAndPolyUns.Item2; + + if (!m_query.filter.passFilter(neighbourRef, neighbourTile, neighbourPoly)) { + continue; + } + + // get the neighbor node + Node neighbourNode = m_nodePool.getNode(neighbourRef, 0); + + // do not expand to nodes that were already visited from the + // same parent + if (neighbourNode.pidx != 0 && neighbourNode.pidx == bestNode.pidx) { + continue; + } + + // If the node is visited the first time, calculate node + // position. + if (neighbourNode.flags == 0) { + Result midpod = getEdgeMidPoint(bestRef, bestPoly, bestTile, neighbourRef, neighbourPoly, + neighbourTile); + if (!midpod.failed()) { + neighbourNode.pos = midpod.result; + } + } + + // Calculate cost and heuristic. + float cost = 0; + float heuristic = 0; + + // raycast parent + bool foundShortCut = false; + if (tryLOS) { + Result rayHit = raycast(parentRef, parentNode.pos, neighbourNode.pos, m_query.filter, + DT_RAYCAST_USE_COSTS, grandpaRef); + if (rayHit.succeeded()) { + foundShortCut = rayHit.result.t >= 1.0f; + if (foundShortCut) { + // shortcut found using raycast. Using shorter cost + // instead + cost = parentNode.cost + rayHit.result.pathCost; + } + } + } + + // update move cost + if (!foundShortCut) { + // No shortcut found. + float curCost = m_query.filter.getCost(bestNode.pos, neighbourNode.pos, parentRef, parentTile, + parentPoly, bestRef, bestTile, bestPoly, neighbourRef, neighbourTile, neighbourPoly); + cost = bestNode.cost + curCost; + } + + // Special case for last node. + if (neighbourRef == m_query.endRef) { + float endCost = m_query.filter.getCost(neighbourNode.pos, m_query.endPos, bestRef, bestTile, + bestPoly, neighbourRef, neighbourTile, neighbourPoly, 0, null, null); + + cost = cost + endCost; + heuristic = 0; + } else { + heuristic = vDist(neighbourNode.pos, m_query.endPos) * H_SCALE; + } + + float total = cost + heuristic; + + // The node is already in open list and the new result is worse, + // skip. + if ((neighbourNode.flags & Node.DT_NODE_OPEN) != 0 && total >= neighbourNode.total) { + continue; + } + // The node is already visited and process, and the new result + // is worse, skip. + if ((neighbourNode.flags & Node.DT_NODE_CLOSED) != 0 && total >= neighbourNode.total) { + continue; + } + + // Add or update the node. + neighbourNode.pidx = foundShortCut ? bestNode.pidx : m_nodePool.getNodeIdx(bestNode); + neighbourNode.id = neighbourRef; + neighbourNode.flags = (neighbourNode.flags & ~(Node.DT_NODE_CLOSED | Node.DT_NODE_PARENT_DETACHED)); + neighbourNode.cost = cost; + neighbourNode.total = total; + if (foundShortCut) { + neighbourNode.flags = (neighbourNode.flags | Node.DT_NODE_PARENT_DETACHED); + } + + if ((neighbourNode.flags & Node.DT_NODE_OPEN) != 0) { + // Already in open, update node location. + m_openList.modify(neighbourNode); + } else { + // Put the node in open list. + neighbourNode.flags |= Node.DT_NODE_OPEN; + m_openList.push(neighbourNode); + } + + // Update nearest node to target so far. + if (heuristic < m_query.lastBestNodeCost) { + m_query.lastBestNodeCost = heuristic; + m_query.lastBestNode = neighbourNode; + } + } + } + + // Exhausted all nodes, but could not find path. + if (m_openList.isEmpty()) { + m_query.status = Status.PARTIAL_RESULT; + } + + return Results.of(m_query.status, iter); + } + + /// Finalizes and returns the results of a sliced path query. + /// @param[out] path An ordered list of polygon references representing the path. (Start to end.) + /// [(polyRef) * @p pathCount] + /// @returns The status flags for the query. + public override Result> finalizeSlicedFindPath() { + + List path = new(64); + if (m_query.status.isFailed()) { + // Reset query. + m_query = new QueryData(); + return Results.failure(path); + } + + if (m_query.startRef == m_query.endRef) { + // Special case: the search starts and ends at same poly. + path.Add(m_query.startRef); + } else { + // Reverse the path. + if (m_query.lastBestNode.id != m_query.endRef) { + m_query.status = Status.PARTIAL_RESULT; + } + + Node prev = null; + Node node = m_query.lastBestNode; + int prevRay = 0; + do { + Node next = m_nodePool.getNodeAtIdx(node.pidx); + node.pidx = m_nodePool.getNodeIdx(prev); + prev = node; + int nextRay = node.flags & Node.DT_NODE_PARENT_DETACHED; // keep track of whether parent is not adjacent + // (i.e. due to raycast shortcut) + node.flags = (node.flags & ~Node.DT_NODE_PARENT_DETACHED) | prevRay; // and store it in the reversed + // path's node + prevRay = nextRay; + node = next; + } while (node != null); + + // Store path + node = prev; + do { + Node next = m_nodePool.getNodeAtIdx(node.pidx); + if ((node.flags & Node.DT_NODE_PARENT_DETACHED) != 0) { + Result iresult = raycast(node.id, node.pos, next.pos, m_query.filter, 0, 0); + if (iresult.succeeded()) { + path.AddRange(iresult.result.path); + } + // raycast ends on poly boundary and the path might include the next poly boundary. + if (path[path.Count - 1] == next.id) { + path.RemoveAt(path.Count - 1); // remove to avoid duplicates + } + } else { + path.Add(node.id); + } + + node = next; + } while (node != null); + } + + Status status = m_query.status; + // Reset query. + m_query = new QueryData(); + + return Results.of(status, path); + } + + /// Finalizes and returns the results of an incomplete sliced path query, returning the path to the furthest + /// polygon on the existing path that was visited during the search. + /// @param[in] existing An array of polygon references for the existing path. + /// @param[in] existingSize The number of polygon in the @p existing array. + /// @param[out] path An ordered list of polygon references representing the path. (Start to end.) + /// [(polyRef) * @p pathCount] + /// @returns The status flags for the query. + public override Result> finalizeSlicedFindPathPartial(List existing) { + + List path = new(64); + if (null == existing || existing.Count <= 0) { + return Results.failure(path); + } + if (m_query.status.isFailed()) { + // Reset query. + m_query = new QueryData(); + return Results.failure(path); + } + if (m_query.startRef == m_query.endRef) { + // Special case: the search starts and ends at same poly. + path.Add(m_query.startRef); + } else { + // Find furthest existing node that was visited. + Node prev = null; + Node node = null; + for (int i = existing.Count - 1; i >= 0; --i) { + node = m_nodePool.findNode(existing[i]); + if (node != null) { + break; + } + } + + if (node == null) { + m_query.status = Status.PARTIAL_RESULT; + node = m_query.lastBestNode; + } + + // Reverse the path. + int prevRay = 0; + do { + Node next = m_nodePool.getNodeAtIdx(node.pidx); + node.pidx = m_nodePool.getNodeIdx(prev); + prev = node; + int nextRay = node.flags & Node.DT_NODE_PARENT_DETACHED; // keep track of whether parent is not adjacent + // (i.e. due to raycast shortcut) + node.flags = (node.flags & ~Node.DT_NODE_PARENT_DETACHED) | prevRay; // and store it in the reversed + // path's node + prevRay = nextRay; + node = next; + } while (node != null); + + // Store path + node = prev; + do { + Node next = m_nodePool.getNodeAtIdx(node.pidx); + if ((node.flags & Node.DT_NODE_PARENT_DETACHED) != 0) { + Result iresult = raycast(node.id, node.pos, next.pos, m_query.filter, 0, 0); + if (iresult.succeeded()) { + path.AddRange(iresult.result.path); + } + // raycast ends on poly boundary and the path might include the next poly boundary. + if (path[path.Count - 1] == next.id) { + path.RemoveAt(path.Count - 1); // remove to avoid duplicates + } + } else { + path.Add(node.id); + } + + node = next; + } while (node != null); + } + Status status = m_query.status; + // Reset query. + m_query = new QueryData(); + + return Results.of(status, path); + } + + public override Result findDistanceToWall(long startRef, float[] centerPos, float maxRadius, + QueryFilter filter) { + + // Validate input + if (!m_nav.isValidPolyRef(startRef) || null == centerPos || !vIsFinite(centerPos) || maxRadius < 0 + || !float.IsFinite(maxRadius) || null == filter) { + return Results.invalidParam(); + } + + m_nodePool.clear(); + m_openList.clear(); + + Node startNode = m_nodePool.getNode(startRef); + vCopy(startNode.pos, centerPos); + startNode.pidx = 0; + startNode.cost = 0; + startNode.total = 0; + startNode.id = startRef; + startNode.flags = Node.DT_NODE_OPEN; + m_openList.push(startNode); + + float radiusSqr = sqr(maxRadius); + float[] hitPos = new float[3]; + VectorPtr bestvj = null; + VectorPtr bestvi = null; + while (!m_openList.isEmpty()) { + Node bestNode = m_openList.pop(); + bestNode.flags &= ~Node.DT_NODE_OPEN; + bestNode.flags |= Node.DT_NODE_CLOSED; + + // Get poly and tile. + // The API input has been cheked already, skip checking internal data. + long bestRef = bestNode.id; + Tuple tileAndPoly = m_nav.getTileAndPolyByRefUnsafe(bestRef); + MeshTile bestTile = tileAndPoly.Item1; + Poly bestPoly = tileAndPoly.Item2; + + // Get parent poly and tile. + long parentRef = 0; + if (bestNode.pidx != 0) { + parentRef = m_nodePool.getNodeAtIdx(bestNode.pidx).id; + } + + // Hit test walls. + for (int i = 0, j = bestPoly.vertCount - 1; i < bestPoly.vertCount; j = i++) { + // Skip non-solid edges. + if ((bestPoly.neis[j] & NavMesh.DT_EXT_LINK) != 0) { + // Tile border. + bool solid = true; + for (int k = bestTile.polyLinks[bestPoly.index]; k != NavMesh.DT_NULL_LINK; k = bestTile.links[k].next) { + Link link = bestTile.links[k]; + if (link.edge == j) { + if (link.refs != 0) { + Tuple linkTileAndPoly = m_nav.getTileAndPolyByRefUnsafe(link.refs); + MeshTile neiTile = linkTileAndPoly.Item1; + Poly neiPoly = linkTileAndPoly.Item2; + if (filter.passFilter(link.refs, neiTile, neiPoly)) { + solid = false; + } + } + break; + } + } + if (!solid) { + continue; + } + } else if (bestPoly.neis[j] != 0) { + // Internal edge + int idx = (bestPoly.neis[j] - 1); + long refs = m_nav.getPolyRefBase(bestTile) | idx; + if (filter.passFilter(refs, bestTile, bestTile.data.polys[idx])) { + continue; + } + } + + // Calc distance to the edge. + int vj = bestPoly.verts[j] * 3; + int vi = bestPoly.verts[i] * 3; + Tuple distseg = distancePtSegSqr2D(centerPos, bestTile.data.verts, vj, vi); + float distSqr = distseg.Item1; + float tseg = distseg.Item2; + + // Edge is too far, skip. + if (distSqr > radiusSqr) { + continue; + } + + // Hit wall, update radius. + radiusSqr = distSqr; + // Calculate hit pos. + hitPos[0] = bestTile.data.verts[vj] + (bestTile.data.verts[vi] - bestTile.data.verts[vj]) * tseg; + hitPos[1] = bestTile.data.verts[vj + 1] + + (bestTile.data.verts[vi + 1] - bestTile.data.verts[vj + 1]) * tseg; + hitPos[2] = bestTile.data.verts[vj + 2] + + (bestTile.data.verts[vi + 2] - bestTile.data.verts[vj + 2]) * tseg; + bestvj = new VectorPtr(bestTile.data.verts, vj); + bestvi = new VectorPtr(bestTile.data.verts, vi); + } + + for (int i = bestTile.polyLinks[bestPoly.index]; i != NavMesh.DT_NULL_LINK; i = bestTile.links[i].next) { + Link link = bestTile.links[i]; + long neighbourRef = link.refs; + // Skip invalid neighbours and do not follow back to parent. + if (neighbourRef == 0 || neighbourRef == parentRef) { + continue; + } + + // Expand to neighbour. + Tuple neighbourTileAndPoly = m_nav.getTileAndPolyByRefUnsafe(neighbourRef); + MeshTile neighbourTile = neighbourTileAndPoly.Item1; + Poly neighbourPoly = neighbourTileAndPoly.Item2; + + // Skip off-mesh connections. + if (neighbourPoly.getType() == Poly.DT_POLYTYPE_OFFMESH_CONNECTION) { + continue; + } + + // Calc distance to the edge. + int va = bestPoly.verts[link.edge] * 3; + int vb = bestPoly.verts[(link.edge + 1) % bestPoly.vertCount] * 3; + Tuple distseg = distancePtSegSqr2D(centerPos, bestTile.data.verts, va, vb); + float distSqr = distseg.Item1; + // If the circle is not touching the next polygon, skip it. + if (distSqr > radiusSqr) { + continue; + } + + if (!filter.passFilter(neighbourRef, neighbourTile, neighbourPoly)) { + continue; + } + + Node neighbourNode = m_nodePool.getNode(neighbourRef); + + if ((neighbourNode.flags & Node.DT_NODE_CLOSED) != 0) { + continue; + } + + // Cost + if (neighbourNode.flags == 0) { + Result midPoint = getEdgeMidPoint(bestRef, bestPoly, bestTile, neighbourRef, neighbourPoly, + neighbourTile); + if (midPoint.succeeded()) { + neighbourNode.pos = midPoint.result; + } + } + + float total = bestNode.total + vDist(bestNode.pos, neighbourNode.pos); + + // The node is already in open list and the new result is worse, skip. + if ((neighbourNode.flags & Node.DT_NODE_OPEN) != 0 && total >= neighbourNode.total) { + continue; + } + + neighbourNode.id = neighbourRef; + neighbourNode.flags = (neighbourNode.flags & ~Node.DT_NODE_CLOSED); + neighbourNode.pidx = m_nodePool.getNodeIdx(bestNode); + neighbourNode.total = total; + + if ((neighbourNode.flags & Node.DT_NODE_OPEN) != 0) { + m_openList.modify(neighbourNode); + } else { + neighbourNode.flags |= Node.DT_NODE_OPEN; + m_openList.push(neighbourNode); + } + } + } + + // Calc hit normal. + float[] hitNormal = new float[3]; + if (bestvi != null && bestvj != null) { + float[] tangent = vSub(bestvi, bestvj); + hitNormal[0] = tangent[2]; + hitNormal[1] = 0; + hitNormal[2] = -tangent[0]; + vNormalize(hitNormal); + } + return Results.success(new FindDistanceToWallResult((float) Math.Sqrt(radiusSqr), hitPos, hitNormal)); + } +} diff --git a/src/DotRecast.Detour/Link.cs b/src/DotRecast.Detour/Link.cs new file mode 100644 index 0000000..0ae0f23 --- /dev/null +++ b/src/DotRecast.Detour/Link.cs @@ -0,0 +1,41 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Detour; + +/** + * Defines a link between polygons. + * + * @note This structure is rarely if ever used by the end user. + * @see MeshTile + */ +public class Link { + /** Neighbour reference. (The neighbor that is linked to.) */ + public long refs; + /** Index of the next link. */ + public int next; + /** Index of the polygon edge that owns this link. */ + public int edge; + /** If a boundary link, defines on which side the link is. */ + public int side; + /** If a boundary link, defines the minimum sub-edge area. */ + public int bmin; + /** If a boundary link, defines the maximum sub-edge area. */ + public int bmax; + +} diff --git a/src/DotRecast.Detour/MeshData.cs b/src/DotRecast.Detour/MeshData.cs new file mode 100644 index 0000000..f6967b5 --- /dev/null +++ b/src/DotRecast.Detour/MeshData.cs @@ -0,0 +1,45 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Detour; + +public class MeshData { + + /** The tile header. */ + public MeshHeader header; + /** The tile vertices. [Size: MeshHeader::vertCount] */ + public float[] verts; + /** The tile polygons. [Size: MeshHeader::polyCount] */ + public Poly[] polys; + /** The tile's detail sub-meshes. [Size: MeshHeader::detailMeshCount] */ + public PolyDetail[] detailMeshes; + /** The detail mesh's unique vertices. [(x, y, z) * MeshHeader::detailVertCount] */ + public float[] detailVerts; + /** + * The detail mesh's triangles. [(vertA, vertB, vertC) * MeshHeader::detailTriCount] See DetailTriEdgeFlags and + * NavMesh::getDetailTriEdgeFlags. + */ + public int[] detailTris; + /** + * The tile bounding volume nodes. [Size: MeshHeader::bvNodeCount] (Will be null if bounding volumes are disabled.) + */ + public BVNode[] bvTree; + /** The tile off-mesh connections. [Size: MeshHeader::offMeshConCount] */ + public OffMeshConnection[] offMeshCons; + +} \ No newline at end of file diff --git a/src/DotRecast.Detour/MeshHeader.cs b/src/DotRecast.Detour/MeshHeader.cs new file mode 100644 index 0000000..2563d2e --- /dev/null +++ b/src/DotRecast.Detour/MeshHeader.cs @@ -0,0 +1,78 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Detour; + +/** Provides high level information related to a dtMeshTile object. */ +public class MeshHeader { + /** A magic number used to detect compatibility of navigation tile data. */ + public const int DT_NAVMESH_MAGIC = 'D' << 24 | 'N' << 16 | 'A' << 8 | 'V'; + /** A version number used to detect compatibility of navigation tile data. */ + public const int DT_NAVMESH_VERSION = 7; + public const int DT_NAVMESH_VERSION_RECAST4J_FIRST = 0x8807; + public const int DT_NAVMESH_VERSION_RECAST4J_NO_POLY_FIRSTLINK = 0x8808; + public const int DT_NAVMESH_VERSION_RECAST4J_32BIT_BVTREE = 0x8809; + public const int DT_NAVMESH_VERSION_RECAST4J_LAST = 0x8809; + /** A magic number used to detect the compatibility of navigation tile states. */ + public const int DT_NAVMESH_STATE_MAGIC = 'D' << 24 | 'N' << 16 | 'M' << 8 | 'S'; + /** A version number used to detect compatibility of navigation tile states. */ + public const int DT_NAVMESH_STATE_VERSION = 1; + + /** Tile magic number. (Used to identify the data format.) */ + public int magic; + /** Tile data format version number. */ + public int version; + /** The x-position of the tile within the dtNavMesh tile grid. (x, y, layer) */ + public int x; + /** The y-position of the tile within the dtNavMesh tile grid. (x, y, layer) */ + public int y; + /** The layer of the tile within the dtNavMesh tile grid. (x, y, layer) */ + public int layer; + /** The user defined id of the tile. */ + public int userId; + /** The number of polygons in the tile. */ + public int polyCount; + /** The number of vertices in the tile. */ + public int vertCount; + /** The number of allocated links. */ + public int maxLinkCount; + /** The number of sub-meshes in the detail mesh. */ + public int detailMeshCount; + /** The number of unique vertices in the detail mesh. (In addition to the polygon vertices.) */ + public int detailVertCount; + /** The number of triangles in the detail mesh. */ + public int detailTriCount; + /** The number of bounding volume nodes. (Zero if bounding volumes are disabled.) */ + public int bvNodeCount; + /** The number of off-mesh connections. */ + public int offMeshConCount; + /** The index of the first polygon which is an off-mesh connection. */ + public int offMeshBase; + /** The height of the agents using the tile. */ + public float walkableHeight; + /** The radius of the agents using the tile. */ + public float walkableRadius; + /** The maximum climb height of the agents using the tile. */ + public float walkableClimb; + /** The minimum bounds of the tile's AABB. [(x, y, z)] */ + public readonly float[] bmin = new float[3]; + /** The maximum bounds of the tile's AABB. [(x, y, z)] */ + public readonly float[] bmax = new float[3]; + /** The bounding volume quantization factor. */ + public float bvQuantFactor; +} diff --git a/src/DotRecast.Detour/MeshTile.cs b/src/DotRecast.Detour/MeshTile.cs new file mode 100644 index 0000000..aba3dfc --- /dev/null +++ b/src/DotRecast.Detour/MeshTile.cs @@ -0,0 +1,45 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.Collections.Generic; + +namespace DotRecast.Detour; + +/** + * Defines a navigation mesh tile. + */ +public class MeshTile { + public readonly int index; + /** Counter describing modifications to the tile. */ + public int salt; + /** The tile data. */ + public MeshData data; + public int[] polyLinks; + /** The tile links. */ + public readonly List links = new(); + /** Index to the next free link. */ + public int linksFreeList = NavMesh.DT_NULL_LINK; // FIXME: Remove + /** Tile flags. (See: #dtTileFlags) */ + public int flags; + + public MeshTile(int index) { + this.index = index; + } + +} diff --git a/src/DotRecast.Detour/MoveAlongSurfaceResult.cs b/src/DotRecast.Detour/MoveAlongSurfaceResult.cs new file mode 100644 index 0000000..cad392e --- /dev/null +++ b/src/DotRecast.Detour/MoveAlongSurfaceResult.cs @@ -0,0 +1,45 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.Collections.Generic; + +namespace DotRecast.Detour; + + +public class MoveAlongSurfaceResult { + + /** The result position of the mover. [(x, y, z)] */ + private readonly float[] resultPos; + /** The reference ids of the polygons visited during the move. */ + private readonly List visited; + + public MoveAlongSurfaceResult(float[] resultPos, List visited) { + this.resultPos = resultPos; + this.visited = visited; + } + + public float[] getResultPos() { + return resultPos; + } + + public List getVisited() { + return visited; + } + +} diff --git a/src/DotRecast.Detour/NavMesh.cs b/src/DotRecast.Detour/NavMesh.cs new file mode 100644 index 0000000..41793cb --- /dev/null +++ b/src/DotRecast.Detour/NavMesh.cs @@ -0,0 +1,1426 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using DotRecast.Core; + +namespace DotRecast.Detour; + +using static DetourCommon; + +public class NavMesh { + + public const int DT_SALT_BITS = 16; + public const int DT_TILE_BITS = 28; + public const int DT_POLY_BITS = 20; + public const int DT_DETAIL_EDGE_BOUNDARY = 0x01; + + /// A flag that indicates that an entity links to an external entity. + /// (E.g. A polygon edge is a portal that links to another polygon.) + public const int DT_EXT_LINK = 0x8000; + + /// A value that indicates the entity does not link to anything. + public const int DT_NULL_LINK = unchecked((int)0xffffffff); + + /// A flag that indicates that an off-mesh connection can be traversed in + /// both directions. (Is bidirectional.) + public const int DT_OFFMESH_CON_BIDIR = 1; + + /// The maximum number of user defined area ids. + public const int DT_MAX_AREAS = 64; + + /// Limit raycasting during any angle pahfinding + /// The limit is given as a multiple of the character radius + public const float DT_RAY_CAST_LIMIT_PROPORTIONS = 50.0f; + + private readonly NavMeshParams m_params; /// < Current initialization params. TODO: do not store this info twice. + private readonly float[] m_orig; /// < Origin of the tile (0,0) + // float m_orig[3]; ///< Origin of the tile (0,0) + float m_tileWidth, m_tileHeight; /// < Dimensions of each tile. + int m_maxTiles; /// < Max number of tiles. + private readonly int m_tileLutMask; /// < Tile hash lookup mask. + private readonly Dictionary> posLookup = new(); + private readonly LinkedList availableTiles = new(); + private readonly MeshTile[] m_tiles; /// < List of tiles. + /** The maximum number of vertices per navigation polygon. */ + private readonly int m_maxVertPerPoly; + private int m_tileCount; + + /** + * The maximum number of tiles supported by the navigation mesh. + * + * @return The maximum number of tiles supported by the navigation mesh. + */ + public int getMaxTiles() { + return m_maxTiles; + } + + /** + * Returns tile in the tile array. + */ + public MeshTile getTile(int i) { + return m_tiles[i]; + } + + /** + * Gets the polygon reference for the tile's base polygon. + * + * @param tile + * The tile. + * @return The polygon reference for the base polygon in the specified tile. + */ + public long getPolyRefBase(MeshTile tile) { + if (tile == null) { + return 0; + } + int it = tile.index; + return encodePolyId(tile.salt, it, 0); + } + + /** + * Derives a standard polygon reference. + * + * @note This function is generally meant for internal use only. + * @param salt + * The tile's salt value. + * @param it + * The index of the tile. + * @param ip + * The index of the polygon within the tile. + * @return encoded polygon reference + */ + public static long encodePolyId(int salt, int it, int ip) { + return (((long) salt) << (DT_POLY_BITS + DT_TILE_BITS)) | ((long) it << DT_POLY_BITS) | ip; + } + + /// Decodes a standard polygon reference. + /// @note This function is generally meant for internal use only. + /// @param[in] ref The polygon reference to decode. + /// @param[out] salt The tile's salt value. + /// @param[out] it The index of the tile. + /// @param[out] ip The index of the polygon within the tile. + /// @see #encodePolyId + static int[] decodePolyId(long refs) { + int salt; + int it; + int ip; + long saltMask = (1L << DT_SALT_BITS) - 1; + long tileMask = (1L << DT_TILE_BITS) - 1; + long polyMask = (1L << DT_POLY_BITS) - 1; + salt = (int) ((refs >> (DT_POLY_BITS + DT_TILE_BITS)) & saltMask); + it = (int) ((refs >> DT_POLY_BITS) & tileMask); + ip = (int) (refs & polyMask); + return new int[] { salt, it, ip }; + } + + /// Extracts a tile's salt value from the specified polygon reference. + /// @note This function is generally meant for internal use only. + /// @param[in] ref The polygon reference. + /// @see #encodePolyId + static int decodePolyIdSalt(long refs) { + long saltMask = (1L << DT_SALT_BITS) - 1; + return (int) ((refs >> (DT_POLY_BITS + DT_TILE_BITS)) & saltMask); + } + + /// Extracts the tile's index from the specified polygon reference. + /// @note This function is generally meant for internal use only. + /// @param[in] ref The polygon reference. + /// @see #encodePolyId + public static int decodePolyIdTile(long refs) { + long tileMask = (1L << DT_TILE_BITS) - 1; + return (int) ((refs >> DT_POLY_BITS) & tileMask); + } + + /// Extracts the polygon's index (within its tile) from the specified + /// polygon reference. + /// @note This function is generally meant for internal use only. + /// @param[in] ref The polygon reference. + /// @see #encodePolyId + static int decodePolyIdPoly(long refs) { + long polyMask = (1L << DT_POLY_BITS) - 1; + return (int) (refs & polyMask); + } + + private int allocLink(MeshTile tile) { + if (tile.linksFreeList == DT_NULL_LINK) { + Link link = new Link(); + link.next = DT_NULL_LINK; + tile.links.Add(link); + return tile.links.Count - 1; + } + int linkIdx = tile.linksFreeList; + tile.linksFreeList = tile.links[linkIdx].next; + return linkIdx; + } + + private void freeLink(MeshTile tile, int link) { + tile.links[link].next = tile.linksFreeList; + tile.linksFreeList = link; + } + + /** + * Calculates the tile grid location for the specified world position. + * + * @param pos + * The world position for the query. [(x, y, z)] + * @return 2-element int array with (tx,ty) tile location + */ + public int[] calcTileLoc(float[] pos) { + int tx = (int) Math.Floor((pos[0] - m_orig[0]) / m_tileWidth); + int ty = (int) Math.Floor((pos[2] - m_orig[2]) / m_tileHeight); + return new int[] { tx, ty }; + } + + public Result> getTileAndPolyByRef(long refs) { + if (refs == 0) { + return Results.invalidParam>("ref = 0"); + } + int[] saltitip = decodePolyId(refs); + int salt = saltitip[0]; + int it = saltitip[1]; + int ip = saltitip[2]; + if (it >= m_maxTiles) { + return Results.invalidParam>("tile > m_maxTiles"); + } + if (m_tiles[it].salt != salt || m_tiles[it].data.header == null) { + return Results.invalidParam>("Invalid salt or header"); + } + if (ip >= m_tiles[it].data.header.polyCount) { + return Results.invalidParam>("poly > polyCount"); + } + return Results.success(Tuple.Create(m_tiles[it], m_tiles[it].data.polys[ip])); + } + + /// @par + /// + /// @warning Only use this function if it is known that the provided polygon + /// reference is valid. This function is faster than #getTileAndPolyByRef, + /// but + /// it does not validate the reference. + public Tuple getTileAndPolyByRefUnsafe(long refs) { + int[] saltitip = decodePolyId(refs); + int it = saltitip[1]; + int ip = saltitip[2]; + return Tuple.Create(m_tiles[it], m_tiles[it].data.polys[ip]); + } + + public bool isValidPolyRef(long refs) { + if (refs == 0) { + return false; + } + int[] saltitip = decodePolyId(refs); + int salt = saltitip[0]; + int it = saltitip[1]; + int ip = saltitip[2]; + if (it >= m_maxTiles) { + return false; + } + if (m_tiles[it].salt != salt || m_tiles[it].data == null) { + return false; + } + if (ip >= m_tiles[it].data.header.polyCount) { + return false; + } + return true; + } + + public NavMeshParams getParams() { + return m_params; + } + + public NavMesh(MeshData data, int maxVertsPerPoly, int flags) + : this(getNavMeshParams(data), maxVertsPerPoly) + { + addTile(data, flags, 0); + } + + public NavMesh(NavMeshParams option, int maxVertsPerPoly) { + m_params = option; + m_orig = option.orig; + m_tileWidth = option.tileWidth; + m_tileHeight = option.tileHeight; + // Init tiles + m_maxTiles = option.maxTiles; + m_maxVertPerPoly = maxVertsPerPoly; + m_tileLutMask = Math.Max(1, nextPow2(option.maxTiles)) - 1; + m_tiles = new MeshTile[m_maxTiles]; + for (int i = 0; i < m_maxTiles; i++) { + m_tiles[i] = new MeshTile(i); + m_tiles[i].salt = 1; + availableTiles.AddLast(m_tiles[i]); + } + + } + + private static NavMeshParams getNavMeshParams(MeshData data) { + NavMeshParams option = new NavMeshParams(); + vCopy(option.orig, data.header.bmin); + option.tileWidth = data.header.bmax[0] - data.header.bmin[0]; + option.tileHeight = data.header.bmax[2] - data.header.bmin[2]; + option.maxTiles = 1; + option.maxPolys = data.header.polyCount; + return option; + } + + // TODO: These methods are duplicates from dtNavMeshQuery, but are needed + // for off-mesh connection finding. + + List queryPolygonsInTile(MeshTile tile, float[] qmin, float[] qmax) { + List polys = new(); + if (tile.data.bvTree != null) { + int nodeIndex = 0; + float[] tbmin = tile.data.header.bmin; + float[] tbmax = tile.data.header.bmax; + float qfac = tile.data.header.bvQuantFactor; + // Calculate quantized box + int[] bmin = new int[3]; + int[] bmax = new int[3]; + // dtClamp query box to world box. + float minx = clamp(qmin[0], tbmin[0], tbmax[0]) - tbmin[0]; + float miny = clamp(qmin[1], tbmin[1], tbmax[1]) - tbmin[1]; + float minz = clamp(qmin[2], tbmin[2], tbmax[2]) - tbmin[2]; + float maxx = clamp(qmax[0], tbmin[0], tbmax[0]) - tbmin[0]; + float maxy = clamp(qmax[1], tbmin[1], tbmax[1]) - tbmin[1]; + float maxz = clamp(qmax[2], tbmin[2], tbmax[2]) - tbmin[2]; + // Quantize + bmin[0] = (int) (qfac * minx) & 0x7ffffffe; + bmin[1] = (int) (qfac * miny) & 0x7ffffffe; + bmin[2] = (int) (qfac * minz) & 0x7ffffffe; + bmax[0] = (int) (qfac * maxx + 1) | 1; + bmax[1] = (int) (qfac * maxy + 1) | 1; + bmax[2] = (int) (qfac * maxz + 1) | 1; + + // Traverse tree + long @base = getPolyRefBase(tile); + int end = tile.data.header.bvNodeCount; + while (nodeIndex < end) { + BVNode node = tile.data.bvTree[nodeIndex]; + bool overlap = overlapQuantBounds(bmin, bmax, node.bmin, node.bmax); + bool isLeafNode = node.i >= 0; + + if (isLeafNode && overlap) { + polys.Add(@base | node.i); + } + + if (overlap || isLeafNode) { + nodeIndex++; + } else { + int escapeIndex = -node.i; + nodeIndex += escapeIndex; + } + } + + return polys; + } else { + float[] bmin = new float[3]; + float[] bmax = new float[3]; + long @base = getPolyRefBase(tile); + for (int i = 0; i < tile.data.header.polyCount; ++i) { + Poly p = tile.data.polys[i]; + // Do not return off-mesh connection polygons. + if (p.getType() == Poly.DT_POLYTYPE_OFFMESH_CONNECTION) { + continue; + } + // Calc polygon bounds. + int v = p.verts[0] * 3; + vCopy(bmin, tile.data.verts, v); + vCopy(bmax, tile.data.verts, v); + for (int j = 1; j < p.vertCount; ++j) { + v = p.verts[j] * 3; + vMin(bmin, tile.data.verts, v); + vMax(bmax, tile.data.verts, v); + } + if (overlapBounds(qmin, qmax, bmin, bmax)) { + polys.Add(@base | i); + } + } + return polys; + } + } + + public long updateTile(MeshData data, int flags) { + long refs = getTileRefAt(data.header.x, data.header.y, data.header.layer); + refs = removeTile(refs); + return addTile(data, flags, refs); + } + + /// Adds a tile to the navigation mesh. + /// @param[in] data Data for the new tile mesh. (See: #dtCreateNavMeshData) + /// @param[in] dataSize Data size of the new tile mesh. + /// @param[in] flags Tile flags. (See: #dtTileFlags) + /// @param[in] lastRef The desired reference for the tile. (When reloading a + /// tile.) [opt] [Default: 0] + /// @param[out] result The tile reference. (If the tile was succesfully + /// added.) [opt] + /// @return The status flags for the operation. + /// @par + /// + /// The add operation will fail if the data is in the wrong format, the + /// allocated tile + /// space is full, or there is a tile already at the specified reference. + /// + /// The lastRef parameter is used to restore a tile with the same tile + /// reference it had previously used. In this case the #long's for the + /// tile will be restored to the same values they were before the tile was + /// removed. + /// + /// The nav mesh assumes exclusive access to the data passed and will make + /// changes to the dynamic portion of the data. For that reason the data + /// should not be reused in other nav meshes until the tile has been successfully + /// removed from this nav mesh. + /// + /// @see dtCreateNavMeshData, #removeTile + public long addTile(MeshData data, int flags, long lastRef) { + // Make sure the data is in right format. + MeshHeader header = data.header; + + // Make sure the location is free. + if (getTileAt(header.x, header.y, header.layer) != null) { + throw new Exception("Tile already exists"); + } + + // Allocate a tile. + MeshTile tile = null; + if (lastRef == 0) { + // Make sure we could allocate a tile. + if (0 == availableTiles.Count) { + throw new Exception("Could not allocate a tile"); + } + + tile = availableTiles.First?.Value; + availableTiles.RemoveFirst(); + m_tileCount++; + } else { + // Try to relocate the tile to specific index with same salt. + int tileIndex = decodePolyIdTile(lastRef); + if (tileIndex >= m_maxTiles) { + throw new Exception("Tile index too high"); + } + // Try to find the specific tile id from the free list. + MeshTile target = m_tiles[tileIndex]; + // Remove from freelist + if (!availableTiles.Remove(target)) { + // Could not find the correct location. + throw new Exception("Could not find tile"); + } + tile = target; + // Restore salt. + tile.salt = decodePolyIdSalt(lastRef); + } + + tile.data = data; + tile.flags = flags; + tile.links.Clear(); + tile.polyLinks = new int[data.polys.Length]; + Array.Fill(tile.polyLinks, NavMesh.DT_NULL_LINK); + + // Insert tile into the position lut. + getTileListByPos(header.x, header.y).Add(tile); + + // Patch header pointers. + + // If there are no items in the bvtree, reset the tree pointer. + if (tile.data.bvTree != null && tile.data.bvTree.Length == 0) { + tile.data.bvTree = null; + } + + // Init tile. + + connectIntLinks(tile); + // Base off-mesh connections to their starting polygons and connect connections inside the tile. + baseOffMeshLinks(tile); + connectExtOffMeshLinks(tile, tile, -1); + + // Connect with layers in current tile. + List neis = getTilesAt(header.x, header.y); + for (int j = 0; j < neis.Count; ++j) { + if (neis[j] == tile) { + continue; + } + connectExtLinks(tile, neis[j], -1); + connectExtLinks(neis[j], tile, -1); + connectExtOffMeshLinks(tile, neis[j], -1); + connectExtOffMeshLinks(neis[j], tile, -1); + } + + // Connect with neighbour tiles. + for (int i = 0; i < 8; ++i) { + neis = getNeighbourTilesAt(header.x, header.y, i); + for (int j = 0; j < neis.Count; ++j) { + connectExtLinks(tile, neis[j], i); + connectExtLinks(neis[j], tile, oppositeTile(i)); + connectExtOffMeshLinks(tile, neis[j], i); + connectExtOffMeshLinks(neis[j], tile, oppositeTile(i)); + } + } + + return getTileRef(tile); + } + + /// Removes the specified tile from the navigation mesh. + /// @param[in] ref The reference of the tile to remove. + /// @param[out] data Data associated with deleted tile. + /// @param[out] dataSize Size of the data associated with deleted tile. + /// + /// This function returns the data for the tile so that, if desired, + /// it can be added back to the navigation mesh at a later point. + /// + /// @see #addTile + public long removeTile(long refs) { + if (refs == 0) { + return 0; + } + int tileIndex = decodePolyIdTile(refs); + int tileSalt = decodePolyIdSalt(refs); + if (tileIndex >= m_maxTiles) { + throw new Exception("Invalid tile index"); + } + MeshTile tile = m_tiles[tileIndex]; + if (tile.salt != tileSalt) { + throw new Exception("Invalid tile salt"); + } + + // Remove tile from hash lookup. + getTileListByPos(tile.data.header.x, tile.data.header.y).Remove(tile); + + // Remove connections to neighbour tiles. + // Create connections with neighbour tiles. + + // Disconnect from other layers in current tile. + List nneis = getTilesAt(tile.data.header.x, tile.data.header.y); + foreach (MeshTile j in nneis) { + if (j == tile) { + continue; + } + unconnectLinks(j, tile); + } + + // Disconnect from neighbour tiles. + for (int i = 0; i < 8; ++i) { + nneis = getNeighbourTilesAt(tile.data.header.x, tile.data.header.y, i); + foreach (MeshTile j in nneis) { + unconnectLinks(j, tile); + } + } + // Reset tile. + tile.data = null; + + tile.flags = 0; + tile.links.Clear(); + tile.linksFreeList=NavMesh.DT_NULL_LINK; + + // Update salt, salt should never be zero. + tile.salt = (tile.salt + 1) & ((1 << DT_SALT_BITS) - 1); + if (tile.salt == 0) { + tile.salt++; + } + + // Add to free list. + availableTiles.AddFirst(tile); + m_tileCount--; + return getTileRef(tile); + } + + /// Builds internal polygons links for a tile. + void connectIntLinks(MeshTile tile) { + if (tile == null) { + return; + } + + long @base = getPolyRefBase(tile); + + for (int i = 0; i < tile.data.header.polyCount; ++i) { + Poly poly = tile.data.polys[i]; + tile.polyLinks[poly.index] = DT_NULL_LINK; + + if (poly.getType() == Poly.DT_POLYTYPE_OFFMESH_CONNECTION) { + continue; + } + + // Build edge links backwards so that the links will be + // in the linked list from lowest index to highest. + for (int j = poly.vertCount - 1; j >= 0; --j) { + // Skip hard and non-internal edges. + if (poly.neis[j] == 0 || (poly.neis[j] & DT_EXT_LINK) != 0) { + continue; + } + + int idx = allocLink(tile); + Link link = tile.links[idx]; + link.refs = @base | (poly.neis[j] - 1); + link.edge = j; + link.side = 0xff; + link.bmin = link.bmax = 0; + // Add to linked list. + link.next = tile.polyLinks[poly.index]; + tile.polyLinks[poly.index] = idx; + } + } + } + + void unconnectLinks(MeshTile tile, MeshTile target) { + if (tile == null || target == null) { + return; + } + + int targetNum = decodePolyIdTile(getTileRef(target)); + + for (int i = 0; i < tile.data.header.polyCount; ++i) { + Poly poly = tile.data.polys[i]; + int j = tile.polyLinks[poly.index]; + int pj = DT_NULL_LINK; + while (j != DT_NULL_LINK) { + if (decodePolyIdTile(tile.links[j].refs) == targetNum) { + // Remove link. + int nj = tile.links[j].next; + if (pj == DT_NULL_LINK) { + tile.polyLinks[poly.index] = nj; + } else { + tile.links[pj].next = nj; + } + freeLink(tile, j); + j = nj; + } else { + // Advance + pj = j; + j = tile.links[j].next; + } + } + } + } + + void connectExtLinks(MeshTile tile, MeshTile target, int side) { + if (tile == null) { + return; + } + + // Connect border links. + for (int i = 0; i < tile.data.header.polyCount; ++i) { + Poly poly = tile.data.polys[i]; + + // Create new links. + // short m = DT_EXT_LINK | (short)side; + + int nv = poly.vertCount; + for (int j = 0; j < nv; ++j) { + // Skip non-portal edges. + if ((poly.neis[j] & DT_EXT_LINK) == 0) { + continue; + } + + int dir = poly.neis[j] & 0xff; + if (side != -1 && dir != side) { + continue; + } + + // Create new links + int va = poly.verts[j] * 3; + int vb = poly.verts[(j + 1) % nv] * 3; + IList> connectedPolys = findConnectingPolys(tile.data.verts, va, vb, target, + oppositeTile(dir)); + foreach (Tuple connectedPoly in connectedPolys) { + int idx = allocLink(tile); + Link link = tile.links[idx]; + link.refs = connectedPoly.Item1; + link.edge = j; + link.side = dir; + + link.next = tile.polyLinks[poly.index]; + tile.polyLinks[poly.index] = idx; + + // Compress portal limits to a byte value. + if (dir == 0 || dir == 4) { + float tmin = (connectedPoly.Item2 - tile.data.verts[va + 2]) + / (tile.data.verts[vb + 2] - tile.data.verts[va + 2]); + float tmax = (connectedPoly.Item3 - tile.data.verts[va + 2]) + / (tile.data.verts[vb + 2] - tile.data.verts[va + 2]); + if (tmin > tmax) { + float temp = tmin; + tmin = tmax; + tmax = temp; + } + link.bmin = (int)Math.Round(clamp(tmin, 0.0f, 1.0f) * 255.0f); + link.bmax = (int)Math.Round(clamp(tmax, 0.0f, 1.0f) * 255.0f); + } else if (dir == 2 || dir == 6) { + float tmin = (connectedPoly.Item2 - tile.data.verts[va]) + / (tile.data.verts[vb] - tile.data.verts[va]); + float tmax = (connectedPoly.Item3 - tile.data.verts[va]) + / (tile.data.verts[vb] - tile.data.verts[va]); + if (tmin > tmax) { + float temp = tmin; + tmin = tmax; + tmax = temp; + } + link.bmin = (int)Math.Round(clamp(tmin, 0.0f, 1.0f) * 255.0f); + link.bmax = (int)Math.Round(clamp(tmax, 0.0f, 1.0f) * 255.0f); + } + } + } + } + } + + void connectExtOffMeshLinks(MeshTile tile, MeshTile target, int side) { + if (tile == null) { + return; + } + + // Connect off-mesh links. + // We are interested on links which land from target tile to this tile. + int oppositeSide = (side == -1) ? 0xff : oppositeTile(side); + + for (int i = 0; i < target.data.header.offMeshConCount; ++i) { + OffMeshConnection targetCon = target.data.offMeshCons[i]; + if (targetCon.side != oppositeSide) { + continue; + } + + Poly targetPoly = target.data.polys[targetCon.poly]; + // Skip off-mesh connections which start location could not be + // connected at all. + if (target.polyLinks[targetPoly.index] == DT_NULL_LINK) { + continue; + } + + float[] ext = new float[] { targetCon.rad, target.data.header.walkableClimb, targetCon.rad }; + + // Find polygon to connect to. + float[] p = new float[3]; + p[0] = targetCon.pos[3]; + p[1] = targetCon.pos[4]; + p[2] = targetCon.pos[5]; + FindNearestPolyResult nearest = findNearestPolyInTile(tile, p, ext); + long refs = nearest.getNearestRef(); + if (refs == 0) { + continue; + } + float[] nearestPt = nearest.getNearestPos(); + // findNearestPoly may return too optimistic results, further check + // to make sure. + + if (sqr(nearestPt[0] - p[0]) + sqr(nearestPt[2] - p[2]) > sqr(targetCon.rad)) { + continue; + } + // Make sure the location is on current mesh. + target.data.verts[targetPoly.verts[1] * 3] = nearestPt[0]; + target.data.verts[targetPoly.verts[1] * 3 + 1] = nearestPt[1]; + target.data.verts[targetPoly.verts[1] * 3 + 2] = nearestPt[2]; + + // Link off-mesh connection to target poly. + int idx = allocLink(target); + Link link = target.links[idx]; + link.refs = refs; + link.edge = 1; + link.side = oppositeSide; + link.bmin = link.bmax = 0; + // Add to linked list. + link.next = target.polyLinks[targetPoly.index]; + target.polyLinks[targetPoly.index] = idx; + + // Link target poly to off-mesh connection. + if ((targetCon.flags & DT_OFFMESH_CON_BIDIR) != 0) { + int tidx = allocLink(tile); + int landPolyIdx = decodePolyIdPoly(refs); + Poly landPoly = tile.data.polys[landPolyIdx]; + link = tile.links[tidx]; + link.refs = getPolyRefBase(target) | (targetCon.poly); + link.edge = 0xff; + link.side = (side == -1 ? 0xff : side); + link.bmin = link.bmax = 0; + // Add to linked list. + link.next = tile.polyLinks[landPoly.index]; + tile.polyLinks[landPoly.index] = tidx; + } + } + } + + private IList> findConnectingPolys(float[] verts, int va, int vb, MeshTile tile, int side) { + if (tile == null) { + return ImmutableArray>.Empty; + } + List> result = new(); + float[] amin = new float[2]; + float[] amax = new float[2]; + calcSlabEndPoints(verts, va, vb, amin, amax, side); + float apos = getSlabCoord(verts, va, side); + + // Remove links pointing to 'side' and compact the links array. + float[] bmin = new float[2]; + float[] bmax = new float[2]; + int m = DT_EXT_LINK | side; + long @base = getPolyRefBase(tile); + + for (int i = 0; i < tile.data.header.polyCount; ++i) { + Poly poly = tile.data.polys[i]; + int nv = poly.vertCount; + for (int j = 0; j < nv; ++j) { + // Skip edges which do not point to the right side. + if (poly.neis[j] != m) { + continue; + } + int vc = poly.verts[j] * 3; + int vd = poly.verts[(j + 1) % nv] * 3; + float bpos = getSlabCoord(tile.data.verts, vc, side); + // Segments are not close enough. + if (Math.Abs(apos - bpos) > 0.01f) { + continue; + } + + // Check if the segments touch. + calcSlabEndPoints(tile.data.verts, vc, vd, bmin, bmax, side); + + if (!overlapSlabs(amin, amax, bmin, bmax, 0.01f, tile.data.header.walkableClimb)) { + continue; + } + + // Add return value. + result.Add(Tuple.Create(@base | i, Math.Max(amin[0], bmin[0]), Math.Min(amax[0], bmax[0]))); + break; + } + } + return result; + } + + static float getSlabCoord(float[] verts, int va, int side) { + if (side == 0 || side == 4) { + return verts[va]; + } else if (side == 2 || side == 6) { + return verts[va + 2]; + } + return 0; + } + + static void calcSlabEndPoints(float[] verts, int va, int vb, float[] bmin, float[] bmax, int side) { + if (side == 0 || side == 4) { + if (verts[va + 2] < verts[vb + 2]) { + bmin[0] = verts[va + 2]; + bmin[1] = verts[va + 1]; + bmax[0] = verts[vb + 2]; + bmax[1] = verts[vb + 1]; + } else { + bmin[0] = verts[vb + 2]; + bmin[1] = verts[vb + 1]; + bmax[0] = verts[va + 2]; + bmax[1] = verts[va + 1]; + } + } else if (side == 2 || side == 6) { + if (verts[va + 0] < verts[vb + 0]) { + bmin[0] = verts[va + 0]; + bmin[1] = verts[va + 1]; + bmax[0] = verts[vb + 0]; + bmax[1] = verts[vb + 1]; + } else { + bmin[0] = verts[vb + 0]; + bmin[1] = verts[vb + 1]; + bmax[0] = verts[va + 0]; + bmax[1] = verts[va + 1]; + } + } + } + + bool overlapSlabs(float[] amin, float[] amax, float[] bmin, float[] bmax, float px, float py) { + // Check for horizontal overlap. + // The segment is shrunken a little so that slabs which touch + // at end points are not connected. + float minx = Math.Max(amin[0] + px, bmin[0] + px); + float maxx = Math.Min(amax[0] - px, bmax[0] - px); + if (minx > maxx) { + return false; + } + + // Check vertical overlap. + float ad = (amax[1] - amin[1]) / (amax[0] - amin[0]); + float ak = amin[1] - ad * amin[0]; + float bd = (bmax[1] - bmin[1]) / (bmax[0] - bmin[0]); + float bk = bmin[1] - bd * bmin[0]; + float aminy = ad * minx + ak; + float amaxy = ad * maxx + ak; + float bminy = bd * minx + bk; + float bmaxy = bd * maxx + bk; + float dmin = bminy - aminy; + float dmax = bmaxy - amaxy; + + // Crossing segments always overlap. + if (dmin * dmax < 0) { + return true; + } + + // Check for overlap at endpoints. + float thr = (py * 2) * (py * 2); + if (dmin * dmin <= thr || dmax * dmax <= thr) { + return true; + } + + return false; + } + + /** + * Builds internal polygons links for a tile. + * + * @param tile + */ + void baseOffMeshLinks(MeshTile tile) { + if (tile == null) { + return; + } + + long @base = getPolyRefBase(tile); + + // Base off-mesh connection start points. + for (int i = 0; i < tile.data.header.offMeshConCount; ++i) { + OffMeshConnection con = tile.data.offMeshCons[i]; + Poly poly = tile.data.polys[con.poly]; + + float[] ext = new float[] { con.rad, tile.data.header.walkableClimb, con.rad }; + + // Find polygon to connect to. + FindNearestPolyResult nearestPoly = findNearestPolyInTile(tile, con.pos, ext); + long refs = nearestPoly.getNearestRef(); + if (refs == 0) { + continue; + } + float[] p = con.pos; // First vertex + float[] nearestPt = nearestPoly.getNearestPos(); + // findNearestPoly may return too optimistic results, further check + // to make sure. + if (sqr(nearestPt[0] - p[0]) + sqr(nearestPt[2] - p[2]) > sqr(con.rad)) { + continue; + } + // Make sure the location is on current mesh. + tile.data.verts[poly.verts[0] * 3] = nearestPt[0]; + tile.data.verts[poly.verts[0] * 3 + 1] = nearestPt[1]; + tile.data.verts[poly.verts[0] * 3 + 2] = nearestPt[2]; + + // Link off-mesh connection to target poly. + int idx = allocLink(tile); + Link link = tile.links[idx]; + link.refs = refs; + link.edge = 0; + link.side = 0xff; + link.bmin = link.bmax = 0; + // Add to linked list. + link.next = tile.polyLinks[poly.index]; + tile.polyLinks[poly.index] = idx; + + // Start end-point is always connect back to off-mesh connection. + int tidx = allocLink(tile); + int landPolyIdx = decodePolyIdPoly(refs); + Poly landPoly = tile.data.polys[landPolyIdx]; + link = tile.links[tidx]; + link.refs = @base | (con.poly); + link.edge = 0xff; + link.side = 0xff; + link.bmin = link.bmax = 0; + // Add to linked list. + link.next = tile.polyLinks[landPoly.index]; + tile.polyLinks[landPoly.index] = tidx; + } + } + + /** + * Returns closest point on polygon. + * + * @param ref + * @param pos + * @return + */ + float[] closestPointOnDetailEdges(MeshTile tile, Poly poly, float[] pos, bool onlyBoundary) { + int ANY_BOUNDARY_EDGE = (DT_DETAIL_EDGE_BOUNDARY << 0) | (DT_DETAIL_EDGE_BOUNDARY << 2) + | (DT_DETAIL_EDGE_BOUNDARY << 4); + int ip = poly.index; + float dmin = float.MaxValue; + float tmin = 0; + float[] pmin = null; + float[] pmax = null; + + if (tile.data.detailMeshes != null) { + + PolyDetail pd = tile.data.detailMeshes[ip]; + for (int i = 0; i < pd.triCount; i++) { + int ti = (pd.triBase + i) * 4; + int[] tris = tile.data.detailTris; + if (onlyBoundary && (tris[ti + 3] & ANY_BOUNDARY_EDGE) == 0) { + continue; + } + + float[][] v = new float[3][]; + for (int j = 0; j < 3; ++j) { + if (tris[ti + j] < poly.vertCount) { + int index = poly.verts[tris[ti + j]] * 3; + v[j] = new float[] { tile.data.verts[index], tile.data.verts[index + 1], + tile.data.verts[index + 2] }; + } else { + int index = (pd.vertBase + (tris[ti + j] - poly.vertCount)) * 3; + v[j] = new float[] { tile.data.detailVerts[index], tile.data.detailVerts[index + 1], + tile.data.detailVerts[index + 2] }; + } + } + + for (int k = 0, j = 2; k < 3; j = k++) { + if ((getDetailTriEdgeFlags(tris[ti + 3], j) & DT_DETAIL_EDGE_BOUNDARY) == 0 + && (onlyBoundary || tris[ti + j] < tris[ti + k])) { + // Only looking at boundary edges and this is internal, or + // this is an inner edge that we will see again or have already seen. + continue; + } + + Tuple dt = distancePtSegSqr2D(pos, v[j], v[k]); + float d = dt.Item1; + float t = dt.Item2; + if (d < dmin) { + dmin = d; + tmin = t; + pmin = v[j]; + pmax = v[k]; + } + } + } + } else { + float[][] v = ArrayUtils.Of(2, 3); + for (int j = 0; j < poly.vertCount; ++j) { + int k = (j + 1) % poly.vertCount; + v[0][0] = tile.data.verts[poly.verts[j] * 3]; + v[0][1] = tile.data.verts[poly.verts[j] * 3 + 1]; + v[0][2] = tile.data.verts[poly.verts[j] * 3 + 2]; + v[1][0] = tile.data.verts[poly.verts[k] * 3]; + v[1][1] = tile.data.verts[poly.verts[k] * 3 + 1]; + v[1][2] = tile.data.verts[poly.verts[k] * 3 + 2]; + + Tuple dt = distancePtSegSqr2D(pos, v[0], v[1]); + float d = dt.Item1; + float t = dt.Item2; + if (d < dmin) { + dmin = d; + tmin = t; + pmin = v[0]; + pmax = v[1]; + } + } + } + + return vLerp(pmin, pmax, tmin); + } + + public float? getPolyHeight(MeshTile tile, Poly poly, float[] pos) { + // Off-mesh connections do not have detail polys and getting height + // over them does not make sense. + if (poly.getType() == Poly.DT_POLYTYPE_OFFMESH_CONNECTION) { + return null; + } + + int ip = poly.index; + + float[] verts = new float[m_maxVertPerPoly * 3]; + int nv = poly.vertCount; + for (int i = 0; i < nv; ++i) { + Array.Copy(tile.data.verts, poly.verts[i] * 3, verts, i * 3, 3); + } + + if (!pointInPolygon(pos, verts, nv)) { + return null; + } + + // Find height at the location. + if (tile.data.detailMeshes != null) { + PolyDetail pd = tile.data.detailMeshes[ip]; + for (int j = 0; j < pd.triCount; ++j) { + int t = (pd.triBase + j) * 4; + float[][] v = new float[3][]; + for (int k = 0; k < 3; ++k) { + if (tile.data.detailTris[t + k] < poly.vertCount) { + int index = poly.verts[tile.data.detailTris[t + k]] * 3; + v[k] = new float[] { tile.data.verts[index], tile.data.verts[index + 1], + tile.data.verts[index + 2] }; + } else { + int index = (pd.vertBase + (tile.data.detailTris[t + k] - poly.vertCount)) * 3; + v[k] = new float[] { tile.data.detailVerts[index], tile.data.detailVerts[index + 1], + tile.data.detailVerts[index + 2] }; + } + } + float? h = closestHeightPointTriangle(pos, v[0], v[1], v[2]); + if (null != h) { + return h; + } + } + } else { + float[][] v = ArrayUtils.Of(3, 3); + v[0][0] = tile.data.verts[poly.verts[0] * 3]; + v[0][1] = tile.data.verts[poly.verts[0] * 3 + 1]; + v[0][2] = tile.data.verts[poly.verts[0] * 3 + 2]; + for (int j = 1; j < poly.vertCount - 1; ++j) { + for (int k = 0; k < 2; ++k) { + v[k + 1][0] = tile.data.verts[poly.verts[j + k] * 3]; + v[k + 1][1] = tile.data.verts[poly.verts[j + k] * 3 + 1]; + v[k + 1][2] = tile.data.verts[poly.verts[j + k] * 3 + 2]; + } + float? h = closestHeightPointTriangle(pos, v[0], v[1], v[2]); + if (null != h) { + return h; + } + } + } + + // If all triangle checks failed above (can happen with degenerate triangles + // or larger floating point values) the point is on an edge, so just select + // closest. This should almost never happen so the extra iteration here is + // ok. + float[] closest = closestPointOnDetailEdges(tile, poly, pos, false); + return closest[1]; + } + + public ClosestPointOnPolyResult closestPointOnPoly(long refs, float[] pos) { + Tuple tileAndPoly = getTileAndPolyByRefUnsafe(refs); + MeshTile tile = tileAndPoly.Item1; + Poly poly = tileAndPoly.Item2; + float[] closest = new float[3]; + vCopy(closest, pos); + float? h = getPolyHeight(tile, poly, pos); + if (null != h) { + closest[1] = h.Value; + return new ClosestPointOnPolyResult(true, closest); + } + + // Off-mesh connections don't have detail polygons. + if (poly.getType() == Poly.DT_POLYTYPE_OFFMESH_CONNECTION) { + int i = poly.verts[0] * 3; + float[] v0 = new float[] { tile.data.verts[i], tile.data.verts[i + 1], tile.data.verts[i + 2] }; + i = poly.verts[1] * 3; + float[] v1 = new float[] { tile.data.verts[i], tile.data.verts[i + 1], tile.data.verts[i + 2] }; + Tuple dt = distancePtSegSqr2D(pos, v0, v1); + return new ClosestPointOnPolyResult(false, vLerp(v0, v1, dt.Item2)); + } + // Outside poly that is not an offmesh connection. + return new ClosestPointOnPolyResult(false, closestPointOnDetailEdges(tile, poly, pos, true)); + } + + FindNearestPolyResult findNearestPolyInTile(MeshTile tile, float[] center, float[] extents) { + float[] nearestPt = null; + bool overPoly = false; + float[] bmin = vSub(center, extents); + float[] bmax = vAdd(center, extents); + + // Get nearby polygons from proximity grid. + List polys = queryPolygonsInTile(tile, bmin, bmax); + + // Find nearest polygon amongst the nearby polygons. + long nearest = 0; + float nearestDistanceSqr = float.MaxValue; + for (int i = 0; i < polys.Count; ++i) { + long refs = polys[i]; + float d; + ClosestPointOnPolyResult cpp = closestPointOnPoly(refs, center); + bool posOverPoly = cpp.isPosOverPoly(); + float[] closestPtPoly = cpp.getClosest(); + + // If a point is directly over a polygon and closer than + // climb height, favor that instead of straight line nearest point. + float[] diff = vSub(center, closestPtPoly); + if (posOverPoly) { + d = Math.Abs(diff[1]) - tile.data.header.walkableClimb; + d = d > 0 ? d * d : 0; + } else { + d = vLenSqr(diff); + } + if (d < nearestDistanceSqr) { + nearestPt = closestPtPoly; + nearestDistanceSqr = d; + nearest = refs; + overPoly = posOverPoly; + } + } + return new FindNearestPolyResult(nearest, nearestPt, overPoly); + } + + MeshTile getTileAt(int x, int y, int layer) { + foreach (MeshTile tile in getTileListByPos(x, y)) { + if (tile.data.header != null && tile.data.header.x == x && tile.data.header.y == y + && tile.data.header.layer == layer) { + return tile; + } + } + return null; + } + + List getNeighbourTilesAt(int x, int y, int side) { + int nx = x, ny = y; + switch (side) { + case 0: + nx++; + break; + case 1: + nx++; + ny++; + break; + case 2: + ny++; + break; + case 3: + nx--; + ny++; + break; + case 4: + nx--; + break; + case 5: + nx--; + ny--; + break; + case 6: + ny--; + break; + case 7: + nx++; + ny--; + break; + } + return getTilesAt(nx, ny); + } + + public List getTilesAt(int x, int y) { + List tiles = new(); + foreach (MeshTile tile in getTileListByPos(x, y)) { + if (tile.data.header != null && tile.data.header.x == x && tile.data.header.y == y) { + tiles.Add(tile); + } + } + return tiles; + } + + public long getTileRefAt(int x, int y, int layer) { + return getTileRef(getTileAt(x, y, layer)); + } + + public MeshTile getTileByRef(long refs) { + if (refs == 0) { + return null; + } + int tileIndex = decodePolyIdTile(refs); + int tileSalt = decodePolyIdSalt(refs); + if (tileIndex >= m_maxTiles) { + return null; + } + MeshTile tile = m_tiles[tileIndex]; + if (tile.salt != tileSalt) { + return null; + } + return tile; + } + + public long getTileRef(MeshTile tile) { + if (tile == null) { + return 0; + } + return encodePolyId(tile.salt, tile.index, 0); + } + + public static int computeTileHash(int x, int y, int mask) { + uint h1 = 0x8da6b343; // Large multiplicative constants; + uint h2 = 0xd8163841; // here arbitrarily chosen primes + uint n = h1 * (uint)x + h2 * (uint)y; + return (int)(n & mask); + } + + /// @par + /// + /// Off-mesh connections are stored in the navigation mesh as special + /// 2-vertex + /// polygons with a single edge. At least one of the vertices is expected to + /// be + /// inside a normal polygon. So an off-mesh connection is "entered" from a + /// normal polygon at one of its endpoints. This is the polygon identified + /// by + /// the prevRef parameter. + public Result> getOffMeshConnectionPolyEndPoints(long prevRef, long polyRef) { + if (polyRef == 0) { + return Results.invalidParam>("polyRef = 0"); + } + + // Get current polygon + int[] saltitip = decodePolyId(polyRef); + int salt = saltitip[0]; + int it = saltitip[1]; + int ip = saltitip[2]; + if (it >= m_maxTiles) { + return Results.invalidParam>("Invalid tile ID > max tiles"); + } + if (m_tiles[it].salt != salt || m_tiles[it].data.header == null) { + return Results.invalidParam>("Invalid salt or missing tile header"); + } + MeshTile tile = m_tiles[it]; + if (ip >= tile.data.header.polyCount) { + return Results.invalidParam>("Invalid poly ID > poly count"); + } + Poly poly = tile.data.polys[ip]; + + // Make sure that the current poly is indeed off-mesh link. + if (poly.getType() != Poly.DT_POLYTYPE_OFFMESH_CONNECTION) { + return Results.invalidParam>("Invalid poly type"); + } + + // Figure out which way to hand out the vertices. + int idx0 = 0, idx1 = 1; + + // Find link that points to first vertex. + for (int i = tile.polyLinks[poly.index]; i != DT_NULL_LINK; i = tile.links[i].next) { + if (tile.links[i].edge == 0) { + if (tile.links[i].refs != prevRef) { + idx0 = 1; + idx1 = 0; + } + break; + } + } + float[] startPos = new float[3]; + float[] endPos = new float[3]; + vCopy(startPos, tile.data.verts, poly.verts[idx0] * 3); + vCopy(endPos, tile.data.verts, poly.verts[idx1] * 3); + return Results.success(Tuple.Create(startPos, endPos)); + + } + + public int getMaxVertsPerPoly() { + return m_maxVertPerPoly; + } + + public int getTileCount() { + return m_tileCount; + } + + public Status setPolyFlags(long refs, int flags) { + if (refs == 0) { + return Status.FAILURE; + } + int[] saltTilePoly = decodePolyId(refs); + int salt = saltTilePoly[0]; + int it = saltTilePoly[1]; + int ip = saltTilePoly[2]; + if (it >= m_maxTiles) { + return Status.FAILURE_INVALID_PARAM; + } + if (m_tiles[it].salt != salt || m_tiles[it].data == null || m_tiles[it].data.header == null) { + return Status.FAILURE_INVALID_PARAM; + } + MeshTile tile = m_tiles[it]; + if (ip >= tile.data.header.polyCount) { + return Status.FAILURE_INVALID_PARAM; + } + Poly poly = tile.data.polys[ip]; + + // Change flags. + poly.flags = flags; + return Status.SUCCSESS; + } + + public Result getPolyFlags(long refs) { + if (refs == 0) { + return Results.failure(); + } + int[] saltTilePoly = decodePolyId(refs); + int salt = saltTilePoly[0]; + int it = saltTilePoly[1]; + int ip = saltTilePoly[2]; + if (it >= m_maxTiles) { + return Results.invalidParam(); + } + if (m_tiles[it].salt != salt || m_tiles[it].data == null || m_tiles[it].data.header == null) { + return Results.invalidParam(); + } + MeshTile tile = m_tiles[it]; + if (ip >= tile.data.header.polyCount) { + return Results.invalidParam(); + } + Poly poly = tile.data.polys[ip]; + + return Results.success(poly.flags); + } + + public Status setPolyArea(long refs, char area) { + if (refs == 0) { + return Status.FAILURE; + } + int[] saltTilePoly = decodePolyId(refs); + int salt = saltTilePoly[0]; + int it = saltTilePoly[1]; + int ip = saltTilePoly[2]; + if (it >= m_maxTiles) { + return Status.FAILURE; + } + if (m_tiles[it].salt != salt || m_tiles[it].data == null || m_tiles[it].data.header == null) { + return Status.FAILURE_INVALID_PARAM; + } + MeshTile tile = m_tiles[it]; + if (ip >= tile.data.header.polyCount) { + return Status.FAILURE_INVALID_PARAM; + } + Poly poly = tile.data.polys[ip]; + + poly.setArea(area); + + return Status.SUCCSESS; + } + + public Result getPolyArea(long refs) { + if (refs == 0) { + return Results.failure(); + } + int[] saltTilePoly = decodePolyId(refs); + int salt = saltTilePoly[0]; + int it = saltTilePoly[1]; + int ip = saltTilePoly[2]; + if (it >= m_maxTiles) { + return Results.invalidParam(); + } + if (m_tiles[it].salt != salt || m_tiles[it].data == null || m_tiles[it].data.header == null) { + return Results.invalidParam(); + } + MeshTile tile = m_tiles[it]; + if (ip >= tile.data.header.polyCount) { + return Results.invalidParam(); + } + Poly poly = tile.data.polys[ip]; + + return Results.success(poly.getArea()); + } + + /** + * Get flags for edge in detail triangle. + * + * @param triFlags + * The flags for the triangle (last component of detail vertices above). + * @param edgeIndex + * The index of the first vertex of the edge. For instance, if 0, + * @return flags for edge AB. + */ + public static int getDetailTriEdgeFlags(int triFlags, int edgeIndex) { + return (triFlags >> (edgeIndex * 2)) & 0x3; + } + + private List getTileListByPos(int x, int z) + { + var tileHash = computeTileHash(x, z, m_tileLutMask); + if (!posLookup.TryGetValue(tileHash, out var tiles)) + { + tiles = new(); + posLookup.Add(tileHash, tiles); + } + + return tiles; + } +} diff --git a/src/DotRecast.Detour/NavMeshBuilder.cs b/src/DotRecast.Detour/NavMeshBuilder.cs new file mode 100644 index 0000000..9f2e71a --- /dev/null +++ b/src/DotRecast.Detour/NavMeshBuilder.cs @@ -0,0 +1,601 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.Collections.Generic; + +namespace DotRecast.Detour; + +using static DetourCommon; + +public class NavMeshBuilder { + + const int MESH_NULL_IDX = 0xffff; + + public class BVItem { + public readonly int[] bmin = new int[3]; + public readonly int[] bmax = new int[3]; + public int i; + }; + + private class CompareItemX : IComparer { + + public int Compare(BVItem a, BVItem b) { + return a.bmin[0].CompareTo(b.bmin[0]); + } + + } + + private class CompareItemY : IComparer { + + public int Compare(BVItem a, BVItem b) { + return a.bmin[1].CompareTo(b.bmin[1]); + } + + } + + private class CompareItemZ : IComparer { + + public int Compare(BVItem a, BVItem b) { + return a.bmin[2].CompareTo(b.bmin[2]); + } + + } + + private static int[][] calcExtends(BVItem[] items, int nitems, int imin, int imax) { + int[] bmin = new int[3]; + int[] bmax = new int[3]; + bmin[0] = items[imin].bmin[0]; + bmin[1] = items[imin].bmin[1]; + bmin[2] = items[imin].bmin[2]; + + bmax[0] = items[imin].bmax[0]; + bmax[1] = items[imin].bmax[1]; + bmax[2] = items[imin].bmax[2]; + + for (int i = imin + 1; i < imax; ++i) { + BVItem it = items[i]; + if (it.bmin[0] < bmin[0]) + bmin[0] = it.bmin[0]; + if (it.bmin[1] < bmin[1]) + bmin[1] = it.bmin[1]; + if (it.bmin[2] < bmin[2]) + bmin[2] = it.bmin[2]; + + if (it.bmax[0] > bmax[0]) + bmax[0] = it.bmax[0]; + if (it.bmax[1] > bmax[1]) + bmax[1] = it.bmax[1]; + if (it.bmax[2] > bmax[2]) + bmax[2] = it.bmax[2]; + } + return new int[][] { bmin, bmax }; + } + + private static int longestAxis(int x, int y, int z) { + int axis = 0; + int maxVal = x; + if (y > maxVal) { + axis = 1; + maxVal = y; + } + if (z > maxVal) { + axis = 2; + maxVal = z; + } + return axis; + } + + public static int subdivide(BVItem[] items, int nitems, int imin, int imax, int curNode, BVNode[] nodes) { + int inum = imax - imin; + int icur = curNode; + + BVNode node = new BVNode(); + nodes[curNode++] = node; + + if (inum == 1) { + // Leaf + node.bmin[0] = items[imin].bmin[0]; + node.bmin[1] = items[imin].bmin[1]; + node.bmin[2] = items[imin].bmin[2]; + + node.bmax[0] = items[imin].bmax[0]; + node.bmax[1] = items[imin].bmax[1]; + node.bmax[2] = items[imin].bmax[2]; + + node.i = items[imin].i; + } else { + // Split + int[][] minmax = calcExtends(items, nitems, imin, imax); + node.bmin = minmax[0]; + node.bmax = minmax[1]; + + int axis = longestAxis(node.bmax[0] - node.bmin[0], node.bmax[1] - node.bmin[1], + node.bmax[2] - node.bmin[2]); + + if (axis == 0) { + // Sort along x-axis + Array.Sort(items, imin, inum, new CompareItemX()); + } else if (axis == 1) { + // Sort along y-axis + Array.Sort(items, imin, inum, new CompareItemY()); + } else { + // Sort along z-axis + Array.Sort(items, imin, inum, new CompareItemZ()); + } + + int isplit = imin + inum / 2; + + // Left + curNode = subdivide(items, nitems, imin, isplit, curNode, nodes); + // Right + curNode = subdivide(items, nitems, isplit, imax, curNode, nodes); + + int iescape = curNode - icur; + // Negative index means escape. + node.i = -iescape; + } + return curNode; + } + + private static int createBVTree(NavMeshDataCreateParams option, BVNode[] nodes) { + // Build tree + float quantFactor = 1 / option.cs; + BVItem[] items = new BVItem[option.polyCount]; + for (int i = 0; i < option.polyCount; i++) { + BVItem it = new BVItem(); + items[i] = it; + it.i = i; + // Calc polygon bounds. Use detail meshes if available. + if (option.detailMeshes != null) { + int vb = option.detailMeshes[i * 4 + 0]; + int ndv = option.detailMeshes[i * 4 + 1]; + float[] bmin = new float[3]; + float[] bmax = new float[3]; + int dv = vb * 3; + vCopy(bmin, option.detailVerts, dv); + vCopy(bmax, option.detailVerts, dv); + for (int j = 1; j < ndv; j++) { + vMin(bmin, option.detailVerts, dv + j * 3); + vMax(bmax, option.detailVerts, dv + j * 3); + } + + // BV-tree uses cs for all dimensions + it.bmin[0] = clamp((int) ((bmin[0] - option.bmin[0]) * quantFactor), 0, int.MaxValue); + it.bmin[1] = clamp((int) ((bmin[1] - option.bmin[1]) * quantFactor), 0, int.MaxValue); + it.bmin[2] = clamp((int) ((bmin[2] - option.bmin[2]) * quantFactor), 0, int.MaxValue); + + it.bmax[0] = clamp((int) ((bmax[0] - option.bmin[0]) * quantFactor), 0, int.MaxValue); + it.bmax[1] = clamp((int) ((bmax[1] - option.bmin[1]) * quantFactor), 0, int.MaxValue); + it.bmax[2] = clamp((int) ((bmax[2] - option.bmin[2]) * quantFactor), 0, int.MaxValue); + } else { + int p = i * option.nvp * 2; + it.bmin[0] = it.bmax[0] = option.verts[option.polys[p] * 3 + 0]; + it.bmin[1] = it.bmax[1] = option.verts[option.polys[p] * 3 + 1]; + it.bmin[2] = it.bmax[2] = option.verts[option.polys[p] * 3 + 2]; + + for (int j = 1; j < option.nvp; ++j) { + if (option.polys[p + j] == MESH_NULL_IDX) + break; + int x = option.verts[option.polys[p + j] * 3 + 0]; + int y = option.verts[option.polys[p + j] * 3 + 1]; + int z = option.verts[option.polys[p + j] * 3 + 2]; + + if (x < it.bmin[0]) + it.bmin[0] = x; + if (y < it.bmin[1]) + it.bmin[1] = y; + if (z < it.bmin[2]) + it.bmin[2] = z; + + if (x > it.bmax[0]) + it.bmax[0] = x; + if (y > it.bmax[1]) + it.bmax[1] = y; + if (z > it.bmax[2]) + it.bmax[2] = z; + } + // Remap y + it.bmin[1] = (int) Math.Floor(it.bmin[1] * option.ch * quantFactor); + it.bmax[1] = (int) Math.Ceiling(it.bmax[1] * option.ch * quantFactor); + } + } + + return subdivide(items, option.polyCount, 0, option.polyCount, 0, nodes); + } + + const int XP = 1 << 0; + const int ZP = 1 << 1; + const int XM = 1 << 2; + const int ZM = 1 << 3; + + public static int classifyOffMeshPoint(VectorPtr pt, float[] bmin, float[] bmax) { + + int outcode = 0; + outcode |= (pt.get(0) >= bmax[0]) ? XP : 0; + outcode |= (pt.get(2) >= bmax[2]) ? ZP : 0; + outcode |= (pt.get(0) < bmin[0]) ? XM : 0; + outcode |= (pt.get(2) < bmin[2]) ? ZM : 0; + + switch (outcode) { + case XP: + return 0; + case XP | ZP: + return 1; + case ZP: + return 2; + case XM | ZP: + return 3; + case XM: + return 4; + case XM | ZM: + return 5; + case ZM: + return 6; + case XP | ZM: + return 7; + } + + return 0xff; + } + + /** + * Builds navigation mesh tile data from the provided tile creation data. + * + * @param option + * Tile creation data. + * + * @return created tile data + */ + public static MeshData createNavMeshData(NavMeshDataCreateParams option) { + if (option.vertCount >= 0xffff) + return null; + if (option.vertCount == 0 || option.verts == null) + return null; + if (option.polyCount == 0 || option.polys == null) + return null; + + int nvp = option.nvp; + + // Classify off-mesh connection points. We store only the connections + // whose start point is inside the tile. + int[] offMeshConClass = null; + int storedOffMeshConCount = 0; + int offMeshConLinkCount = 0; + + if (option.offMeshConCount > 0) { + offMeshConClass = new int[option.offMeshConCount * 2]; + + // Find tight heigh bounds, used for culling out off-mesh start + // locations. + float hmin = float.MaxValue; + float hmax = -float.MaxValue; + + if (option.detailVerts != null && option.detailVertsCount != 0) { + for (int i = 0; i < option.detailVertsCount; ++i) { + float h = option.detailVerts[i * 3 + 1]; + hmin = Math.Min(hmin, h); + hmax = Math.Max(hmax, h); + } + } else { + for (int i = 0; i < option.vertCount; ++i) { + int iv = i * 3; + float h = option.bmin[1] + option.verts[iv + 1] * option.ch; + hmin = Math.Min(hmin, h); + hmax = Math.Max(hmax, h); + } + } + hmin -= option.walkableClimb; + hmax += option.walkableClimb; + float[] bmin = new float[3]; + float[] bmax = new float[3]; + vCopy(bmin, option.bmin); + vCopy(bmax, option.bmax); + bmin[1] = hmin; + bmax[1] = hmax; + + for (int i = 0; i < option.offMeshConCount; ++i) { + VectorPtr p0 = new VectorPtr(option.offMeshConVerts, (i * 2 + 0) * 3); + VectorPtr p1 = new VectorPtr(option.offMeshConVerts, (i * 2 + 1) * 3); + + offMeshConClass[i * 2 + 0] = classifyOffMeshPoint(p0, bmin, bmax); + offMeshConClass[i * 2 + 1] = classifyOffMeshPoint(p1, bmin, bmax); + + // Zero out off-mesh start positions which are not even + // potentially touching the mesh. + if (offMeshConClass[i * 2 + 0] == 0xff) { + if (p0.get(1) < bmin[1] || p0.get(1) > bmax[1]) + offMeshConClass[i * 2 + 0] = 0; + } + + // Count how many links should be allocated for off-mesh + // connections. + if (offMeshConClass[i * 2 + 0] == 0xff) + offMeshConLinkCount++; + if (offMeshConClass[i * 2 + 1] == 0xff) + offMeshConLinkCount++; + + if (offMeshConClass[i * 2 + 0] == 0xff) + storedOffMeshConCount++; + } + } + + // Off-mesh connectionss are stored as polygons, adjust values. + int totPolyCount = option.polyCount + storedOffMeshConCount; + int totVertCount = option.vertCount + storedOffMeshConCount * 2; + + // Find portal edges which are at tile borders. + int edgeCount = 0; + int portalCount = 0; + for (int i = 0; i < option.polyCount; ++i) { + int p = i * 2 * nvp; + for (int j = 0; j < nvp; ++j) { + if (option.polys[p + j] == MESH_NULL_IDX) + break; + edgeCount++; + + if ((option.polys[p + nvp + j] & 0x8000) != 0) { + int dir = option.polys[p + nvp + j] & 0xf; + if (dir != 0xf) + portalCount++; + } + } + } + + int maxLinkCount = edgeCount + portalCount * 2 + offMeshConLinkCount * 2; + + // Find unique detail vertices. + int uniqueDetailVertCount = 0; + int detailTriCount = 0; + if (option.detailMeshes != null) { + // Has detail mesh, count unique detail vertex count and use input + // detail tri count. + detailTriCount = option.detailTriCount; + for (int i = 0; i < option.polyCount; ++i) { + int p = i * nvp * 2; + int ndv = option.detailMeshes[i * 4 + 1]; + int nv = 0; + for (int j = 0; j < nvp; ++j) { + if (option.polys[p + j] == MESH_NULL_IDX) + break; + nv++; + } + ndv -= nv; + uniqueDetailVertCount += ndv; + } + } else { + // No input detail mesh, build detail mesh from nav polys. + uniqueDetailVertCount = 0; // No extra detail verts. + detailTriCount = 0; + for (int i = 0; i < option.polyCount; ++i) { + int p = i * nvp * 2; + int nv = 0; + for (int j = 0; j < nvp; ++j) { + if (option.polys[p + j] == MESH_NULL_IDX) + break; + nv++; + } + detailTriCount += nv - 2; + } + } + + int bvTreeSize = option.buildBvTree ? option.polyCount * 2 : 0; + MeshHeader header = new MeshHeader(); + float[] navVerts = new float[3 * totVertCount]; + Poly[] navPolys = new Poly[totPolyCount]; + PolyDetail[] navDMeshes = new PolyDetail[option.polyCount]; + float[] navDVerts = new float[3 * uniqueDetailVertCount]; + int[] navDTris = new int[4 * detailTriCount]; + BVNode[] navBvtree = new BVNode[bvTreeSize]; + OffMeshConnection[] offMeshCons = new OffMeshConnection[storedOffMeshConCount]; + + // Store header + header.magic = MeshHeader.DT_NAVMESH_MAGIC; + header.version = MeshHeader.DT_NAVMESH_VERSION; + header.x = option.tileX; + header.y = option.tileZ; + header.layer = option.tileLayer; + header.userId = option.userId; + header.polyCount = totPolyCount; + header.vertCount = totVertCount; + header.maxLinkCount = maxLinkCount; + vCopy(header.bmin, option.bmin); + vCopy(header.bmax, option.bmax); + header.detailMeshCount = option.polyCount; + header.detailVertCount = uniqueDetailVertCount; + header.detailTriCount = detailTriCount; + header.bvQuantFactor = 1.0f / option.cs; + header.offMeshBase = option.polyCount; + header.walkableHeight = option.walkableHeight; + header.walkableRadius = option.walkableRadius; + header.walkableClimb = option.walkableClimb; + header.offMeshConCount = storedOffMeshConCount; + header.bvNodeCount = bvTreeSize; + + int offMeshVertsBase = option.vertCount; + int offMeshPolyBase = option.polyCount; + + // Store vertices + // Mesh vertices + for (int i = 0; i < option.vertCount; ++i) { + int iv = i * 3; + int v = i * 3; + navVerts[v] = option.bmin[0] + option.verts[iv] * option.cs; + navVerts[v + 1] = option.bmin[1] + option.verts[iv + 1] * option.ch; + navVerts[v + 2] = option.bmin[2] + option.verts[iv + 2] * option.cs; + } + // Off-mesh link vertices. + int n = 0; + for (int i = 0; i < option.offMeshConCount; ++i) { + // Only store connections which start from this tile. + if (offMeshConClass[i * 2 + 0] == 0xff) { + int linkv = i * 2 * 3; + int v = (offMeshVertsBase + n * 2) * 3; + Array.Copy(option.offMeshConVerts, linkv, navVerts, v, 6); + n++; + } + } + + // Store polygons + // Mesh polys + int src = 0; + for (int i = 0; i < option.polyCount; ++i) { + Poly p = new Poly(i, nvp); + navPolys[i] = p; + p.vertCount = 0; + p.flags = option.polyFlags[i]; + p.setArea(option.polyAreas[i]); + p.setType(Poly.DT_POLYTYPE_GROUND); + for (int j = 0; j < nvp; ++j) { + if (option.polys[src + j] == MESH_NULL_IDX) + break; + p.verts[j] = option.polys[src + j]; + if ((option.polys[src + nvp + j] & 0x8000) != 0) { + // Border or portal edge. + int dir = option.polys[src + nvp + j] & 0xf; + if (dir == 0xf) // Border + p.neis[j] = 0; + else if (dir == 0) // Portal x- + p.neis[j] = NavMesh.DT_EXT_LINK | 4; + else if (dir == 1) // Portal z+ + p.neis[j] = NavMesh.DT_EXT_LINK | 2; + else if (dir == 2) // Portal x+ + p.neis[j] = NavMesh.DT_EXT_LINK | 0; + else if (dir == 3) // Portal z- + p.neis[j] = NavMesh.DT_EXT_LINK | 6; + } else { + // Normal connection + p.neis[j] = option.polys[src + nvp + j] + 1; + } + + p.vertCount++; + } + src += nvp * 2; + } + // Off-mesh connection vertices. + n = 0; + for (int i = 0; i < option.offMeshConCount; ++i) { + // Only store connections which start from this tile. + if (offMeshConClass[i * 2 + 0] == 0xff) { + Poly p = new Poly(offMeshPolyBase + n, nvp); + navPolys[offMeshPolyBase + n] = p; + p.vertCount = 2; + p.verts[0] = offMeshVertsBase + n * 2; + p.verts[1] = offMeshVertsBase + n * 2 + 1; + p.flags = option.offMeshConFlags[i]; + p.setArea(option.offMeshConAreas[i]); + p.setType(Poly.DT_POLYTYPE_OFFMESH_CONNECTION); + n++; + } + } + + // Store detail meshes and vertices. + // The nav polygon vertices are stored as the first vertices on each + // mesh. + // We compress the mesh data by skipping them and using the navmesh + // coordinates. + if (option.detailMeshes != null) { + int vbase = 0; + for (int i = 0; i < option.polyCount; ++i) { + PolyDetail dtl = new PolyDetail(); + navDMeshes[i] = dtl; + int vb = option.detailMeshes[i * 4 + 0]; + int ndv = option.detailMeshes[i * 4 + 1]; + int nv = navPolys[i].vertCount; + dtl.vertBase = vbase; + dtl.vertCount = (ndv - nv); + dtl.triBase = option.detailMeshes[i * 4 + 2]; + dtl.triCount = option.detailMeshes[i * 4 + 3]; + // Copy vertices except the first 'nv' verts which are equal to + // nav poly verts. + if (ndv - nv != 0) { + Array.Copy(option.detailVerts, (vb + nv) * 3, navDVerts, vbase * 3, 3 * (ndv - nv)); + vbase += ndv - nv; + } + } + // Store triangles. + Array.Copy(option.detailTris, 0, navDTris, 0, 4 * option.detailTriCount); + } else { + // Create dummy detail mesh by triangulating polys. + int tbase = 0; + for (int i = 0; i < option.polyCount; ++i) { + PolyDetail dtl = new PolyDetail(); + navDMeshes[i] = dtl; + int nv = navPolys[i].vertCount; + dtl.vertBase = 0; + dtl.vertCount = 0; + dtl.triBase = tbase; + dtl.triCount = (nv - 2); + // Triangulate polygon (local indices). + for (int j = 2; j < nv; ++j) { + int t = tbase * 4; + navDTris[t + 0] = 0; + navDTris[t + 1] = (j - 1); + navDTris[t + 2] = j; + // Bit for each edge that belongs to poly boundary. + navDTris[t + 3] = (1 << 2); + if (j == 2) + navDTris[t + 3] |= (1 << 0); + if (j == nv - 1) + navDTris[t + 3] |= (1 << 4); + tbase++; + } + } + } + + // Store and create BVtree. + // TODO: take detail mesh into account! use byte per bbox extent? + if (option.buildBvTree) { + // Do not set header.bvNodeCount set to make it work look exactly the same as in original Detour + header.bvNodeCount = createBVTree(option, navBvtree); + } + + // Store Off-Mesh connections. + n = 0; + for (int i = 0; i < option.offMeshConCount; ++i) { + // Only store connections which start from this tile. + if (offMeshConClass[i * 2 + 0] == 0xff) { + OffMeshConnection con = new OffMeshConnection(); + offMeshCons[n] = con; + con.poly = (offMeshPolyBase + n); + // Copy connection end-points. + int endPts = i * 2 * 3; + Array.Copy(option.offMeshConVerts, endPts, con.pos, 0, 6); + con.rad = option.offMeshConRad[i]; + con.flags = option.offMeshConDir[i] != 0 ? NavMesh.DT_OFFMESH_CON_BIDIR : 0; + con.side = offMeshConClass[i * 2 + 1]; + if (option.offMeshConUserID != null) + con.userId = option.offMeshConUserID[i]; + n++; + } + } + + MeshData nmd = new MeshData(); + nmd.header = header; + nmd.verts = navVerts; + nmd.polys = navPolys; + nmd.detailMeshes = navDMeshes; + nmd.detailVerts = navDVerts; + nmd.detailTris = navDTris; + nmd.bvTree = navBvtree; + nmd.offMeshCons = offMeshCons; + return nmd; + } + +} diff --git a/src/DotRecast.Detour/NavMeshDataCreateParams.cs b/src/DotRecast.Detour/NavMeshDataCreateParams.cs new file mode 100644 index 0000000..05a4b1c --- /dev/null +++ b/src/DotRecast.Detour/NavMeshDataCreateParams.cs @@ -0,0 +1,101 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Detour; + +/// Represents the source data used to build an navigation mesh tile. +public class NavMeshDataCreateParams { + + /// @name Polygon Mesh Attributes + /// Used to create the base navigation graph. + /// See #rcPolyMesh for details related to these attributes. + /// @{ + + public int[] verts; /// < The polygon mesh vertices. [(x, y, z) * #vertCount] [Unit: vx] + public int vertCount; /// < The number vertices in the polygon mesh. [Limit: >= 3] + public int[] polys; /// < The polygon data. [Size: #polyCount * 2 * #nvp] + public int[] polyFlags; /// < The user defined flags assigned to each polygon. [Size: #polyCount] + public int[] polyAreas; /// < The user defined area ids assigned to each polygon. [Size: #polyCount] + public int polyCount; /// < Number of polygons in the mesh. [Limit: >= 1] + public int nvp; /// < Number maximum number of vertices per polygon. [Limit: >= 3] + + /// @} + /// @name Height Detail Attributes (Optional) + /// See #rcPolyMeshDetail for details related to these attributes. + /// @{ + + public int[] detailMeshes; /// < The height detail sub-mesh data. [Size: 4 * #polyCount] + public float[] detailVerts; /// < The detail mesh vertices. [Size: 3 * #detailVertsCount] [Unit: wu] + public int detailVertsCount; /// < The number of vertices in the detail mesh. + public int[] detailTris; /// < The detail mesh triangles. [Size: 4 * #detailTriCount] + public int detailTriCount; /// < The number of triangles in the detail mesh. + + /// @} + /// @name Off-Mesh Connections Attributes (Optional) + /// Used to define a custom point-to-point edge within the navigation graph, an + /// off-mesh connection is a user defined traversable connection made up to two vertices, + /// at least one of which resides within a navigation mesh polygon. + /// @{ + + /// Off-mesh connection vertices. [(ax, ay, az, bx, by, bz) * #offMeshConCount] [Unit: wu] + public float[] offMeshConVerts; + /// Off-mesh connection radii. [Size: #offMeshConCount] [Unit: wu] + public float[] offMeshConRad; + /// User defined flags assigned to the off-mesh connections. [Size: #offMeshConCount] + public int[] offMeshConFlags; + /// User defined area ids assigned to the off-mesh connections. [Size: #offMeshConCount] + public int[] offMeshConAreas; + /// The permitted travel direction of the off-mesh connections. [Size: #offMeshConCount] + /// + /// 0 = Travel only from endpoint A to endpoint B.
+ /// #DT_OFFMESH_CON_BIDIR = Bidirectional travel. + public int[] offMeshConDir; + /// The user defined ids of the off-mesh connection. [Size: #offMeshConCount] + public int[] offMeshConUserID; + /// The number of off-mesh connections. [Limit: >= 0] + public int offMeshConCount; + + /// @} + /// @name Tile Attributes + /// @note The tile grid/layer data can be left at zero if the destination is a single tile mesh. + /// @{ + + public int userId; /// < The user defined id of the tile. + public int tileX; /// < The tile's x-grid location within the multi-tile destination mesh. (Along the x-axis.) + public int tileZ; /// < The tile's y-grid location within the multi-tile desitation mesh. (Along the z-axis.) + public int tileLayer; /// < The tile's layer within the layered destination mesh. [Limit: >= 0] (Along the y-axis.) + public float[] bmin; /// < The minimum bounds of the tile. [(x, y, z)] [Unit: wu] + public float[] bmax; /// < The maximum bounds of the tile. [(x, y, z)] [Unit: wu] + + /// @} + /// @name General Configuration Attributes + /// @{ + + public float walkableHeight; /// < The agent height. [Unit: wu] + public float walkableRadius; /// < The agent radius. [Unit: wu] + public float walkableClimb; /// < The agent maximum traversable ledge. (Up/Down) [Unit: wu] + public float cs; /// < The xz-plane cell size of the polygon mesh. [Limit: > 0] [Unit: wu] + public float ch; /// < The y-axis cell height of the polygon mesh. [Limit: > 0] [Unit: wu] + + /// True if a bounding volume tree should be built for the tile. + /// @note The BVTree is not normally needed for layered navigation meshes. + public bool buildBvTree; + + /// @} + +} \ No newline at end of file diff --git a/src/DotRecast.Detour/NavMeshParams.cs b/src/DotRecast.Detour/NavMeshParams.cs new file mode 100644 index 0000000..324fab9 --- /dev/null +++ b/src/DotRecast.Detour/NavMeshParams.cs @@ -0,0 +1,38 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Detour; + +/** + * Configuration parameters used to define multi-tile navigation meshes. The values are used to allocate space during + * the initialization of a navigation mesh. + * + * @see NavMesh + */ +public class NavMeshParams { + /** The world space origin of the navigation mesh's tile space. [(x, y, z)] */ + public readonly float[] orig = new float[3]; + /** The width of each tile. (Along the x-axis.) */ + public float tileWidth; + /** The height of each tile. (Along the z-axis.) */ + public float tileHeight; + /** The maximum number of tiles the navigation mesh can contain. */ + public int maxTiles; + /** The maximum number of polygons each tile can contain. */ + public int maxPolys; +} diff --git a/src/DotRecast.Detour/NavMeshQuery.cs b/src/DotRecast.Detour/NavMeshQuery.cs new file mode 100644 index 0000000..b521fc1 --- /dev/null +++ b/src/DotRecast.Detour/NavMeshQuery.cs @@ -0,0 +1,3055 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace DotRecast.Detour; + +using static DetourCommon; +using static Node; + +public class NavMeshQuery { + + /** + * Use raycasts during pathfind to "shortcut" (raycast still consider costs) Options for + * NavMeshQuery::initSlicedFindPath and updateSlicedFindPath + */ + public const int DT_FINDPATH_ANY_ANGLE = 0x02; + + /** Raycast should calculate movement cost along the ray and fill RaycastHit::cost */ + public const int DT_RAYCAST_USE_COSTS = 0x01; + + /// Vertex flags returned by findStraightPath. + /** The vertex is the start position in the path. */ + public const int DT_STRAIGHTPATH_START = 0x01; + /** The vertex is the end position in the path. */ + public const int DT_STRAIGHTPATH_END = 0x02; + /** The vertex is the start of an off-mesh connection. */ + public const int DT_STRAIGHTPATH_OFFMESH_CONNECTION = 0x04; + + /// Options for findStraightPath. + public const int DT_STRAIGHTPATH_AREA_CROSSINGS = 0x01; /// < Add a vertex at every polygon edge crossing + /// where area changes. + public const int DT_STRAIGHTPATH_ALL_CROSSINGS = 0x02; /// < Add a vertex at every polygon edge crossing. + + protected readonly NavMesh m_nav; + protected readonly NodePool m_nodePool; + protected readonly NodeQueue m_openList; + protected QueryData m_query; /// < Sliced query state. + + public NavMeshQuery(NavMesh nav) { + m_nav = nav; + m_nodePool = new NodePool(); + m_openList = new NodeQueue(); + } + + public class FRand { + + private readonly Random r; + + public FRand() { + r = new Random(); + } + + public FRand(long seed) { + r = new Random((int)seed); // TODO : 랜덤 시드 확인 필요 + } + + public float frand() + { + return (float)r.NextDouble(); + } + } + + /** + * Returns random location on navmesh. Polygons are chosen weighted by area. The search runs in linear related to + * number of polygon. + * + * @param filter + * The polygon filter to apply to the query. + * @param frand + * Function returning a random number [0..1). + * @return Random location + */ + public Result findRandomPoint(QueryFilter filter, FRand frand) { + // Randomly pick one tile. Assume that all tiles cover roughly the same area. + if (null == filter || null == frand) { + return Results.invalidParam(); + } + MeshTile tile = null; + float tsum = 0.0f; + for (int i = 0; i < m_nav.getMaxTiles(); i++) { + MeshTile mt = m_nav.getTile(i); + if (mt == null || mt.data == null || mt.data.header == null) { + continue; + } + + // Choose random tile using reservoi sampling. + float area = 1.0f; // Could be tile area too. + tsum += area; + float u = frand.frand(); + if (u * tsum <= area) { + tile = mt; + } + } + if (tile == null) { + return Results.invalidParam("Tile not found"); + } + + // Randomly pick one polygon weighted by polygon area. + Poly poly = null; + long polyRef = 0; + long @base = m_nav.getPolyRefBase(tile); + + float areaSum = 0.0f; + for (int i = 0; i < tile.data.header.polyCount; ++i) { + Poly p = tile.data.polys[i]; + // Do not return off-mesh connection polygons. + if (p.getType() != Poly.DT_POLYTYPE_GROUND) { + continue; + } + // Must pass filter + long refs = @base | i; + if (!filter.passFilter(refs, tile, p)) { + continue; + } + + // Calc area of the polygon. + float polyArea = 0.0f; + for (int j = 2; j < p.vertCount; ++j) { + int va = p.verts[0] * 3; + int vb = p.verts[j - 1] * 3; + int vc = p.verts[j] * 3; + polyArea += triArea2D(tile.data.verts, va, vb, vc); + } + + // Choose random polygon weighted by area, using reservoi sampling. + areaSum += polyArea; + float u = frand.frand(); + if (u * areaSum <= polyArea) { + poly = p; + polyRef = refs; + } + } + + if (poly == null) { + return Results.invalidParam("Poly not found"); + } + + // Randomly pick point on polygon. + float[] verts = new float[3 * m_nav.getMaxVertsPerPoly()]; + float[] areas = new float[m_nav.getMaxVertsPerPoly()]; + Array.Copy(tile.data.verts, poly.verts[0] * 3, verts, 0, 3); + for (int j = 1; j < poly.vertCount; ++j) { + Array.Copy(tile.data.verts, poly.verts[j] * 3, verts, j * 3, 3); + } + + float s = frand.frand(); + float t = frand.frand(); + + float[] pt = randomPointInConvexPoly(verts, poly.vertCount, areas, s, t); + ClosestPointOnPolyResult closest = closestPointOnPoly(polyRef, pt).result; + return Results.success(new FindRandomPointResult(polyRef, closest.getClosest())); + } + + /** + * Returns random location on navmesh within the reach of specified location. Polygons are chosen weighted by area. + * The search runs in linear related to number of polygon. The location is not exactly constrained by the circle, + * but it limits the visited polygons. + * + * @param startRef + * The reference id of the polygon where the search starts. + * @param centerPos + * The center of the search circle. [(x, y, z)] + * @param maxRadius + * @param filter + * The polygon filter to apply to the query. + * @param frand + * Function returning a random number [0..1). + * @return Random location + */ + public Result findRandomPointAroundCircle(long startRef, float[] centerPos, float maxRadius, + QueryFilter filter, FRand frand) { + return findRandomPointAroundCircle(startRef, centerPos, maxRadius, filter, frand, PolygonByCircleConstraint.noop()); + } + + /** + * Returns random location on navmesh within the reach of specified location. Polygons are chosen weighted by area. + * The search runs in linear related to number of polygon. The location is strictly constrained by the circle. + * + * @param startRef + * The reference id of the polygon where the search starts. + * @param centerPos + * The center of the search circle. [(x, y, z)] + * @param maxRadius + * @param filter + * The polygon filter to apply to the query. + * @param frand + * Function returning a random number [0..1). + * @return Random location + */ + public Result findRandomPointWithinCircle(long startRef, float[] centerPos, float maxRadius, + QueryFilter filter, FRand frand) { + return findRandomPointAroundCircle(startRef, centerPos, maxRadius, filter, frand, PolygonByCircleConstraint.strict()); + } + + public Result findRandomPointAroundCircle(long startRef, float[] centerPos, float maxRadius, + QueryFilter filter, FRand frand, PolygonByCircleConstraint constraint) { + + // Validate input + if (!m_nav.isValidPolyRef(startRef) || null == centerPos || !vIsFinite(centerPos) || maxRadius < 0 + || !float.IsFinite(maxRadius) || null == filter || null == frand) { + return Results.invalidParam(); + } + + Tuple tileAndPoly = m_nav.getTileAndPolyByRefUnsafe(startRef); + MeshTile startTile = tileAndPoly.Item1; + Poly startPoly = tileAndPoly.Item2; + if (!filter.passFilter(startRef, startTile, startPoly)) { + return Results.invalidParam("Invalid start ref"); + } + + m_nodePool.clear(); + m_openList.clear(); + + Node startNode = m_nodePool.getNode(startRef); + vCopy(startNode.pos, centerPos); + startNode.pidx = 0; + startNode.cost = 0; + startNode.total = 0; + startNode.id = startRef; + startNode.flags = DT_NODE_OPEN; + m_openList.push(startNode); + + float radiusSqr = maxRadius * maxRadius; + float areaSum = 0.0f; + + Poly randomPoly = null; + long randomPolyRef = 0; + float[] randomPolyVerts = null; + + while (!m_openList.isEmpty()) { + Node bestNode = m_openList.pop(); + bestNode.flags &= ~DT_NODE_OPEN; + bestNode.flags |= DT_NODE_CLOSED; + // Get poly and tile. + // The API input has been cheked already, skip checking internal data. + long bestRef = bestNode.id; + Tuple bestTilePoly = m_nav.getTileAndPolyByRefUnsafe(bestRef); + MeshTile bestTile = bestTilePoly.Item1; + Poly bestPoly = bestTilePoly.Item2; + + // Place random locations on on ground. + if (bestPoly.getType() == Poly.DT_POLYTYPE_GROUND) { + // Calc area of the polygon. + float polyArea = 0.0f; + float[] polyVerts = new float[bestPoly.vertCount * 3]; + for (int j = 0; j < bestPoly.vertCount; ++j) { + Array.Copy(bestTile.data.verts, bestPoly.verts[j] * 3, polyVerts, j * 3, 3); + } + float[] constrainedVerts = constraint.aply(polyVerts, centerPos, maxRadius); + if (constrainedVerts != null) { + int vertCount = constrainedVerts.Length / 3; + for (int j = 2; j < vertCount; ++j) { + int va = 0; + int vb = (j - 1) * 3; + int vc = j * 3; + polyArea += triArea2D(constrainedVerts, va, vb, vc); + } + // Choose random polygon weighted by area, using reservoi sampling. + areaSum += polyArea; + float u = frand.frand(); + if (u * areaSum <= polyArea) { + randomPoly = bestPoly; + randomPolyRef = bestRef; + randomPolyVerts = constrainedVerts; + } + } + } + + // Get parent poly and tile. + long parentRef = 0; + if (bestNode.pidx != 0) { + parentRef = m_nodePool.getNodeAtIdx(bestNode.pidx).id; + } + + for (int i = bestTile.polyLinks[bestPoly.index]; i != NavMesh.DT_NULL_LINK; i = bestTile.links[i].next) { + Link link = bestTile.links[i]; + long neighbourRef = link.refs; + // Skip invalid neighbours and do not follow back to parent. + if (neighbourRef == 0 || neighbourRef == parentRef) { + continue; + } + + // Expand to neighbour + Tuple neighbourTilePoly = m_nav.getTileAndPolyByRefUnsafe(neighbourRef); + MeshTile neighbourTile = neighbourTilePoly.Item1; + Poly neighbourPoly = neighbourTilePoly.Item2; + + // Do not advance if the polygon is excluded by the filter. + if (!filter.passFilter(neighbourRef, neighbourTile, neighbourPoly)) { + continue; + } + + // Find edge and calc distance to the edge. + Result portalpoints = getPortalPoints(bestRef, bestPoly, bestTile, neighbourRef, + neighbourPoly, neighbourTile, 0, 0); + if (portalpoints.failed()) { + continue; + } + float[] va = portalpoints.result.left; + float[] vb = portalpoints.result.right; + + // If the circle is not touching the next polygon, skip it. + Tuple distseg = distancePtSegSqr2D(centerPos, va, vb); + float distSqr = distseg.Item1; + if (distSqr > radiusSqr) { + continue; + } + + Node neighbourNode = m_nodePool.getNode(neighbourRef); + + if ((neighbourNode.flags & Node.DT_NODE_CLOSED) != 0) { + continue; + } + + // Cost + if (neighbourNode.flags == 0) { + neighbourNode.pos = vLerp(va, vb, 0.5f); + } + + float total = bestNode.total + vDist(bestNode.pos, neighbourNode.pos); + + // The node is already in open list and the new result is worse, skip. + if ((neighbourNode.flags & Node.DT_NODE_OPEN) != 0 && total >= neighbourNode.total) { + continue; + } + + neighbourNode.id = neighbourRef; + neighbourNode.flags = (neighbourNode.flags & ~Node.DT_NODE_CLOSED); + neighbourNode.pidx = m_nodePool.getNodeIdx(bestNode); + neighbourNode.total = total; + + if ((neighbourNode.flags & Node.DT_NODE_OPEN) != 0) { + m_openList.modify(neighbourNode); + } else { + neighbourNode.flags = Node.DT_NODE_OPEN; + m_openList.push(neighbourNode); + } + } + } + + if (randomPoly == null) { + return Results.failure(); + } + + // Randomly pick point on polygon. + float s = frand.frand(); + float t = frand.frand(); + + float[] areas = new float[randomPolyVerts.Length / 3]; + float[] pt = randomPointInConvexPoly(randomPolyVerts, randomPolyVerts.Length / 3, areas, s, t); + ClosestPointOnPolyResult closest = closestPointOnPoly(randomPolyRef, pt).result; + return Results.success(new FindRandomPointResult(randomPolyRef, closest.getClosest())); + } + + ////////////////////////////////////////////////////////////////////////////////////////// + /// @par + /// + /// Uses the detail polygons to find the surface height. (Most accurate.) + /// + /// @p pos does not have to be within the bounds of the polygon or navigation mesh. + /// + /// See closestPointOnPolyBoundary() for a limited but faster option. + /// + /// Finds the closest point on the specified polygon. + /// @param[in] ref The reference id of the polygon. + /// @param[in] pos The position to check. [(x, y, z)] + /// @param[out] closest + /// @param[out] posOverPoly + /// @returns The status flags for the query. + public Result closestPointOnPoly(long refs, float[] pos) { + if (!m_nav.isValidPolyRef(refs) || null == pos || !vIsFinite(pos)) { + return Results.invalidParam(); + } + return Results.success(m_nav.closestPointOnPoly(refs, pos)); + } + + /// @par + /// + /// Much faster than closestPointOnPoly(). + /// + /// If the provided position lies within the polygon's xz-bounds (above or below), + /// then @p pos and @p closest will be equal. + /// + /// The height of @p closest will be the polygon boundary. The height detail is not used. + /// + /// @p pos does not have to be within the bounds of the polybon or the navigation mesh. + /// + /// Returns a point on the boundary closest to the source point if the source point is outside the + /// polygon's xz-bounds. + /// @param[in] ref The reference id to the polygon. + /// @param[in] pos The position to check. [(x, y, z)] + /// @param[out] closest The closest point. [(x, y, z)] + /// @returns The status flags for the query. + public Result closestPointOnPolyBoundary(long refs, float[] pos) { + + Result> tileAndPoly = m_nav.getTileAndPolyByRef(refs); + if (tileAndPoly.failed()) { + return Results.of(tileAndPoly.status, tileAndPoly.message); + } + MeshTile tile = tileAndPoly.result.Item1; + Poly poly = tileAndPoly.result.Item2; + if (tile == null) { + return Results.invalidParam("Invalid tile"); + } + + if (null == pos || !vIsFinite(pos)) { + return Results.invalidParam(); + } + // Collect vertices. + float[] verts = new float[m_nav.getMaxVertsPerPoly() * 3]; + float[] edged = new float[m_nav.getMaxVertsPerPoly()]; + float[] edget = new float[m_nav.getMaxVertsPerPoly()]; + int nv = poly.vertCount; + for (int i = 0; i < nv; ++i) { + Array.Copy(tile.data.verts, poly.verts[i] * 3, verts, i * 3, 3); + } + + float[] closest; + if (distancePtPolyEdgesSqr(pos, verts, nv, edged, edget)) { + closest = vCopy(pos); + } else { + // Point is outside the polygon, dtClamp to nearest edge. + float dmin = edged[0]; + int imin = 0; + for (int i = 1; i < nv; ++i) { + if (edged[i] < dmin) { + dmin = edged[i]; + imin = i; + } + } + int va = imin * 3; + int vb = ((imin + 1) % nv) * 3; + closest = vLerp(verts, va, vb, edget[imin]); + } + return Results.success(closest); + } + + /// @par + /// + /// Will return #DT_FAILURE if the provided position is outside the xz-bounds + /// of the polygon. + /// + /// Gets the height of the polygon at the provided position using the height detail. (Most accurate.) + /// @param[in] ref The reference id of the polygon. + /// @param[in] pos A position within the xz-bounds of the polygon. [(x, y, z)] + /// @param[out] height The height at the surface of the polygon. + /// @returns The status flags for the query. + public Result getPolyHeight(long refs, float[] pos) { + + Result> tileAndPoly = m_nav.getTileAndPolyByRef(refs); + if (tileAndPoly.failed()) { + return Results.of(tileAndPoly.status, tileAndPoly.message); + } + MeshTile tile = tileAndPoly.result.Item1; + Poly poly = tileAndPoly.result.Item2; + + if (null == pos || !vIsFinite2D(pos)) { + return Results.invalidParam(); + } + + // We used to return success for offmesh connections, but the + // getPolyHeight in DetourNavMesh does not do this, so special + // case it here. + if (poly.getType() == Poly.DT_POLYTYPE_OFFMESH_CONNECTION) { + int i = poly.verts[0] * 3; + float[] v0 = new float[] { tile.data.verts[i], tile.data.verts[i + 1], tile.data.verts[i + 2] }; + i = poly.verts[1] * 3; + float[] v1 = new float[] { tile.data.verts[i], tile.data.verts[i + 1], tile.data.verts[i + 2] }; + Tuple dt = distancePtSegSqr2D(pos, v0, v1); + return Results.success(v0[1] + (v1[1] - v0[1]) * dt.Item2); + } + float? height = m_nav.getPolyHeight(tile, poly, pos); + return null != height ? Results.success(height.Value) : Results.invalidParam(); + } + + /** + * Finds the polygon nearest to the specified center point. If center and nearestPt point to an equal position, + * isOverPoly will be true; however there's also a special case of climb height inside the polygon + * + * @param center + * The center of the search box. [(x, y, z)] + * @param halfExtents + * The search distance along each axis. [(x, y, z)] + * @param filter + * The polygon filter to apply to the query. + * @return FindNearestPolyResult containing nearestRef, nearestPt and overPoly + */ + public Result findNearestPoly(float[] center, float[] halfExtents, QueryFilter filter) { + + // Get nearby polygons from proximity grid. + FindNearestPolyQuery query = new FindNearestPolyQuery(this, center); + Status status = queryPolygons(center, halfExtents, filter, query); + if (status.isFailed()) { + return Results.of(status, ""); + } + + return Results.success(query.result()); + } + + // FIXME: (PP) duplicate? + protected void queryPolygonsInTile(MeshTile tile, float[] qmin, float[] qmax, QueryFilter filter, PolyQuery query) { + if (tile.data.bvTree != null) { + int nodeIndex = 0; + float[] tbmin = tile.data.header.bmin; + float[] tbmax = tile.data.header.bmax; + float qfac = tile.data.header.bvQuantFactor; + // Calculate quantized box + int[] bmin = new int[3]; + int[] bmax = new int[3]; + // dtClamp query box to world box. + float minx = clamp(qmin[0], tbmin[0], tbmax[0]) - tbmin[0]; + float miny = clamp(qmin[1], tbmin[1], tbmax[1]) - tbmin[1]; + float minz = clamp(qmin[2], tbmin[2], tbmax[2]) - tbmin[2]; + float maxx = clamp(qmax[0], tbmin[0], tbmax[0]) - tbmin[0]; + float maxy = clamp(qmax[1], tbmin[1], tbmax[1]) - tbmin[1]; + float maxz = clamp(qmax[2], tbmin[2], tbmax[2]) - tbmin[2]; + // Quantize + bmin[0] = (int) (qfac * minx) & 0x7ffffffe; + bmin[1] = (int) (qfac * miny) & 0x7ffffffe; + bmin[2] = (int) (qfac * minz) & 0x7ffffffe; + bmax[0] = (int) (qfac * maxx + 1) | 1; + bmax[1] = (int) (qfac * maxy + 1) | 1; + bmax[2] = (int) (qfac * maxz + 1) | 1; + + // Traverse tree + long @base = m_nav.getPolyRefBase(tile); + int end = tile.data.header.bvNodeCount; + while (nodeIndex < end) { + BVNode node = tile.data.bvTree[nodeIndex]; + bool overlap = overlapQuantBounds(bmin, bmax, node.bmin, node.bmax); + bool isLeafNode = node.i >= 0; + + if (isLeafNode && overlap) { + long refs = @base | node.i; + if (filter.passFilter(refs, tile, tile.data.polys[node.i])) { + query.process(tile, tile.data.polys[node.i], refs); + } + } + + if (overlap || isLeafNode) { + nodeIndex++; + } else { + int escapeIndex = -node.i; + nodeIndex += escapeIndex; + } + } + } else { + float[] bmin = new float[3]; + float[] bmax = new float[3]; + long @base = m_nav.getPolyRefBase(tile); + for (int i = 0; i < tile.data.header.polyCount; ++i) { + Poly p = tile.data.polys[i]; + // Do not return off-mesh connection polygons. + if (p.getType() == Poly.DT_POLYTYPE_OFFMESH_CONNECTION) { + continue; + } + long refs = @base | i; + if (!filter.passFilter(refs, tile, p)) { + continue; + } + // Calc polygon bounds. + int v = p.verts[0] * 3; + vCopy(bmin, tile.data.verts, v); + vCopy(bmax, tile.data.verts, v); + for (int j = 1; j < p.vertCount; ++j) { + v = p.verts[j] * 3; + vMin(bmin, tile.data.verts, v); + vMax(bmax, tile.data.verts, v); + } + if (overlapBounds(qmin, qmax, bmin, bmax)) { + query.process(tile, p, refs); + } + } + } + } + + /** + * Finds polygons that overlap the search box. + * + * If no polygons are found, the function will return with a polyCount of zero. + * + * @param center + * The center of the search box. [(x, y, z)] + * @param halfExtents + * The search distance along each axis. [(x, y, z)] + * @param filter + * The polygon filter to apply to the query. + * @return The reference ids of the polygons that overlap the query box. + */ + public Status queryPolygons(float[] center, float[] halfExtents, QueryFilter filter, PolyQuery query) { + if (null == center || !vIsFinite(center) || null == halfExtents || !vIsFinite(halfExtents) + || null == filter) { + return Status.FAILURE_INVALID_PARAM; + } + // Find tiles the query touches. + float[] bmin = vSub(center, halfExtents); + float[] bmax = vAdd(center, halfExtents); + foreach (var t in queryTiles(center, halfExtents)) { + queryPolygonsInTile(t, bmin, bmax, filter, query); + } + return Status.SUCCSESS; + } + + /** + * Finds tiles that overlap the search box. + */ + public IList queryTiles(float[] center, float[] halfExtents) { + if (null == center || !vIsFinite(center) || null == halfExtents || !vIsFinite(halfExtents)) { + return ImmutableArray.Empty; + } + float[] bmin = vSub(center, halfExtents); + float[] bmax = vAdd(center, halfExtents); + int[] minxy = m_nav.calcTileLoc(bmin); + int minx = minxy[0]; + int miny = minxy[1]; + int[] maxxy = m_nav.calcTileLoc(bmax); + int maxx = maxxy[0]; + int maxy = maxxy[1]; + List tiles = new(); + for (int y = miny; y <= maxy; ++y) { + for (int x = minx; x <= maxx; ++x) { + tiles.AddRange(m_nav.getTilesAt(x, y)); + } + } + return tiles; + } + /** + * Finds a path from the start polygon to the end polygon. + * + * If the end polygon cannot be reached through the navigation graph, the last polygon in the path will be the + * nearest the end polygon. + * + * The start and end positions are used to calculate traversal costs. (The y-values impact the result.) + * + * @param startRef + * The refrence id of the start polygon. + * @param endRef + * The reference id of the end polygon. + * @param startPos + * A position within the start polygon. [(x, y, z)] + * @param endPos + * A position within the end polygon. [(x, y, z)] + * @param filter + * The polygon filter to apply to the query. + * @return Found path + */ + public virtual Result> findPath(long startRef, long endRef, float[] startPos, float[] endPos, + QueryFilter filter) { + return findPath(startRef, endRef, startPos, endPos, filter, new DefaultQueryHeuristic(), 0, 0); + } + + public virtual Result> findPath(long startRef, long endRef, float[] startPos, float[] endPos, QueryFilter filter, + int options, float raycastLimit) { + return findPath(startRef, endRef, startPos, endPos, filter, new DefaultQueryHeuristic(), options, raycastLimit); + } + + public Result> findPath(long startRef, long endRef, float[] startPos, float[] endPos, QueryFilter filter, + QueryHeuristic heuristic, int options, float raycastLimit) { + // Validate input + if (!m_nav.isValidPolyRef(startRef) || !m_nav.isValidPolyRef(endRef) || null == startPos + || !vIsFinite(startPos) || null == endPos || !vIsFinite(endPos) || null == filter) { + return Results.invalidParam>(); + } + + float raycastLimitSqr = sqr(raycastLimit); + + // trade quality with performance? + if ((options & DT_FINDPATH_ANY_ANGLE) != 0 && raycastLimit < 0f) { + // limiting to several times the character radius yields nice results. It is not sensitive + // so it is enough to compute it from the first tile. + MeshTile tile = m_nav.getTileByRef(startRef); + float agentRadius = tile.data.header.walkableRadius; + raycastLimitSqr = sqr(agentRadius * NavMesh.DT_RAY_CAST_LIMIT_PROPORTIONS); + } + + if (startRef == endRef) { + List singlePath = new(1); + singlePath.Add(startRef); + return Results.success(singlePath); + } + + m_nodePool.clear(); + m_openList.clear(); + + Node startNode = m_nodePool.getNode(startRef); + vCopy(startNode.pos, startPos); + startNode.pidx = 0; + startNode.cost = 0; + startNode.total = heuristic.getCost(startPos, endPos); + startNode.id = startRef; + startNode.flags = Node.DT_NODE_OPEN; + m_openList.push(startNode); + + Node lastBestNode = startNode; + float lastBestNodeCost = startNode.total; + + Status status = Status.SUCCSESS; + + while (!m_openList.isEmpty()) { + // Remove node from open list and put it in closed list. + Node bestNode = m_openList.pop(); + bestNode.flags &= ~Node.DT_NODE_OPEN; + bestNode.flags |= Node.DT_NODE_CLOSED; + + // Reached the goal, stop searching. + if (bestNode.id == endRef) { + lastBestNode = bestNode; + break; + } + + // Get current poly and tile. + // The API input has been cheked already, skip checking internal data. + long bestRef = bestNode.id; + Tuple tileAndPoly = m_nav.getTileAndPolyByRefUnsafe(bestRef); + MeshTile bestTile = tileAndPoly.Item1; + Poly bestPoly = tileAndPoly.Item2; + + // Get parent poly and tile. + long parentRef = 0, grandpaRef = 0; + MeshTile parentTile = null; + Poly parentPoly = null; + Node parentNode = null; + if (bestNode.pidx != 0) { + parentNode = m_nodePool.getNodeAtIdx(bestNode.pidx); + parentRef = parentNode.id; + if (parentNode.pidx != 0) { + grandpaRef = m_nodePool.getNodeAtIdx(parentNode.pidx).id; + } + } + if (parentRef != 0) { + tileAndPoly = m_nav.getTileAndPolyByRefUnsafe(parentRef); + parentTile = tileAndPoly.Item1; + parentPoly = tileAndPoly.Item2; + } + + // decide whether to test raycast to previous nodes + bool tryLOS = false; + if ((options & DT_FINDPATH_ANY_ANGLE) != 0) { + if ((parentRef != 0) && (raycastLimitSqr >= float.MaxValue + || vDistSqr(parentNode.pos, bestNode.pos) < raycastLimitSqr)) { + tryLOS = true; + } + } + + for (int i = bestTile.polyLinks[bestPoly.index]; i != NavMesh.DT_NULL_LINK; i = bestTile.links[i].next) { + long neighbourRef = bestTile.links[i].refs; + + // Skip invalid ids and do not expand back to where we came from. + if (neighbourRef == 0 || neighbourRef == parentRef) { + continue; + } + + // Get neighbour poly and tile. + // The API input has been cheked already, skip checking internal data. + tileAndPoly = m_nav.getTileAndPolyByRefUnsafe(neighbourRef); + MeshTile neighbourTile = tileAndPoly.Item1; + Poly neighbourPoly = tileAndPoly.Item2; + + if (!filter.passFilter(neighbourRef, neighbourTile, neighbourPoly)) { + continue; + } + + // get the node + Node neighbourNode = m_nodePool.getNode(neighbourRef, 0); + + // do not expand to nodes that were already visited from the + // same parent + if (neighbourNode.pidx != 0 && neighbourNode.pidx == bestNode.pidx) { + continue; + } + + // If the node is visited the first time, calculate node position. + float[] neighbourPos = neighbourNode.pos; + Result midpod = neighbourRef == endRef + ? getEdgeIntersectionPoint(bestNode.pos, bestRef, bestPoly, bestTile, endPos, neighbourRef, + neighbourPoly, neighbourTile) + : getEdgeMidPoint(bestRef, bestPoly, bestTile, neighbourRef, neighbourPoly, neighbourTile); + if (!midpod.failed()) { + neighbourPos = midpod.result; + } + + // Calculate cost and heuristic. + float cost = 0; + float heuristicCost = 0; + + // raycast parent + bool foundShortCut = false; + List shortcut = null; + if (tryLOS) { + Result rayHit = raycast(parentRef, parentNode.pos, neighbourPos, filter, + DT_RAYCAST_USE_COSTS, grandpaRef); + if (rayHit.succeeded()) { + foundShortCut = rayHit.result.t >= 1.0f; + if (foundShortCut) { + shortcut = rayHit.result.path; + // shortcut found using raycast. Using shorter cost + // instead + cost = parentNode.cost + rayHit.result.pathCost; + } + } + } + + // update move cost + if (!foundShortCut) { + float curCost = filter.getCost(bestNode.pos, neighbourPos, parentRef, parentTile, + parentPoly, bestRef, bestTile, bestPoly, neighbourRef, neighbourTile, neighbourPoly); + cost = bestNode.cost + curCost; + } + + // Special case for last node. + if (neighbourRef == endRef) { + // Cost + float endCost = filter.getCost(neighbourPos, endPos, bestRef, bestTile, bestPoly, neighbourRef, + neighbourTile, neighbourPoly, 0L, null, null); + cost = cost + endCost; + } else { + // Cost + heuristicCost = heuristic.getCost(neighbourPos, endPos); + } + + float total = cost + heuristicCost; + + // The node is already in open list and the new result is worse, skip. + if ((neighbourNode.flags & Node.DT_NODE_OPEN) != 0 && total >= neighbourNode.total) { + continue; + } + // The node is already visited and process, and the new result is worse, skip. + if ((neighbourNode.flags & Node.DT_NODE_CLOSED) != 0 && total >= neighbourNode.total) { + continue; + } + + // Add or update the node. + neighbourNode.pidx = foundShortCut ? bestNode.pidx : m_nodePool.getNodeIdx(bestNode); + neighbourNode.id = neighbourRef; + neighbourNode.flags = (neighbourNode.flags & ~Node.DT_NODE_CLOSED); + neighbourNode.cost = cost; + neighbourNode.total = total; + neighbourNode.pos = neighbourPos; + neighbourNode.shortcut = shortcut; + + if ((neighbourNode.flags & Node.DT_NODE_OPEN) != 0) { + // Already in open, update node location. + m_openList.modify(neighbourNode); + } else { + // Put the node in open list. + neighbourNode.flags |= Node.DT_NODE_OPEN; + m_openList.push(neighbourNode); + } + + // Update nearest node to target so far. + if (heuristicCost < lastBestNodeCost) { + lastBestNodeCost = heuristicCost; + lastBestNode = neighbourNode; + } + } + } + + List path = getPathToNode(lastBestNode); + + if (lastBestNode.id != endRef) { + status = Status.PARTIAL_RESULT; + } + + return Results.of(status, path); + } + + /** + * Intializes a sliced path query. + * + * Common use case: -# Call initSlicedFindPath() to initialize the sliced path query. -# Call updateSlicedFindPath() + * until it returns complete. -# Call finalizeSlicedFindPath() to get the path. + * + * @param startRef + * The reference id of the start polygon. + * @param endRef + * The reference id of the end polygon. + * @param startPos + * A position within the start polygon. [(x, y, z)] + * @param endPos + * A position within the end polygon. [(x, y, z)] + * @param filter + * The polygon filter to apply to the query. + * @param options + * query options (see: #FindPathOptions) + * @return + */ + public Status initSlicedFindPath(long startRef, long endRef, float[] startPos, float[] endPos, QueryFilter filter, + int options) { + return initSlicedFindPath(startRef, endRef, startPos, endPos, filter, options, new DefaultQueryHeuristic(), -1.0f); + } + + public Status initSlicedFindPath(long startRef, long endRef, float[] startPos, float[] endPos, QueryFilter filter, + int options, float raycastLimit) { + return initSlicedFindPath(startRef, endRef, startPos, endPos, filter, options, new DefaultQueryHeuristic(), raycastLimit); + } + + public Status initSlicedFindPath(long startRef, long endRef, float[] startPos, float[] endPos, QueryFilter filter, + int options, QueryHeuristic heuristic, float raycastLimit) { + // Init path state. + m_query = new QueryData(); + m_query.status = Status.FAILURE; + m_query.startRef = startRef; + m_query.endRef = endRef; + vCopy(m_query.startPos, startPos); + vCopy(m_query.endPos, endPos); + m_query.filter = filter; + m_query.options = options; + m_query.heuristic = heuristic; + m_query.raycastLimitSqr = sqr(raycastLimit); + + // Validate input + if (!m_nav.isValidPolyRef(startRef) || !m_nav.isValidPolyRef(endRef) || null == startPos + || !vIsFinite(startPos) || null == endPos || !vIsFinite(endPos) || null == filter) { + return Status.FAILURE_INVALID_PARAM; + } + + // trade quality with performance? + if ((options & DT_FINDPATH_ANY_ANGLE) != 0 && raycastLimit < 0f) { + // limiting to several times the character radius yields nice results. It is not sensitive + // so it is enough to compute it from the first tile. + MeshTile tile = m_nav.getTileByRef(startRef); + float agentRadius = tile.data.header.walkableRadius; + m_query.raycastLimitSqr = sqr(agentRadius * NavMesh.DT_RAY_CAST_LIMIT_PROPORTIONS); + } + + if (startRef == endRef) { + m_query.status = Status.SUCCSESS; + return Status.SUCCSESS; + } + + m_nodePool.clear(); + m_openList.clear(); + + Node startNode = m_nodePool.getNode(startRef); + vCopy(startNode.pos, startPos); + startNode.pidx = 0; + startNode.cost = 0; + startNode.total = heuristic.getCost(startPos, endPos); + startNode.id = startRef; + startNode.flags = Node.DT_NODE_OPEN; + m_openList.push(startNode); + + m_query.status = Status.IN_PROGRESS; + m_query.lastBestNode = startNode; + m_query.lastBestNodeCost = startNode.total; + + return m_query.status; + } + + /** + * Updates an in-progress sliced path query. + * + * @param maxIter + * The maximum number of iterations to perform. + * @return The status flags for the query. + */ + public virtual Result updateSlicedFindPath(int maxIter) { + if (!m_query.status.isInProgress()) { + return Results.of(m_query.status, 0); + } + + // Make sure the request is still valid. + if (!m_nav.isValidPolyRef(m_query.startRef) || !m_nav.isValidPolyRef(m_query.endRef)) { + m_query.status = Status.FAILURE; + return Results.of(m_query.status, 0); + } + + int iter = 0; + while (iter < maxIter && !m_openList.isEmpty()) { + iter++; + + // Remove node from open list and put it in closed list. + Node bestNode = m_openList.pop(); + bestNode.flags &= ~Node.DT_NODE_OPEN; + bestNode.flags |= Node.DT_NODE_CLOSED; + + // Reached the goal, stop searching. + if (bestNode.id == m_query.endRef) { + m_query.lastBestNode = bestNode; + m_query.status = Status.SUCCSESS; + return Results.of(m_query.status, iter); + } + + // Get current poly and tile. + // The API input has been cheked already, skip checking internal + // data. + long bestRef = bestNode.id; + Result> tileAndPoly = m_nav.getTileAndPolyByRef(bestRef); + if (tileAndPoly.failed()) { + m_query.status = Status.FAILURE; + // The polygon has disappeared during the sliced query, fail. + return Results.of(m_query.status, iter); + } + MeshTile bestTile = tileAndPoly.result.Item1; + Poly bestPoly = tileAndPoly.result.Item2; + // Get parent and grand parent poly and tile. + long parentRef = 0, grandpaRef = 0; + MeshTile parentTile = null; + Poly parentPoly = null; + Node parentNode = null; + if (bestNode.pidx != 0) { + parentNode = m_nodePool.getNodeAtIdx(bestNode.pidx); + parentRef = parentNode.id; + if (parentNode.pidx != 0) { + grandpaRef = m_nodePool.getNodeAtIdx(parentNode.pidx).id; + } + } + if (parentRef != 0) { + bool invalidParent = false; + tileAndPoly = m_nav.getTileAndPolyByRef(parentRef); + invalidParent = tileAndPoly.failed(); + if (invalidParent || (grandpaRef != 0 && !m_nav.isValidPolyRef(grandpaRef))) { + // The polygon has disappeared during the sliced query, + // fail. + m_query.status = Status.FAILURE; + return Results.of(m_query.status, iter); + } + parentTile = tileAndPoly.result.Item1; + parentPoly = tileAndPoly.result.Item2; + } + + // decide whether to test raycast to previous nodes + bool tryLOS = false; + if ((m_query.options & DT_FINDPATH_ANY_ANGLE) != 0) { + if ((parentRef != 0) && (m_query.raycastLimitSqr >= float.MaxValue + || vDistSqr(parentNode.pos, bestNode.pos) < m_query.raycastLimitSqr)) { + tryLOS = true; + } + } + + for (int i = bestTile.polyLinks[bestPoly.index]; i != NavMesh.DT_NULL_LINK; i = bestTile.links[i].next) { + long neighbourRef = bestTile.links[i].refs; + + // Skip invalid ids and do not expand back to where we came + // from. + if (neighbourRef == 0 || neighbourRef == parentRef) { + continue; + } + + // Get neighbour poly and tile. + // The API input has been cheked already, skip checking internal + // data. + Tuple tileAndPolyUns = m_nav.getTileAndPolyByRefUnsafe(neighbourRef); + MeshTile neighbourTile = tileAndPolyUns.Item1; + Poly neighbourPoly = tileAndPolyUns.Item2; + + if (!m_query.filter.passFilter(neighbourRef, neighbourTile, neighbourPoly)) { + continue; + } + + // get the neighbor node + Node neighbourNode = m_nodePool.getNode(neighbourRef, 0); + + // do not expand to nodes that were already visited from the + // same parent + if (neighbourNode.pidx != 0 && neighbourNode.pidx == bestNode.pidx) { + continue; + } + + // If the node is visited the first time, calculate node + // position. + float[] neighbourPos = neighbourNode.pos; + Result midpod = neighbourRef == m_query.endRef + ? getEdgeIntersectionPoint(bestNode.pos, bestRef, bestPoly, bestTile, m_query.endPos, + neighbourRef, neighbourPoly, neighbourTile) + : getEdgeMidPoint(bestRef, bestPoly, bestTile, neighbourRef, neighbourPoly, neighbourTile); + if (!midpod.failed()) { + neighbourPos = midpod.result; + } + + // Calculate cost and heuristic. + float cost = 0; + float heuristic = 0; + + // raycast parent + bool foundShortCut = false; + List shortcut = null; + if (tryLOS) { + Result rayHit = raycast(parentRef, parentNode.pos, neighbourPos, m_query.filter, + DT_RAYCAST_USE_COSTS, grandpaRef); + if (rayHit.succeeded()) { + foundShortCut = rayHit.result.t >= 1.0f; + if (foundShortCut) { + shortcut = rayHit.result.path; + // shortcut found using raycast. Using shorter cost + // instead + cost = parentNode.cost + rayHit.result.pathCost; + } + } + } + + // update move cost + if (!foundShortCut) { + // No shortcut found. + float curCost = m_query.filter.getCost(bestNode.pos, neighbourPos, parentRef, parentTile, + parentPoly, bestRef, bestTile, bestPoly, neighbourRef, neighbourTile, neighbourPoly); + cost = bestNode.cost + curCost; + } + + // Special case for last node. + if (neighbourRef == m_query.endRef) { + float endCost = m_query.filter.getCost(neighbourPos, m_query.endPos, bestRef, bestTile, + bestPoly, neighbourRef, neighbourTile, neighbourPoly, 0, null, null); + + cost = cost + endCost; + heuristic = 0; + } else { + heuristic = m_query.heuristic.getCost(neighbourPos, m_query.endPos); + } + + float total = cost + heuristic; + + // The node is already in open list and the new result is worse, + // skip. + if ((neighbourNode.flags & Node.DT_NODE_OPEN) != 0 && total >= neighbourNode.total) { + continue; + } + // The node is already visited and process, and the new result + // is worse, skip. + if ((neighbourNode.flags & Node.DT_NODE_CLOSED) != 0 && total >= neighbourNode.total) { + continue; + } + + // Add or update the node. + neighbourNode.pidx = foundShortCut ? bestNode.pidx : m_nodePool.getNodeIdx(bestNode); + neighbourNode.id = neighbourRef; + neighbourNode.flags = (neighbourNode.flags & ~Node.DT_NODE_CLOSED); + neighbourNode.cost = cost; + neighbourNode.total = total; + neighbourNode.pos = neighbourPos; + neighbourNode.shortcut = shortcut; + + if ((neighbourNode.flags & Node.DT_NODE_OPEN) != 0) { + // Already in open, update node location. + m_openList.modify(neighbourNode); + } else { + // Put the node in open list. + neighbourNode.flags |= Node.DT_NODE_OPEN; + m_openList.push(neighbourNode); + } + + // Update nearest node to target so far. + if (heuristic < m_query.lastBestNodeCost) { + m_query.lastBestNodeCost = heuristic; + m_query.lastBestNode = neighbourNode; + } + } + } + + // Exhausted all nodes, but could not find path. + if (m_openList.isEmpty()) { + m_query.status = Status.PARTIAL_RESULT; + } + + return Results.of(m_query.status, iter); + } + + /// Finalizes and returns the results of a sliced path query. + /// @param[out] path An ordered list of polygon references representing the path. (Start to end.) + /// [(polyRef) * @p pathCount] + /// @returns The status flags for the query. + public virtual Result> finalizeSlicedFindPath() { + + List path = new(64); + if (m_query.status.isFailed()) { + // Reset query. + m_query = new QueryData(); + return Results.failure(path); + } + + if (m_query.startRef == m_query.endRef) { + // Special case: the search starts and ends at same poly. + path.Add(m_query.startRef); + } else { + // Reverse the path. + if (m_query.lastBestNode.id != m_query.endRef) { + m_query.status = Status.PARTIAL_RESULT; + } + path = getPathToNode(m_query.lastBestNode); + } + + Status status = m_query.status; + // Reset query. + m_query = new QueryData(); + + return Results.of(status, path); + } + + /// Finalizes and returns the results of an incomplete sliced path query, returning the path to the furthest + /// polygon on the existing path that was visited during the search. + /// @param[in] existing An array of polygon references for the existing path. + /// @param[in] existingSize The number of polygon in the @p existing array. + /// @param[out] path An ordered list of polygon references representing the path. (Start to end.) + /// [(polyRef) * @p pathCount] + /// @returns The status flags for the query. + public virtual Result> finalizeSlicedFindPathPartial(List existing) { + + List path = new(64); + if (null == existing || existing.Count <= 0) { + return Results.failure(path); + } + if (m_query.status.isFailed()) { + // Reset query. + m_query = new QueryData(); + return Results.failure(path); + } + if (m_query.startRef == m_query.endRef) { + // Special case: the search starts and ends at same poly. + path.Add(m_query.startRef); + } else { + // Find furthest existing node that was visited. + Node node = null; + for (int i = existing.Count - 1; i >= 0; --i) { + node = m_nodePool.findNode(existing[i]); + if (node != null) { + break; + } + } + + if (node == null) { + m_query.status = Status.PARTIAL_RESULT; + node = m_query.lastBestNode; + } + + path = getPathToNode(node); + + } + Status status = m_query.status; + // Reset query. + m_query = new QueryData(); + + return Results.of(status, path); + } + + protected Status appendVertex(float[] pos, int flags, long refs, List straightPath, + int maxStraightPath) { + if (straightPath.Count > 0 && vEqual(straightPath[straightPath.Count - 1].pos, pos)) { + // The vertices are equal, update flags and poly. + straightPath[straightPath.Count - 1].flags = flags; + straightPath[straightPath.Count - 1].refs = refs; + } else { + if (straightPath.Count < maxStraightPath) { + // Append new vertex. + straightPath.Add(new StraightPathItem(pos, flags, refs)); + } + // If reached end of path or there is no space to append more vertices, return. + if (flags == DT_STRAIGHTPATH_END || straightPath.Count >= maxStraightPath) { + return Status.SUCCSESS; + } + } + return Status.IN_PROGRESS; + } + + protected Status appendPortals(int startIdx, int endIdx, float[] endPos, List path, + List straightPath, int maxStraightPath, int options) { + float[] startPos = straightPath[straightPath.Count - 1].pos; + // Append or update last vertex + Status stat; + for (int i = startIdx; i < endIdx; i++) { + // Calculate portal + long from = path[i]; + Result> tileAndPoly = m_nav.getTileAndPolyByRef(from); + if (tileAndPoly.failed()) { + return Status.FAILURE; + } + MeshTile fromTile = tileAndPoly.result.Item1; + Poly fromPoly = tileAndPoly.result.Item2; + + long to = path[i + 1]; + tileAndPoly = m_nav.getTileAndPolyByRef(to); + if (tileAndPoly.failed()) { + return Status.FAILURE; + } + MeshTile toTile = tileAndPoly.result.Item1; + Poly toPoly = tileAndPoly.result.Item2; + + Result portals = getPortalPoints(from, fromPoly, fromTile, to, toPoly, toTile, 0, 0); + if (portals.failed()) { + break; + } + float[] left = portals.result.left; + float[] right = portals.result.right; + + if ((options & DT_STRAIGHTPATH_AREA_CROSSINGS) != 0) { + // Skip intersection if only area crossings are requested. + if (fromPoly.getArea() == toPoly.getArea()) { + continue; + } + } + + // Append intersection + Tuple interect = intersectSegSeg2D(startPos, endPos, left, right); + if (null != interect) { + float t = interect.Item2; + float[] pt = vLerp(left, right, t); + stat = appendVertex(pt, 0, path[i + 1], straightPath, maxStraightPath); + if (!stat.isInProgress()) { + return stat; + } + } + } + return Status.IN_PROGRESS; + } + + /// @par + /// Finds the straight path from the start to the end position within the polygon corridor. + /// + /// This method peforms what is often called 'string pulling'. + /// + /// The start position is clamped to the first polygon in the path, and the + /// end position is clamped to the last. So the start and end positions should + /// normally be within or very near the first and last polygons respectively. + /// + /// The returned polygon references represent the reference id of the polygon + /// that is entered at the associated path position. The reference id associated + /// with the end point will always be zero. This allows, for example, matching + /// off-mesh link points to their representative polygons. + /// + /// If the provided result buffers are too small for the entire result set, + /// they will be filled as far as possible from the start toward the end + /// position. + /// + /// @param[in] startPos Path start position. [(x, y, z)] + /// @param[in] endPos Path end position. [(x, y, z)] + /// @param[in] path An array of polygon references that represent the path corridor. + /// @param[out] straightPath Points describing the straight path. [(x, y, z) * @p straightPathCount]. + /// @param[in] maxStraightPath The maximum number of points the straight path arrays can hold. [Limit: > 0] + /// @param[in] options Query options. (see: #dtStraightPathOptions) + /// @returns The status flags for the query. + public virtual Result> findStraightPath(float[] startPos, float[] endPos, List path, + int maxStraightPath, int options) { + + List straightPath = new(); + if (null == startPos || !vIsFinite(startPos) || null == endPos || !vIsFinite(endPos) + || null == path || 0 == path.Count || path[0] == 0 || maxStraightPath <= 0) { + return Results.invalidParam>(); + } + // TODO: Should this be callers responsibility? + Result closestStartPosRes = closestPointOnPolyBoundary(path[0], startPos); + if (closestStartPosRes.failed()) { + return Results.invalidParam>("Cannot find start position"); + } + float[] closestStartPos = closestStartPosRes.result; + Result closestEndPosRes = closestPointOnPolyBoundary(path[path.Count - 1], endPos); + if (closestEndPosRes.failed()) { + return Results.invalidParam>("Cannot find end position"); + } + float[] closestEndPos = closestEndPosRes.result; + // Add start point. + Status stat = appendVertex(closestStartPos, DT_STRAIGHTPATH_START, path[0], straightPath, maxStraightPath); + if (!stat.isInProgress()) { + return Results.success(straightPath); + } + + if (path.Count > 1) { + float[] portalApex = vCopy(closestStartPos); + float[] portalLeft = vCopy(portalApex); + float[] portalRight = vCopy(portalApex); + int apexIndex = 0; + int leftIndex = 0; + int rightIndex = 0; + + int leftPolyType = 0; + int rightPolyType = 0; + + long leftPolyRef = path[0]; + long rightPolyRef = path[0]; + + for (int i = 0; i < path.Count; ++i) { + float[] left; + float[] right; + int toType; + + if (i + 1 < path.Count) { + // Next portal. + Result portalPoints = getPortalPoints(path[i], path[i + 1]); + if (portalPoints.failed()) { + closestEndPosRes = closestPointOnPolyBoundary(path[i], endPos); + if (closestEndPosRes.failed()) { + return Results.invalidParam>(); + } + closestEndPos = closestEndPosRes.result; + // Append portals along the current straight path segment. + if ((options & (DT_STRAIGHTPATH_AREA_CROSSINGS | DT_STRAIGHTPATH_ALL_CROSSINGS)) != 0) { + // Ignore status return value as we're just about to return anyway. + appendPortals(apexIndex, i, closestEndPos, path, straightPath, maxStraightPath, options); + } + // Ignore status return value as we're just about to return anyway. + appendVertex(closestEndPos, 0, path[i], straightPath, maxStraightPath); + return Results.success(straightPath); + } + left = portalPoints.result.left; + right = portalPoints.result.right; + toType = portalPoints.result.toType; + + // If starting really close the portal, advance. + if (i == 0) { + Tuple dt = distancePtSegSqr2D(portalApex, left, right); + if (dt.Item1 < sqr(0.001f)) { + continue; + } + } + } else { + // End of the path. + left = vCopy(closestEndPos); + right = vCopy(closestEndPos); + toType = Poly.DT_POLYTYPE_GROUND; + } + + // Right vertex. + if (triArea2D(portalApex, portalRight, right) <= 0.0f) { + if (vEqual(portalApex, portalRight) || triArea2D(portalApex, portalLeft, right) > 0.0f) { + portalRight = vCopy(right); + rightPolyRef = (i + 1 < path.Count) ? path[i + 1] : 0; + rightPolyType = toType; + rightIndex = i; + } else { + // Append portals along the current straight path segment. + if ((options & (DT_STRAIGHTPATH_AREA_CROSSINGS | DT_STRAIGHTPATH_ALL_CROSSINGS)) != 0) { + stat = appendPortals(apexIndex, leftIndex, portalLeft, path, straightPath, maxStraightPath, + options); + if (!stat.isInProgress()) { + return Results.success(straightPath); + } + } + + portalApex = vCopy(portalLeft); + apexIndex = leftIndex; + + int flags = 0; + if (leftPolyRef == 0) { + flags = DT_STRAIGHTPATH_END; + } else if (leftPolyType == Poly.DT_POLYTYPE_OFFMESH_CONNECTION) { + flags = DT_STRAIGHTPATH_OFFMESH_CONNECTION; + } + long refs = leftPolyRef; + + // Append or update vertex + stat = appendVertex(portalApex, flags, refs, straightPath, maxStraightPath); + if (!stat.isInProgress()) { + return Results.success(straightPath); + } + + portalLeft = vCopy(portalApex); + portalRight = vCopy(portalApex); + leftIndex = apexIndex; + rightIndex = apexIndex; + + // Restart + i = apexIndex; + + continue; + } + } + + // Left vertex. + if (triArea2D(portalApex, portalLeft, left) >= 0.0f) { + if (vEqual(portalApex, portalLeft) || triArea2D(portalApex, portalRight, left) < 0.0f) { + portalLeft = vCopy(left); + leftPolyRef = (i + 1 < path.Count) ? path[i + 1] : 0; + leftPolyType = toType; + leftIndex = i; + } else { + // Append portals along the current straight path segment. + if ((options & (DT_STRAIGHTPATH_AREA_CROSSINGS | DT_STRAIGHTPATH_ALL_CROSSINGS)) != 0) { + stat = appendPortals(apexIndex, rightIndex, portalRight, path, straightPath, + maxStraightPath, options); + if (!stat.isInProgress()) { + return Results.success(straightPath); + } + } + + portalApex = vCopy(portalRight); + apexIndex = rightIndex; + + int flags = 0; + if (rightPolyRef == 0) { + flags = DT_STRAIGHTPATH_END; + } else if (rightPolyType == Poly.DT_POLYTYPE_OFFMESH_CONNECTION) { + flags = DT_STRAIGHTPATH_OFFMESH_CONNECTION; + } + long refs = rightPolyRef; + + // Append or update vertex + stat = appendVertex(portalApex, flags, refs, straightPath, maxStraightPath); + if (!stat.isInProgress()) { + return Results.success(straightPath); + } + + portalLeft = vCopy(portalApex); + portalRight = vCopy(portalApex); + leftIndex = apexIndex; + rightIndex = apexIndex; + + // Restart + i = apexIndex; + + continue; + } + } + } + + // Append portals along the current straight path segment. + if ((options & (DT_STRAIGHTPATH_AREA_CROSSINGS | DT_STRAIGHTPATH_ALL_CROSSINGS)) != 0) { + stat = appendPortals(apexIndex, path.Count - 1, closestEndPos, path, straightPath, maxStraightPath, + options); + if (!stat.isInProgress()) { + return Results.success(straightPath); + } + } + } + + // Ignore status return value as we're just about to return anyway. + appendVertex(closestEndPos, DT_STRAIGHTPATH_END, 0, straightPath, maxStraightPath); + return Results.success(straightPath); + } + + /// @par + /// + /// This method is optimized for small delta movement and a small number of + /// polygons. If used for too great a distance, the result set will form an + /// incomplete path. + /// + /// @p resultPos will equal the @p endPos if the end is reached. + /// Otherwise the closest reachable position will be returned. + /// + /// @p resultPos is not projected onto the surface of the navigation + /// mesh. Use #getPolyHeight if this is needed. + /// + /// This method treats the end position in the same manner as + /// the #raycast method. (As a 2D point.) See that method's documentation + /// for details. + /// + /// If the @p visited array is too small to hold the entire result set, it will + /// be filled as far as possible from the start position toward the end + /// position. + /// + /// Moves from the start to the end position constrained to the navigation mesh. + /// @param[in] startRef The reference id of the start polygon. + /// @param[in] startPos A position of the mover within the start polygon. [(x, y, x)] + /// @param[in] endPos The desired end position of the mover. [(x, y, z)] + /// @param[in] filter The polygon filter to apply to the query. + /// @returns Path + public Result moveAlongSurface(long startRef, float[] startPos, float[] endPos, + QueryFilter filter) { + + // Validate input + if (!m_nav.isValidPolyRef(startRef) || null == startPos || !vIsFinite(startPos) + || null == endPos || !vIsFinite(endPos) || null == filter) { + return Results.invalidParam(); + } + + NodePool tinyNodePool = new NodePool(); + + Node startNode = tinyNodePool.getNode(startRef); + startNode.pidx = 0; + startNode.cost = 0; + startNode.total = 0; + startNode.id = startRef; + startNode.flags = Node.DT_NODE_CLOSED; + LinkedList stack = new(); + stack.AddLast(startNode); + + float[] bestPos = new float[3]; + float bestDist = float.MaxValue; + Node bestNode = null; + vCopy(bestPos, startPos); + + // Search constraints + float[] searchPos = vLerp(startPos, endPos, 0.5f); + float searchRadSqr = sqr(vDist(startPos, endPos) / 2.0f + 0.001f); + + float[] verts = new float[m_nav.getMaxVertsPerPoly() * 3]; + + while (0 < stack.Count) { + // Pop front. + Node curNode = stack.First?.Value; + stack.RemoveFirst(); + + // Get poly and tile. + // The API input has been cheked already, skip checking internal data. + long curRef = curNode.id; + Tuple tileAndPoly = m_nav.getTileAndPolyByRefUnsafe(curRef); + MeshTile curTile = tileAndPoly.Item1; + Poly curPoly = tileAndPoly.Item2; + + // Collect vertices. + int nverts = curPoly.vertCount; + for (int i = 0; i < nverts; ++i) { + Array.Copy(curTile.data.verts, curPoly.verts[i] * 3, verts, i * 3, 3); + } + + // If target is inside the poly, stop search. + if (pointInPolygon(endPos, verts, nverts)) { + bestNode = curNode; + vCopy(bestPos, endPos); + break; + } + + // Find wall edges and find nearest point inside the walls. + for (int i = 0, j = curPoly.vertCount - 1; i < curPoly.vertCount; j = i++) { + // Find links to neighbours. + int MAX_NEIS = 8; + int nneis = 0; + long[] neis = new long[MAX_NEIS]; + + if ((curPoly.neis[j] & NavMesh.DT_EXT_LINK) != 0) { + // Tile border. + for (int k = curTile.polyLinks[curPoly.index]; k != NavMesh.DT_NULL_LINK; k = curTile.links[k].next) { + Link link = curTile.links[k]; + if (link.edge == j) { + if (link.refs != 0) { + tileAndPoly = m_nav.getTileAndPolyByRefUnsafe(link.refs); + MeshTile neiTile = tileAndPoly.Item1; + Poly neiPoly = tileAndPoly.Item2; + if (filter.passFilter(link.refs, neiTile, neiPoly)) { + if (nneis < MAX_NEIS) { + neis[nneis++] = link.refs; + } + } + } + } + } + } else if (curPoly.neis[j] != 0) { + int idx = curPoly.neis[j] - 1; + long refs = m_nav.getPolyRefBase(curTile) | idx; + if (filter.passFilter(refs, curTile, curTile.data.polys[idx])) { + // Internal edge, encode id. + neis[nneis++] = refs; + } + } + + if (nneis == 0) { + // Wall edge, calc distance. + int vj = j * 3; + int vi = i * 3; + Tuple distSeg = distancePtSegSqr2D(endPos, verts, vj, vi); + float distSqr = distSeg.Item1; + float tseg = distSeg.Item2; + if (distSqr < bestDist) { + // Update nearest distance. + bestPos = vLerp(verts, vj, vi, tseg); + bestDist = distSqr; + bestNode = curNode; + } + } else { + for (int k = 0; k < nneis; ++k) { + Node neighbourNode = tinyNodePool.getNode(neis[k]); + // Skip if already visited. + if ((neighbourNode.flags & Node.DT_NODE_CLOSED) != 0) { + continue; + } + + // Skip the link if it is too far from search constraint. + // TODO: Maybe should use getPortalPoints(), but this one is way faster. + int vj = j * 3; + int vi = i * 3; + Tuple distseg = distancePtSegSqr2D(searchPos, verts, vj, vi); + float distSqr = distseg.Item1; + if (distSqr > searchRadSqr) { + continue; + } + + // Mark as the node as visited and push to queue. + neighbourNode.pidx = tinyNodePool.getNodeIdx(curNode); + neighbourNode.flags |= Node.DT_NODE_CLOSED; + stack.AddLast(neighbourNode); + } + } + } + } + + List visited = new(); + if (bestNode != null) { + // Reverse the path. + Node prev = null; + Node node = bestNode; + do { + Node next = tinyNodePool.getNodeAtIdx(node.pidx); + node.pidx = tinyNodePool.getNodeIdx(prev); + prev = node; + node = next; + } while (node != null); + + // Store result + node = prev; + do { + visited.Add(node.id); + node = tinyNodePool.getNodeAtIdx(node.pidx); + } while (node != null); + } + return Results.success(new MoveAlongSurfaceResult(bestPos, visited)); + } + + public class PortalResult { + public readonly float[] left; + public readonly float[] right; + public readonly int fromType; + public readonly int toType; + + public PortalResult(float[] left, float[] right, int fromType, int toType) { + this.left = left; + this.right = right; + this.fromType = fromType; + this.toType = toType; + } + + } + + protected Result getPortalPoints(long from, long to) { + Result> tileAndPolyResult = m_nav.getTileAndPolyByRef(from); + if (tileAndPolyResult.failed()) { + return Results.of(tileAndPolyResult.status, tileAndPolyResult.message); + } + Tuple tileAndPoly = tileAndPolyResult.result; + MeshTile fromTile = tileAndPoly.Item1; + Poly fromPoly = tileAndPoly.Item2; + int fromType = fromPoly.getType(); + + tileAndPolyResult = m_nav.getTileAndPolyByRef(to); + if (tileAndPolyResult.failed()) { + return Results.of(tileAndPolyResult.status, tileAndPolyResult.message); + } + tileAndPoly = tileAndPolyResult.result; + MeshTile toTile = tileAndPoly.Item1; + Poly toPoly = tileAndPoly.Item2; + int toType = toPoly.getType(); + + return getPortalPoints(from, fromPoly, fromTile, to, toPoly, toTile, fromType, toType); + } + + // Returns portal points between two polygons. + protected Result getPortalPoints(long from, Poly fromPoly, MeshTile fromTile, long to, Poly toPoly, + MeshTile toTile, int fromType, int toType) { + float[] left = new float[3]; + float[] right = new float[3]; + // Find the link that points to the 'to' polygon. + Link link = null; + for (int i = fromTile.polyLinks[fromPoly.index]; i != NavMesh.DT_NULL_LINK; i = fromTile.links[i].next) { + if (fromTile.links[i].refs == to) { + link = fromTile.links[i]; + break; + } + } + if (link == null) { + return Results.invalidParam("No link found"); + } + + // Handle off-mesh connections. + if (fromPoly.getType() == Poly.DT_POLYTYPE_OFFMESH_CONNECTION) { + // Find link that points to first vertex. + for (int i = fromTile.polyLinks[fromPoly.index]; i != NavMesh.DT_NULL_LINK; i = fromTile.links[i].next) { + if (fromTile.links[i].refs == to) { + int v = fromTile.links[i].edge; + Array.Copy(fromTile.data.verts, fromPoly.verts[v] * 3, left, 0, 3); + Array.Copy(fromTile.data.verts, fromPoly.verts[v] * 3, right, 0, 3); + return Results.success(new PortalResult(left, right, fromType, toType)); + } + } + return Results.invalidParam("Invalid offmesh from connection"); + } + + if (toPoly.getType() == Poly.DT_POLYTYPE_OFFMESH_CONNECTION) { + for (int i = toTile.polyLinks[toPoly.index]; i != NavMesh.DT_NULL_LINK; i = toTile.links[i].next) { + if (toTile.links[i].refs == from) { + int v = toTile.links[i].edge; + Array.Copy(toTile.data.verts, toPoly.verts[v] * 3, left, 0, 3); + Array.Copy(toTile.data.verts, toPoly.verts[v] * 3, right, 0, 3); + return Results.success(new PortalResult(left, right, fromType, toType)); + } + } + return Results.invalidParam("Invalid offmesh to connection"); + } + + // Find portal vertices. + int v0 = fromPoly.verts[link.edge]; + int v1 = fromPoly.verts[(link.edge + 1) % fromPoly.vertCount]; + Array.Copy(fromTile.data.verts, v0 * 3, left, 0, 3); + Array.Copy(fromTile.data.verts, v1 * 3, right, 0, 3); + + // If the link is at tile boundary, dtClamp the vertices to + // the link width. + if (link.side != 0xff) { + // Unpack portal limits. + if (link.bmin != 0 || link.bmax != 255) { + float s = 1.0f / 255.0f; + float tmin = link.bmin * s; + float tmax = link.bmax * s; + left = vLerp(fromTile.data.verts, v0 * 3, v1 * 3, tmin); + right = vLerp(fromTile.data.verts, v0 * 3, v1 * 3, tmax); + } + } + + return Results.success(new PortalResult(left, right, fromType, toType)); + } + + protected Result getEdgeMidPoint(long from, Poly fromPoly, MeshTile fromTile, long to, + Poly toPoly, MeshTile toTile) { + Result ppoints = getPortalPoints(from, fromPoly, fromTile, to, toPoly, toTile, 0, 0); + if (ppoints.failed()) { + return Results.of(ppoints.status, ppoints.message); + } + float[] left = ppoints.result.left; + float[] right = ppoints.result.right; + float[] mid = new float[3]; + mid[0] = (left[0] + right[0]) * 0.5f; + mid[1] = (left[1] + right[1]) * 0.5f; + mid[2] = (left[2] + right[2]) * 0.5f; + return Results.success(mid); + } + + protected Result getEdgeIntersectionPoint(float[] fromPos, long from, Poly fromPoly, MeshTile fromTile, + float[] toPos, long to, Poly toPoly, MeshTile toTile) { + Result ppoints = getPortalPoints(from, fromPoly, fromTile, to, toPoly, toTile, 0, 0); + if (ppoints.failed()) { + return Results.of(ppoints.status, ppoints.message); + } + float[] left = ppoints.result.left; + float[] right = ppoints.result.right; + float t = 0.5f; + Tuple interect = intersectSegSeg2D(fromPos, toPos, left, right); + if (null != interect) { + t = clamp(interect.Item2, 0.1f, 0.9f); + } + float[] pt = vLerp(left, right, t); + return Results.success(pt); + } + + private static float s = 1.0f / 255.0f; + + /// @par + /// + /// This method is meant to be used for quick, short distance checks. + /// + /// If the path array is too small to hold the result, it will be filled as + /// far as possible from the start postion toward the end position. + /// + /// Using the Hit Parameter t of RaycastHit + /// + /// If the hit parameter is a very high value (FLT_MAX), then the ray has hit + /// the end position. In this case the path represents a valid corridor to the + /// end position and the value of @p hitNormal is undefined. + /// + /// If the hit parameter is zero, then the start position is on the wall that + /// was hit and the value of @p hitNormal is undefined. + /// + /// If 0 < t < 1.0 then the following applies: + /// + /// @code + /// distanceToHitBorder = distanceToEndPosition * t + /// hitPoint = startPos + (endPos - startPos) * t + /// @endcode + /// + /// Use Case Restriction + /// + /// The raycast ignores the y-value of the end position. (2D check.) This + /// places significant limits on how it can be used. For example: + /// + /// Consider a scene where there is a main floor with a second floor balcony + /// that hangs over the main floor. So the first floor mesh extends below the + /// balcony mesh. The start position is somewhere on the first floor. The end + /// position is on the balcony. + /// + /// The raycast will search toward the end position along the first floor mesh. + /// If it reaches the end position's xz-coordinates it will indicate FLT_MAX + /// (no wall hit), meaning it reached the end position. This is one example of why + /// this method is meant for short distance checks. + /// + /// Casts a 'walkability' ray along the surface of the navigation mesh from + /// the start position toward the end position. + /// @note A wrapper around raycast(..., RaycastHit*). Retained for backward compatibility. + /// @param[in] startRef The reference id of the start polygon. + /// @param[in] startPos A position within the start polygon representing + /// the start of the ray. [(x, y, z)] + /// @param[in] endPos The position to cast the ray toward. [(x, y, z)] + /// @param[out] t The hit parameter. (FLT_MAX if no wall hit.) + /// @param[out] hitNormal The normal of the nearest wall hit. [(x, y, z)] + /// @param[in] filter The polygon filter to apply to the query. + /// @param[out] path The reference ids of the visited polygons. [opt] + /// @param[out] pathCount The number of visited polygons. [opt] + /// @param[in] maxPath The maximum number of polygons the @p path array can hold. + /// @returns The status flags for the query. + public Result raycast(long startRef, float[] startPos, float[] endPos, QueryFilter filter, int options, + long prevRef) { + // Validate input + if (!m_nav.isValidPolyRef(startRef) || null == startPos || !vIsFinite(startPos) + || null == endPos || !vIsFinite(endPos) || null == filter + || (prevRef != 0 && !m_nav.isValidPolyRef(prevRef))) { + return Results.invalidParam(); + } + + RaycastHit hit = new RaycastHit(); + + float[] verts = new float[m_nav.getMaxVertsPerPoly() * 3 + 3]; + + float[] curPos = new float[3], lastPos = new float[3]; + + vCopy(curPos, startPos); + float[] dir = vSub(endPos, startPos); + + MeshTile prevTile, tile, nextTile; + Poly prevPoly, poly, nextPoly; + + // The API input has been checked already, skip checking internal data. + long curRef = startRef; + Tuple tileAndPolyUns = m_nav.getTileAndPolyByRefUnsafe(curRef); + tile = tileAndPolyUns.Item1; + poly = tileAndPolyUns.Item2; + nextTile = prevTile = tile; + nextPoly = prevPoly = poly; + if (prevRef != 0) { + tileAndPolyUns = m_nav.getTileAndPolyByRefUnsafe(prevRef); + prevTile = tileAndPolyUns.Item1; + prevPoly = tileAndPolyUns.Item2; + } + while (curRef != 0) { + // Cast ray against current polygon. + + // Collect vertices. + int nv = 0; + for (int i = 0; i < poly.vertCount; ++i) { + Array.Copy(tile.data.verts, poly.verts[i] * 3, verts, nv * 3, 3); + nv++; + } + + IntersectResult iresult = intersectSegmentPoly2D(startPos, endPos, verts, nv); + if (!iresult.intersects) { + // Could not hit the polygon, keep the old t and report hit. + return Results.success(hit); + } + + hit.hitEdgeIndex = iresult.segMax; + + // Keep track of furthest t so far. + if (iresult.tmax > hit.t) { + hit.t = iresult.tmax; + } + + // Store visited polygons. + hit.path.Add(curRef); + + // Ray end is completely inside the polygon. + if (iresult.segMax == -1) { + hit.t = float.MaxValue; + + // add the cost + if ((options & DT_RAYCAST_USE_COSTS) != 0) { + hit.pathCost += filter.getCost(curPos, endPos, prevRef, prevTile, prevPoly, curRef, tile, poly, + curRef, tile, poly); + } + return Results.success(hit); + } + + // Follow neighbours. + long nextRef = 0; + + for (int i = tile.polyLinks[poly.index]; i != NavMesh.DT_NULL_LINK; i = tile.links[i].next) { + Link link = tile.links[i]; + + // Find link which contains this edge. + if (link.edge != iresult.segMax) { + continue; + } + + // Get pointer to the next polygon. + tileAndPolyUns = m_nav.getTileAndPolyByRefUnsafe(link.refs); + nextTile = tileAndPolyUns.Item1; + nextPoly = tileAndPolyUns.Item2; + // Skip off-mesh connections. + if (nextPoly.getType() == Poly.DT_POLYTYPE_OFFMESH_CONNECTION) { + continue; + } + + // Skip links based on filter. + if (!filter.passFilter(link.refs, nextTile, nextPoly)) { + continue; + } + + // If the link is internal, just return the ref. + if (link.side == 0xff) { + nextRef = link.refs; + break; + } + + // If the link is at tile boundary, + + // Check if the link spans the whole edge, and accept. + if (link.bmin == 0 && link.bmax == 255) { + nextRef = link.refs; + break; + } + + // Check for partial edge links. + int v0 = poly.verts[link.edge]; + int v1 = poly.verts[(link.edge + 1) % poly.vertCount]; + int left = v0 * 3; + int right = v1 * 3; + + // Check that the intersection lies inside the link portal. + if (link.side == 0 || link.side == 4) { + // Calculate link size. + float lmin = tile.data.verts[left + 2] + + (tile.data.verts[right + 2] - tile.data.verts[left + 2]) * (link.bmin * s); + float lmax = tile.data.verts[left + 2] + + (tile.data.verts[right + 2] - tile.data.verts[left + 2]) * (link.bmax * s); + if (lmin > lmax) { + float temp = lmin; + lmin = lmax; + lmax = temp; + } + + // Find Z intersection. + float z = startPos[2] + (endPos[2] - startPos[2]) * iresult.tmax; + if (z >= lmin && z <= lmax) { + nextRef = link.refs; + break; + } + } else if (link.side == 2 || link.side == 6) { + // Calculate link size. + float lmin = tile.data.verts[left] + + (tile.data.verts[right] - tile.data.verts[left]) * (link.bmin * s); + float lmax = tile.data.verts[left] + + (tile.data.verts[right] - tile.data.verts[left]) * (link.bmax * s); + if (lmin > lmax) { + float temp = lmin; + lmin = lmax; + lmax = temp; + } + + // Find X intersection. + float x = startPos[0] + (endPos[0] - startPos[0]) * iresult.tmax; + if (x >= lmin && x <= lmax) { + nextRef = link.refs; + break; + } + } + } + + // add the cost + if ((options & DT_RAYCAST_USE_COSTS) != 0) { + // compute the intersection point at the furthest end of the polygon + // and correct the height (since the raycast moves in 2d) + vCopy(lastPos, curPos); + curPos = vMad(startPos, dir, hit.t); + VectorPtr e1 = new VectorPtr(verts, iresult.segMax * 3); + VectorPtr e2 = new VectorPtr(verts, ((iresult.segMax + 1) % nv) * 3); + float[] eDir = vSub(e2, e1); + float[] diff = vSub(new VectorPtr(curPos), e1); + float s = sqr(eDir[0]) > sqr(eDir[2]) ? diff[0] / eDir[0] : diff[2] / eDir[2]; + curPos[1] = e1.get(1) + eDir[1] * s; + + hit.pathCost += filter.getCost(lastPos, curPos, prevRef, prevTile, prevPoly, curRef, tile, poly, + nextRef, nextTile, nextPoly); + } + + if (nextRef == 0) { + // No neighbour, we hit a wall. + + // Calculate hit normal. + int a = iresult.segMax; + int b = iresult.segMax + 1 < nv ? iresult.segMax + 1 : 0; + int va = a * 3; + int vb = b * 3; + float dx = verts[vb] - verts[va]; + float dz = verts[vb + 2] - verts[va + 2]; + hit.hitNormal[0] = dz; + hit.hitNormal[1] = 0; + hit.hitNormal[2] = -dx; + vNormalize(hit.hitNormal); + return Results.success(hit); + } + + // No hit, advance to neighbour polygon. + prevRef = curRef; + curRef = nextRef; + prevTile = tile; + tile = nextTile; + prevPoly = poly; + poly = nextPoly; + } + + return Results.success(hit); + } + + /// @par + /// + /// At least one result array must be provided. + /// + /// The order of the result set is from least to highest cost to reach the polygon. + /// + /// A common use case for this method is to perform Dijkstra searches. + /// Candidate polygons are found by searching the graph beginning at the start polygon. + /// + /// If a polygon is not found via the graph search, even if it intersects the + /// search circle, it will not be included in the result set. For example: + /// + /// polyA is the start polygon. + /// polyB shares an edge with polyA. (Is adjacent.) + /// polyC shares an edge with polyB, but not with polyA + /// Even if the search circle overlaps polyC, it will not be included in the + /// result set unless polyB is also in the set. + /// + /// The value of the center point is used as the start position for cost + /// calculations. It is not projected onto the surface of the mesh, so its + /// y-value will effect the costs. + /// + /// Intersection tests occur in 2D. All polygons and the search circle are + /// projected onto the xz-plane. So the y-value of the center point does not + /// effect intersection tests. + /// + /// If the result arrays are to small to hold the entire result set, they will be + /// filled to capacity. + /// + /// @} + /// @name Dijkstra Search Functions + /// @{ + + /// Finds the polygons along the navigation graph that touch the specified circle. + /// @param[in] startRef The reference id of the polygon where the search starts. + /// @param[in] centerPos The center of the search circle. [(x, y, z)] + /// @param[in] radius The radius of the search circle. + /// @param[in] filter The polygon filter to apply to the query. + /// @param[out] resultRef The reference ids of the polygons touched by the circle. [opt] + /// @param[out] resultParent The reference ids of the parent polygons for each result. + /// Zero if a result polygon has no parent. [opt] + /// @param[out] resultCost The search cost from @p centerPos to the polygon. [opt] + /// @param[out] resultCount The number of polygons found. [opt] + /// @param[in] maxResult The maximum number of polygons the result arrays can hold. + /// @returns The status flags for the query. + public Result findPolysAroundCircle(long startRef, float[] centerPos, float radius, + QueryFilter filter) { + + // Validate input + + if (!m_nav.isValidPolyRef(startRef) || null == centerPos || !vIsFinite(centerPos) || radius < 0 + || !float.IsFinite(radius) || null == filter) { + return Results.invalidParam(); + } + + List resultRef = new(); + List resultParent = new(); + List resultCost = new(); + + m_nodePool.clear(); + m_openList.clear(); + + Node startNode = m_nodePool.getNode(startRef); + vCopy(startNode.pos, centerPos); + startNode.pidx = 0; + startNode.cost = 0; + startNode.total = 0; + startNode.id = startRef; + startNode.flags = Node.DT_NODE_OPEN; + m_openList.push(startNode); + + float radiusSqr = sqr(radius); + + while (!m_openList.isEmpty()) { + Node bestNode = m_openList.pop(); + bestNode.flags &= ~Node.DT_NODE_OPEN; + bestNode.flags |= Node.DT_NODE_CLOSED; + + // Get poly and tile. + // The API input has been cheked already, skip checking internal data. + long bestRef = bestNode.id; + Tuple tileAndPoly = m_nav.getTileAndPolyByRefUnsafe(bestRef); + MeshTile bestTile = tileAndPoly.Item1; + Poly bestPoly = tileAndPoly.Item2; + + // Get parent poly and tile. + long parentRef = 0; + MeshTile parentTile = null; + Poly parentPoly = null; + if (bestNode.pidx != 0) { + parentRef = m_nodePool.getNodeAtIdx(bestNode.pidx).id; + } + if (parentRef != 0) { + tileAndPoly = m_nav.getTileAndPolyByRefUnsafe(parentRef); + parentTile = tileAndPoly.Item1; + parentPoly = tileAndPoly.Item2; + } + + resultRef.Add(bestRef); + resultParent.Add(parentRef); + resultCost.Add(bestNode.total); + + for (int i = bestTile.polyLinks[bestPoly.index]; i != NavMesh.DT_NULL_LINK; i = bestTile.links[i].next) { + Link link = bestTile.links[i]; + long neighbourRef = link.refs; + // Skip invalid neighbours and do not follow back to parent. + if (neighbourRef == 0 || neighbourRef == parentRef) { + continue; + } + + // Expand to neighbour + tileAndPoly = m_nav.getTileAndPolyByRefUnsafe(neighbourRef); + MeshTile neighbourTile = tileAndPoly.Item1; + Poly neighbourPoly = tileAndPoly.Item2; + + // Do not advance if the polygon is excluded by the filter. + if (!filter.passFilter(neighbourRef, neighbourTile, neighbourPoly)) { + continue; + } + + // Find edge and calc distance to the edge. + Result pp = getPortalPoints(bestRef, bestPoly, bestTile, neighbourRef, neighbourPoly, + neighbourTile, 0, 0); + if (pp.failed()) { + continue; + } + float[] va = pp.result.left; + float[] vb = pp.result.right; + + // If the circle is not touching the next polygon, skip it. + Tuple distseg = distancePtSegSqr2D(centerPos, va, vb); + float distSqr = distseg.Item1; + if (distSqr > radiusSqr) { + continue; + } + + Node neighbourNode = m_nodePool.getNode(neighbourRef); + + if ((neighbourNode.flags & Node.DT_NODE_CLOSED) != 0) { + continue; + } + + // Cost + if (neighbourNode.flags == 0) { + neighbourNode.pos = vLerp(va, vb, 0.5f); + } + + float cost = filter.getCost(bestNode.pos, neighbourNode.pos, parentRef, parentTile, parentPoly, bestRef, + bestTile, bestPoly, neighbourRef, neighbourTile, neighbourPoly); + + float total = bestNode.total + cost; + // The node is already in open list and the new result is worse, skip. + if ((neighbourNode.flags & Node.DT_NODE_OPEN) != 0 && total >= neighbourNode.total) { + continue; + } + + neighbourNode.id = neighbourRef; + neighbourNode.pidx = m_nodePool.getNodeIdx(bestNode); + neighbourNode.total = total; + + if ((neighbourNode.flags & Node.DT_NODE_OPEN) != 0) { + m_openList.modify(neighbourNode); + } else { + neighbourNode.flags = Node.DT_NODE_OPEN; + m_openList.push(neighbourNode); + } + } + } + + return Results.success(new FindPolysAroundResult(resultRef, resultParent, resultCost)); + } + + /// @par + /// + /// The order of the result set is from least to highest cost. + /// + /// At least one result array must be provided. + /// + /// A common use case for this method is to perform Dijkstra searches. + /// Candidate polygons are found by searching the graph beginning at the start + /// polygon. + /// + /// The same intersection test restrictions that apply to findPolysAroundCircle() + /// method apply to this method. + /// + /// The 3D centroid of the search polygon is used as the start position for cost + /// calculations. + /// + /// Intersection tests occur in 2D. All polygons are projected onto the + /// xz-plane. So the y-values of the vertices do not effect intersection tests. + /// + /// If the result arrays are is too small to hold the entire result set, they will + /// be filled to capacity. + /// + /// Finds the polygons along the naviation graph that touch the specified convex polygon. + /// @param[in] startRef The reference id of the polygon where the search starts. + /// @param[in] verts The vertices describing the convex polygon. (CCW) + /// [(x, y, z) * @p nverts] + /// @param[in] nverts The number of vertices in the polygon. + /// @param[in] filter The polygon filter to apply to the query. + /// @param[out] resultRef The reference ids of the polygons touched by the search polygon. [opt] + /// @param[out] resultParent The reference ids of the parent polygons for each result. Zero if a + /// result polygon has no parent. [opt] + /// @param[out] resultCost The search cost from the centroid point to the polygon. [opt] + /// @param[out] resultCount The number of polygons found. + /// @param[in] maxResult The maximum number of polygons the result arrays can hold. + /// @returns The status flags for the query. + public Result findPolysAroundShape(long startRef, float[] verts, QueryFilter filter) { + // Validate input + int nverts = verts.Length / 3; + if (!m_nav.isValidPolyRef(startRef) || null == verts || nverts < 3 || null == filter) { + return Results.invalidParam(); + } + + List resultRef = new(); + List resultParent = new(); + List resultCost = new(); + + m_nodePool.clear(); + m_openList.clear(); + + float[] centerPos = new float[] { 0, 0, 0 }; + for (int i = 0; i < nverts; ++i) { + centerPos[0] += verts[i * 3]; + centerPos[1] += verts[i * 3 + 1]; + centerPos[2] += verts[i * 3 + 2]; + } + float scale = 1.0f / nverts; + centerPos[0] *= scale; + centerPos[1] *= scale; + centerPos[2] *= scale; + + Node startNode = m_nodePool.getNode(startRef); + vCopy(startNode.pos, centerPos); + startNode.pidx = 0; + startNode.cost = 0; + startNode.total = 0; + startNode.id = startRef; + startNode.flags = Node.DT_NODE_OPEN; + m_openList.push(startNode); + + while (!m_openList.isEmpty()) { + Node bestNode = m_openList.pop(); + bestNode.flags &= ~Node.DT_NODE_OPEN; + bestNode.flags |= Node.DT_NODE_CLOSED; + + // Get poly and tile. + // The API input has been cheked already, skip checking internal data. + long bestRef = bestNode.id; + Tuple tileAndPoly = m_nav.getTileAndPolyByRefUnsafe(bestRef); + MeshTile bestTile = tileAndPoly.Item1; + Poly bestPoly = tileAndPoly.Item2; + + // Get parent poly and tile. + long parentRef = 0; + MeshTile parentTile = null; + Poly parentPoly = null; + if (bestNode.pidx != 0) { + parentRef = m_nodePool.getNodeAtIdx(bestNode.pidx).id; + } + if (parentRef != 0) { + tileAndPoly = m_nav.getTileAndPolyByRefUnsafe(parentRef); + parentTile = tileAndPoly.Item1; + parentPoly = tileAndPoly.Item2; + } + + resultRef.Add(bestRef); + resultParent.Add(parentRef); + resultCost.Add(bestNode.total); + + for (int i = bestTile.polyLinks[bestPoly.index]; i != NavMesh.DT_NULL_LINK; i = bestTile.links[i].next) { + Link link = bestTile.links[i]; + long neighbourRef = link.refs; + // Skip invalid neighbours and do not follow back to parent. + if (neighbourRef == 0 || neighbourRef == parentRef) { + continue; + } + + // Expand to neighbour + tileAndPoly = m_nav.getTileAndPolyByRefUnsafe(neighbourRef); + MeshTile neighbourTile = tileAndPoly.Item1; + Poly neighbourPoly = tileAndPoly.Item2; + + // Do not advance if the polygon is excluded by the filter. + if (!filter.passFilter(neighbourRef, neighbourTile, neighbourPoly)) { + continue; + } + + // Find edge and calc distance to the edge. + Result pp = getPortalPoints(bestRef, bestPoly, bestTile, neighbourRef, neighbourPoly, + neighbourTile, 0, 0); + if (pp.failed()) { + continue; + } + float[] va = pp.result.left; + float[] vb = pp.result.right; + + // If the poly is not touching the edge to the next polygon, skip the connection it. + IntersectResult ir = intersectSegmentPoly2D(va, vb, verts, nverts); + if (!ir.intersects) { + continue; + } + if (ir.tmin > 1.0f || ir.tmax < 0.0f) { + continue; + } + + Node neighbourNode = m_nodePool.getNode(neighbourRef); + + if ((neighbourNode.flags & Node.DT_NODE_CLOSED) != 0) { + continue; + } + + // Cost + if (neighbourNode.flags == 0) { + neighbourNode.pos = vLerp(va, vb, 0.5f); + } + + float cost = filter.getCost(bestNode.pos, neighbourNode.pos, parentRef, parentTile, parentPoly, bestRef, + bestTile, bestPoly, neighbourRef, neighbourTile, neighbourPoly); + + float total = bestNode.total + cost; + + // The node is already in open list and the new result is worse, skip. + if ((neighbourNode.flags & Node.DT_NODE_OPEN) != 0 && total >= neighbourNode.total) { + continue; + } + + neighbourNode.id = neighbourRef; + neighbourNode.pidx = m_nodePool.getNodeIdx(bestNode); + neighbourNode.total = total; + + if ((neighbourNode.flags & Node.DT_NODE_OPEN) != 0) { + m_openList.modify(neighbourNode); + } else { + neighbourNode.flags = Node.DT_NODE_OPEN; + m_openList.push(neighbourNode); + } + + } + } + + return Results.success(new FindPolysAroundResult(resultRef, resultParent, resultCost)); + } + + /// @par + /// + /// This method is optimized for a small search radius and small number of result + /// polygons. + /// + /// Candidate polygons are found by searching the navigation graph beginning at + /// the start polygon. + /// + /// The same intersection test restrictions that apply to the findPolysAroundCircle + /// mehtod applies to this method. + /// + /// The value of the center point is used as the start point for cost calculations. + /// It is not projected onto the surface of the mesh, so its y-value will effect + /// the costs. + /// + /// Intersection tests occur in 2D. All polygons and the search circle are + /// projected onto the xz-plane. So the y-value of the center point does not + /// effect intersection tests. + /// + /// If the result arrays are is too small to hold the entire result set, they will + /// be filled to capacity. + /// + /// Finds the non-overlapping navigation polygons in the local neighbourhood around the center position. + /// @param[in] startRef The reference id of the polygon where the search starts. + /// @param[in] centerPos The center of the query circle. [(x, y, z)] + /// @param[in] radius The radius of the query circle. + /// @param[in] filter The polygon filter to apply to the query. + /// @param[out] resultRef The reference ids of the polygons touched by the circle. + /// @param[out] resultParent The reference ids of the parent polygons for each result. + /// Zero if a result polygon has no parent. [opt] + /// @param[out] resultCount The number of polygons found. + /// @param[in] maxResult The maximum number of polygons the result arrays can hold. + /// @returns The status flags for the query. + public Result findLocalNeighbourhood(long startRef, float[] centerPos, float radius, + QueryFilter filter) { + + // Validate input + if (!m_nav.isValidPolyRef(startRef) || null == centerPos || !vIsFinite(centerPos) || radius < 0 + || !float.IsFinite(radius) || null == filter) { + return Results.invalidParam(); + } + + List resultRef = new(); + List resultParent = new(); + + NodePool tinyNodePool = new NodePool(); + + Node startNode = tinyNodePool.getNode(startRef); + startNode.pidx = 0; + startNode.id = startRef; + startNode.flags = Node.DT_NODE_CLOSED; + LinkedList stack = new(); + stack.AddLast(startNode); + + resultRef.Add(startNode.id); + resultParent.Add(0L); + + float radiusSqr = sqr(radius); + + float[] pa = new float[m_nav.getMaxVertsPerPoly() * 3]; + float[] pb = new float[m_nav.getMaxVertsPerPoly() * 3]; + + while (0 < stack.Count) { + // Pop front. + Node curNode = stack.First?.Value; + stack.RemoveFirst(); + + // Get poly and tile. + // The API input has been cheked already, skip checking internal data. + long curRef = curNode.id; + Tuple tileAndPoly = m_nav.getTileAndPolyByRefUnsafe(curRef); + MeshTile curTile = tileAndPoly.Item1; + Poly curPoly = tileAndPoly.Item2; + + for (int i = curTile.polyLinks[curPoly.index]; i != NavMesh.DT_NULL_LINK; i = curTile.links[i].next) { + Link link = curTile.links[i]; + long neighbourRef = link.refs; + // Skip invalid neighbours. + if (neighbourRef == 0) { + continue; + } + + Node neighbourNode = tinyNodePool.getNode(neighbourRef); + // Skip visited. + if ((neighbourNode.flags & Node.DT_NODE_CLOSED) != 0) { + continue; + } + + // Expand to neighbour + tileAndPoly = m_nav.getTileAndPolyByRefUnsafe(neighbourRef); + MeshTile neighbourTile = tileAndPoly.Item1; + Poly neighbourPoly = tileAndPoly.Item2; + + // Skip off-mesh connections. + if (neighbourPoly.getType() == Poly.DT_POLYTYPE_OFFMESH_CONNECTION) { + continue; + } + + // Do not advance if the polygon is excluded by the filter. + if (!filter.passFilter(neighbourRef, neighbourTile, neighbourPoly)) { + continue; + } + + // Find edge and calc distance to the edge. + Result pp = getPortalPoints(curRef, curPoly, curTile, neighbourRef, neighbourPoly, + neighbourTile, 0, 0); + if (pp.failed()) { + continue; + } + float[] va = pp.result.left; + float[] vb = pp.result.right; + + // If the circle is not touching the next polygon, skip it. + Tuple distseg = distancePtSegSqr2D(centerPos, va, vb); + float distSqr = distseg.Item1; + if (distSqr > radiusSqr) { + continue; + } + + // Mark node visited, this is done before the overlap test so that + // we will not visit the poly again if the test fails. + neighbourNode.flags |= Node.DT_NODE_CLOSED; + neighbourNode.pidx = tinyNodePool.getNodeIdx(curNode); + + // Check that the polygon does not collide with existing polygons. + + // Collect vertices of the neighbour poly. + int npa = neighbourPoly.vertCount; + for (int k = 0; k < npa; ++k) { + Array.Copy(neighbourTile.data.verts, neighbourPoly.verts[k] * 3, pa, k * 3, 3); + } + + bool overlap = false; + for (int j = 0; j < resultRef.Count; ++j) { + long pastRef = resultRef[j]; + + // Connected polys do not overlap. + bool connected = false; + for (int k = curTile.polyLinks[curPoly.index]; k != NavMesh.DT_NULL_LINK; k = curTile.links[k].next) { + if (curTile.links[k].refs == pastRef) { + connected = true; + break; + } + } + if (connected) { + continue; + } + + // Potentially overlapping. + tileAndPoly = m_nav.getTileAndPolyByRefUnsafe(pastRef); + MeshTile pastTile = tileAndPoly.Item1; + Poly pastPoly = tileAndPoly.Item2; + + // Get vertices and test overlap + int npb = pastPoly.vertCount; + for (int k = 0; k < npb; ++k) { + Array.Copy(pastTile.data.verts, pastPoly.verts[k] * 3, pb, k * 3, 3); + } + + if (overlapPolyPoly2D(pa, npa, pb, npb)) { + overlap = true; + break; + } + } + if (overlap) { + continue; + } + + resultRef.Add(neighbourRef); + resultParent.Add(curRef); + stack.AddLast(neighbourNode); + } + } + + return Results.success(new FindLocalNeighbourhoodResult(resultRef, resultParent)); + } + + public class SegInterval { + public long refs; + public int tmin, tmax; + + public SegInterval(long refs, int tmin, int tmax) { + this.refs = refs; + this.tmin = tmin; + this.tmax = tmax; + } + + } + + protected void insertInterval(List ints, int tmin, int tmax, long refs) { + // Find insertion point. + int idx = 0; + while (idx < ints.Count) { + if (tmax <= ints[idx].tmin) { + break; + } + idx++; + } + // Store + ints.Insert(idx, new SegInterval(refs, tmin, tmax)); + } + + /// @par + /// + /// If the @p segmentRefs parameter is provided, then all polygon segments will be returned. + /// Otherwise only the wall segments are returned. + /// + /// A segment that is normally a portal will be included in the result set as a + /// wall if the @p filter results in the neighbor polygon becoomming impassable. + /// + /// The @p segmentVerts and @p segmentRefs buffers should normally be sized for the + /// maximum segments per polygon of the source navigation mesh. + /// + /// Returns the segments for the specified polygon, optionally including portals. + /// @param[in] ref The reference id of the polygon. + /// @param[in] filter The polygon filter to apply to the query. + /// @param[out] segmentVerts The segments. [(ax, ay, az, bx, by, bz) * segmentCount] + /// @param[out] segmentRefs The reference ids of each segment's neighbor polygon. + /// Or zero if the segment is a wall. [opt] [(parentRef) * @p segmentCount] + /// @param[out] segmentCount The number of segments returned. + /// @param[in] maxSegments The maximum number of segments the result arrays can hold. + /// @returns The status flags for the query. + public Result getPolyWallSegments(long refs, bool storePortals, QueryFilter filter) { + Result> tileAndPoly = m_nav.getTileAndPolyByRef(refs); + if (tileAndPoly.failed()) { + return Results.of(tileAndPoly.status, tileAndPoly.message); + } + if (null == filter) { + return Results.invalidParam(); + } + MeshTile tile = tileAndPoly.result.Item1; + Poly poly = tileAndPoly.result.Item2; + + List segmentRefs = new(); + List segmentVerts = new(); + List ints = new(16); + + for (int i = 0, j = poly.vertCount - 1; i < poly.vertCount; j = i++) { + // Skip non-solid edges. + ints.Clear(); + if ((poly.neis[j] & NavMesh.DT_EXT_LINK) != 0) { + // Tile border. + for (int k = tile.polyLinks[poly.index]; k != NavMesh.DT_NULL_LINK; k = tile.links[k].next) { + Link link = tile.links[k]; + if (link.edge == j) { + if (link.refs != 0) { + Tuple tileAndPolyUnsafe = m_nav.getTileAndPolyByRefUnsafe(link.refs); + MeshTile neiTile = tileAndPolyUnsafe.Item1; + Poly neiPoly = tileAndPolyUnsafe.Item2; + if (filter.passFilter(link.refs, neiTile, neiPoly)) { + insertInterval(ints, link.bmin, link.bmax, link.refs); + } + } + } + } + } else { + // Internal edge + long neiRef = 0; + if (poly.neis[j] != 0) { + int idx = (poly.neis[j] - 1); + neiRef = m_nav.getPolyRefBase(tile) | idx; + if (!filter.passFilter(neiRef, tile, tile.data.polys[idx])) { + neiRef = 0; + } + } + // If the edge leads to another polygon and portals are not stored, skip. + if (neiRef != 0 && !storePortals) { + continue; + } + + int ivj = poly.verts[j] * 3; + int ivi = poly.verts[i] * 3; + float[] seg = new float[6]; + Array.Copy(tile.data.verts, ivj, seg, 0, 3); + Array.Copy(tile.data.verts, ivi, seg, 3, 3); + segmentVerts.Add(seg); + segmentRefs.Add(neiRef); + continue; + } + + // Add sentinels + insertInterval(ints, -1, 0, 0); + insertInterval(ints, 255, 256, 0); + + // Store segments. + int vj = poly.verts[j] * 3; + int vi = poly.verts[i] * 3; + for (int k = 1; k < ints.Count; ++k) { + // Portal segment. + if (storePortals && ints[k].refs != 0) { + float tmin = ints[k].tmin / 255.0f; + float tmax = ints[k].tmax / 255.0f; + float[] seg = new float[6]; + Array.Copy(vLerp(tile.data.verts, vj, vi, tmin), 0, seg, 0, 3); + Array.Copy(vLerp(tile.data.verts, vj, vi, tmax), 0, seg, 3, 3); + segmentVerts.Add(seg); + segmentRefs.Add(ints[k].refs); + } + + // Wall segment. + int imin = ints[k - 1].tmax; + int imax = ints[k].tmin; + if (imin != imax) { + float tmin = imin / 255.0f; + float tmax = imax / 255.0f; + float[] seg = new float[6]; + Array.Copy(vLerp(tile.data.verts, vj, vi, tmin), 0, seg, 0, 3); + Array.Copy(vLerp(tile.data.verts, vj, vi, tmax), 0, seg, 3, 3); + segmentVerts.Add(seg); + segmentRefs.Add(0L); + } + } + } + + return Results.success(new GetPolyWallSegmentsResult(segmentVerts, segmentRefs)); + } + + /// @par + /// + /// @p hitPos is not adjusted using the height detail data. + /// + /// @p hitDist will equal the search radius if there is no wall within the + /// radius. In this case the values of @p hitPos and @p hitNormal are + /// undefined. + /// + /// The normal will become unpredicable if @p hitDist is a very small number. + /// + /// Finds the distance from the specified position to the nearest polygon wall. + /// @param[in] startRef The reference id of the polygon containing @p centerPos. + /// @param[in] centerPos The center of the search circle. [(x, y, z)] + /// @param[in] maxRadius The radius of the search circle. + /// @param[in] filter The polygon filter to apply to the query. + /// @param[out] hitDist The distance to the nearest wall from @p centerPos. + /// @param[out] hitPos The nearest position on the wall that was hit. [(x, y, z)] + /// @param[out] hitNormal The normalized ray formed from the wall point to the + /// source point. [(x, y, z)] + /// @returns The status flags for the query. + public virtual Result findDistanceToWall(long startRef, float[] centerPos, float maxRadius, + QueryFilter filter) { + + // Validate input + if (!m_nav.isValidPolyRef(startRef) || null == centerPos || !vIsFinite(centerPos) || maxRadius < 0 + || !float.IsFinite(maxRadius) || null == filter) { + return Results.invalidParam(); + } + + m_nodePool.clear(); + m_openList.clear(); + + Node startNode = m_nodePool.getNode(startRef); + vCopy(startNode.pos, centerPos); + startNode.pidx = 0; + startNode.cost = 0; + startNode.total = 0; + startNode.id = startRef; + startNode.flags = Node.DT_NODE_OPEN; + m_openList.push(startNode); + + float radiusSqr = sqr(maxRadius); + float[] hitPos = new float[3]; + VectorPtr bestvj = null; + VectorPtr bestvi = null; + while (!m_openList.isEmpty()) { + Node bestNode = m_openList.pop(); + bestNode.flags &= ~Node.DT_NODE_OPEN; + bestNode.flags |= Node.DT_NODE_CLOSED; + + // Get poly and tile. + // The API input has been cheked already, skip checking internal data. + long bestRef = bestNode.id; + Tuple tileAndPoly = m_nav.getTileAndPolyByRefUnsafe(bestRef); + MeshTile bestTile = tileAndPoly.Item1; + Poly bestPoly = tileAndPoly.Item2; + + // Get parent poly and tile. + long parentRef = 0; + if (bestNode.pidx != 0) { + parentRef = m_nodePool.getNodeAtIdx(bestNode.pidx).id; + } + + // Hit test walls. + for (int i = 0, j = bestPoly.vertCount - 1; i < bestPoly.vertCount; j = i++) { + // Skip non-solid edges. + if ((bestPoly.neis[j] & NavMesh.DT_EXT_LINK) != 0) { + // Tile border. + bool solid = true; + for (int k = bestTile.polyLinks[bestPoly.index]; k != NavMesh.DT_NULL_LINK; k = bestTile.links[k].next) { + Link link = bestTile.links[k]; + if (link.edge == j) { + if (link.refs != 0) { + Tuple linkTileAndPoly = m_nav.getTileAndPolyByRefUnsafe(link.refs); + MeshTile neiTile = linkTileAndPoly.Item1; + Poly neiPoly = linkTileAndPoly.Item2; + if (filter.passFilter(link.refs, neiTile, neiPoly)) { + solid = false; + } + } + break; + } + } + if (!solid) { + continue; + } + } else if (bestPoly.neis[j] != 0) { + // Internal edge + int idx = (bestPoly.neis[j] - 1); + long refs = m_nav.getPolyRefBase(bestTile) | idx; + if (filter.passFilter(refs, bestTile, bestTile.data.polys[idx])) { + continue; + } + } + + // Calc distance to the edge. + int vj = bestPoly.verts[j] * 3; + int vi = bestPoly.verts[i] * 3; + Tuple distseg = distancePtSegSqr2D(centerPos, bestTile.data.verts, vj, vi); + float distSqr = distseg.Item1; + float tseg = distseg.Item2; + + // Edge is too far, skip. + if (distSqr > radiusSqr) { + continue; + } + + // Hit wall, update radius. + radiusSqr = distSqr; + // Calculate hit pos. + hitPos[0] = bestTile.data.verts[vj] + (bestTile.data.verts[vi] - bestTile.data.verts[vj]) * tseg; + hitPos[1] = bestTile.data.verts[vj + 1] + + (bestTile.data.verts[vi + 1] - bestTile.data.verts[vj + 1]) * tseg; + hitPos[2] = bestTile.data.verts[vj + 2] + + (bestTile.data.verts[vi + 2] - bestTile.data.verts[vj + 2]) * tseg; + bestvj = new VectorPtr(bestTile.data.verts, vj); + bestvi = new VectorPtr(bestTile.data.verts, vi); + } + + for (int i = bestTile.polyLinks[bestPoly.index]; i != NavMesh.DT_NULL_LINK; i = bestTile.links[i].next) { + Link link = bestTile.links[i]; + long neighbourRef = link.refs; + // Skip invalid neighbours and do not follow back to parent. + if (neighbourRef == 0 || neighbourRef == parentRef) { + continue; + } + + // Expand to neighbour. + Tuple neighbourTileAndPoly = m_nav.getTileAndPolyByRefUnsafe(neighbourRef); + MeshTile neighbourTile = neighbourTileAndPoly.Item1; + Poly neighbourPoly = neighbourTileAndPoly.Item2; + + // Skip off-mesh connections. + if (neighbourPoly.getType() == Poly.DT_POLYTYPE_OFFMESH_CONNECTION) { + continue; + } + + // Calc distance to the edge. + int va = bestPoly.verts[link.edge] * 3; + int vb = bestPoly.verts[(link.edge + 1) % bestPoly.vertCount] * 3; + Tuple distseg = distancePtSegSqr2D(centerPos, bestTile.data.verts, va, vb); + float distSqr = distseg.Item1; + // If the circle is not touching the next polygon, skip it. + if (distSqr > radiusSqr) { + continue; + } + + if (!filter.passFilter(neighbourRef, neighbourTile, neighbourPoly)) { + continue; + } + + Node neighbourNode = m_nodePool.getNode(neighbourRef); + + if ((neighbourNode.flags & Node.DT_NODE_CLOSED) != 0) { + continue; + } + + // Cost + if (neighbourNode.flags == 0) { + Result midPoint = getEdgeMidPoint(bestRef, bestPoly, bestTile, neighbourRef, neighbourPoly, + neighbourTile); + if (midPoint.succeeded()) { + neighbourNode.pos = midPoint.result; + } + } + + float total = bestNode.total + vDist(bestNode.pos, neighbourNode.pos); + + // The node is already in open list and the new result is worse, skip. + if ((neighbourNode.flags & Node.DT_NODE_OPEN) != 0 && total >= neighbourNode.total) { + continue; + } + + neighbourNode.id = neighbourRef; + neighbourNode.flags = (neighbourNode.flags & ~Node.DT_NODE_CLOSED); + neighbourNode.pidx = m_nodePool.getNodeIdx(bestNode); + neighbourNode.total = total; + + if ((neighbourNode.flags & Node.DT_NODE_OPEN) != 0) { + m_openList.modify(neighbourNode); + } else { + neighbourNode.flags |= Node.DT_NODE_OPEN; + m_openList.push(neighbourNode); + } + } + } + + // Calc hit normal. + float[] hitNormal = new float[3]; + if (bestvi != null && bestvj != null) { + float[] tangent = vSub(bestvi, bestvj); + hitNormal[0] = tangent[2]; + hitNormal[1] = 0; + hitNormal[2] = -tangent[0]; + vNormalize(hitNormal); + } + return Results.success(new FindDistanceToWallResult((float) Math.Sqrt(radiusSqr), hitPos, hitNormal)); + } + + /// Returns true if the polygon reference is valid and passes the filter restrictions. + /// @param[in] ref The polygon reference to check. + /// @param[in] filter The filter to apply. + public bool isValidPolyRef(long refs, QueryFilter filter) { + Result> tileAndPolyResult = m_nav.getTileAndPolyByRef(refs); + if (tileAndPolyResult.failed()) { + return false; + } + Tuple tileAndPoly = tileAndPolyResult.result; + // If cannot pass filter, assume flags has changed and boundary is invalid. + if (!filter.passFilter(refs, tileAndPoly.Item1, tileAndPoly.Item2)) { + return false; + } + return true; + } + + /// Gets the navigation mesh the query object is using. + /// @return The navigation mesh the query object is using. + public NavMesh getAttachedNavMesh() { + return m_nav; + } + + /** + * Gets a path from the explored nodes in the previous search. + * + * @param endRef + * The reference id of the end polygon. + * @returns An ordered list of polygon references representing the path. (Start to end.) + * @remarks The result of this function depends on the state of the query object. For that reason it should only be + * used immediately after one of the two Dijkstra searches, findPolysAroundCircle or findPolysAroundShape. + */ + public Result> getPathFromDijkstraSearch(long endRef) { + if (!m_nav.isValidPolyRef(endRef)) { + return Results.invalidParam>("Invalid end ref"); + } + List nodes = m_nodePool.findNodes(endRef); + if (nodes.Count != 1) { + return Results.invalidParam>("Invalid end ref"); + } + Node endNode = nodes[0]; + if ((endNode.flags & DT_NODE_CLOSED) == 0) { + return Results.invalidParam>("Invalid end ref"); + } + return Results.success(getPathToNode(endNode)); + } + + /** + * Gets the path leading to the specified end node. + */ + protected List getPathToNode(Node endNode) { + List path = new(); + // Reverse the path. + Node curNode = endNode; + do { + path.Insert(0, curNode.id); + Node nextNode = m_nodePool.getNodeAtIdx(curNode.pidx); + if (curNode.shortcut != null) { + // remove potential duplicates from shortcut path + for (int i = curNode.shortcut.Count - 1; i >=0; i--) { + long id = curNode.shortcut[i]; + if (id != curNode.id && id != nextNode.id) { + path.Insert(0, id); + } + } + } + curNode = nextNode; + } while (curNode != null); + + return path; + } + + /** + * The closed list is the list of polygons that were fully evaluated during the last navigation graph search. (A* or + * Dijkstra) + */ + public bool isInClosedList(long refs) { + if (m_nodePool == null) { + return false; + } + foreach (Node n in m_nodePool.findNodes(refs)) { + if ((n.flags & DT_NODE_CLOSED) != 0) { + return true; + } + } + return false; + } + + public NodePool getNodePool() { + return m_nodePool; + } + +} \ No newline at end of file diff --git a/src/DotRecast.Detour/Node.cs b/src/DotRecast.Detour/Node.cs new file mode 100644 index 0000000..b59a1be --- /dev/null +++ b/src/DotRecast.Detour/Node.cs @@ -0,0 +1,61 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.Collections.Generic; + +namespace DotRecast.Detour; + + +public class Node { + + public const int DT_NODE_OPEN = 0x01; + public const int DT_NODE_CLOSED = 0x02; + /** parent of the node is not adjacent. Found using raycast. */ + public const int DT_NODE_PARENT_DETACHED = 0x04; + + public readonly int index; + + /** Position of the node. */ + public float[] pos = new float[3]; + /** Cost of reaching the given node. */ + public float cost; + /** Total cost of reaching the goal via the given node including heuristics. */ + public float total; + /** Index to parent node. */ + public int pidx; + /** + * extra state information. A polyRef can have multiple nodes with different extra info. see DT_MAX_STATES_PER_NODE + */ + public int state; + /** Node flags. A combination of dtNodeFlags. */ + public int flags; + /** Polygon ref the node corresponds to. */ + public long id; + /** Shortcut found by raycast. */ + public List shortcut; + + public Node(int index) { + this.index = index; + } + + public override string ToString() { + return "Node [id=" + id + "]"; + } + +} diff --git a/src/DotRecast.Detour/NodePool.cs b/src/DotRecast.Detour/NodePool.cs new file mode 100644 index 0000000..d0a3062 --- /dev/null +++ b/src/DotRecast.Detour/NodePool.cs @@ -0,0 +1,98 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.Collections.Generic; + +namespace DotRecast.Detour; + + +public class NodePool +{ + + private readonly Dictionary> m_map = new(); + private readonly List m_nodes = new(); + + public NodePool() { + + } + + public void clear() { + m_nodes.Clear(); + m_map.Clear(); + } + + public List findNodes(long id) { + var hasNode = m_map.TryGetValue(id, out var nodes);; + if (nodes == null) { + nodes = new(); + } + return nodes; + } + + public Node findNode(long id) { + var hasNode = m_map.TryGetValue(id, out var nodes);; + if (nodes != null && 0 != nodes.Count) { + return nodes[0]; + } + return null; + } + + public Node getNode(long id, int state) { + var hasNode = m_map.TryGetValue(id, out var nodes);; + if (nodes != null) { + foreach (Node node in nodes) { + if (node.state == state) { + return node; + } + } + } + return create(id, state); + } + + protected Node create(long id, int state) { + Node node = new Node(m_nodes.Count + 1); + node.id = id; + node.state = state; + m_nodes.Add(node); + var hasNode = m_map.TryGetValue(id, out var nodes);; + if (nodes == null) { + nodes = new(); + m_map.Add(id, nodes); + } + nodes.Add(node); + return node; + } + + public int getNodeIdx(Node node) { + return node != null ? node.index : 0; + } + + public Node getNodeAtIdx(int idx) { + return idx != 0 ? m_nodes[idx - 1] : null; + } + + public Node getNode(long refs) { + return getNode(refs, 0); + } + + public Dictionary> getNodeMap() { + return m_map; + } + +} diff --git a/src/DotRecast.Detour/NodeQueue.cs b/src/DotRecast.Detour/NodeQueue.cs new file mode 100644 index 0000000..9c7af75 --- /dev/null +++ b/src/DotRecast.Detour/NodeQueue.cs @@ -0,0 +1,62 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Detour; + +using System.Collections.Generic; + +public class NodeQueue { + + private readonly List m_heap = new(); + + public int count() + { + return m_heap.Count; + } + + public void clear() { + m_heap.Clear(); + } + + public Node top() + { + return m_heap[0]; + } + + public Node pop() + { + var node = top(); + m_heap.Remove(node); + return node; + } + + public void push(Node node) { + m_heap.Add(node); + m_heap.Sort((x, y) => x.total.CompareTo(y.total)); + } + + public void modify(Node node) { + m_heap.Remove(node); + push(node); + } + + public bool isEmpty() + { + return 0 == m_heap.Count; + } +} diff --git a/src/DotRecast.Detour/OffMeshConnection.cs b/src/DotRecast.Detour/OffMeshConnection.cs new file mode 100644 index 0000000..7bb70e3 --- /dev/null +++ b/src/DotRecast.Detour/OffMeshConnection.cs @@ -0,0 +1,43 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Detour; + +/** + * Defines an navigation mesh off-mesh connection within a dtMeshTile object. An off-mesh connection is a user defined + * traversable connection made up to two vertices. + */ +public class OffMeshConnection { + /** The endpoints of the connection. [(ax, ay, az, bx, by, bz)] */ + public float[] pos = new float[6]; + /** The radius of the endpoints. [Limit: >= 0] */ + public float rad; + /** The polygon reference of the connection within the tile. */ + public int poly; + /** + * Link flags. + * + * @note These are not the connection's user defined flags. Those are assigned via the connection's Poly definition. + * These are link flags used for internal purposes. + */ + public int flags; + /** End point side. */ + public int side; + /** The id of the offmesh connection. (User assigned when the navigation mesh is built.) */ + public int userId; +} \ No newline at end of file diff --git a/src/DotRecast.Detour/Poly.cs b/src/DotRecast.Detour/Poly.cs new file mode 100644 index 0000000..7e78e88 --- /dev/null +++ b/src/DotRecast.Detour/Poly.cs @@ -0,0 +1,70 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Detour; + +/** Defines a polygon within a MeshTile object. */ +public class Poly { + + public readonly int index; + /** The polygon is a standard convex polygon that is part of the surface of the mesh. */ + public const int DT_POLYTYPE_GROUND = 0; + /** The polygon is an off-mesh connection consisting of two vertices. */ + public const int DT_POLYTYPE_OFFMESH_CONNECTION = 1; + /** The indices of the polygon's vertices. The actual vertices are located in MeshTile::verts. */ + public readonly int[] verts; + /** Packed data representing neighbor polygons references and flags for each edge. */ + public readonly int[] neis; + /** The user defined polygon flags. */ + public int flags; + /** The number of vertices in the polygon. */ + public int vertCount; + /** + * The bit packed area id and polygon type. + * + * @note Use the structure's set and get methods to access this value. + */ + public int areaAndtype; + + public Poly(int index, int maxVertsPerPoly) { + this.index = index; + verts = new int[maxVertsPerPoly]; + neis = new int[maxVertsPerPoly]; + } + + /** Sets the user defined area id. [Limit: < {@link org.recast4j.detour.NavMesh#DT_MAX_AREAS}] */ + public void setArea(int a) { + areaAndtype = (areaAndtype & 0xc0) | (a & 0x3f); + } + + /** Sets the polygon type. (See: #dtPolyTypes.) */ + public void setType(int t) { + areaAndtype = (areaAndtype & 0x3f) | (t << 6); + } + + /** Gets the user defined area id. */ + public int getArea() { + return areaAndtype & 0x3f; + } + + /** Gets the polygon type. (See: #dtPolyTypes) */ + public int getType() { + return areaAndtype >> 6; + } + +} diff --git a/src/DotRecast.Detour/PolyDetail.cs b/src/DotRecast.Detour/PolyDetail.cs new file mode 100644 index 0000000..1306202 --- /dev/null +++ b/src/DotRecast.Detour/PolyDetail.cs @@ -0,0 +1,31 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Detour; + +/** Defines the location of detail sub-mesh data within a dtMeshTile. */ +public class PolyDetail { + /** The offset of the vertices in the MeshTile::detailVerts array. */ + public int vertBase; + /** The offset of the triangles in the MeshTile::detailTris array. */ + public int triBase; + /** The number of vertices in the sub-mesh. */ + public int vertCount; + /** The number of triangles in the sub-mesh. */ + public int triCount; +} diff --git a/src/DotRecast.Detour/PolyQuery.cs b/src/DotRecast.Detour/PolyQuery.cs new file mode 100644 index 0000000..c99a430 --- /dev/null +++ b/src/DotRecast.Detour/PolyQuery.cs @@ -0,0 +1,6 @@ +namespace DotRecast.Detour; + +public interface PolyQuery { + + void process(MeshTile tile, Poly poly, long refs); +} diff --git a/src/DotRecast.Detour/PolygonByCircleConstraint.cs b/src/DotRecast.Detour/PolygonByCircleConstraint.cs new file mode 100644 index 0000000..6ae4420 --- /dev/null +++ b/src/DotRecast.Detour/PolygonByCircleConstraint.cs @@ -0,0 +1,94 @@ +/* +recast4j copyright (c) 2021 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; + +namespace DotRecast.Detour; + +using static DetourCommon; + +public interface PolygonByCircleConstraint { + + float[] aply(float[] polyVerts, float[] circleCenter, float radius); + + public static PolygonByCircleConstraint noop() { + return new NoOpPolygonByCircleConstraint(); + } + + public static PolygonByCircleConstraint strict() { + return new StrictPolygonByCircleConstraint(); + } + + public class NoOpPolygonByCircleConstraint : PolygonByCircleConstraint { + + public float[] aply(float[] polyVerts, float[] circleCenter, float radius) { + return polyVerts; + } + + } + + /** + * Calculate the intersection between a polygon and a circle. A dodecagon is used as an approximation of the circle. + */ + public class StrictPolygonByCircleConstraint : PolygonByCircleConstraint { + + private const int CIRCLE_SEGMENTS = 12; + private static float[] unitCircle; + + public float[] aply(float[] verts, float[] center, float radius) { + float radiusSqr = radius * radius; + int outsideVertex = -1; + for (int pv = 0; pv < verts.Length; pv += 3) { + if (vDist2DSqr(center, verts, pv) > radiusSqr) { + outsideVertex = pv; + break; + } + } + if (outsideVertex == -1) { + // polygon inside circle + return verts; + } + float[] qCircle = circle(center, radius); + float[] intersection = ConvexConvexIntersection.intersect(verts, qCircle); + if (intersection == null && pointInPolygon(center, verts, verts.Length / 3)) { + // circle inside polygon + return qCircle; + } + return intersection; + } + + private float[] circle(float[] center, float radius) { + if (unitCircle == null) { + unitCircle = new float[CIRCLE_SEGMENTS * 3]; + for (int i = 0; i < CIRCLE_SEGMENTS; i++) { + double a = i * Math.PI * 2 / CIRCLE_SEGMENTS; + unitCircle[3 * i] = (float) Math.Cos(a); + unitCircle[3 * i + 1] = 0; + unitCircle[3 * i + 2] = (float) -Math.Sin(a); + } + } + float[] circle = new float[12 * 3]; + for (int i = 0; i < CIRCLE_SEGMENTS * 3; i += 3) { + circle[i] = unitCircle[i] * radius + center[0]; + circle[i + 1] = center[1]; + circle[i + 2] = unitCircle[i + 2] * radius + center[2]; + } + return circle; + } + } +} diff --git a/src/DotRecast.Detour/QueryData.cs b/src/DotRecast.Detour/QueryData.cs new file mode 100644 index 0000000..a5c17b0 --- /dev/null +++ b/src/DotRecast.Detour/QueryData.cs @@ -0,0 +1,32 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Detour; + +public class QueryData { + public Status status; + public Node lastBestNode; + public float lastBestNodeCost; + public long startRef, endRef; + public float[] startPos = new float[3]; + public float[] endPos = new float[3]; + public QueryFilter filter; + public int options; + public float raycastLimitSqr; + public QueryHeuristic heuristic; +} \ No newline at end of file diff --git a/src/DotRecast.Detour/QueryFilter.cs b/src/DotRecast.Detour/QueryFilter.cs new file mode 100644 index 0000000..7263215 --- /dev/null +++ b/src/DotRecast.Detour/QueryFilter.cs @@ -0,0 +1,28 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Detour; + +public interface QueryFilter { + + bool passFilter(long refs, MeshTile tile, Poly poly); + + float getCost(float[] pa, float[] pb, long prevRef, MeshTile prevTile, Poly prevPoly, long curRef, MeshTile curTile, + Poly curPoly, long nextRef, MeshTile nextTile, Poly nextPoly); +} + diff --git a/src/DotRecast.Detour/QueryHeuristic.cs b/src/DotRecast.Detour/QueryHeuristic.cs new file mode 100644 index 0000000..5106d88 --- /dev/null +++ b/src/DotRecast.Detour/QueryHeuristic.cs @@ -0,0 +1,25 @@ +/* +recast4j copyright (c) 2021 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +namespace DotRecast.Detour; + +public interface QueryHeuristic { + + float getCost(float[] neighbourPos, float[] endPos); + +} diff --git a/src/DotRecast.Detour/RaycastHit.cs b/src/DotRecast.Detour/RaycastHit.cs new file mode 100644 index 0000000..c242bbf --- /dev/null +++ b/src/DotRecast.Detour/RaycastHit.cs @@ -0,0 +1,39 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.Collections.Generic; + +namespace DotRecast.Detour; + + +/** + * Provides information about raycast hit. Filled by NavMeshQuery::raycast + */ +public class RaycastHit { + /** The hit parameter. (float.MaxValue if no wall hit.) */ + public float t; + /** hitNormal The normal of the nearest wall hit. [(x, y, z)] */ + public readonly float[] hitNormal = new float[3]; + /** Visited polygons. */ + public readonly List path = new(); + /** The cost of the path until hit. */ + public float pathCost; + /** The index of the edge on the readonly polygon where the wall was hit. */ + public int hitEdgeIndex; +} diff --git a/src/DotRecast.Detour/Result.cs b/src/DotRecast.Detour/Result.cs new file mode 100644 index 0000000..7ac1db6 --- /dev/null +++ b/src/DotRecast.Detour/Result.cs @@ -0,0 +1,82 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Detour; + +public static class Results +{ + public static Result success(T result) { + return new Result(result, Status.SUCCSESS, null); + } + + public static Result failure() { + return new Result(default, Status.FAILURE, null); + } + + public static Result invalidParam() { + return new Result(default, Status.FAILURE_INVALID_PARAM, null); + } + + public static Result failure(string message) { + return new Result(default, Status.FAILURE, message); + } + + public static Result invalidParam(string message) { + return new Result(default, Status.FAILURE_INVALID_PARAM, message); + } + + public static Result failure(T result) { + return new Result(result, Status.FAILURE, null); + } + + public static Result partial(T result) { + return new Result(default, Status.PARTIAL_RESULT, null); + } + + public static Result of(Status status, string message) { + return new Result(default, status, message); + } + + public static Result of(Status status, T result) { + return new Result(result, status, null); + } + +} + +public class Result { + + public readonly T result; + public readonly Status status; + public readonly string message; + + internal Result(T result, Status status, string message) { + this.result = result; + this.status = status; + this.message = message; + } + + + public bool failed() { + return status.isFailed(); + } + + public bool succeeded() { + return status.isSuccess(); + } + +} diff --git a/src/DotRecast.Detour/Status.cs b/src/DotRecast.Detour/Status.cs new file mode 100644 index 0000000..71d1daa --- /dev/null +++ b/src/DotRecast.Detour/Status.cs @@ -0,0 +1,54 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Detour; + +public class Status { + public static Status FAILURE = new(0); + public static Status SUCCSESS = new(1); + public static Status IN_PROGRESS = new(2); + public static Status PARTIAL_RESULT = new(3); + public static Status FAILURE_INVALID_PARAM = new(4); + + public int Value { get; } + + private Status(int vlaue) + { + Value = vlaue; + } +} + +public static class StatusEx +{ + public static bool isFailed(this Status @this) { + return @this == Status.FAILURE || @this == Status.FAILURE_INVALID_PARAM; + } + + public static bool isInProgress(this Status @this) { + return @this == Status.IN_PROGRESS; + } + + public static bool isSuccess(this Status @this) { + return @this == Status.SUCCSESS || @this == Status.PARTIAL_RESULT; + } + + public static bool isPartial(this Status @this) { + return @this == Status.PARTIAL_RESULT; + } + +} diff --git a/src/DotRecast.Detour/StraightPathItem.cs b/src/DotRecast.Detour/StraightPathItem.cs new file mode 100644 index 0000000..9578efb --- /dev/null +++ b/src/DotRecast.Detour/StraightPathItem.cs @@ -0,0 +1,47 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Detour; + +using static DetourCommon; + +//TODO: (PP) Add comments +public class StraightPathItem { + public float[] pos; + public int flags; + public long refs; + + public StraightPathItem(float[] pos, int flags, long refs) { + this.pos = vCopy(pos); + this.flags = flags; + this.refs = refs; + } + + public float[] getPos() { + return pos; + } + + public int getFlags() { + return flags; + } + + public long getRef() { + return refs; + } + +} \ No newline at end of file diff --git a/src/DotRecast.Detour/VectorPtr.cs b/src/DotRecast.Detour/VectorPtr.cs new file mode 100644 index 0000000..c4457f3 --- /dev/null +++ b/src/DotRecast.Detour/VectorPtr.cs @@ -0,0 +1,46 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +namespace DotRecast.Detour; + +/** + * Wrapper for 3-element pieces (3D vectors) of a bigger float array. + * + */ +public class VectorPtr +{ + private readonly float[] array; + private readonly int index; + + public VectorPtr(float[] array) : + this(array, 0) + { + } + + public VectorPtr(float[] array, int index) + { + this.array = array; + this.index = index; + } + + public float get(int offset) + { + return array[index + offset]; + } +} \ No newline at end of file diff --git a/src/DotRecast.Recast.Demo/Builder/AbstractNavMeshBuilder.cs b/src/DotRecast.Recast.Demo/Builder/AbstractNavMeshBuilder.cs new file mode 100644 index 0000000..5e578ab --- /dev/null +++ b/src/DotRecast.Recast.Demo/Builder/AbstractNavMeshBuilder.cs @@ -0,0 +1,78 @@ +using DotRecast.Detour; +using DotRecast.Recast.Demo.Geom; + +namespace DotRecast.Recast.Demo.Builder; + +public abstract class AbstractNavMeshBuilder { + + protected NavMeshDataCreateParams getNavMeshCreateParams(DemoInputGeomProvider m_geom, float m_cellSize, + float m_cellHeight, float m_agentHeight, float m_agentRadius, float m_agentMaxClimb, + RecastBuilderResult rcResult) { + PolyMesh m_pmesh = rcResult.getMesh(); + PolyMeshDetail m_dmesh = rcResult.getMeshDetail(); + NavMeshDataCreateParams option = new NavMeshDataCreateParams(); + for (int i = 0; i < m_pmesh.npolys; ++i) { + m_pmesh.flags[i] = 1; + } + option.verts = m_pmesh.verts; + option.vertCount = m_pmesh.nverts; + option.polys = m_pmesh.polys; + option.polyAreas = m_pmesh.areas; + option.polyFlags = m_pmesh.flags; + option.polyCount = m_pmesh.npolys; + option.nvp = m_pmesh.nvp; + if (m_dmesh != null) { + option.detailMeshes = m_dmesh.meshes; + option.detailVerts = m_dmesh.verts; + option.detailVertsCount = m_dmesh.nverts; + option.detailTris = m_dmesh.tris; + option.detailTriCount = m_dmesh.ntris; + } + option.walkableHeight = m_agentHeight; + option.walkableRadius = m_agentRadius; + option.walkableClimb = m_agentMaxClimb; + option.bmin = m_pmesh.bmin; + option.bmax = m_pmesh.bmax; + option.cs = m_cellSize; + option.ch = m_cellHeight; + option.buildBvTree = true; + + option.offMeshConCount = m_geom.getOffMeshConnections().Count; + option.offMeshConVerts = new float[option.offMeshConCount * 6]; + option.offMeshConRad = new float[option.offMeshConCount]; + option.offMeshConDir = new int[option.offMeshConCount]; + option.offMeshConAreas = new int[option.offMeshConCount]; + option.offMeshConFlags = new int[option.offMeshConCount]; + option.offMeshConUserID = new int[option.offMeshConCount]; + for (int i = 0; i < option.offMeshConCount; i++) { + DemoOffMeshConnection offMeshCon = m_geom.getOffMeshConnections()[i]; + for (int j = 0; j < 6; j++) { + option.offMeshConVerts[6 * i + j] = offMeshCon.verts[j]; + } + option.offMeshConRad[i] = offMeshCon.radius; + option.offMeshConDir[i] = offMeshCon.bidir ? 1 : 0; + option.offMeshConAreas[i] = offMeshCon.area; + option.offMeshConFlags[i] = offMeshCon.flags; + } + return option; + } + + protected MeshData updateAreaAndFlags(MeshData meshData) { + // Update poly flags from areas. + for (int i = 0; i < meshData.polys.Length; ++i) { + if (meshData.polys[i].getArea() == SampleAreaModifications.SAMPLE_POLYAREA_TYPE_WALKABLE) { + meshData.polys[i].setArea(SampleAreaModifications.SAMPLE_POLYAREA_TYPE_GROUND); + } + if (meshData.polys[i].getArea() == SampleAreaModifications.SAMPLE_POLYAREA_TYPE_GROUND + || meshData.polys[i].getArea() == SampleAreaModifications.SAMPLE_POLYAREA_TYPE_GRASS + || meshData.polys[i].getArea() == SampleAreaModifications.SAMPLE_POLYAREA_TYPE_ROAD) { + meshData.polys[i].flags = SampleAreaModifications.SAMPLE_POLYFLAGS_WALK; + } else if (meshData.polys[i].getArea() == SampleAreaModifications.SAMPLE_POLYAREA_TYPE_WATER) { + meshData.polys[i].flags = SampleAreaModifications.SAMPLE_POLYFLAGS_SWIM; + } else if (meshData.polys[i].getArea() == SampleAreaModifications.SAMPLE_POLYAREA_TYPE_DOOR) { + meshData.polys[i].flags = SampleAreaModifications.SAMPLE_POLYFLAGS_DOOR; + } + } + return meshData; + } +} diff --git a/src/DotRecast.Recast.Demo/Builder/SampleAreaModifications.cs b/src/DotRecast.Recast.Demo/Builder/SampleAreaModifications.cs new file mode 100644 index 0000000..16f2cfd --- /dev/null +++ b/src/DotRecast.Recast.Demo/Builder/SampleAreaModifications.cs @@ -0,0 +1,46 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Recast.Demo.Builder; + +public class SampleAreaModifications { + + public const int SAMPLE_POLYAREA_TYPE_GROUND = 0x0; + public const int SAMPLE_POLYAREA_TYPE_WATER = 0x1; + public const int SAMPLE_POLYAREA_TYPE_ROAD = 0x2; + public const int SAMPLE_POLYAREA_TYPE_DOOR = 0x3; + public const int SAMPLE_POLYAREA_TYPE_GRASS = 0x4; + public const int SAMPLE_POLYAREA_TYPE_JUMP = 0x5; + public const int SAMPLE_POLYAREA_TYPE_JUMP_AUTO = 0x6; + public const int SAMPLE_POLYAREA_TYPE_WALKABLE = 0x3f; + + public static AreaModification SAMPLE_AREAMOD_WALKABLE = new AreaModification(SAMPLE_POLYAREA_TYPE_WALKABLE); + public static AreaModification SAMPLE_AREAMOD_GROUND = new AreaModification(SAMPLE_POLYAREA_TYPE_GROUND); + public static AreaModification SAMPLE_AREAMOD_WATER = new AreaModification(SAMPLE_POLYAREA_TYPE_WATER); + public static AreaModification SAMPLE_AREAMOD_ROAD = new AreaModification(SAMPLE_POLYAREA_TYPE_ROAD); + public static AreaModification SAMPLE_AREAMOD_GRASS = new AreaModification(SAMPLE_POLYAREA_TYPE_GRASS); + public static AreaModification SAMPLE_AREAMOD_DOOR = new AreaModification(SAMPLE_POLYAREA_TYPE_DOOR); + public static AreaModification SAMPLE_AREAMOD_JUMP = new AreaModification(SAMPLE_POLYAREA_TYPE_JUMP); + + public static readonly int SAMPLE_POLYFLAGS_WALK = 0x01; // Ability to walk (ground, grass, road) + public static readonly int SAMPLE_POLYFLAGS_SWIM = 0x02; // Ability to swim (water). + public static readonly int SAMPLE_POLYFLAGS_DOOR = 0x04; // Ability to move through doors. + public static readonly int SAMPLE_POLYFLAGS_JUMP = 0x08; // Ability to jump. + public static readonly int SAMPLE_POLYFLAGS_DISABLED = 0x10; // Disabled polygon + public static readonly int SAMPLE_POLYFLAGS_ALL = 0xffff; // All abilities. +} diff --git a/src/DotRecast.Recast.Demo/Builder/SoloNavMeshBuilder.cs b/src/DotRecast.Recast.Demo/Builder/SoloNavMeshBuilder.cs new file mode 100644 index 0000000..914dcb3 --- /dev/null +++ b/src/DotRecast.Recast.Demo/Builder/SoloNavMeshBuilder.cs @@ -0,0 +1,70 @@ +/* +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using DotRecast.Detour; +using DotRecast.Recast.Demo.Geom; + +namespace DotRecast.Recast.Demo.Builder; + +public class SoloNavMeshBuilder : AbstractNavMeshBuilder { + + public Tuple, NavMesh> build(DemoInputGeomProvider m_geom, PartitionType m_partitionType, + float m_cellSize, float m_cellHeight, float m_agentHeight, float m_agentRadius, float m_agentMaxClimb, + float m_agentMaxSlope, int m_regionMinSize, int m_regionMergeSize, float m_edgeMaxLen, float m_edgeMaxError, + int m_vertsPerPoly, float m_detailSampleDist, float m_detailSampleMaxError, bool filterLowHangingObstacles, + bool filterLedgeSpans, bool filterWalkableLowHeightSpans) { + + RecastBuilderResult rcResult = buildRecastResult(m_geom, m_partitionType, m_cellSize, m_cellHeight, m_agentHeight, + m_agentRadius, m_agentMaxClimb, m_agentMaxSlope, m_regionMinSize, m_regionMergeSize, m_edgeMaxLen, m_edgeMaxError, + m_vertsPerPoly, m_detailSampleDist, m_detailSampleMaxError, filterLowHangingObstacles, filterLedgeSpans, + filterWalkableLowHeightSpans); + return Tuple.Create(ImmutableArray.Create(rcResult) as IList, + buildNavMesh( + buildMeshData(m_geom, m_cellSize, m_cellHeight, m_agentHeight, m_agentRadius, m_agentMaxClimb, rcResult), + m_vertsPerPoly)); + } + + private NavMesh buildNavMesh(MeshData meshData, int m_vertsPerPoly) { + return new NavMesh(meshData, m_vertsPerPoly, 0); + } + + private RecastBuilderResult buildRecastResult(DemoInputGeomProvider m_geom, PartitionType m_partitionType, float m_cellSize, + float m_cellHeight, float m_agentHeight, float m_agentRadius, float m_agentMaxClimb, float m_agentMaxSlope, + int m_regionMinSize, int m_regionMergeSize, float m_edgeMaxLen, float m_edgeMaxError, int m_vertsPerPoly, + float m_detailSampleDist, float m_detailSampleMaxError, bool filterLowHangingObstacles, bool filterLedgeSpans, + bool filterWalkableLowHeightSpans) { + RecastConfig cfg = new RecastConfig(m_partitionType, m_cellSize, m_cellHeight, m_agentMaxSlope, filterLowHangingObstacles, + filterLedgeSpans, filterWalkableLowHeightSpans, m_agentHeight, m_agentRadius, m_agentMaxClimb, m_regionMinSize, + m_regionMergeSize, m_edgeMaxLen, m_edgeMaxError, m_vertsPerPoly, m_detailSampleDist, m_detailSampleMaxError, + SampleAreaModifications.SAMPLE_AREAMOD_WALKABLE, true); + RecastBuilderConfig bcfg = new RecastBuilderConfig(cfg, m_geom.getMeshBoundsMin(), m_geom.getMeshBoundsMax()); + RecastBuilder rcBuilder = new RecastBuilder(); + return rcBuilder.build(m_geom, bcfg); + } + + private MeshData buildMeshData(DemoInputGeomProvider m_geom, float m_cellSize, float m_cellHeight, float m_agentHeight, + float m_agentRadius, float m_agentMaxClimb, RecastBuilderResult rcResult) { + NavMeshDataCreateParams option = getNavMeshCreateParams(m_geom, m_cellSize, m_cellHeight, m_agentHeight, m_agentRadius, + m_agentMaxClimb, rcResult); + return updateAreaAndFlags(NavMeshBuilder.createNavMeshData(option)); + } + +} diff --git a/src/DotRecast.Recast.Demo/Builder/TileNavMeshBuilder.cs b/src/DotRecast.Recast.Demo/Builder/TileNavMeshBuilder.cs new file mode 100644 index 0000000..26b87da --- /dev/null +++ b/src/DotRecast.Recast.Demo/Builder/TileNavMeshBuilder.cs @@ -0,0 +1,127 @@ +/* +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using DotRecast.Core; +using DotRecast.Detour; +using DotRecast.Recast.Demo.Geom; + +namespace DotRecast.Recast.Demo.Builder; + +public class TileNavMeshBuilder : AbstractNavMeshBuilder { + + public TileNavMeshBuilder() { + } + + public Tuple, NavMesh> build(DemoInputGeomProvider m_geom, PartitionType m_partitionType, + float m_cellSize, float m_cellHeight, float m_agentHeight, float m_agentRadius, float m_agentMaxClimb, + float m_agentMaxSlope, int m_regionMinSize, int m_regionMergeSize, float m_edgeMaxLen, float m_edgeMaxError, + int m_vertsPerPoly, float m_detailSampleDist, float m_detailSampleMaxError, bool filterLowHangingObstacles, + bool filterLedgeSpans, bool filterWalkableLowHeightSpans, int tileSize) { + + List rcResult = buildRecastResult(m_geom, m_partitionType, m_cellSize, m_cellHeight, m_agentHeight, + m_agentRadius, m_agentMaxClimb, m_agentMaxSlope, m_regionMinSize, m_regionMergeSize, m_edgeMaxLen, m_edgeMaxError, + m_vertsPerPoly, m_detailSampleDist, m_detailSampleMaxError, filterLowHangingObstacles, filterLedgeSpans, + filterWalkableLowHeightSpans, tileSize); + return Tuple.Create((IList) rcResult, + buildNavMesh(m_geom, + buildMeshData(m_geom, m_cellSize, m_cellHeight, m_agentHeight, m_agentRadius, m_agentMaxClimb, rcResult), + m_cellSize, tileSize, m_vertsPerPoly)); + } + + private List buildRecastResult(DemoInputGeomProvider m_geom, PartitionType m_partitionType, + float m_cellSize, float m_cellHeight, float m_agentHeight, float m_agentRadius, float m_agentMaxClimb, + float m_agentMaxSlope, int m_regionMinSize, int m_regionMergeSize, float m_edgeMaxLen, float m_edgeMaxError, + int m_vertsPerPoly, float m_detailSampleDist, float m_detailSampleMaxError, bool filterLowHangingObstacles, + bool filterLedgeSpans, bool filterWalkableLowHeightSpans, int tileSize) { + RecastConfig cfg = new RecastConfig(true, tileSize, tileSize, RecastConfig.calcBorder(m_agentRadius, m_cellSize), + m_partitionType, m_cellSize, m_cellHeight, m_agentMaxSlope, filterLowHangingObstacles, filterLedgeSpans, + filterWalkableLowHeightSpans, m_agentHeight, m_agentRadius, m_agentMaxClimb, + m_regionMinSize * m_regionMinSize * m_cellSize * m_cellSize, + m_regionMergeSize * m_regionMergeSize * m_cellSize * m_cellSize, m_edgeMaxLen, m_edgeMaxError, m_vertsPerPoly, + true, m_detailSampleDist, m_detailSampleMaxError, SampleAreaModifications.SAMPLE_AREAMOD_WALKABLE); + RecastBuilder rcBuilder = new RecastBuilder(); + return rcBuilder.buildTiles(m_geom, cfg, Task.Factory); + } + + private NavMesh buildNavMesh(DemoInputGeomProvider geom, List meshData, float cellSize, int tileSize, + int vertsPerPoly) { + NavMeshParams navMeshParams = new NavMeshParams(); + navMeshParams.orig[0] = geom.getMeshBoundsMin()[0]; + navMeshParams.orig[1] = geom.getMeshBoundsMin()[1]; + navMeshParams.orig[2] = geom.getMeshBoundsMin()[2]; + navMeshParams.tileWidth = tileSize * cellSize; + navMeshParams.tileHeight = tileSize * cellSize; + + // snprintf(text, 64, "Tiles %d x %d", tw, th); + + navMeshParams.maxTiles = getMaxTiles(geom, cellSize, tileSize); + navMeshParams.maxPolys = getMaxPolysPerTile(geom, cellSize, tileSize); + NavMesh navMesh = new NavMesh(navMeshParams, vertsPerPoly); + meshData.forEach(md => navMesh.addTile(md, 0, 0)); + return navMesh; + } + + public int getMaxTiles(DemoInputGeomProvider geom, float cellSize, int tileSize) { + int tileBits = getTileBits(geom, cellSize, tileSize); + return 1 << tileBits; + } + + public int getMaxPolysPerTile(DemoInputGeomProvider geom, float cellSize, int tileSize) { + int polyBits = 22 - getTileBits(geom, cellSize, tileSize); + return 1 << polyBits; + } + + private int getTileBits(DemoInputGeomProvider geom, float cellSize, int tileSize) { + int[] wh = Recast.calcGridSize(geom.getMeshBoundsMin(), geom.getMeshBoundsMax(), cellSize); + int tw = (wh[0] + tileSize - 1) / tileSize; + int th = (wh[1] + tileSize - 1) / tileSize; + int tileBits = Math.Min(DetourCommon.ilog2(DetourCommon.nextPow2(tw * th)), 14); + return tileBits; + } + + public int[] getTiles(DemoInputGeomProvider geom, float cellSize, int tileSize) { + int[] wh = Recast.calcGridSize(geom.getMeshBoundsMin(), geom.getMeshBoundsMax(), cellSize); + int tw = (wh[0] + tileSize - 1) / tileSize; + int th = (wh[1] + tileSize - 1) / tileSize; + return new int[] { tw, th }; + } + + private List buildMeshData(DemoInputGeomProvider m_geom, float m_cellSize, float m_cellHeight, float m_agentHeight, + float m_agentRadius, float m_agentMaxClimb, List rcResult) { + + // Add tiles to nav mesh + List meshData = new(); + foreach (RecastBuilderResult result in rcResult) { + int x = result.tileX; + int z = result.tileZ; + NavMeshDataCreateParams option = getNavMeshCreateParams(m_geom, m_cellSize, m_cellHeight, m_agentHeight, + m_agentRadius, m_agentMaxClimb, result); + option.tileX = x; + option.tileZ = z; + MeshData md = NavMeshBuilder.createNavMeshData(option); + if (md != null) { + meshData.Add(updateAreaAndFlags(md)); + } + } + return meshData; + } + +} diff --git a/src/DotRecast.Recast.Demo/DotRecast.Recast.Demo.csproj b/src/DotRecast.Recast.Demo/DotRecast.Recast.Demo.csproj new file mode 100644 index 0000000..059e5da --- /dev/null +++ b/src/DotRecast.Recast.Demo/DotRecast.Recast.Demo.csproj @@ -0,0 +1,24 @@ + + + + Exe + net7.0 + true + + + + + + + + + + + + + + + + + + diff --git a/src/DotRecast.Recast.Demo/Draw/DebugDraw.cs b/src/DotRecast.Recast.Demo/Draw/DebugDraw.cs new file mode 100644 index 0000000..a2283e6 --- /dev/null +++ b/src/DotRecast.Recast.Demo/Draw/DebugDraw.cs @@ -0,0 +1,603 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using DotRecast.Core; +using DotRecast.Recast.Demo.Builder; +using Silk.NET.OpenGL; + +namespace DotRecast.Recast.Demo.Draw; + +public class DebugDraw { + + private readonly GLCheckerTexture g_tex = new GLCheckerTexture(); + private readonly OpenGLDraw openGlDraw = new ModernOpenGLDraw(); + private readonly int[] boxIndices = { 7, 6, 5, 4, 0, 1, 2, 3, 1, 5, 6, 2, 3, 7, 4, 0, 2, 6, 7, 3, 0, 4, 5, 1, }; + + private readonly float[][] frustumPlanes = + { + new[] { 0f, 0f, 0f, 0f }, + new[] { 0f, 0f, 0f, 0f }, + new[] { 0f, 0f, 0f, 0f }, + new[] { 0f, 0f, 0f, 0f }, + new[] { 0f, 0f, 0f, 0f }, + new[] { 0f, 0f, 0f, 0f }, + }; + + + public void begin(DebugDrawPrimitives prim) { + begin(prim, 1f); + } + + public void debugDrawCylinderWire(float minx, float miny, float minz, float maxx, float maxy, float maxz, int col, + float lineWidth) { + begin(DebugDrawPrimitives.LINES, lineWidth); + appendCylinderWire(minx, miny, minz, maxx, maxy, maxz, col); + end(); + } + + private const int CYLINDER_NUM_SEG = 16; + private readonly float[] cylinderDir = new float[CYLINDER_NUM_SEG * 2]; + private bool cylinderInit = false; + + private void initCylinder() { + if (!cylinderInit) { + cylinderInit = true; + for (int i = 0; i < CYLINDER_NUM_SEG; ++i) { + float a = (float) (i * Math.PI * 2 / CYLINDER_NUM_SEG); + cylinderDir[i * 2] = (float) Math.Cos(a); + cylinderDir[i * 2 + 1] = (float) Math.Sin(a); + } + } + } + + void appendCylinderWire(float minx, float miny, float minz, float maxx, float maxy, float maxz, int col) { + + initCylinder(); + + float cx = (maxx + minx) / 2; + float cz = (maxz + minz) / 2; + float rx = (maxx - minx) / 2; + float rz = (maxz - minz) / 2; + + for (int i = 0, j = CYLINDER_NUM_SEG - 1; i < CYLINDER_NUM_SEG; j = i++) { + vertex(cx + cylinderDir[j * 2 + 0] * rx, miny, cz + cylinderDir[j * 2 + 1] * rz, col); + vertex(cx + cylinderDir[i * 2 + 0] * rx, miny, cz + cylinderDir[i * 2 + 1] * rz, col); + vertex(cx + cylinderDir[j * 2 + 0] * rx, maxy, cz + cylinderDir[j * 2 + 1] * rz, col); + vertex(cx + cylinderDir[i * 2 + 0] * rx, maxy, cz + cylinderDir[i * 2 + 1] * rz, col); + } + for (int i = 0; i < CYLINDER_NUM_SEG; i += CYLINDER_NUM_SEG / 4) { + vertex(cx + cylinderDir[i * 2 + 0] * rx, miny, cz + cylinderDir[i * 2 + 1] * rz, col); + vertex(cx + cylinderDir[i * 2 + 0] * rx, maxy, cz + cylinderDir[i * 2 + 1] * rz, col); + } + } + + public void debugDrawBoxWire(float minx, float miny, float minz, float maxx, float maxy, float maxz, int col, + float lineWidth) { + + begin(DebugDrawPrimitives.LINES, lineWidth); + appendBoxWire(minx, miny, minz, maxx, maxy, maxz, col); + end(); + } + + public void appendBoxWire(float minx, float miny, float minz, float maxx, float maxy, float maxz, int col) { + // Top + vertex(minx, miny, minz, col); + vertex(maxx, miny, minz, col); + vertex(maxx, miny, minz, col); + vertex(maxx, miny, maxz, col); + vertex(maxx, miny, maxz, col); + vertex(minx, miny, maxz, col); + vertex(minx, miny, maxz, col); + vertex(minx, miny, minz, col); + + // bottom + vertex(minx, maxy, minz, col); + vertex(maxx, maxy, minz, col); + vertex(maxx, maxy, minz, col); + vertex(maxx, maxy, maxz, col); + vertex(maxx, maxy, maxz, col); + vertex(minx, maxy, maxz, col); + vertex(minx, maxy, maxz, col); + vertex(minx, maxy, minz, col); + + // Sides + vertex(minx, miny, minz, col); + vertex(minx, maxy, minz, col); + vertex(maxx, miny, minz, col); + vertex(maxx, maxy, minz, col); + vertex(maxx, miny, maxz, col); + vertex(maxx, maxy, maxz, col); + vertex(minx, miny, maxz, col); + vertex(minx, maxy, maxz, col); + } + + public void appendBox(float minx, float miny, float minz, float maxx, float maxy, float maxz, int[] fcol) { + float[][] verts = { + new[] { minx, miny, minz }, + new[] { maxx, miny, minz }, + new[] { maxx, miny, maxz }, + new[] { minx, miny, maxz }, + new[] { minx, maxy, minz }, + new[] { maxx, maxy, minz }, + new[] { maxx, maxy, maxz }, + new[] { minx, maxy, maxz } }; + + int idx = 0; + for (int i = 0; i < 6; ++i) { + vertex(verts[boxIndices[idx]], fcol[i]); + idx++; + vertex(verts[boxIndices[idx]], fcol[i]); + idx++; + vertex(verts[boxIndices[idx]], fcol[i]); + idx++; + vertex(verts[boxIndices[idx]], fcol[i]); + idx++; + } + } + + public void debugDrawArc(float x0, float y0, float z0, float x1, float y1, float z1, float h, float as0, float as1, int col, + float lineWidth) { + begin(DebugDrawPrimitives.LINES, lineWidth); + appendArc(x0, y0, z0, x1, y1, z1, h, as0, as1, col); + end(); + } + + public void begin(DebugDrawPrimitives prim, float size) { + getOpenGlDraw().begin(prim, size); + } + + public void vertex(float[] pos, int color) { + getOpenGlDraw().vertex(pos, color); + } + + public void vertex(float x, float y, float z, int color) { + getOpenGlDraw().vertex(x, y, z, color); + } + + public void vertex(float[] pos, int color, float[] uv) { + getOpenGlDraw().vertex(pos, color, uv); + } + + public void vertex(float x, float y, float z, int color, float u, float v) { + getOpenGlDraw().vertex(x, y, z, color, u, v); + } + + public void end() { + getOpenGlDraw().end(); + } + + public void debugDrawCircle(float x, float y, float z, float r, int col, float lineWidth) { + begin(DebugDrawPrimitives.LINES, lineWidth); + appendCircle(x, y, z, r, col); + end(); + } + + private bool circleInit = false; + private const int CIRCLE_NUM_SEG = 40; + private readonly float[] circeDir = new float[CIRCLE_NUM_SEG * 2]; + private float[] _viewMatrix = new float[16]; + private readonly float[] _projectionMatrix = new float[16]; + + public void appendCircle(float x, float y, float z, float r, int col) { + if (!circleInit) { + circleInit = true; + for (int i = 0; i < CIRCLE_NUM_SEG; ++i) { + float a = (float) (i * Math.PI * 2 / CIRCLE_NUM_SEG); + circeDir[i * 2] = (float) Math.Cos(a); + circeDir[i * 2 + 1] = (float) Math.Sin(a); + } + } + for (int i = 0, j = CIRCLE_NUM_SEG - 1; i < CIRCLE_NUM_SEG; j = i++) { + vertex(x + circeDir[j * 2 + 0] * r, y, z + circeDir[j * 2 + 1] * r, col); + vertex(x + circeDir[i * 2 + 0] * r, y, z + circeDir[i * 2 + 1] * r, col); + } + } + + private static readonly int NUM_ARC_PTS = 8; + private static readonly float PAD = 0.05f; + private static readonly float ARC_PTS_SCALE = (1.0f - PAD * 2) / NUM_ARC_PTS; + + public void appendArc(float x0, float y0, float z0, float x1, float y1, float z1, float h, float as0, float as1, int col) { + float dx = x1 - x0; + float dy = y1 - y0; + float dz = z1 - z0; + float len = (float) Math.Sqrt(dx * dx + dy * dy + dz * dz); + float[] prev = new float[3]; + evalArc(x0, y0, z0, dx, dy, dz, len * h, PAD, prev); + for (int i = 1; i <= NUM_ARC_PTS; ++i) { + float u = PAD + i * ARC_PTS_SCALE; + float[] pt = new float[3]; + evalArc(x0, y0, z0, dx, dy, dz, len * h, u, pt); + vertex(prev[0], prev[1], prev[2], col); + vertex(pt[0], pt[1], pt[2], col); + prev[0] = pt[0]; + prev[1] = pt[1]; + prev[2] = pt[2]; + } + + // End arrows + if (as0 > 0.001f) { + float[] p = new float[3], q = new float[3]; + evalArc(x0, y0, z0, dx, dy, dz, len * h, PAD, p); + evalArc(x0, y0, z0, dx, dy, dz, len * h, PAD + 0.05f, q); + appendArrowHead(p, q, as0, col); + } + + if (as1 > 0.001f) { + float[] p = new float[3], q = new float[3]; + evalArc(x0, y0, z0, dx, dy, dz, len * h, 1 - PAD, p); + evalArc(x0, y0, z0, dx, dy, dz, len * h, 1 - (PAD + 0.05f), q); + appendArrowHead(p, q, as1, col); + } + } + + private void evalArc(float x0, float y0, float z0, float dx, float dy, float dz, float h, float u, float[] res) { + res[0] = x0 + dx * u; + res[1] = y0 + dy * u + h * (1 - (u * 2 - 1) * (u * 2 - 1)); + res[2] = z0 + dz * u; + } + + public void debugDrawCross(float x, float y, float z, float size, int col, float lineWidth) { + begin(DebugDrawPrimitives.LINES, lineWidth); + appendCross(x, y, z, size, col); + end(); + } + + private void appendCross(float x, float y, float z, float s, int col) { + vertex(x - s, y, z, col); + vertex(x + s, y, z, col); + vertex(x, y - s, z, col); + vertex(x, y + s, z, col); + vertex(x, y, z - s, col); + vertex(x, y, z + s, col); + } + + public void debugDrawBox(float minx, float miny, float minz, float maxx, float maxy, float maxz, int[] fcol) { + begin(DebugDrawPrimitives.QUADS); + appendBox(minx, miny, minz, maxx, maxy, maxz, fcol); + end(); + } + + public void debugDrawCylinder(float minx, float miny, float minz, float maxx, float maxy, float maxz, int col) { + begin(DebugDrawPrimitives.TRIS); + appendCylinder(minx, miny, minz, maxx, maxy, maxz, col); + end(); + } + + public void appendCylinder(float minx, float miny, float minz, float maxx, float maxy, float maxz, int col) { + initCylinder(); + + int col2 = duMultCol(col, 160); + + float cx = (maxx + minx) / 2; + float cz = (maxz + minz) / 2; + float rx = (maxx - minx) / 2; + float rz = (maxz - minz) / 2; + + for (int i = 2; i < CYLINDER_NUM_SEG; ++i) { + int a = 0, b = i - 1, c = i; + vertex(cx + cylinderDir[a * 2 + 0] * rx, miny, cz + cylinderDir[a * 2 + 1] * rz, col2); + vertex(cx + cylinderDir[b * 2 + 0] * rx, miny, cz + cylinderDir[b * 2 + 1] * rz, col2); + vertex(cx + cylinderDir[c * 2 + 0] * rx, miny, cz + cylinderDir[c * 2 + 1] * rz, col2); + } + for (int i = 2; i < CYLINDER_NUM_SEG; ++i) { + int a = 0, b = i, c = i - 1; + vertex(cx + cylinderDir[a * 2 + 0] * rx, maxy, cz + cylinderDir[a * 2 + 1] * rz, col); + vertex(cx + cylinderDir[b * 2 + 0] * rx, maxy, cz + cylinderDir[b * 2 + 1] * rz, col); + vertex(cx + cylinderDir[c * 2 + 0] * rx, maxy, cz + cylinderDir[c * 2 + 1] * rz, col); + } + for (int i = 0, j = CYLINDER_NUM_SEG - 1; i < CYLINDER_NUM_SEG; j = i++) { + vertex(cx + cylinderDir[i * 2 + 0] * rx, miny, cz + cylinderDir[i * 2 + 1] * rz, col2); + vertex(cx + cylinderDir[j * 2 + 0] * rx, miny, cz + cylinderDir[j * 2 + 1] * rz, col2); + vertex(cx + cylinderDir[j * 2 + 0] * rx, maxy, cz + cylinderDir[j * 2 + 1] * rz, col); + + vertex(cx + cylinderDir[i * 2 + 0] * rx, miny, cz + cylinderDir[i * 2 + 1] * rz, col2); + vertex(cx + cylinderDir[j * 2 + 0] * rx, maxy, cz + cylinderDir[j * 2 + 1] * rz, col); + vertex(cx + cylinderDir[i * 2 + 0] * rx, maxy, cz + cylinderDir[i * 2 + 1] * rz, col); + } + } + + public void debugDrawArrow(float x0, float y0, float z0, float x1, float y1, float z1, float as0, float as1, int col, + float lineWidth) { + + begin(DebugDrawPrimitives.LINES, lineWidth); + appendArrow(x0, y0, z0, x1, y1, z1, as0, as1, col); + end(); + } + + public void appendArrow(float x0, float y0, float z0, float x1, float y1, float z1, float as0, float as1, int col) { + + vertex(x0, y0, z0, col); + vertex(x1, y1, z1, col); + + // End arrows + float[] p = new float[] { x0, y0, z0 }; + float[] q = new float[] { x1, y1, z1 }; + if (as0 > 0.001f) + appendArrowHead(p, q, as0, col); + if (as1 > 0.001f) + appendArrowHead(q, p, as1, col); + } + + void appendArrowHead(float[] p, float[] q, float s, int col) { + float eps = 0.001f; + if (vdistSqr(p, q) < eps * eps) { + return; + } + float[] ax = new float[3], ay = { 0, 1, 0 }, az = new float[3]; + vsub(az, q, p); + vnormalize(az); + vcross(ax, ay, az); + vcross(ay, az, ax); + vnormalize(ay); + + vertex(p, col); + // vertex(p[0]+az[0]*s+ay[0]*s/2, p[1]+az[1]*s+ay[1]*s/2, p[2]+az[2]*s+ay[2]*s/2, col); + vertex(p[0] + az[0] * s + ax[0] * s / 3, p[1] + az[1] * s + ax[1] * s / 3, p[2] + az[2] * s + ax[2] * s / 3, col); + + vertex(p, col); + // vertex(p[0]+az[0]*s-ay[0]*s/2, p[1]+az[1]*s-ay[1]*s/2, p[2]+az[2]*s-ay[2]*s/2, col); + vertex(p[0] + az[0] * s - ax[0] * s / 3, p[1] + az[1] * s - ax[1] * s / 3, p[2] + az[2] * s - ax[2] * s / 3, col); + + } + + public void vcross(float[] dest, float[] v1, float[] v2) { + dest[0] = v1[1] * v2[2] - v1[2] * v2[1]; + dest[1] = v1[2] * v2[0] - v1[0] * v2[2]; + dest[2] = v1[0] * v2[1] - v1[1] * v2[0]; + } + + public void vnormalize(float[] v) { + float d = (float) (1.0f / Math.Sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2])); + v[0] *= d; + v[1] *= d; + v[2] *= d; + } + + public void vsub(float[] dest, float[] v1, float[] v2) { + dest[0] = v1[0] - v2[0]; + dest[1] = v1[1] - v2[1]; + dest[2] = v1[2] - v2[2]; + } + + public float vdistSqr(float[] v1, float[] v2) { + float x = v1[0] - v2[0]; + float y = v1[1] - v2[1]; + float z = v1[2] - v2[2]; + return x * x + y * y + z * z; + } + +// public static int areaToCol(int area) { +// if (area == 0) { +// return duRGBA(0, 192, 255, 255); +// } else { +// return duIntToCol(area, 255); +// } +// } + + public static int areaToCol(int area) { + switch (area) { + // Ground (0) : light blue + case SampleAreaModifications.SAMPLE_POLYAREA_TYPE_WALKABLE: + case SampleAreaModifications.SAMPLE_POLYAREA_TYPE_GROUND: + return duRGBA(0, 192, 255, 255); + // Water : blue + case SampleAreaModifications.SAMPLE_POLYAREA_TYPE_WATER: + return duRGBA(0, 0, 255, 255); + // Road : brown + case SampleAreaModifications.SAMPLE_POLYAREA_TYPE_ROAD: + return duRGBA(50, 20, 12, 255); + // Door : cyan + case SampleAreaModifications.SAMPLE_POLYAREA_TYPE_DOOR: + return duRGBA(0, 255, 255, 255); + // Grass : green + case SampleAreaModifications.SAMPLE_POLYAREA_TYPE_GRASS: + return duRGBA(0, 255, 0, 255); + // Jump : yellow + case SampleAreaModifications.SAMPLE_POLYAREA_TYPE_JUMP: + return duRGBA(255, 255, 0, 255); + // Unexpected : red + default: + return duRGBA(255, 0, 0, 255); + } + } + + public static int duRGBA(int r, int g, int b, int a) { + return (r) | (g << 8) | (b << 16) | (a << 24); + } + + public static int duLerpCol(int ca, int cb, int u) { + int ra = ca & 0xff; + int ga = (ca >> 8) & 0xff; + int ba = (ca >> 16) & 0xff; + int aa = (ca >> 24) & 0xff; + int rb = cb & 0xff; + int gb = (cb >> 8) & 0xff; + int bb = (cb >> 16) & 0xff; + int ab = (cb >> 24) & 0xff; + + int r = (ra * (255 - u) + rb * u) / 255; + int g = (ga * (255 - u) + gb * u) / 255; + int b = (ba * (255 - u) + bb * u) / 255; + int a = (aa * (255 - u) + ab * u) / 255; + return duRGBA(r, g, b, a); + } + + public static int bit(int a, int b) { + return (a & (1 << b)) >>> b; + } + + public static int duIntToCol(int i, int a) { + int r = bit(i, 1) + bit(i, 3) * 2 + 1; + int g = bit(i, 2) + bit(i, 4) * 2 + 1; + int b = bit(i, 0) + bit(i, 5) * 2 + 1; + return duRGBA(r * 63, g * 63, b * 63, a); + } + + public static void duCalcBoxColors(int[] colors, int colTop, int colSide) { + colors[0] = duMultCol(colTop, 250); + colors[1] = duMultCol(colSide, 140); + colors[2] = duMultCol(colSide, 165); + colors[3] = duMultCol(colSide, 165); + colors[4] = duMultCol(colSide, 217); + colors[5] = duMultCol(colSide, 217); + } + + public static int duMultCol(int col, int d) { + int r = col & 0xff; + int g = (col >> 8) & 0xff; + int b = (col >> 16) & 0xff; + int a = (col >> 24) & 0xff; + return duRGBA((r * d) >> 8, (g * d) >> 8, (b * d) >> 8, a); + } + + public static int duTransCol(int c, int a) { + return (a << 24) | (c & 0x00ffffff); + } + + public static int duDarkenCol(int col) { + return (int)(((col >> 1) & 0x007f7f7f) | (col & 0xff000000)); + } + + public void fog(float start, float end) { + getOpenGlDraw().fog(start, end); + } + + public void fog(bool state) { + getOpenGlDraw().fog(state); + } + + public void depthMask(bool state) { + getOpenGlDraw().depthMask(state); + } + + public void texture(bool state) { + getOpenGlDraw().texture(g_tex, state); + } + + public void init(GL gl, float fogDistance) { + getOpenGlDraw().init(gl); + } + + public void clear() { + getOpenGlDraw().clear(); + } + + public float[] projectionMatrix(float fovy, float aspect, float near, float far) { + GLU.glhPerspectivef2(_projectionMatrix, fovy, aspect, near, far); + getOpenGlDraw().projectionMatrix(_projectionMatrix); + updateFrustum(); + return _projectionMatrix; + } + + public float[] viewMatrix(float[] cameraPos, float[] cameraEulers) { + float[] rx = GLU.build_4x4_rotation_matrix(cameraEulers[0], 1, 0, 0); + float[] ry = GLU.build_4x4_rotation_matrix(cameraEulers[1], 0, 1, 0); + float[] r = GLU.mul(rx, ry); + float[] t = new float[16]; + t[0] = t[5] = t[10] = t[15] = 1; + t[12] = -cameraPos[0]; + t[13] = -cameraPos[1]; + t[14] = -cameraPos[2]; + _viewMatrix = GLU.mul(r, t); + getOpenGlDraw().viewMatrix(_viewMatrix); + updateFrustum(); + return _viewMatrix; + } + + private OpenGLDraw getOpenGlDraw() { + return openGlDraw; + } + + private void updateFrustum() { + float[] vpm = GLU.mul(_projectionMatrix, _viewMatrix); + // left + frustumPlanes[0] = normalizePlane(vpm[0 + 3] + vpm[0 + 0], vpm[4 + 3] + vpm[4 + 0], vpm[8 + 3] + vpm[8 + 0], + vpm[12 + 3] + vpm[12 + 0]); + // right + frustumPlanes[1] = normalizePlane(vpm[0 + 3] - vpm[0 + 0], vpm[4 + 3] - vpm[4 + 0], vpm[8 + 3] - vpm[8 + 0], + vpm[12 + 3] - vpm[12 + 0]); + // top + frustumPlanes[2] = normalizePlane(vpm[0 + 3] - vpm[0 + 1], vpm[4 + 3] - vpm[4 + 1], vpm[8 + 3] - vpm[8 + 1], + vpm[12 + 3] - vpm[12 + 1]); + // bottom + frustumPlanes[3] = normalizePlane(vpm[0 + 3] + vpm[0 + 1], vpm[4 + 3] + vpm[4 + 1], vpm[8 + 3] + vpm[8 + 1], + vpm[12 + 3] + vpm[12 + 1]); + // near + frustumPlanes[4] = normalizePlane(vpm[0 + 3] + vpm[0 + 2], vpm[4 + 3] + vpm[4 + 2], vpm[8 + 3] + vpm[8 + 2], + vpm[12 + 3] + vpm[12 + 2]); + // far + frustumPlanes[5] = normalizePlane(vpm[0 + 3] - vpm[0 + 2], vpm[4 + 3] - vpm[4 + 2], vpm[8 + 3] - vpm[8 + 2], + vpm[12 + 3] - vpm[12 + 2]); + } + + private float[] normalizePlane(float px, float py, float pz, float pw) { + float length = (float) Math.Sqrt(px * px + py * py + pz * pz); + if (length != 0) { + length = 1f / length; + px *= length; + py *= length; + pz *= length; + pw *= length; + } + return new float[] { px, py, pz, pw }; + } + + public bool frustumTest(float[] bmin, float[] bmax) { + return frustumTest(new float[] { bmin[0], bmin[1], bmin[2], bmax[0], bmax[1], bmax[2] }); + } + + public bool frustumTest(float[] bounds) { + foreach (float[] plane in frustumPlanes) { + float p_x; + float p_y; + float p_z; + float n_x; + float n_y; + float n_z; + if (plane[0] >= 0) { + p_x = bounds[3]; + n_x = bounds[0]; + } else { + p_x = bounds[0]; + n_x = bounds[3]; + } + if (plane[1] >= 0) { + p_y = bounds[4]; + n_y = bounds[1]; + } else { + p_y = bounds[1]; + n_y = bounds[4]; + } + if (plane[2] >= 0) { + p_z = bounds[5]; + n_z = bounds[2]; + } else { + p_z = bounds[2]; + n_z = bounds[5]; + } + if (plane[0] * p_x + plane[1] * p_y + plane[2] * p_z + plane[3] < 0) { + return false; + } + } + return true; + } + +} diff --git a/src/DotRecast.Recast.Demo/Draw/DebugDrawPrimitives.cs b/src/DotRecast.Recast.Demo/Draw/DebugDrawPrimitives.cs new file mode 100644 index 0000000..185dd7e --- /dev/null +++ b/src/DotRecast.Recast.Demo/Draw/DebugDrawPrimitives.cs @@ -0,0 +1,27 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Recast.Demo.Draw; + +public enum DebugDrawPrimitives { + + POINTS, + LINES, + TRIS, + QUADS +} diff --git a/src/DotRecast.Recast.Demo/Draw/DrawMode.cs b/src/DotRecast.Recast.Demo/Draw/DrawMode.cs new file mode 100644 index 0000000..ecfb260 --- /dev/null +++ b/src/DotRecast.Recast.Demo/Draw/DrawMode.cs @@ -0,0 +1,50 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Recast.Demo.Draw; + +public class DrawMode { + public static readonly DrawMode DRAWMODE_MESH = new("Input Mesh"); + public static readonly DrawMode DRAWMODE_NAVMESH = new("Navmesh"); + public static readonly DrawMode DRAWMODE_NAVMESH_INVIS = new("Navmesh Invis"); + public static readonly DrawMode DRAWMODE_NAVMESH_TRANS = new("Navmesh Trans"); + public static readonly DrawMode DRAWMODE_NAVMESH_BVTREE = new("Navmesh BVTree"); + public static readonly DrawMode DRAWMODE_NAVMESH_NODES = new("Navmesh Nodes"); + public static readonly DrawMode DRAWMODE_NAVMESH_PORTALS = new("Navmesh Portals"); + public static readonly DrawMode DRAWMODE_VOXELS = new("Voxels"); + public static readonly DrawMode DRAWMODE_VOXELS_WALKABLE = new("Walkable Voxels"); + public static readonly DrawMode DRAWMODE_COMPACT = new("Compact"); + public static readonly DrawMode DRAWMODE_COMPACT_DISTANCE = new("Compact Distance"); + public static readonly DrawMode DRAWMODE_COMPACT_REGIONS = new("Compact Regions"); + public static readonly DrawMode DRAWMODE_REGION_CONNECTIONS = new("Region Connections"); + public static readonly DrawMode DRAWMODE_RAW_CONTOURS = new("Raw Contours"); + public static readonly DrawMode DRAWMODE_BOTH_CONTOURS = new("Both Contours"); + public static readonly DrawMode DRAWMODE_CONTOURS = new("Contours"); + public static readonly DrawMode DRAWMODE_POLYMESH = new("Poly Mesh"); + public static readonly DrawMode DRAWMODE_POLYMESH_DETAIL = new("Poly Mesh Detils"); + + private readonly string text; + + private DrawMode(string text) { + this.text = text; + } + + public override string ToString() { + return text; + } +} diff --git a/src/DotRecast.Recast.Demo/Draw/GLCheckerTexture.cs b/src/DotRecast.Recast.Demo/Draw/GLCheckerTexture.cs new file mode 100644 index 0000000..13d65c8 --- /dev/null +++ b/src/DotRecast.Recast.Demo/Draw/GLCheckerTexture.cs @@ -0,0 +1,62 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using Silk.NET.OpenGL; + +namespace DotRecast.Recast.Demo.Draw; + +public class GLCheckerTexture { + + int m_texId; + + public void release() { + // if (m_texId != 0) { + // glDeleteTextures(m_texId); + // } + } + + public void bind() { + // if (m_texId == 0) { + // // Create checker pattern. + // int col0 = DebugDraw.duRGBA(215, 215, 215, 255); + // int col1 = DebugDraw.duRGBA(255, 255, 255, 255); + // int TSIZE = 64; + // int[] data = new int[TSIZE * TSIZE]; + // + // m_texId = glGenTextures(); + // glBindTexture(GL_TEXTURE_2D, m_texId); + // + // int level = 0; + // int size = TSIZE; + // while (size > 0) { + // for (int y = 0; y < size; ++y) + // for (int x = 0; x < size; ++x) + // data[x + y * size] = (x == 0 || y == 0) ? col0 : col1; + // glTexImage2D(GL_TEXTURE_2D, level, GL_RGBA, size, size, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); + // size /= 2; + // level++; + // } + // + // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST); + // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + // } else { + // glBindTexture(GL_TEXTURE_2D, m_texId); + // } + } +} diff --git a/src/DotRecast.Recast.Demo/Draw/GLU.cs b/src/DotRecast.Recast.Demo/Draw/GLU.cs new file mode 100644 index 0000000..a4112a9 --- /dev/null +++ b/src/DotRecast.Recast.Demo/Draw/GLU.cs @@ -0,0 +1,432 @@ +/* +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using DotRecast.Core; + +namespace DotRecast.Recast.Demo.Draw; + + + +public class GLU { + + public static float[] gluPerspective(float fovy, float aspect, float near, float far) { + float[] projectionMatrix = new float[16]; + glhPerspectivef2(projectionMatrix, fovy, aspect, near, far); + //glLoadMatrixf(projectionMatrix); + return projectionMatrix; + } + + public static void glhPerspectivef2(float[] matrix, float fovyInDegrees, float aspectRatio, float znear, + float zfar) { + float ymax, xmax; + ymax = (float) (znear * Math.Tan(fovyInDegrees * Math.PI / 360.0)); + xmax = ymax * aspectRatio; + glhFrustumf2(matrix, -xmax, xmax, -ymax, ymax, znear, zfar); + } + + private static void glhFrustumf2(float[] matrix, float left, float right, float bottom, float top, float znear, + float zfar) { + float temp, temp2, temp3, temp4; + temp = 2.0f * znear; + temp2 = right - left; + temp3 = top - bottom; + temp4 = zfar - znear; + matrix[0] = temp / temp2; + matrix[1] = 0.0f; + matrix[2] = 0.0f; + matrix[3] = 0.0f; + matrix[4] = 0.0f; + matrix[5] = temp / temp3; + matrix[6] = 0.0f; + matrix[7] = 0.0f; + matrix[8] = (right + left) / temp2; + matrix[9] = (top + bottom) / temp3; + matrix[10] = (-zfar - znear) / temp4; + matrix[11] = -1.0f; + matrix[12] = 0.0f; + matrix[13] = 0.0f; + matrix[14] = (-temp * zfar) / temp4; + matrix[15] = 0.0f; + } + + public static int glhUnProjectf(float winx, float winy, float winz, float[] modelview, float[] projection, + int[] viewport, float[] objectCoordinate) { + // Transformation matrices + float[] m = new float[16], A = new float[16]; + float[] @in = new float[4], @out = new float[4]; + // Calculation for inverting a matrix, compute projection x modelview + // and store in A[16] + MultiplyMatrices4by4OpenGL_FLOAT(A, projection, modelview); + // Now compute the inverse of matrix A + if (glhInvertMatrixf2(A, m) == 0) + return 0; + // Transformation of normalized coordinates between -1 and 1 + @in[0] = (winx - viewport[0]) / viewport[2] * 2.0f - 1.0f; + @in[1] = (winy - viewport[1]) / viewport[3] * 2.0f - 1.0f; + @in[2] = 2.0f * winz - 1.0f; + @in[3] = 1.0f; + // Objects coordinates + MultiplyMatrixByVector4by4OpenGL_FLOAT(@out, m, @in); + if (@out[3] == 0.0) + return 0; + @out[3] = 1.0f / @out[3]; + objectCoordinate[0] = @out[0] * @out[3]; + objectCoordinate[1] = @out[1] * @out[3]; + objectCoordinate[2] = @out[2] * @out[3]; + return 1; + } + + static void MultiplyMatrices4by4OpenGL_FLOAT(float[] result, float[] matrix1, float[] matrix2) { + result[0] = matrix1[0] * matrix2[0] + matrix1[4] * matrix2[1] + matrix1[8] * matrix2[2] + + matrix1[12] * matrix2[3]; + result[4] = matrix1[0] * matrix2[4] + matrix1[4] * matrix2[5] + matrix1[8] * matrix2[6] + + matrix1[12] * matrix2[7]; + result[8] = matrix1[0] * matrix2[8] + matrix1[4] * matrix2[9] + matrix1[8] * matrix2[10] + + matrix1[12] * matrix2[11]; + result[12] = matrix1[0] * matrix2[12] + matrix1[4] * matrix2[13] + matrix1[8] * matrix2[14] + + matrix1[12] * matrix2[15]; + result[1] = matrix1[1] * matrix2[0] + matrix1[5] * matrix2[1] + matrix1[9] * matrix2[2] + + matrix1[13] * matrix2[3]; + result[5] = matrix1[1] * matrix2[4] + matrix1[5] * matrix2[5] + matrix1[9] * matrix2[6] + + matrix1[13] * matrix2[7]; + result[9] = matrix1[1] * matrix2[8] + matrix1[5] * matrix2[9] + matrix1[9] * matrix2[10] + + matrix1[13] * matrix2[11]; + result[13] = matrix1[1] * matrix2[12] + matrix1[5] * matrix2[13] + matrix1[9] * matrix2[14] + + matrix1[13] * matrix2[15]; + result[2] = matrix1[2] * matrix2[0] + matrix1[6] * matrix2[1] + matrix1[10] * matrix2[2] + + matrix1[14] * matrix2[3]; + result[6] = matrix1[2] * matrix2[4] + matrix1[6] * matrix2[5] + matrix1[10] * matrix2[6] + + matrix1[14] * matrix2[7]; + result[10] = matrix1[2] * matrix2[8] + matrix1[6] * matrix2[9] + matrix1[10] * matrix2[10] + + matrix1[14] * matrix2[11]; + result[14] = matrix1[2] * matrix2[12] + matrix1[6] * matrix2[13] + matrix1[10] * matrix2[14] + + matrix1[14] * matrix2[15]; + result[3] = matrix1[3] * matrix2[0] + matrix1[7] * matrix2[1] + matrix1[11] * matrix2[2] + + matrix1[15] * matrix2[3]; + result[7] = matrix1[3] * matrix2[4] + matrix1[7] * matrix2[5] + matrix1[11] * matrix2[6] + + matrix1[15] * matrix2[7]; + result[11] = matrix1[3] * matrix2[8] + matrix1[7] * matrix2[9] + matrix1[11] * matrix2[10] + + matrix1[15] * matrix2[11]; + result[15] = matrix1[3] * matrix2[12] + matrix1[7] * matrix2[13] + matrix1[11] * matrix2[14] + + matrix1[15] * matrix2[15]; + } + + static void MultiplyMatrixByVector4by4OpenGL_FLOAT(float[] resultvector, float[] matrix, float[] pvector) { + resultvector[0] = matrix[0] * pvector[0] + matrix[4] * pvector[1] + matrix[8] * pvector[2] + + matrix[12] * pvector[3]; + resultvector[1] = matrix[1] * pvector[0] + matrix[5] * pvector[1] + matrix[9] * pvector[2] + + matrix[13] * pvector[3]; + resultvector[2] = matrix[2] * pvector[0] + matrix[6] * pvector[1] + matrix[10] * pvector[2] + + matrix[14] * pvector[3]; + resultvector[3] = matrix[3] * pvector[0] + matrix[7] * pvector[1] + matrix[11] * pvector[2] + + matrix[15] * pvector[3]; + } + + // This code comes directly from GLU except that it is for float + static int glhInvertMatrixf2(float[] m, float[] @out) { + float[][] wtmp = ArrayUtils.Of(4, 8); + float m0, m1, m2, m3, s; + float[] r0, r1, r2, r3; + r0 = wtmp[0]; + r1 = wtmp[1]; + r2 = wtmp[2]; + r3 = wtmp[3]; + r0[0] = MAT(m, 0, 0); + r0[1] = MAT(m, 0, 1); + r0[2] = MAT(m, 0, 2); + r0[3] = MAT(m, 0, 3); + r0[4] = 1.0f; + r0[5] = r0[6] = r0[7] = 0.0f; + r1[0] = MAT(m, 1, 0); + r1[1] = MAT(m, 1, 1); + r1[2] = MAT(m, 1, 2); + r1[3] = MAT(m, 1, 3); + r1[5] = 1.0f; + r1[4] = r1[6] = r1[7] = 0.0f; + r2[0] = MAT(m, 2, 0); + r2[1] = MAT(m, 2, 1); + r2[2] = MAT(m, 2, 2); + r2[3] = MAT(m, 2, 3); + r2[6] = 1.0f; + r2[4] = r2[5] = r2[7] = 0.0f; + r3[0] = MAT(m, 3, 0); + r3[1] = MAT(m, 3, 1); + r3[2] = MAT(m, 3, 2); + r3[3] = MAT(m, 3, 3); + r3[7] = 1.0f; + r3[4] = r3[5] = r3[6] = 0.0f; + /* choose pivot - or die */ + if (Math.Abs(r3[0]) > Math.Abs(r2[0])) { + float[] r = r2; + r2 = r3; + r3 = r; + } + if (Math.Abs(r2[0]) > Math.Abs(r1[0])) { + float[] r = r2; + r2 = r1; + r1 = r; + } + if (Math.Abs(r1[0]) > Math.Abs(r0[0])) { + float[] r = r1; + r1 = r0; + r0 = r; + } + if (0.0 == r0[0]) + return 0; + /* eliminate first variable */ + m1 = r1[0] / r0[0]; + m2 = r2[0] / r0[0]; + m3 = r3[0] / r0[0]; + s = r0[1]; + r1[1] -= m1 * s; + r2[1] -= m2 * s; + r3[1] -= m3 * s; + s = r0[2]; + r1[2] -= m1 * s; + r2[2] -= m2 * s; + r3[2] -= m3 * s; + s = r0[3]; + r1[3] -= m1 * s; + r2[3] -= m2 * s; + r3[3] -= m3 * s; + s = r0[4]; + if (s != 0.0) { + r1[4] -= m1 * s; + r2[4] -= m2 * s; + r3[4] -= m3 * s; + } + s = r0[5]; + if (s != 0.0) { + r1[5] -= m1 * s; + r2[5] -= m2 * s; + r3[5] -= m3 * s; + } + s = r0[6]; + if (s != 0.0) { + r1[6] -= m1 * s; + r2[6] -= m2 * s; + r3[6] -= m3 * s; + } + s = r0[7]; + if (s != 0.0) { + r1[7] -= m1 * s; + r2[7] -= m2 * s; + r3[7] -= m3 * s; + } + /* choose pivot - or die */ + if (Math.Abs(r3[1]) > Math.Abs(r2[1])) { + float[] r = r2; + r2 = r3; + r3 = r; + } + if (Math.Abs(r2[1]) > Math.Abs(r1[1])) { + float[] r = r2; + r2 = r1; + r1 = r; + } + if (0.0 == r1[1]) + return 0; + /* eliminate second variable */ + m2 = r2[1] / r1[1]; + m3 = r3[1] / r1[1]; + r2[2] -= m2 * r1[2]; + r3[2] -= m3 * r1[2]; + r2[3] -= m2 * r1[3]; + r3[3] -= m3 * r1[3]; + s = r1[4]; + if (0.0 != s) { + r2[4] -= m2 * s; + r3[4] -= m3 * s; + } + s = r1[5]; + if (0.0 != s) { + r2[5] -= m2 * s; + r3[5] -= m3 * s; + } + s = r1[6]; + if (0.0 != s) { + r2[6] -= m2 * s; + r3[6] -= m3 * s; + } + s = r1[7]; + if (0.0 != s) { + r2[7] -= m2 * s; + r3[7] -= m3 * s; + } + /* choose pivot - or die */ + if (Math.Abs(r3[2]) > Math.Abs(r2[2])) { + float[] r = r2; + r2 = r3; + r3 = r; + } + if (0.0 == r2[2]) + return 0; + /* eliminate third variable */ + m3 = r3[2] / r2[2]; + r3[3] -= m3 * r2[3]; + r3[4] -= m3 * r2[4]; + r3[5] -= m3 * r2[5]; + r3[6] -= m3 * r2[6]; + r3[7] -= m3 * r2[7]; + /* last check */ + if (0.0 == r3[3]) + return 0; + s = 1.0f / r3[3]; /* now back substitute row 3 */ + r3[4] *= s; + r3[5] *= s; + r3[6] *= s; + r3[7] *= s; + m2 = r2[3]; /* now back substitute row 2 */ + s = 1.0f / r2[2]; + r2[4] = s * (r2[4] - r3[4] * m2); + r2[5] = s * (r2[5] - r3[5] * m2); + r2[6] = s * (r2[6] - r3[6] * m2); + r2[7] = s * (r2[7] - r3[7] * m2); + m1 = r1[3]; + r1[4] -= r3[4] * m1; + r1[5] -= r3[5] * m1; + r1[6] -= r3[6] * m1; + r1[7] -= r3[7] * m1; + m0 = r0[3]; + r0[4] -= r3[4] * m0; + r0[5] -= r3[5] * m0; + r0[6] -= r3[6] * m0; + r0[7] -= r3[7] * m0; + m1 = r1[2]; /* now back substitute row 1 */ + s = 1.0f / r1[1]; + r1[4] = s * (r1[4] - r2[4] * m1); + r1[5] = s * (r1[5] - r2[5] * m1); + r1[6] = s * (r1[6] - r2[6] * m1); + r1[7] = s * (r1[7] - r2[7] * m1); + m0 = r0[2]; + r0[4] -= r2[4] * m0; + r0[5] -= r2[5] * m0; + r0[6] -= r2[6] * m0; + r0[7] -= r2[7] * m0; + m0 = r0[1]; /* now back substitute row 0 */ + s = 1.0f / r0[0]; + r0[4] = s * (r0[4] - r1[4] * m0); + r0[5] = s * (r0[5] - r1[5] * m0); + r0[6] = s * (r0[6] - r1[6] * m0); + r0[7] = s * (r0[7] - r1[7] * m0); + MAT(@out, 0, 0, r0[4]); + MAT(@out, 0, 1, r0[5]); + MAT(@out, 0, 2, r0[6]); + MAT(@out, 0, 3, r0[7]); + MAT(@out, 1, 0, r1[4]); + MAT(@out, 1, 1, r1[5]); + MAT(@out, 1, 2, r1[6]); + MAT(@out, 1, 3, r1[7]); + MAT(@out, 2, 0, r2[4]); + MAT(@out, 2, 1, r2[5]); + MAT(@out, 2, 2, r2[6]); + MAT(@out, 2, 3, r2[7]); + MAT(@out, 3, 0, r3[4]); + MAT(@out, 3, 1, r3[5]); + MAT(@out, 3, 2, r3[6]); + MAT(@out, 3, 3, r3[7]); + return 1; + } + + static float MAT(float[] m, int r, int c) { + return m[(c) * 4 + (r)]; + } + + static void MAT(float[] m, int r, int c, float v) { + m[(c) * 4 + (r)] = v; + } + + public static float[] build_4x4_rotation_matrix(float a, float x, float y, float z) { + float[] matrix = new float[16]; + a = (float) (a * Math.PI / 180.0); // convert to radians + float s = (float) Math.Sin(a); + float c = (float) Math.Cos(a); + float t = 1.0f - c; + + float tx = t * x; + float ty = t * y; + float tz = t * z; + + float sz = s * z; + float sy = s * y; + float sx = s * x; + + matrix[0] = tx * x + c; + matrix[1] = tx * y + sz; + matrix[2] = tx * z - sy; + matrix[3] = 0; + + matrix[4] = tx * y - sz; + matrix[5] = ty * y + c; + matrix[6] = ty * z + sx; + matrix[7] = 0; + + matrix[8] = tx * z + sy; + matrix[9] = ty * z - sx; + matrix[10] = tz * z + c; + matrix[11] = 0; + + matrix[12] = 0; + matrix[13] = 0; + matrix[14] = 0; + matrix[15] = 1; + return matrix; + + } + + public static float[] mul(float[] left, float[] right) { + float m00 = left[0] * right[0] + left[4] * right[1] + left[8] * right[2] + left[12] * right[3]; + float m01 = left[1] * right[0] + left[5] * right[1] + left[9] * right[2] + left[13] * right[3]; + float m02 = left[2] * right[0] + left[6] * right[1] + left[10] * right[2] + left[14] * right[3]; + float m03 = left[3] * right[0] + left[7] * right[1] + left[11] * right[2] + left[15] * right[3]; + float m10 = left[0] * right[4] + left[4] * right[5] + left[8] * right[6] + left[12] * right[7]; + float m11 = left[1] * right[4] + left[5] * right[5] + left[9] * right[6] + left[13] * right[7]; + float m12 = left[2] * right[4] + left[6] * right[5] + left[10] * right[6] + left[14] * right[7]; + float m13 = left[3] * right[4] + left[7] * right[5] + left[11] * right[6] + left[15] * right[7]; + float m20 = left[0] * right[8] + left[4] * right[9] + left[8] * right[10] + left[12] * right[11]; + float m21 = left[1] * right[8] + left[5] * right[9] + left[9] * right[10] + left[13] * right[11]; + float m22 = left[2] * right[8] + left[6] * right[9] + left[10] * right[10] + left[14] * right[11]; + float m23 = left[3] * right[8] + left[7] * right[9] + left[11] * right[10] + left[15] * right[11]; + float m30 = left[0] * right[12] + left[4] * right[13] + left[8] * right[14] + left[12] * right[15]; + float m31 = left[1] * right[12] + left[5] * right[13] + left[9] * right[14] + left[13] * right[15]; + float m32 = left[2] * right[12] + left[6] * right[13] + left[10] * right[14] + left[14] * right[15]; + float m33 = left[3] * right[12] + left[7] * right[13] + left[11] * right[14] + left[15] * right[15]; + + float[] dest = new float[16]; + dest[0] = m00; + dest[1] = m01; + dest[2] = m02; + dest[3] = m03; + dest[4] = m10; + dest[5] = m11; + dest[6] = m12; + dest[7] = m13; + dest[8] = m20; + dest[9] = m21; + dest[10] = m22; + dest[11] = m23; + dest[12] = m30; + dest[13] = m31; + dest[14] = m32; + dest[15] = m33; + + return dest; + } + +} diff --git a/src/DotRecast.Recast.Demo/Draw/LegacyOpenGLDraw.cs b/src/DotRecast.Recast.Demo/Draw/LegacyOpenGLDraw.cs new file mode 100644 index 0000000..8068667 --- /dev/null +++ b/src/DotRecast.Recast.Demo/Draw/LegacyOpenGLDraw.cs @@ -0,0 +1,123 @@ +using Silk.NET.OpenGL; + +namespace DotRecast.Recast.Demo.Draw; + +public class LegacyOpenGLDraw : OpenGLDraw +{ + + private GL _gl; + + public void fog(bool state) { + // if (state) { + // _gl.Enable(GL_FOG); + // } else { + // _gl.Disable(GL_FOG); + // } + } + + public void init(GL gl) + { + _gl = gl; + + // // Fog. + // float fogDistance = 1000f; + // float fogColor[] = { 0.32f, 0.31f, 0.30f, 1.0f }; + // glEnable(GL_FOG); + // glFogi(GL_FOG_MODE, GL_LINEAR); + // glFogf(GL_FOG_START, fogDistance * 0.1f); + // glFogf(GL_FOG_END, fogDistance * 1.25f); + // glFogfv(GL_FOG_COLOR, fogColor); + // glDepthFunc(GL_LEQUAL); + } + + public void clear() { + // glClearColor(0.3f, 0.3f, 0.32f, 1.0f); + // glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + // glEnable(GL_BLEND); + // glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + // glDisable(GL_TEXTURE_2D); + // glEnable(GL_DEPTH_TEST); + // glEnable(GL_CULL_FACE); + } + + public void projectionMatrix(float[] matrix) { + // glMatrixMode(GL_PROJECTION); + // glLoadMatrixf(matrix); + } + + public void viewMatrix(float[] matrix) { + // glMatrixMode(GL_MODELVIEW); + // glLoadMatrixf(matrix); + } + + public void begin(DebugDrawPrimitives prim, float size) { + // switch (prim) { + // case POINTS: + // glPointSize(size); + // glBegin(GL_POINTS); + // break; + // case LINES: + // glLineWidth(size); + // glBegin(GL_LINES); + // break; + // case TRIS: + // glBegin(GL_TRIANGLES); + // break; + // case QUADS: + // glBegin(GL_QUADS); + // break; + // } + } + + public void vertex(float[] pos, int color) { + // glColor4ubv(color); + // glVertex3fv(pos); + } + + public void vertex(float x, float y, float z, int color) { + // glColor4ubv(color); + // glVertex3f(x, y, z); + } + + public void vertex(float[] pos, int color, float[] uv) { + // glColor4ubv(color); + // glTexCoord2fv(uv); + // glVertex3fv(pos); + } + + public void vertex(float x, float y, float z, int color, float u, float v) { + // glColor4ubv(color); + // glTexCoord2f(u, v); + // glVertex3f(x, y, z); + } + + private void glColor4ubv(int color) { + // glColor4ub((byte) (color & 0xFF), (byte) ((color >> 8) & 0xFF), (byte) ((color >> 16) & 0xFF), + // (byte) ((color >> 24) & 0xFF)); + } + + public void depthMask(bool state) { + // glDepthMask(state); + } + + public void texture(GLCheckerTexture g_tex, bool state) { + // if (state) { + // glEnable(GL_TEXTURE_2D); + // g_tex.bind(); + // } else { + // glDisable(GL_TEXTURE_2D); + // } + } + + public void end() { + // glEnd(); + // glLineWidth(1.0f); + // glPointSize(1.0f); + } + + public void fog(float start, float end) { + // glFogf(GL_FOG_START, start); + // glFogf(GL_FOG_END, end); + } + +} diff --git a/src/DotRecast.Recast.Demo/Draw/ModernOpenGLDraw.cs b/src/DotRecast.Recast.Demo/Draw/ModernOpenGLDraw.cs new file mode 100644 index 0000000..992cd19 --- /dev/null +++ b/src/DotRecast.Recast.Demo/Draw/ModernOpenGLDraw.cs @@ -0,0 +1,264 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.DotNet.PlatformAbstractions; +using Silk.NET.OpenGL; +using Silk.NET.Windowing; + +namespace DotRecast.Recast.Demo.Draw; + +public class ModernOpenGLDraw : OpenGLDraw { + private GL _gl; + private uint program; + private int uniformTexture; + private int uniformProjectionMatrix; + private uint vbo; + private uint ebo; + private uint vao; + private DebugDrawPrimitives currentPrim; + private float fogStart; + private float fogEnd; + private bool fogEnabled; + private int uniformViewMatrix; + private readonly List vertices = new(); + private GLCheckerTexture _texture; + private float[] _viewMatrix; + private float[] _projectionMatrix; + private int uniformUseTexture; + private int uniformFog; + private int uniformFogStart; + private int uniformFogEnd; + + public void init(GL gl) + { + _gl = gl; + string NK_SHADER_VERSION = PlatformID.MacOSX == Environment.OSVersion.Platform ? "#version 150\n" : "#version 300 es\n"; + string vertex_shader = NK_SHADER_VERSION + "uniform mat4 ProjMtx;\n"// + + "uniform mat4 ViewMtx;\n"// + + "in vec3 Position;\n"// + + "in vec2 TexCoord;\n"// + + "in vec4 Color;\n"// + + "out vec2 Frag_UV;\n"// + + "out vec4 Frag_Color;\n"// + + "out float Frag_Depth;\n"// + + "void main() {\n"// + + " Frag_UV = TexCoord;\n"// + + " Frag_Color = Color;\n"// + + " vec4 VSPosition = ViewMtx * vec4(Position, 1);\n"// + + " Frag_Depth = -VSPosition.z;\n"// + + " gl_Position = ProjMtx * VSPosition;\n"// + + "}\n"; + string fragment_shader = NK_SHADER_VERSION + "precision mediump float;\n"// + + "uniform sampler2D Texture;\n"// + + "uniform float UseTexture;\n"// + + "uniform float EnableFog;\n"// + + "uniform float FogStart;\n"// + + "uniform float FogEnd;\n"// + + "const vec4 FogColor = vec4(0.3f, 0.3f, 0.32f, 1.0f);\n"// + + "in vec2 Frag_UV;\n"// + + "in vec4 Frag_Color;\n"// + + "in float Frag_Depth;\n"// + + "out vec4 Out_Color;\n"// + + "void main(){\n"// + + " Out_Color = mix(FogColor, Frag_Color * mix(vec4(1), texture(Texture, Frag_UV.st), UseTexture), 1.0 - EnableFog * clamp( (Frag_Depth - FogStart) / (FogEnd - FogStart), 0.0, 1.0) );\n"// + + "}\n"; + + program = _gl.CreateProgram(); + uint vert_shdr = _gl.CreateShader(GLEnum.VertexShader); + uint frag_shdr = _gl.CreateShader(GLEnum.FragmentShader); + _gl.ShaderSource(vert_shdr, vertex_shader); + _gl.ShaderSource(frag_shdr, fragment_shader); + _gl.CompileShader(vert_shdr); + _gl.CompileShader(frag_shdr); + gl.GetShader(vert_shdr, GLEnum.CompileStatus, out var status); + if (status != (int) GLEnum.True) { + throw new InvalidOperationException(); + } + gl.GetShader(frag_shdr, GLEnum.CompileStatus, out status); + if (status != (int) GLEnum.True) { + throw new InvalidOperationException(); + } + _gl.AttachShader(program, vert_shdr); + _gl.AttachShader(program, frag_shdr); + _gl.LinkProgram(program); + _gl.GetProgram(program, GLEnum.LinkStatus, out status); + if (status != (int) GLEnum.True) { + throw new InvalidOperationException(); + } + uniformTexture = _gl.GetUniformLocation(program, "Texture"); + uniformUseTexture = _gl.GetUniformLocation(program, "UseTexture"); + uniformFog = _gl.GetUniformLocation(program, "EnableFog"); + uniformFogStart = _gl.GetUniformLocation(program, "FogStart"); + uniformFogEnd = _gl.GetUniformLocation(program, "FogEnd"); + uniformProjectionMatrix = _gl.GetUniformLocation(program, "ProjMtx"); + uniformViewMatrix = _gl.GetUniformLocation(program, "ViewMtx"); + uint attrib_pos = (uint) _gl.GetAttribLocation(program, "Position"); + uint attrib_uv = (uint) _gl.GetAttribLocation(program, "TexCoord"); + uint attrib_col = (uint) _gl.GetAttribLocation(program, "Color"); + + // buffer setup + _gl.GenBuffers(1, out vbo); + _gl.GenBuffers(1, out ebo); + _gl.GenVertexArrays(1, out vao); + + _gl.BindVertexArray(vao); + _gl.BindBuffer(GLEnum.ArrayBuffer, vbo); + _gl.BindBuffer(GLEnum.ElementArrayBuffer, ebo); + + _gl.EnableVertexAttribArray(attrib_pos); + _gl.EnableVertexAttribArray(attrib_uv); + _gl.EnableVertexAttribArray(attrib_col); + + // _gl.VertexAttribPointer(attrib_pos, 3, GLEnum.Float, false, 24, 0); + // _gl.VertexAttribPointer(attrib_uv, 2, GLEnum.Float, false, 24, 12); + // _gl.VertexAttribPointer(attrib_col, 4, GLEnum.UnsignedByte, true, 24, 20); + + _gl.VertexAttribP3(attrib_pos, GLEnum.Float, false, 0); + _gl.VertexAttribP2(attrib_uv, GLEnum.Float, false, 12); + _gl.VertexAttribP4(attrib_col, GLEnum.UnsignedByte, true, 20); + + _gl.BindTexture(GLEnum.Texture2D, 0); + _gl.BindBuffer(GLEnum.ArrayBuffer, 0); + _gl.BindBuffer(GLEnum.ElementArrayBuffer, 0); + _gl.BindVertexArray(0); + } + + public void clear() { + _gl.ClearColor(0.3f, 0.3f, 0.32f, 1.0f); + _gl.Clear((uint)GLEnum.ColorBufferBit | (uint)GLEnum.DepthBufferBit); + _gl.Enable(GLEnum.Blend); + _gl.BlendFunc(GLEnum.SrcAlpha, GLEnum.OneMinusSrcAlpha); + _gl.Disable(GLEnum.Texture2D); + _gl.Enable(GLEnum.DepthTest); + _gl.Enable(GLEnum.CullFace); + } + + public void begin(DebugDrawPrimitives prim, float size) { + currentPrim = prim; + vertices.Clear(); + _gl.LineWidth(size); + _gl.PointSize(size); + } + + public void end() { + // if (vertices.isEmpty()) { + // return; + // } + // glUseProgram(program); + // glUniform1i(uniformTexture, 0); + // glUniformMatrix4fv(uniformViewMatrix, false, viewMatrix); + // glUniformMatrix4fv(uniformProjectionMatrix, false, projectionMatrix); + // glUniform1f(uniformFogStart, fogStart); + // glUniform1f(uniformFogEnd, fogEnd); + // glUniform1f(uniformFog, fogEnabled ? 1.0f : 0.0f); + // glBindVertexArray(vao); + // glBindBuffer(GL_ARRAY_BUFFER, vbo); + // glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo); + // // glBufferData(GL_ARRAY_BUFFER, MAX_VERTEX_BUFFER, GL_STREAM_DRAW); + // // glBufferData(GL_ELEMENT_ARRAY_BUFFER, MAX_ELEMENT_BUFFER, GL_STREAM_DRAW); + // + // int vboSize = vertices.size() * 24; + // int eboSize = currentPrim == DebugDrawPrimitives.QUADS ? vertices.size() * 6 : vertices.size() * 4; + // + // glBufferData(GL_ARRAY_BUFFER, vboSize, GL_STREAM_DRAW); + // glBufferData(GL_ELEMENT_ARRAY_BUFFER, eboSize, GL_STREAM_DRAW); + // // load draw vertices & elements directly into vertex + element buffer + // ByteBuffer verts = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY, vboSize, null); + // ByteBuffer elems = glMapBuffer(GL_ELEMENT_ARRAY_BUFFER, GL_WRITE_ONLY, eboSize, null); + // vertices.forEach(v => v.store(verts)); + // if (currentPrim == DebugDrawPrimitives.QUADS) { + // for (int i = 0; i < vertices.size(); i += 4) { + // elems.putInt(i); + // elems.putInt(i + 1); + // elems.putInt(i + 2); + // elems.putInt(i); + // elems.putInt(i + 2); + // elems.putInt(i + 3); + // } + // + // } else { + // for (int i = 0; i < vertices.size(); i++) { + // elems.putInt(i); + // } + // } + // + // glUnmapBuffer(GL_ELEMENT_ARRAY_BUFFER); + // glUnmapBuffer(GL_ARRAY_BUFFER); + // if (texture != null) { + // texture.bind(); + // glUniform1f(uniformUseTexture, 1.0f); + // } else { + // glUniform1f(uniformUseTexture, 0.0f); + // } + // + // switch (currentPrim) { + // case POINTS: + // glDrawElements(GL_POINTS, vertices.size(), GL_UNSIGNED_INT, 0); + // break; + // case LINES: + // glDrawElements(GL_LINES, vertices.size(), GL_UNSIGNED_INT, 0); + // break; + // case TRIS: + // glDrawElements(GL_TRIANGLES, vertices.size(), GL_UNSIGNED_INT, 0); + // break; + // case QUADS: + // glDrawElements(GL_TRIANGLES, vertices.size() * 6 / 4, GL_UNSIGNED_INT, 0); + // break; + // default: + // break; + // } + // + // glUseProgram(0); + // glBindBuffer(GL_ARRAY_BUFFER, 0); + // glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + // glBindVertexArray(0); + // vertices.clear(); + // glLineWidth(1.0f); + // glPointSize(1.0f); + } + + public void vertex(float x, float y, float z, int color) { + vertices.Add(new OpenGLVertex(x, y, z, color)); + } + + public void vertex(float[] pos, int color) { + vertices.Add(new OpenGLVertex(pos, color)); + } + + public void vertex(float[] pos, int color, float[] uv) { + vertices.Add(new OpenGLVertex(pos, uv, color)); + } + + public void vertex(float x, float y, float z, int color, float u, float v) { + vertices.Add(new OpenGLVertex(x, y, z, u, v, color)); + } + + public void depthMask(bool state) { + _gl.DepthMask(state); + } + + public void texture(GLCheckerTexture g_tex, bool state) { + _texture = state ? g_tex : null; + if (_texture != null) { + _texture.bind(); + } + } + + public void projectionMatrix(float[] projectionMatrix) { + this._projectionMatrix = projectionMatrix; + } + + public void viewMatrix(float[] viewMatrix) { + this._viewMatrix = viewMatrix; + } + + public void fog(float start, float end) { + fogStart = start; + fogEnd = end; + } + + public void fog(bool state) { + fogEnabled = state; + } + +} diff --git a/src/DotRecast.Recast.Demo/Draw/NavMeshRenderer.cs b/src/DotRecast.Recast.Demo/Draw/NavMeshRenderer.cs new file mode 100644 index 0000000..c7633c3 --- /dev/null +++ b/src/DotRecast.Recast.Demo/Draw/NavMeshRenderer.cs @@ -0,0 +1,254 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.Collections.Generic; +using System.Linq; +using DotRecast.Detour; +using DotRecast.Recast.Demo.Builder; +using DotRecast.Recast.Demo.Geom; +using DotRecast.Recast.Demo.Settings; + +namespace DotRecast.Recast.Demo.Draw; + +public class NavMeshRenderer { + + private readonly RecastDebugDraw debugDraw; + + private readonly int navMeshDrawFlags = RecastDebugDraw.DRAWNAVMESH_OFFMESHCONS + | RecastDebugDraw.DRAWNAVMESH_CLOSEDLIST; + + public NavMeshRenderer(RecastDebugDraw debugDraw) { + this.debugDraw = debugDraw; + } + + public RecastDebugDraw getDebugDraw() { + return debugDraw; + } + + public void render(Sample sample) { + if (sample == null) { + return; + } + NavMeshQuery navQuery = sample.getNavMeshQuery(); + DemoInputGeomProvider geom = sample.getInputGeom(); + IList rcBuilderResults = sample.getRecastResults(); + NavMesh navMesh = sample.getNavMesh(); + SettingsUI settingsUI = sample.getSettingsUI(); + debugDraw.fog(true); + debugDraw.depthMask(true); + DrawMode drawMode = settingsUI.getDrawMode(); + + float texScale = 1.0f / (settingsUI.getCellSize() * 10.0f); + float m_agentMaxSlope = settingsUI.getAgentMaxSlope(); + + if (drawMode != DrawMode.DRAWMODE_NAVMESH_TRANS) { + // Draw mesh + if (geom != null) { + debugDraw.debugDrawTriMeshSlope(geom.vertices, geom.faces, geom.normals, m_agentMaxSlope, texScale); + drawOffMeshConnections(geom, false); + } + } + + debugDraw.fog(false); + debugDraw.depthMask(false); + if (geom != null) { + drawGeomBounds(geom); + } + + if (navMesh != null && navQuery != null + && (drawMode == DrawMode.DRAWMODE_NAVMESH || drawMode == DrawMode.DRAWMODE_NAVMESH_TRANS + || drawMode == DrawMode.DRAWMODE_NAVMESH_BVTREE || drawMode == DrawMode.DRAWMODE_NAVMESH_NODES + || drawMode == DrawMode.DRAWMODE_NAVMESH_INVIS + || drawMode == DrawMode.DRAWMODE_NAVMESH_PORTALS)) { + if (drawMode != DrawMode.DRAWMODE_NAVMESH_INVIS) { + debugDraw.debugDrawNavMeshWithClosedList(navMesh, navQuery, navMeshDrawFlags); + } + if (drawMode == DrawMode.DRAWMODE_NAVMESH_BVTREE) { + debugDraw.debugDrawNavMeshBVTree(navMesh); + } + if (drawMode == DrawMode.DRAWMODE_NAVMESH_PORTALS) { + debugDraw.debugDrawNavMeshPortals(navMesh); + } + if (drawMode == DrawMode.DRAWMODE_NAVMESH_NODES) { + debugDraw.debugDrawNavMeshNodes(navQuery); + debugDraw.debugDrawNavMeshPolysWithFlags(navMesh, SampleAreaModifications.SAMPLE_POLYFLAGS_DISABLED, + DebugDraw.duRGBA(0, 0, 0, 128)); + } + } + + debugDraw.depthMask(true); + + foreach (RecastBuilderResult rcBuilderResult in rcBuilderResults) { + if (rcBuilderResult.getCompactHeightfield() != null && drawMode == DrawMode.DRAWMODE_COMPACT) { + debugDraw.debugDrawCompactHeightfieldSolid(rcBuilderResult.getCompactHeightfield()); + } + if (rcBuilderResult.getCompactHeightfield() != null && drawMode == DrawMode.DRAWMODE_COMPACT_DISTANCE) { + debugDraw.debugDrawCompactHeightfieldDistance(rcBuilderResult.getCompactHeightfield()); + } + if (rcBuilderResult.getCompactHeightfield() != null && drawMode == DrawMode.DRAWMODE_COMPACT_REGIONS) { + debugDraw.debugDrawCompactHeightfieldRegions(rcBuilderResult.getCompactHeightfield()); + } + if (rcBuilderResult.getSolidHeightfield() != null && drawMode == DrawMode.DRAWMODE_VOXELS) { + debugDraw.fog(true); + debugDraw.debugDrawHeightfieldSolid(rcBuilderResult.getSolidHeightfield()); + debugDraw.fog(false); + } + if (rcBuilderResult.getSolidHeightfield() != null && drawMode == DrawMode.DRAWMODE_VOXELS_WALKABLE) { + debugDraw.fog(true); + debugDraw.debugDrawHeightfieldWalkable(rcBuilderResult.getSolidHeightfield()); + debugDraw.fog(false); + } + if (rcBuilderResult.getContourSet() != null && drawMode == DrawMode.DRAWMODE_RAW_CONTOURS) { + debugDraw.depthMask(false); + debugDraw.debugDrawRawContours(rcBuilderResult.getContourSet(), 1f); + debugDraw.depthMask(true); + } + if (rcBuilderResult.getContourSet() != null && drawMode == DrawMode.DRAWMODE_BOTH_CONTOURS) { + debugDraw.depthMask(false); + debugDraw.debugDrawRawContours(rcBuilderResult.getContourSet(), 0.5f); + debugDraw.debugDrawContours(rcBuilderResult.getContourSet()); + debugDraw.depthMask(true); + } + if (rcBuilderResult.getContourSet() != null && drawMode == DrawMode.DRAWMODE_CONTOURS) { + debugDraw.depthMask(false); + debugDraw.debugDrawContours(rcBuilderResult.getContourSet()); + debugDraw.depthMask(true); + } + if (rcBuilderResult.getCompactHeightfield() != null && drawMode == DrawMode.DRAWMODE_REGION_CONNECTIONS) { + debugDraw.debugDrawCompactHeightfieldRegions(rcBuilderResult.getCompactHeightfield()); + debugDraw.depthMask(false); + if (rcBuilderResult.getContourSet() != null) { + debugDraw.debugDrawRegionConnections(rcBuilderResult.getContourSet()); + } + debugDraw.depthMask(true); + } + if (rcBuilderResult.getMesh() != null && drawMode == DrawMode.DRAWMODE_POLYMESH) { + debugDraw.depthMask(false); + debugDraw.debugDrawPolyMesh(rcBuilderResult.getMesh()); + debugDraw.depthMask(true); + } + if (rcBuilderResult.getMeshDetail() != null && drawMode == DrawMode.DRAWMODE_POLYMESH_DETAIL) { + debugDraw.depthMask(false); + debugDraw.debugDrawPolyMeshDetail(rcBuilderResult.getMeshDetail()); + debugDraw.depthMask(true); + } + } + + if (geom != null) { + drawConvexVolumes(geom); + } + } + + private void drawGeomBounds(DemoInputGeomProvider geom) { + // Draw bounds + float[] bmin = geom.getMeshBoundsMin(); + float[] bmax = geom.getMeshBoundsMax(); + debugDraw.debugDrawBoxWire(bmin[0], bmin[1], bmin[2], bmax[0], bmax[1], bmax[2], + DebugDraw.duRGBA(255, 255, 255, 128), 1.0f); + debugDraw.begin(DebugDrawPrimitives.POINTS, 5.0f); + debugDraw.vertex(bmin[0], bmin[1], bmin[2], DebugDraw.duRGBA(255, 255, 255, 128)); + debugDraw.end(); + } + + public void drawOffMeshConnections(DemoInputGeomProvider geom, bool hilight) { + int conColor = DebugDraw.duRGBA(192, 0, 128, 192); + int baseColor = DebugDraw.duRGBA(0, 0, 0, 64); + debugDraw.depthMask(false); + + debugDraw.begin(DebugDrawPrimitives.LINES, 2.0f); + foreach (DemoOffMeshConnection con in geom.getOffMeshConnections()) { + + float[] v = con.verts; + debugDraw.vertex(v[0], v[1], v[2], baseColor); + debugDraw.vertex(v[0], v[1] + 0.2f, v[2], baseColor); + + debugDraw.vertex(v[3], v[4], v[5], baseColor); + debugDraw.vertex(v[3], v[4] + 0.2f, v[5], baseColor); + + debugDraw.appendCircle(v[0], v[1] + 0.1f, v[2], con.radius, baseColor); + debugDraw.appendCircle(v[3], v[4] + 0.1f, v[5], con.radius, baseColor); + + if (hilight) { + debugDraw.appendArc(v[0], v[1], v[2], v[3], v[4], v[5], 0.25f, con.bidir ? 0.6f : 0.0f, 0.6f, conColor); + } + } + debugDraw.end(); + + debugDraw.depthMask(true); + } + + void drawConvexVolumes(DemoInputGeomProvider geom) { + debugDraw.depthMask(false); + + debugDraw.begin(DebugDrawPrimitives.TRIS); + + foreach (ConvexVolume vol in geom.convexVolumes()) { + int col = DebugDraw.duTransCol(DebugDraw.areaToCol(vol.areaMod.getMaskedValue()), 32); + for (int j = 0, k = vol.verts.Length - 3; j < vol.verts.Length; k = j, j += 3) { + float[] va = new float[] { vol.verts[k], vol.verts[k + 1], vol.verts[k + 2] }; + float[] vb = new float[] { vol.verts[j], vol.verts[j + 1], vol.verts[j + 2] }; + + debugDraw.vertex(vol.verts[0], vol.hmax, vol.verts[2], col); + debugDraw.vertex(vb[0], vol.hmax, vb[2], col); + debugDraw.vertex(va[0], vol.hmax, va[2], col); + + debugDraw.vertex(va[0], vol.hmin, va[2], DebugDraw.duDarkenCol(col)); + debugDraw.vertex(va[0], vol.hmax, va[2], col); + debugDraw.vertex(vb[0], vol.hmax, vb[2], col); + + debugDraw.vertex(va[0], vol.hmin, va[2], DebugDraw.duDarkenCol(col)); + debugDraw.vertex(vb[0], vol.hmax, vb[2], col); + debugDraw.vertex(vb[0], vol.hmin, vb[2], DebugDraw.duDarkenCol(col)); + } + } + + debugDraw.end(); + + debugDraw.begin(DebugDrawPrimitives.LINES, 2.0f); + foreach (ConvexVolume vol in geom.convexVolumes()) { + int col = DebugDraw.duTransCol(DebugDraw.areaToCol(vol.areaMod.getMaskedValue()), 220); + for (int j = 0, k = vol.verts.Length - 3; j < vol.verts.Length; k = j, j += 3) { + float[] va = new float[] { vol.verts[k], vol.verts[k + 1], vol.verts[k + 2] }; + float[] vb = new float[] { vol.verts[j], vol.verts[j + 1], vol.verts[j + 2] }; + debugDraw.vertex(va[0], vol.hmin, va[2], DebugDraw.duDarkenCol(col)); + debugDraw.vertex(vb[0], vol.hmin, vb[2], DebugDraw.duDarkenCol(col)); + debugDraw.vertex(va[0], vol.hmax, va[2], col); + debugDraw.vertex(vb[0], vol.hmax, vb[2], col); + debugDraw.vertex(va[0], vol.hmin, va[2], DebugDraw.duDarkenCol(col)); + debugDraw.vertex(va[0], vol.hmax, va[2], col); + } + } + debugDraw.end(); + + debugDraw.begin(DebugDrawPrimitives.POINTS, 3.0f); + foreach (ConvexVolume vol in geom.convexVolumes()) { + int col = DebugDraw + .duDarkenCol(DebugDraw.duTransCol(DebugDraw.areaToCol(vol.areaMod.getMaskedValue()), 220)); + for (int j = 0; j < vol.verts.Length; j += 3) { + debugDraw.vertex(vol.verts[j + 0], vol.verts[j + 1] + 0.1f, vol.verts[j + 2], col); + debugDraw.vertex(vol.verts[j + 0], vol.hmin, vol.verts[j + 2], col); + debugDraw.vertex(vol.verts[j + 0], vol.hmax, vol.verts[j + 2], col); + } + } + debugDraw.end(); + + debugDraw.depthMask(true); + } + +} diff --git a/src/DotRecast.Recast.Demo/Draw/OpenGLDraw.cs b/src/DotRecast.Recast.Demo/Draw/OpenGLDraw.cs new file mode 100644 index 0000000..23c1c00 --- /dev/null +++ b/src/DotRecast.Recast.Demo/Draw/OpenGLDraw.cs @@ -0,0 +1,35 @@ +using Silk.NET.OpenGL; + +namespace DotRecast.Recast.Demo.Draw; + +public interface OpenGLDraw { + + void init(GL gl); + + void clear(); + + void begin(DebugDrawPrimitives prim, float size); + + void end(); + + void vertex(float x, float y, float z, int color); + + void vertex(float[] pos, int color); + + void vertex(float[] pos, int color, float[] uv); + + void vertex(float x, float y, float z, int color, float u, float v); + + void fog(bool state); + + void depthMask(bool state); + + void texture(GLCheckerTexture g_tex, bool state); + + void projectionMatrix(float[] projectionMatrix); + + void viewMatrix(float[] viewMatrix); + + void fog(float start, float end); + +} diff --git a/src/DotRecast.Recast.Demo/Draw/OpenGLVertex.cs b/src/DotRecast.Recast.Demo/Draw/OpenGLVertex.cs new file mode 100644 index 0000000..74ac0c8 --- /dev/null +++ b/src/DotRecast.Recast.Demo/Draw/OpenGLVertex.cs @@ -0,0 +1,43 @@ +using DotRecast.Core; + +namespace DotRecast.Recast.Demo.Draw; + +public class OpenGLVertex { + + private readonly float x; + private readonly float y; + private readonly float z; + private readonly int color; + private readonly float u; + private readonly float v; + + public OpenGLVertex(float[] pos, float[] uv, int color) : + this(pos[0], pos[1], pos[2], uv[0], uv[1], color) { + } + + public OpenGLVertex(float[] pos, int color) : + this(pos[0], pos[1], pos[2], 0f, 0f, color) { + } + + public OpenGLVertex(float x, float y, float z, int color) : + this(x, y, z, 0f, 0f, color) { + } + + public OpenGLVertex(float x, float y, float z, float u, float v, int color) { + this.x = x; + this.y = y; + this.z = z; + this.u = u; + this.v = v; + this.color = color; + } + + public void store(ByteBuffer buffer) { + buffer.putFloat(x); + buffer.putFloat(y); + buffer.putFloat(z); + buffer.putFloat(u); + buffer.putFloat(v); + buffer.putInt(color); + } +} diff --git a/src/DotRecast.Recast.Demo/Draw/RecastDebugDraw.cs b/src/DotRecast.Recast.Demo/Draw/RecastDebugDraw.cs new file mode 100644 index 0000000..2b951c6 --- /dev/null +++ b/src/DotRecast.Recast.Demo/Draw/RecastDebugDraw.cs @@ -0,0 +1,1136 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.Collections.Generic; +using DotRecast.Detour; +using DotRecast.Recast.Demo.Builder; + +namespace DotRecast.Recast.Demo.Draw; + +public class RecastDebugDraw : DebugDraw { + + public static readonly int DRAWNAVMESH_OFFMESHCONS = 0x01; + public static readonly int DRAWNAVMESH_CLOSEDLIST = 0x02; + public static readonly int DRAWNAVMESH_COLOR_TILES = 0x04; + + public void debugDrawTriMeshSlope(float[] verts, int[] tris, float[] normals, float walkableSlopeAngle, + float texScale) { + + float walkableThr = (float) Math.Cos(walkableSlopeAngle / 180.0f * Math.PI); + + float[] uva = new float[2]; + float[] uvb = new float[2]; + float[] uvc = new float[2]; + + texture(true); + + int unwalkable = duRGBA(192, 128, 0, 255); + begin(DebugDrawPrimitives.TRIS); + for (int i = 0; i < tris.Length; i += 3) { + float[] norm = new float[] { normals[i], normals[i + 1], normals[i + 2] }; + + int color; + char a = (char) (220 * (2 + norm[0] + norm[1]) / 4); + if (norm[1] < walkableThr) { + color = duLerpCol(duRGBA(a, a, a, 255), unwalkable, 64); + } else { + color = duRGBA(a, a, a, 255); + } + + float[] va = new float[] { verts[tris[i] * 3], verts[tris[i] * 3 + 1], verts[tris[i] * 3 + 2] }; + float[] vb = new float[] { verts[tris[i + 1] * 3], verts[tris[i + 1] * 3 + 1], verts[tris[i + 1] * 3 + 2] }; + float[] vc = new float[] { verts[tris[i + 2] * 3], verts[tris[i + 2] * 3 + 1], verts[tris[i + 2] * 3 + 2] }; + + int ax = 0, ay = 0; + if (Math.Abs(norm[1]) > Math.Abs(norm[ax])) { + ax = 1; + } + if (Math.Abs(norm[2]) > Math.Abs(norm[ax])) { + ax = 2; + } + ax = (1 << ax) & 3; // +1 mod 3 + ay = (1 << ax) & 3; // +1 mod 3 + + uva[0] = va[ax] * texScale; + uva[1] = va[ay] * texScale; + uvb[0] = vb[ax] * texScale; + uvb[1] = vb[ay] * texScale; + uvc[0] = vc[ax] * texScale; + uvc[1] = vc[ay] * texScale; + + vertex(va, color, uva); + vertex(vb, color, uvb); + vertex(vc, color, uvc); + } + end(); + + texture(false); + } + + public void debugDrawNavMeshWithClosedList(NavMesh mesh, NavMeshQuery query, int flags) { + NavMeshQuery q = (flags & DRAWNAVMESH_CLOSEDLIST) != 0 ? query : null; + for (int i = 0; i < mesh.getMaxTiles(); ++i) { + MeshTile tile = mesh.getTile(i); + if (tile != null && tile.data != null) { + drawMeshTile(mesh, q, tile, flags); + } + } + } + + private void drawMeshTile(NavMesh mesh, NavMeshQuery query, MeshTile tile, int flags) { + long @base = mesh.getPolyRefBase(tile); + + int tileNum = NavMesh.decodePolyIdTile(@base); + int tileColor = duIntToCol(tileNum, 128); + depthMask(false); + begin(DebugDrawPrimitives.TRIS); + for (int i = 0; i < tile.data.header.polyCount; ++i) { + Poly p = tile.data.polys[i]; + if (p.getType() == Poly.DT_POLYTYPE_OFFMESH_CONNECTION) { + continue; + } + int col; + if (query != null && query.isInClosedList(@base | i)) { + col = duRGBA(255, 196, 0, 64); + } else { + if ((flags & DRAWNAVMESH_COLOR_TILES) != 0) { + col = tileColor; + } else { + if ((p.flags & SampleAreaModifications.SAMPLE_POLYFLAGS_DISABLED) != 0) { + col = duRGBA(64, 64, 64, 64); + } else { + col = duTransCol(areaToCol(p.getArea()), 64); + } + } + } + + drawPoly(tile, i, col); + + } + end(); + + // Draw inter poly boundaries + drawPolyBoundaries(tile, duRGBA(0, 48, 64, 32), 1.5f, true); + + // Draw outer poly boundaries + drawPolyBoundaries(tile, duRGBA(0, 48, 64, 220), 2.5f, false); + + if ((flags & DRAWNAVMESH_OFFMESHCONS) != 0) { + begin(DebugDrawPrimitives.LINES, 2.0f); + for (int i = 0; i < tile.data.header.polyCount; ++i) { + Poly p = tile.data.polys[i]; + + if (p.getType() != Poly.DT_POLYTYPE_OFFMESH_CONNECTION) { + continue; + } + + int col, col2; + if (query != null && query.isInClosedList(@base | i)) { + col = duRGBA(255, 196, 0, 220); + } else { + col = duDarkenCol(duTransCol(areaToCol(p.getArea()), 220)); + } + + OffMeshConnection con = tile.data.offMeshCons[i - tile.data.header.offMeshBase]; + float[] va = new float[] { tile.data.verts[p.verts[0] * 3], tile.data.verts[p.verts[0] * 3 + 1], + tile.data.verts[p.verts[0] * 3 + 2] }; + float[] vb = new float[] { tile.data.verts[p.verts[1] * 3], tile.data.verts[p.verts[1] * 3 + 1], + tile.data.verts[p.verts[1] * 3 + 2] }; + + // Check to see if start and end end-points have links. + bool startSet = false; + bool endSet = false; + for (int k = tile.polyLinks[p.index]; k != NavMesh.DT_NULL_LINK; k = tile.links[k].next) { + if (tile.links[k].edge == 0) { + startSet = true; + } + if (tile.links[k].edge == 1) { + endSet = true; + } + } + + // End points and their on-mesh locations. + vertex(va[0], va[1], va[2], col); + vertex(con.pos[0], con.pos[1], con.pos[2], col); + col2 = startSet ? col : duRGBA(220, 32, 16, 196); + appendCircle(con.pos[0], con.pos[1] + 0.1f, con.pos[2], con.rad, col2); + + vertex(vb[0], vb[1], vb[2], col); + vertex(con.pos[3], con.pos[4], con.pos[5], col); + col2 = endSet ? col : duRGBA(220, 32, 16, 196); + appendCircle(con.pos[3], con.pos[4] + 0.1f, con.pos[5], con.rad, col2); + + // End point vertices. + vertex(con.pos[0], con.pos[1], con.pos[2], duRGBA(0, 48, 64, 196)); + vertex(con.pos[0], con.pos[1] + 0.2f, con.pos[2], duRGBA(0, 48, 64, 196)); + + vertex(con.pos[3], con.pos[4], con.pos[5], duRGBA(0, 48, 64, 196)); + vertex(con.pos[3], con.pos[4] + 0.2f, con.pos[5], duRGBA(0, 48, 64, 196)); + + // Connection arc. + appendArc(con.pos[0], con.pos[1], con.pos[2], con.pos[3], con.pos[4], con.pos[5], 0.25f, + (con.flags & 1) != 0 ? 0.6f : 0, 0.6f, col); + + } + + end(); + } + + int vcol = duRGBA(0, 0, 0, 196); + begin(DebugDrawPrimitives.POINTS, 3.0f); + for (int i = 0; i < tile.data.header.vertCount; i++) { + int v = i * 3; + vertex(tile.data.verts[v], tile.data.verts[v + 1], tile.data.verts[v + 2], vcol); + } + end(); + + depthMask(true); + } + + private void drawPoly(MeshTile tile, int index, int col) { + Poly p = tile.data.polys[index]; + if (tile.data.detailMeshes != null) { + PolyDetail pd = tile.data.detailMeshes[index]; + if (pd != null) { + for (int j = 0; j < pd.triCount; ++j) { + int t = (pd.triBase + j) * 4; + for (int k = 0; k < 3; ++k) { + int v = tile.data.detailTris[t + k]; + if (v < p.vertCount) { + vertex(tile.data.verts[p.verts[v] * 3], tile.data.verts[p.verts[v] * 3 + 1], + tile.data.verts[p.verts[v] * 3 + 2], col); + } else { + vertex(tile.data.detailVerts[(pd.vertBase + v - p.vertCount) * 3], + tile.data.detailVerts[(pd.vertBase + v - p.vertCount) * 3 + 1], + tile.data.detailVerts[(pd.vertBase + v - p.vertCount) * 3 + 2], col); + } + } + } + } + } else { + for (int j = 1; j < p.vertCount - 1; ++j) { + vertex(tile.data.verts[p.verts[0] * 3], tile.data.verts[p.verts[0] * 3 + 1], + tile.data.verts[p.verts[0] * 3 + 2], col); + for (int k = 0; k < 2; ++k) { + vertex(tile.data.verts[p.verts[j + k] * 3], tile.data.verts[p.verts[j + k] * 3 + 1], + tile.data.verts[p.verts[j + k] * 3 + 2], col); + } + } + } + } + + void drawPolyBoundaries(MeshTile tile, int col, float linew, bool inner) { + float thr = 0.01f * 0.01f; + + begin(DebugDrawPrimitives.LINES, linew); + + for (int i = 0; i < tile.data.header.polyCount; ++i) { + Poly p = tile.data.polys[i]; + + if (p.getType() == Poly.DT_POLYTYPE_OFFMESH_CONNECTION) { + continue; + } + + for (int j = 0, nj = p.vertCount; j < nj; ++j) { + int c = col; + if (inner) { + if (p.neis[j] == 0) { + continue; + } + if ((p.neis[j] & NavMesh.DT_EXT_LINK) != 0) { + bool con = false; + for (int k = tile.polyLinks[p.index]; k != NavMesh.DT_NULL_LINK; k = tile.links[k].next) { + if (tile.links[k].edge == j) { + con = true; + break; + } + } + if (con) { + c = duRGBA(255, 255, 255, 48); + } else { + c = duRGBA(0, 0, 0, 48); + } + } else { + c = duRGBA(0, 48, 64, 32); + } + } else { + if (p.neis[j] != 0) { + continue; + } + } + + float[] v0 = new float[] { tile.data.verts[p.verts[j] * 3], tile.data.verts[p.verts[j] * 3 + 1], + tile.data.verts[p.verts[j] * 3 + 2] }; + float[] v1 = new float[] { tile.data.verts[p.verts[(j + 1) % nj] * 3], + tile.data.verts[p.verts[(j + 1) % nj] * 3 + 1], + tile.data.verts[p.verts[(j + 1) % nj] * 3 + 2] }; + + // Draw detail mesh edges which align with the actual poly edge. + // This is really slow. + if (tile.data.detailMeshes != null) { + PolyDetail pd = tile.data.detailMeshes[i]; + for (int k = 0; k < pd.triCount; ++k) { + int t = (pd.triBase + k) * 4; + float[][] tv = new float[3][]; + for (int m = 0; m < 3; ++m) { + int v = tile.data.detailTris[t + m]; + if (v < p.vertCount) { + tv[m] = new float[] { tile.data.verts[p.verts[v] * 3], tile.data.verts[p.verts[v] * 3 + 1], + tile.data.verts[p.verts[v] * 3 + 2] }; + } else { + tv[m] = new float[] { tile.data.detailVerts[(pd.vertBase + (v - p.vertCount)) * 3], + tile.data.detailVerts[(pd.vertBase + (v - p.vertCount)) * 3 + 1], + tile.data.detailVerts[(pd.vertBase + (v - p.vertCount)) * 3 + 2] }; + } + } + for (int m = 0, n = 2; m < 3; n = m++) { + if ((NavMesh.getDetailTriEdgeFlags(tile.data.detailTris[t + 3], n) & NavMesh.DT_DETAIL_EDGE_BOUNDARY) == 0) + continue; + + if (((tile.data.detailTris[t + 3] >> (n * 2)) & 0x3) == 0) { + continue; // Skip inner detail edges. + } + if (distancePtLine2d(tv[n], v0, v1) < thr && distancePtLine2d(tv[m], v0, v1) < thr) { + vertex(tv[n], c); + vertex(tv[m], c); + } + } + } + } else { + vertex(v0, c); + vertex(v1, c); + } + } + } + end(); + } + + static float distancePtLine2d(float[] pt, float[] p, float[] q) { + float pqx = q[0] - p[0]; + float pqz = q[2] - p[2]; + float dx = pt[0] - p[0]; + float dz = pt[2] - p[2]; + float d = pqx * pqx + pqz * pqz; + float t = pqx * dx + pqz * dz; + if (d != 0) { + t /= d; + } + dx = p[0] + t * pqx - pt[0]; + dz = p[2] + t * pqz - pt[2]; + return dx * dx + dz * dz; + } + + public void debugDrawNavMeshBVTree(NavMesh mesh) { + + for (int i = 0; i < mesh.getMaxTiles(); ++i) { + MeshTile tile = mesh.getTile(i); + if (tile != null && tile.data != null && tile.data.header != null) { + drawMeshTileBVTree(tile); + } + } + } + + private void drawMeshTileBVTree(MeshTile tile) { + // Draw BV nodes. + float cs = 1.0f / tile.data.header.bvQuantFactor; + begin(DebugDrawPrimitives.LINES, 1.0f); + for (int i = 0; i < tile.data.header.bvNodeCount; ++i) { + BVNode n = tile.data.bvTree[i]; + if (n.i < 0) { + continue; + } + appendBoxWire(tile.data.header.bmin[0] + n.bmin[0] * cs, tile.data.header.bmin[1] + n.bmin[1] * cs, + tile.data.header.bmin[2] + n.bmin[2] * cs, tile.data.header.bmin[0] + n.bmax[0] * cs, + tile.data.header.bmin[1] + n.bmax[1] * cs, tile.data.header.bmin[2] + n.bmax[2] * cs, + duRGBA(255, 255, 255, 128)); + } + end(); + } + + public void debugDrawCompactHeightfieldSolid(CompactHeightfield chf) { + float cs = chf.cs; + float ch = chf.ch; + + begin(DebugDrawPrimitives.QUADS); + + for (int y = 0; y < chf.height; ++y) { + for (int x = 0; x < chf.width; ++x) { + float fx = chf.bmin[0] + x * cs; + float fz = chf.bmin[2] + y * cs; + CompactCell c = chf.cells[x + y * chf.width]; + + for (int i = c.index, ni = c.index + c.count; i < ni; ++i) { + CompactSpan s = chf.spans[i]; + + int area = chf.areas[i]; + int color; + if (area == SampleAreaModifications.SAMPLE_POLYAREA_TYPE_WALKABLE) { + color = duRGBA(0, 192, 255, 64); + } else if (area == RecastConstants.RC_NULL_AREA) { + color = duRGBA(0, 0, 0, 64); + } else { + color = areaToCol(area); + } + + float fy = chf.bmin[1] + (s.y + 1) * ch; + vertex(fx, fy, fz, color); + vertex(fx, fy, fz + cs, color); + vertex(fx + cs, fy, fz + cs, color); + vertex(fx + cs, fy, fz, color); + } + } + } + end(); + } + + public void debugDrawRegionConnections(ContourSet cset) { + float alpha = 1f; + + float[] orig = cset.bmin; + float cs = cset.cs; + float ch = cset.ch; + + int color = duRGBA(0, 0, 0, 196); + + begin(DebugDrawPrimitives.LINES, 2.0f); + + for (int i = 0; i < cset.conts.Count; ++i) { + Contour cont = cset.conts[i]; + float[] pos = getContourCenter(cont, orig, cs, ch); + for (int j = 0; j < cont.nverts; ++j) { + int v = j * 4; + if (cont.verts[v + 3] == 0 || (short) cont.verts[v + 3] < cont.reg) { + continue; + } + Contour cont2 = findContourFromSet(cset, (short) cont.verts[v + 3]); + if (cont2 != null) { + float[] pos2 = getContourCenter(cont2, orig, cs, ch); + appendArc(pos[0], pos[1], pos[2], pos2[0], pos2[1], pos2[2], 0.25f, 0.6f, 0.6f, color); + } + } + } + + end(); + + char a = (char) (alpha * 255.0f); + + begin(DebugDrawPrimitives.POINTS, 7.0f); + + for (int i = 0; i < cset.conts.Count; ++i) { + Contour cont = cset.conts[i]; + int col = duDarkenCol(duIntToCol(cont.reg, a)); + float[] pos = getContourCenter(cont, orig, cs, ch); + vertex(pos, col); + } + end(); + } + + private float[] getContourCenter(Contour cont, float[] orig, float cs, float ch) { + float[] center = new float[3]; + center[0] = 0; + center[1] = 0; + center[2] = 0; + if (cont.nverts == 0) { + return center; + } + for (int i = 0; i < cont.nverts; ++i) { + int v = i * 4; + center[0] += cont.verts[v + 0]; + center[1] += cont.verts[v + 1]; + center[2] += cont.verts[v + 2]; + } + float s = 1.0f / cont.nverts; + center[0] *= s * cs; + center[1] *= s * ch; + center[2] *= s * cs; + center[0] += orig[0]; + center[1] += orig[1] + 4 * ch; + center[2] += orig[2]; + return center; + } + + private Contour findContourFromSet(ContourSet cset, int reg) { + for (int i = 0; i < cset.conts.Count; ++i) { + if (cset.conts[i].reg == reg) { + return cset.conts[i]; + } + } + return null; + } + + public void debugDrawRawContours(ContourSet cset, float alpha) { + + float[] orig = cset.bmin; + float cs = cset.cs; + float ch = cset.ch; + + char a = (char) (alpha * 255.0f); + + begin(DebugDrawPrimitives.LINES, 2.0f); + + for (int i = 0; i < cset.conts.Count; ++i) { + Contour c = cset.conts[i]; + int color = duIntToCol(c.reg, a); + + for (int j = 0; j < c.nrverts; ++j) { + int v0 = c.rverts[j * 4]; + int v1 = c.rverts[j * 4 + 1]; + int v2 = c.rverts[j * 4 + 2]; + float fx = orig[0] + v0 * cs; + float fy = orig[1] + (v1 + 1 + (i & 1)) * ch; + float fz = orig[2] + v2 * cs; + vertex(fx, fy, fz, color); + if (j > 0) { + vertex(fx, fy, fz, color); + } + } + + // Loop last segment. + { + int v0 = c.rverts[0]; + int v1 = c.rverts[1]; + int v2 = c.rverts[2]; + float fx = orig[0] + v0 * cs; + float fy = orig[1] + (v1 + 1 + (i & 1)) * ch; + float fz = orig[2] + v2 * cs; + vertex(fx, fy, fz, color); + } + } + end(); + + begin(DebugDrawPrimitives.POINTS, 2.0f); + + for (int i = 0; i < cset.conts.Count; ++i) { + Contour c = cset.conts[i]; + int color = duDarkenCol(duIntToCol(c.reg, a)); + + for (int j = 0; j < c.nrverts; ++j) { + int v0 = c.rverts[j * 4]; + int v1 = c.rverts[j * 4 + 1]; + int v2 = c.rverts[j * 4 + 2]; + int v3 = c.rverts[j * 4 + 3]; + float off = 0; + int colv = color; + if ((v3 & RecastConstants.RC_BORDER_VERTEX) != 0) { + colv = duRGBA(255, 255, 255, a); + off = ch * 2; + } + + float fx = orig[0] + v0 * cs; + float fy = orig[1] + (v1 + 1 + (i & 1)) * ch + off; + float fz = orig[2] + v2 * cs; + vertex(fx, fy, fz, colv); + } + } + end(); + } + + public void debugDrawContours(ContourSet cset) { + float alpha = 1f; + float[] orig = cset.bmin; + float cs = cset.cs; + float ch = cset.ch; + + char a = (char) (alpha * 255.0f); + + begin(DebugDrawPrimitives.LINES, 2.5f); + + for (int i = 0; i < cset.conts.Count; ++i) { + Contour c = cset.conts[i]; + if (c.nverts == 0) { + continue; + } + int color = duIntToCol(c.reg, a); + int bcolor = duLerpCol(color, duRGBA(255, 255, 255, a), 128); + + for (int j = 0, k = c.nverts - 1; j < c.nverts; k = j++) { + int va0 = c.verts[k * 4]; + int va1 = c.verts[k * 4 + 1]; + int va2 = c.verts[k * 4 + 2]; + int va3 = c.verts[k * 4 + 3]; + int vb0 = c.verts[j * 4]; + int vb1 = c.verts[j * 4 + 1]; + int vb2 = c.verts[j * 4 + 2]; + int col = (va3 & RecastConstants.RC_AREA_BORDER) != 0 ? bcolor : color; + + float fx = orig[0] + va0 * cs; + float fy = orig[1] + (va1 + 1 + (i & 1)) * ch; + float fz = orig[2] + va2 * cs; + vertex(fx, fy, fz, col); + + fx = orig[0] + vb0 * cs; + fy = orig[1] + (vb1 + 1 + (i & 1)) * ch; + fz = orig[2] + vb2 * cs; + vertex(fx, fy, fz, col); + } + } + end(); + + begin(DebugDrawPrimitives.POINTS, 3.0f); + + for (int i = 0; i < cset.conts.Count; ++i) { + Contour c = cset.conts[i]; + int color = duDarkenCol(duIntToCol(c.reg, a)); + + for (int j = 0; j < c.nverts; ++j) { + int v0 = c.verts[j * 4]; + int v1 = c.verts[j * 4 + 1]; + int v2 = c.verts[j * 4 + 2]; + int v3 = c.verts[j * 4 + 3]; + float off = 0; + int colv = color; + if ((v3 & RecastConstants.RC_BORDER_VERTEX) != 0) { + colv = duRGBA(255, 255, 255, a); + off = ch * 2; + } + + float fx = orig[0] + v0 * cs; + float fy = orig[1] + (v1 + 1 + (i & 1)) * ch + off; + float fz = orig[2] + v2 * cs; + vertex(fx, fy, fz, colv); + } + } + end(); + } + + public void debugDrawHeightfieldSolid(Heightfield hf) { + + if (!frustumTest(hf.bmin, hf.bmax)) { + return; + } + + float[] orig = hf.bmin; + float cs = hf.cs; + float ch = hf.ch; + + int w = hf.width; + int h = hf.height; + + int[] fcol = new int[6]; + duCalcBoxColors(fcol, duRGBA(255, 255, 255, 255), duRGBA(255, 255, 255, 255)); + + begin(DebugDrawPrimitives.QUADS); + + for (int y = 0; y < h; ++y) { + for (int x = 0; x < w; ++x) { + float fx = orig[0] + x * cs; + float fz = orig[2] + y * cs; + Span s = hf.spans[x + y * w]; + while (s != null) { + appendBox(fx, orig[1] + s.smin * ch, fz, fx + cs, orig[1] + s.smax * ch, fz + cs, fcol); + s = s.next; + } + } + } + end(); + } + + public void debugDrawHeightfieldWalkable(Heightfield hf) { + float[] orig = hf.bmin; + float cs = hf.cs; + float ch = hf.ch; + + int w = hf.width; + int h = hf.height; + + int[] fcol = new int[6]; + duCalcBoxColors(fcol, duRGBA(255, 255, 255, 255), duRGBA(217, 217, 217, 255)); + + begin(DebugDrawPrimitives.QUADS); + + for (int y = 0; y < h; ++y) { + for (int x = 0; x < w; ++x) { + float fx = orig[0] + x * cs; + float fz = orig[2] + y * cs; + Span s = hf.spans[x + y * w]; + while (s != null) { + if (s.area == SampleAreaModifications.SAMPLE_POLYAREA_TYPE_WALKABLE) { + fcol[0] = duRGBA(64, 128, 160, 255); + } else if (s.area == RecastConstants.RC_NULL_AREA) { + fcol[0] = duRGBA(64, 64, 64, 255); + } else { + fcol[0] = duMultCol(areaToCol(s.area), 200); + } + + appendBox(fx, orig[1] + s.smin * ch, fz, fx + cs, orig[1] + s.smax * ch, fz + cs, fcol); + s = s.next; + } + } + } + + end(); + } + + public void debugDrawCompactHeightfieldRegions(CompactHeightfield chf) { + float cs = chf.cs; + float ch = chf.ch; + + begin(DebugDrawPrimitives.QUADS); + + for (int y = 0; y < chf.height; ++y) { + for (int x = 0; x < chf.width; ++x) { + float fx = chf.bmin[0] + x * cs; + float fz = chf.bmin[2] + y * cs; + CompactCell c = chf.cells[x + y * chf.width]; + + for (int i = c.index, ni = c.index + c.count; i < ni; ++i) { + CompactSpan s = chf.spans[i]; + float fy = chf.bmin[1] + (s.y) * ch; + int color; + if (s.reg != 0) { + color = duIntToCol(s.reg, 192); + } else { + color = duRGBA(0, 0, 0, 64); + } + + vertex(fx, fy, fz, color); + vertex(fx, fy, fz + cs, color); + vertex(fx + cs, fy, fz + cs, color); + vertex(fx + cs, fy, fz, color); + } + } + } + + end(); + } + + public void debugDrawCompactHeightfieldDistance(CompactHeightfield chf) { + if (chf.dist == null) { + return; + } + + float cs = chf.cs; + float ch = chf.ch; + + float maxd = chf.maxDistance; + if (maxd < 1.0f) { + maxd = 1; + } + float dscale = 255.0f / maxd; + + begin(DebugDrawPrimitives.QUADS); + + for (int y = 0; y < chf.height; ++y) { + for (int x = 0; x < chf.width; ++x) { + float fx = chf.bmin[0] + x * cs; + float fz = chf.bmin[2] + y * cs; + CompactCell c = chf.cells[x + y * chf.width]; + + for (int i = c.index, ni = c.index + c.count; i < ni; ++i) { + CompactSpan s = chf.spans[i]; + float fy = chf.bmin[1] + (s.y + 1) * ch; + char cd = (char) (chf.dist[i] * dscale); + int color = duRGBA(cd, cd, cd, 255); + vertex(fx, fy, fz, color); + vertex(fx, fy, fz + cs, color); + vertex(fx + cs, fy, fz + cs, color); + vertex(fx + cs, fy, fz, color); + } + } + } + end(); + } + + public void debugDrawPolyMesh(PolyMesh mesh) { + int nvp = mesh.nvp; + float cs = mesh.cs; + float ch = mesh.ch; + float[] orig = mesh.bmin; + + begin(DebugDrawPrimitives.TRIS); + + for (int i = 0; i < mesh.npolys; ++i) { + int p = i * nvp * 2; + int area = mesh.areas[i]; + + int color; + if (area == SampleAreaModifications.SAMPLE_POLYAREA_TYPE_WALKABLE) { + color = duRGBA(0, 192, 255, 64); + } else if (area == RecastConstants.RC_NULL_AREA) { + color = duRGBA(0, 0, 0, 64); + } else { + color = areaToCol(area); + } + + int[] vi = new int[3]; + for (int j = 2; j < nvp; ++j) { + if (mesh.polys[p + j] == RecastConstants.RC_MESH_NULL_IDX) { + break; + } + vi[0] = mesh.polys[p + 0]; + vi[1] = mesh.polys[p + j - 1]; + vi[2] = mesh.polys[p + j]; + for (int k = 0; k < 3; ++k) { + int v0 = mesh.verts[vi[k] * 3]; + int v1 = mesh.verts[vi[k] * 3 + 1]; + int v2 = mesh.verts[vi[k] * 3 + 2]; + float x = orig[0] + v0 * cs; + float y = orig[1] + (v1 + 1) * ch; + float z = orig[2] + v2 * cs; + vertex(x, y, z, color); + } + } + } + end(); + + // Draw neighbours edges + int coln = duRGBA(0, 48, 64, 32); + begin(DebugDrawPrimitives.LINES, 1.5f); + for (int i = 0; i < mesh.npolys; ++i) { + int p = i * nvp * 2; + for (int j = 0; j < nvp; ++j) { + if (mesh.polys[p + j] == RecastConstants.RC_MESH_NULL_IDX) { + break; + } + if ((mesh.polys[p + nvp + j] & 0x8000) != 0) { + continue; + } + int nj = (j + 1 >= nvp || mesh.polys[p + j + 1] == RecastConstants.RC_MESH_NULL_IDX) ? 0 : j + 1; + int[] vi = { mesh.polys[p + j], mesh.polys[p + nj] }; + + for (int k = 0; k < 2; ++k) { + int v = vi[k] * 3; + float x = orig[0] + mesh.verts[v] * cs; + float y = orig[1] + (mesh.verts[v + 1] + 1) * ch + 0.1f; + float z = orig[2] + mesh.verts[v + 2] * cs; + vertex(x, y, z, coln); + } + } + } + end(); + + // Draw boundary edges + int colb = duRGBA(0, 48, 64, 220); + begin(DebugDrawPrimitives.LINES, 2.5f); + for (int i = 0; i < mesh.npolys; ++i) { + int p = i * nvp * 2; + for (int j = 0; j < nvp; ++j) { + if (mesh.polys[p + j] == RecastConstants.RC_MESH_NULL_IDX) { + break; + } + if ((mesh.polys[p + nvp + j] & 0x8000) == 0) { + continue; + } + int nj = (j + 1 >= nvp || mesh.polys[p + j + 1] == RecastConstants.RC_MESH_NULL_IDX) ? 0 : j + 1; + int[] vi = { mesh.polys[p + j], mesh.polys[p + nj] }; + + int col = colb; + if ((mesh.polys[p + nvp + j] & 0xf) != 0xf) { + col = duRGBA(255, 255, 255, 128); + } + for (int k = 0; k < 2; ++k) { + int v = vi[k] * 3; + float x = orig[0] + mesh.verts[v] * cs; + float y = orig[1] + (mesh.verts[v + 1] + 1) * ch + 0.1f; + float z = orig[2] + mesh.verts[v + 2] * cs; + vertex(x, y, z, col); + } + } + } + end(); + + begin(DebugDrawPrimitives.POINTS, 3.0f); + int colv = duRGBA(0, 0, 0, 220); + for (int i = 0; i < mesh.nverts; ++i) { + int v = i * 3; + float x = orig[0] + mesh.verts[v] * cs; + float y = orig[1] + (mesh.verts[v + 1] + 1) * ch + 0.1f; + float z = orig[2] + mesh.verts[v + 2] * cs; + vertex(x, y, z, colv); + } + end(); + } + + public void debugDrawPolyMeshDetail(PolyMeshDetail dmesh) { + + begin(DebugDrawPrimitives.TRIS); + + for (int i = 0; i < dmesh.nmeshes; ++i) { + int m = i * 4; + int bverts = dmesh.meshes[m]; + int btris = dmesh.meshes[m + 2]; + int ntris = dmesh.meshes[m + 3]; + int verts = bverts * 3; + int tris = btris * 4; + + int color = duIntToCol(i, 192); + + for (int j = 0; j < ntris; ++j) { + vertex(dmesh.verts[verts + dmesh.tris[tris + j * 4 + 0] * 3], + dmesh.verts[verts + dmesh.tris[tris + j * 4 + 0] * 3 + 1], + dmesh.verts[verts + dmesh.tris[tris + j * 4 + 0] * 3 + 2], color); + vertex(dmesh.verts[verts + dmesh.tris[tris + j * 4 + 1] * 3], + dmesh.verts[verts + dmesh.tris[tris + j * 4 + 1] * 3 + 1], + dmesh.verts[verts + dmesh.tris[tris + j * 4 + 1] * 3 + 2], color); + vertex(dmesh.verts[verts + dmesh.tris[tris + j * 4 + 2] * 3], + dmesh.verts[verts + dmesh.tris[tris + j * 4 + 2] * 3 + 1], + dmesh.verts[verts + dmesh.tris[tris + j * 4 + 2] * 3 + 2], color); + } + } + end(); + + // Internal edges. + begin(DebugDrawPrimitives.LINES, 1.0f); + int coli = duRGBA(0, 0, 0, 64); + for (int i = 0; i < dmesh.nmeshes; ++i) { + int m = i * 4; + int bverts = dmesh.meshes[m]; + int btris = dmesh.meshes[m + 2]; + int ntris = dmesh.meshes[m + 3]; + int verts = bverts * 3; + int tris = btris * 4; + + for (int j = 0; j < ntris; ++j) { + int t = tris + j * 4; + for (int k = 0, kp = 2; k < 3; kp = k++) { + int ef = (dmesh.tris[t + 3] >> (kp * 2)) & 0x3; + if (ef == 0) { + // Internal edge + if (dmesh.tris[t + kp] < dmesh.tris[t + k]) { + vertex(dmesh.verts[verts + dmesh.tris[t + kp] * 3], + dmesh.verts[verts + dmesh.tris[t + kp] * 3 + 1], + dmesh.verts[verts + dmesh.tris[t + kp] * 3 + 2], coli); + vertex(dmesh.verts[verts + dmesh.tris[t + k] * 3], + dmesh.verts[verts + dmesh.tris[t + k] * 3 + 1], + dmesh.verts[verts + dmesh.tris[t + k] * 3 + 2], coli); + } + } + } + } + } + end(); + + // External edges. + begin(DebugDrawPrimitives.LINES, 2.0f); + int cole = duRGBA(0, 0, 0, 64); + for (int i = 0; i < dmesh.nmeshes; ++i) { + int m = i * 4; + int bverts = dmesh.meshes[m]; + int btris = dmesh.meshes[m + 2]; + int ntris = dmesh.meshes[m + 3]; + int verts = bverts * 3; + int tris = btris * 4; + + for (int j = 0; j < ntris; ++j) { + int t = tris + j * 4; + for (int k = 0, kp = 2; k < 3; kp = k++) { + int ef = (dmesh.tris[t + 3] >> (kp * 2)) & 0x3; + if (ef != 0) { + // Ext edge + vertex(dmesh.verts[verts + dmesh.tris[t + kp] * 3], + dmesh.verts[verts + dmesh.tris[t + kp] * 3 + 1], + dmesh.verts[verts + dmesh.tris[t + kp] * 3 + 2], cole); + vertex(dmesh.verts[verts + dmesh.tris[t + k] * 3], + dmesh.verts[verts + dmesh.tris[t + k] * 3 + 1], + dmesh.verts[verts + dmesh.tris[t + k] * 3 + 2], cole); + } + } + } + } + end(); + + begin(DebugDrawPrimitives.POINTS, 3.0f); + int colv = duRGBA(0, 0, 0, 64); + for (int i = 0; i < dmesh.nmeshes; ++i) { + int m = i * 4; + int bverts = dmesh.meshes[m]; + int nverts = dmesh.meshes[m + 1]; + int verts = bverts * 3; + for (int j = 0; j < nverts; ++j) { + vertex(dmesh.verts[verts + j * 3], dmesh.verts[verts + j * 3 + 1], dmesh.verts[verts + j * 3 + 2], + colv); + } + } + end(); + } + + public void debugDrawNavMeshNodes(NavMeshQuery query) { + NodePool pool = query.getNodePool(); + if (pool != null) { + float off = 0.5f; + begin(DebugDrawPrimitives.POINTS, 4.0f); + + foreach (List nodes in pool.getNodeMap().Values) { + + foreach (Node node in nodes) { + if (node == null) { + continue; + } + vertex(node.pos[0], node.pos[1] + off, node.pos[2], duRGBA(255, 192, 0, 255)); + } + } + end(); + + begin(DebugDrawPrimitives.LINES, 2.0f); + foreach (List nodes in pool.getNodeMap().Values) { + + foreach (Node node in nodes) { + if (node == null) { + continue; + } + if (node.pidx == 0) { + continue; + } + Node parent = pool.getNodeAtIdx(node.pidx); + if (parent == null) { + continue; + } + vertex(node.pos[0], node.pos[1] + off, node.pos[2], duRGBA(255, 192, 0, 128)); + vertex(parent.pos[0], parent.pos[1] + off, parent.pos[2], duRGBA(255, 192, 0, 128)); + } + } + end(); + } + } + + public void debugDrawNavMeshPolysWithFlags(NavMesh mesh, int polyFlags, int col) { + + for (int i = 0; i < mesh.getMaxTiles(); ++i) { + MeshTile tile = mesh.getTile(i); + if (tile == null || tile.data == null || tile.data.header == null) { + continue; + } + long @base = mesh.getPolyRefBase(tile); + + for (int j = 0; j < tile.data.header.polyCount; ++j) { + Poly p = tile.data.polys[j]; + if ((p.flags & polyFlags) == 0) { + continue; + } + debugDrawNavMeshPoly(mesh, @base | j, col); + } + } + } + + public void debugDrawNavMeshPoly(NavMesh mesh, long refs, int col) { + if (refs == 0) { + return; + } + Result> tileAndPolyResult = mesh.getTileAndPolyByRef(refs); + if (tileAndPolyResult.failed()) { + return; + } + Tuple tileAndPoly = tileAndPolyResult.result; + MeshTile tile = tileAndPoly.Item1; + Poly poly = tileAndPoly.Item2; + + depthMask(false); + + int c = duTransCol(col, 64); + int ip = poly.index; + + if (poly.getType() == Poly.DT_POLYTYPE_OFFMESH_CONNECTION) { + OffMeshConnection con = tile.data.offMeshCons[ip - tile.data.header.offMeshBase]; + + begin(DebugDrawPrimitives.LINES, 2.0f); + + // Connection arc. + appendArc(con.pos[0], con.pos[1], con.pos[2], con.pos[3], con.pos[4], con.pos[5], 0.25f, + (con.flags & 1) != 0 ? 0.6f : 0.0f, 0.6f, c); + + end(); + } else { + begin(DebugDrawPrimitives.TRIS); + drawPoly(tile, ip, col); + end(); + } + + depthMask(true); + + } + + public void debugDrawNavMeshPortals(NavMesh mesh) { + for (int i = 0; i < mesh.getMaxTiles(); ++i) { + MeshTile tile = mesh.getTile(i); + if (tile.data != null && tile.data.header != null) { + drawMeshTilePortal(tile); + } + } + } + + private void drawMeshTilePortal(MeshTile tile) { + float padx = 0.04f; + float pady = tile.data.header.walkableClimb; + + begin(DebugDrawPrimitives.LINES, 2.0f); + + for (int side = 0; side < 8; ++side) { + int m = NavMesh.DT_EXT_LINK | (short) side; + + for (int i = 0; i < tile.data.header.polyCount; ++i) { + Poly poly = tile.data.polys[i]; + + // Create new links. + int nv = poly.vertCount; + for (int j = 0; j < nv; ++j) { + // Skip edges which do not point to the right side. + if (poly.neis[j] != m) + continue; + + // Create new links + float[] va = new float[] { tile.data.verts[poly.verts[j] * 3], + tile.data.verts[poly.verts[j] * 3 + 1], tile.data.verts[poly.verts[j] * 3 + 2] }; + float[] vb = new float[] { tile.data.verts[poly.verts[(j + 1) % nv] * 3], + tile.data.verts[poly.verts[(j + 1) % nv] * 3 + 1], + tile.data.verts[poly.verts[(j + 1) % nv] * 3 + 2] }; + + if (side == 0 || side == 4) { + int col = side == 0 ? duRGBA(128, 0, 0, 128) : duRGBA(128, 0, 128, 128); + + float x = va[0] + ((side == 0) ? -padx : padx); + + vertex(x, va[1] - pady, va[2], col); + vertex(x, va[1] + pady, va[2], col); + + vertex(x, va[1] + pady, va[2], col); + vertex(x, vb[1] + pady, vb[2], col); + + vertex(x, vb[1] + pady, vb[2], col); + vertex(x, vb[1] - pady, vb[2], col); + + vertex(x, vb[1] - pady, vb[2], col); + vertex(x, va[1] - pady, va[2], col); + } else if (side == 2 || side == 6) { + int col = side == 2 ? duRGBA(0, 128, 0, 128) : duRGBA(0, 128, 128, 128); + + float z = va[2] + ((side == 2) ? -padx : padx); + + vertex(va[0], va[1] - pady, z, col); + vertex(va[0], va[1] + pady, z, col); + + vertex(va[0], va[1] + pady, z, col); + vertex(vb[0], vb[1] + pady, z, col); + + vertex(vb[0], vb[1] + pady, z, col); + vertex(vb[0], vb[1] - pady, z, col); + + vertex(vb[0], vb[1] - pady, z, col); + vertex(va[0], va[1] - pady, z, col); + } + + } + } + } + + end(); + + } + +} diff --git a/src/DotRecast.Recast.Demo/Geom/ChunkyTriMesh.cs b/src/DotRecast.Recast.Demo/Geom/ChunkyTriMesh.cs new file mode 100644 index 0000000..a86c9e8 --- /dev/null +++ b/src/DotRecast.Recast.Demo/Geom/ChunkyTriMesh.cs @@ -0,0 +1,263 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.Collections.Generic; +using DotRecast.Recast.Geom; + +namespace DotRecast.Recast.Demo.Geom; + +public class ChunkyTriMesh { + + private class BoundsItem { + public readonly float[] bmin = new float[2]; + public readonly float[] bmax = new float[2]; + public int i; + } + + private class CompareItemX : IComparer { + public int Compare(BoundsItem a, BoundsItem b) { + return a.bmin[0].CompareTo(b.bmin[0]); + } + } + + private class CompareItemY : IComparer { + public int Compare(BoundsItem a, BoundsItem b) { + return a.bmin[1].CompareTo(b.bmin[1]); + } + } + + List nodes; + int ntris; + int maxTrisPerChunk; + + private void calcExtends(BoundsItem[] items, int imin, int imax, float[] bmin, float[] bmax) { + bmin[0] = items[imin].bmin[0]; + bmin[1] = items[imin].bmin[1]; + + bmax[0] = items[imin].bmax[0]; + bmax[1] = items[imin].bmax[1]; + + for (int i = imin + 1; i < imax; ++i) { + BoundsItem it = items[i]; + if (it.bmin[0] < bmin[0]) { + bmin[0] = it.bmin[0]; + } + if (it.bmin[1] < bmin[1]) { + bmin[1] = it.bmin[1]; + } + + if (it.bmax[0] > bmax[0]) { + bmax[0] = it.bmax[0]; + } + if (it.bmax[1] > bmax[1]) { + bmax[1] = it.bmax[1]; + } + } + } + + private int longestAxis(float x, float y) { + return y > x ? 1 : 0; + } + + private void subdivide(BoundsItem[] items, int imin, int imax, int trisPerChunk, List nodes, + int[] inTris) { + int inum = imax - imin; + + ChunkyTriMeshNode node = new ChunkyTriMeshNode(); + nodes.Add(node); + + if (inum <= trisPerChunk) { + // Leaf + calcExtends(items, imin, imax, node.bmin, node.bmax); + + // Copy triangles. + node.i = nodes.Count; + node.tris = new int[inum * 3]; + + int dst = 0; + for (int i = imin; i < imax; ++i) { + int src = items[i].i * 3; + node.tris[dst++] = inTris[src]; + node.tris[dst++] = inTris[src + 1]; + node.tris[dst++] = inTris[src + 2]; + } + } else { + // Split + calcExtends(items, imin, imax, node.bmin, node.bmax); + + int axis = longestAxis(node.bmax[0] - node.bmin[0], node.bmax[1] - node.bmin[1]); + + if (axis == 0) { + Array.Sort(items, imin, imax - imax, new CompareItemX()); + // Sort along x-axis + } else if (axis == 1) { + Array.Sort(items, imin, imax - imin, new CompareItemY()); + // Sort along y-axis + } + + int isplit = imin + inum / 2; + + // Left + subdivide(items, imin, isplit, trisPerChunk, nodes, inTris); + // Right + subdivide(items, isplit, imax, trisPerChunk, nodes, inTris); + + // Negative index means escape. + node.i = -nodes.Count; + } + } + + public ChunkyTriMesh(float[] verts, int[] tris, int ntris, int trisPerChunk) { + int nchunks = (ntris + trisPerChunk - 1) / trisPerChunk; + + nodes = new(nchunks); + this.ntris = ntris; + + // Build tree + BoundsItem[] items = new BoundsItem[ntris]; + + for (int i = 0; i < ntris; i++) { + int t = i * 3; + BoundsItem it = items[i] = new BoundsItem(); + it.i = i; + // Calc triangle XZ bounds. + it.bmin[0] = it.bmax[0] = verts[tris[t] * 3 + 0]; + it.bmin[1] = it.bmax[1] = verts[tris[t] * 3 + 2]; + for (int j = 1; j < 3; ++j) { + int v = tris[t + j] * 3; + if (verts[v] < it.bmin[0]) { + it.bmin[0] = verts[v]; + } + if (verts[v + 2] < it.bmin[1]) { + it.bmin[1] = verts[v + 2]; + } + + if (verts[v] > it.bmax[0]) { + it.bmax[0] = verts[v]; + } + if (verts[v + 2] > it.bmax[1]) { + it.bmax[1] = verts[v + 2]; + } + } + } + + subdivide(items, 0, ntris, trisPerChunk, nodes, tris); + + // Calc max tris per node. + maxTrisPerChunk = 0; + foreach (ChunkyTriMeshNode node in nodes) { + bool isLeaf = node.i >= 0; + if (!isLeaf) { + continue; + } + if (node.tris.Length / 3 > maxTrisPerChunk) { + maxTrisPerChunk = node.tris.Length / 3; + } + } + + } + + public List getChunksOverlappingRect(float[] bmin, float[] bmax) { + // Traverse tree + List ids = new(); + int i = 0; + while (i < nodes.Count) { + ChunkyTriMeshNode node = nodes[i]; + bool overlap = checkOverlapRect(bmin, bmax, node.bmin, node.bmax); + bool isLeafNode = node.i >= 0; + + if (isLeafNode && overlap) { + ids.Add(node); + } + + if (overlap || isLeafNode) { + i++; + } else { + i = -node.i; + } + } + return ids; + } + + private bool checkOverlapRect(float[] amin, float[] amax, float[] bmin, float[] bmax) { + bool overlap = true; + overlap = (amin[0] > bmax[0] || amax[0] < bmin[0]) ? false : overlap; + overlap = (amin[1] > bmax[1] || amax[1] < bmin[1]) ? false : overlap; + return overlap; + } + + public List getChunksOverlappingSegment(float[] p, float[] q) { + // Traverse tree + List ids = new(); + int i = 0; + while (i < nodes.Count) { + ChunkyTriMeshNode node = nodes[i]; + bool overlap = checkOverlapSegment(p, q, node.bmin, node.bmax); + bool isLeafNode = node.i >= 0; + + if (isLeafNode && overlap) { + ids.Add(node); + } + + if (overlap || isLeafNode) { + i++; + } else { + i = -node.i; + } + } + return ids; + } + + private bool checkOverlapSegment(float[] p, float[] q, float[] bmin, float[] bmax) { + float EPSILON = 1e-6f; + + float tmin = 0; + float tmax = 1; + float[] d = new float[2]; + d[0] = q[0] - p[0]; + d[1] = q[1] - p[1]; + + for (int i = 0; i < 2; i++) { + if (Math.Abs(d[i]) < EPSILON) { + // Ray is parallel to slab. No hit if origin not within slab + if (p[i] < bmin[i] || p[i] > bmax[i]) + return false; + } else { + // Compute intersection t value of ray with near and far plane of slab + float ood = 1.0f / d[i]; + float t1 = (bmin[i] - p[i]) * ood; + float t2 = (bmax[i] - p[i]) * ood; + if (t1 > t2) { + float tmp = t1; + t1 = t2; + t2 = tmp; + } + if (t1 > tmin) + tmin = t1; + if (t2 < tmax) + tmax = t2; + if (tmin > tmax) + return false; + } + } + return true; + } + +} diff --git a/src/DotRecast.Recast.Demo/Geom/DemoInputGeomProvider.cs b/src/DotRecast.Recast.Demo/Geom/DemoInputGeomProvider.cs new file mode 100644 index 0000000..ec61aee --- /dev/null +++ b/src/DotRecast.Recast.Demo/Geom/DemoInputGeomProvider.cs @@ -0,0 +1,185 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using DotRecast.Recast.Geom; + +namespace DotRecast.Recast.Demo.Geom; + +public class DemoInputGeomProvider : InputGeomProvider { + + public readonly float[] vertices; + public readonly int[] faces; + public readonly float[] normals; + private readonly float[] bmin; + private readonly float[] bmax; + private readonly List _convexVolumes = new(); + private readonly List offMeshConnections = new(); + private readonly ChunkyTriMesh chunkyTriMesh; + + public DemoInputGeomProvider(List vertexPositions, List meshFaces) : + this(mapVertices(vertexPositions), mapFaces(meshFaces)) { + } + + private static int[] mapFaces(List meshFaces) { + int[] faces = new int[meshFaces.Count]; + for (int i = 0; i < faces.Length; i++) { + faces[i] = meshFaces[i]; + } + return faces; + } + + private static float[] mapVertices(List vertexPositions) { + float[] vertices = new float[vertexPositions.Count]; + for (int i = 0; i < vertices.Length; i++) { + vertices[i] = vertexPositions[i]; + } + return vertices; + } + + public DemoInputGeomProvider(float[] vertices, int[] faces) { + this.vertices = vertices; + this.faces = faces; + normals = new float[faces.Length]; + calculateNormals(); + bmin = new float[3]; + bmax = new float[3]; + RecastVectors.copy(bmin, vertices, 0); + RecastVectors.copy(bmax, vertices, 0); + for (int i = 1; i < vertices.Length / 3; i++) { + RecastVectors.min(bmin, vertices, i * 3); + RecastVectors.max(bmax, vertices, i * 3); + } + chunkyTriMesh = new ChunkyTriMesh(vertices, faces, faces.Length / 3, 256); + } + + public float[] getMeshBoundsMin() { + return bmin; + } + + public float[] getMeshBoundsMax() { + return bmax; + } + + public void calculateNormals() { + for (int i = 0; i < faces.Length; i += 3) { + int v0 = faces[i] * 3; + int v1 = faces[i + 1] * 3; + int v2 = faces[i + 2] * 3; + float[] e0 = new float[3], e1 = new float[3]; + for (int j = 0; j < 3; ++j) { + e0[j] = vertices[v1 + j] - vertices[v0 + j]; + e1[j] = vertices[v2 + j] - vertices[v0 + j]; + } + normals[i] = e0[1] * e1[2] - e0[2] * e1[1]; + normals[i + 1] = e0[2] * e1[0] - e0[0] * e1[2]; + normals[i + 2] = e0[0] * e1[1] - e0[1] * e1[0]; + float d = (float) Math.Sqrt(normals[i] * normals[i] + normals[i + 1] * normals[i + 1] + normals[i + 2] * normals[i + 2]); + if (d > 0) { + d = 1.0f / d; + normals[i] *= d; + normals[i + 1] *= d; + normals[i + 2] *= d; + } + } + } + + public IList convexVolumes() { + return _convexVolumes; + } + + public IEnumerable meshes() { + return ImmutableArray.Create(new TriMesh(vertices, faces)); + } + + public List getOffMeshConnections() { + return offMeshConnections; + } + + public void addOffMeshConnection(float[] start, float[] end, float radius, bool bidir, int area, int flags) { + offMeshConnections.Add(new DemoOffMeshConnection(start, end, radius, bidir, area, flags)); + } + + public void removeOffMeshConnections(Predicate filter) { + //offMeshConnections.retainAll(offMeshConnections.stream().filter(c -> !filter.test(c)).collect(toList())); + offMeshConnections.RemoveAll(filter); // TODO : 확인 필요 + } + + public float? raycastMesh(float[] src, float[] dst) { + + // Prune hit ray. + float[] btminmax = Intersections.intersectSegmentAABB(src, dst, bmin, bmax); + if (null == btminmax) { + return null; + } + float btmin = btminmax[0]; + float btmax = btminmax[1]; + float[] p = new float[2], q = new float[2]; + p[0] = src[0] + (dst[0] - src[0]) * btmin; + p[1] = src[2] + (dst[2] - src[2]) * btmin; + q[0] = src[0] + (dst[0] - src[0]) * btmax; + q[1] = src[2] + (dst[2] - src[2]) * btmax; + + List chunks = chunkyTriMesh.getChunksOverlappingSegment(p, q); + if (0 == chunks.Count) { + return null; + } + + float tmin = 1.0f; + bool hit = false; + foreach (ChunkyTriMeshNode chunk in chunks) { + int[] tris = chunk.tris; + for (int j = 0; j < chunk.tris.Length; j += 3) { + float[] v1 = new float[] { vertices[tris[j] * 3], vertices[tris[j] * 3 + 1], + vertices[tris[j] * 3 + 2] }; + float[] v2 = new float[] { vertices[tris[j + 1] * 3], vertices[tris[j + 1] * 3 + 1], + vertices[tris[j + 1] * 3 + 2] }; + float[] v3 = new float[] { vertices[tris[j + 2] * 3], vertices[tris[j + 2] * 3 + 1], + vertices[tris[j + 2] * 3 + 2] }; + float? t = Intersections.intersectSegmentTriangle(src, dst, v1, v2, v3); + if (null != t) { + if (t.Value < tmin) { + tmin = t.Value; + } + hit = true; + } + } + } + + return hit ? tmin : null; + } + + + public void addConvexVolume(float[] verts, float minh, float maxh, AreaModification areaMod) { + ConvexVolume volume = new ConvexVolume(); + volume.verts = verts; + volume.hmin = minh; + volume.hmax = maxh; + volume.areaMod = areaMod; + _convexVolumes.Add(volume); + } + + public void clearConvexVolumes() { + _convexVolumes.Clear(); + } + +} diff --git a/src/DotRecast.Recast.Demo/Geom/DemoOffMeshConnection.cs b/src/DotRecast.Recast.Demo/Geom/DemoOffMeshConnection.cs new file mode 100644 index 0000000..138c759 --- /dev/null +++ b/src/DotRecast.Recast.Demo/Geom/DemoOffMeshConnection.cs @@ -0,0 +1,43 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Recast.Demo.Geom; + +public class DemoOffMeshConnection { + + public readonly float[] verts; + public readonly float radius; + public readonly bool bidir; + public readonly int area; + public readonly int flags; + + public DemoOffMeshConnection(float[] start, float[] end, float radius, bool bidir, int area, int flags) { + verts = new float[6]; + verts[0] = start[0]; + verts[1] = start[1]; + verts[2] = start[2]; + verts[3] = end[0]; + verts[4] = end[1]; + verts[5] = end[2]; + this.radius = radius; + this.bidir = bidir; + this.area = area; + this.flags = flags; + } + +} diff --git a/src/DotRecast.Recast.Demo/Geom/Intersections.cs b/src/DotRecast.Recast.Demo/Geom/Intersections.cs new file mode 100644 index 0000000..2df0de4 --- /dev/null +++ b/src/DotRecast.Recast.Demo/Geom/Intersections.cs @@ -0,0 +1,113 @@ +/* +recast4j copyright (c) 2021 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using DotRecast.Core; +using static DotRecast.Detour.DetourCommon; + +namespace DotRecast.Recast.Demo.Geom; + +public class Intersections { + + public static float? intersectSegmentTriangle(float[] sp, float[] sq, float[] a, float[] b, float[] c) { + float v, w; + float[] ab = vSub(b, a); + float[] ac = vSub(c, a); + float[] qp = vSub(sp, sq); + + // Compute triangle normal. Can be precalculated or cached if + // intersecting multiple segments against the same triangle + float[] norm = DemoMath.vCross(ab, ac); + + // Compute denominator d. If d <= 0, segment is parallel to or points + // away from triangle, so exit early + float d = DemoMath.vDot(qp, norm); + if (d <= 0.0f) { + return null; + } + + // Compute intersection t value of pq with plane of triangle. A ray + // intersects iff 0 <= t. Segment intersects iff 0 <= t <= 1. Delay + // dividing by d until intersection has been found to pierce triangle + float[] ap = vSub(sp, a); + float t = DemoMath.vDot(ap, norm); + if (t < 0.0f) { + return null; + } + if (t > d) { + return null; // For segment; exclude this code line for a ray test + } + + // Compute barycentric coordinate components and test if within bounds + float[] e = DemoMath.vCross(qp, ap); + v = DemoMath.vDot(ac, e); + if (v < 0.0f || v > d) { + return null; + } + w = -DemoMath.vDot(ab, e); + if (w < 0.0f || v + w > d) { + return null; + } + + // Segment/ray intersects triangle. Perform delayed division + t /= d; + + return t; + } + + public static float[] intersectSegmentAABB(float[] sp, float[] sq, float[] amin, float[] amax) { + + float EPS = 1e-6f; + + float[] d = new float[3]; + d[0] = sq[0] - sp[0]; + d[1] = sq[1] - sp[1]; + d[2] = sq[2] - sp[2]; + float tmin = 0.0f; + float tmax = 1.0f; + + for (int i = 0; i < 3; i++) { + if (Math.Abs(d[i]) < EPS) { + if (sp[i] < amin[i] || sp[i] > amax[i]) { + return null; + } + } else { + float ood = 1.0f / d[i]; + float t1 = (amin[i] - sp[i]) * ood; + float t2 = (amax[i] - sp[i]) * ood; + if (t1 > t2) { + float tmp = t1; + t1 = t2; + t2 = tmp; + } + if (t1 > tmin) { + tmin = t1; + } + if (t2 < tmax) { + tmax = t2; + } + if (tmin > tmax) { + return null; + } + } + } + + return new float[] { tmin, tmax }; + } + +} diff --git a/src/DotRecast.Recast.Demo/Geom/NavMeshRaycast.cs b/src/DotRecast.Recast.Demo/Geom/NavMeshRaycast.cs new file mode 100644 index 0000000..f9fd26f --- /dev/null +++ b/src/DotRecast.Recast.Demo/Geom/NavMeshRaycast.cs @@ -0,0 +1,79 @@ +/* +recast4j copyright (c) 2021 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using DotRecast.Core; +using DotRecast.Detour; + +namespace DotRecast.Recast.Demo.Geom; + +/** + * Simple helper to find an intersection between a ray and a nav mesh + */ +public class NavMeshRaycast { + + public static float? raycast(NavMesh mesh, float[] src, float[]dst) { + for (int t = 0; t < mesh.getMaxTiles(); ++t) { + MeshTile tile = mesh.getTile(t); + if (tile != null && tile.data != null) { + float? intersection = raycast(tile, src, dst); + if (null != intersection) { + return intersection; + } + } + } + + return null; + } + + private static float? raycast(MeshTile tile, float[] sp, float[]sq) { + for (int i = 0; i < tile.data.header.polyCount; ++i) { + Poly p = tile.data.polys[i]; + if (p.getType() == Poly.DT_POLYTYPE_OFFMESH_CONNECTION) { + continue; + } + PolyDetail pd = tile.data.detailMeshes[i]; + + if (pd != null) { + float[][] verts = ArrayUtils.Of(3, 3); + for (int j = 0; j < pd.triCount; ++j) { + int t = (pd.triBase + j) * 4; + for (int k = 0; k < 3; ++k) { + int v = tile.data.detailTris[t + k]; + if (v < p.vertCount) { + verts[k][0] = tile.data.verts[p.verts[v] * 3]; + verts[k][1] = tile.data.verts[p.verts[v] * 3 + 1]; + verts[k][2] = tile.data.verts[p.verts[v] * 3 + 2]; + } else { + verts[k][0] = tile.data.detailVerts[(pd.vertBase + v - p.vertCount) * 3]; + verts[k][1] = tile.data.detailVerts[(pd.vertBase + v - p.vertCount) * 3 + 1]; + verts[k][2] = tile.data.detailVerts[(pd.vertBase + v - p.vertCount) * 3 + 2]; + } + } + float? intersection = Intersections.intersectSegmentTriangle(sp, sq, verts[0], verts[1], verts[2]); + if (null != intersection) { + return intersection; + } + } + } else { + // FIXME: Use Poly if PolyDetail is unavailable + } + } + + return null; + } +} diff --git a/src/DotRecast.Recast.Demo/Geom/NavMeshUtils.cs b/src/DotRecast.Recast.Demo/Geom/NavMeshUtils.cs new file mode 100644 index 0000000..e02dc99 --- /dev/null +++ b/src/DotRecast.Recast.Demo/Geom/NavMeshUtils.cs @@ -0,0 +1,44 @@ +/* +recast4j copyright (c) 2021 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using DotRecast.Detour; + +namespace DotRecast.Recast.Demo.Geom; + +public class NavMeshUtils { + + public static float[][] getNavMeshBounds(NavMesh mesh) { + float[] bmin = new float[] { float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity }; + float[] bmax = new float[] { float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity }; + for (int t = 0; t < mesh.getMaxTiles(); ++t) { + MeshTile tile = mesh.getTile(t); + if (tile != null && tile.data != null) { + for (int i = 0; i < tile.data.verts.Length; i += 3) { + bmin[0] = Math.Min(bmin[0], tile.data.verts[i]); + bmin[1] = Math.Min(bmin[1], tile.data.verts[i + 1]); + bmin[2] = Math.Min(bmin[2], tile.data.verts[i + 2]); + bmax[0] = Math.Max(bmax[0], tile.data.verts[i]); + bmax[1] = Math.Max(bmax[1], tile.data.verts[i + 1]); + bmax[2] = Math.Max(bmax[2], tile.data.verts[i + 2]); + } + } + } + return new float[][] { bmin, bmax }; + } +} diff --git a/src/DotRecast.Recast.Demo/Geom/PolyMeshRaycast.cs b/src/DotRecast.Recast.Demo/Geom/PolyMeshRaycast.cs new file mode 100644 index 0000000..1c9deb1 --- /dev/null +++ b/src/DotRecast.Recast.Demo/Geom/PolyMeshRaycast.cs @@ -0,0 +1,67 @@ +/* +recast4j copyright (c) 2021 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.Collections.Generic; +using DotRecast.Core; + +namespace DotRecast.Recast.Demo.Geom; + +public class PolyMeshRaycast { + + public static float? raycast(IList results, float[] src, float[] dst) { + foreach (RecastBuilderResult result in results) { + if (result.getMeshDetail() != null) { + float? intersection = raycast(result.getMesh(), result.getMeshDetail(), src, dst); + if (null != intersection) { + return intersection; + } + } + } + + return null; + } + + private static float? raycast(PolyMesh poly, PolyMeshDetail meshDetail, float[] sp, float[] sq) { + if (meshDetail != null) { + for (int i = 0; i < meshDetail.nmeshes; ++i) { + int m = i * 4; + int bverts = meshDetail.meshes[m]; + int btris = meshDetail.meshes[m + 2]; + int ntris = meshDetail.meshes[m + 3]; + int verts = bverts * 3; + int tris = btris * 4; + for (int j = 0; j < ntris; ++j) { + float[][] vs = ArrayUtils.Of(3, 3); + for (int k = 0; k < 3; ++k) { + vs[k][0] = meshDetail.verts[verts + meshDetail.tris[tris + j * 4 + k] * 3]; + vs[k][1] = meshDetail.verts[verts + meshDetail.tris[tris + j * 4 + k] * 3 + 1]; + vs[k][2] = meshDetail.verts[verts + meshDetail.tris[tris + j * 4 + k] * 3 + 2]; + } + float? intersection = Intersections.intersectSegmentTriangle(sp, sq, vs[0], vs[1], vs[2]); + if (null != intersection) { + return intersection; + } + } + } + } else { + // TODO: check PolyMesh instead + } + + return null; + } +} diff --git a/src/DotRecast.Recast.Demo/RecastDemo.cs b/src/DotRecast.Recast.Demo/RecastDemo.cs new file mode 100644 index 0000000..4eff934 --- /dev/null +++ b/src/DotRecast.Recast.Demo/RecastDemo.cs @@ -0,0 +1,687 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Linq; +using Serilog; +using Silk.NET.GLFW; +using Silk.NET.Input; +using Silk.NET.Maths; +using Silk.NET.OpenGL; +using Silk.NET.OpenGL.Extensions.ImGui; +using Silk.NET.Windowing; +using DotRecast.Core; +using DotRecast.Detour; +using DotRecast.Detour.Extras.Unity.Astar; +using DotRecast.Detour.Io; +using DotRecast.Recast.Demo.Builder; +using DotRecast.Recast.Demo.Draw; +using DotRecast.Recast.Demo.Geom; +using DotRecast.Recast.Demo.Settings; +using DotRecast.Recast.Demo.Tools; +using DotRecast.Recast.Demo.UI; +using ImGuiNET; +using Silk.NET.SDL; +using Silk.NET.Windowing.Sdl; +using static DotRecast.Detour.DetourCommon; +using Window = Silk.NET.Windowing.Window; + +namespace DotRecast.Recast.Demo; + +public class RecastDemo : MouseListener +{ + private readonly ILogger logger = Log.ForContext(); + private NuklearUI nuklearUI; + private IWindow window; + private IInputContext _input; + private ImGuiController _imgui; + private GL _gl; + private int width = 1000; + private int height = 900; + private readonly string title = "DotRecast Demo"; + //private readonly RecastDebugDraw dd; + private readonly NavMeshRenderer renderer; + private bool building = false; + private float timeAcc = 0; + private float camr = 1000; + + private readonly SoloNavMeshBuilder soloNavMeshBuilder = new SoloNavMeshBuilder(); + private readonly TileNavMeshBuilder tileNavMeshBuilder = new TileNavMeshBuilder(); + + //private Sample sample; + + private bool processHitTest = false; + private bool processHitTestShift; + private int modState; + + private readonly float[] mousePos = new float[2]; + + private bool mouseOverMenu; + private bool pan; + private bool movedDuringPan; + private bool rotate; + private bool movedDuringRotate; + private float scrollZoom; + private readonly float[] origMousePos = new float[2]; + private readonly float[] origCameraEulers = new float[2]; + private readonly float[] origCameraPos = new float[3]; + + private readonly float[] cameraEulers = { 45, -45 }; + private readonly float[] cameraPos = { 0, 0, 0 }; + + private readonly float[] rayStart = new float[3]; + private readonly float[] rayEnd = new float[3]; + + private bool markerPositionSet; + private readonly float[] markerPosition = new float[3]; + private ToolsUI toolsUI; + private SettingsUI settingsUI; + private long prevFrameTime; + + public RecastDemo() + { + // dd = new RecastDebugDraw(); + // renderer = new NavMeshRenderer(dd); + } + + public void start() + { + window = CreateWindow(); + window.Run(); + } + + private Mouse createMouse(IInputContext input) + { + Mouse mouse = new Mouse(input); + mouse.addListener(this); + return mouse; + } + + public void scroll(double xoffset, double yoffset) + { + if (yoffset < 0) + { + // wheel down + if (!mouseOverMenu) + { + scrollZoom += 1.0f; + } + } + else + { + if (!mouseOverMenu) + { + scrollZoom -= 1.0f; + } + } + + // float[] modelviewMatrix = dd.viewMatrix(cameraPos, cameraEulers); + // cameraPos[0] += scrollZoom * 2.0f * modelviewMatrix[2]; + // cameraPos[1] += scrollZoom * 2.0f * modelviewMatrix[6]; + // cameraPos[2] += scrollZoom * 2.0f * modelviewMatrix[10]; + scrollZoom = 0; + } + + public void position(double x, double y) + { + mousePos[0] = (float)x; + mousePos[1] = (float)y; + int dx = (int)(mousePos[0] - origMousePos[0]); + int dy = (int)(mousePos[1] - origMousePos[1]); + if (rotate) + { + cameraEulers[0] = origCameraEulers[0] + dy * 0.25f; + cameraEulers[1] = origCameraEulers[1] + dx * 0.25f; + if (dx * dx + dy * dy > 3 * 3) + { + movedDuringRotate = true; + } + } + + // if (pan) + // { + // float[] modelviewMatrix = dd.viewMatrix(cameraPos, cameraEulers); + // cameraPos[0] = origCameraPos[0]; + // cameraPos[1] = origCameraPos[1]; + // cameraPos[2] = origCameraPos[2]; + // + // cameraPos[0] -= 0.1f * dx * modelviewMatrix[0]; + // cameraPos[1] -= 0.1f * dx * modelviewMatrix[4]; + // cameraPos[2] -= 0.1f * dx * modelviewMatrix[8]; + // + // cameraPos[0] += 0.1f * dy * modelviewMatrix[1]; + // cameraPos[1] += 0.1f * dy * modelviewMatrix[5]; + // cameraPos[2] += 0.1f * dy * modelviewMatrix[9]; + // if (dx * dx + dy * dy > 3 * 3) + // { + // movedDuringPan = true; + // } + // } + } + + public void button(int button, int mods, bool down) + { + modState = mods; + if (down) + { + if (button == 1) + { + if (!mouseOverMenu) + { + // Rotate view + rotate = true; + movedDuringRotate = false; + origMousePos[0] = mousePos[0]; + origMousePos[1] = mousePos[1]; + origCameraEulers[0] = cameraEulers[0]; + origCameraEulers[1] = cameraEulers[1]; + } + } + else if (button == 2) + { + if (!mouseOverMenu) + { + // Pan view + pan = true; + movedDuringPan = false; + origMousePos[0] = mousePos[0]; + origMousePos[1] = mousePos[1]; + origCameraPos[0] = cameraPos[0]; + origCameraPos[1] = cameraPos[1]; + origCameraPos[2] = cameraPos[2]; + } + } + } + else + { + // Handle mouse clicks here. + if (button == 1) + { + rotate = false; + if (!mouseOverMenu) + { + if (!movedDuringRotate) + { + processHitTest = true; + processHitTestShift = true; + } + } + } + else if (button == 0) + { + if (!mouseOverMenu) + { + processHitTest = true; + //processHitTestShift = (mods & Keys.GLFW_MOD_SHIFT) != 0 ? true : false; + //processHitTestShift = (mods & Keys.) != 0 ? true : false; + } + } + else if (button == 2) + { + pan = false; + } + } + } + + private IWindow CreateWindow() + { + SdlWindowing.Use(); + //var glfw = GlfwProvider.GLFW.Value; + + // glfw.SetErrorCallback(ErrorCallback); + // if (!glfw.Init()) + // { + // throw new InvalidOperationException("Unable to initialize GLFW"); + // } + + // glfw.DefaultWindowHints(); + // glfw.WindowHint(WindowHintBool.Visible, false); + // glfw.WindowHint(WindowHintBool.Resizable, false); + // glfw.WindowHint(WindowHintBool.SrgbCapable, true); + // glfw.WindowHint(WindowHintInt.RedBits, 8); + // glfw.WindowHint(WindowHintInt.GreenBits, 8); + // glfw.WindowHint(WindowHintInt.BlueBits, 8); + // glfw.WindowHint(WindowHintInt.Samples, 4); + // glfw.WindowHint(WindowHintBool.DoubleBuffer, true); + // glfw.WindowHint(WindowHintInt.DepthBits, 24); + // + // glfw.WindowHint(WindowHintInt.ContextVersionMajor, 3); + // glfw.WindowHint(WindowHintInt.ContextVersionMinor, 2); + // glfw.WindowHint(WindowHintBool.OpenGLForwardCompat, true); + // glfw.WindowHint(WindowHintOpenGlProfile.OpenGlProfile, OpenGlProfile.Core); + // glfw.WindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + // glfw.WindowHint(GLFW_CONTEXT_VERSION_MINOR, 1); + + var monitor = Window.Platforms.First().GetMainMonitor(); + // // if (monitors.limit() > 1) { + // // monitor = monitors[1]; + // // } + var resolution = monitor.VideoMode.Resolution.Value; + + float aspect = 16.0f / 9.0f; + width = Math.Min(resolution.X, (int)(resolution.Y * aspect)) - 100; + height = resolution.Y - 100; + + // glfwWindowHint(GLFW_RED_BITS, mode.redBits()); + // glfwWindowHint(GLFW_GREEN_BITS, mode.greenBits()); + // glfwWindowHint(GLFW_BLUE_BITS, mode.blueBits()); + // glfwWindowHint(GLFW_REFRESH_RATE, mode.refreshRate()); + + var options = WindowOptions.Default; + options.Title = title; + options.Size = new Vector2D(width, height); + options.Position = new Vector2D((resolution.X - width) / 2, (resolution.Y - height) / 2); + window = Window.Create(options); + + if (window == null) + { + throw new Exception("Failed to create the GLFW window"); + } + + window.Load += OnWindowOnLoad; + window.Update += OnWindowOnUpdate; + window.Render += OnWindowOnRender; + + + // // -- move somewhere else: + // glfw.SetWindowPos(window, (mode->Width - width) / 2, (mode->Height - height) / 2); + // // glfwSetWindowMonitor(window.getWindow(), monitor, 0, 0, mode.width(), mode.height(), mode.refreshRate()); + // glfw.ShowWindow(window); + // glfw.MakeContextCurrent(window); + //} + + //glfw.SwapInterval(1); + + return window; + } + + private DemoInputGeomProvider loadInputMesh(byte[] stream) + { + DemoInputGeomProvider geom = DemoObjImporter.load(stream); + //sample = new Sample(geom, ImmutableArray.Empty, null, settingsUI, dd); + toolsUI.setEnabled(true); + return geom; + } + + private void loadNavMesh(FileStream file, string filename) + { + NavMesh mesh = null; + if (filename.EndsWith(".zip") || filename.EndsWith(".bytes")) + { + UnityAStarPathfindingImporter importer = new UnityAStarPathfindingImporter(); + mesh = importer.load(file)[0]; + } + else if (filename.EndsWith(".bin") || filename.EndsWith(".navmesh")) + { + MeshSetReader reader = new MeshSetReader(); + using (var fis = new BinaryReader(file)) + { + mesh = reader.read(fis, 6); + } + } + + if (mesh != null) + { + //sample = new Sample(null, ImmutableArray.Empty, mesh, settingsUI, dd); + toolsUI.setEnabled(true); + } + } + + private void OnWindowOnLoad() + { + _input = window.CreateInput(); + _gl = window.CreateOpenGL(); + _imgui = new ImGuiController(_gl, window, _input); + + //dd.init(_gl, camr); + + // // if (capabilities.OpenGL43) { + // // GL43.glDebugMessageControl(GL43.GL_DEBUG_SOURCE_API, GL43.GL_DEBUG_TYPE_OTHER, + // // GL43.GL_DEBUG_SEVERITY_NOTIFICATION, + // // (int[]) null, false); + // // } else if (capabilities.GL_ARB_debug_output) { + // // ARBDebugOutput.glDebugMessageControlARB(ARBDebugOutput.GL_DEBUG_SOURCE_API_ARB, + // // ARBDebugOutput.GL_DEBUG_TYPE_OTHER_ARB, ARBDebugOutput.GL_DEBUG_SEVERITY_LOW_ARB, (int[]) null, false); + // // } + var vendor = _gl.GetStringS(GLEnum.Vendor); + logger.Debug(vendor); + + var version = _gl.GetStringS(GLEnum.Version); + logger.Debug(version); + + var renderGl = _gl.GetStringS(GLEnum.Renderer); + logger.Debug(renderGl); + + var glslString = _gl.GetStringS(GLEnum.ShadingLanguageVersion); + logger.Debug(glslString); + + window.CreateInput(); + + + settingsUI = new SettingsUI(); + toolsUI = new ToolsUI( + new TestNavmeshTool(), + new OffMeshConnectionTool(), + new ConvexVolumeTool(), + new CrowdTool(), + new JumpLinkBuilderTool(), + new DynamicUpdateTool()); + + nuklearUI = new NuklearUI(window, _input, settingsUI, toolsUI); + + DemoInputGeomProvider geom = loadInputMesh(Loader.ToBytes("nav_test.obj")); + //sample = new Sample(geom, ImmutableArray.Empty, null, settingsUI, dd); + } + + private void OnWindowOnUpdate(double dt) + { + /* + * try (MemoryStack stack = stackPush()) { int[] w = stack.mallocInt(1); int[] h = + * stack.mallocInt(1); glfwGetWindowSize(win, w, h); width = w[0]; height = h[0]; } + */ + // if (sample.getInputGeom() != null) + // { + // float[] bmin = sample.getInputGeom().getMeshBoundsMin(); + // float[] bmax = sample.getInputGeom().getMeshBoundsMax(); + // int[] voxels = Recast.calcGridSize(bmin, bmax, settingsUI.getCellSize()); + // settingsUI.setVoxels(voxels); + // settingsUI.setTiles(tileNavMeshBuilder.getTiles(sample.getInputGeom(), settingsUI.getCellSize(), settingsUI.getTileSize())); + // settingsUI.setMaxTiles(tileNavMeshBuilder.getMaxTiles(sample.getInputGeom(), settingsUI.getCellSize(), settingsUI.getTileSize())); + // settingsUI.setMaxPolys(tileNavMeshBuilder.getMaxPolysPerTile(sample.getInputGeom(), settingsUI.getCellSize(), settingsUI.getTileSize())); + // } + + nuklearUI.inputBegin(); + window.DoEvents(); + nuklearUI.inputEnd(window); + + long time = Stopwatch.GetTimestamp() / 1000; + //float dt = (time - prevFrameTime) / 1000000.0f; + prevFrameTime = time; + + // Update sample simulation. + float SIM_RATE = 20; + float DELTA_TIME = 1.0f / SIM_RATE; + timeAcc = clamp((float)(timeAcc + dt), -1.0f, 1.0f); + int simIter = 0; + // while (timeAcc > DELTA_TIME) + // { + // timeAcc -= DELTA_TIME; + // if (simIter < 5 && sample != null) + // { + // toolsUI.handleUpdate(DELTA_TIME); + // } + // + // simIter++; + // } + + // Set the viewport. + // glViewport(0, 0, width, height); + int[] viewport = new int[] { 0, 0, width, height }; + // glGetIntegerv(GL_VIEWPORT, viewport); + + // Clear the screen + // dd.clear(); + // float[] projectionMatrix = dd.projectionMatrix(50f, (float)width / (float)height, 1.0f, camr); + // float[] modelviewMatrix = dd.viewMatrix(cameraPos, cameraEulers); + + //mouseOverMenu = nuklearUI.layout(window, 0, 0, width, height, (int)mousePos[0], (int)mousePos[1]); + + if (settingsUI.isMeshInputTrigerred()) + { + // aFilterPatterns.put(stack.UTF8("*.obj")); + // aFilterPatterns.flip(); + // string filename = TinyFileDialogs.tinyfd_openFileDialog("Open Mesh File", "", aFilterPatterns, + // "Mesh File (*.obj)", false); + // if (filename != null) { + // try (InputStream stream = new FileInputStream(filename)) { + // sample.update(loadInputMesh(stream), null, null); + // } catch (IOException e) { + // Console.WriteLine(e).printStackTrace(); + // } + // } + } + else if (settingsUI.isNavMeshInputTrigerred()) + { + // try (MemoryStack stack = stackPush()) { + // PointerBuffer aFilterPatterns = stack.mallocPointer(4); + // aFilterPatterns.put(stack.UTF8("*.bin")); + // aFilterPatterns.put(stack.UTF8("*.zip")); + // aFilterPatterns.put(stack.UTF8("*.bytes")); + // aFilterPatterns.put(stack.UTF8("*.navmesh")); + // aFilterPatterns.flip(); + // string filename = TinyFileDialogs.tinyfd_openFileDialog("Open Nav Mesh File", "", aFilterPatterns, + // "Nav Mesh File", false); + // if (filename != null) { + // File file = new File(filename); + // if (file.exists()) { + // try { + // loadNavMesh(file, filename); + // geom = null; + // } catch (Exception e) { + // Console.WriteLine(e); + // } + // } + // } + // } + } + // else if (settingsUI.isBuildTriggered() && sample.getInputGeom() != null) + // { + // if (!building) + // { + // float m_cellSize = settingsUI.getCellSize(); + // float m_cellHeight = settingsUI.getCellHeight(); + // float m_agentHeight = settingsUI.getAgentHeight(); + // float m_agentRadius = settingsUI.getAgentRadius(); + // float m_agentMaxClimb = settingsUI.getAgentMaxClimb(); + // float m_agentMaxSlope = settingsUI.getAgentMaxSlope(); + // int m_regionMinSize = settingsUI.getMinRegionSize(); + // int m_regionMergeSize = settingsUI.getMergedRegionSize(); + // float m_edgeMaxLen = settingsUI.getEdgeMaxLen(); + // float m_edgeMaxError = settingsUI.getEdgeMaxError(); + // int m_vertsPerPoly = settingsUI.getVertsPerPoly(); + // float m_detailSampleDist = settingsUI.getDetailSampleDist(); + // float m_detailSampleMaxError = settingsUI.getDetailSampleMaxError(); + // int m_tileSize = settingsUI.getTileSize(); + // long t = Stopwatch.GetTimestamp(); + // + // Tuple, NavMesh> buildResult; + // if (settingsUI.isTiled()) + // { + // buildResult = tileNavMeshBuilder.build(sample.getInputGeom(), settingsUI.getPartitioning(), m_cellSize, + // m_cellHeight, m_agentHeight, m_agentRadius, m_agentMaxClimb, m_agentMaxSlope, m_regionMinSize, + // m_regionMergeSize, m_edgeMaxLen, m_edgeMaxError, m_vertsPerPoly, m_detailSampleDist, + // m_detailSampleMaxError, settingsUI.isFilterLowHangingObstacles(), settingsUI.isFilterLedgeSpans(), + // settingsUI.isFilterWalkableLowHeightSpans(), m_tileSize); + // } + // else + // { + // buildResult = soloNavMeshBuilder.build(sample.getInputGeom(), settingsUI.getPartitioning(), m_cellSize, + // m_cellHeight, m_agentHeight, m_agentRadius, m_agentMaxClimb, m_agentMaxSlope, m_regionMinSize, + // m_regionMergeSize, m_edgeMaxLen, m_edgeMaxError, m_vertsPerPoly, m_detailSampleDist, + // m_detailSampleMaxError, settingsUI.isFilterLowHangingObstacles(), settingsUI.isFilterLedgeSpans(), + // settingsUI.isFilterWalkableLowHeightSpans()); + // } + // + // sample.update(sample.getInputGeom(), buildResult.Item1, buildResult.Item2); + // sample.setChanged(false); + // settingsUI.setBuildTime((Stopwatch.GetTimestamp() - t) / 1_000_000); + // toolsUI.setSample(sample); + // } + // } + else + { + building = false; + } + + if (!mouseOverMenu) + { + // GLU.glhUnProjectf(mousePos[0], viewport[3] - 1 - mousePos[1], 0.0f, modelviewMatrix, projectionMatrix, viewport, + // rayStart); + // GLU.glhUnProjectf(mousePos[0], viewport[3] - 1 - mousePos[1], 1.0f, modelviewMatrix, projectionMatrix, viewport, + // rayEnd); + + // Hit test mesh. + // DemoInputGeomProvider inputGeom = sample.getInputGeom(); + // if (processHitTest && sample != null) + // { + // float? hit = null; + // if (inputGeom != null) + // { + // hit = inputGeom.raycastMesh(rayStart, rayEnd); + // } + // + // if (!hit.HasValue && sample.getNavMesh() != null) + // { + // hit = NavMeshRaycast.raycast(sample.getNavMesh(), rayStart, rayEnd); + // } + // + // if (!hit.HasValue && sample.getRecastResults() != null) + // { + // hit = PolyMeshRaycast.raycast(sample.getRecastResults(), rayStart, rayEnd); + // } + // + // float[] rayDir = new float[] { rayEnd[0] - rayStart[0], rayEnd[1] - rayStart[1], rayEnd[2] - rayStart[2] }; + // Tool rayTool = toolsUI.getTool(); + // vNormalize(rayDir); + // if (rayTool != null) + // { + // rayTool.handleClickRay(rayStart, rayDir, processHitTestShift); + // } + // // TODO : 잠시 주석 + // // if (hit.HasValue) { + // // float hitTime = hit.Value; + // // if ((modState & GLFW_MOD_CONTROL) != 0) { + // // // Marker + // // markerPositionSet = true; + // // markerPosition[0] = rayStart[0] + (rayEnd[0] - rayStart[0]) * hitTime; + // // markerPosition[1] = rayStart[1] + (rayEnd[1] - rayStart[1]) * hitTime; + // // markerPosition[2] = rayStart[2] + (rayEnd[2] - rayStart[2]) * hitTime; + // // } else { + // // float[] pos = new float[3]; + // // pos[0] = rayStart[0] + (rayEnd[0] - rayStart[0]) * hitTime; + // // pos[1] = rayStart[1] + (rayEnd[1] - rayStart[1]) * hitTime; + // // pos[2] = rayStart[2] + (rayEnd[2] - rayStart[2]) * hitTime; + // // if (rayTool != null) { + // // rayTool.handleClick(rayStart, pos, processHitTestShift); + // // } + // // } + // // } else { + // // if ((modState & GLFW_MOD_CONTROL) != 0) { + // // // Marker + // // markerPositionSet = false; + // // } + // // } + // } + + processHitTest = false; + } + + // if (sample.isChanged()) + // { + // float[] bmin = null; + // float[] bmax = null; + // if (sample.getInputGeom() != null) + // { + // bmin = sample.getInputGeom().getMeshBoundsMin(); + // bmax = sample.getInputGeom().getMeshBoundsMax(); + // } + // else if (sample.getNavMesh() != null) + // { + // float[][] bounds = NavMeshUtils.getNavMeshBounds(sample.getNavMesh()); + // bmin = bounds[0]; + // bmax = bounds[1]; + // } + // else if (0 < sample.getRecastResults().Count) + // { + // foreach (RecastBuilderResult result in sample.getRecastResults()) + // { + // if (result.getSolidHeightfield() != null) + // { + // if (bmin == null) + // { + // bmin = new float[] { float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity }; + // bmax = new float[] { float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity }; + // } + // + // for (int i = 0; i < 3; i++) + // { + // bmin[i] = Math.Min(bmin[i], result.getSolidHeightfield().bmin[i]); + // bmax[i] = Math.Max(bmax[i], result.getSolidHeightfield().bmax[i]); + // } + // } + // } + // } + // + // if (bmin != null && bmax != null) + // { + // camr = (float)(Math.Sqrt( + // DemoMath.sqr(bmax[0] - bmin[0]) + DemoMath.sqr(bmax[1] - bmin[1]) + DemoMath.sqr(bmax[2] - bmin[2])) + // / 2); + // cameraPos[0] = (bmax[0] + bmin[0]) / 2 + camr; + // cameraPos[1] = (bmax[1] + bmin[1]) / 2 + camr; + // cameraPos[2] = (bmax[2] + bmin[2]) / 2 + camr; + // camr *= 3; + // cameraEulers[0] = 45; + // cameraEulers[1] = -45; + // } + // + // sample.setChanged(false); + // toolsUI.setSample(sample); + // } + + // dd.fog(camr * 0.1f, camr * 1.25f); + // renderer.render(sample); + // Tool tool = toolsUI.getTool(); + // if (tool != null) + // { + // tool.handleRender(renderer); + // } + // + // dd.fog(false); + _imgui.Update((float)dt); + } + + private unsafe void OnWindowOnRender(double dt) + { + _gl.Clear(ClearBufferMask.ColorBufferBit); + // Render GUI + //mouseOverMenu = nuklearUI.layout(window, 0, 0, width, height, (int)mousePos[0], (int)mousePos[1]); + //nuklearUI.render(); + ImGui.Button("hello"); + ImGui.Button("world"); + + _imgui.Render(); + } + + + public static void Main(string[] args) + { + var demo = new RecastDemo(); + demo.start(); + } + + private static void ErrorCallback(Silk.NET.GLFW.ErrorCode code, string message) + { + Console.WriteLine($"GLFW error [{code}]: {message}"); + } +} \ No newline at end of file diff --git a/src/DotRecast.Recast.Demo/Sample.cs b/src/DotRecast.Recast.Demo/Sample.cs new file mode 100644 index 0000000..c9e95bc --- /dev/null +++ b/src/DotRecast.Recast.Demo/Sample.cs @@ -0,0 +1,86 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.Collections.Generic; +using DotRecast.Detour; +using DotRecast.Recast.Demo.Draw; +using DotRecast.Recast.Demo.Geom; +using DotRecast.Recast.Demo.Settings; + +namespace DotRecast.Recast.Demo; + +public class Sample { + + private DemoInputGeomProvider inputGeom; + private NavMesh navMesh; + private NavMeshQuery navMeshQuery; + private readonly SettingsUI settingsUI; + private IList recastResults; + private bool changed; + + public Sample(DemoInputGeomProvider inputGeom, IList recastResults, NavMesh navMesh, + SettingsUI settingsUI, RecastDebugDraw debugDraw) { + this.inputGeom = inputGeom; + this.recastResults = recastResults; + this.navMesh = navMesh; + this.settingsUI = settingsUI; + setQuery(navMesh); + changed = true; + } + + private void setQuery(NavMesh navMesh) { + navMeshQuery = navMesh != null ? new NavMeshQuery(navMesh) : null; + } + + public DemoInputGeomProvider getInputGeom() { + return inputGeom; + } + + public IList getRecastResults() { + return recastResults; + } + + public NavMesh getNavMesh() { + return navMesh; + } + + public SettingsUI getSettingsUI() { + return settingsUI; + } + + public NavMeshQuery getNavMeshQuery() { + return navMeshQuery; + } + + public bool isChanged() { + return changed; + } + + public void setChanged(bool changed) { + this.changed = changed; + } + + public void update(DemoInputGeomProvider geom, IList recastResults, NavMesh navMesh) { + inputGeom = geom; + this.recastResults = recastResults; + this.navMesh = navMesh; + setQuery(navMesh); + changed = true; + } +} diff --git a/src/DotRecast.Recast.Demo/Settings/SettingsUI.cs b/src/DotRecast.Recast.Demo/Settings/SettingsUI.cs new file mode 100644 index 0000000..0c1f054 --- /dev/null +++ b/src/DotRecast.Recast.Demo/Settings/SettingsUI.cs @@ -0,0 +1,336 @@ +/* +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using DotRecast.Recast.Demo.Draw; +using DotRecast.Recast.Demo.UI; +using ImGuiNET; +using Silk.NET.OpenGL.Extensions.ImGui; +using Silk.NET.Windowing; + +namespace DotRecast.Recast.Demo.Settings; + +public class SettingsUI : NuklearUIModule { + + private readonly float[] cellSize = new[] { 0.3f }; + private readonly float[] cellHeight = new[] { 0.2f }; + + private readonly float[] agentHeight = new[] { 2f }; + private readonly float[] agentRadius = new[] { 0.6f }; + private readonly float[] agentMaxClimb = new[] { 0.9f }; + private readonly float[] agentMaxSlope = new[] { 45f }; + + private readonly int[] minRegionSize = new[] { 8 }; + private readonly int[] mergedRegionSize = new[] { 20 }; + + private PartitionType partitioning = PartitionType.WATERSHED; + + private bool filterLowHangingObstacles = true; + private bool filterLedgeSpans = true; + private bool filterWalkableLowHeightSpans = true; + + private readonly float[] edgeMaxLen = new[] { 12f }; + private readonly float[] edgeMaxError = new[] { 1.3f }; + private readonly int[] vertsPerPoly = new[] { 6 }; + + private readonly float[] detailSampleDist = new[] { 6f }; + private readonly float[] detailSampleMaxError = new[] { 1f }; + + private bool tiled = false; + private readonly int[] tileSize = new[] { 32 }; + + // public readonly NkColor white = NkColor.create(); + // public readonly NkColor background = NkColor.create(); + // public readonly NkColor transparent = NkColor.create(); + private bool buildTriggered; + private long buildTime; + private readonly int[] voxels = new int[2]; + private readonly int[] tiles = new int[2]; + private int maxTiles; + private int maxPolys; + + private DrawMode drawMode = DrawMode.DRAWMODE_NAVMESH; + private bool meshInputTrigerred; + private bool navMeshInputTrigerred; + + public bool layout(IWindow i, int x, int y, int width, int height, int mouseX, int mouseY) { + bool mouseInside = false; + // nk_rgb(255, 255, 255, white); + // nk_rgba(0, 0, 0, 192, background); + // nk_rgba(255, 0, 0, 0, transparent); + // try (MemoryStack stack = stackPush()) { + // ctx.style().text().color().set(white); + // ctx.style().option().text_normal().set(white); + // ctx.style().property().label_normal().set(white); + // ctx.style().window().background().set(background); + // NkStyleItem styleItem = NkStyleItem.mallocStack(stack); + // nk_style_item_color(background, styleItem); + // ctx.style().window().fixed_background().set(styleItem); + // nk_style_item_color(white, styleItem); + // ctx.style().option().cursor_hover().set(styleItem); + // ctx.style().option().cursor_normal().set(styleItem); + // nk_style_item_color(transparent, styleItem); + // ctx.style().tab().node_minimize_button().normal().set(styleItem); + // ctx.style().tab().node_minimize_button().active().set(styleItem); + // ctx.style().tab().node_maximize_button().normal().set(styleItem); + // ctx.style().tab().node_maximize_button().active().set(styleItem); + // } + // try (MemoryStack stack = stackPush()) { + // NkRect rect = NkRect.mallocStack(stack); + // if (nk_begin(ctx, "Properties", nk_rect(width - 255, 5, 250, height - 10, rect), + // NK_WINDOW_BORDER | NK_WINDOW_MOVABLE | NK_WINDOW_TITLE)) { + // + // nk_layout_row_dynamic(ctx, 18, 1); + // nk_label(ctx, "Input Mesh", NK_TEXT_ALIGN_LEFT); + // nk_layout_row_dynamic(ctx, 20, 1); + // meshInputTrigerred = nk_button_text(ctx, "Load Source Geom..."); + // nk_layout_row_dynamic(ctx, 18, 1); + // nk_label(ctx, string.format("Verts: %d Tris: %d", 0, 0), NK_TEXT_ALIGN_RIGHT); + // + // nk_layout_row_dynamic(ctx, 18, 1); + // nk_label(ctx, "Rasterization", NK_TEXT_ALIGN_LEFT); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_float(ctx, "Cell Size", 0.1f, cellSize, 1f, 0.01f, 0.01f); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_float(ctx, "Cell Height", 0.1f, cellHeight, 1f, 0.01f, 0.01f); + // nk_layout_row_dynamic(ctx, 18, 1); + // nk_label(ctx, string.format("Voxels %d x %d", voxels[0], voxels[1]), NK_TEXT_ALIGN_RIGHT); + // + // nk_layout_row_dynamic(ctx, 18, 1); + // nk_label(ctx, "Agent", NK_TEXT_ALIGN_LEFT); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_float(ctx, "Height", 0.1f, agentHeight, 5f, 0.1f, 0.1f); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_float(ctx, "Radius", 0.0f, agentRadius, 5f, 0.1f, 0.1f); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_float(ctx, "Max Climb", 0.1f, agentMaxClimb, 5f, 0.1f, 0.1f); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_float(ctx, "Max Slope", 0f, agentMaxSlope, 90f, 1f, 1f); + // + // nk_layout_row_dynamic(ctx, 3, 1); + // nk_spacing(ctx, 1); + // nk_layout_row_dynamic(ctx, 18, 1); + // nk_label(ctx, "Region", NK_TEXT_ALIGN_LEFT); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_int(ctx, "Min Region Size", 0, minRegionSize, 150, 1, 1f); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_int(ctx, "Merged Region Size", 0, mergedRegionSize, 150, 1, 1f); + // + // nk_layout_row_dynamic(ctx, 3, 1); + // nk_spacing(ctx, 1); + // nk_layout_row_dynamic(ctx, 18, 1); + // nk_label(ctx, "Partitioning", NK_TEXT_ALIGN_LEFT); + // partitioning = NuklearUIHelper.nk_radio(ctx, PartitionType.values(), partitioning, + // p => p.name().substring(0, 1) + p.name().substring(1).toLowerCase()); + // + // nk_layout_row_dynamic(ctx, 3, 1); + // nk_spacing(ctx, 1); + // nk_layout_row_dynamic(ctx, 18, 1); + // nk_label(ctx, "Filtering", NK_TEXT_ALIGN_LEFT); + // nk_layout_row_dynamic(ctx, 20, 1); + // filterLowHangingObstacles = nk_option_text(ctx, "Low Hanging Obstacles", filterLowHangingObstacles); + // nk_layout_row_dynamic(ctx, 20, 1); + // filterLedgeSpans = nk_option_text(ctx, "Ledge Spans", filterLedgeSpans); + // nk_layout_row_dynamic(ctx, 20, 1); + // filterWalkableLowHeightSpans = nk_option_text(ctx, "Walkable Low Height Spans", + // filterWalkableLowHeightSpans); + // + // nk_layout_row_dynamic(ctx, 3, 1); + // nk_spacing(ctx, 1); + // nk_layout_row_dynamic(ctx, 18, 1); + // nk_label(ctx, "Polygonization", NK_TEXT_ALIGN_LEFT); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_float(ctx, "Max Edge Length", 0f, edgeMaxLen, 50f, 0.1f, 0.1f); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_float(ctx, "Max Edge Error", 0.1f, edgeMaxError, 3f, 0.1f, 0.1f); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_int(ctx, "Vert Per Poly", 3, vertsPerPoly, 12, 1, 1); + // + // nk_layout_row_dynamic(ctx, 3, 1); + // nk_spacing(ctx, 1); + // nk_layout_row_dynamic(ctx, 18, 1); + // nk_label(ctx, "Detail Mesh", NK_TEXT_ALIGN_LEFT); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_float(ctx, "Sample Distance", 0f, detailSampleDist, 16f, 0.1f, 0.1f); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_float(ctx, "Max Sample Error", 0f, detailSampleMaxError, 16f, 0.1f, 0.1f); + // + // nk_layout_row_dynamic(ctx, 3, 1); + // nk_spacing(ctx, 1); + // nk_layout_row_dynamic(ctx, 18, 1); + // nk_label(ctx, "Tiling", NK_TEXT_ALIGN_LEFT); + // nk_layout_row_dynamic(ctx, 20, 1); + // tiled = nk_check_text(ctx, "Enable", tiled); + // if (tiled) { + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_int(ctx, "Tile Size", 16, tileSize, 1024, 16, 16); + // nk_layout_row_dynamic(ctx, 18, 1); + // nk_label(ctx, string.format("Tiles %d x %d", tiles[0], tiles[1]), NK_TEXT_ALIGN_RIGHT); + // nk_layout_row_dynamic(ctx, 18, 1); + // nk_label(ctx, string.format("Max Tiles %d", maxTiles), NK_TEXT_ALIGN_RIGHT); + // nk_layout_row_dynamic(ctx, 18, 1); + // nk_label(ctx, string.format("Max Polys %d", maxPolys), NK_TEXT_ALIGN_RIGHT); + // } + // nk_layout_row_dynamic(ctx, 18, 1); + // nk_label(ctx, string.format("Build Time: %d ms", buildTime), NK_TEXT_ALIGN_LEFT); + // + // nk_layout_row_dynamic(ctx, 20, 1); + // buildTriggered = nk_button_text(ctx, "Build"); + // nk_layout_row_dynamic(ctx, 3, 1); + // nk_spacing(ctx, 1); + // nk_layout_row_dynamic(ctx, 18, 1); + // navMeshInputTrigerred = nk_button_text(ctx, "Load Nav Mesh..."); + // + // nk_layout_row_dynamic(ctx, 18, 1); + // nk_label(ctx, "Draw", NK_TEXT_ALIGN_LEFT); + // drawMode = NuklearUIHelper.nk_radio(ctx, DrawMode.values(), drawMode, dm => dm.toString()); + // + // nk_window_get_bounds(ctx, rect); + // if (mouseX >= rect.x() && mouseX <= rect.x() + rect.w() && mouseY >= rect.y() + // && mouseY <= rect.y() + rect.h()) { + // mouseInside = true; + // } + // } + // nk_end(ctx); + // } + return mouseInside; + } + + public float getCellSize() { + //return cellSize[0]; + return 0; + } + + public float getCellHeight() { + //return cellHeight[0]; + return 0; + } + + public float getAgentHeight() { + //return agentHeight[0]; + return 0; + } + + public float getAgentRadius() { + //return agentRadius[0]; + return 0; + } + + public float getAgentMaxClimb() { + //return agentMaxClimb[0]; + return 0; + } + + public float getAgentMaxSlope() { + //return agentMaxSlope[0]; + return 0; + } + + public int getMinRegionSize() { + //return minRegionSize[0]; + return 0; + } + + public int getMergedRegionSize() { + //return mergedRegionSize[0]; + return 0; + } + + public PartitionType getPartitioning() { + return partitioning; + } + + public bool isBuildTriggered() { + return buildTriggered; + } + + public bool isFilterLowHangingObstacles() { + return filterLowHangingObstacles; + } + + public bool isFilterLedgeSpans() { + return filterLedgeSpans; + } + + public bool isFilterWalkableLowHeightSpans() { + return filterWalkableLowHeightSpans; + } + + public void setBuildTime(long buildTime) { + this.buildTime = buildTime; + } + + public DrawMode getDrawMode() { + return drawMode; + } + + public float getEdgeMaxLen() { + return edgeMaxLen[0]; + } + + public float getEdgeMaxError() { + return edgeMaxError[0]; + } + + public int getVertsPerPoly() { + return vertsPerPoly[0]; + } + + public float getDetailSampleDist() { + return detailSampleDist[0]; + } + + public float getDetailSampleMaxError() { + return detailSampleMaxError[0]; + } + + public void setVoxels(int[] voxels) { + this.voxels[0] = voxels[0]; + this.voxels[1] = voxels[1]; + } + + public bool isTiled() { + return tiled; + } + + public int getTileSize() { + return tileSize[0]; + } + + public void setTiles(int[] tiles) { + this.tiles[0] = tiles[0]; + this.tiles[1] = tiles[1]; + } + + public void setMaxTiles(int maxTiles) { + this.maxTiles = maxTiles; + } + + public void setMaxPolys(int maxPolys) { + this.maxPolys = maxPolys; + } + + public bool isMeshInputTrigerred() { + return meshInputTrigerred; + } + + public bool isNavMeshInputTrigerred() { + return navMeshInputTrigerred; + } + +} diff --git a/src/DotRecast.Recast.Demo/Tools/ConvexVolumeTool.cs b/src/DotRecast.Recast.Demo/Tools/ConvexVolumeTool.cs new file mode 100644 index 0000000..4749262 --- /dev/null +++ b/src/DotRecast.Recast.Demo/Tools/ConvexVolumeTool.cs @@ -0,0 +1,204 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.Collections.Generic; +using Silk.NET.Windowing; + +using DotRecast.Core; +using DotRecast.Recast.Demo.Builder; +using DotRecast.Recast.Demo.Draw; +using DotRecast.Recast.Demo.Geom; + +using static DotRecast.Recast.Demo.Draw.DebugDraw; +using static DotRecast.Recast.Demo.Draw.DebugDrawPrimitives; + +namespace DotRecast.Recast.Demo.Tools; + +public class ConvexVolumeTool : Tool { + + private Sample sample; + private AreaModification areaType = SampleAreaModifications.SAMPLE_AREAMOD_GRASS; + private readonly float[] boxHeight = new[] { 6f }; + private readonly float[] boxDescent = new[] { 1f }; + private readonly float[] polyOffset = new[] { 0f }; + private readonly List pts = new(); + private readonly List hull = new(); + + public override void setSample(Sample m_sample) { + sample = m_sample; + } + + public override void handleClick(float[] s, float[] p, bool shift) { + DemoInputGeomProvider geom = sample.getInputGeom(); + if (geom == null) { + return; + } + + if (shift) { + // Delete + int nearestIndex = -1; + IList vols = geom.convexVolumes(); + for (int i = 0; i < vols.Count; ++i) { + if (PolyUtils.pointInPoly(vols[i].verts, p) && p[1] >= vols[i].hmin + && p[1] <= vols[i].hmax) { + nearestIndex = i; + } + } + // If end point close enough, delete it. + if (nearestIndex != -1) { + geom.convexVolumes().RemoveAt(nearestIndex); + } + } else { + // Create + + // If clicked on that last pt, create the shape. + if (pts.Count > 0 && DemoMath.vDistSqr(p, + new float[] { pts[pts.Count - 3], pts[pts.Count - 2], pts[pts.Count - 1] }, + 0) < 0.2f * 0.2f) { + if (hull.Count > 2) { + // Create shape. + float[] verts = new float[hull.Count * 3]; + for (int i = 0; i < hull.Count; ++i) { + verts[i * 3] = pts[hull[i] * 3]; + verts[i * 3 + 1] = pts[hull[i] * 3 + 1]; + verts[i * 3 + 2] = pts[hull[i] * 3 + 2]; + } + + float minh = float.MaxValue, maxh = 0; + for (int i = 0; i < hull.Count; ++i) { + minh = Math.Min(minh, verts[i * 3 + 1]); + } + minh -= boxDescent[0]; + maxh = minh + boxHeight[0]; + + if (polyOffset[0] > 0.01f) { + float[] offset = new float[verts.Length * 2]; + int noffset = PolyUtils.offsetPoly(verts, hull.Count, polyOffset[0], offset, + offset.Length); + if (noffset > 0) { + geom.addConvexVolume(ArrayUtils.CopyOf(offset, 0, noffset * 3), minh, maxh, areaType); + } + } else { + geom.addConvexVolume(verts, minh, maxh, areaType); + } + } + pts.Clear(); + hull.Clear(); + } else { + // Add new point + pts.Add(p[0]); + pts.Add(p[1]); + pts.Add(p[2]); + // Update hull. + if (pts.Count > 3) { + hull.Clear(); + hull.AddRange(ConvexUtils.convexhull(pts)); + } else { + hull.Clear(); + } + } + } + + } + + public override void handleRender(NavMeshRenderer renderer) { + RecastDebugDraw dd = renderer.getDebugDraw(); + // Find height extent of the shape. + float minh = float.MaxValue, maxh = 0; + for (int i = 0; i < pts.Count; i += 3) { + minh = Math.Min(minh, pts[i + 1]); + } + minh -= boxDescent[0]; + maxh = minh + boxHeight[0]; + + dd.begin(POINTS, 4.0f); + for (int i = 0; i < pts.Count; i += 3) { + int col = duRGBA(255, 255, 255, 255); + if (i == pts.Count - 3) { + col = duRGBA(240, 32, 16, 255); + } + dd.vertex(pts[i + 0], pts[i + 1] + 0.1f, pts[i + 2], col); + } + dd.end(); + + dd.begin(LINES, 2.0f); + for (int i = 0, j = hull.Count - 1; i < hull.Count; j = i++) { + int vi = hull[j] * 3; + int vj = hull[i] * 3; + dd.vertex(pts[vj + 0], minh, pts[vj + 2], duRGBA(255, 255, 255, 64)); + dd.vertex(pts[vi + 0], minh, pts[vi + 2], duRGBA(255, 255, 255, 64)); + dd.vertex(pts[vj + 0], maxh, pts[vj + 2], duRGBA(255, 255, 255, 64)); + dd.vertex(pts[vi + 0], maxh, pts[vi + 2], duRGBA(255, 255, 255, 64)); + dd.vertex(pts[vj + 0], minh, pts[vj + 2], duRGBA(255, 255, 255, 64)); + dd.vertex(pts[vj + 0], maxh, pts[vj + 2], duRGBA(255, 255, 255, 64)); + } + dd.end(); + } + + public override void layout(IWindow ctx) { + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_float(ctx, "Shape Height", 0.1f, boxHeight, 20f, 0.1f, 0.1f); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_float(ctx, "Shape Descent", 0.1f, boxDescent, 20f, 0.1f, 0.1f); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_float(ctx, "Poly Offset", 0.1f, polyOffset, 10f, 0.1f, 0.1f); + // nk_label(ctx, "Area Type", NK_TEXT_ALIGN_LEFT); + // nk_layout_row_dynamic(ctx, 20, 1); + // if (nk_option_label(ctx, "Ground", areaType == SampleAreaModifications.SAMPLE_AREAMOD_GROUND)) { + // areaType = SampleAreaModifications.SAMPLE_AREAMOD_GROUND; + // } + // if (nk_option_label(ctx, "Water", areaType == SampleAreaModifications.SAMPLE_AREAMOD_WATER)) { + // areaType = SampleAreaModifications.SAMPLE_AREAMOD_WATER; + // } + // if (nk_option_label(ctx, "Road", areaType == SampleAreaModifications.SAMPLE_AREAMOD_ROAD)) { + // areaType = SampleAreaModifications.SAMPLE_AREAMOD_ROAD; + // } + // if (nk_option_label(ctx, "Door", areaType == SampleAreaModifications.SAMPLE_AREAMOD_DOOR)) { + // areaType = SampleAreaModifications.SAMPLE_AREAMOD_DOOR; + // } + // if (nk_option_label(ctx, "Grass", areaType == SampleAreaModifications.SAMPLE_AREAMOD_GRASS)) { + // areaType = SampleAreaModifications.SAMPLE_AREAMOD_GRASS; + // } + // if (nk_option_label(ctx, "Jump", areaType == SampleAreaModifications.SAMPLE_AREAMOD_JUMP)) { + // areaType = SampleAreaModifications.SAMPLE_AREAMOD_JUMP; + // } + // if (nk_button_text(ctx, "Clear Shape")) { + // hull.clear(); + // pts.clear(); + // } + // if (nk_button_text(ctx, "Remove All")) { + // hull.clear(); + // pts.clear(); + // DemoInputGeomProvider geom = sample.getInputGeom(); + // if (geom != null) { + // geom.clearConvexVolumes(); + // } + // } + } + + public override string getName() { + return "Create Convex Volumes"; + } + + public override void handleUpdate(float dt) { + // TODO Auto-generated method stub + } + +} diff --git a/src/DotRecast.Recast.Demo/Tools/CrowdProfilingTool.cs b/src/DotRecast.Recast.Demo/Tools/CrowdProfilingTool.cs new file mode 100644 index 0000000..21da13f --- /dev/null +++ b/src/DotRecast.Recast.Demo/Tools/CrowdProfilingTool.cs @@ -0,0 +1,393 @@ +/* +recast4j copyright (c) 2021 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using DotRecast.Core; +using DotRecast.Detour; +using DotRecast.Detour.Crowd; +using DotRecast.Recast.Demo.Builder; +using DotRecast.Recast.Demo.Draw; +using Silk.NET.Windowing; + +using static DotRecast.Recast.Demo.Draw.DebugDraw; + +namespace DotRecast.Recast.Demo.Tools; + +public class CrowdProfilingTool { + + private readonly Func agentParamsSupplier; + private readonly int[] expandSimOptions = new[] { 1 }; + private readonly int[] expandCrowdOptions = new[] { 1 }; + private readonly int[] agents = new[] { 1000 }; + private readonly int[] randomSeed = new[] { 270 }; + private readonly int[] numberOfZones = new[] { 4 }; + private readonly float[] zoneRadius = new[] { 20f }; + private readonly float[] percentMobs = new[] { 80f }; + private readonly float[] percentTravellers = new[] { 15f }; + private readonly int[] pathQueueSize = new[] { 32 }; + private readonly int[] maxIterations = new[] { 300 }; + private Crowd crowd; + private NavMesh navMesh; + private CrowdConfig config; + private NavMeshQuery.FRand rnd; + private readonly List zones = new(); + private long crowdUpdateTime; + + public CrowdProfilingTool(Func agentParamsSupplier) { + this.agentParamsSupplier = agentParamsSupplier; + } + + public void layout(IWindow ctx) { + // nk_layout_row_dynamic(ctx, 1, 1); + // nk_spacing(ctx, 1); + // if (nk_tree_state_push(ctx, 0, "Simulation Options", expandSimOptions)) { + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_int(ctx, "Agents", 0, agents, 10000, 1, 1); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_int(ctx, "Random Seed", 0, randomSeed, 1024, 1, 1); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_int(ctx, "Number of Zones", 0, numberOfZones, 10, 1, 1); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_float(ctx, "Zone Radius", 0, zoneRadius, 100, 1, 1); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_float(ctx, "Mobs %", 0, percentMobs, 100, 1, 1); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_float(ctx, "Travellers %", 0, percentTravellers, 100, 1, 1); + // nk_tree_state_pop(ctx); + // } + // if (nk_tree_state_push(ctx, 0, "Crowd Options", expandCrowdOptions)) { + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_int(ctx, "Path Queue Size", 0, pathQueueSize, 1024, 1, 1); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_int(ctx, "Max Iterations", 0, maxIterations, 4000, 1, 1); + // nk_tree_state_pop(ctx); + // } + // nk_layout_row_dynamic(ctx, 1, 1); + // nk_spacing(ctx, 1); + // nk_layout_row_dynamic(ctx, 20, 1); + // if (nk_button_text(ctx, "Start")) { + // if (navMesh != null) { + // rnd = new NavMeshQuery.FRand(randomSeed[0]); + // createCrowd(); + // createZones(); + // NavMeshQuery navquery = new NavMeshQuery(navMesh); + // QueryFilter filter = new DefaultQueryFilter(); + // for (int i = 0; i < agents[0]; i++) { + // float tr = rnd.frand(); + // AgentType type = AgentType.MOB; + // float mobsPcnt = percentMobs[0] / 100f; + // if (tr > mobsPcnt) { + // tr = rnd.frand(); + // float travellerPcnt = percentTravellers[0] / 100f; + // if (tr > travellerPcnt) { + // type = AgentType.VILLAGER; + // } else { + // type = AgentType.TRAVELLER; + // } + // } + // float[] pos = null; + // switch (type) { + // case MOB: + // pos = getMobPosition(navquery, filter, pos); + // break; + // case VILLAGER: + // pos = getVillagerPosition(navquery, filter, pos); + // break; + // case TRAVELLER: + // pos = getVillagerPosition(navquery, filter, pos); + // break; + // } + // if (pos != null) { + // addAgent(pos, type); + // } + // } + // } + // } + // if (crowd != null) { + // nk_layout_row_dynamic(ctx, 18, 1); + // nk_label(ctx, string.format("Max time to enqueue request: %.3f s", crowd.telemetry().maxTimeToEnqueueRequest()), + // NK_TEXT_ALIGN_LEFT); + // nk_layout_row_dynamic(ctx, 18, 1); + // nk_label(ctx, string.format("Max time to find path: %.3f s", crowd.telemetry().maxTimeToFindPath()), + // NK_TEXT_ALIGN_LEFT); + // List> timings = crowd.telemetry().executionTimings().entrySet().stream() + // .map(e => Tuple.Create(e.getKey(), e.getValue())).sorted((t1, t2) => long.compare(t2.Item2, t1.Item2)) + // .collect(toList()); + // foreach (Tuple e in timings) { + // nk_layout_row_dynamic(ctx, 18, 1); + // nk_label(ctx, string.format("%s: %d us", e.Item1, e.Item2 / 1_000), NK_TEXT_ALIGN_LEFT); + // } + // nk_layout_row_dynamic(ctx, 1, 1); + // nk_spacing(ctx, 1); + // nk_layout_row_dynamic(ctx, 18, 1); + // nk_label(ctx, string.format("Update Time: %d ms", crowdUpdateTime), NK_TEXT_ALIGN_LEFT); + // } + } + + private float[] getMobPosition(NavMeshQuery navquery, QueryFilter filter, float[] pos) { + Result result = navquery.findRandomPoint(filter, rnd); + if (result.succeeded()) { + pos = result.result.getRandomPt(); + } + return pos; + } + + private float[] getVillagerPosition(NavMeshQuery navquery, QueryFilter filter, float[] pos) { + if (0 < zones.Count) { + int zone = (int) (rnd.frand() * zones.Count); + Result result = navquery.findRandomPointWithinCircle(zones[zone].getRandomRef(), + zones[zone].getRandomPt(), zoneRadius[0], filter, rnd); + if (result.succeeded()) { + pos = result.result.getRandomPt(); + } + } + return pos; + } + + private void createZones() { + zones.Clear(); + QueryFilter filter = new DefaultQueryFilter(); + NavMeshQuery navquery = new NavMeshQuery(navMesh); + for (int i = 0; i < numberOfZones[0]; i++) { + float zoneSeparation = zoneRadius[0] * zoneRadius[0] * 16; + for (int k = 0; k < 100; k++) { + Result result = navquery.findRandomPoint(filter, rnd); + if (result.succeeded()) { + bool valid = true; + foreach (FindRandomPointResult zone in zones) { + if (DemoMath.vDistSqr(zone.getRandomPt(), result.result.getRandomPt(), 0) < zoneSeparation) { + valid = false; + break; + } + } + if (valid) { + zones.Add(result.result); + break; + } + } + } + } + } + + private void createCrowd() { + crowd = new Crowd(config, navMesh, __ => new DefaultQueryFilter(SampleAreaModifications.SAMPLE_POLYFLAGS_ALL, + SampleAreaModifications.SAMPLE_POLYFLAGS_DISABLED, new float[] { 1f, 10f, 1f, 1f, 2f, 1.5f })); + + ObstacleAvoidanceQuery.ObstacleAvoidanceParams option = new ObstacleAvoidanceQuery.ObstacleAvoidanceParams(crowd.getObstacleAvoidanceParams(0)); + // Low (11) + option.velBias = 0.5f; + option.adaptiveDivs = 5; + option.adaptiveRings = 2; + option.adaptiveDepth = 1; + crowd.setObstacleAvoidanceParams(0, option); + // Medium (22) + option.velBias = 0.5f; + option.adaptiveDivs = 5; + option.adaptiveRings = 2; + option.adaptiveDepth = 2; + crowd.setObstacleAvoidanceParams(1, option); + // Good (45) + option.velBias = 0.5f; + option.adaptiveDivs = 7; + option.adaptiveRings = 2; + option.adaptiveDepth = 3; + crowd.setObstacleAvoidanceParams(2, option); + // High (66) + option.velBias = 0.5f; + option.adaptiveDivs = 7; + option.adaptiveRings = 3; + option.adaptiveDepth = 3; + crowd.setObstacleAvoidanceParams(3, option); + } + + public void update(float dt) { + long startTime = Stopwatch.GetTimestamp(); + if (crowd != null) { + crowd.config().pathQueueSize = pathQueueSize[0]; + crowd.config().maxFindPathIterations = maxIterations[0]; + crowd.update(dt, null); + } + long endTime = Stopwatch.GetTimestamp(); + if (crowd != null) { + + NavMeshQuery navquery = new NavMeshQuery(navMesh); + QueryFilter filter = new DefaultQueryFilter(); + foreach (CrowdAgent ag in crowd.getActiveAgents()) { + if (needsNewTarget(ag)) { + AgentData agentData = (AgentData) ag.option.userData; + switch (agentData.type) { + case AgentType.MOB: + moveMob(navquery, filter, ag, agentData); + break; + case AgentType.VILLAGER: + moveVillager(navquery, filter, ag, agentData); + break; + case AgentType.TRAVELLER: + moveTraveller(navquery, filter, ag, agentData); + break; + } + } + } + } + crowdUpdateTime = (endTime - startTime) / 1_000_000; + } + + private void moveMob(NavMeshQuery navquery, QueryFilter filter, CrowdAgent ag, AgentData agentData) { + // Move somewhere + Result nearestPoly = navquery.findNearestPoly(ag.npos, crowd.getQueryExtents(), filter); + if (nearestPoly.succeeded()) { + Result result = navquery.findRandomPointAroundCircle(nearestPoly.result.getNearestRef(), + agentData.home, zoneRadius[0] * 2f, filter, rnd); + if (result.succeeded()) { + crowd.requestMoveTarget(ag, result.result.getRandomRef(), result.result.getRandomPt()); + } + } + } + + private void moveVillager(NavMeshQuery navquery, QueryFilter filter, CrowdAgent ag, AgentData agentData) { + // Move somewhere close + Result nearestPoly = navquery.findNearestPoly(ag.npos, crowd.getQueryExtents(), filter); + if (nearestPoly.succeeded()) { + Result result = navquery.findRandomPointAroundCircle(nearestPoly.result.getNearestRef(), + agentData.home, zoneRadius[0] * 0.2f, filter, rnd); + if (result.succeeded()) { + crowd.requestMoveTarget(ag, result.result.getRandomRef(), result.result.getRandomPt()); + } + } + } + + private void moveTraveller(NavMeshQuery navquery, QueryFilter filter, CrowdAgent ag, AgentData agentData) { + // Move to another zone + List potentialTargets = new(); + foreach (FindRandomPointResult zone in zones) { + if (DemoMath.vDistSqr(zone.getRandomPt(), ag.npos, 0) > zoneRadius[0] * zoneRadius[0]) { + potentialTargets.Add(zone); + } + } + if (0 < potentialTargets.Count) { + potentialTargets.Shuffle(); + crowd.requestMoveTarget(ag, potentialTargets[0].getRandomRef(), potentialTargets[0].getRandomPt()); + } + } + + private bool needsNewTarget(CrowdAgent ag) { + if (ag.targetState == CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_NONE + || ag.targetState == CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_FAILED) { + return true; + } + if (ag.targetState == CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_VALID) { + float dx = ag.targetPos[0] - ag.npos[0]; + float dy = ag.targetPos[1] - ag.npos[1]; + float dz = ag.targetPos[2] - ag.npos[2]; + return dx * dx + dy * dy + dz * dz < 0.3f; + } + return false; + } + + public void setup(float maxAgentRadius, NavMesh nav) { + navMesh = nav; + if (nav != null) { + config = new CrowdConfig(maxAgentRadius); + } + } + + public void handleRender(NavMeshRenderer renderer) { + RecastDebugDraw dd = renderer.getDebugDraw(); + dd.depthMask(false); + if (crowd != null) { + foreach (CrowdAgent ag in crowd.getActiveAgents()) { + float radius = ag.option.radius; + float[] pos = ag.npos; + dd.debugDrawCircle(pos[0], pos[1], pos[2], radius, duRGBA(0, 0, 0, 32), 2.0f); + } + + foreach (CrowdAgent ag in crowd.getActiveAgents()) { + AgentData agentData = (AgentData) ag.option.userData; + + float height = ag.option.height; + float radius = ag.option.radius; + float[] pos = ag.npos; + + int col = duRGBA(220, 220, 220, 128); + if (agentData.type == AgentType.TRAVELLER) { + col = duRGBA(100, 160, 100, 128); + } + if (agentData.type == AgentType.VILLAGER) { + col = duRGBA(120, 80, 160, 128); + } + if (ag.targetState == CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_REQUESTING + || ag.targetState == CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_WAITING_FOR_QUEUE) + col = duLerpCol(col, duRGBA(255, 255, 32, 128), 128); + else if (ag.targetState == CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_WAITING_FOR_PATH) + col = duLerpCol(col, duRGBA(255, 64, 32, 128), 128); + else if (ag.targetState == CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_FAILED) + col = duRGBA(255, 32, 16, 128); + else if (ag.targetState == CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_VELOCITY) + col = duLerpCol(col, duRGBA(64, 255, 0, 128), 128); + + dd.debugDrawCylinder(pos[0] - radius, pos[1] + radius * 0.1f, pos[2] - radius, pos[0] + radius, pos[1] + height, + pos[2] + radius, col); + } + } + + dd.depthMask(true); + } + + private CrowdAgent addAgent(float[] p, AgentType type) { + CrowdAgentParams ap = agentParamsSupplier.Invoke(); + ap.userData = new AgentData(type, p); + return crowd.addAgent(p, ap); + } + + public enum AgentType { + VILLAGER, TRAVELLER, MOB, + } + + private class AgentData { + public readonly AgentType type; + public readonly float[] home = new float[3]; + + public AgentData(AgentType type, float[] home) { + this.type = type; + RecastVectors.copy(this.home, home); + } + + } + + public void updateAgentParams(int updateFlags, int obstacleAvoidanceType, float separationWeight) { + if (crowd != null) { + foreach (CrowdAgent ag in crowd.getActiveAgents()) { + CrowdAgentParams option = new CrowdAgentParams(); + option.radius = ag.option.radius; + option.height = ag.option.height; + option.maxAcceleration = ag.option.maxAcceleration; + option.maxSpeed = ag.option.maxSpeed; + option.collisionQueryRange = ag.option.collisionQueryRange; + option.pathOptimizationRange = ag.option.pathOptimizationRange; + option.queryFilterType = ag.option.queryFilterType; + option.userData = ag.option.userData; + option.updateFlags = updateFlags; + option.obstacleAvoidanceType = obstacleAvoidanceType; + option.separationWeight = separationWeight; + crowd.updateAgentParameters(ag, option); + } + } + } +} diff --git a/src/DotRecast.Recast.Demo/Tools/CrowdTool.cs b/src/DotRecast.Recast.Demo/Tools/CrowdTool.cs new file mode 100644 index 0000000..bbd039d --- /dev/null +++ b/src/DotRecast.Recast.Demo/Tools/CrowdTool.cs @@ -0,0 +1,735 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2021 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Silk.NET.Windowing; +using DotRecast.Detour; +using DotRecast.Detour.Crowd; +using DotRecast.Detour.Crowd.Tracking; +using DotRecast.Recast.Demo.Builder; +using DotRecast.Recast.Demo.Draw; +using DotRecast.Recast.Demo.Geom; + +using static DotRecast.Recast.Demo.Draw.DebugDraw; +using static DotRecast.Recast.Demo.Draw.DebugDrawPrimitives; + +namespace DotRecast.Recast.Demo.Tools; + +public class CrowdTool : Tool { + + private enum ToolMode { + CREATE, MOVE_TARGET, SELECT, TOGGLE_POLYS, PROFILING + } + + private readonly CrowdToolParams toolParams = new CrowdToolParams(); + private Sample sample; + private NavMesh m_nav; + private Crowd crowd; + private readonly CrowdProfilingTool profilingTool; + private readonly CrowdAgentDebugInfo m_agentDebug = new CrowdAgentDebugInfo(); + + private static readonly int AGENT_MAX_TRAIL = 64; + + private class AgentTrail { + public float[] trail = new float[AGENT_MAX_TRAIL * 3]; + public int htrail; + }; + + private readonly Dictionary m_trails = new(); + private float[] m_targetPos; + private long m_targetRef; + private ToolMode m_mode = ToolMode.CREATE; + private long crowdUpdateTime; + + public CrowdTool() { + m_agentDebug.vod = new ObstacleAvoidanceDebugData(2048); + profilingTool = new CrowdProfilingTool(getAgentParams); + } + + public override void setSample(Sample psample) { + if (sample != psample) { + sample = psample; + } + + NavMesh nav = sample.getNavMesh(); + + if (nav != null && m_nav != nav) { + m_nav = nav; + + CrowdConfig config = new CrowdConfig(sample.getSettingsUI().getAgentRadius()); + + crowd = new Crowd(config, nav, __ => new DefaultQueryFilter(SampleAreaModifications.SAMPLE_POLYFLAGS_ALL, + SampleAreaModifications.SAMPLE_POLYFLAGS_DISABLED, new float[] { 1f, 10f, 1f, 1f, 2f, 1.5f })); + + // Setup local avoidance option to different qualities. + // Use mostly default settings, copy from dtCrowd. + ObstacleAvoidanceQuery.ObstacleAvoidanceParams option = new ObstacleAvoidanceQuery.ObstacleAvoidanceParams(crowd.getObstacleAvoidanceParams(0)); + + // Low (11) + option.velBias = 0.5f; + option.adaptiveDivs = 5; + option.adaptiveRings = 2; + option.adaptiveDepth = 1; + crowd.setObstacleAvoidanceParams(0, option); + + // Medium (22) + option.velBias = 0.5f; + option.adaptiveDivs = 5; + option.adaptiveRings = 2; + option.adaptiveDepth = 2; + crowd.setObstacleAvoidanceParams(1, option); + + // Good (45) + option.velBias = 0.5f; + option.adaptiveDivs = 7; + option.adaptiveRings = 2; + option.adaptiveDepth = 3; + crowd.setObstacleAvoidanceParams(2, option); + + // High (66) + option.velBias = 0.5f; + option.adaptiveDivs = 7; + option.adaptiveRings = 3; + option.adaptiveDepth = 3; + + crowd.setObstacleAvoidanceParams(3, option); + + profilingTool.setup(sample.getSettingsUI().getAgentRadius(), m_nav); + } + } + + public override void handleClick(float[] s, float[] p, bool shift) { + if (m_mode == ToolMode.PROFILING) { + return; + } + if (crowd == null) { + return; + } + if (m_mode == ToolMode.CREATE) { + if (shift) { + // Delete + CrowdAgent ahit = hitTestAgents(s, p); + if (ahit != null) { + removeAgent(ahit); + } + } else { + // Add + addAgent(p); + } + } else if (m_mode == ToolMode.MOVE_TARGET) { + setMoveTarget(p, shift); + } else if (m_mode == ToolMode.SELECT) { + // Highlight + CrowdAgent ahit = hitTestAgents(s, p); + hilightAgent(ahit); + } else if (m_mode == ToolMode.TOGGLE_POLYS) { + NavMesh nav = sample.getNavMesh(); + NavMeshQuery navquery = sample.getNavMeshQuery(); + if (nav != null && navquery != null) { + QueryFilter filter = new DefaultQueryFilter(); + float[] halfExtents = crowd.getQueryExtents(); + Result result = navquery.findNearestPoly(p, halfExtents, filter); + long refs = result.result.getNearestRef(); + if (refs != 0) { + Result flags = nav.getPolyFlags(refs); + if (flags.succeeded()) { + nav.setPolyFlags(refs, flags.result ^ SampleAreaModifications.SAMPLE_POLYFLAGS_DISABLED); + } + } + } + } + } + + private void removeAgent(CrowdAgent agent) { + crowd.removeAgent(agent); + if (agent == m_agentDebug.agent) { + m_agentDebug.agent = null; + } + } + + private void addAgent(float[] p) { + CrowdAgentParams ap = getAgentParams(); + CrowdAgent ag = crowd.addAgent(p, ap); + if (ag != null) { + if (m_targetRef != 0) + crowd.requestMoveTarget(ag, m_targetRef, m_targetPos); + + // Init trail + if (!m_trails.TryGetValue(ag.idx, out var trail)) + { + trail = new AgentTrail(); + m_trails.Add(ag.idx, trail); + } + for (int i = 0; i < AGENT_MAX_TRAIL; ++i) { + trail.trail[i * 3] = p[0]; + trail.trail[i * 3 + 1] = p[1]; + trail.trail[i * 3 + 2] = p[2]; + } + trail.htrail = 0; + } + + } + + private CrowdAgentParams getAgentParams() { + CrowdAgentParams ap = new CrowdAgentParams(); + ap.radius = sample.getSettingsUI().getAgentRadius(); + ap.height = sample.getSettingsUI().getAgentHeight(); + ap.maxAcceleration = 8.0f; + ap.maxSpeed = 3.5f; + ap.collisionQueryRange = ap.radius * 12.0f; + ap.pathOptimizationRange = ap.radius * 30.0f; + ap.updateFlags = getUpdateFlags(); + ap.obstacleAvoidanceType = toolParams.m_obstacleAvoidanceType[0]; + ap.separationWeight = toolParams.m_separationWeight[0]; + return ap; + } + + private CrowdAgent hitTestAgents(float[] s, float[] p) { + + CrowdAgent isel = null; + float tsel = float.MaxValue; + + foreach (CrowdAgent ag in crowd.getActiveAgents()) { + float[] bmin = new float[3], bmax = new float[3]; + getAgentBounds(ag, bmin, bmax); + float[] isect = Intersections.intersectSegmentAABB(s, p, bmin, bmax); + if (null != isect) { + float tmin = isect[0]; + if (tmin > 0 && tmin < tsel) { + isel = ag; + tsel = tmin; + } + } + } + + return isel; + } + + private void getAgentBounds(CrowdAgent ag, float[] bmin, float[] bmax) { + float[] p = ag.npos; + float r = ag.option.radius; + float h = ag.option.height; + bmin[0] = p[0] - r; + bmin[1] = p[1]; + bmin[2] = p[2] - r; + bmax[0] = p[0] + r; + bmax[1] = p[1] + h; + bmax[2] = p[2] + r; + } + + private void setMoveTarget(float[] p, bool adjust) { + if (sample == null || crowd == null) + return; + + // Find nearest point on navmesh and set move request to that location. + NavMeshQuery navquery = sample.getNavMeshQuery(); + QueryFilter filter = crowd.getFilter(0); + float[] halfExtents = crowd.getQueryExtents(); + + if (adjust) { + // Request velocity + if (m_agentDebug.agent != null) { + float[] vel = calcVel(m_agentDebug.agent.npos, p, m_agentDebug.agent.option.maxSpeed); + crowd.requestMoveVelocity(m_agentDebug.agent, vel); + } else { + foreach (CrowdAgent ag in crowd.getActiveAgents()) { + float[] vel = calcVel(ag.npos, p, ag.option.maxSpeed); + crowd.requestMoveVelocity(ag, vel); + } + } + } else { + Result result = navquery.findNearestPoly(p, halfExtents, filter); + m_targetRef = result.result.getNearestRef(); + m_targetPos = result.result.getNearestPos(); + if (m_agentDebug.agent != null) { + crowd.requestMoveTarget(m_agentDebug.agent, m_targetRef, m_targetPos); + } else { + foreach (CrowdAgent ag in crowd.getActiveAgents()) { + crowd.requestMoveTarget(ag, m_targetRef, m_targetPos); + } + } + } + } + + private float[] calcVel(float[] pos, float[] tgt, float speed) { + float[] vel = DetourCommon.vSub(tgt, pos); + vel[1] = 0.0f; + DetourCommon.vNormalize(vel); + return DetourCommon.vScale(vel, speed); + } + + public override void handleRender(NavMeshRenderer renderer) { + if (m_mode == ToolMode.PROFILING) { + profilingTool.handleRender(renderer); + return; + } + RecastDebugDraw dd = renderer.getDebugDraw(); + float rad = sample.getSettingsUI().getAgentRadius(); + NavMesh nav = sample.getNavMesh(); + if (nav == null || crowd == null) + return; + + if (toolParams.m_showNodes && crowd.getPathQueue() != null) { +// NavMeshQuery navquery = crowd.getPathQueue().getNavQuery(); +// if (navquery != null) { +// dd.debugDrawNavMeshNodes(navquery); +// } + } + dd.depthMask(false); + + // Draw paths + if (toolParams.m_showPath) { + foreach (CrowdAgent ag in crowd.getActiveAgents()) { + if (!toolParams.m_showDetailAll && ag != m_agentDebug.agent) + continue; + List path = ag.corridor.getPath(); + int npath = ag.corridor.getPathCount(); + for (int j = 0; j < npath; ++j) { + dd.debugDrawNavMeshPoly(nav, path[j], duRGBA(255, 255, 255, 24)); + } + } + } + + if (m_targetRef != 0) + dd.debugDrawCross(m_targetPos[0], m_targetPos[1] + 0.1f, m_targetPos[2], rad, duRGBA(255, 255, 255, 192), 2.0f); + + // Occupancy grid. + if (toolParams.m_showGrid) { + float gridy = -float.MaxValue; + foreach (CrowdAgent ag in crowd.getActiveAgents()) { + float[] pos = ag.corridor.getPos(); + gridy = Math.Max(gridy, pos[1]); + } + gridy += 1.0f; + + dd.begin(QUADS); + ProximityGrid grid = crowd.getGrid(); + float cs = grid.getCellSize(); + foreach (int[] ic in grid.getItemCounts()) { + int x = ic[0]; + int y = ic[1]; + int count = ic[2]; + if (count != 0) { + int col = duRGBA(128, 0, 0, Math.Min(count * 40, 255)); + dd.vertex(x * cs, gridy, y * cs, col); + dd.vertex(x * cs, gridy, y * cs + cs, col); + dd.vertex(x * cs + cs, gridy, y * cs + cs, col); + dd.vertex(x * cs + cs, gridy, y * cs, col); + } + } + dd.end(); + } + + // Trail + foreach (CrowdAgent ag in crowd.getActiveAgents()) { + + AgentTrail trail = m_trails[ag.idx]; + float[] pos = ag.npos; + + dd.begin(LINES, 3.0f); + float[] prev = new float[3]; + float preva = 1; + DetourCommon.vCopy(prev, pos); + for (int j = 0; j < AGENT_MAX_TRAIL - 1; ++j) { + int idx = (trail.htrail + AGENT_MAX_TRAIL - j) % AGENT_MAX_TRAIL; + int v = idx * 3; + float a = 1 - j / (float) AGENT_MAX_TRAIL; + dd.vertex(prev[0], prev[1] + 0.1f, prev[2], duRGBA(0, 0, 0, (int) (128 * preva))); + dd.vertex(trail.trail[v], trail.trail[v + 1] + 0.1f, trail.trail[v + 2], duRGBA(0, 0, 0, (int) (128 * a))); + preva = a; + DetourCommon.vCopy(prev, trail.trail, v); + } + dd.end(); + + } + + // Corners & co + foreach (CrowdAgent ag in crowd.getActiveAgents()) { + if (toolParams.m_showDetailAll == false && ag != m_agentDebug.agent) + continue; + + float radius = ag.option.radius; + float[] pos = ag.npos; + + if (toolParams.m_showCorners) { + if (0 < ag.corners.Count) { + dd.begin(LINES, 2.0f); + for (int j = 0; j < ag.corners.Count; ++j) { + float[] va = j == 0 ? pos : ag.corners[j - 1].getPos(); + float[] vb = ag.corners[j].getPos(); + dd.vertex(va[0], va[1] + radius, va[2], duRGBA(128, 0, 0, 192)); + dd.vertex(vb[0], vb[1] + radius, vb[2], duRGBA(128, 0, 0, 192)); + } + if ((ag.corners[ag.corners.Count - 1].getFlags() + & NavMeshQuery.DT_STRAIGHTPATH_OFFMESH_CONNECTION) != 0) { + float[] v = ag.corners[ag.corners.Count - 1].getPos(); + dd.vertex(v[0], v[1], v[2], duRGBA(192, 0, 0, 192)); + dd.vertex(v[0], v[1] + radius * 2, v[2], duRGBA(192, 0, 0, 192)); + } + + dd.end(); + + if (toolParams.m_anticipateTurns) { + /* float dvel[3], pos[3]; + calcSmoothSteerDirection(ag.pos, ag.cornerVerts, ag.ncorners, dvel); + pos[0] = ag.pos[0] + dvel[0]; + pos[1] = ag.pos[1] + dvel[1]; + pos[2] = ag.pos[2] + dvel[2]; + + float off = ag.radius+0.1f; + float[] tgt = &ag.cornerVerts[0]; + float y = ag.pos[1]+off; + + dd.begin(DU_DRAW_LINES, 2.0f); + + dd.vertex(ag.pos[0],y,ag.pos[2], duRGBA(255,0,0,192)); + dd.vertex(pos[0],y,pos[2], duRGBA(255,0,0,192)); + + dd.vertex(pos[0],y,pos[2], duRGBA(255,0,0,192)); + dd.vertex(tgt[0],y,tgt[2], duRGBA(255,0,0,192)); + + dd.end();*/ + } + } + } + + if (toolParams.m_showCollisionSegments) { + float[] center = ag.boundary.getCenter(); + dd.debugDrawCross(center[0], center[1] + radius, center[2], 0.2f, duRGBA(192, 0, 128, 255), 2.0f); + dd.debugDrawCircle(center[0], center[1] + radius, center[2], ag.option.collisionQueryRange, + duRGBA(192, 0, 128, 128), 2.0f); + + dd.begin(LINES, 3.0f); + for (int j = 0; j < ag.boundary.getSegmentCount(); ++j) { + int col = duRGBA(192, 0, 128, 192); + float[] s = ag.boundary.getSegment(j); + float[] s0 = new float[] { s[0], s[1], s[2] }; + float[] s3 = new float[] { s[3], s[4], s[5] }; + if (DetourCommon.triArea2D(pos, s0, s3) < 0.0f) + col = duDarkenCol(col); + + dd.appendArrow(s[0], s[1] + 0.2f, s[2], s[3], s[4] + 0.2f, s[5], 0.0f, 0.3f, col); + } + dd.end(); + } + + if (toolParams.m_showNeis) { + dd.debugDrawCircle(pos[0], pos[1] + radius, pos[2], ag.option.collisionQueryRange, duRGBA(0, 192, 128, 128), + 2.0f); + + dd.begin(LINES, 2.0f); + for (int j = 0; j < ag.neis.Count; ++j) { + CrowdAgent nei = ag.neis[j].agent; + if (nei != null) { + dd.vertex(pos[0], pos[1] + radius, pos[2], duRGBA(0, 192, 128, 128)); + dd.vertex(nei.npos[0], nei.npos[1] + radius, nei.npos[2], duRGBA(0, 192, 128, 128)); + } + } + dd.end(); + } + + if (toolParams.m_showOpt) { + dd.begin(LINES, 2.0f); + dd.vertex(m_agentDebug.optStart[0], m_agentDebug.optStart[1] + 0.3f, m_agentDebug.optStart[2], + duRGBA(0, 128, 0, 192)); + dd.vertex(m_agentDebug.optEnd[0], m_agentDebug.optEnd[1] + 0.3f, m_agentDebug.optEnd[2], duRGBA(0, 128, 0, 192)); + dd.end(); + } + } + + // Agent cylinders. + foreach (CrowdAgent ag in crowd.getActiveAgents()) { + + float radius = ag.option.radius; + float[] pos = ag.npos; + + int col = duRGBA(0, 0, 0, 32); + if (m_agentDebug.agent == ag) + col = duRGBA(255, 0, 0, 128); + + dd.debugDrawCircle(pos[0], pos[1], pos[2], radius, col, 2.0f); + } + + foreach (CrowdAgent ag in crowd.getActiveAgents()) { + + float height = ag.option.height; + float radius = ag.option.radius; + float[] pos = ag.npos; + + int col = duRGBA(220, 220, 220, 128); + if (ag.targetState == CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_REQUESTING + || ag.targetState == CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_WAITING_FOR_QUEUE) + col = duLerpCol(col, duRGBA(128, 0, 255, 128), 32); + else if (ag.targetState == CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_WAITING_FOR_PATH) + col = duLerpCol(col, duRGBA(128, 0, 255, 128), 128); + else if (ag.targetState == CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_FAILED) + col = duRGBA(255, 32, 16, 128); + else if (ag.targetState == CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_VELOCITY) + col = duLerpCol(col, duRGBA(64, 255, 0, 128), 128); + + dd.debugDrawCylinder(pos[0] - radius, pos[1] + radius * 0.1f, pos[2] - radius, pos[0] + radius, pos[1] + height, + pos[2] + radius, col); + } + + if (toolParams.m_showVO) { + foreach (CrowdAgent ag in crowd.getActiveAgents()) { + if (toolParams.m_showDetailAll == false && ag != m_agentDebug.agent) + continue; + + // Draw detail about agent sela + ObstacleAvoidanceDebugData vod = m_agentDebug.vod; + + float dx = ag.npos[0]; + float dy = ag.npos[1] + ag.option.height; + float dz = ag.npos[2]; + + dd.debugDrawCircle(dx, dy, dz, ag.option.maxSpeed, duRGBA(255, 255, 255, 64), 2.0f); + + dd.begin(QUADS); + for (int j = 0; j < vod.getSampleCount(); ++j) { + float[] p = vod.getSampleVelocity(j); + float sr = vod.getSampleSize(j); + float pen = vod.getSamplePenalty(j); + float pen2 = vod.getSamplePreferredSidePenalty(j); + int col = duLerpCol(duRGBA(255, 255, 255, 220), duRGBA(128, 96, 0, 220), (int) (pen * 255)); + col = duLerpCol(col, duRGBA(128, 0, 0, 220), (int) (pen2 * 128)); + dd.vertex(dx + p[0] - sr, dy, dz + p[2] - sr, col); + dd.vertex(dx + p[0] - sr, dy, dz + p[2] + sr, col); + dd.vertex(dx + p[0] + sr, dy, dz + p[2] + sr, col); + dd.vertex(dx + p[0] + sr, dy, dz + p[2] - sr, col); + } + dd.end(); + } + } + + // Velocity stuff. + foreach (CrowdAgent ag in crowd.getActiveAgents()) { + + float radius = ag.option.radius; + float height = ag.option.height; + float[] pos = ag.npos; + float[] vel = ag.vel; + float[] dvel = ag.dvel; + + int col = duRGBA(220, 220, 220, 192); + if (ag.targetState == CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_REQUESTING + || ag.targetState == CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_WAITING_FOR_QUEUE) + col = duLerpCol(col, duRGBA(128, 0, 255, 192), 48); + else if (ag.targetState == CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_WAITING_FOR_PATH) + col = duLerpCol(col, duRGBA(128, 0, 255, 192), 128); + else if (ag.targetState == CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_FAILED) + col = duRGBA(255, 32, 16, 192); + else if (ag.targetState == CrowdAgent.MoveRequestState.DT_CROWDAGENT_TARGET_VELOCITY) + col = duLerpCol(col, duRGBA(64, 255, 0, 192), 128); + + dd.debugDrawCircle(pos[0], pos[1] + height, pos[2], radius, col, 2.0f); + + dd.debugDrawArrow(pos[0], pos[1] + height, pos[2], pos[0] + dvel[0], pos[1] + height + dvel[1], pos[2] + dvel[2], + 0.0f, 0.4f, duRGBA(0, 192, 255, 192), m_agentDebug.agent == ag ? 2.0f : 1.0f); + + dd.debugDrawArrow(pos[0], pos[1] + height, pos[2], pos[0] + vel[0], pos[1] + height + vel[1], pos[2] + vel[2], 0.0f, + 0.4f, duRGBA(0, 0, 0, 160), 2.0f); + } + + dd.depthMask(true); + } + + public override void handleUpdate(float dt) { + updateTick(dt); + } + + private void updateTick(float dt) { + if (m_mode == ToolMode.PROFILING) { + profilingTool.update(dt); + return; + } + if (crowd == null) + return; + NavMesh nav = sample.getNavMesh(); + if (nav == null) + return; + + long startTime = Stopwatch.GetTimestamp(); + crowd.update(dt, m_agentDebug); + long endTime = Stopwatch.GetTimestamp(); + + // Update agent trails + foreach (CrowdAgent ag in crowd.getActiveAgents()) { + AgentTrail trail = m_trails[ag.idx]; + // Update agent movement trail. + trail.htrail = (trail.htrail + 1) % AGENT_MAX_TRAIL; + trail.trail[trail.htrail * 3] = ag.npos[0]; + trail.trail[trail.htrail * 3 + 1] = ag.npos[1]; + trail.trail[trail.htrail * 3 + 2] = ag.npos[2]; + } + + m_agentDebug.vod.normalizeSamples(); + + // m_crowdSampleCount.addSample((float) crowd.getVelocitySampleCount()); + crowdUpdateTime = (endTime - startTime) / 1_000_000; + } + + private void hilightAgent(CrowdAgent agent) { + m_agentDebug.agent = agent; + } + + public override void layout(IWindow ctx) { + // ToolMode previousToolMode = m_mode; + // nk_layout_row_dynamic(ctx, 20, 1); + // if (nk_option_label(ctx, "Create Agents", m_mode == ToolMode.CREATE)) { + // m_mode = ToolMode.CREATE; + // } + // nk_layout_row_dynamic(ctx, 20, 1); + // if (nk_option_label(ctx, "Move Target", m_mode == ToolMode.MOVE_TARGET)) { + // m_mode = ToolMode.MOVE_TARGET; + // } + // nk_layout_row_dynamic(ctx, 20, 1); + // if (nk_option_label(ctx, "Select Agent", m_mode == ToolMode.SELECT)) { + // m_mode = ToolMode.SELECT; + // } + // nk_layout_row_dynamic(ctx, 20, 1); + // if (nk_option_label(ctx, "Toggle Polys", m_mode == ToolMode.TOGGLE_POLYS)) { + // m_mode = ToolMode.TOGGLE_POLYS; + // } + // nk_layout_row_dynamic(ctx, 20, 1); + // if (nk_option_label(ctx, "Profiling", m_mode == ToolMode.PROFILING)) { + // m_mode = ToolMode.PROFILING; + // } + // nk_layout_row_dynamic(ctx, 1, 1); + // nk_spacing(ctx, 1); + // if (nk_tree_state_push(ctx, 0, "Options", toolParams.m_expandOptions)) { + // bool m_optimizeVis = toolParams.m_optimizeVis; + // bool m_optimizeTopo = toolParams.m_optimizeTopo; + // bool m_anticipateTurns = toolParams.m_anticipateTurns; + // bool m_obstacleAvoidance = toolParams.m_obstacleAvoidance; + // bool m_separation = toolParams.m_separation; + // int m_obstacleAvoidanceType = toolParams.m_obstacleAvoidanceType[0]; + // float m_separationWeight = toolParams.m_separationWeight[0]; + // nk_layout_row_dynamic(ctx, 20, 1); + // toolParams.m_optimizeVis = nk_option_text(ctx, "Optimize Visibility", toolParams.m_optimizeVis); + // nk_layout_row_dynamic(ctx, 20, 1); + // toolParams.m_optimizeTopo = nk_option_text(ctx, "Optimize Topology", toolParams.m_optimizeTopo); + // nk_layout_row_dynamic(ctx, 20, 1); + // toolParams.m_anticipateTurns = nk_option_text(ctx, "Anticipate Turns", toolParams.m_anticipateTurns); + // nk_layout_row_dynamic(ctx, 20, 1); + // toolParams.m_obstacleAvoidance = nk_option_text(ctx, "Obstacle Avoidance", toolParams.m_obstacleAvoidance); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_int(ctx, "Avoidance Quality", 0, toolParams.m_obstacleAvoidanceType, 3, 1, 0.1f); + // nk_layout_row_dynamic(ctx, 20, 1); + // toolParams.m_separation = nk_option_text(ctx, "Separation", toolParams.m_separation); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_float(ctx, "Separation Weight", 0f, toolParams.m_separationWeight, 20f, 0.01f, 0.01f); + // if (m_optimizeVis != toolParams.m_optimizeVis || m_optimizeTopo != toolParams.m_optimizeTopo + // || m_anticipateTurns != toolParams.m_anticipateTurns || m_obstacleAvoidance != toolParams.m_obstacleAvoidance + // || m_separation != toolParams.m_separation + // || m_obstacleAvoidanceType != toolParams.m_obstacleAvoidanceType[0] + // || m_separationWeight != toolParams.m_separationWeight[0]) { + // updateAgentParams(); + // } + // nk_tree_state_pop(ctx); + // } + // if (m_mode == ToolMode.PROFILING) { + // profilingTool.layout(ctx); + // } + // if (m_mode != ToolMode.PROFILING) { + // nk_layout_row_dynamic(ctx, 1, 1); + // nk_spacing(ctx, 1); + // if (nk_tree_state_push(ctx, 0, "Selected Debug Draw", toolParams.m_expandSelectedDebugDraw)) { + // nk_layout_row_dynamic(ctx, 20, 1); + // toolParams.m_showCorners = nk_option_text(ctx, "Show Corners", toolParams.m_showCorners); + // nk_layout_row_dynamic(ctx, 20, 1); + // toolParams.m_showCollisionSegments = nk_option_text(ctx, "Show Collision Segs", toolParams.m_showCollisionSegments); + // nk_layout_row_dynamic(ctx, 20, 1); + // toolParams.m_showPath = nk_option_text(ctx, "Show Path", toolParams.m_showPath); + // nk_layout_row_dynamic(ctx, 20, 1); + // toolParams.m_showVO = nk_option_text(ctx, "Show VO", toolParams.m_showVO); + // nk_layout_row_dynamic(ctx, 20, 1); + // toolParams.m_showOpt = nk_option_text(ctx, "Show Path Optimization", toolParams.m_showOpt); + // nk_layout_row_dynamic(ctx, 20, 1); + // toolParams.m_showNeis = nk_option_text(ctx, "Show Neighbours", toolParams.m_showNeis); + // nk_tree_state_pop(ctx); + // } + // nk_layout_row_dynamic(ctx, 1, 1); + // nk_spacing(ctx, 1); + // if (nk_tree_state_push(ctx, 0, "Debug Draw", toolParams.m_expandDebugDraw)) { + // nk_layout_row_dynamic(ctx, 20, 1); + // toolParams.m_showGrid = nk_option_text(ctx, "Show Prox Grid", toolParams.m_showGrid); + // nk_layout_row_dynamic(ctx, 20, 1); + // toolParams.m_showNodes = nk_option_text(ctx, "Show Nodes", toolParams.m_showNodes); + // nk_tree_state_pop(ctx); + // } + // nk_layout_row_dynamic(ctx, 2, 1); + // nk_spacing(ctx, 1); + // nk_layout_row_dynamic(ctx, 18, 1); + // nk_label(ctx, string.format("Update Time: %d ms", crowdUpdateTime), NK_TEXT_ALIGN_LEFT); + // } + } + + private void updateAgentParams() { + if (crowd == null) { + return; + } + + int updateFlags = getUpdateFlags(); + profilingTool.updateAgentParams(updateFlags, toolParams.m_obstacleAvoidanceType[0], toolParams.m_separationWeight[0]); + foreach (CrowdAgent ag in crowd.getActiveAgents()) { + CrowdAgentParams option = new CrowdAgentParams(); + option.radius = ag.option.radius; + option.height = ag.option.height; + option.maxAcceleration = ag.option.maxAcceleration; + option.maxSpeed = ag.option.maxSpeed; + option.collisionQueryRange = ag.option.collisionQueryRange; + option.pathOptimizationRange = ag.option.pathOptimizationRange; + option.obstacleAvoidanceType = ag.option.obstacleAvoidanceType; + option.queryFilterType = ag.option.queryFilterType; + option.userData = ag.option.userData; + option.updateFlags = updateFlags; + option.obstacleAvoidanceType = toolParams.m_obstacleAvoidanceType[0]; + option.separationWeight = toolParams.m_separationWeight[0]; + crowd.updateAgentParameters(ag, option); + } + } + + private int getUpdateFlags() { + int updateFlags = 0; + if (toolParams.m_anticipateTurns) { + updateFlags |= CrowdAgentParams.DT_CROWD_ANTICIPATE_TURNS; + } + if (toolParams.m_optimizeVis) { + updateFlags |= CrowdAgentParams.DT_CROWD_OPTIMIZE_VIS; + } + if (toolParams.m_optimizeTopo) { + updateFlags |= CrowdAgentParams.DT_CROWD_OPTIMIZE_TOPO; + } + if (toolParams.m_obstacleAvoidance) { + updateFlags |= CrowdAgentParams.DT_CROWD_OBSTACLE_AVOIDANCE; + } + if (toolParams.m_separation) { + updateFlags |= CrowdAgentParams.DT_CROWD_SEPARATION; + } + return updateFlags; + } + + public override string getName() { + return "Crowd"; + } + +} diff --git a/src/DotRecast.Recast.Demo/Tools/CrowdToolParams.cs b/src/DotRecast.Recast.Demo/Tools/CrowdToolParams.cs new file mode 100644 index 0000000..3a2c644 --- /dev/null +++ b/src/DotRecast.Recast.Demo/Tools/CrowdToolParams.cs @@ -0,0 +1,46 @@ +/* +recast4j copyright (c) 2020-2021 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Recast.Demo.Tools; + + +public class CrowdToolParams { + + public readonly int[] m_expandSelectedDebugDraw = new[] { 1 }; + public bool m_showCorners; + public bool m_showCollisionSegments; + public bool m_showPath; + public bool m_showVO; + public bool m_showOpt; + public bool m_showNeis; + + public readonly int[] m_expandDebugDraw = new[] { 0 }; + public bool m_showLabels; + public bool m_showGrid; + public bool m_showNodes; + public bool m_showPerfGraph; + public bool m_showDetailAll; + + public readonly int[] m_expandOptions = new[] { 1 }; + public bool m_anticipateTurns = true; + public bool m_optimizeVis = true; + public bool m_optimizeTopo = true; + public bool m_obstacleAvoidance = true; + public readonly int[] m_obstacleAvoidanceType = new[] { 3 }; + public bool m_separation; + public readonly float[] m_separationWeight = new[] { 2f }; +} diff --git a/src/DotRecast.Recast.Demo/Tools/DemoObjImporter.cs b/src/DotRecast.Recast.Demo/Tools/DemoObjImporter.cs new file mode 100644 index 0000000..1df84d9 --- /dev/null +++ b/src/DotRecast.Recast.Demo/Tools/DemoObjImporter.cs @@ -0,0 +1,14 @@ +using System; +using System.IO; +using DotRecast.Recast.Demo.Geom; + +namespace DotRecast.Recast.Demo.Tools; + +public static class DemoObjImporter +{ + public static DemoInputGeomProvider load(byte[] chunk) { + var context = ObjImporter.loadContext(chunk); + return new DemoInputGeomProvider(context.vertexPositions, context.meshFaces); + + } +} \ No newline at end of file diff --git a/src/DotRecast.Recast.Demo/Tools/DynamicUpdateTool.cs b/src/DotRecast.Recast.Demo/Tools/DynamicUpdateTool.cs new file mode 100644 index 0000000..9b25fd3 --- /dev/null +++ b/src/DotRecast.Recast.Demo/Tools/DynamicUpdateTool.cs @@ -0,0 +1,675 @@ +/* +recast4j copyright (c) 2021 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading.Tasks; +using DotRecast.Core; +using DotRecast.Detour.Dynamic; +using DotRecast.Detour.Dynamic.Colliders; +using DotRecast.Detour.Dynamic.Io; +using DotRecast.Recast.Demo.Builder; +using DotRecast.Recast.Demo.Draw; +using DotRecast.Recast.Demo.Geom; +using DotRecast.Recast.Demo.Tools.Gizmos; +using DotRecast.Recast.Demo.UI; +using Silk.NET.Windowing; + +using static DotRecast.Recast.Demo.Draw.DebugDraw; +using static DotRecast.Recast.Demo.Draw.DebugDrawPrimitives; +using static DotRecast.Detour.DetourCommon; + +namespace DotRecast.Recast.Demo.Tools; +public class DynamicUpdateTool : Tool { + + private enum ToolMode { + BUILD, COLLIDERS, RAYCAST + } + + private enum ColliderShape { + SPHERE, CAPSULE, BOX, CYLINDER, COMPOSITE, CONVEX, TRIMESH_BRIDGE, TRIMESH_HOUSE + } + + private Sample sample; + private ToolMode mode = ToolMode.BUILD; + private readonly float[] cellSize = new[] { 0.3f }; + private PartitionType partitioning = PartitionType.WATERSHED; + private bool filterLowHangingObstacles = true; + private bool filterLedgeSpans = true; + private bool filterWalkableLowHeightSpans = true; + private readonly float[] walkableHeight = new[] { 2f }; + private readonly float[] walkableRadius = new[] { 0.6f }; + private readonly float[] walkableClimb = new[] { 0.9f }; + private readonly float[] walkableSlopeAngle = new[] { 45f }; + private readonly float[] minRegionArea = new[] { 6f }; + private readonly float[] regionMergeSize = new[] { 36f }; + private readonly float[] maxEdgeLen = new[] { 12f }; + private readonly float[] maxSimplificationError = new[] { 1.3f }; + private readonly int[] vertsPerPoly = new[] { 6 }; + private bool buildDetailMesh = true; + private bool compression = true; + private readonly float[] detailSampleDist = new[] { 6f }; + private readonly float[] detailSampleMaxError = new[] { 1f }; + private bool showColliders = false; + private long buildTime; + private long raycastTime; + private ColliderShape colliderShape = ColliderShape.SPHERE; + + private DynamicNavMesh dynaMesh; + private readonly TaskFactory executor; + private readonly Dictionary colliders = new(); + private readonly Dictionary colliderGizmos = new(); + private readonly Random random = Random.Shared; + private readonly DemoInputGeomProvider bridgeGeom; + private readonly DemoInputGeomProvider houseGeom; + private readonly DemoInputGeomProvider convexGeom; + private bool sposSet; + private bool eposSet; + private float[] spos; + private float[] epos; + private bool raycastHit; + private float[] raycastHitPos; + + public DynamicUpdateTool() + { + executor = Task.Factory; + bridgeGeom = DemoObjImporter.load(Loader.ToBytes("bridge.obj")); + houseGeom = DemoObjImporter.load(Loader.ToBytes("house.obj")); + convexGeom = DemoObjImporter.load(Loader.ToBytes("convex.obj")); + } + + public override void setSample(Sample sample) { + this.sample = sample; + } + + public override void handleClick(float[] s, float[] p, bool shift) { + if (mode == ToolMode.COLLIDERS) { + if (!shift) { + Tuple colliderWithGizmo = null; + if (dynaMesh != null) { + if (colliderShape == ColliderShape.SPHERE) { + colliderWithGizmo = sphereCollider(p); + } else if (colliderShape == ColliderShape.CAPSULE) { + colliderWithGizmo = capsuleCollider(p); + } else if (colliderShape == ColliderShape.BOX) { + colliderWithGizmo = boxCollider(p); + } else if (colliderShape == ColliderShape.CYLINDER) { + colliderWithGizmo = cylinderCollider(p); + } else if (colliderShape == ColliderShape.COMPOSITE) { + colliderWithGizmo = compositeCollider(p); + } else if (colliderShape == ColliderShape.TRIMESH_BRIDGE) { + colliderWithGizmo = trimeshBridge(p); + } else if (colliderShape == ColliderShape.TRIMESH_HOUSE) { + colliderWithGizmo = trimeshHouse(p); + } else if (colliderShape == ColliderShape.CONVEX) { + colliderWithGizmo = convexTrimesh(p); + } + } + if (colliderWithGizmo != null) { + long id = dynaMesh.addCollider(colliderWithGizmo.Item1); + colliders.Add(id, colliderWithGizmo.Item1); + colliderGizmos.Add(id, colliderWithGizmo.Item2); + } + } + } + if (mode == ToolMode.RAYCAST) { + if (shift) { + sposSet = true; + spos = ArrayUtils.CopyOf(p, p.Length); + } else { + eposSet = true; + epos = ArrayUtils.CopyOf(p, p.Length); + } + if (sposSet && eposSet && dynaMesh != null) { + float[] sp = { spos[0], spos[1] + 1.3f, spos[2] }; + float[] ep = { epos[0], epos[1] + 1.3f, epos[2] }; + long t1 = Stopwatch.GetTimestamp(); + float? hitPos = dynaMesh.voxelQuery().raycast(sp, ep); + long t2 = Stopwatch.GetTimestamp(); + raycastTime = (t2 - t1) / 1_000_000L; + raycastHit = hitPos.HasValue; + raycastHitPos = hitPos.HasValue + ? new float[] { sp[0] + hitPos.Value * (ep[0] - sp[0]), sp[1] + hitPos.Value * (ep[1] - sp[1]), sp[2] + hitPos.Value * (ep[2] - sp[2]) } + : ep; + } + } + } + + private Tuple sphereCollider(float[] p) { + float radius = 1 + (float)random.NextDouble() * 10; + return Tuple.Create( + new SphereCollider(p, radius, SampleAreaModifications.SAMPLE_POLYAREA_TYPE_WATER, dynaMesh.config.walkableClimb), + GizmoFactory.sphere(p, radius)); + } + + private Tuple capsuleCollider(float[] p) { + float radius = 0.4f + (float)random.NextDouble() * 4f; + float[] a = new float[] { (1f - 2 * (float)random.NextDouble()), 0.01f + (float)random.NextDouble(), (1f - 2 * (float)random.NextDouble()) }; + vNormalize(a); + float len = 1f + (float)random.NextDouble() * 20f; + a[0] *= len; + a[1] *= len; + a[2] *= len; + float[] start = new float[] { p[0], p[1], p[2] }; + float[] end = new float[] { p[0] + a[0], p[1] + a[1], p[2] + a[2] }; + return Tuple.Create(new CapsuleCollider(start, end, radius, SampleAreaModifications.SAMPLE_POLYAREA_TYPE_WATER, + dynaMesh.config.walkableClimb), GizmoFactory.capsule(start, end, radius)); + } + + private Tuple boxCollider(float[] p) { + float[] extent = new float[] { 0.5f + (float)random.NextDouble() * 6f, 0.5f + (float)random.NextDouble() * 6f, + 0.5f + (float)random.NextDouble() * 6f }; + float[] forward = new float[] { (1f - 2 * (float)random.NextDouble()), 0, (1f - 2 * (float)random.NextDouble()) }; + float[] up = new float[] { (1f - 2 * (float)random.NextDouble()), 0.01f + (float)random.NextDouble(), (1f - 2 * (float)random.NextDouble()) }; + float[][] halfEdges = BoxCollider.getHalfEdges(up, forward, extent); + return Tuple.Create( + new BoxCollider(p, halfEdges, SampleAreaModifications.SAMPLE_POLYAREA_TYPE_WATER, dynaMesh.config.walkableClimb), + GizmoFactory.box(p, halfEdges)); + } + + private Tuple cylinderCollider(float[] p) { + float radius = 0.7f + (float)random.NextDouble() * 4f; + float[] a = new float[] { (1f - 2 * (float)random.NextDouble()), 0.01f + (float)random.NextDouble(), (1f - 2 * (float)random.NextDouble()) }; + vNormalize(a); + float len = 2f + (float)random.NextDouble() * 20f; + a[0] *= len; + a[1] *= len; + a[2] *= len; + float[] start = new float[] { p[0], p[1], p[2] }; + float[] end = new float[] { p[0] + a[0], p[1] + a[1], p[2] + a[2] }; + return Tuple.Create(new CylinderCollider(start, end, radius, SampleAreaModifications.SAMPLE_POLYAREA_TYPE_WATER, + dynaMesh.config.walkableClimb), GizmoFactory.cylinder(start, end, radius)); + } + + private Tuple compositeCollider(float[] p) { + float[] baseExtent = new float[] { 5, 3, 8 }; + float[] baseCenter = new float[] { p[0], p[1] + 3, p[2] }; + float[] baseUp = new float[] { 0, 1, 0 }; + float[] forward = new float[] { (1f - 2 * (float)random.NextDouble()), 0, (1f - 2 * (float)random.NextDouble()) }; + vNormalize(forward); + float[] side = DemoMath.vCross(forward, baseUp); + BoxCollider @base = new BoxCollider(baseCenter, BoxCollider.getHalfEdges(baseUp, forward, baseExtent), + SampleAreaModifications.SAMPLE_POLYAREA_TYPE_ROAD, dynaMesh.config.walkableClimb); + float[] roofExtent = new float[] { 4.5f, 4.5f, 8f }; + float[] rx = GLU.build_4x4_rotation_matrix(45, forward[0], forward[1], forward[2]); + float[] roofUp = mulMatrixVector(new float[3], rx, baseUp); + float[] roofCenter = new float[] { p[0], p[1] + 6, p[2] }; + BoxCollider roof = new BoxCollider(roofCenter, BoxCollider.getHalfEdges(roofUp, forward, roofExtent), + SampleAreaModifications.SAMPLE_POLYAREA_TYPE_ROAD, dynaMesh.config.walkableClimb); + float[] trunkStart = new float[] { baseCenter[0] - forward[0] * 15 + side[0] * 6, p[1], + baseCenter[2] - forward[2] * 15 + side[2] * 6 }; + float[] trunkEnd = new float[] { trunkStart[0], trunkStart[1] + 10, trunkStart[2] }; + CapsuleCollider trunk = new CapsuleCollider(trunkStart, trunkEnd, 0.5f, SampleAreaModifications.SAMPLE_POLYAREA_TYPE_ROAD, + dynaMesh.config.walkableClimb); + float[] crownCenter = new float[] { baseCenter[0] - forward[0] * 15 + side[0] * 6, p[1] + 10, + baseCenter[2] - forward[2] * 15 + side[2] * 6 }; + SphereCollider crown = new SphereCollider(crownCenter, 4f, SampleAreaModifications.SAMPLE_POLYAREA_TYPE_GRASS, + dynaMesh.config.walkableClimb); + CompositeCollider collider = new CompositeCollider(@base, roof, trunk, crown); + ColliderGizmo baseGizmo = GizmoFactory.box(baseCenter, BoxCollider.getHalfEdges(baseUp, forward, baseExtent)); + ColliderGizmo roofGizmo = GizmoFactory.box(roofCenter, BoxCollider.getHalfEdges(roofUp, forward, roofExtent)); + ColliderGizmo trunkGizmo = GizmoFactory.capsule(trunkStart, trunkEnd, 0.5f); + ColliderGizmo crownGizmo = GizmoFactory.sphere(crownCenter, 4f); + ColliderGizmo gizmo = GizmoFactory.composite(baseGizmo, roofGizmo, trunkGizmo, crownGizmo); + return Tuple.Create(collider, gizmo); + } + + private Tuple trimeshBridge(float[] p) { + return trimeshCollider(p, bridgeGeom); + } + + private Tuple trimeshHouse(float[] p) { + return trimeshCollider(p, houseGeom); + } + + private Tuple convexTrimesh(float[] p) { + float[] verts = transformVertices(p, convexGeom, 360); + ConvexTrimeshCollider collider = new ConvexTrimeshCollider(verts, convexGeom.faces, + SampleAreaModifications.SAMPLE_POLYAREA_TYPE_ROAD, dynaMesh.config.walkableClimb * 10); + return Tuple.Create(collider, GizmoFactory.trimesh(verts, convexGeom.faces)); + } + + private Tuple trimeshCollider(float[] p, DemoInputGeomProvider geom) { + float[] verts = transformVertices(p, geom, 0); + TrimeshCollider collider = new TrimeshCollider(verts, geom.faces, SampleAreaModifications.SAMPLE_POLYAREA_TYPE_ROAD, + dynaMesh.config.walkableClimb * 10); + return Tuple.Create(collider, GizmoFactory.trimesh(verts, geom.faces)); + } + + private float[] transformVertices(float[] p, DemoInputGeomProvider geom, float ax) { + float[] rx = GLU.build_4x4_rotation_matrix((float)random.NextDouble() * ax, 1, 0, 0); + float[] ry = GLU.build_4x4_rotation_matrix((float)random.NextDouble() * 360, 0, 1, 0); + float[] m = GLU.mul(rx, ry); + float[] verts = new float[geom.vertices.Length]; + float[] v = new float[3]; + float[] vr = new float[3]; + for (int i = 0; i < geom.vertices.Length; i += 3) { + v[0] = geom.vertices[i]; + v[1] = geom.vertices[i + 1]; + v[2] = geom.vertices[i + 2]; + mulMatrixVector(vr, m, v); + vr[0] += p[0]; + vr[1] += p[1] - 0.1f; + vr[2] += p[2]; + verts[i] = vr[0]; + verts[i + 1] = vr[1]; + verts[i + 2] = vr[2]; + } + return verts; + } + + private float[] mulMatrixVector(float[] resultvector, float[] matrix, float[] pvector) { + resultvector[0] = matrix[0] * pvector[0] + matrix[4] * pvector[1] + matrix[8] * pvector[2]; + resultvector[1] = matrix[1] * pvector[0] + matrix[5] * pvector[1] + matrix[9] * pvector[2]; + resultvector[2] = matrix[2] * pvector[0] + matrix[6] * pvector[1] + matrix[10] * pvector[2]; + return resultvector; + } + + public override void handleClickRay(float[] start, float[] dir, bool shift) { + if (mode == ToolMode.COLLIDERS) { + if (shift) { + foreach (var e in colliders) { + if (hit(start, dir, e.Value.bounds())) { + dynaMesh.removeCollider(e.Key); + colliders.Remove(e.Key); + colliderGizmos.Remove(e.Key); + break; + } + } + } + } + } + + private bool hit(float[] point, float[] dir, float[] bounds) { + float cx = 0.5f * (bounds[0] + bounds[3]); + float cy = 0.5f * (bounds[1] + bounds[4]); + float cz = 0.5f * (bounds[2] + bounds[5]); + float dx = 0.5f * (bounds[3] - bounds[0]); + float dy = 0.5f * (bounds[4] - bounds[1]); + float dz = 0.5f * (bounds[5] - bounds[2]); + float rSqr = dx * dx + dy * dy + dz * dz; + float mx = point[0] - cx; + float my = point[1] - cy; + float mz = point[2] - cz; + float c = mx * mx + my * my + mz * mz - rSqr; + if (c <= 0.0f) { + return true; + } + float b = mx * dir[0] + my * dir[1] + mz * dir[2]; + if (b > 0.0f) { + return false; + } + float disc = b * b - c; + return disc >= 0.0f; + } + + public override void handleRender(NavMeshRenderer renderer) { + if (mode == ToolMode.COLLIDERS) { + if (showColliders) { + colliderGizmos.Values.forEach(g => g.render(renderer.getDebugDraw())); + } + } + if (mode == ToolMode.RAYCAST) { + RecastDebugDraw dd = renderer.getDebugDraw(); + int startCol = duRGBA(128, 25, 0, 192); + int endCol = duRGBA(51, 102, 0, 129); + if (sposSet) { + drawAgent(dd, spos, startCol); + } + if (eposSet) { + drawAgent(dd, epos, endCol); + } + dd.depthMask(false); + if (raycastHitPos != null) { + int spathCol = raycastHit ? duRGBA(128, 32, 16, 220) : duRGBA(64, 128, 240, 220); + dd.begin(LINES, 2.0f); + dd.vertex(spos[0], spos[1] + 1.3f, spos[2], spathCol); + dd.vertex(raycastHitPos[0], raycastHitPos[1], raycastHitPos[2], spathCol); + dd.end(); + } + dd.depthMask(true); + } + } + + private void drawAgent(RecastDebugDraw dd, float[] pos, int col) { + float r = sample.getSettingsUI().getAgentRadius(); + float h = sample.getSettingsUI().getAgentHeight(); + float c = sample.getSettingsUI().getAgentMaxClimb(); + dd.depthMask(false); + // Agent dimensions. + dd.debugDrawCylinderWire(pos[0] - r, pos[1] + 0.02f, pos[2] - r, pos[0] + r, pos[1] + h, pos[2] + r, col, 2.0f); + dd.debugDrawCircle(pos[0], pos[1] + c, pos[2], r, duRGBA(0, 0, 0, 64), 1.0f); + int colb = duRGBA(0, 0, 0, 196); + dd.begin(LINES); + dd.vertex(pos[0], pos[1] - c, pos[2], colb); + dd.vertex(pos[0], pos[1] + c, pos[2], colb); + dd.vertex(pos[0] - r / 2, pos[1] + 0.02f, pos[2], colb); + dd.vertex(pos[0] + r / 2, pos[1] + 0.02f, pos[2], colb); + dd.vertex(pos[0], pos[1] + 0.02f, pos[2] - r / 2, colb); + dd.vertex(pos[0], pos[1] + 0.02f, pos[2] + r / 2, colb); + dd.end(); + dd.depthMask(true); + } + + public override void handleUpdate(float dt) { + if (dynaMesh != null) { + updateDynaMesh(); + } + } + + private void updateDynaMesh() { + long t = Stopwatch.GetTimestamp(); + try + { + bool updated = dynaMesh.update(executor).Result; + if (updated) { + buildTime = (Stopwatch.GetTimestamp() - t) / 1_000_000; + sample.update(null, dynaMesh.recastResults(), dynaMesh.navMesh()); + sample.setChanged(false); + } + } catch (Exception e) { + Console.WriteLine(e); + } + } + + public override void layout(IWindow ctx) { + + // nk_layout_row_dynamic(ctx, 18, 1); + // if (nk_option_label(ctx, "Build", mode == ToolMode.BUILD)) { + // mode = ToolMode.BUILD; + // } + // nk_layout_row_dynamic(ctx, 18, 1); + // if (nk_option_label(ctx, "Colliders", mode == ToolMode.COLLIDERS)) { + // mode = ToolMode.COLLIDERS; + // } + // nk_layout_row_dynamic(ctx, 18, 1); + // if (nk_option_label(ctx, "Raycast", mode == ToolMode.RAYCAST)) { + // mode = ToolMode.RAYCAST; + // } + // + // nk_layout_row_dynamic(ctx, 1, 1); + // nk_spacing(ctx, 1); + // if (mode == ToolMode.BUILD) { + // nk_layout_row_dynamic(ctx, 18, 1); + // if (nk_button_text(ctx, "Load Voxels...")) { + // load(); + // } + // if (dynaMesh != null) { + // nk_layout_row_dynamic(ctx, 18, 1); + // compression = nk_check_text(ctx, "Compression", compression); + // if (nk_button_text(ctx, "Save Voxels...")) { + // save(); + // } + // } + // nk_layout_row_dynamic(ctx, 1, 1); + // nk_spacing(ctx, 1); + // nk_layout_row_dynamic(ctx, 18, 1); + // nk_label(ctx, "Rasterization", NK_TEXT_ALIGN_LEFT); + // nk_layout_row_dynamic(ctx, 18, 2); + // nk_label(ctx, "Cell Size", NK_TEXT_ALIGN_LEFT); + // nk_label(ctx, string.format("%.2f", cellSize[0]), NK_TEXT_ALIGN_RIGHT); + // nk_layout_row_dynamic(ctx, 1, 1); + // nk_spacing(ctx, 1); + // nk_layout_row_dynamic(ctx, 18, 1); + // nk_label(ctx, "Agent", NK_TEXT_ALIGN_LEFT); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_float(ctx, "Height", 0f, walkableHeight, 5f, 0.01f, 0.01f); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_float(ctx, "Radius", 0f, walkableRadius, 10f, 0.01f, 0.01f); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_float(ctx, "Max Climb", 0f, walkableClimb, 10f, 0.01f, 0.01f); + // nk_layout_row_dynamic(ctx, 18, 2); + // nk_label(ctx, "Max Slope", NK_TEXT_ALIGN_LEFT); + // nk_label(ctx, string.format("%.0f", walkableSlopeAngle[0]), NK_TEXT_ALIGN_RIGHT); + // + // nk_layout_row_dynamic(ctx, 1, 1); + // nk_spacing(ctx, 1); + // nk_layout_row_dynamic(ctx, 18, 1); + // nk_label(ctx, "Partitioning", NK_TEXT_ALIGN_LEFT); + // partitioning = NuklearUIHelper.nk_radio(ctx, PartitionType.values(), partitioning, + // p => p.name().substring(0, 1) + p.name().substring(1).toLowerCase()); + // + // nk_layout_row_dynamic(ctx, 1, 1); + // nk_spacing(ctx, 1); + // nk_layout_row_dynamic(ctx, 18, 1); + // nk_label(ctx, "Filtering", NK_TEXT_ALIGN_LEFT); + // nk_layout_row_dynamic(ctx, 18, 1); + // filterLowHangingObstacles = nk_option_text(ctx, "Low Hanging Obstacles", filterLowHangingObstacles); + // nk_layout_row_dynamic(ctx, 18, 1); + // filterLedgeSpans = nk_option_text(ctx, "Ledge Spans", filterLedgeSpans); + // nk_layout_row_dynamic(ctx, 18, 1); + // filterWalkableLowHeightSpans = nk_option_text(ctx, "Walkable Low Height Spans", filterWalkableLowHeightSpans); + // + // nk_layout_row_dynamic(ctx, 1, 1); + // nk_spacing(ctx, 1); + // nk_layout_row_dynamic(ctx, 18, 1); + // nk_label(ctx, "Region", NK_TEXT_ALIGN_LEFT); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_float(ctx, "Min Region Size", 0, minRegionArea, 150, 0.1f, 0.1f); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_float(ctx, "Merged Region Size", 0, regionMergeSize, 400, 0.1f, 0.1f); + // + // nk_layout_row_dynamic(ctx, 1, 1); + // nk_spacing(ctx, 1); + // nk_layout_row_dynamic(ctx, 18, 1); + // nk_label(ctx, "Polygonization", NK_TEXT_ALIGN_LEFT); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_float(ctx, "Max Edge Length", 0f, maxEdgeLen, 50f, 0.1f, 0.1f); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_float(ctx, "Max Edge Error", 0.1f, maxSimplificationError, 10f, 0.1f, 0.1f); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_int(ctx, "Verts Per Poly", 3, vertsPerPoly, 12, 1, 1); + // + // nk_layout_row_dynamic(ctx, 1, 1); + // nk_spacing(ctx, 1); + // nk_layout_row_dynamic(ctx, 18, 1); + // nk_label(ctx, "Detail Mesh", NK_TEXT_ALIGN_LEFT); + // nk_layout_row_dynamic(ctx, 20, 1); + // buildDetailMesh = nk_check_text(ctx, "Enable", buildDetailMesh); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_float(ctx, "Sample Distance", 0f, detailSampleDist, 16f, 0.1f, 0.1f); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_float(ctx, "Max Sample Error", 0f, detailSampleMaxError, 16f, 0.1f, 0.1f); + // nk_layout_row_dynamic(ctx, 1, 1); + // nk_spacing(ctx, 1); + // nk_layout_row_dynamic(ctx, 20, 1); + // if (nk_button_text(ctx, "Build")) { + // if (dynaMesh != null) { + // buildDynaMesh(); + // sample.setChanged(false); + // } + // } + // } + // if (mode == ToolMode.COLLIDERS) { + // nk_layout_row_dynamic(ctx, 1, 1); + // nk_spacing(ctx, 1); + // nk_layout_row_dynamic(ctx, 18, 1); + // nk_label(ctx, "Colliders", NK_TEXT_ALIGN_LEFT); + // nk_layout_row_dynamic(ctx, 20, 1); + // showColliders = nk_check_text(ctx, "Show", showColliders); + // nk_layout_row_dynamic(ctx, 20, 1); + // if (nk_option_label(ctx, "Sphere", colliderShape == ColliderShape.SPHERE)) { + // colliderShape = ColliderShape.SPHERE; + // } + // nk_layout_row_dynamic(ctx, 18, 1); + // if (nk_option_label(ctx, "Capsule", colliderShape == ColliderShape.CAPSULE)) { + // colliderShape = ColliderShape.CAPSULE; + // } + // nk_layout_row_dynamic(ctx, 18, 1); + // if (nk_option_label(ctx, "Box", colliderShape == ColliderShape.BOX)) { + // colliderShape = ColliderShape.BOX; + // } + // nk_layout_row_dynamic(ctx, 18, 1); + // if (nk_option_label(ctx, "Cylinder", colliderShape == ColliderShape.CYLINDER)) { + // colliderShape = ColliderShape.CYLINDER; + // } + // nk_layout_row_dynamic(ctx, 18, 1); + // if (nk_option_label(ctx, "Composite", colliderShape == ColliderShape.COMPOSITE)) { + // colliderShape = ColliderShape.COMPOSITE; + // } + // nk_layout_row_dynamic(ctx, 18, 1); + // if (nk_option_label(ctx, "Convex Trimesh", colliderShape == ColliderShape.CONVEX)) { + // colliderShape = ColliderShape.CONVEX; + // } + // nk_layout_row_dynamic(ctx, 18, 1); + // if (nk_option_label(ctx, "Trimesh Bridge", colliderShape == ColliderShape.TRIMESH_BRIDGE)) { + // colliderShape = ColliderShape.TRIMESH_BRIDGE; + // } + // nk_layout_row_dynamic(ctx, 18, 1); + // if (nk_option_label(ctx, "Trimesh House", colliderShape == ColliderShape.TRIMESH_HOUSE)) { + // colliderShape = ColliderShape.TRIMESH_HOUSE; + // } + // } + // nk_layout_row_dynamic(ctx, 2, 1); + // nk_spacing(ctx, 1); + // nk_layout_row_dynamic(ctx, 18, 1); + // if (mode == ToolMode.RAYCAST) { + // nk_label(ctx, string.format("Raycast Time: %d ms", raycastTime), NK_TEXT_ALIGN_LEFT); + // if (sposSet) { + // nk_layout_row_dynamic(ctx, 18, 1); + // nk_label(ctx, string.format("Start: %.3f, %.3f, %.3f", spos[0], spos[1] + 1.3f, spos[2]), NK_TEXT_ALIGN_LEFT); + // } + // if (eposSet) { + // nk_layout_row_dynamic(ctx, 18, 1); + // nk_label(ctx, string.format("End: %.3f, %.3f, %.3f", epos[0], epos[1] + 1.3f, epos[2]), NK_TEXT_ALIGN_LEFT); + // } + // if (raycastHit) { + // nk_layout_row_dynamic(ctx, 18, 1); + // nk_label(ctx, string.format("Hit: %.3f, %.3f, %.3f", raycastHitPos[0], raycastHitPos[1], raycastHitPos[2]), + // NK_TEXT_ALIGN_LEFT); + // } + // } else { + // nk_label(ctx, string.format("Build Time: %d ms", buildTime), NK_TEXT_ALIGN_LEFT); + // } + + } + + private void load() { + // try (MemoryStack stack = stackPush()) { + // PointerBuffer aFilterPatterns = stack.mallocPointer(1); + // aFilterPatterns.put(stack.UTF8("*.voxels")); + // aFilterPatterns.flip(); + // string filename = TinyFileDialogs.tinyfd_openFileDialog("Open Voxel File", "", aFilterPatterns, "Voxel File", false); + // if (filename != null) { + // load(filename); + // } + // } + + } + + private void load(string filename) { + // File file = new File(filename); + // if (file.exists()) { + // VoxelFileReader reader = new VoxelFileReader(); + // try (FileInputStream fis = new FileInputStream(file)) { + // VoxelFile voxelFile = reader.read(fis); + // dynaMesh = new DynamicNavMesh(voxelFile); + // dynaMesh.config.keepIntermediateResults = true; + // updateUI(); + // buildDynaMesh(); + // colliders.clear(); + // } catch (Exception e) { + // Console.WriteLine(e); + // dynaMesh = null; + // } + // } + } + + private void save() { + // try (MemoryStack stack = stackPush()) { + // PointerBuffer aFilterPatterns = stack.mallocPointer(1); + // aFilterPatterns.put(stack.UTF8("*.voxels")); + // aFilterPatterns.flip(); + // string filename = TinyFileDialogs.tinyfd_saveFileDialog("Save Voxel File", "", aFilterPatterns, "Voxel File"); + // if (filename != null) { + // save(filename); + // } + // } + + } + + private void save(string filename) { + // File file = new File(filename); + // try (FileOutputStream fos = new FileOutputStream(file)) { + // VoxelFile voxelFile = VoxelFile.from(dynaMesh); + // VoxelFileWriter writer = new VoxelFileWriter(); + // writer.write(fos, voxelFile, compression); + // } catch (Exception e) { + // Console.WriteLine(e); + // } + + } + + private void buildDynaMesh() { + configDynaMesh(); + long t = Stopwatch.GetTimestamp(); + try + { + var _ = dynaMesh.build(executor).Result; + } catch (Exception e) { + Console.WriteLine(e); + } + buildTime = (Stopwatch.GetTimestamp() - t) / 1_000_000; + sample.update(null, dynaMesh.recastResults(), dynaMesh.navMesh()); + } + + private void configDynaMesh() { + dynaMesh.config.partitionType = partitioning; + dynaMesh.config.walkableHeight = walkableHeight[0]; + dynaMesh.config.walkableSlopeAngle = walkableSlopeAngle[0]; + dynaMesh.config.walkableRadius = walkableRadius[0]; + dynaMesh.config.walkableClimb = walkableClimb[0]; + dynaMesh.config.filterLowHangingObstacles = filterLowHangingObstacles; + dynaMesh.config.filterLedgeSpans = filterLedgeSpans; + dynaMesh.config.filterWalkableLowHeightSpans = filterWalkableLowHeightSpans; + dynaMesh.config.minRegionArea = minRegionArea[0]; + dynaMesh.config.regionMergeArea = regionMergeSize[0]; + dynaMesh.config.maxEdgeLen = maxEdgeLen[0]; + dynaMesh.config.maxSimplificationError = maxSimplificationError[0]; + dynaMesh.config.vertsPerPoly = vertsPerPoly[0]; + dynaMesh.config.buildDetailMesh = buildDetailMesh; + dynaMesh.config.detailSampleDistance = detailSampleDist[0]; + dynaMesh.config.detailSampleMaxError = detailSampleMaxError[0]; + } + + private void updateUI() { + cellSize[0] = dynaMesh.config.cellSize; + partitioning = dynaMesh.config.partitionType; + walkableHeight[0] = dynaMesh.config.walkableHeight; + walkableSlopeAngle[0] = dynaMesh.config.walkableSlopeAngle; + walkableRadius[0] = dynaMesh.config.walkableRadius; + walkableClimb[0] = dynaMesh.config.walkableClimb; + minRegionArea[0] = dynaMesh.config.minRegionArea; + regionMergeSize[0] = dynaMesh.config.regionMergeArea; + maxEdgeLen[0] = dynaMesh.config.maxEdgeLen; + maxSimplificationError[0] = dynaMesh.config.maxSimplificationError; + vertsPerPoly[0] = dynaMesh.config.vertsPerPoly; + buildDetailMesh = dynaMesh.config.buildDetailMesh; + detailSampleDist[0] = dynaMesh.config.detailSampleDistance; + detailSampleMaxError[0] = dynaMesh.config.detailSampleMaxError; + filterLowHangingObstacles = dynaMesh.config.filterLowHangingObstacles; + filterLedgeSpans = dynaMesh.config.filterLedgeSpans; + filterWalkableLowHeightSpans = dynaMesh.config.filterWalkableLowHeightSpans; + } + + public override string getName() { + return "Dynamic Updates"; + } + +} diff --git a/src/DotRecast.Recast.Demo/Tools/Gizmos/BoxGizmo.cs b/src/DotRecast.Recast.Demo/Tools/Gizmos/BoxGizmo.cs new file mode 100644 index 0000000..5316fd0 --- /dev/null +++ b/src/DotRecast.Recast.Demo/Tools/Gizmos/BoxGizmo.cs @@ -0,0 +1,68 @@ +using DotRecast.Detour.Dynamic.Colliders; +using DotRecast.Recast.Demo.Draw; + +namespace DotRecast.Recast.Demo.Tools.Gizmos; + + +public class BoxGizmo : ColliderGizmo { + private static readonly int[] TRIANLGES = { 0, 1, 2, 0, 2, 3, 4, 7, 6, 4, 6, 5, 0, 4, 5, 0, 5, 1, 1, 5, 6, 1, 6, 2, + 2, 6, 7, 2, 7, 3, 4, 0, 3, 4, 3, 7 }; + private static readonly float[][] VERTS = { + new[] { -1f, -1f, -1f, }, + new[] { 1f, -1f, -1f, }, + new[] { 1f, -1f, 1f, }, + new[] { -1f, -1f, 1f, }, + new[] { -1f, 1f, -1f, }, + new[] { 1f, 1f, -1f, }, + new[] { 1f, 1f, 1f, }, + new[] { -1f, 1f, 1f, }, + }; + + private readonly float[] vertices = new float[8 * 3]; + private readonly float[] center; + private readonly float[][] halfEdges; + + public BoxGizmo(float[] center, float[] extent, float[] forward, float[] up) : + this(center, BoxCollider.getHalfEdges(up, forward, extent)) + { + } + + public BoxGizmo(float[] center, float[][] halfEdges) { + this.center = center; + this.halfEdges = halfEdges; + for (int i = 0; i < 8; ++i) { + float s0 = (i & 1) != 0 ? 1f : -1f; + float s1 = (i & 2) != 0 ? 1f : -1f; + float s2 = (i & 4) != 0 ? 1f : -1f; + vertices[i * 3 + 0] = center[0] + s0 * halfEdges[0][0] + s1 * halfEdges[1][0] + s2 * halfEdges[2][0]; + vertices[i * 3 + 1] = center[1] + s0 * halfEdges[0][1] + s1 * halfEdges[1][1] + s2 * halfEdges[2][1]; + vertices[i * 3 + 2] = center[2] + s0 * halfEdges[0][2] + s1 * halfEdges[1][2] + s2 * halfEdges[2][2]; + } + } + + public void render(RecastDebugDraw debugDraw) { + float[] trX = new float[] { halfEdges[0][0], halfEdges[1][0], halfEdges[2][0] }; + float[] trY = new float[] { halfEdges[0][1], halfEdges[1][1], halfEdges[2][1] }; + float[] trZ = new float[] { halfEdges[0][2], halfEdges[1][2], halfEdges[2][2] }; + float[] vertices = new float[8 * 3]; + for (int i = 0; i < 8; i++) { + vertices[i * 3 + 0] = RecastVectors.dot(VERTS[i], trX) + center[0]; + vertices[i * 3 + 1] = RecastVectors.dot(VERTS[i], trY) + center[1]; + vertices[i * 3 + 2] = RecastVectors.dot(VERTS[i], trZ) + center[2]; + } + debugDraw.begin(DebugDrawPrimitives.TRIS); + for (int i = 0; i < 12; i++) { + int col = DebugDraw.duRGBA(200, 200, 50, 160); + if (i == 4 || i == 5 || i == 8 || i == 9) { + col = DebugDraw.duRGBA(160, 160, 40, 160); + } else if (i > 4) { + col = DebugDraw.duRGBA(120, 120, 30, 160); + } + for (int j = 0; j < 3; j++) { + int v = TRIANLGES[i * 3 + j] * 3; + debugDraw.vertex(vertices[v], vertices[v + 1], vertices[v + 2], col); + } + } + debugDraw.end(); + } +} diff --git a/src/DotRecast.Recast.Demo/Tools/Gizmos/CapsuleGizmo.cs b/src/DotRecast.Recast.Demo/Tools/Gizmos/CapsuleGizmo.cs new file mode 100644 index 0000000..ece480c --- /dev/null +++ b/src/DotRecast.Recast.Demo/Tools/Gizmos/CapsuleGizmo.cs @@ -0,0 +1,80 @@ +using DotRecast.Recast.Demo.Draw; + +using static DotRecast.Recast.RecastVectors; +using static DotRecast.Detour.DetourCommon; +using static DotRecast.Recast.Demo.Tools.Gizmos.GizmoHelper; + +namespace DotRecast.Recast.Demo.Tools.Gizmos; + + +public class CapsuleGizmo : ColliderGizmo { + private readonly float[] vertices; + private readonly int[] triangles; + private readonly float[] center; + private readonly float[] gradient; + + public CapsuleGizmo(float[] start, float[] end, float radius) { + center = new float[] { 0.5f * (start[0] + end[0]), 0.5f * (start[1] + end[1]), + 0.5f * (start[2] + end[2]) }; + float[] axis = new float[] { end[0] - start[0], end[1] - start[1], end[2] - start[2] }; + float[][] normals = new float[3][]; + normals[1] = new float[] { end[0] - start[0], end[1] - start[1], end[2] - start[2] }; + normalize(normals[1]); + normals[0] = getSideVector(axis); + normals[2] = new float[3]; + cross(normals[2], normals[0], normals[1]); + normalize(normals[2]); + triangles = generateSphericalTriangles(); + float[] trX = new float[] { normals[0][0], normals[1][0], normals[2][0] }; + float[] trY = new float[] { normals[0][1], normals[1][1], normals[2][1] }; + float[] trZ = new float[] { normals[0][2], normals[1][2], normals[2][2] }; + float[] spVertices = generateSphericalVertices(); + float halfLength = 0.5f * vLen(axis); + vertices = new float[spVertices.Length]; + gradient = new float[spVertices.Length / 3]; + float[] v = new float[3]; + for (int i = 0; i < spVertices.Length; i += 3) { + float offset = (i >= spVertices.Length / 2) ? -halfLength : halfLength; + float x = radius * spVertices[i]; + float y = radius * spVertices[i + 1] + offset; + float z = radius * spVertices[i + 2]; + vertices[i] = x * trX[0] + y * trX[1] + z * trX[2] + center[0]; + vertices[i + 1] = x * trY[0] + y * trY[1] + z * trY[2] + center[1]; + vertices[i + 2] = x * trZ[0] + y * trZ[1] + z * trZ[2] + center[2]; + v[0] = vertices[i] - center[0]; + v[1] = vertices[i + 1] - center[1]; + v[2] = vertices[i + 2] - center[2]; + normalize(v); + gradient[i / 3] = clamp(0.57735026f * (v[0] + v[1] + v[2]), -1, 1); + } + + } + + private float[] getSideVector(float[] axis) { + float[] side = { 1, 0, 0 }; + if (axis[0] > 0.8) { + side = new float[] { 0, 0, 1 }; + } + float[] forward = new float[3]; + cross(forward, side, axis); + cross(side, axis, forward); + normalize(side); + return side; + } + + public void render(RecastDebugDraw debugDraw) { + + debugDraw.begin(DebugDrawPrimitives.TRIS); + for (int i = 0; i < triangles.Length; i += 3) { + for (int j = 0; j < 3; j++) { + int v = triangles[i + j] * 3; + float c = gradient[triangles[i + j]]; + int col = DebugDraw.duLerpCol(DebugDraw.duRGBA(32, 32, 0, 160), DebugDraw.duRGBA(220, 220, 0, 160), + (int) (127 * (1 + c))); + debugDraw.vertex(vertices[v], vertices[v + 1], vertices[v + 2], col); + } + } + debugDraw.end(); + } + +} \ No newline at end of file diff --git a/src/DotRecast.Recast.Demo/Tools/Gizmos/ColliderGizmo.cs b/src/DotRecast.Recast.Demo/Tools/Gizmos/ColliderGizmo.cs new file mode 100644 index 0000000..2b0f3f6 --- /dev/null +++ b/src/DotRecast.Recast.Demo/Tools/Gizmos/ColliderGizmo.cs @@ -0,0 +1,8 @@ +using DotRecast.Recast.Demo.Draw; + +namespace DotRecast.Recast.Demo.Tools.Gizmos; + +public interface ColliderGizmo { + + void render(RecastDebugDraw debugDraw); +} diff --git a/src/DotRecast.Recast.Demo/Tools/Gizmos/CompositeGizmo.cs b/src/DotRecast.Recast.Demo/Tools/Gizmos/CompositeGizmo.cs new file mode 100644 index 0000000..0bcb805 --- /dev/null +++ b/src/DotRecast.Recast.Demo/Tools/Gizmos/CompositeGizmo.cs @@ -0,0 +1,17 @@ +using DotRecast.Core; +using DotRecast.Recast.Demo.Draw; + +namespace DotRecast.Recast.Demo.Tools.Gizmos; + +public class CompositeGizmo : ColliderGizmo { + + private readonly ColliderGizmo[] gizmos; + + public CompositeGizmo(params ColliderGizmo[] gizmos) { + this.gizmos = gizmos; + } + + public void render(RecastDebugDraw debugDraw) { + gizmos.forEach(g => g.render(debugDraw)); + } +} \ No newline at end of file diff --git a/src/DotRecast.Recast.Demo/Tools/Gizmos/CylinderGizmo.cs b/src/DotRecast.Recast.Demo/Tools/Gizmos/CylinderGizmo.cs new file mode 100644 index 0000000..c79938a --- /dev/null +++ b/src/DotRecast.Recast.Demo/Tools/Gizmos/CylinderGizmo.cs @@ -0,0 +1,82 @@ +using DotRecast.Recast.Demo.Draw; +using static DotRecast.Recast.RecastVectors; +using static DotRecast.Detour.DetourCommon; +using static DotRecast.Recast.Demo.Tools.Gizmos.GizmoHelper; + + +namespace DotRecast.Recast.Demo.Tools.Gizmos; + + +public class CylinderGizmo : ColliderGizmo { + private readonly float[] vertices; + private readonly int[] triangles; + private readonly float[] center; + private readonly float[] gradient; + + public CylinderGizmo(float[] start, float[] end, float radius) { + center = new float[] { 0.5f * (start[0] + end[0]), 0.5f * (start[1] + end[1]), + 0.5f * (start[2] + end[2]) }; + float[] axis = new float[] { end[0] - start[0], end[1] - start[1], end[2] - start[2] }; + float[][] normals = new float[3][]; + normals[1] = new float[] { end[0] - start[0], end[1] - start[1], end[2] - start[2] }; + normalize(normals[1]); + normals[0] = getSideVector(axis); + normals[2] = new float[3]; + cross(normals[2], normals[0], normals[1]); + normalize(normals[2]); + triangles = generateCylindricalTriangles(); + float[] trX = new float[] { normals[0][0], normals[1][0], normals[2][0] }; + float[] trY = new float[] { normals[0][1], normals[1][1], normals[2][1] }; + float[] trZ = new float[] { normals[0][2], normals[1][2], normals[2][2] }; + vertices = generateCylindricalVertices(); + float halfLength = 0.5f * vLen(axis); + gradient = new float[vertices.Length / 3]; + float[] v = new float[3]; + for (int i = 0; i < vertices.Length; i += 3) { + float offset = (i >= vertices.Length / 2) ? -halfLength : halfLength; + float x = radius * vertices[i]; + float y = vertices[i + 1] + offset; + float z = radius * vertices[i + 2]; + vertices[i] = x * trX[0] + y * trX[1] + z * trX[2] + center[0]; + vertices[i + 1] = x * trY[0] + y * trY[1] + z * trY[2] + center[1]; + vertices[i + 2] = x * trZ[0] + y * trZ[1] + z * trZ[2] + center[2]; + if (i < vertices.Length / 4 || i >= 3 * vertices.Length / 4) { + gradient[i / 3] = 1; + } else { + v[0] = vertices[i] - center[0]; + v[1] = vertices[i + 1] - center[1]; + v[2] = vertices[i + 2] - center[2]; + normalize(v); + gradient[i / 3] = clamp(0.57735026f * (v[0] + v[1] + v[2]), -1, 1); + } + } + } + + private float[] getSideVector(float[] axis) { + float[] side = { 1, 0, 0 }; + if (axis[0] > 0.8) { + side = new float[] { 0, 0, 1 }; + } + float[] forward = new float[3]; + cross(forward, side, axis); + cross(side, axis, forward); + normalize(side); + return side; + } + + public void render(RecastDebugDraw debugDraw) { + + debugDraw.begin(DebugDrawPrimitives.TRIS); + for (int i = 0; i < triangles.Length; i += 3) { + for (int j = 0; j < 3; j++) { + int v = triangles[i + j] * 3; + float c = gradient[triangles[i + j]]; + int col = DebugDraw.duLerpCol(DebugDraw.duRGBA(32, 32, 0, 160), DebugDraw.duRGBA(220, 220, 0, 160), + (int) (127 * (1 + c))); + debugDraw.vertex(vertices[v], vertices[v + 1], vertices[v + 2], col); + } + } + debugDraw.end(); + } + +} diff --git a/src/DotRecast.Recast.Demo/Tools/Gizmos/GizmoFactory.cs b/src/DotRecast.Recast.Demo/Tools/Gizmos/GizmoFactory.cs new file mode 100644 index 0000000..644f053 --- /dev/null +++ b/src/DotRecast.Recast.Demo/Tools/Gizmos/GizmoFactory.cs @@ -0,0 +1,28 @@ +namespace DotRecast.Recast.Demo.Tools.Gizmos; + +public static class GizmoFactory +{ + public static ColliderGizmo box(float[] center, float[][] halfEdges) { + return new BoxGizmo(center, halfEdges); + } + + public static ColliderGizmo sphere(float[] center, float radius) { + return new SphereGizmo(center, radius); + } + + public static ColliderGizmo capsule(float[] start, float[] end, float radius) { + return new CapsuleGizmo(start, end, radius); + } + + public static ColliderGizmo cylinder(float[] start, float[] end, float radius) { + return new CylinderGizmo(start, end, radius); + } + + public static ColliderGizmo trimesh(float[] verts, int[] faces) { + return new TrimeshGizmo(verts, faces); + } + + public static ColliderGizmo composite(params ColliderGizmo[] gizmos) { + return new CompositeGizmo(gizmos); + } +} \ No newline at end of file diff --git a/src/DotRecast.Recast.Demo/Tools/Gizmos/GizmoHelper.cs b/src/DotRecast.Recast.Demo/Tools/Gizmos/GizmoHelper.cs new file mode 100644 index 0000000..033d44f --- /dev/null +++ b/src/DotRecast.Recast.Demo/Tools/Gizmos/GizmoHelper.cs @@ -0,0 +1,162 @@ +using System; +using DotRecast.Recast.Demo.Draw; +using static DotRecast.Detour.DetourCommon; + +namespace DotRecast.Recast.Demo.Tools.Gizmos; + +public class GizmoHelper { + + private static readonly int SEGMENTS = 16; + private static readonly int RINGS = 8; + + private static float[] sphericalVertices; + + public static float[] generateSphericalVertices() { + if (sphericalVertices == null) { + sphericalVertices = generateSphericalVertices(SEGMENTS, RINGS); + } + return sphericalVertices; + } + + private static float[] generateSphericalVertices(int segments, int rings) { + float[] vertices = new float[6 + 3 * (segments + 1) * (rings + 1)]; + // top + int vi = 0; + vertices[vi++] = 0; + vertices[vi++] = 1; + vertices[vi++] = 0; + for (int r = 0; r <= rings; r++) { + double theta = Math.PI * (r + 1) / (rings + 2); + vi = generateRingVertices(segments, vertices, vi, theta); + } + // bottom + vertices[vi++] = 0; + vertices[vi++] = -1; + vertices[vi++] = 0; + return vertices; + } + + public static float[] generateCylindricalVertices() { + return generateCylindricalVertices(SEGMENTS); + } + + private static float[] generateCylindricalVertices(int segments) { + float[] vertices = new float[3 * (segments + 1) * 4]; + int vi = 0; + for (int r = 0; r < 4; r++) { + vi = generateRingVertices(segments, vertices, vi, Math.PI * 0.5); + } + return vertices; + + } + + private static int generateRingVertices(int segments, float[] vertices, int vi, double theta) { + double cosTheta = Math.Cos(theta); + double sinTheta = Math.Sin(theta); + for (int p = 0; p <= segments; p++) { + double phi = 2 * Math.PI * p / segments; + double cosPhi = Math.Cos(phi); + double sinPhi = Math.Sin(phi); + vertices[vi++] = (float) (sinTheta * cosPhi); + vertices[vi++] = (float) cosTheta; + vertices[vi++] = (float) (sinTheta * sinPhi); + } + return vi; + } + + public static int[] generateSphericalTriangles() { + return generateSphericalTriangles(SEGMENTS, RINGS); + } + + private static int[] generateSphericalTriangles(int segments, int rings) { + int[] triangles = new int[6 * (segments + rings * (segments + 1))]; + int ti = generateSphereUpperCapTriangles(segments, triangles, 0); + ti = generateRingTriangles(segments, rings, triangles, 1, ti); + generateSphereLowerCapTriangles(segments, rings, triangles, ti); + return triangles; + } + + public static int generateRingTriangles(int segments, int rings, int[] triangles, int vertexOffset, int ti) { + for (int r = 0; r < rings; r++) { + for (int p = 0; p < segments; p++) { + int current = p + r * (segments + 1) + vertexOffset; + int next = p + 1 + r * (segments + 1) + vertexOffset; + int currentBottom = p + (r + 1) * (segments + 1) + vertexOffset; + int nextBottom = p + 1 + (r + 1) * (segments + 1) + vertexOffset; + triangles[ti++] = current; + triangles[ti++] = next; + triangles[ti++] = nextBottom; + triangles[ti++] = current; + triangles[ti++] = nextBottom; + triangles[ti++] = currentBottom; + } + } + return ti; + } + + private static int generateSphereUpperCapTriangles(int segments, int[] triangles, int ti) { + for (int p = 0; p < segments; p++) { + triangles[ti++] = p + 2; + triangles[ti++] = p + 1; + triangles[ti++] = 0; + } + return ti; + } + + private static void generateSphereLowerCapTriangles(int segments, int rings, int[] triangles, int ti) { + int lastVertex = 1 + (segments + 1) * (rings + 1); + for (int p = 0; p < segments; p++) { + triangles[ti++] = lastVertex; + triangles[ti++] = lastVertex - (p + 2); + triangles[ti++] = lastVertex - (p + 1); + } + } + + public static int[] generateCylindricalTriangles() { + return generateCylindricalTriangles(SEGMENTS); + } + + private static int[] generateCylindricalTriangles(int segments) { + int circleTriangles = segments - 2; + int[] triangles = new int[6 * (circleTriangles + (segments + 1))]; + int vi = 0; + int ti = generateCircleTriangles(segments, triangles, vi, 0, false); + ti = generateRingTriangles(segments, 1, triangles, segments + 1, ti); + int vertexCount = (segments + 1) * 4; + ti = generateCircleTriangles(segments, triangles, vertexCount - segments, ti, true); + return triangles; + } + + private static int generateCircleTriangles(int segments, int[] triangles, int vi, int ti, bool invert) { + for (int p = 0; p < segments - 2; p++) { + if (invert) { + triangles[ti++] = vi; + triangles[ti++] = vi + p + 1; + triangles[ti++] = vi + p + 2; + } else { + triangles[ti++] = vi + p + 2; + triangles[ti++] = vi + p + 1; + triangles[ti++] = vi; + } + } + return ti; + } + + public static int getColorByNormal(float[] vertices, int v0, int v1, int v2) { + float[] e0 = new float[3], e1 = new float[3]; + float[] normal = new float[3]; + for (int j = 0; j < 3; ++j) { + e0[j] = vertices[v1 + j] - vertices[v0 + j]; + e1[j] = vertices[v2 + j] - vertices[v0 + j]; + } + normal[0] = e0[1] * e1[2] - e0[2] * e1[1]; + normal[1] = e0[2] * e1[0] - e0[0] * e1[2]; + normal[2] = e0[0] * e1[1] - e0[1] * e1[0]; + RecastVectors.normalize(normal); + float c = clamp(0.57735026f * (normal[0] + normal[1] + normal[2]), -1, 1); + int col = DebugDraw.duLerpCol(DebugDraw.duRGBA(32, 32, 0, 160), DebugDraw.duRGBA(220, 220, 0, 160), + (int) (127 * (1 + c))); + return col; + } + +} diff --git a/src/DotRecast.Recast.Demo/Tools/Gizmos/SphereGizmo.cs b/src/DotRecast.Recast.Demo/Tools/Gizmos/SphereGizmo.cs new file mode 100644 index 0000000..69ba71e --- /dev/null +++ b/src/DotRecast.Recast.Demo/Tools/Gizmos/SphereGizmo.cs @@ -0,0 +1,38 @@ +using DotRecast.Recast.Demo.Draw; + +using static DotRecast.Detour.DetourCommon; +using static DotRecast.Recast.Demo.Tools.Gizmos.GizmoHelper; + + +namespace DotRecast.Recast.Demo.Tools.Gizmos; + + +public class SphereGizmo : ColliderGizmo { + private readonly float[] vertices; + private readonly int[] triangles; + private readonly float radius; + private readonly float[] center; + + public SphereGizmo(float[] center, float radius) { + this.center = center; + this.radius = radius; + vertices = generateSphericalVertices(); + triangles = generateSphericalTriangles(); + } + + public void render(RecastDebugDraw debugDraw) { + debugDraw.begin(DebugDrawPrimitives.TRIS); + for (int i = 0; i < triangles.Length; i += 3) { + for (int j = 0; j < 3; j++) { + int v = triangles[i + j] * 3; + float c = clamp(0.57735026f * (vertices[v] + vertices[v + 1] + vertices[v + 2]), -1, 1); + int col = DebugDraw.duLerpCol(DebugDraw.duRGBA(32, 32, 0, 160), DebugDraw.duRGBA(220, 220, 0, 160), + (int) (127 * (1 + c))); + debugDraw.vertex(radius * vertices[v] + center[0], radius * vertices[v + 1] + center[1], + radius * vertices[v + 2] + center[2], col); + } + } + debugDraw.end(); + } + +} \ No newline at end of file diff --git a/src/DotRecast.Recast.Demo/Tools/Gizmos/TrimeshGizmo.cs b/src/DotRecast.Recast.Demo/Tools/Gizmos/TrimeshGizmo.cs new file mode 100644 index 0000000..1cd32a6 --- /dev/null +++ b/src/DotRecast.Recast.Demo/Tools/Gizmos/TrimeshGizmo.cs @@ -0,0 +1,28 @@ +using DotRecast.Recast.Demo.Draw; + +namespace DotRecast.Recast.Demo.Tools.Gizmos; + +public class TrimeshGizmo : ColliderGizmo { + private readonly float[] vertices; + private readonly int[] triangles; + + public TrimeshGizmo(float[] vertices, int[] triangles) { + this.vertices = vertices; + this.triangles = triangles; + } + + public void render(RecastDebugDraw debugDraw) { + debugDraw.begin(DebugDrawPrimitives.TRIS); + for (int i = 0; i < triangles.Length; i += 3) { + int v0 = 3 * triangles[i]; + int v1 = 3 * triangles[i + 1]; + int v2 = 3 * triangles[i + 2]; + int col = GizmoHelper.getColorByNormal(vertices, v0, v1, v2); + debugDraw.vertex(vertices[v0], vertices[v0 + 1], vertices[v0 + 2], col); + debugDraw.vertex(vertices[v1], vertices[v1 + 1], vertices[v1 + 2], col); + debugDraw.vertex(vertices[v2], vertices[v2 + 1], vertices[v2 + 2], col); + } + debugDraw.end(); + } + +} \ No newline at end of file diff --git a/src/DotRecast.Recast.Demo/Tools/JumpLinkBuilderTool.cs b/src/DotRecast.Recast.Demo/Tools/JumpLinkBuilderTool.cs new file mode 100644 index 0000000..547272c --- /dev/null +++ b/src/DotRecast.Recast.Demo/Tools/JumpLinkBuilderTool.cs @@ -0,0 +1,427 @@ +/* +recast4j copyright (c) 2020-2021 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.Collections.Generic; +using Silk.NET.Windowing; +using DotRecast.Detour.Extras.Jumplink; +using DotRecast.Recast.Demo.Builder; +using DotRecast.Recast.Demo.Draw; +using DotRecast.Recast.Demo.Geom; + +using static DotRecast.Detour.DetourCommon; +using static DotRecast.Recast.Demo.Draw.DebugDraw; +using static DotRecast.Recast.Demo.Draw.DebugDrawPrimitives; + +namespace DotRecast.Recast.Demo.Tools; + +public class JumpLinkBuilderTool : Tool { + + private readonly List links = new(); + private Sample sample; + private JumpLinkBuilder annotationBuilder; + private readonly int selEdge = -1; + private readonly JumpLinkBuilderToolParams option = new JumpLinkBuilderToolParams(); + + public override void setSample(Sample sample) { + this.sample = sample; + annotationBuilder = null; + } + + public override void handleClick(float[] s, float[] p, bool shift) { + + } + + public override void handleRender(NavMeshRenderer renderer) { + int col0 = duLerpCol(duRGBA(32, 255, 96, 255), duRGBA(255, 255, 255, 255), 200); + int col1 = duRGBA(32, 255, 96, 255); + RecastDebugDraw dd = renderer.getDebugDraw(); + dd.depthMask(false); + + if ((option.flags & JumpLinkBuilderToolParams.DRAW_WALKABLE_BORDER) != 0) { + if (annotationBuilder != null) { + foreach (Edge[] edges in annotationBuilder.getEdges()) { + dd.begin(LINES, 3.0f); + for (int i = 0; i < edges.Length; ++i) { + int col = duRGBA(0, 96, 128, 255); + if (i == selEdge) + continue; + dd.vertex(edges[i].sp, col); + dd.vertex(edges[i].sq, col); + } + dd.end(); + + dd.begin(POINTS, 8.0f); + for (int i = 0; i < edges.Length; ++i) { + int col = duRGBA(0, 96, 128, 255); + if (i == selEdge) + continue; + dd.vertex(edges[i].sp, col); + dd.vertex(edges[i].sq, col); + } + dd.end(); + + if (selEdge >= 0 && selEdge < edges.Length) { + int col = duRGBA(48, 16, 16, 255); // duRGBA(255,192,0,255); + dd.begin(LINES, 3.0f); + dd.vertex(edges[selEdge].sp, col); + dd.vertex(edges[selEdge].sq, col); + dd.end(); + dd.begin(POINTS, 8.0f); + dd.vertex(edges[selEdge].sp, col); + dd.vertex(edges[selEdge].sq, col); + dd.end(); + } + + dd.begin(POINTS, 4.0f); + for (int i = 0; i < edges.Length; ++i) { + int col = duRGBA(190, 190, 190, 255); + dd.vertex(edges[i].sp, col); + dd.vertex(edges[i].sq, col); + } + dd.end(); + } + } + } + + if ((option.flags & JumpLinkBuilderToolParams.DRAW_ANNOTATIONS) != 0) { + dd.begin(QUADS); + foreach (JumpLink link in links) { + for (int j = 0; j < link.nspine - 1; ++j) { + int u = (j * 255) / link.nspine; + int col = duTransCol(duLerpCol(col0, col1, u), 128); + dd.vertex(link.spine1[j * 3], link.spine1[j * 3 + 1], link.spine1[j * 3 + 2], col); + dd.vertex(link.spine1[(j + 1) * 3], link.spine1[(j + 1) * 3 + 1], link.spine1[(j + 1) * 3 + 2], + col); + dd.vertex(link.spine0[(j + 1) * 3], link.spine0[(j + 1) * 3 + 1], link.spine0[(j + 1) * 3 + 2], + col); + dd.vertex(link.spine0[j * 3], link.spine0[j * 3 + 1], link.spine0[j * 3 + 2], col); + } + } + dd.end(); + dd.begin(LINES, 3.0f); + foreach (JumpLink link in links) { + for (int j = 0; j < link.nspine - 1; ++j) { + // int u = (j*255)/link.nspine; + int col = duTransCol(duDarkenCol(col1)/*duDarkenCol(duLerpCol(col0,col1,u))*/, 128); + + dd.vertex(link.spine0[j * 3], link.spine0[j * 3 + 1], link.spine0[j * 3 + 2], col); + dd.vertex(link.spine0[(j + 1) * 3], link.spine0[(j + 1) * 3 + 1], link.spine0[(j + 1) * 3 + 2], + col); + dd.vertex(link.spine1[j * 3], link.spine1[j * 3 + 1], link.spine1[j * 3 + 2], col); + dd.vertex(link.spine1[(j + 1) * 3], link.spine1[(j + 1) * 3 + 1], link.spine1[(j + 1) * 3 + 2], + col); + } + + dd.vertex(link.spine0[0], link.spine0[1], link.spine0[2], duDarkenCol(col1)); + dd.vertex(link.spine1[0], link.spine1[1], link.spine1[2], duDarkenCol(col1)); + + dd.vertex(link.spine0[(link.nspine - 1) * 3], link.spine0[(link.nspine - 1) * 3 + 1], + link.spine0[(link.nspine - 1) * 3 + 2], duDarkenCol(col1)); + dd.vertex(link.spine1[(link.nspine - 1) * 3], link.spine1[(link.nspine - 1) * 3 + 1], + link.spine1[(link.nspine - 1) * 3 + 2], duDarkenCol(col1)); + } + dd.end(); + } + if (annotationBuilder != null) { + foreach (JumpLink link in links) { + if ((option.flags & JumpLinkBuilderToolParams.DRAW_ANIM_TRAJECTORY) != 0) { + float r = link.start.height; + + int col = duLerpCol(duRGBA(255, 192, 0, 255), + duRGBA(255, 255, 255, 255), 64); + int cola = duTransCol(col, 192); + int colb = duRGBA(255, 255, 255, 255); + + // Start segment. + dd.begin(LINES, 3.0f); + dd.vertex(link.start.p, col); + dd.vertex(link.start.q, col); + dd.end(); + + dd.begin(LINES, 1.0f); + dd.vertex(link.start.p[0], link.start.p[1], link.start.p[2], colb); + dd.vertex(link.start.p[0], link.start.p[1] + r, link.start.p[2], colb); + dd.vertex(link.start.p[0], link.start.p[1] + r, link.start.p[2], colb); + dd.vertex(link.start.q[0], link.start.q[1] + r, link.start.q[2], colb); + dd.vertex(link.start.q[0], link.start.q[1] + r, link.start.q[2], colb); + dd.vertex(link.start.q[0], link.start.q[1], link.start.q[2], colb); + dd.vertex(link.start.q[0], link.start.q[1], link.start.q[2], colb); + dd.vertex(link.start.p[0], link.start.p[1], link.start.p[2], colb); + dd.end(); + + GroundSegment end = link.end; + r = end.height; + // End segment. + dd.begin(LINES, 3.0f); + dd.vertex(end.p, col); + dd.vertex(end.q, col); + dd.end(); + + dd.begin(LINES, 1.0f); + dd.vertex(end.p[0], end.p[1], end.p[2], colb); + dd.vertex(end.p[0], end.p[1] + r, end.p[2], colb); + dd.vertex(end.p[0], end.p[1] + r, end.p[2], colb); + dd.vertex(end.q[0], end.q[1] + r, end.q[2], colb); + dd.vertex(end.q[0], end.q[1] + r, end.q[2], colb); + dd.vertex(end.q[0], end.q[1], end.q[2], colb); + dd.vertex(end.q[0], end.q[1], end.q[2], colb); + dd.vertex(end.p[0], end.p[1], end.p[2], colb); + dd.end(); + + dd.begin(LINES, 4.0f); + drawTrajectory(dd, link, link.start.p, end.p, link.trajectory, cola); + drawTrajectory(dd, link, link.start.q, end.q, link.trajectory, cola); + dd.end(); + + dd.begin(LINES, 8.0f); + dd.vertex(link.start.p, duDarkenCol(col)); + dd.vertex(link.start.q, duDarkenCol(col)); + dd.vertex(end.p, duDarkenCol(col)); + dd.vertex(end.q, duDarkenCol(col)); + dd.end(); + + int colm = duRGBA(255, 255, 255, 255); + dd.begin(LINES, 3.0f); + dd.vertex(link.start.p, colm); + dd.vertex(link.start.q, colm); + dd.vertex(end.p, colm); + dd.vertex(end.q, colm); + dd.end(); + } + if ((option.flags & JumpLinkBuilderToolParams.DRAW_LAND_SAMPLES) != 0) { + dd.begin(POINTS, 8.0f); + for (int i = 0; i < link.start.gsamples.Length; ++i) { + GroundSample s = link.start.gsamples[i]; + float u = i / (float) (link.start.gsamples.Length - 1); + float[] spt = vLerp(link.start.p, link.start.q, u); + int col = duRGBA(48, 16, 16, 255); // duRGBA(255,(s->flags & 4)?255:0,0,255); + float off = 0.1f; + if (!s.validHeight) { + off = 0; + col = duRGBA(220, 32, 32, 255); + } + spt[1] = s.p[1] + off; + dd.vertex(spt, col); + } + dd.end(); + + dd.begin(POINTS, 4.0f); + for (int i = 0; i < link.start.gsamples.Length; ++i) { + GroundSample s = link.start.gsamples[i]; + float u = i / (float) (link.start.gsamples.Length - 1); + float[] spt = vLerp(link.start.p, link.start.q, u); + int col = duRGBA(255, 255, 255, 255); + float off = 0; + if (s.validHeight) { + off = 0.1f; + } + spt[1] = s.p[1] + off; + dd.vertex(spt, col); + } + dd.end(); + { + GroundSegment end = link.end; + dd.begin(POINTS, 8.0f); + for (int i = 0; i < end.gsamples.Length; ++i) { + GroundSample s = end.gsamples[i]; + float u = i / (float) (end.gsamples.Length - 1); + float[] spt = vLerp(end.p, end.q, u); + int col = duRGBA(48, 16, 16, 255); // duRGBA(255,(s->flags & 4)?255:0,0,255); + float off = 0.1f; + if (!s.validHeight) { + off = 0; + col = duRGBA(220, 32, 32, 255); + } + spt[1] = s.p[1] + off; + dd.vertex(spt, col); + } + dd.end(); + dd.begin(POINTS, 4.0f); + for (int i = 0; i < end.gsamples.Length; ++i) { + GroundSample s = end.gsamples[i]; + float u = i / (float) (end.gsamples.Length - 1); + float[] spt = vLerp(end.p, end.q, u); + int col = duRGBA(255, 255, 255, 255); + float off = 0; + if (s.validHeight) { + off = 0.1f; + } + spt[1] = s.p[1] + off; + dd.vertex(spt, col); + } + dd.end(); + } + } + } + } + dd.depthMask(true); + } + + private void drawTrajectory(RecastDebugDraw dd, JumpLink link, float[] pa, float[] pb, Trajectory tra, int cola) { + + } + + public override void handleUpdate(float dt) { + + } + + public override void layout(IWindow ctx) { + // if (!sample.getRecastResults().isEmpty()) { + // + // nk_layout_row_dynamic(ctx, 18, 1); + // nk_label(ctx, "Options", NK_TEXT_ALIGN_LEFT); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_float(ctx, "Ground Tolerance", 0f, option.groundTolerance, 2f, 0.05f, 0.01f); + // nk_layout_row_dynamic(ctx, 5, 1); + // nk_spacing(ctx, 1); + // + // nk_layout_row_dynamic(ctx, 18, 1); + // nk_label(ctx, "Climb Down", NK_TEXT_ALIGN_LEFT); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_float(ctx, "Distance", 0f, option.climbDownDistance, 5f, 0.05f, 0.01f); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_float(ctx, "Min Cliff Height", 0f, option.climbDownMinHeight, 10f, 0.05f, 0.01f); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_float(ctx, "Max Cliff Height", 0f, option.climbDownMaxHeight, 10f, 0.05f, 0.01f); + // nk_layout_row_dynamic(ctx, 5, 1); + // nk_spacing(ctx, 1); + // + // nk_layout_row_dynamic(ctx, 18, 1); + // nk_label(ctx, "Jump Down", NK_TEXT_ALIGN_LEFT); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_float(ctx, "Max Distance", 0f, option.edgeJumpEndDistance, 10f, 0.05f, 0.01f); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_float(ctx, "Jump Height", 0f, option.edgeJumpHeight, 10f, 0.05f, 0.01f); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_float(ctx, "Max Jump Down", 0f, option.edgeJumpDownMaxHeight, 10f, 0.05f, 0.01f); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_property_float(ctx, "Max Jump Up", 0f, option.edgeJumpUpMaxHeight, 10f, 0.05f, 0.01f); + // nk_layout_row_dynamic(ctx, 5, 1); + // nk_spacing(ctx, 1); + // nk_layout_row_dynamic(ctx, 18, 1); + // nk_label(ctx, "Mode", NK_TEXT_ALIGN_LEFT); + // nk_layout_row_dynamic(ctx, 20, 1); + // int buildTypes = 0; + // buildTypes |= nk_option_text(ctx, "Climb Down", + // (option.buildTypes & (1 << JumpLinkType.EDGE_CLIMB_DOWN.ordinal())) != 0) + // ? (1 << JumpLinkType.EDGE_CLIMB_DOWN.ordinal()) + // : 0; + // nk_layout_row_dynamic(ctx, 20, 1); + // buildTypes |= nk_option_text(ctx, "Edge Jump", + // (option.buildTypes & (1 << JumpLinkType.EDGE_JUMP.ordinal())) != 0) + // ? (1 << JumpLinkType.EDGE_JUMP.ordinal()) + // : 0; + // option.buildTypes = buildTypes; + // bool build = false; + // bool buildOffMeshConnections = false; + // if (nk_button_text(ctx, "Build")) { + // build = true; + // } + // if (nk_button_text(ctx, "Build Off-Mesh Links")) { + // buildOffMeshConnections = true; + // } + // if (build || buildOffMeshConnections) { + // if (annotationBuilder == null) { + // if (sample != null && !sample.getRecastResults().isEmpty()) { + // annotationBuilder = new JumpLinkBuilder(sample.getRecastResults()); + // } + // } + // links.clear(); + // if (annotationBuilder != null) { + // float cellSize = sample.getSettingsUI().getCellSize(); + // float agentHeight = sample.getSettingsUI().getAgentHeight(); + // float agentRadius = sample.getSettingsUI().getAgentRadius(); + // float agentClimb = sample.getSettingsUI().getAgentMaxClimb(); + // float cellHeight = sample.getSettingsUI().getCellHeight(); + // if ((buildTypes & (1 << JumpLinkType.EDGE_CLIMB_DOWN.ordinal())) != 0) { + // JumpLinkBuilderConfig config = new JumpLinkBuilderConfig(cellSize, cellHeight, agentRadius, + // agentHeight, agentClimb, option.groundTolerance[0], -agentRadius * 0.2f, + // cellSize + 2 * agentRadius + option.climbDownDistance[0], + // -option.climbDownMaxHeight[0], -option.climbDownMinHeight[0], 0); + // links.addAll(annotationBuilder.build(config, JumpLinkType.EDGE_CLIMB_DOWN)); + // } + // if ((buildTypes & (1 << JumpLinkType.EDGE_JUMP.ordinal())) != 0) { + // JumpLinkBuilderConfig config = new JumpLinkBuilderConfig(cellSize, cellHeight, agentRadius, + // agentHeight, agentClimb, option.groundTolerance[0], -agentRadius * 0.2f, + // option.edgeJumpEndDistance[0], -option.edgeJumpDownMaxHeight[0], + // option.edgeJumpUpMaxHeight[0], option.edgeJumpHeight[0]); + // links.addAll(annotationBuilder.build(config, JumpLinkType.EDGE_JUMP)); + // } + // if (buildOffMeshConnections) { + // DemoInputGeomProvider geom = sample.getInputGeom(); + // if (geom != null) { + // int area = SampleAreaModifications.SAMPLE_POLYAREA_TYPE_JUMP_AUTO; + // geom.removeOffMeshConnections(c => c.area == area); + // links.forEach(l => addOffMeshLink(l, geom, agentRadius)); + // } + // } + // } + // } + // nk_spacing(ctx, 1); + // nk_layout_row_dynamic(ctx, 18, 1); + // nk_label(ctx, "Debug Draw Options", NK_TEXT_ALIGN_LEFT); + // nk_layout_row_dynamic(ctx, 20, 1); + // int newFlags = 0; + // newFlags |= nk_option_text(ctx, "Walkable Border", + // (params.flags & JumpLinkBuilderToolParams.DRAW_WALKABLE_BORDER) != 0) + // ? JumpLinkBuilderToolParams.DRAW_WALKABLE_BORDER + // : 0; + // nk_layout_row_dynamic(ctx, 20, 1); + // newFlags |= nk_option_text(ctx, "Selected Edge", + // (params.flags & JumpLinkBuilderToolParams.DRAW_SELECTED_EDGE) != 0) + // ? JumpLinkBuilderToolParams.DRAW_SELECTED_EDGE + // : 0; + // nk_layout_row_dynamic(ctx, 20, 1); + // newFlags |= nk_option_text(ctx, "Anim Trajectory", + // (params.flags & JumpLinkBuilderToolParams.DRAW_ANIM_TRAJECTORY) != 0) + // ? JumpLinkBuilderToolParams.DRAW_ANIM_TRAJECTORY + // : 0; + // nk_layout_row_dynamic(ctx, 20, 1); + // newFlags |= nk_option_text(ctx, "Land Samples", + // (params.flags & JumpLinkBuilderToolParams.DRAW_LAND_SAMPLES) != 0) + // ? JumpLinkBuilderToolParams.DRAW_LAND_SAMPLES + // : 0; + // nk_layout_row_dynamic(ctx, 20, 1); + // newFlags |= nk_option_text(ctx, "All Annotations", + // (params.flags & JumpLinkBuilderToolParams.DRAW_ANNOTATIONS) != 0) + // ? JumpLinkBuilderToolParams.DRAW_ANNOTATIONS + // : 0; + // params.flags = newFlags; + // } + + } + + private void addOffMeshLink(JumpLink link, DemoInputGeomProvider geom, float agentRadius) { + int area = SampleAreaModifications.SAMPLE_POLYAREA_TYPE_JUMP_AUTO; + int flags = SampleAreaModifications.SAMPLE_POLYFLAGS_JUMP; + float[] prev = new float[3]; + for (int i = 0; i < link.startSamples.Length; i++) { + float[] p = link.startSamples[i].p; + float[] q = link.endSamples[i].p; + if (i == 0 || vDist2D(prev, p) > agentRadius) { + geom.addOffMeshConnection(p, q, agentRadius, false, area, flags); + prev = p; + } + } + } + + public override string getName() { + return "Annotation Builder"; + } + +} diff --git a/src/DotRecast.Recast.Demo/Tools/JumpLinkBuilderToolParams.cs b/src/DotRecast.Recast.Demo/Tools/JumpLinkBuilderToolParams.cs new file mode 100644 index 0000000..50626a3 --- /dev/null +++ b/src/DotRecast.Recast.Demo/Tools/JumpLinkBuilderToolParams.cs @@ -0,0 +1,45 @@ +/* +recast4j copyright (c) 2020-2021 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using DotRecast.Detour.Extras.Jumplink; + +namespace DotRecast.Recast.Demo.Tools; + + +public class JumpLinkBuilderToolParams { + + public const int DRAW_WALKABLE_SURFACE = 1 << 0; + public const int DRAW_WALKABLE_BORDER = 1 << 1; + public const int DRAW_SELECTED_EDGE = 1 << 2; + public const int DRAW_ANIM_TRAJECTORY = 1 << 3; + public const int DRAW_LAND_SAMPLES = 1 << 4; + public const int DRAW_COLLISION_SLICES = 1 << 5; + public const int DRAW_ANNOTATIONS = 1 << 6; + + public int flags = DRAW_WALKABLE_SURFACE | DRAW_WALKABLE_BORDER | DRAW_SELECTED_EDGE | DRAW_ANIM_TRAJECTORY | DRAW_LAND_SAMPLES | DRAW_ANNOTATIONS; + public readonly float[] groundTolerance = new[] { 0.3f }; + public readonly float[] climbDownDistance = new[] { 0.4f }; + public readonly float[] climbDownMaxHeight = new[] { 3.2f }; + public readonly float[] climbDownMinHeight = new[] { 1.5f }; + public readonly float[] edgeJumpEndDistance = new[] { 2f }; + public readonly float[] edgeJumpHeight = new[] { 0.4f }; + public readonly float[] edgeJumpDownMaxHeight = new[] { 2.5f }; + public readonly float[] edgeJumpUpMaxHeight = new[] { 0.3f }; + public int buildTypes = (1 << (int)JumpLinkType.EDGE_CLIMB_DOWN) | (1 << (int)JumpLinkType.EDGE_JUMP); + +} diff --git a/src/DotRecast.Recast.Demo/Tools/OffMeshConnectionTool.cs b/src/DotRecast.Recast.Demo/Tools/OffMeshConnectionTool.cs new file mode 100644 index 0000000..970ba90 --- /dev/null +++ b/src/DotRecast.Recast.Demo/Tools/OffMeshConnectionTool.cs @@ -0,0 +1,109 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using Silk.NET.Windowing; +using DotRecast.Core; +using DotRecast.Recast.Demo.Builder; +using DotRecast.Recast.Demo.Draw; +using DotRecast.Recast.Demo.Geom; + +using static DotRecast.Recast.Demo.Draw.DebugDraw; + +namespace DotRecast.Recast.Demo.Tools; +public class OffMeshConnectionTool : Tool { + + private Sample sample; + private bool hitPosSet; + private float[] hitPos; + private bool bidir; + + public override void setSample(Sample m_sample) { + sample = m_sample; + } + + public override void handleClick(float[] s, float[] p, bool shift) { + DemoInputGeomProvider geom = sample.getInputGeom(); + if (geom == null) { + return; + } + + if (shift) { + // Delete + // Find nearest link end-point + float nearestDist = float.MaxValue; + DemoOffMeshConnection nearestConnection = null; + foreach (DemoOffMeshConnection offMeshCon in geom.getOffMeshConnections()) { + float d = Math.Min(DemoMath.vDistSqr(p, offMeshCon.verts, 0), DemoMath.vDistSqr(p, offMeshCon.verts, 3)); + if (d < nearestDist && Math.Sqrt(d) < sample.getSettingsUI().getAgentRadius()) { + nearestDist = d; + nearestConnection = offMeshCon; + } + } + if (nearestConnection != null) { + geom.getOffMeshConnections().Remove(nearestConnection); + } + } else { + // Create + if (!hitPosSet) { + hitPos = ArrayUtils.CopyOf(p, p.Length); + hitPosSet = true; + } else { + int area = SampleAreaModifications.SAMPLE_POLYAREA_TYPE_JUMP; + int flags = SampleAreaModifications.SAMPLE_POLYFLAGS_JUMP; + geom.addOffMeshConnection(hitPos, p, sample.getSettingsUI().getAgentRadius(), bidir, area, flags); + hitPosSet = false; + } + } + + } + + public override void handleRender(NavMeshRenderer renderer) { + if (sample == null) { + return; + } + RecastDebugDraw dd = renderer.getDebugDraw(); + float s = sample.getSettingsUI().getAgentRadius(); + + if (hitPosSet) { + dd.debugDrawCross(hitPos[0], hitPos[1] + 0.1f, hitPos[2], s, duRGBA(0, 0, 0, 128), 2.0f); + } + DemoInputGeomProvider geom = sample.getInputGeom(); + if (geom != null) { + renderer.drawOffMeshConnections(geom, true); + } + } + + public override void layout(IWindow ctx) { + // nk_layout_row_dynamic(ctx, 20, 1); + // bidir = !nk_option_label(ctx, "One Way", !bidir); + // nk_layout_row_dynamic(ctx, 20, 1); + // bidir = nk_option_label(ctx, "Bidirectional", bidir); + } + + public override string getName() { + return "Create Off-Mesh Links"; + } + + public override void handleUpdate(float dt) { + // TODO Auto-generated method stub + + } + +} diff --git a/src/DotRecast.Recast.Demo/Tools/PathUtils.cs b/src/DotRecast.Recast.Demo/Tools/PathUtils.cs new file mode 100644 index 0000000..002f593 --- /dev/null +++ b/src/DotRecast.Recast.Demo/Tools/PathUtils.cs @@ -0,0 +1,171 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.Collections.Generic; +using DotRecast.Detour; + +namespace DotRecast.Recast.Demo.Tools; + + +public static class PathUtils { + + private readonly static int MAX_STEER_POINTS = 3; + + + public static SteerTarget getSteerTarget(NavMeshQuery navQuery, float[] startPos, float[] endPos, + float minTargetDist, List path) { + // Find steer target. + Result> result = navQuery.findStraightPath(startPos, endPos, path, MAX_STEER_POINTS, 0); + if (result.failed()) { + return null; + } + List straightPath = result.result; + float[] steerPoints = new float[straightPath.Count * 3]; + for (int i = 0; i < straightPath.Count; i++) { + steerPoints[i * 3] = straightPath[i].getPos()[0]; + steerPoints[i * 3 + 1] = straightPath[i].getPos()[1]; + steerPoints[i * 3 + 2] = straightPath[i].getPos()[2]; + } + + // Find vertex far enough to steer to. + int ns = 0; + while (ns < straightPath.Count) { + // Stop at Off-Mesh link or when point is further than slop away. + if (((straightPath[ns].getFlags() & NavMeshQuery.DT_STRAIGHTPATH_OFFMESH_CONNECTION) != 0) + || !inRange(straightPath[ns].getPos(), startPos, minTargetDist, 1000.0f)) + break; + ns++; + } + // Failed to find good point to steer to. + if (ns >= straightPath.Count) + return null; + + float[] steerPos = new float[] { straightPath[ns].getPos()[0], startPos[1], + straightPath[ns].getPos()[2] }; + int steerPosFlag = straightPath[ns].getFlags(); + long steerPosRef = straightPath[ns].getRef(); + + SteerTarget target = new SteerTarget(steerPos, steerPosFlag, steerPosRef, steerPoints); + return target; + + } + + public static bool inRange(float[] v1, float[] v2, float r, float h) { + float dx = v2[0] - v1[0]; + float dy = v2[1] - v1[1]; + float dz = v2[2] - v1[2]; + return (dx * dx + dz * dz) < r * r && Math.Abs(dy) < h; + } + + public static List fixupCorridor(List path, List visited) { + int furthestPath = -1; + int furthestVisited = -1; + + // Find furthest common polygon. + for (int i = path.Count - 1; i >= 0; --i) { + bool found = false; + for (int j = visited.Count - 1; j >= 0; --j) { + if (path[i] == visited[j]) { + furthestPath = i; + furthestVisited = j; + found = true; + } + } + if (found) + break; + } + + // If no intersection found just return current path. + if (furthestPath == -1 || furthestVisited == -1) + return path; + + // Concatenate paths. + + // Adjust beginning of the buffer to include the visited. + int req = visited.Count - furthestVisited; + int orig = Math.Min(furthestPath + 1, path.Count); + int size = Math.Max(0, path.Count - orig); + List fixupPath = new(); + // Store visited + for (int i = 0; i < req; ++i) { + fixupPath.Add(visited[(visited.Count - 1) - i]); + } + for (int i = 0; i < size; i++) { + fixupPath.Add(path[orig + i]); + } + + return fixupPath; + } + + // This function checks if the path has a small U-turn, that is, + // a polygon further in the path is adjacent to the first polygon + // in the path. If that happens, a shortcut is taken. + // This can happen if the target (T) location is at tile boundary, + // and we're (S) approaching it parallel to the tile edge. + // The choice at the vertex can be arbitrary, + // +---+---+ + // |:::|:::| + // +-S-+-T-+ + // |:::| | <-- the step can end up in here, resulting U-turn path. + // +---+---+ + public static List fixupShortcuts(List path, NavMeshQuery navQuery) { + if (path.Count < 3) { + return path; + } + + // Get connected polygons + List neis = new(); + + Result> tileAndPoly = navQuery.getAttachedNavMesh().getTileAndPolyByRef(path[0]); + if (tileAndPoly.failed()) { + return path; + } + MeshTile tile = tileAndPoly.result.Item1; + Poly poly = tileAndPoly.result.Item2; + + for (int k = tile.polyLinks[poly.index]; k != NavMesh.DT_NULL_LINK; k = tile.links[k].next) { + Link link = tile.links[k]; + if (link.refs != 0) { + neis.Add(link.refs); + } + } + + // If any of the neighbour polygons is within the next few polygons + // in the path, short cut to that polygon directly. + int maxLookAhead = 6; + int cut = 0; + for (int i = Math.Min(maxLookAhead, path.Count) - 1; i > 1 && cut == 0; i--) { + for (int j = 0; j < neis.Count; j++) { + if (path[i] == neis[j]) { + cut = i; + break; + } + } + } + if (cut > 1) { + List shortcut = new(); + shortcut.Add(path[0]); + shortcut.AddRange(path.GetRange(cut, path.Count - cut)); + return shortcut; + } + return path; + } + +} diff --git a/src/DotRecast.Recast.Demo/Tools/PolyUtils.cs b/src/DotRecast.Recast.Demo/Tools/PolyUtils.cs new file mode 100644 index 0000000..790ef13 --- /dev/null +++ b/src/DotRecast.Recast.Demo/Tools/PolyUtils.cs @@ -0,0 +1,110 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2021 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; + +namespace DotRecast.Recast.Demo.Tools; + +public class PolyUtils { + + public static bool pointInPoly(float[] verts, float[] p) { + int i, j; + bool c = false; + for (i = 0, j = verts.Length / 3 - 1; i < verts.Length / 3; j = i++) { + float[] vi = new float[] { verts[i * 3], verts[i * 3 + 1], verts[i * 3 + 2] }; + float[] vj = new float[] { verts[j * 3], verts[j * 3 + 1], verts[j * 3 + 2] }; + if (((vi[2] > p[2]) != (vj[2] > p[2])) + && (p[0] < (vj[0] - vi[0]) * (p[2] - vi[2]) / (vj[2] - vi[2]) + vi[0])) { + c = !c; + } + } + return c; + } + + public static int offsetPoly(float[] verts, int nverts, float offset, float[] outVerts, int maxOutVerts) { + float MITER_LIMIT = 1.20f; + + int n = 0; + + for (int i = 0; i < nverts; i++) { + int a = (i + nverts - 1) % nverts; + int b = i; + int c = (i + 1) % nverts; + int va = a * 3; + int vb = b * 3; + int vc = c * 3; + float dx0 = verts[vb] - verts[va]; + float dy0 = verts[vb + 2] - verts[va + 2]; + float d0 = dx0 * dx0 + dy0 * dy0; + if (d0 > 1e-6f) { + d0 = (float) (1.0f / Math.Sqrt(d0)); + dx0 *= d0; + dy0 *= d0; + } + float dx1 = verts[vc] - verts[vb]; + float dy1 = verts[vc + 2] - verts[vb + 2]; + float d1 = dx1 * dx1 + dy1 * dy1; + if (d1 > 1e-6f) { + d1 = (float) (1.0f / Math.Sqrt(d1)); + dx1 *= d1; + dy1 *= d1; + } + float dlx0 = -dy0; + float dly0 = dx0; + float dlx1 = -dy1; + float dly1 = dx1; + float cross = dx1 * dy0 - dx0 * dy1; + float dmx = (dlx0 + dlx1) * 0.5f; + float dmy = (dly0 + dly1) * 0.5f; + float dmr2 = dmx * dmx + dmy * dmy; + bool bevel = dmr2 * MITER_LIMIT * MITER_LIMIT < 1.0f; + if (dmr2 > 1e-6f) { + float scale = 1.0f / dmr2; + dmx *= scale; + dmy *= scale; + } + + if (bevel && cross < 0.0f) { + if (n + 2 >= maxOutVerts) { + return 0; + } + float d = (1.0f - (dx0 * dx1 + dy0 * dy1)) * 0.5f; + outVerts[n * 3 + 0] = verts[vb] + (-dlx0 + dx0 * d) * offset; + outVerts[n * 3 + 1] = verts[vb + 1]; + outVerts[n * 3 + 2] = verts[vb + 2] + (-dly0 + dy0 * d) * offset; + n++; + outVerts[n * 3 + 0] = verts[vb] + (-dlx1 - dx1 * d) * offset; + outVerts[n * 3 + 1] = verts[vb + 1]; + outVerts[n * 3 + 2] = verts[vb + 2] + (-dly1 - dy1 * d) * offset; + n++; + } else { + if (n + 1 >= maxOutVerts) { + return 0; + } + outVerts[n * 3 + 0] = verts[vb] - dmx * offset; + outVerts[n * 3 + 1] = verts[vb + 1]; + outVerts[n * 3 + 2] = verts[vb + 2] - dmy * offset; + n++; + } + } + + return n; + } + +} diff --git a/src/DotRecast.Recast.Demo/Tools/SteerTarget.cs b/src/DotRecast.Recast.Demo/Tools/SteerTarget.cs new file mode 100644 index 0000000..ffe9950 --- /dev/null +++ b/src/DotRecast.Recast.Demo/Tools/SteerTarget.cs @@ -0,0 +1,15 @@ +namespace DotRecast.Recast.Demo.Tools; + +public class SteerTarget { + public readonly float[] steerPos; + public readonly int steerPosFlag; + public readonly long steerPosRef; + public readonly float[] steerPoints; + + public SteerTarget(float[] steerPos, int steerPosFlag, long steerPosRef, float[] steerPoints) { + this.steerPos = steerPos; + this.steerPosFlag = steerPosFlag; + this.steerPosRef = steerPosRef; + this.steerPoints = steerPoints; + } +} diff --git a/src/DotRecast.Recast.Demo/Tools/TestNavmeshTool.cs b/src/DotRecast.Recast.Demo/Tools/TestNavmeshTool.cs new file mode 100644 index 0000000..a3c3b15 --- /dev/null +++ b/src/DotRecast.Recast.Demo/Tools/TestNavmeshTool.cs @@ -0,0 +1,880 @@ +using System; +using System.Collections.Generic; +using Silk.NET.Windowing; + +using DotRecast.Core; +using DotRecast.Detour; +using DotRecast.Recast.Demo.Builder; +using DotRecast.Recast.Demo.Draw; + +using static DotRecast.Detour.DetourCommon; +using static DotRecast.Recast.Demo.Draw.DebugDraw; +using static DotRecast.Recast.Demo.Draw.DebugDrawPrimitives; + +namespace DotRecast.Recast.Demo.Tools; + + +public class TestNavmeshTool : Tool { + + private readonly static int MAX_POLYS = 256; + private readonly static int MAX_SMOOTH = 2048; + private Sample m_sample; + private ToolMode m_toolMode = ToolMode.PATHFIND_FOLLOW; + private bool m_sposSet; + private bool m_eposSet; + private float[] m_spos; + private float[] m_epos; + private readonly DefaultQueryFilter m_filter; + private readonly float[] m_polyPickExt = new float[] { 2, 4, 2 }; + private long m_startRef; + private long m_endRef; + private float[] m_hitPos; + private float m_distanceToWall; + private float[] m_hitNormal; + private List m_straightPath; + private int m_straightPathOptions; + private List m_polys; + private bool m_hitResult; + private List m_parent; + private float m_neighbourhoodRadius; + private readonly float[] m_queryPoly = new float[12]; + private List m_smoothPath; + private Status m_pathFindStatus = Status.FAILURE; + private bool enableRaycast = true; + private readonly List randomPoints = new(); + private bool constrainByCircle; + + private enum ToolMode { + PATHFIND_FOLLOW, + PATHFIND_STRAIGHT, + PATHFIND_SLICED, + DISTANCE_TO_WALL, + RAYCAST, + FIND_POLYS_IN_CIRCLE, + FIND_POLYS_IN_SHAPE, + FIND_LOCAL_NEIGHBOURHOOD, + RANDOM_POINTS_IN_CIRCLE + } + + public TestNavmeshTool() { + m_filter = new DefaultQueryFilter(SampleAreaModifications.SAMPLE_POLYFLAGS_ALL, + SampleAreaModifications.SAMPLE_POLYFLAGS_DISABLED, new float[] { 1f, 1f, 1f, 1f, 2f, 1.5f }); + } + + public override void setSample(Sample m_sample) { + this.m_sample = m_sample; + } + + public override void handleClick(float[] s, float[] p, bool shift) { + if (shift) { + m_sposSet = true; + m_spos = ArrayUtils.CopyOf(p, p.Length); + } else { + m_eposSet = true; + m_epos = ArrayUtils.CopyOf(p, p.Length); + } + recalc(); + } + + public override void layout(IWindow ctx) { + ToolMode previousToolMode = m_toolMode; + int previousStraightPathOptions = m_straightPathOptions; + int previousIncludeFlags = m_filter.getIncludeFlags(); + int previousExcludeFlags = m_filter.getExcludeFlags(); + bool previousConstrainByCircle = constrainByCircle; + + // nk_layout_row_dynamic(ctx, 20, 1); + // if (nk_option_label(ctx, "Pathfind Follow", m_toolMode == ToolMode.PATHFIND_FOLLOW)) { + // m_toolMode = ToolMode.PATHFIND_FOLLOW; + // } + // nk_layout_row_dynamic(ctx, 20, 1); + // if (nk_option_label(ctx, "Pathfind Straight", m_toolMode == ToolMode.PATHFIND_STRAIGHT)) { + // m_toolMode = ToolMode.PATHFIND_STRAIGHT; + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_label(ctx, "Vertices at crossings", NK_TEXT_ALIGN_LEFT); + // nk_layout_row_dynamic(ctx, 20, 1); + // if (nk_option_label(ctx, "None", m_straightPathOptions == 0)) { + // m_straightPathOptions = 0; + // } + // nk_layout_row_dynamic(ctx, 20, 1); + // if (nk_option_label(ctx, "Area", m_straightPathOptions == NavMeshQuery.DT_STRAIGHTPATH_AREA_CROSSINGS)) { + // m_straightPathOptions = NavMeshQuery.DT_STRAIGHTPATH_AREA_CROSSINGS; + // } + // nk_layout_row_dynamic(ctx, 20, 1); + // if (nk_option_label(ctx, "All", m_straightPathOptions == NavMeshQuery.DT_STRAIGHTPATH_ALL_CROSSINGS)) { + // m_straightPathOptions = NavMeshQuery.DT_STRAIGHTPATH_ALL_CROSSINGS; + // } + // nk_layout_row_dynamic(ctx, 5, 1); + // nk_spacing(ctx, 1); + // } + // nk_layout_row_dynamic(ctx, 20, 1); + // if (nk_option_label(ctx, "Pathfind Sliced", m_toolMode == ToolMode.PATHFIND_SLICED)) { + // m_toolMode = ToolMode.PATHFIND_SLICED; + // } + // nk_layout_row_dynamic(ctx, 5, 1); + // nk_spacing(ctx, 1); + // nk_layout_row_dynamic(ctx, 20, 1); + // if (nk_option_label(ctx, "Distance to Wall", m_toolMode == ToolMode.DISTANCE_TO_WALL)) { + // m_toolMode = ToolMode.DISTANCE_TO_WALL; + // } + // nk_layout_row_dynamic(ctx, 5, 1); + // nk_spacing(ctx, 1); + // nk_layout_row_dynamic(ctx, 20, 1); + // if (nk_option_label(ctx, "Raycast", m_toolMode == ToolMode.RAYCAST)) { + // m_toolMode = ToolMode.RAYCAST; + // } + // nk_layout_row_dynamic(ctx, 5, 1); + // nk_spacing(ctx, 1); + // nk_layout_row_dynamic(ctx, 20, 1); + // if (nk_option_label(ctx, "Find Polys in Circle", m_toolMode == ToolMode.FIND_POLYS_IN_CIRCLE)) { + // m_toolMode = ToolMode.FIND_POLYS_IN_CIRCLE; + // } + // if (nk_option_label(ctx, "Find Polys in Shape", m_toolMode == ToolMode.FIND_POLYS_IN_SHAPE)) { + // m_toolMode = ToolMode.FIND_POLYS_IN_SHAPE; + // } + // nk_layout_row_dynamic(ctx, 5, 1); + // nk_spacing(ctx, 1); + // nk_layout_row_dynamic(ctx, 20, 1); + // if (nk_option_label(ctx, "Find Local Neighbourhood", m_toolMode == ToolMode.FIND_LOCAL_NEIGHBOURHOOD)) { + // m_toolMode = ToolMode.FIND_LOCAL_NEIGHBOURHOOD; + // } + // if (nk_option_label(ctx, "Random Points in Circle", m_toolMode == ToolMode.RANDOM_POINTS_IN_CIRCLE)) { + // m_toolMode = ToolMode.RANDOM_POINTS_IN_CIRCLE; + // nk_layout_row_dynamic(ctx, 20, 1); + // constrainByCircle = nk_check_text(ctx, "Constrained", constrainByCircle); + // } + // + // nk_layout_row_dynamic(ctx, 5, 1); + // nk_spacing(ctx, 1); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_label(ctx, "Include Flags", NK_TEXT_ALIGN_LEFT); + // nk_layout_row_dynamic(ctx, 20, 1); + // int includeFlags = 0; + // if (nk_option_label(ctx, "Walk", + // (m_filter.getIncludeFlags() & SampleAreaModifications.SAMPLE_POLYFLAGS_WALK) != 0)) { + // includeFlags |= SampleAreaModifications.SAMPLE_POLYFLAGS_WALK; + // } + // if (nk_option_label(ctx, "Swim", + // (m_filter.getIncludeFlags() & SampleAreaModifications.SAMPLE_POLYFLAGS_SWIM) != 0)) { + // includeFlags |= SampleAreaModifications.SAMPLE_POLYFLAGS_SWIM; + // } + // if (nk_option_label(ctx, "Door", + // (m_filter.getIncludeFlags() & SampleAreaModifications.SAMPLE_POLYFLAGS_DOOR) != 0)) { + // includeFlags |= SampleAreaModifications.SAMPLE_POLYFLAGS_DOOR; + // } + // if (nk_option_label(ctx, "Jump", + // (m_filter.getIncludeFlags() & SampleAreaModifications.SAMPLE_POLYFLAGS_JUMP) != 0)) { + // includeFlags |= SampleAreaModifications.SAMPLE_POLYFLAGS_JUMP; + // } + // m_filter.setIncludeFlags(includeFlags); + // + // nk_layout_row_dynamic(ctx, 5, 1); + // nk_spacing(ctx, 1); + // nk_layout_row_dynamic(ctx, 20, 1); + // nk_label(ctx, "Exclude Flags", NK_TEXT_ALIGN_LEFT); + // nk_layout_row_dynamic(ctx, 20, 1); + // int excludeFlags = 0; + // if (nk_option_label(ctx, "Walk", + // (m_filter.getExcludeFlags() & SampleAreaModifications.SAMPLE_POLYFLAGS_WALK) != 0)) { + // excludeFlags |= SampleAreaModifications.SAMPLE_POLYFLAGS_WALK; + // } + // if (nk_option_label(ctx, "Swim", + // (m_filter.getExcludeFlags() & SampleAreaModifications.SAMPLE_POLYFLAGS_SWIM) != 0)) { + // excludeFlags |= SampleAreaModifications.SAMPLE_POLYFLAGS_SWIM; + // } + // if (nk_option_label(ctx, "Door", + // (m_filter.getExcludeFlags() & SampleAreaModifications.SAMPLE_POLYFLAGS_DOOR) != 0)) { + // excludeFlags |= SampleAreaModifications.SAMPLE_POLYFLAGS_DOOR; + // } + // if (nk_option_label(ctx, "Jump", + // (m_filter.getExcludeFlags() & SampleAreaModifications.SAMPLE_POLYFLAGS_JUMP) != 0)) { + // excludeFlags |= SampleAreaModifications.SAMPLE_POLYFLAGS_JUMP; + // } + // m_filter.setExcludeFlags(excludeFlags); + // + // nk_layout_row_dynamic(ctx, 30, 1); + // bool previousEnableRaycast = enableRaycast; + // enableRaycast = nk_check_label(ctx, "Raycast shortcuts", enableRaycast); + // + // if (previousToolMode != m_toolMode || m_straightPathOptions != previousStraightPathOptions + // || previousIncludeFlags != includeFlags || previousExcludeFlags != excludeFlags + // || previousEnableRaycast != enableRaycast || previousConstrainByCircle != constrainByCircle) { + // recalc(); + // } + } + + public override string getName() { + return "Test Navmesh"; + } + + private void recalc() { + if (m_sample.getNavMesh() == null) { + return; + } + NavMeshQuery m_navQuery = m_sample.getNavMeshQuery(); + if (m_sposSet) { + m_startRef = m_navQuery.findNearestPoly(m_spos, m_polyPickExt, m_filter).result.getNearestRef(); + } else { + m_startRef = 0; + } + if (m_eposSet) { + m_endRef = m_navQuery.findNearestPoly(m_epos, m_polyPickExt, m_filter).result.getNearestRef(); + } else { + m_endRef = 0; + } + NavMesh m_navMesh = m_sample.getNavMesh(); + if (m_toolMode == ToolMode.PATHFIND_FOLLOW) { + if (m_sposSet && m_eposSet && m_startRef != 0 && m_endRef != 0) { + m_polys = m_navQuery.findPath(m_startRef, m_endRef, m_spos, m_epos, m_filter, + enableRaycast ? NavMeshQuery.DT_FINDPATH_ANY_ANGLE : 0, float.MaxValue).result; + if (0 < m_polys.Count) { + List polys = new(m_polys); + // Iterate over the path to find smooth path on the detail mesh surface. + float[] iterPos = m_navQuery.closestPointOnPoly(m_startRef, m_spos).result.getClosest(); + float[] targetPos = m_navQuery.closestPointOnPoly(polys[polys.Count - 1], m_epos).result + .getClosest(); + + float STEP_SIZE = 0.5f; + float SLOP = 0.01f; + + m_smoothPath = new(); + m_smoothPath.Add(iterPos); + + // Move towards target a small advancement at a time until target reached or + // when ran out of memory to store the path. + while (0 < polys.Count && m_smoothPath.Count < MAX_SMOOTH) { + // Find location to steer towards. + SteerTarget steerTarget = PathUtils.getSteerTarget(m_navQuery, iterPos, targetPos, + SLOP, polys); + if (null == steerTarget) { + break; + } + bool endOfPath = (steerTarget.steerPosFlag & NavMeshQuery.DT_STRAIGHTPATH_END) != 0 + ? true + : false; + bool offMeshConnection = (steerTarget.steerPosFlag + & NavMeshQuery.DT_STRAIGHTPATH_OFFMESH_CONNECTION) != 0 ? true : false; + + // Find movement delta. + float[] delta = vSub(steerTarget.steerPos, iterPos); + float len = (float) Math.Sqrt(DemoMath.vDot(delta, delta)); + // If the steer target is end of path or off-mesh link, do not move past the location. + if ((endOfPath || offMeshConnection) && len < STEP_SIZE) { + len = 1; + } else { + len = STEP_SIZE / len; + } + float[] moveTgt = vMad(iterPos, delta, len); + // Move + Result result = m_navQuery.moveAlongSurface(polys[0], iterPos, + moveTgt, m_filter); + MoveAlongSurfaceResult moveAlongSurface = result.result; + + iterPos = new float[3]; + iterPos[0] = moveAlongSurface.getResultPos()[0]; + iterPos[1] = moveAlongSurface.getResultPos()[1]; + iterPos[2] = moveAlongSurface.getResultPos()[2]; + + List visited = result.result.getVisited(); + polys = PathUtils.fixupCorridor(polys, visited); + polys = PathUtils.fixupShortcuts(polys, m_navQuery); + + Result polyHeight = m_navQuery.getPolyHeight(polys[0], moveAlongSurface.getResultPos()); + if (polyHeight.succeeded()) { + iterPos[1] = polyHeight.result; + } + + // Handle end of path and off-mesh links when close enough. + if (endOfPath && PathUtils.inRange(iterPos, steerTarget.steerPos, SLOP, 1.0f)) { + // Reached end of path. + vCopy(iterPos, targetPos); + if (m_smoothPath.Count < MAX_SMOOTH) { + m_smoothPath.Add(iterPos); + } + break; + } else if (offMeshConnection + && PathUtils.inRange(iterPos, steerTarget.steerPos, SLOP, 1.0f)) { + // Reached off-mesh connection. + // Advance the path up to and over the off-mesh connection. + long prevRef = 0; + long polyRef = polys[0]; + int npos = 0; + while (npos < polys.Count && polyRef != steerTarget.steerPosRef) { + prevRef = polyRef; + polyRef = polys[npos]; + npos++; + } + polys = polys.GetRange(npos, polys.Count - npos); + + // Handle the connection. + Result> offMeshCon = m_navMesh + .getOffMeshConnectionPolyEndPoints(prevRef, polyRef); + if (offMeshCon.succeeded()) { + float[] startPos = offMeshCon.result.Item1; + float[] endPos = offMeshCon.result.Item2; + if (m_smoothPath.Count < MAX_SMOOTH) { + m_smoothPath.Add(startPos); + // Hack to make the dotted path not visible during off-mesh connection. + if ((m_smoothPath.Count & 1) != 0) { + m_smoothPath.Add(startPos); + } + } + // Move position at the other side of the off-mesh link. + vCopy(iterPos, endPos); + iterPos[1] = m_navQuery.getPolyHeight(polys[0], iterPos).result; + } + } + + // Store results. + if (m_smoothPath.Count < MAX_SMOOTH) { + m_smoothPath.Add(iterPos); + } + } + } + } else { + m_polys = null; + m_smoothPath = null; + } + } else if (m_toolMode == ToolMode.PATHFIND_STRAIGHT) { + if (m_sposSet && m_eposSet && m_startRef != 0 && m_endRef != 0) { + m_polys = m_navQuery.findPath(m_startRef, m_endRef, m_spos, m_epos, m_filter, + enableRaycast ? NavMeshQuery.DT_FINDPATH_ANY_ANGLE : 0, float.MaxValue).result; + if (0 < m_polys.Count) { + // In case of partial path, make sure the end point is clamped to the last polygon. + float[] epos = new float[] { m_epos[0], m_epos[1], m_epos[2] }; + if (m_polys[m_polys.Count - 1] != m_endRef) { + Result result = m_navQuery + .closestPointOnPoly(m_polys[m_polys.Count - 1], m_epos); + if (result.succeeded()) { + epos = result.result.getClosest(); + } + } + m_straightPath = m_navQuery.findStraightPath(m_spos, epos, m_polys, MAX_POLYS, + m_straightPathOptions).result; + } + } else { + m_straightPath = null; + } + } else if (m_toolMode == ToolMode.PATHFIND_SLICED) { + m_polys = null; + m_straightPath = null; + if (m_sposSet && m_eposSet && m_startRef != 0 && m_endRef != 0) { + m_pathFindStatus = m_navQuery.initSlicedFindPath(m_startRef, m_endRef, m_spos, m_epos, m_filter, + enableRaycast ? NavMeshQuery.DT_FINDPATH_ANY_ANGLE : 0, float.MaxValue); + } + } else if (m_toolMode == ToolMode.RAYCAST) { + m_straightPath = null; + if (m_sposSet && m_eposSet && m_startRef != 0) { + { + Result hit = m_navQuery.raycast(m_startRef, m_spos, m_epos, m_filter, 0, 0); + if (hit.succeeded()) { + m_polys = hit.result.path; + if (hit.result.t > 1) { + // No hit + m_hitPos = ArrayUtils.CopyOf(m_epos, m_epos.Length); + m_hitResult = false; + } else { + // Hit + m_hitPos = vLerp(m_spos, m_epos, hit.result.t); + m_hitNormal = ArrayUtils.CopyOf(hit.result.hitNormal, hit.result.hitNormal.Length); + m_hitResult = true; + } + // Adjust height. + if (hit.result.path.Count > 0) { + Result result = m_navQuery + .getPolyHeight(hit.result.path[hit.result.path.Count - 1], m_hitPos); + if (result.succeeded()) { + m_hitPos[1] = result.result; + } + } + } + m_straightPath = new(); + m_straightPath.Add(new StraightPathItem(m_spos, 0, 0)); + m_straightPath.Add(new StraightPathItem(m_hitPos, 0, 0)); + } + } + } else if (m_toolMode == ToolMode.DISTANCE_TO_WALL) { + m_distanceToWall = 0; + if (m_sposSet && m_startRef != 0) { + m_distanceToWall = 0.0f; + Result result = m_navQuery.findDistanceToWall(m_startRef, m_spos, 100.0f, + m_filter); + if (result.succeeded()) { + m_distanceToWall = result.result.getDistance(); + m_hitPos = result.result.getPosition(); + m_hitNormal = result.result.getNormal(); + } + } + } else if (m_toolMode == ToolMode.FIND_POLYS_IN_CIRCLE) { + if (m_sposSet && m_startRef != 0 && m_eposSet) { + float dx = m_epos[0] - m_spos[0]; + float dz = m_epos[2] - m_spos[2]; + float dist = (float) Math.Sqrt(dx * dx + dz * dz); + Result result = m_navQuery.findPolysAroundCircle(m_startRef, m_spos, dist, + m_filter); + if (result.succeeded()) { + m_polys = result.result.getRefs(); + m_parent = result.result.getParentRefs(); + } + } + } else if (m_toolMode == ToolMode.FIND_POLYS_IN_SHAPE) { + if (m_sposSet && m_startRef != 0 && m_eposSet) { + float nx = (m_epos[2] - m_spos[2]) * 0.25f; + float nz = -(m_epos[0] - m_spos[0]) * 0.25f; + float agentHeight = m_sample != null ? m_sample.getSettingsUI().getAgentHeight() : 0; + + m_queryPoly[0] = m_spos[0] + nx * 1.2f; + m_queryPoly[1] = m_spos[1] + agentHeight / 2; + m_queryPoly[2] = m_spos[2] + nz * 1.2f; + + m_queryPoly[3] = m_spos[0] - nx * 1.3f; + m_queryPoly[4] = m_spos[1] + agentHeight / 2; + m_queryPoly[5] = m_spos[2] - nz * 1.3f; + + m_queryPoly[6] = m_epos[0] - nx * 0.8f; + m_queryPoly[7] = m_epos[1] + agentHeight / 2; + m_queryPoly[8] = m_epos[2] - nz * 0.8f; + + m_queryPoly[9] = m_epos[0] + nx; + m_queryPoly[10] = m_epos[1] + agentHeight / 2; + m_queryPoly[11] = m_epos[2] + nz; + + Result result = m_navQuery.findPolysAroundShape(m_startRef, m_queryPoly, m_filter); + if (result.succeeded()) { + m_polys = result.result.getRefs(); + m_parent = result.result.getParentRefs(); + } + } + } else if (m_toolMode == ToolMode.FIND_LOCAL_NEIGHBOURHOOD) { + if (m_sposSet && m_startRef != 0) { + m_neighbourhoodRadius = m_sample.getSettingsUI().getAgentRadius() * 20.0f; + Result result = m_navQuery.findLocalNeighbourhood(m_startRef, m_spos, + m_neighbourhoodRadius, m_filter); + if (result.succeeded()) { + m_polys = result.result.getRefs(); + m_parent = result.result.getParentRefs(); + } + } + } else if (m_toolMode == ToolMode.RANDOM_POINTS_IN_CIRCLE) { + randomPoints.Clear(); + if (m_sposSet && m_startRef != 0 && m_eposSet) { + float dx = m_epos[0] - m_spos[0]; + float dz = m_epos[2] - m_spos[2]; + float dist = (float) Math.Sqrt(dx * dx + dz * dz); + PolygonByCircleConstraint constraint = constrainByCircle ? PolygonByCircleConstraint.strict() + : PolygonByCircleConstraint.noop(); + for (int i = 0; i < 200; i++) { + Result result = m_navQuery.findRandomPointAroundCircle(m_startRef, m_spos, dist, + m_filter, new NavMeshQuery.FRand(), constraint); + if (result.succeeded()) { + randomPoints.Add(result.result.getRandomPt()); + } + } + } + } + } + + public override void handleRender(NavMeshRenderer renderer) { + if (m_sample == null) { + return; + } + RecastDebugDraw dd = renderer.getDebugDraw(); + int startCol = duRGBA(128, 25, 0, 192); + int endCol = duRGBA(51, 102, 0, 129); + int pathCol = duRGBA(0, 0, 0, 64); + + float agentRadius = m_sample.getSettingsUI().getAgentRadius(); + float agentHeight = m_sample.getSettingsUI().getAgentHeight(); + float agentClimb = m_sample.getSettingsUI().getAgentMaxClimb(); + + if (m_sposSet) { + drawAgent(dd, m_spos, startCol); + } + if (m_eposSet) { + drawAgent(dd, m_epos, endCol); + } + dd.depthMask(true); + + NavMesh m_navMesh = m_sample.getNavMesh(); + if (m_navMesh == null) { + return; + } + + if (m_toolMode == ToolMode.PATHFIND_FOLLOW) { + dd.debugDrawNavMeshPoly(m_navMesh, m_startRef, startCol); + dd.debugDrawNavMeshPoly(m_navMesh, m_endRef, endCol); + + if (m_polys != null) { + foreach (long poly in m_polys) { + if (poly == m_startRef || poly == m_endRef) { + continue; + } + dd.debugDrawNavMeshPoly(m_navMesh, poly, pathCol); + } + } + if (m_smoothPath != null) { + dd.depthMask(false); + int spathCol = duRGBA(0, 0, 0, 220); + dd.begin(LINES, 3.0f); + for (int i = 0; i < m_smoothPath.Count; ++i) { + dd.vertex(m_smoothPath[i][0], m_smoothPath[i][1] + 0.1f, m_smoothPath[i][2], spathCol); + } + dd.end(); + dd.depthMask(true); + } + /* + if (m_pathIterNum) + { + duDebugDrawNavMeshPoly(&dd, *m_navMesh, m_pathIterPolys[0], DebugDraw.duRGBA(255,255,255,128)); + + dd.depthMask(false); + dd.begin(DebugDrawPrimitives.LINES, 1.0f); + + int prevCol = DebugDraw.duRGBA(255,192,0,220); + int curCol = DebugDraw.duRGBA(255,255,255,220); + int steerCol = DebugDraw.duRGBA(0,192,255,220); + + dd.vertex(m_prevIterPos[0],m_prevIterPos[1]-0.3f,m_prevIterPos[2], prevCol); + dd.vertex(m_prevIterPos[0],m_prevIterPos[1]+0.3f,m_prevIterPos[2], prevCol); + + dd.vertex(m_iterPos[0],m_iterPos[1]-0.3f,m_iterPos[2], curCol); + dd.vertex(m_iterPos[0],m_iterPos[1]+0.3f,m_iterPos[2], curCol); + + dd.vertex(m_prevIterPos[0],m_prevIterPos[1]+0.3f,m_prevIterPos[2], prevCol); + dd.vertex(m_iterPos[0],m_iterPos[1]+0.3f,m_iterPos[2], prevCol); + + dd.vertex(m_prevIterPos[0],m_prevIterPos[1]+0.3f,m_prevIterPos[2], steerCol); + dd.vertex(m_steerPos[0],m_steerPos[1]+0.3f,m_steerPos[2], steerCol); + + for (int i = 0; i < m_steerPointCount-1; ++i) + { + dd.vertex(m_steerPoints[i*3+0],m_steerPoints[i*3+1]+0.2f,m_steerPoints[i*3+2], duDarkenCol(steerCol)); + dd.vertex(m_steerPoints[(i+1)*3+0],m_steerPoints[(i+1)*3+1]+0.2f,m_steerPoints[(i+1)*3+2], duDarkenCol(steerCol)); + } + + dd.end(); + dd.depthMask(true); + } + */ + } else if (m_toolMode == ToolMode.PATHFIND_STRAIGHT || m_toolMode == ToolMode.PATHFIND_SLICED) { + dd.debugDrawNavMeshPoly(m_navMesh, m_startRef, startCol); + dd.debugDrawNavMeshPoly(m_navMesh, m_endRef, endCol); + + if (m_polys != null) { + foreach (long poly in m_polys) { + dd.debugDrawNavMeshPoly(m_navMesh, poly, pathCol); + } + } + if (m_straightPath != null) { + dd.depthMask(false); + int spathCol = duRGBA(64, 16, 0, 220); + int offMeshCol = duRGBA(128, 96, 0, 220); + dd.begin(LINES, 2.0f); + for (int i = 0; i < m_straightPath.Count - 1; ++i) { + StraightPathItem straightPathItem = m_straightPath[i]; + StraightPathItem straightPathItem2 = m_straightPath[i + 1]; + int col; + if ((straightPathItem.getFlags() & NavMeshQuery.DT_STRAIGHTPATH_OFFMESH_CONNECTION) != 0) { + col = offMeshCol; + } else { + col = spathCol; + } + dd.vertex(straightPathItem.getPos()[0], straightPathItem.getPos()[1] + 0.4f, + straightPathItem.getPos()[2], col); + dd.vertex(straightPathItem2.getPos()[0], straightPathItem2.getPos()[1] + 0.4f, + straightPathItem2.getPos()[2], col); + } + dd.end(); + dd.begin(POINTS, 6.0f); + for (int i = 0; i < m_straightPath.Count; ++i) { + StraightPathItem straightPathItem = m_straightPath[i]; + int col; + if ((straightPathItem.getFlags() & NavMeshQuery.DT_STRAIGHTPATH_START) != 0) { + col = startCol; + } else if ((straightPathItem.getFlags() & NavMeshQuery.DT_STRAIGHTPATH_END) != 0) { + col = endCol; + } else if ((straightPathItem.getFlags() & NavMeshQuery.DT_STRAIGHTPATH_OFFMESH_CONNECTION) != 0) { + col = offMeshCol; + } else { + col = spathCol; + } + dd.vertex(straightPathItem.getPos()[0], straightPathItem.getPos()[1] + 0.4f, + straightPathItem.getPos()[2], col); + } + dd.end(); + dd.depthMask(true); + } + } else if (m_toolMode == ToolMode.RAYCAST) { + dd.debugDrawNavMeshPoly(m_navMesh, m_startRef, startCol); + + if (m_straightPath != null) { + if (m_polys != null) { + foreach (long poly in m_polys) { + dd.debugDrawNavMeshPoly(m_navMesh, poly, pathCol); + } + } + + dd.depthMask(false); + int spathCol = m_hitResult ? duRGBA(64, 16, 0, 220) : duRGBA(240, 240, 240, 220); + dd.begin(LINES, 2.0f); + for (int i = 0; i < m_straightPath.Count - 1; ++i) { + StraightPathItem straightPathItem = m_straightPath[i]; + StraightPathItem straightPathItem2 = m_straightPath[i + 1]; + dd.vertex(straightPathItem.getPos()[0], straightPathItem.getPos()[1] + 0.4f, + straightPathItem.getPos()[2], spathCol); + dd.vertex(straightPathItem2.getPos()[0], straightPathItem2.getPos()[1] + 0.4f, + straightPathItem2.getPos()[2], spathCol); + } + dd.end(); + dd.begin(POINTS, 4.0f); + for (int i = 0; i < m_straightPath.Count; ++i) { + StraightPathItem straightPathItem = m_straightPath[i]; + dd.vertex(straightPathItem.getPos()[0], straightPathItem.getPos()[1] + 0.4f, + straightPathItem.getPos()[2], spathCol); + } + dd.end(); + + if (m_hitResult) { + int hitCol = duRGBA(0, 0, 0, 128); + dd.begin(LINES, 2.0f); + dd.vertex(m_hitPos[0], m_hitPos[1] + 0.4f, m_hitPos[2], hitCol); + dd.vertex(m_hitPos[0] + m_hitNormal[0] * agentRadius, + m_hitPos[1] + 0.4f + m_hitNormal[1] * agentRadius, + m_hitPos[2] + m_hitNormal[2] * agentRadius, hitCol); + dd.end(); + } + dd.depthMask(true); + } + } else if (m_toolMode == ToolMode.DISTANCE_TO_WALL) { + dd.debugDrawNavMeshPoly(m_navMesh, m_startRef, startCol); + dd.depthMask(false); + if (m_spos != null) { + dd.debugDrawCircle(m_spos[0], m_spos[1] + agentHeight / 2, m_spos[2], m_distanceToWall, + duRGBA(64, 16, 0, 220), 2.0f); + } + if (m_hitPos != null) { + dd.begin(LINES, 3.0f); + dd.vertex(m_hitPos[0], m_hitPos[1] + 0.02f, m_hitPos[2], duRGBA(0, 0, 0, 192)); + dd.vertex(m_hitPos[0], m_hitPos[1] + agentHeight, m_hitPos[2], duRGBA(0, 0, 0, 192)); + dd.end(); + } + dd.depthMask(true); + } else if (m_toolMode == ToolMode.FIND_POLYS_IN_CIRCLE) { + if (m_polys != null) { + for (int i = 0; i < m_polys.Count; i++) { + dd.debugDrawNavMeshPoly(m_navMesh, m_polys[i], pathCol); + dd.depthMask(false); + if (m_parent[i] != 0) { + dd.depthMask(false); + float[] p0 = getPolyCenter(m_navMesh, m_parent[i]); + float[] p1 = getPolyCenter(m_navMesh, m_polys[i]); + dd.debugDrawArc(p0[0], p0[1], p0[2], p1[0], p1[1], p1[2], 0.25f, 0.0f, 0.4f, + duRGBA(0, 0, 0, 128), 2.0f); + dd.depthMask(true); + } + dd.depthMask(true); + } + } + + if (m_sposSet && m_eposSet) { + dd.depthMask(false); + float dx = m_epos[0] - m_spos[0]; + float dz = m_epos[2] - m_spos[2]; + float dist = (float) Math.Sqrt(dx * dx + dz * dz); + dd.debugDrawCircle(m_spos[0], m_spos[1] + agentHeight / 2, m_spos[2], dist, duRGBA(64, 16, 0, 220), + 2.0f); + dd.depthMask(true); + } + } else if (m_toolMode == ToolMode.FIND_POLYS_IN_SHAPE) { + if (m_polys != null) { + for (int i = 0; i < m_polys.Count; i++) { + dd.debugDrawNavMeshPoly(m_navMesh, m_polys[i], pathCol); + dd.depthMask(false); + if (m_parent[i] != 0) { + dd.depthMask(false); + float[] p0 = getPolyCenter(m_navMesh, m_parent[i]); + float[] p1 = getPolyCenter(m_navMesh, m_polys[i]); + dd.debugDrawArc(p0[0], p0[1], p0[2], p1[0], p1[1], p1[2], 0.25f, 0.0f, 0.4f, + duRGBA(0, 0, 0, 128), 2.0f); + dd.depthMask(true); + } + dd.depthMask(true); + } + } + + if (m_sposSet && m_eposSet) { + dd.depthMask(false); + int col = duRGBA(64, 16, 0, 220); + dd.begin(LINES, 2.0f); + for (int i = 0, j = 3; i < 4; j = i++) { + dd.vertex(m_queryPoly[j * 3], m_queryPoly[j * 3 + 1], m_queryPoly[j * 3 + 2], col); + dd.vertex(m_queryPoly[i * 3], m_queryPoly[i * 3 + 1], m_queryPoly[i * 3 + 2], col); + } + dd.end(); + dd.depthMask(true); + } + } else if (m_toolMode == ToolMode.FIND_LOCAL_NEIGHBOURHOOD) { + if (m_polys != null) { + for (int i = 0; i < m_polys.Count; i++) { + dd.debugDrawNavMeshPoly(m_navMesh, m_polys[i], pathCol); + dd.depthMask(false); + if (m_parent[i] != 0) { + dd.depthMask(false); + float[] p0 = getPolyCenter(m_navMesh, m_parent[i]); + float[] p1 = getPolyCenter(m_navMesh, m_polys[i]); + dd.debugDrawArc(p0[0], p0[1], p0[2], p1[0], p1[1], p1[2], 0.25f, 0.0f, 0.4f, + duRGBA(0, 0, 0, 128), 2.0f); + dd.depthMask(true); + } + dd.depthMask(true); + if (m_sample.getNavMeshQuery() != null) { + Result result = m_sample.getNavMeshQuery() + .getPolyWallSegments(m_polys[i], false, m_filter); + if (result.succeeded()) { + dd.begin(LINES, 2.0f); + GetPolyWallSegmentsResult wallSegments = result.result; + for (int j = 0; j < wallSegments.getSegmentVerts().Count; ++j) { + float[] s = wallSegments.getSegmentVerts()[j]; + float[] s3 = new float[] { s[3], s[4], s[5] }; + // Skip too distant segments. + Tuple distSqr = DetourCommon.distancePtSegSqr2D(m_spos, s, 0, 3); + if (distSqr.Item1 > DemoMath.sqr(m_neighbourhoodRadius)) { + continue; + } + float[] delta = vSub(s3, s); + float[] p0 = vMad(s, delta, 0.5f); + float[] norm = new float[] { delta[2], 0, -delta[0] }; + vNormalize(norm); + float[] p1 = vMad(p0, norm, agentRadius * 0.5f); + // Skip backfacing segments. + if (wallSegments.getSegmentRefs()[j] != 0) { + int col = duRGBA(255, 255, 255, 32); + dd.vertex(s[0], s[1] + agentClimb, s[2], col); + dd.vertex(s[3], s[4] + agentClimb, s[5], col); + } else { + int col = duRGBA(192, 32, 16, 192); + if (DetourCommon.triArea2D(m_spos, s, s3) < 0.0f) { + col = duRGBA(96, 32, 16, 192); + } + dd.vertex(p0[0], p0[1] + agentClimb, p0[2], col); + dd.vertex(p1[0], p1[1] + agentClimb, p1[2], col); + + dd.vertex(s[0], s[1] + agentClimb, s[2], col); + dd.vertex(s[3], s[4] + agentClimb, s[5], col); + } + } + dd.end(); + } + } + + dd.depthMask(true); + } + + if (m_sposSet) { + dd.depthMask(false); + dd.debugDrawCircle(m_spos[0], m_spos[1] + agentHeight / 2, m_spos[2], m_neighbourhoodRadius, + duRGBA(64, 16, 0, 220), 2.0f); + dd.depthMask(true); + } + } + } else if (m_toolMode == ToolMode.RANDOM_POINTS_IN_CIRCLE) { + dd.depthMask(false); + dd.begin(POINTS, 4.0f); + int col = duRGBA(64, 16, 0, 220); + foreach (float[] point in randomPoints) { + dd.vertex(point[0], point[1] + 0.1f, point[2], col); + } + dd.end(); + if (m_sposSet && m_eposSet) { + dd.depthMask(false); + float dx = m_epos[0] - m_spos[0]; + float dz = m_epos[2] - m_spos[2]; + float dist = (float) Math.Sqrt(dx * dx + dz * dz); + dd.debugDrawCircle(m_spos[0], m_spos[1] + agentHeight / 2, m_spos[2], dist, duRGBA(64, 16, 0, 220), + 2.0f); + dd.depthMask(true); + } + dd.depthMask(true); + } + } + + private void drawAgent(RecastDebugDraw dd, float[] pos, int col) { + float r = m_sample.getSettingsUI().getAgentRadius(); + float h = m_sample.getSettingsUI().getAgentHeight(); + float c = m_sample.getSettingsUI().getAgentMaxClimb(); + dd.depthMask(false); + // Agent dimensions. + dd.debugDrawCylinderWire(pos[0] - r, pos[1] + 0.02f, pos[2] - r, pos[0] + r, pos[1] + h, pos[2] + r, col, 2.0f); + dd.debugDrawCircle(pos[0], pos[1] + c, pos[2], r, duRGBA(0, 0, 0, 64), 1.0f); + int colb = duRGBA(0, 0, 0, 196); + dd.begin(LINES); + dd.vertex(pos[0], pos[1] - c, pos[2], colb); + dd.vertex(pos[0], pos[1] + c, pos[2], colb); + dd.vertex(pos[0] - r / 2, pos[1] + 0.02f, pos[2], colb); + dd.vertex(pos[0] + r / 2, pos[1] + 0.02f, pos[2], colb); + dd.vertex(pos[0], pos[1] + 0.02f, pos[2] - r / 2, colb); + dd.vertex(pos[0], pos[1] + 0.02f, pos[2] + r / 2, colb); + dd.end(); + dd.depthMask(true); + } + + private float[] getPolyCenter(NavMesh navMesh, long refs) { + float[] center = new float[3]; + center[0] = 0; + center[1] = 0; + center[2] = 0; + Result> tileAndPoly = navMesh.getTileAndPolyByRef(refs); + if (tileAndPoly.succeeded()) { + MeshTile tile = tileAndPoly.result.Item1; + Poly poly = tileAndPoly.result.Item2; + for (int i = 0; i < poly.vertCount; ++i) { + int v = poly.verts[i] * 3; + center[0] += tile.data.verts[v]; + center[1] += tile.data.verts[v + 1]; + center[2] += tile.data.verts[v + 2]; + } + float s = 1.0f / poly.vertCount; + center[0] *= s; + center[1] *= s; + center[2] *= s; + } + return center; + } + + public override void handleUpdate(float dt) { + // TODO Auto-generated method stub + if (m_toolMode == ToolMode.PATHFIND_SLICED) { + NavMeshQuery m_navQuery = m_sample.getNavMeshQuery(); + if (m_pathFindStatus.isInProgress()) { + m_pathFindStatus = m_navQuery.updateSlicedFindPath(1).status; + } + if (m_pathFindStatus.isSuccess()) { + m_polys = m_navQuery.finalizeSlicedFindPath().result; + m_straightPath = null; + if (m_polys != null) { + // In case of partial path, make sure the end point is clamped to the last polygon. + float[] epos = new float[3]; + DetourCommon.vCopy(epos, m_epos); + if (m_polys[m_polys.Count - 1] != m_endRef) { + Result result = m_navQuery + .closestPointOnPoly(m_polys[m_polys.Count - 1], m_epos); + if (result.succeeded()) { + epos = result.result.getClosest(); + } + } + + { + Result> result = m_navQuery.findStraightPath(m_spos, epos, m_polys, + MAX_POLYS, NavMeshQuery.DT_STRAIGHTPATH_ALL_CROSSINGS); + if (result.succeeded()) + { + m_straightPath = result.result; + } + } + } + m_pathFindStatus = Status.FAILURE; + } + + } + } +} diff --git a/src/DotRecast.Recast.Demo/Tools/Tool.cs b/src/DotRecast.Recast.Demo/Tools/Tool.cs new file mode 100644 index 0000000..e6812fb --- /dev/null +++ b/src/DotRecast.Recast.Demo/Tools/Tool.cs @@ -0,0 +1,43 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using DotRecast.Recast.Demo.Draw; +using Silk.NET.Windowing; + +namespace DotRecast.Recast.Demo.Tools; + +public abstract class Tool { + + public abstract void setSample(Sample m_sample); + + public abstract void handleClick(float[] s, float[] p, bool shift); + + public abstract void handleRender(NavMeshRenderer renderer); + + public abstract void handleUpdate(float dt); + + public abstract void layout(IWindow ctx); + + public abstract string getName(); + + public virtual void handleClickRay(float[] start, float[] direction, bool shift) + { + // ... + } +} diff --git a/src/DotRecast.Recast.Demo/Tools/ToolUIModule.cs b/src/DotRecast.Recast.Demo/Tools/ToolUIModule.cs new file mode 100644 index 0000000..315f679 --- /dev/null +++ b/src/DotRecast.Recast.Demo/Tools/ToolUIModule.cs @@ -0,0 +1,26 @@ +/* +recast4j copyright (c) 2021 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using Silk.NET.Windowing; + +namespace DotRecast.Recast.Demo.Tools; + +public interface ToolUIModule { + + void layout(IWindow ctx); +} diff --git a/src/DotRecast.Recast.Demo/Tools/ToolsUI.cs b/src/DotRecast.Recast.Demo/Tools/ToolsUI.cs new file mode 100644 index 0000000..377d7b8 --- /dev/null +++ b/src/DotRecast.Recast.Demo/Tools/ToolsUI.cs @@ -0,0 +1,82 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2021 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using DotRecast.Core; +using DotRecast.Recast.Demo.UI; +using Silk.NET.Windowing; + +namespace DotRecast.Recast.Demo.Tools; + +public class ToolsUI : NuklearUIModule { + + //private readonly NkColor white = NkColor.create(); + private Tool currentTool; + private bool enabled; + private readonly Tool[] tools; + + public ToolsUI(params Tool[] tools) { + this.tools = tools; + } + + public bool layout(IWindow ctx, int x, int y, int width, int height, int mouseX, int mouseY) { + bool mouseInside = false; + // nk_rgb(255, 255, 255, white); + // try (MemoryStack stack = stackPush()) { + // NkRect rect = NkRect.mallocStack(stack); + // if (nk_begin(ctx, "Tools", nk_rect(5, 5, 250, height - 10, rect), NK_WINDOW_BORDER | NK_WINDOW_MOVABLE | NK_WINDOW_TITLE)) { + // if (enabled) { + // foreach (Tool tool in tools) { + // nk_layout_row_dynamic(ctx, 20, 1); + // if (nk_option_label(ctx, tool.getName(), tool == currentTool)) { + // currentTool = tool; + // } + // } + // nk_layout_row_dynamic(ctx, 3, 1); + // nk_spacing(ctx, 1); + // if (currentTool != null) { + // currentTool.layout(ctx); + // } + // } + // nk_window_get_bounds(ctx, rect); + // if (mouseX >= rect.x() && mouseX <= rect.x() + rect.w() && mouseY >= rect.y() && mouseY <= rect.y() + rect.h()) { + // mouseInside = true; + // } + // } + // nk_end(ctx); + // } + return mouseInside; + } + + public void setEnabled(bool enabled) { + this.enabled = enabled; + } + + public Tool getTool() { + return currentTool; + } + + public void setSample(Sample sample) { + tools.forEach(t => t.setSample(sample)); + } + + public void handleUpdate(float dt) { + tools.forEach(t => t.handleUpdate(dt)); + } + +} diff --git a/src/DotRecast.Recast.Demo/UI/Mouse.cs b/src/DotRecast.Recast.Demo/UI/Mouse.cs new file mode 100644 index 0000000..1147a8c --- /dev/null +++ b/src/DotRecast.Recast.Demo/UI/Mouse.cs @@ -0,0 +1,131 @@ +/* +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.Collections.Generic; +using Silk.NET.Input; +using Silk.NET.Windowing; + +namespace DotRecast.Recast.Demo.UI; + +public class Mouse { + + private double x; + private double y; + private double scrollX; + private double scrollY; + + private double px; + private double py; + private double pScrollX; + private double pScrollY; + private readonly HashSet pressed = new(); + private readonly List listeners = new(); + + public Mouse(IInputContext input) + { + foreach (IMouse mouse in input.Mice) + { + mouse.MouseDown += (mouse, button) => buttonPress((int)button, 0); + mouse.MouseUp += (mouse, button) => buttonRelease((int)button, 0); + // if (action == GLFW_PRESS) { + // buttonPress(button, mods); + // } else if (action == GLFW_RELEASE) { + // buttonRelease(button, mods); + // } + } + // glfwSetCursorPosCallback(window, (win, x, y) => cursorPos(x, y)); + // glfwSetScrollCallback(window, (win, x, y) => scroll(x, y)); + } + + public void cursorPos(double x, double y) { + foreach (MouseListener l in listeners) { + l.position(x, y); + } + this.x = x; + this.y = y; + } + + public void scroll(double xoffset, double yoffset) { + foreach (MouseListener l in listeners) { + l.scroll(xoffset, yoffset); + } + scrollX += xoffset; + scrollY += yoffset; + } + + public double getDX() { + return x - px; + } + + public double getDY() { + return y - py; + } + + public double getDScrollX() { + return scrollX - pScrollX; + } + + public double getDScrollY() { + return scrollY - pScrollY; + } + + public double getX() { + return x; + } + + public void setX(double x) { + this.x = x; + } + + public double getY() { + return y; + } + + public void setY(double y) { + this.y = y; + } + + public void setDelta() { + px = x; + py = y; + pScrollX = scrollX; + pScrollY = scrollY; + } + + public void buttonPress(int button, int mods) { + foreach (MouseListener l in listeners) { + l.button(button, mods, true); + } + pressed.Add(button); + } + + public void buttonRelease(int button, int mods) { + foreach (MouseListener l in listeners) { + l.button(button, mods, false); + } + pressed.Remove(button); + } + + public bool isPressed(int button) { + return pressed.Contains(button); + } + + public void addListener(MouseListener listener) { + listeners.Add(listener); + } +} diff --git a/src/DotRecast.Recast.Demo/UI/MouseListener.cs b/src/DotRecast.Recast.Demo/UI/MouseListener.cs new file mode 100644 index 0000000..79f482a --- /dev/null +++ b/src/DotRecast.Recast.Demo/UI/MouseListener.cs @@ -0,0 +1,28 @@ +/* +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Recast.Demo.UI; + +public interface MouseListener { + + void button(int button, int mods, bool down); + + void scroll(double xoffset, double yoffset); + + void position(double x, double y); + +} diff --git a/src/DotRecast.Recast.Demo/UI/NuklearGL.cs b/src/DotRecast.Recast.Demo/UI/NuklearGL.cs new file mode 100644 index 0000000..d5cdd22 --- /dev/null +++ b/src/DotRecast.Recast.Demo/UI/NuklearGL.cs @@ -0,0 +1,333 @@ +/* +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.IO; +using DotRecast.Core; +using DotRecast.Detour.Io; +using Microsoft.DotNet.PlatformAbstractions; + +namespace DotRecast.Recast.Demo.UI; + + +public class NuklearGL { + + private static readonly int BUFFER_INITIAL_SIZE = 4 * 1024; + private static readonly int MAX_VERTEX_BUFFER = 512 * 1024; + private static readonly int MAX_ELEMENT_BUFFER = 128 * 1024; + private static readonly int FONT_BITMAP_W = 1024; + private static readonly int FONT_BITMAP_H = 1024; + private static readonly int FONT_HEIGHT = 15; + + private readonly NuklearUI context; + // private readonly NkDrawNullTexture null_texture = NkDrawNullTexture.create(); + // private readonly NkBuffer cmds = NkBuffer.create(); + // private readonly NkUserFont default_font; + private readonly int program; + private readonly int uniform_tex; + private readonly int uniform_proj; + private readonly int vbo; + private readonly int ebo; + private readonly int vao; + //private readonly Buffer vertexLayout; + + public NuklearGL(NuklearUI context) { + this.context = context; + // nk_buffer_init(cmds, context.allocator, BUFFER_INITIAL_SIZE); + // vertexLayout = NkDrawVertexLayoutElement.create(4)// + // .position(0).attribute(NK_VERTEX_POSITION).format(NK_FORMAT_FLOAT).offset(0)// + // .position(1).attribute(NK_VERTEX_TEXCOORD).format(NK_FORMAT_FLOAT).offset(8)// + // .position(2).attribute(NK_VERTEX_COLOR).format(NK_FORMAT_R8G8B8A8).offset(16)// + // .position(3).attribute(NK_VERTEX_ATTRIBUTE_COUNT).format(NK_FORMAT_COUNT).offset(0)// + // .flip(); + // string NK_SHADER_VERSION = Platform.get() == Platform.MACOSX ? "#version 150\n" : "#version 300 es\n"; + // string vertex_shader = NK_SHADER_VERSION + "uniform mat4 ProjMtx;\n" + "in vec2 Position;\n" + // + "in vec2 TexCoord;\n" + "in vec4 Color;\n" + "out vec2 Frag_UV;\n" + "out vec4 Frag_Color;\n" + // + "void main() {\n" + " Frag_UV = TexCoord;\n" + " Frag_Color = Color;\n" + // + " gl_Position = ProjMtx * vec4(Position.xy, 0, 1);\n" + "}\n"; + // string fragment_shader = NK_SHADER_VERSION + "precision mediump float;\n" + "uniform sampler2D Texture;\n" + // + "in vec2 Frag_UV;\n" + "in vec4 Frag_Color;\n" + "out vec4 Out_Color;\n" + "void main(){\n" + // + " Out_Color = Frag_Color * texture(Texture, Frag_UV.st);\n" + "}\n"; + // + // program = glCreateProgram(); + // int vert_shdr = glCreateShader(GL_VERTEX_SHADER); + // int frag_shdr = glCreateShader(GL_FRAGMENT_SHADER); + // glShaderSource(vert_shdr, vertex_shader); + // glShaderSource(frag_shdr, fragment_shader); + // glCompileShader(vert_shdr); + // glCompileShader(frag_shdr); + // if (glGetShaderi(vert_shdr, GL_COMPILE_STATUS) != GL_TRUE) { + // throw new IllegalStateException(); + // } + // if (glGetShaderi(frag_shdr, GL_COMPILE_STATUS) != GL_TRUE) { + // throw new IllegalStateException(); + // } + // glAttachShader(program, vert_shdr); + // glAttachShader(program, frag_shdr); + // glLinkProgram(program); + // if (glGetProgrami(program, GL_LINK_STATUS) != GL_TRUE) { + // throw new IllegalStateException(); + // } + // + // uniform_tex = glGetUniformLocation(program, "Texture"); + // uniform_proj = glGetUniformLocation(program, "ProjMtx"); + // int attrib_pos = glGetAttribLocation(program, "Position"); + // int attrib_uv = glGetAttribLocation(program, "TexCoord"); + // int attrib_col = glGetAttribLocation(program, "Color"); + // + // // buffer setup + // vbo = glGenBuffers(); + // ebo = glGenBuffers(); + // vao = glGenVertexArrays(); + // + // glBindVertexArray(vao); + // glBindBuffer(GL_ARRAY_BUFFER, vbo); + // glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo); + // + // glEnableVertexAttribArray(attrib_pos); + // glEnableVertexAttribArray(attrib_uv); + // glEnableVertexAttribArray(attrib_col); + // + // glVertexAttribPointer(attrib_pos, 2, GL_FLOAT, false, 20, 0); + // glVertexAttribPointer(attrib_uv, 2, GL_FLOAT, false, 20, 8); + // glVertexAttribPointer(attrib_col, 4, GL_UNSIGNED_BYTE, true, 20, 16); + // + // // null texture setup + // int nullTexID = glGenTextures(); + // + // null_texture.texture().id(nullTexID); + // null_texture.uv().set(0.5f, 0.5f); + // + // glBindTexture(GL_TEXTURE_2D, nullTexID); + // try (MemoryStack stack = stackPush()) { + // glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8_REV, + // stack.ints(0xFFFFFFFF)); + // } + // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + // + // glBindTexture(GL_TEXTURE_2D, 0); + // glBindBuffer(GL_ARRAY_BUFFER, 0); + // glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + // glBindVertexArray(0); + // default_font = setupFont(); + // nk_style_set_font(context.ctx, default_font); + } + + private long setupFont() { + return 0; + // NkUserFont font = NkUserFont.create(); + // ByteBuffer ttf; + // try { + // ttf = IOUtils.toByteBuffer(Loader.ToBytes("fonts/DroidSans.ttf"), true); + // } catch (IOException e) { + // throw new RuntimeException(e); + // } + // + // int fontTexID = glGenTextures(); + // + // STBTTFontinfo fontInfo = STBTTFontinfo.malloc(); + // STBTTPackedchar.Buffer cdata = STBTTPackedchar.create(95); + // + // float scale; + // float descent; + // + // try (MemoryStack stack = stackPush()) { + // stbtt_InitFont(fontInfo, ttf); + // scale = stbtt_ScaleForPixelHeight(fontInfo, FONT_HEIGHT); + // + // int[] d = stack.mallocInt(1); + // stbtt_GetFontVMetrics(fontInfo, null, d, null); + // descent = d[0] * scale; + // + // ByteBuffer bitmap = memAlloc(FONT_BITMAP_W * FONT_BITMAP_H); + // + // STBTTPackContext pc = STBTTPackContext.mallocStack(stack); + // stbtt_PackBegin(pc, bitmap, FONT_BITMAP_W, FONT_BITMAP_H, 0, 1); + // stbtt_PackSetOversampling(pc, 1, 1); + // stbtt_PackFontRange(pc, ttf, 0, FONT_HEIGHT, 32, cdata); + // stbtt_PackEnd(pc); + // + // // Convert R8 to RGBA8 + // ByteBuffer texture = memAlloc(FONT_BITMAP_W * FONT_BITMAP_H * 4); + // for (int i = 0; i < bitmap.capacity(); i++) { + // texture.putInt((bitmap[i] << 24) | 0x00FFFFFF); + // } + // texture.flip(); + // + // glBindTexture(GL_TEXTURE_2D, fontTexID); + // glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, FONT_BITMAP_W, FONT_BITMAP_H, 0, GL_RGBA, + // GL_UNSIGNED_INT_8_8_8_8_REV, texture); + // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + // + // memFree(texture); + // memFree(bitmap); + // } + // int[] cache = new int[1024]; + // try (MemoryStack stack = stackPush()) { + // int[] advance = stack.mallocInt(1); + // for (int i = 0; i < 1024; i++) { + // stbtt_GetCodepointHMetrics(fontInfo, i, advance, null); + // cache[i] = advance[0]; + // } + // } + // font.width((handle, h, text, len) => { + // float text_width = 0; + // try (MemoryStack stack = stackPush()) { + // int[] unicode = stack.mallocInt(1); + // + // int glyph_len = nnk_utf_decode(text, memAddress(unicode), len); + // int text_len = glyph_len; + // + // if (glyph_len == 0) { + // return 0; + // } + // + // int[] advance = stack.mallocInt(1); + // while (text_len <= len && glyph_len != 0) { + // if (unicode[0] == NK_UTF_INVALID) { + // break; + // } + // + // /* query currently drawn glyph information */ + // // stbtt_GetCodepointHMetrics(fontInfo, unicode[0], advance, null); + // // text_width += advance[0] * scale; + // + // text_width += cache[unicode[0]] * scale; + // /* offset next glyph */ + // glyph_len = nnk_utf_decode(text + text_len, memAddress(unicode), len - text_len); + // text_len += glyph_len; + // } + // } + // return text_width; + // }).height(FONT_HEIGHT).query((handle, font_height, glyph, codepoint, next_codepoint) => { + // try (MemoryStack stack = stackPush()) { + // float[] x = stack.floats(0.0f); + // float[] y = stack.floats(0.0f); + // + // STBTTAlignedQuad q = STBTTAlignedQuad.mallocStack(stack); + // // int[] advance = stack.mallocInt(1); + // + // stbtt_GetPackedQuad(cdata, FONT_BITMAP_W, FONT_BITMAP_H, codepoint - 32, x, y, q, false); + // // stbtt_GetCodepointHMetrics(fontInfo, codepoint, advance, null); + // + // NkUserFontGlyph ufg = NkUserFontGlyph.create(glyph); + // + // ufg.width(q.x1() - q.x0()); + // ufg.height(q.y1() - q.y0()); + // ufg.offset().set(q.x0(), q.y0() + (FONT_HEIGHT + descent)); + // // ufg.xadvance(advance[0] * scale); + // ufg.xadvance(cache[codepoint] * scale); + // ufg.uv(0).set(q.s0(), q.t0()); + // ufg.uv(1).set(q.s1(), q.t1()); + // } + // }).texture().id(fontTexID); + // return font; + } + + void render(long win, int AA) { + // int width; + // int height; + // int display_width; + // int display_height; + // try (MemoryStack stack = stackPush()) { + // int[] w = stack.mallocInt(1); + // int[] h = stack.mallocInt(1); + // + // glfwGetWindowSize(win, w, h); + // width = w[0]; + // height = h[0]; + // + // glfwGetFramebufferSize(win, w, h); + // display_width = w[0]; + // display_height = h[0]; + // } + // + // try (MemoryStack stack = stackPush()) { + // // setup global state + // glEnable(GL_BLEND); + // glBlendEquation(GL_FUNC_ADD); + // glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + // glDisable(GL_CULL_FACE); + // glDisable(GL_DEPTH_TEST); + // glEnable(GL_SCISSOR_TEST); + // glActiveTexture(GL_TEXTURE0); + // + // glUseProgram(program); + // // setup program + // glUniform1i(uniform_tex, 0); + // glUniformMatrix4fv(uniform_proj, false, stack.floats(2.0f / width, 0.0f, 0.0f, 0.0f, 0.0f, -2.0f / height, + // 0.0f, 0.0f, 0.0f, 0.0f, -1.0f, 0.0f, -1.0f, 1.0f, 0.0f, 1.0f)); + // glViewport(0, 0, display_width, display_height); + // } + // // allocate vertex and element buffer + // glBindVertexArray(vao); + // glBindBuffer(GL_ARRAY_BUFFER, vbo); + // glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo); + // + // glBufferData(GL_ARRAY_BUFFER, MAX_VERTEX_BUFFER, GL_STREAM_DRAW); + // glBufferData(GL_ELEMENT_ARRAY_BUFFER, MAX_ELEMENT_BUFFER, GL_STREAM_DRAW); + // + // // load draw vertices & elements directly into vertex + element buffer + // ByteBuffer vertices = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY, MAX_VERTEX_BUFFER, null); + // ByteBuffer elements = glMapBuffer(GL_ELEMENT_ARRAY_BUFFER, GL_WRITE_ONLY, MAX_ELEMENT_BUFFER, null); + // + // try (MemoryStack stack = stackPush()) { + // // fill convert configuration + // NkConvertConfig config = NkConvertConfig.callocStack(stack).vertex_layout(vertexLayout).vertex_size(20) + // .vertex_alignment(4).null_texture(null_texture).circle_segment_count(22).curve_segment_count(22) + // .arc_segment_count(22).global_alpha(1f).shape_AA(AA).line_AA(AA); + // + // // setup buffers to load vertices and elements + // NkBuffer vbuf = NkBuffer.mallocStack(stack); + // NkBuffer ebuf = NkBuffer.mallocStack(stack); + // + // nk_buffer_init_fixed(vbuf, vertices/* , max_vertex_buffer */); + // nk_buffer_init_fixed(ebuf, elements/* , max_element_buffer */); + // nk_convert(context.ctx, cmds, vbuf, ebuf, config); + // } + // glUnmapBuffer(GL_ELEMENT_ARRAY_BUFFER); + // glUnmapBuffer(GL_ARRAY_BUFFER); + // + // // iterate over and execute each draw command + // float fb_scale_x = (float) display_width / (float) width; + // float fb_scale_y = (float) display_height / (float) height; + // + // long offset = NULL; + // for (NkDrawCommand cmd = nk__draw_begin(context.ctx, cmds); cmd != null; cmd = nk__draw_next(cmd, cmds, + // context.ctx)) { + // if (cmd.elem_count() == 0) { + // continue; + // } + // glBindTexture(GL_TEXTURE_2D, cmd.texture().id()); + // glScissor((int) (cmd.clip_rect().x() * fb_scale_x), + // (int) ((height - (int) (cmd.clip_rect().y() + cmd.clip_rect().h())) * fb_scale_y), + // (int) (cmd.clip_rect().w() * fb_scale_x), (int) (cmd.clip_rect().h() * fb_scale_y)); + // glDrawElements(GL_TRIANGLES, cmd.elem_count(), GL_UNSIGNED_SHORT, offset); + // offset += cmd.elem_count() * 2; + // } + // nk_clear(context.ctx); + // glUseProgram(0); + // glBindTexture(GL_TEXTURE_2D, 0); + // glBindBuffer(GL_ARRAY_BUFFER, 0); + // glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + // glBindVertexArray(0); + // glDisable(GL_BLEND); + // glDisable(GL_SCISSOR_TEST); + } +} diff --git a/src/DotRecast.Recast.Demo/UI/NuklearUI.cs b/src/DotRecast.Recast.Demo/UI/NuklearUI.cs new file mode 100644 index 0000000..6627804 --- /dev/null +++ b/src/DotRecast.Recast.Demo/UI/NuklearUI.cs @@ -0,0 +1,150 @@ +/* +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using Silk.NET.Input; +using Silk.NET.OpenGL; +using Silk.NET.OpenGL.Extensions.ImGui; +using Silk.NET.Windowing; + +namespace DotRecast.Recast.Demo.UI; + +public class NuklearUI { + + // readonly NkAllocator allocator; + private readonly IWindow _window; + private readonly GL _gl; + // readonly NkColor background; + // readonly NkColor white; + private readonly NuklearUIModule[] _modules; + private readonly NuklearGL glContext; + private bool mouseOverUI; + + public NuklearUI(IWindow window, IInputContext input, params NuklearUIModule[] modules) + { + var mouse = new Mouse(input); + _window = window; + _gl = GL.GetApi(window); + // allocator = NkAllocator.create(); + // allocator.alloc((handle, old, size) => { + // long mem = nmemAlloc(size); + // if (mem == NULL) { + // throw new OutOfMemoryError(); + // } + // return mem; + // + // }); + // allocator.mfree((handle, ptr) => nmemFree(ptr)); + // background = NkColor.create(); + // nk_rgb(28, 48, 62, background); + // white = NkColor.create(); + // nk_rgb(255, 255, 255, white); + // nk_init(ctx, allocator, null); + setupMouse(mouse); + // setupClipboard(window); + // glfwSetCharCallback(window, (w, codepoint) => nk_input_unicode(ctx, codepoint)); + // glContext = new NuklearGL(this); + _modules = modules; + } + + private void setupMouse(Mouse mouse) { + // mouse.addListener(new MouseListener() { + // + // @Override + // public void scroll(double xoffset, double yoffset) { + // if (mouseOverUI) { + // try (MemoryStack stack = stackPush()) { + // NkVec2 scroll = NkVec2.mallocStack(stack).x((float) xoffset).y((float) yoffset); + // nk_input_scroll(ctx, scroll); + // } + // } + // } + // + // @Override + // public void button(int button, int mods, bool down) { + // try (MemoryStack stack = stackPush()) { + // int nkButton; + // switch (button) { + // case GLFW_MOUSE_BUTTON_RIGHT: + // nkButton = NK_BUTTON_RIGHT; + // break; + // case GLFW_MOUSE_BUTTON_MIDDLE: + // nkButton = NK_BUTTON_MIDDLE; + // break; + // default: + // nkButton = NK_BUTTON_LEFT; + // } + // nk_input_button(ctx, nkButton, (int) mouse.getX(), (int) mouse.getY(), down); + // } + // } + // + // @Override + // public void position(double x, double y) { + // nk_input_motion(ctx, (int) x, (int) y); + // } + // }); + } + + private void setupClipboard(long window) { + // ctx.clip().copy((handle, text, len) => { + // if (len == 0) { + // return; + // } + // + // try (MemoryStack stack = stackPush()) { + // ByteBuffer str = stack.malloc(len + 1); + // memCopy(text, memAddress(str), len); + // str.put(len, (byte) 0); + // glfwSetClipboardString(window, str); + // } + // }); + // ctx.clip().paste((handle, edit) => { + // long text = nglfwGetClipboardString(window); + // if (text != NULL) { + // nnk_textedit_paste(edit, text, nnk_strlen(text)); + // } + // }); + } + + public void inputBegin() { + //nk_input_begin(ctx); + } + + public void inputEnd(IWindow win) { + // NkMouse mouse = ctx.input().mouse(); + // if (mouse.grab()) { + // glfwSetInputMode(win, GLFW_CURSOR, GLFW_CURSOR_HIDDEN); + // } else if (mouse.grabbed()) { + // float prevX = mouse.prev().x(); + // float prevY = mouse.prev().y(); + // glfwSetCursorPos(win, prevX, prevY); + // mouse.pos().x(prevX); + // mouse.pos().y(prevY); + // } else if (mouse.ungrab()) { + // glfwSetInputMode(win, GLFW_CURSOR, GLFW_CURSOR_NORMAL); + // } + // nk_input_end(ctx); + } + + public bool layout(IWindow ctx, int x, int y, int width, int height, int mouseX, int mouseY) { + mouseOverUI = false; + foreach (NuklearUIModule m in _modules) { + mouseOverUI = m.layout(ctx, x, y, width, height, mouseX, mouseY) | mouseOverUI; + } + return mouseOverUI; + } +} diff --git a/src/DotRecast.Recast.Demo/UI/NuklearUIHelper.cs b/src/DotRecast.Recast.Demo/UI/NuklearUIHelper.cs new file mode 100644 index 0000000..efda347 --- /dev/null +++ b/src/DotRecast.Recast.Demo/UI/NuklearUIHelper.cs @@ -0,0 +1,74 @@ +/* +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.Runtime.InteropServices.JavaScript; + +namespace DotRecast.Recast.Demo.UI; + +public class NuklearUIHelper { + + // public static void nk_color_rgb(IWindow ctx, NkColorf color) { + // try (MemoryStack stack = stackPush()) { + // if (nk_combo_begin_color(ctx, nk_rgb_cf(color, NkColor.mallocStack(stack)), + // NkVec2.mallocStack(stack).set(nk_widget_width(ctx), 400))) { + // nk_layout_row_dynamic(ctx, 120, 1); + // nk_color_picker(ctx, color, NK_RGB); + // nk_layout_row_dynamic(ctx, 20, 1); + // color.r(nk_propertyf(ctx, "#R:", 0, color.r(), 1f, 0.01f, 0.005f)); + // color.g(nk_propertyf(ctx, "#G:", 0, color.g(), 1f, 0.01f, 0.005f)); + // color.b(nk_propertyf(ctx, "#B:", 0, color.b(), 1f, 0.01f, 0.005f)); + // nk_combo_end(ctx); + // } + // } + // } + // + // public static > T nk_radio(IWindow ctx, T[] values, T currentValue, JSType.Function nameFormatter) { + // try (MemoryStack stack = stackPush()) { + // foreach (T v in values) { + // nk_layout_row_dynamic(ctx, 20, 1); + // if (nk_option_text(ctx, nameFormatter.apply(v), currentValue == v)) { + // currentValue = v; + // } + // } + // } + // return currentValue; + // } + // + // public static > T nk_combo(IWindow ctx, T[] values, T currentValue) { + // try (MemoryStack stack = stackPush()) { + // if (nk_combo_begin_label(ctx, currentValue.toString(), NkVec2.mallocStack(stack).set(nk_widget_width(ctx), 200))) { + // nk_layout_row_dynamic(ctx, 20, 1); + // foreach (T v in values) { + // if (nk_combo_item_label(ctx, v.toString(), NK_TEXT_LEFT)) { + // currentValue = v; + // } + // } + // nk_combo_end(ctx); + // } + // } + // return currentValue; + // } + // + // public static NkColorf nk_colorf(int r, int g, int b) { + // NkColorf color = NkColorf.create(); + // color.r(r / 255f); + // color.g(g / 255f); + // color.b(b / 255f); + // return color; + // } +} diff --git a/src/DotRecast.Recast.Demo/UI/NuklearUIModule.cs b/src/DotRecast.Recast.Demo/UI/NuklearUIModule.cs new file mode 100644 index 0000000..854c5f9 --- /dev/null +++ b/src/DotRecast.Recast.Demo/UI/NuklearUIModule.cs @@ -0,0 +1,29 @@ +/* +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using Silk.NET.Windowing; + +namespace DotRecast.Recast.Demo.UI; + + + +public interface NuklearUIModule { + + bool layout(IWindow ctx, int x, int y, int width, int height, int mouseX, int mouseY); + +} diff --git a/src/DotRecast.Recast.Demo/imgui.ini b/src/DotRecast.Recast.Demo/imgui.ini new file mode 100644 index 0000000..7e8c982 --- /dev/null +++ b/src/DotRecast.Recast.Demo/imgui.ini @@ -0,0 +1,5 @@ +[Window][Debug##Default] +Pos=280,129 +Size=400,400 +Collapsed=0 + diff --git a/src/DotRecast.Recast/AreaModification.cs b/src/DotRecast.Recast/AreaModification.cs new file mode 100644 index 0000000..a9fd498 --- /dev/null +++ b/src/DotRecast.Recast/AreaModification.cs @@ -0,0 +1,69 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +namespace DotRecast.Recast; + +public class AreaModification +{ + public readonly int RC_AREA_FLAGS_MASK = 0x3F; + + private readonly int value; + private readonly int mask; + + /** + * Mask is set to all available bits, which means value is fully applied + * + * @param value + * The area id to apply. [Limit: <= #RC_AREA_FLAGS_MASK] + */ + public AreaModification(int value) + { + this.value = value; + mask = RC_AREA_FLAGS_MASK; + } + + /** + * + * @param value + * The area id to apply. [Limit: <= #RC_AREA_FLAGS_MASK] + * @param mask + * Bitwise mask used when applying value. [Limit: <= #RC_AREA_FLAGS_MASK] + */ + public AreaModification(int value, int mask) + { + this.value = value; + this.mask = mask; + } + + public AreaModification(AreaModification other) + { + value = other.value; + mask = other.mask; + } + + public int getMaskedValue() + { + return value & mask; + } + + public int apply(int area) + { + return ((value & mask) | (area & ~mask)); + } +} \ No newline at end of file diff --git a/src/DotRecast.Recast/CompactCell.cs b/src/DotRecast.Recast/CompactCell.cs new file mode 100644 index 0000000..c2fbef0 --- /dev/null +++ b/src/DotRecast.Recast/CompactCell.cs @@ -0,0 +1,30 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +namespace DotRecast.Recast; + +/** Provides information on the content of a cell column in a compact heightfield. */ +public class CompactCell +{ + /** Index to the first span in the column. */ + public int index; + + /** Number of spans in the column. */ + public int count; +} \ No newline at end of file diff --git a/src/DotRecast.Recast/CompactHeightfield.cs b/src/DotRecast.Recast/CompactHeightfield.cs new file mode 100644 index 0000000..2dcc49f --- /dev/null +++ b/src/DotRecast.Recast/CompactHeightfield.cs @@ -0,0 +1,72 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +namespace DotRecast.Recast; + +/** A compact, static heightfield representing unobstructed space. */ +public class CompactHeightfield +{ + /** The width of the heightfield. (Along the x-axis in cell units.) */ + public int width; + + /** The height of the heightfield. (Along the z-axis in cell units.) */ + public int height; + + /** The number of spans in the heightfield. */ + public int spanCount; + + /** The walkable height used during the build of the field. (See: RecastConfig::walkableHeight) */ + public int walkableHeight; + + /** The walkable climb used during the build of the field. (See: RecastConfig::walkableClimb) */ + public int walkableClimb; + + /** The AABB border size used during the build of the field. (See: RecastConfig::borderSize) */ + public int borderSize; + + /** The maximum distance value of any span within the field. */ + public int maxDistance; + + /** The maximum region id of any span within the field. */ + public int maxRegions; + + /** The minimum bounds in world space. [(x, y, z)] */ + public float[] bmin = new float[3]; + + /** The maximum bounds in world space. [(x, y, z)] */ + public float[] bmax = new float[3]; + + /** The size of each cell. (On the xz-plane.) */ + public float cs; + + /** The height of each cell. (The minimum increment along the y-axis.) */ + public float ch; + + /** Array of cells. [Size: #width*#height] */ + public CompactCell[] cells; + + /** Array of spans. [Size: #spanCount] */ + public CompactSpan[] spans; + + /** Array containing border distance data. [Size: #spanCount] */ + public int[] dist; + + /** Array containing area id data. [Size: #spanCount] */ + public int[] areas; +} \ No newline at end of file diff --git a/src/DotRecast.Recast/CompactSpan.cs b/src/DotRecast.Recast/CompactSpan.cs new file mode 100644 index 0000000..5e43ec2 --- /dev/null +++ b/src/DotRecast.Recast/CompactSpan.cs @@ -0,0 +1,36 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +namespace DotRecast.Recast; + +/** Represents a span of unobstructed space within a compact heightfield. */ +public class CompactSpan +{ + /** The lower extent of the span. (Measured from the heightfield's base.) */ + public int y; + + /** The id of the region the span belongs to. (Or zero if not in a region.) */ + public int reg; + + /** Packed neighbor connection data. */ + public int con; + + /** The height of the span. (Measured from #y.) */ + public int h; +} \ No newline at end of file diff --git a/src/DotRecast.Recast/Contour.cs b/src/DotRecast.Recast/Contour.cs new file mode 100644 index 0000000..7712bfd --- /dev/null +++ b/src/DotRecast.Recast/Contour.cs @@ -0,0 +1,42 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +namespace DotRecast.Recast; + +/** Represents a simple, non-overlapping contour in field space. */ +public class Contour +{ + /** Simplified contour vertex and connection data. [Size: 4 * #nverts] */ + public int[] verts; + + /** The number of vertices in the simplified contour. */ + public int nverts; + + /** Raw contour vertex and connection data. [Size: 4 * #nrverts] */ + public int[] rverts; + + /** The number of vertices in the raw contour. */ + public int nrverts; + + /** The region id of the contour. */ + public int area; + + /** The area id of the contour. */ + public int reg; +} \ No newline at end of file diff --git a/src/DotRecast.Recast/ContourSet.cs b/src/DotRecast.Recast/ContourSet.cs new file mode 100644 index 0000000..928e689 --- /dev/null +++ b/src/DotRecast.Recast/ContourSet.cs @@ -0,0 +1,53 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.Collections.Generic; + +namespace DotRecast.Recast; + +/** Represents a group of related contours. */ +public class ContourSet +{ + /** A list of the contours in the set. */ + public List conts = new(); + + /** The minimum bounds in world space. [(x, y, z)] */ + public float[] bmin = new float[3]; + + /** The maximum bounds in world space. [(x, y, z)] */ + public float[] bmax = new float[3]; + + /** The size of each cell. (On the xz-plane.) */ + public float cs; + + /** The height of each cell. (The minimum increment along the y-axis.) */ + public float ch; + + /** The width of the set. (Along the x-axis in cell units.) */ + public int width; + + /** The height of the set. (Along the z-axis in cell units.) */ + public int height; + + /** The AABB border size used to generate the source data from which the contours were derived. */ + public int borderSize; + + /** The max edge error that this contour set was simplified with. */ + public float maxError; +} \ No newline at end of file diff --git a/src/DotRecast.Recast/ConvexVolume.cs b/src/DotRecast.Recast/ConvexVolume.cs new file mode 100644 index 0000000..1bafe70 --- /dev/null +++ b/src/DotRecast.Recast/ConvexVolume.cs @@ -0,0 +1,28 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +namespace DotRecast.Recast; + +public class ConvexVolume +{ + public float[] verts; + public float hmin; + public float hmax; + public AreaModification areaMod; +} \ No newline at end of file diff --git a/src/DotRecast.Recast/DotRecast.Recast.csproj b/src/DotRecast.Recast/DotRecast.Recast.csproj new file mode 100644 index 0000000..561560c --- /dev/null +++ b/src/DotRecast.Recast/DotRecast.Recast.csproj @@ -0,0 +1,12 @@ + + + + net7.0 + + + + + + + + diff --git a/src/DotRecast.Recast/Geom/ChunkyTriMesh.cs b/src/DotRecast.Recast/Geom/ChunkyTriMesh.cs new file mode 100644 index 0000000..ee36195 --- /dev/null +++ b/src/DotRecast.Recast/Geom/ChunkyTriMesh.cs @@ -0,0 +1,246 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.Collections.Generic; + +namespace DotRecast.Recast.Geom; + +public class ChunkyTriMesh +{ + private class BoundsItem + { + public readonly float[] bmin = new float[2]; + public readonly float[] bmax = new float[2]; + public int i; + } + + private class CompareItemX : IComparer + { + public int Compare(BoundsItem? a, BoundsItem? b) + { + return a.bmin[0].CompareTo(b.bmin[0]); + } + } + + private class CompareItemY : IComparer + { + public int Compare(BoundsItem? a, BoundsItem? b) + { + return a.bmin[1].CompareTo(b.bmin[1]); + } + } + + List nodes; + int ntris; + int maxTrisPerChunk; + + private void calcExtends(BoundsItem[] items, int imin, int imax, float[] bmin, float[] bmax) + { + bmin[0] = items[imin].bmin[0]; + bmin[1] = items[imin].bmin[1]; + + bmax[0] = items[imin].bmax[0]; + bmax[1] = items[imin].bmax[1]; + + for (int i = imin + 1; i < imax; ++i) + { + BoundsItem it = items[i]; + if (it.bmin[0] < bmin[0]) + { + bmin[0] = it.bmin[0]; + } + + if (it.bmin[1] < bmin[1]) + { + bmin[1] = it.bmin[1]; + } + + if (it.bmax[0] > bmax[0]) + { + bmax[0] = it.bmax[0]; + } + + if (it.bmax[1] > bmax[1]) + { + bmax[1] = it.bmax[1]; + } + } + } + + private int longestAxis(float x, float y) + { + return y > x ? 1 : 0; + } + + private void subdivide(BoundsItem[] items, int imin, int imax, int trisPerChunk, List nodes, + int[] inTris) + { + int inum = imax - imin; + + ChunkyTriMeshNode node = new ChunkyTriMeshNode(); + nodes.Add(node); + + if (inum <= trisPerChunk) + { + // Leaf + calcExtends(items, imin, imax, node.bmin, node.bmax); + + // Copy triangles. + node.i = nodes.Count; + node.tris = new int[inum * 3]; + + int dst = 0; + for (int i = imin; i < imax; ++i) + { + int src = items[i].i * 3; + node.tris[dst++] = inTris[src]; + node.tris[dst++] = inTris[src + 1]; + node.tris[dst++] = inTris[src + 2]; + } + } + else + { + // Split + calcExtends(items, imin, imax, node.bmin, node.bmax); + + int axis = longestAxis(node.bmax[0] - node.bmin[0], node.bmax[1] - node.bmin[1]); + + if (axis == 0) + { + Array.Sort(items, imin, imax - imin, new CompareItemX()); + // Sort along x-axis + } + else if (axis == 1) + { + Array.Sort(items, imin, imax - imin, new CompareItemY()); + // Sort along y-axis + } + + int isplit = imin + inum / 2; + + // Left + subdivide(items, imin, isplit, trisPerChunk, nodes, inTris); + // Right + subdivide(items, isplit, imax, trisPerChunk, nodes, inTris); + + // Negative index means escape. + node.i = -nodes.Count; + } + } + + public ChunkyTriMesh(float[] verts, int[] tris, int ntris, int trisPerChunk) + { + int nchunks = (ntris + trisPerChunk - 1) / trisPerChunk; + + nodes = new(nchunks); + this.ntris = ntris; + + // Build tree + BoundsItem[] items = new BoundsItem[ntris]; + + for (int i = 0; i < ntris; i++) + { + int t = i * 3; + BoundsItem it = items[i] = new BoundsItem(); + it.i = i; + // Calc triangle XZ bounds. + it.bmin[0] = it.bmax[0] = verts[tris[t] * 3 + 0]; + it.bmin[1] = it.bmax[1] = verts[tris[t] * 3 + 2]; + for (int j = 1; j < 3; ++j) + { + int v = tris[t + j] * 3; + if (verts[v] < it.bmin[0]) + { + it.bmin[0] = verts[v]; + } + + if (verts[v + 2] < it.bmin[1]) + { + it.bmin[1] = verts[v + 2]; + } + + if (verts[v] > it.bmax[0]) + { + it.bmax[0] = verts[v]; + } + + if (verts[v + 2] > it.bmax[1]) + { + it.bmax[1] = verts[v + 2]; + } + } + } + + subdivide(items, 0, ntris, trisPerChunk, nodes, tris); + + // Calc max tris per node. + maxTrisPerChunk = 0; + foreach (ChunkyTriMeshNode node in nodes) + { + bool isLeaf = node.i >= 0; + if (!isLeaf) + { + continue; + } + + if (node.tris.Length / 3 > maxTrisPerChunk) + { + maxTrisPerChunk = node.tris.Length / 3; + } + } + } + + private bool checkOverlapRect(float[] amin, float[] amax, float[] bmin, float[] bmax) + { + bool overlap = true; + overlap = (amin[0] > bmax[0] || amax[0] < bmin[0]) ? false : overlap; + overlap = (amin[1] > bmax[1] || amax[1] < bmin[1]) ? false : overlap; + return overlap; + } + + public List getChunksOverlappingRect(float[] bmin, float[] bmax) + { + // Traverse tree + List ids = new(); + int i = 0; + while (i < nodes.Count) + { + ChunkyTriMeshNode node = nodes[i]; + bool overlap = checkOverlapRect(bmin, bmax, node.bmin, node.bmax); + bool isLeafNode = node.i >= 0; + + if (isLeafNode && overlap) + { + ids.Add(node); + } + + if (overlap || isLeafNode) + { + i++; + } + else + { + i = -node.i; + } + } + + return ids; + } +} \ No newline at end of file diff --git a/src/DotRecast.Recast/Geom/ChunkyTriMeshNode.cs b/src/DotRecast.Recast/Geom/ChunkyTriMeshNode.cs new file mode 100644 index 0000000..c822707 --- /dev/null +++ b/src/DotRecast.Recast/Geom/ChunkyTriMeshNode.cs @@ -0,0 +1,9 @@ +namespace DotRecast.Recast.Geom; + +public class ChunkyTriMeshNode +{ + public readonly float[] bmin = new float[2]; + public readonly float[] bmax = new float[2]; + public int i; + public int[] tris; +} diff --git a/src/DotRecast.Recast/Geom/ConvexVolumeProvider.cs b/src/DotRecast.Recast/Geom/ConvexVolumeProvider.cs new file mode 100644 index 0000000..70140c9 --- /dev/null +++ b/src/DotRecast.Recast/Geom/ConvexVolumeProvider.cs @@ -0,0 +1,26 @@ +/* +recast4j copyright (c) 2021 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.Collections.Generic; + +namespace DotRecast.Recast.Geom; + +public interface ConvexVolumeProvider +{ + IList convexVolumes(); +} \ No newline at end of file diff --git a/src/DotRecast.Recast/Geom/InputGeomProvider.cs b/src/DotRecast.Recast/Geom/InputGeomProvider.cs new file mode 100644 index 0000000..41579de --- /dev/null +++ b/src/DotRecast.Recast/Geom/InputGeomProvider.cs @@ -0,0 +1,31 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.Collections.Generic; + +namespace DotRecast.Recast.Geom; + +public interface InputGeomProvider : ConvexVolumeProvider +{ + float[] getMeshBoundsMin(); + + float[] getMeshBoundsMax(); + + IEnumerable meshes(); +} \ No newline at end of file diff --git a/src/DotRecast.Recast/Geom/SimpleInputGeomProvider.cs b/src/DotRecast.Recast/Geom/SimpleInputGeomProvider.cs new file mode 100644 index 0000000..e89494d --- /dev/null +++ b/src/DotRecast.Recast/Geom/SimpleInputGeomProvider.cs @@ -0,0 +1,136 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace DotRecast.Recast.Geom; + +public class SimpleInputGeomProvider : InputGeomProvider +{ + public readonly float[] vertices; + public readonly int[] faces; + public readonly float[] normals; + readonly float[] bmin; + readonly float[] bmax; + readonly List volumes = new(); + + public SimpleInputGeomProvider(List vertexPositions, List meshFaces) + : this(mapVertices(vertexPositions), mapFaces(meshFaces)) + { + } + + private static int[] mapFaces(List meshFaces) + { + int[] faces = new int[meshFaces.Count]; + for (int i = 0; i < faces.Length; i++) + { + faces[i] = meshFaces[i]; + } + + return faces; + } + + private static float[] mapVertices(List vertexPositions) + { + float[] vertices = new float[vertexPositions.Count]; + for (int i = 0; i < vertices.Length; i++) + { + vertices[i] = vertexPositions[i]; + } + + return vertices; + } + + public SimpleInputGeomProvider(float[] vertices, int[] faces) + { + this.vertices = vertices; + this.faces = faces; + normals = new float[faces.Length]; + calculateNormals(); + bmin = new float[3]; + bmax = new float[3]; + RecastVectors.copy(bmin, vertices, 0); + RecastVectors.copy(bmax, vertices, 0); + for (int i = 1; i < vertices.Length / 3; i++) + { + RecastVectors.min(bmin, vertices, i * 3); + RecastVectors.max(bmax, vertices, i * 3); + } + } + + public float[] getMeshBoundsMin() + { + return bmin; + } + + public float[] getMeshBoundsMax() + { + return bmax; + } + + public IList convexVolumes() + { + return volumes; + } + + public void addConvexVolume(float[] verts, float minh, float maxh, AreaModification areaMod) + { + ConvexVolume vol = new ConvexVolume(); + vol.hmin = minh; + vol.hmax = maxh; + vol.verts = verts; + vol.areaMod = areaMod; + volumes.Add(vol); + } + + public IEnumerable meshes() + { + return ImmutableArray.Create(new TriMesh(vertices, faces)); + } + + public void calculateNormals() + { + for (int i = 0; i < faces.Length; i += 3) + { + int v0 = faces[i] * 3; + int v1 = faces[i + 1] * 3; + int v2 = faces[i + 2] * 3; + float[] e0 = new float[3], e1 = new float[3]; + for (int j = 0; j < 3; ++j) + { + e0[j] = vertices[v1 + j] - vertices[v0 + j]; + e1[j] = vertices[v2 + j] - vertices[v0 + j]; + } + + normals[i] = e0[1] * e1[2] - e0[2] * e1[1]; + normals[i + 1] = e0[2] * e1[0] - e0[0] * e1[2]; + normals[i + 2] = e0[0] * e1[1] - e0[1] * e1[0]; + float d = (float)Math.Sqrt(normals[i] * normals[i] + normals[i + 1] * normals[i + 1] + normals[i + 2] * normals[i + 2]); + if (d > 0) + { + d = 1.0f / d; + normals[i] *= d; + normals[i + 1] *= d; + normals[i + 2] *= d; + } + } + } +} \ No newline at end of file diff --git a/src/DotRecast.Recast/Geom/SingleTrimeshInputGeomProvider.cs b/src/DotRecast.Recast/Geom/SingleTrimeshInputGeomProvider.cs new file mode 100644 index 0000000..e060cec --- /dev/null +++ b/src/DotRecast.Recast/Geom/SingleTrimeshInputGeomProvider.cs @@ -0,0 +1,64 @@ +/* +recast4j copyright (c) 2021 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace DotRecast.Recast.Geom; + +public class SingleTrimeshInputGeomProvider : InputGeomProvider +{ + private readonly float[] bmin; + private readonly float[] bmax; + private readonly ImmutableArray _meshes; + + public SingleTrimeshInputGeomProvider(float[] vertices, int[] faces) + { + bmin = new float[3]; + bmax = new float[3]; + RecastVectors.copy(bmin, vertices, 0); + RecastVectors.copy(bmax, vertices, 0); + for (int i = 1; i < vertices.Length / 3; i++) + { + RecastVectors.min(bmin, vertices, i * 3); + RecastVectors.max(bmax, vertices, i * 3); + } + + _meshes = ImmutableArray.Create(new TriMesh(vertices, faces)); + } + + public float[] getMeshBoundsMin() + { + return bmin; + } + + public float[] getMeshBoundsMax() + { + return bmax; + } + + public IEnumerable meshes() + { + return _meshes; + } + + public IList convexVolumes() + { + return ImmutableArray.Empty; + } +} \ No newline at end of file diff --git a/src/DotRecast.Recast/Geom/TriMesh.cs b/src/DotRecast.Recast/Geom/TriMesh.cs new file mode 100644 index 0000000..a9102eb --- /dev/null +++ b/src/DotRecast.Recast/Geom/TriMesh.cs @@ -0,0 +1,51 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.Collections.Generic; + +namespace DotRecast.Recast.Geom; + +public class TriMesh +{ + private readonly float[] vertices; + private readonly int[] faces; + private readonly ChunkyTriMesh chunkyTriMesh; + + public TriMesh(float[] vertices, int[] faces) + { + this.vertices = vertices; + this.faces = faces; + chunkyTriMesh = new ChunkyTriMesh(vertices, faces, faces.Length / 3, 32); + } + + public int[] getTris() + { + return faces; + } + + public float[] getVerts() + { + return vertices; + } + + public List getChunksOverlappingRect(float[] bmin, float[] bmax) + { + return chunkyTriMesh.getChunksOverlappingRect(bmin, bmax); + } +} \ No newline at end of file diff --git a/src/DotRecast.Recast/Heightfield.cs b/src/DotRecast.Recast/Heightfield.cs new file mode 100644 index 0000000..2a406f8 --- /dev/null +++ b/src/DotRecast.Recast/Heightfield.cs @@ -0,0 +1,60 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +namespace DotRecast.Recast; + +/** Represents a heightfield layer within a layer set. */ +public class Heightfield +{ + /** The width of the heightfield. (Along the x-axis in cell units.) */ + public readonly int width; + + /** The height of the heightfield. (Along the z-axis in cell units.) */ + public readonly int height; + + /** The minimum bounds in world space. [(x, y, z)] */ + public readonly float[] bmin; + + /** The maximum bounds in world space. [(x, y, z)] */ + public readonly float[] bmax; + + /** The size of each cell. (On the xz-plane.) */ + public readonly float cs; + + /** The height of each cell. (The minimum increment along the y-axis.) */ + public readonly float ch; + + /** Heightfield of spans (width*height). */ + public readonly Span[] spans; + + /** Border size in cell units */ + public readonly int borderSize; + + public Heightfield(int width, int height, float[] bmin, float[] bmax, float cs, float ch, int borderSize) + { + this.width = width; + this.height = height; + this.bmin = bmin; + this.bmax = bmax; + this.cs = cs; + this.ch = ch; + this.borderSize = borderSize; + spans = new Span[width * height]; + } +} \ No newline at end of file diff --git a/src/DotRecast.Recast/HeightfieldLayerSet.cs b/src/DotRecast.Recast/HeightfieldLayerSet.cs new file mode 100644 index 0000000..f038825 --- /dev/null +++ b/src/DotRecast.Recast/HeightfieldLayerSet.cs @@ -0,0 +1,77 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +namespace DotRecast.Recast; + +/// Represents a set of heightfield layers. +/// @ingroup recast +/// @see rcAllocHeightfieldLayerSet, rcFreeHeightfieldLayerSet +public class HeightfieldLayerSet +{ + /// Represents a heightfield layer within a layer set. + /// @see rcHeightfieldLayerSet + public class HeightfieldLayer + { + public readonly float[] bmin = new float[3]; + + /// < The minimum bounds in world space. [(x, y, z)] + public readonly float[] bmax = new float[3]; + + /// < The maximum bounds in world space. [(x, y, z)] + public float cs; + + /// < The size of each cell. (On the xz-plane.) + public float ch; + + /// < The height of each cell. (The minimum increment along the y-axis.) + public int width; + + /// < The width of the heightfield. (Along the x-axis in cell units.) + public int height; + + /// < The height of the heightfield. (Along the z-axis in cell units.) + public int minx; + + /// < The minimum x-bounds of usable data. + public int maxx; + + /// < The maximum x-bounds of usable data. + public int miny; + + /// < The minimum y-bounds of usable data. (Along the z-axis.) + public int maxy; + + /// < The maximum y-bounds of usable data. (Along the z-axis.) + public int hmin; + + /// < The minimum height bounds of usable data. (Along the y-axis.) + public int hmax; + + /// < The maximum height bounds of usable data. (Along the y-axis.) + public int[] heights; + + /// < The heightfield. [Size: width * height] + public int[] areas; + + /// < Area ids. [Size: Same as #heights] + public int[] cons; /// < Packed neighbor connection information. [Size: Same as #heights] + } + + public HeightfieldLayer[] layers; /// < The layers in the set. [Size: #nlayers] +} \ No newline at end of file diff --git a/src/DotRecast.Recast/InputGeomReader.cs b/src/DotRecast.Recast/InputGeomReader.cs new file mode 100644 index 0000000..b77afae --- /dev/null +++ b/src/DotRecast.Recast/InputGeomReader.cs @@ -0,0 +1,5 @@ +namespace DotRecast.Recast; + +public class InputGeomReader +{ +} \ No newline at end of file diff --git a/src/DotRecast.Recast/ObjImporter.cs b/src/DotRecast.Recast/ObjImporter.cs new file mode 100644 index 0000000..cfe552d --- /dev/null +++ b/src/DotRecast.Recast/ObjImporter.cs @@ -0,0 +1,138 @@ +/* +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.Collections.Generic; +using System.IO; +using DotRecast.Recast.Geom; + +namespace DotRecast.Recast; + +public static class ObjImporter +{ + public class ObjImporterContext + { + public List vertexPositions = new(); + public List meshFaces = new(); + } + + public static InputGeomProvider load(byte[] chunck) + { + var context = loadContext(chunck); + return new SimpleInputGeomProvider(context.vertexPositions, context.meshFaces); + } + + public static ObjImporterContext loadContext(byte[] chunck) + { + ObjImporterContext context = new ObjImporterContext(); + try + { + using StreamReader reader = new StreamReader(new MemoryStream(chunck)); + string line; + while ((line = reader.ReadLine()) != null) + { + line = line.Trim(); + readLine(line, context); + } + } + catch (Exception e) + { + throw new Exception(e.Message, e); + } + + return context; + } + + + public static void readLine(string line, ObjImporterContext context) + { + if (line.StartsWith("v")) + { + readVertex(line, context); + } + else if (line.StartsWith("f")) + { + readFace(line, context); + } + } + + private static void readVertex(string line, ObjImporterContext context) + { + if (line.StartsWith("v ")) + { + float[] vert = readVector3f(line); + foreach (float vp in vert) + { + context.vertexPositions.Add(vp); + } + } + } + + private static float[] readVector3f(string line) + { + string[] v = line.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (v.Length < 4) + { + throw new Exception("Invalid vector, expected 3 coordinates, found " + (v.Length - 1)); + } + + return new float[] { float.Parse(v[1]), float.Parse(v[2]), float.Parse(v[3]) }; + } + + private static void readFace(string line, ObjImporterContext context) + { + string[] v = line.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (v.Length < 4) + { + throw new Exception("Invalid number of face vertices: 3 coordinates expected, found " + v.Length); + } + + for (int j = 0; j < v.Length - 3; j++) + { + context.meshFaces.Add(readFaceVertex(v[1], context)); + for (int i = 0; i < 2; i++) + { + context.meshFaces.Add(readFaceVertex(v[2 + j + i], context)); + } + } + } + + private static int readFaceVertex(string face, ObjImporterContext context) + { + string[] v = face.Split("/"); + return getIndex(int.Parse(v[0]), context.vertexPositions.Count); + } + + private static int getIndex(int posi, int size) + { + if (posi > 0) + { + posi--; + } + else if (posi < 0) + { + posi = size + posi; + } + else + { + throw new Exception("0 vertex index"); + } + + return posi; + } +} \ No newline at end of file diff --git a/src/DotRecast.Recast/PartitionType.cs b/src/DotRecast.Recast/PartitionType.cs new file mode 100644 index 0000000..9b944b7 --- /dev/null +++ b/src/DotRecast.Recast/PartitionType.cs @@ -0,0 +1,10 @@ +namespace DotRecast.Recast; + +/// < Tessellate edges between areas during contour +/// simplification. +public enum PartitionType +{ + WATERSHED, + MONOTONE, + LAYERS +} \ No newline at end of file diff --git a/src/DotRecast.Recast/PolyMesh.cs b/src/DotRecast.Recast/PolyMesh.cs new file mode 100644 index 0000000..c7fa513 --- /dev/null +++ b/src/DotRecast.Recast/PolyMesh.cs @@ -0,0 +1,69 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +Recast4J Copyright (c) 2015 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +namespace DotRecast.Recast; + +/** Represents a polygon mesh suitable for use in building a navigation mesh. */ +public class PolyMesh +{ + /** The mesh vertices. [Form: (x, y, z) coordinates * #nverts] */ + public int[] verts; + + /** Polygon and neighbor data. [Length: #maxpolys * 2 * #nvp] */ + public int[] polys; + + /** The region id assigned to each polygon. [Length: #maxpolys] */ + public int[] regs; + + /** The area id assigned to each polygon. [Length: #maxpolys] */ + public int[] areas; + + /** The number of vertices. */ + public int nverts; + + /** The number of polygons. */ + public int npolys; + + /** The maximum number of vertices per polygon. */ + public int nvp; + + /** The number of allocated polygons. */ + public int maxpolys; + + /** The user defined flags for each polygon. [Length: #maxpolys] */ + public int[] flags; + + /** The minimum bounds in world space. [(x, y, z)] */ + public readonly float[] bmin = new float[3]; + + /** The maximum bounds in world space. [(x, y, z)] */ + public readonly float[] bmax = new float[3]; + + /** The size of each cell. (On the xz-plane.) */ + public float cs; + + /** The height of each cell. (The minimum increment along the y-axis.) */ + public float ch; + + /** The AABB border size used to generate the source data from which the mesh was derived. */ + public int borderSize; + + /** The max error of the polygon edges in the mesh. */ + public float maxEdgeError; +} \ No newline at end of file diff --git a/src/DotRecast.Recast/PolyMeshDetail.cs b/src/DotRecast.Recast/PolyMeshDetail.cs new file mode 100644 index 0000000..2fe31fb --- /dev/null +++ b/src/DotRecast.Recast/PolyMeshDetail.cs @@ -0,0 +1,45 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +namespace DotRecast.Recast; + +/** + * Contains triangle meshes that represent detailed height data associated with the polygons in its associated polygon + * mesh object. + */ +public class PolyMeshDetail +{ + /** The sub-mesh data. [Size: 4*#nmeshes] */ + public int[] meshes; + + /** The mesh vertices. [Size: 3*#nverts] */ + public float[] verts; + + /** The mesh triangles. [Size: 4*#ntris] */ + public int[] tris; + + /** The number of sub-meshes defined by #meshes. */ + public int nmeshes; + + /** The number of vertices in #verts. */ + public int nverts; + + /** The number of triangles in #tris. */ + public int ntris; +} \ No newline at end of file diff --git a/src/DotRecast.Recast/Recast.cs b/src/DotRecast.Recast/Recast.cs new file mode 100644 index 0000000..421f048 --- /dev/null +++ b/src/DotRecast.Recast/Recast.cs @@ -0,0 +1,121 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; + +namespace DotRecast.Recast; + +using static RecastConstants; + +public class Recast +{ + void calcBounds(float[] verts, int nv, float[] bmin, float[] bmax) + { + for (int i = 0; i < 3; i++) + { + bmin[i] = verts[i]; + bmax[i] = verts[i]; + } + + for (int i = 1; i < nv; ++i) + { + for (int j = 0; j < 3; j++) + { + bmin[j] = Math.Min(bmin[j], verts[i * 3 + j]); + bmax[j] = Math.Max(bmax[j], verts[i * 3 + j]); + } + } + // Calculate bounding box. + } + + public static int[] calcGridSize(float[] bmin, float[] bmax, float cs) + { + return new int[] { (int)((bmax[0] - bmin[0]) / cs + 0.5f), (int)((bmax[2] - bmin[2]) / cs + 0.5f) }; + } + + public static int[] calcTileCount(float[] bmin, float[] bmax, float cs, int tileSizeX, int tileSizeZ) + { + int[] gwd = Recast.calcGridSize(bmin, bmax, cs); + int gw = gwd[0]; + int gd = gwd[1]; + int tw = (gw + tileSizeX - 1) / tileSizeX; + int td = (gd + tileSizeZ - 1) / tileSizeZ; + return new int[] { tw, td }; + } + + /// @par + /// + /// Modifies the area id of all triangles with a slope below the specified value. + /// + /// See the #rcConfig documentation for more information on the configuration parameters. + /// + /// @see rcHeightfield, rcClearUnwalkableTriangles, rcRasterizeTriangles + public static int[] markWalkableTriangles(Telemetry ctx, float walkableSlopeAngle, float[] verts, int[] tris, int nt, + AreaModification areaMod) + { + int[] areas = new int[nt]; + float walkableThr = (float)Math.Cos(walkableSlopeAngle / 180.0f * Math.PI); + float[] norm = new float[3]; + for (int i = 0; i < nt; ++i) + { + int tri = i * 3; + calcTriNormal(verts, tris[tri], tris[tri + 1], tris[tri + 2], norm); + // Check if the face is walkable. + if (norm[1] > walkableThr) + areas[i] = areaMod.apply(areas[i]); + } + + return areas; + } + + static void calcTriNormal(float[] verts, int v0, int v1, int v2, float[] norm) + { + float[] e0 = new float[3]; + float[] e1 = new float[3]; + RecastVectors.sub(e0, verts, v1 * 3, v0 * 3); + RecastVectors.sub(e1, verts, v2 * 3, v0 * 3); + RecastVectors.cross(norm, e0, e1); + RecastVectors.normalize(norm); + } + + /// @par + /// + /// Only sets the area id's for the unwalkable triangles. Does not alter the + /// area id's for walkable triangles. + /// + /// See the #rcConfig documentation for more information on the configuration parameters. + /// + /// @see rcHeightfield, rcClearUnwalkableTriangles, rcRasterizeTriangles + public static void clearUnwalkableTriangles(Telemetry ctx, float walkableSlopeAngle, float[] verts, int nv, + int[] tris, int nt, int[] areas) + { + float walkableThr = (float)Math.Cos(walkableSlopeAngle / 180.0f * Math.PI); + + float[] norm = new float[3]; + + for (int i = 0; i < nt; ++i) + { + int tri = i * 3; + calcTriNormal(verts, tris[tri], tris[tri + 1], tris[tri + 2], norm); + // Check if the face is walkable. + if (norm[1] <= walkableThr) + areas[i] = RC_NULL_AREA; + } + } +} \ No newline at end of file diff --git a/src/DotRecast.Recast/RecastArea.cs b/src/DotRecast.Recast/RecastArea.cs new file mode 100644 index 0000000..3a02d50 --- /dev/null +++ b/src/DotRecast.Recast/RecastArea.cs @@ -0,0 +1,584 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4J Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; + +namespace DotRecast.Recast; + +using static RecastConstants; + +public class RecastArea +{ + /// @par + /// + /// Basically, any spans that are closer to a boundary or obstruction than the specified radius + /// are marked as unwalkable. + /// + /// This method is usually called immediately after the heightfield has been built. + /// + /// @see rcCompactHeightfield, rcBuildCompactHeightfield, rcConfig::walkableRadius + public static void erodeWalkableArea(Telemetry ctx, int radius, CompactHeightfield chf) + { + int w = chf.width; + int h = chf.height; + ctx.startTimer("ERODE_AREA"); + + int[] dist = new int[chf.spanCount]; + Array.Fill(dist, 255); + // Mark boundary cells. + for (int y = 0; y < h; ++y) + { + for (int x = 0; x < w; ++x) + { + CompactCell c = chf.cells[x + y * w]; + for (int i = c.index, ni = c.index + c.count; i < ni; ++i) + { + if (chf.areas[i] == RC_NULL_AREA) + { + dist[i] = 0; + } + else + { + CompactSpan s = chf.spans[i]; + int nc = 0; + for (int dir = 0; dir < 4; ++dir) + { + if (RecastCommon.GetCon(s, dir) != RC_NOT_CONNECTED) + { + int nx = x + RecastCommon.GetDirOffsetX(dir); + int ny = y + RecastCommon.GetDirOffsetY(dir); + int nidx = chf.cells[nx + ny * w].index + RecastCommon.GetCon(s, dir); + if (chf.areas[nidx] != RC_NULL_AREA) + { + nc++; + } + } + } + + // At least one missing neighbour. + if (nc != 4) + dist[i] = 0; + } + } + } + } + + int nd; + + // Pass 1 + for (int y = 0; y < h; ++y) + { + for (int x = 0; x < w; ++x) + { + CompactCell c = chf.cells[x + y * w]; + for (int i = c.index, ni = c.index + c.count; i < ni; ++i) + { + CompactSpan s = chf.spans[i]; + + if (RecastCommon.GetCon(s, 0) != RC_NOT_CONNECTED) + { + // (-1,0) + int ax = x + RecastCommon.GetDirOffsetX(0); + int ay = y + RecastCommon.GetDirOffsetY(0); + int ai = chf.cells[ax + ay * w].index + RecastCommon.GetCon(s, 0); + CompactSpan @as = chf.spans[ai]; + nd = Math.Min(dist[ai] + 2, 255); + if (nd < dist[i]) + dist[i] = nd; + + // (-1,-1) + if (RecastCommon.GetCon(@as, 3) != RC_NOT_CONNECTED) + { + int aax = ax + RecastCommon.GetDirOffsetX(3); + int aay = ay + RecastCommon.GetDirOffsetY(3); + int aai = chf.cells[aax + aay * w].index + RecastCommon.GetCon(@as, 3); + nd = Math.Min(dist[aai] + 3, 255); + if (nd < dist[i]) + dist[i] = nd; + } + } + + if (RecastCommon.GetCon(s, 3) != RC_NOT_CONNECTED) + { + // (0,-1) + int ax = x + RecastCommon.GetDirOffsetX(3); + int ay = y + RecastCommon.GetDirOffsetY(3); + int ai = chf.cells[ax + ay * w].index + RecastCommon.GetCon(s, 3); + CompactSpan @as = chf.spans[ai]; + nd = Math.Min(dist[ai] + 2, 255); + if (nd < dist[i]) + dist[i] = nd; + + // (1,-1) + if (RecastCommon.GetCon(@as, 2) != RC_NOT_CONNECTED) + { + int aax = ax + RecastCommon.GetDirOffsetX(2); + int aay = ay + RecastCommon.GetDirOffsetY(2); + int aai = chf.cells[aax + aay * w].index + RecastCommon.GetCon(@as, 2); + nd = Math.Min(dist[aai] + 3, 255); + if (nd < dist[i]) + dist[i] = nd; + } + } + } + } + } + + // Pass 2 + for (int y = h - 1; y >= 0; --y) + { + for (int x = w - 1; x >= 0; --x) + { + CompactCell c = chf.cells[x + y * w]; + for (int i = c.index, ni = c.index + c.count; i < ni; ++i) + { + CompactSpan s = chf.spans[i]; + + if (RecastCommon.GetCon(s, 2) != RC_NOT_CONNECTED) + { + // (1,0) + int ax = x + RecastCommon.GetDirOffsetX(2); + int ay = y + RecastCommon.GetDirOffsetY(2); + int ai = chf.cells[ax + ay * w].index + RecastCommon.GetCon(s, 2); + CompactSpan @as = chf.spans[ai]; + nd = Math.Min(dist[ai] + 2, 255); + if (nd < dist[i]) + dist[i] = nd; + + // (1,1) + if (RecastCommon.GetCon(@as, 1) != RC_NOT_CONNECTED) + { + int aax = ax + RecastCommon.GetDirOffsetX(1); + int aay = ay + RecastCommon.GetDirOffsetY(1); + int aai = chf.cells[aax + aay * w].index + RecastCommon.GetCon(@as, 1); + nd = Math.Min(dist[aai] + 3, 255); + if (nd < dist[i]) + dist[i] = nd; + } + } + + if (RecastCommon.GetCon(s, 1) != RC_NOT_CONNECTED) + { + // (0,1) + int ax = x + RecastCommon.GetDirOffsetX(1); + int ay = y + RecastCommon.GetDirOffsetY(1); + int ai = chf.cells[ax + ay * w].index + RecastCommon.GetCon(s, 1); + CompactSpan @as = chf.spans[ai]; + nd = Math.Min(dist[ai] + 2, 255); + if (nd < dist[i]) + dist[i] = nd; + + // (-1,1) + if (RecastCommon.GetCon(@as, 0) != RC_NOT_CONNECTED) + { + int aax = ax + RecastCommon.GetDirOffsetX(0); + int aay = ay + RecastCommon.GetDirOffsetY(0); + int aai = chf.cells[aax + aay * w].index + RecastCommon.GetCon(@as, 0); + nd = Math.Min(dist[aai] + 3, 255); + if (nd < dist[i]) + dist[i] = nd; + } + } + } + } + } + + int thr = radius * 2; + for (int i = 0; i < chf.spanCount; ++i) + if (dist[i] < thr) + chf.areas[i] = RC_NULL_AREA; + + ctx.stopTimer("ERODE_AREA"); + } + + /// @par + /// + /// This filter is usually applied after applying area id's using functions + /// such as #rcMarkBoxArea, #rcMarkConvexPolyArea, and #rcMarkCylinderArea. + /// + /// @see rcCompactHeightfield + public bool medianFilterWalkableArea(Telemetry ctx, CompactHeightfield chf) + { + int w = chf.width; + int h = chf.height; + + ctx.startTimer("MEDIAN_AREA"); + + int[] areas = new int[chf.spanCount]; + + for (int y = 0; y < h; ++y) + { + for (int x = 0; x < w; ++x) + { + CompactCell c = chf.cells[x + y * w]; + for (int i = c.index, ni = c.index + c.count; i < ni; ++i) + { + CompactSpan s = chf.spans[i]; + if (chf.areas[i] == RC_NULL_AREA) + { + areas[i] = chf.areas[i]; + continue; + } + + int[] nei = new int[9]; + for (int j = 0; j < 9; ++j) + nei[j] = chf.areas[i]; + + for (int dir = 0; dir < 4; ++dir) + { + if (RecastCommon.GetCon(s, dir) != RC_NOT_CONNECTED) + { + int ax = x + RecastCommon.GetDirOffsetX(dir); + int ay = y + RecastCommon.GetDirOffsetY(dir); + int ai = chf.cells[ax + ay * w].index + RecastCommon.GetCon(s, dir); + if (chf.areas[ai] != RC_NULL_AREA) + nei[dir * 2 + 0] = chf.areas[ai]; + + CompactSpan @as = chf.spans[ai]; + int dir2 = (dir + 1) & 0x3; + if (RecastCommon.GetCon(@as, dir2) != RC_NOT_CONNECTED) + { + int ax2 = ax + RecastCommon.GetDirOffsetX(dir2); + int ay2 = ay + RecastCommon.GetDirOffsetY(dir2); + int ai2 = chf.cells[ax2 + ay2 * w].index + RecastCommon.GetCon(@as, dir2); + if (chf.areas[ai2] != RC_NULL_AREA) + nei[dir * 2 + 1] = chf.areas[ai2]; + } + } + } + + Array.Sort(nei); + areas[i] = nei[4]; + } + } + } + + chf.areas = areas; + + ctx.stopTimer("MEDIAN_AREA"); + + return true; + } + + /// @par + /// + /// The value of spacial parameters are in world units. + /// + /// @see rcCompactHeightfield, rcMedianFilterWalkableArea + public void markBoxArea(Telemetry ctx, float[] bmin, float[] bmax, AreaModification areaMod, CompactHeightfield chf) + { + ctx.startTimer("MARK_BOX_AREA"); + + int minx = (int)((bmin[0] - chf.bmin[0]) / chf.cs); + int miny = (int)((bmin[1] - chf.bmin[1]) / chf.ch); + int minz = (int)((bmin[2] - chf.bmin[2]) / chf.cs); + int maxx = (int)((bmax[0] - chf.bmin[0]) / chf.cs); + int maxy = (int)((bmax[1] - chf.bmin[1]) / chf.ch); + int maxz = (int)((bmax[2] - chf.bmin[2]) / chf.cs); + + if (maxx < 0) + return; + if (minx >= chf.width) + return; + if (maxz < 0) + return; + if (minz >= chf.height) + return; + + if (minx < 0) + minx = 0; + if (maxx >= chf.width) + maxx = chf.width - 1; + if (minz < 0) + minz = 0; + if (maxz >= chf.height) + maxz = chf.height - 1; + + for (int z = minz; z <= maxz; ++z) + { + for (int x = minx; x <= maxx; ++x) + { + CompactCell c = chf.cells[x + z * chf.width]; + for (int i = c.index, ni = c.index + c.count; i < ni; ++i) + { + CompactSpan s = chf.spans[i]; + if (s.y >= miny && s.y <= maxy) + { + if (chf.areas[i] != RC_NULL_AREA) + chf.areas[i] = areaMod.apply(chf.areas[i]); + } + } + } + } + + ctx.stopTimer("MARK_BOX_AREA"); + } + + static bool pointInPoly(float[] verts, float[] p) + { + bool c = false; + int i, j; + for (i = 0, j = verts.Length - 3; i < verts.Length; j = i, i += 3) + { + int vi = i; + int vj = j; + if (((verts[vi + 2] > p[2]) != (verts[vj + 2] > p[2])) + && (p[0] < (verts[vj] - verts[vi]) * (p[2] - verts[vi + 2]) / (verts[vj + 2] - verts[vi + 2]) + + verts[vi])) + c = !c; + } + + return c; + } + + /// @par + /// + /// The value of spacial parameters are in world units. + /// + /// The y-values of the polygon vertices are ignored. So the polygon is effectively + /// projected onto the xz-plane at @p hmin, then extruded to @p hmax. + /// + /// @see rcCompactHeightfield, rcMedianFilterWalkableArea + public static void markConvexPolyArea(Telemetry ctx, float[] verts, float hmin, float hmax, AreaModification areaMod, + CompactHeightfield chf) + { + ctx.startTimer("MARK_CONVEXPOLY_AREA"); + + float[] bmin = new float[3]; + float[] bmax = new float[3]; + RecastVectors.copy(bmin, verts, 0); + RecastVectors.copy(bmax, verts, 0); + for (int i = 3; i < verts.Length; i += 3) + { + RecastVectors.min(bmin, verts, i); + RecastVectors.max(bmax, verts, i); + } + + bmin[1] = hmin; + bmax[1] = hmax; + + int minx = (int)((bmin[0] - chf.bmin[0]) / chf.cs); + int miny = (int)((bmin[1] - chf.bmin[1]) / chf.ch); + int minz = (int)((bmin[2] - chf.bmin[2]) / chf.cs); + int maxx = (int)((bmax[0] - chf.bmin[0]) / chf.cs); + int maxy = (int)((bmax[1] - chf.bmin[1]) / chf.ch); + int maxz = (int)((bmax[2] - chf.bmin[2]) / chf.cs); + + if (maxx < 0) + return; + if (minx >= chf.width) + return; + if (maxz < 0) + return; + if (minz >= chf.height) + return; + + if (minx < 0) + minx = 0; + if (maxx >= chf.width) + maxx = chf.width - 1; + if (minz < 0) + minz = 0; + if (maxz >= chf.height) + maxz = chf.height - 1; + + // TODO: Optimize. + for (int z = minz; z <= maxz; ++z) + { + for (int x = minx; x <= maxx; ++x) + { + CompactCell c = chf.cells[x + z * chf.width]; + for (int i = c.index, ni = c.index + c.count; i < ni; ++i) + { + CompactSpan s = chf.spans[i]; + if (chf.areas[i] == RC_NULL_AREA) + continue; + if (s.y >= miny && s.y <= maxy) + { + float[] p = new float[3]; + p[0] = chf.bmin[0] + (x + 0.5f) * chf.cs; + p[1] = 0; + p[2] = chf.bmin[2] + (z + 0.5f) * chf.cs; + + if (pointInPoly(verts, p)) + { + chf.areas[i] = areaMod.apply(chf.areas[i]); + } + } + } + } + } + + ctx.stopTimer("MARK_CONVEXPOLY_AREA"); + } + + int offsetPoly(float[] verts, int nverts, float offset, float[] outVerts, int maxOutVerts) + { + float MITER_LIMIT = 1.20f; + + int n = 0; + + for (int i = 0; i < nverts; i++) + { + int a = (i + nverts - 1) % nverts; + int b = i; + int c = (i + 1) % nverts; + int va = a * 3; + int vb = b * 3; + int vc = c * 3; + float dx0 = verts[vb] - verts[va]; + float dy0 = verts[vb + 2] - verts[va + 2]; + float d0 = dx0 * dx0 + dy0 * dy0; + if (d0 > 1e-6f) + { + d0 = (float)(1.0f / Math.Sqrt(d0)); + dx0 *= d0; + dy0 *= d0; + } + + float dx1 = verts[vc] - verts[vb]; + float dy1 = verts[vc + 2] - verts[vb + 2]; + float d1 = dx1 * dx1 + dy1 * dy1; + if (d1 > 1e-6f) + { + d1 = (float)(1.0f / Math.Sqrt(d1)); + dx1 *= d1; + dy1 *= d1; + } + + float dlx0 = -dy0; + float dly0 = dx0; + float dlx1 = -dy1; + float dly1 = dx1; + float cross = dx1 * dy0 - dx0 * dy1; + float dmx = (dlx0 + dlx1) * 0.5f; + float dmy = (dly0 + dly1) * 0.5f; + float dmr2 = dmx * dmx + dmy * dmy; + bool bevel = dmr2 * MITER_LIMIT * MITER_LIMIT < 1.0f; + if (dmr2 > 1e-6f) + { + float scale = 1.0f / dmr2; + dmx *= scale; + dmy *= scale; + } + + if (bevel && cross < 0.0f) + { + if (n + 2 >= maxOutVerts) + return 0; + float d = (1.0f - (dx0 * dx1 + dy0 * dy1)) * 0.5f; + outVerts[n * 3 + 0] = verts[vb] + (-dlx0 + dx0 * d) * offset; + outVerts[n * 3 + 1] = verts[vb + 1]; + outVerts[n * 3 + 2] = verts[vb + 2] + (-dly0 + dy0 * d) * offset; + n++; + outVerts[n * 3 + 0] = verts[vb] + (-dlx1 - dx1 * d) * offset; + outVerts[n * 3 + 1] = verts[vb + 1]; + outVerts[n * 3 + 2] = verts[vb + 2] + (-dly1 - dy1 * d) * offset; + n++; + } + else + { + if (n + 1 >= maxOutVerts) + return 0; + outVerts[n * 3 + 0] = verts[vb] - dmx * offset; + outVerts[n * 3 + 1] = verts[vb + 1]; + outVerts[n * 3 + 2] = verts[vb + 2] - dmy * offset; + n++; + } + } + + return n; + } + + /// @par + /// + /// The value of spacial parameters are in world units. + /// + /// @see rcCompactHeightfield, rcMedianFilterWalkableArea + public void markCylinderArea(Telemetry ctx, float[] pos, float r, float h, AreaModification areaMod, + CompactHeightfield chf) + { + ctx.startTimer("MARK_CYLINDER_AREA"); + + float[] bmin = new float[3]; + float[] bmax = new float[3]; + bmin[0] = pos[0] - r; + bmin[1] = pos[1]; + bmin[2] = pos[2] - r; + bmax[0] = pos[0] + r; + bmax[1] = pos[1] + h; + bmax[2] = pos[2] + r; + float r2 = r * r; + + int minx = (int)((bmin[0] - chf.bmin[0]) / chf.cs); + int miny = (int)((bmin[1] - chf.bmin[1]) / chf.ch); + int minz = (int)((bmin[2] - chf.bmin[2]) / chf.cs); + int maxx = (int)((bmax[0] - chf.bmin[0]) / chf.cs); + int maxy = (int)((bmax[1] - chf.bmin[1]) / chf.ch); + int maxz = (int)((bmax[2] - chf.bmin[2]) / chf.cs); + + if (maxx < 0) + return; + if (minx >= chf.width) + return; + if (maxz < 0) + return; + if (minz >= chf.height) + return; + + if (minx < 0) + minx = 0; + if (maxx >= chf.width) + maxx = chf.width - 1; + if (minz < 0) + minz = 0; + if (maxz >= chf.height) + maxz = chf.height - 1; + + for (int z = minz; z <= maxz; ++z) + { + for (int x = minx; x <= maxx; ++x) + { + CompactCell c = chf.cells[x + z * chf.width]; + for (int i = c.index, ni = c.index + c.count; i < ni; ++i) + { + CompactSpan s = chf.spans[i]; + + if (chf.areas[i] == RC_NULL_AREA) + continue; + + if (s.y >= miny && s.y <= maxy) + { + float sx = chf.bmin[0] + (x + 0.5f) * chf.cs; + float sz = chf.bmin[2] + (z + 0.5f) * chf.cs; + float dx = sx - pos[0]; + float dz = sz - pos[2]; + + if (dx * dx + dz * dz < r2) + { + chf.areas[i] = areaMod.apply(chf.areas[i]); + } + } + } + } + } + + ctx.stopTimer("MARK_CYLINDER_AREA"); + } +} \ No newline at end of file diff --git a/src/DotRecast.Recast/RecastBuilder.cs b/src/DotRecast.Recast/RecastBuilder.cs new file mode 100644 index 0000000..4656e9c --- /dev/null +++ b/src/DotRecast.Recast/RecastBuilder.cs @@ -0,0 +1,319 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using DotRecast.Core; +using DotRecast.Recast.Geom; + +namespace DotRecast.Recast; + +public class RecastBuilder +{ + public interface RecastBuilderProgressListener + { + void onProgress(int completed, int total); + } + + private readonly RecastBuilderProgressListener progressListener; + + public RecastBuilder() + { + progressListener = null; + } + + public RecastBuilder(RecastBuilderProgressListener progressListener) + { + this.progressListener = progressListener; + } + + public List buildTiles(InputGeomProvider geom, RecastConfig cfg, TaskFactory taskFactory) { + float[] bmin = geom.getMeshBoundsMin(); + float[] bmax = geom.getMeshBoundsMax(); + int[] twh = Recast.calcTileCount(bmin, bmax, cfg.cs, cfg.tileSizeX, cfg.tileSizeZ); + int tw = twh[0]; + int th = twh[1]; + List results = new(); + if (null != taskFactory) + { + buildMultiThreadAsync(geom, cfg, bmin, bmax, tw, th, results, taskFactory, default); + } else { + buildSingleThreadAsync(geom, cfg, bmin, bmax, tw, th, results); + } + + return results; + } + + + public Task buildTilesAsync(InputGeomProvider geom, RecastConfig cfg, int threads, List results, TaskFactory taskFactory, CancellationToken cancellationToken) + { + float[] bmin = geom.getMeshBoundsMin(); + float[] bmax = geom.getMeshBoundsMax(); + int[] twh = Recast.calcTileCount(bmin, bmax, cfg.cs, cfg.tileSizeX, cfg.tileSizeZ); + int tw = twh[0]; + int th = twh[1]; + Task task; + if (1 < threads) + { + task = buildMultiThreadAsync(geom, cfg, bmin, bmax, tw, th, results, taskFactory, cancellationToken); + } + else + { + task = buildSingleThreadAsync(geom, cfg, bmin, bmax, tw, th, results); + } + + return task; + } + + private Task buildSingleThreadAsync(InputGeomProvider geom, RecastConfig cfg, float[] bmin, float[] bmax, + int tw, int th, List results) + { + AtomicInteger counter = new AtomicInteger(0); + for (int y = 0; y < th; ++y) + { + for (int x = 0; x < tw; ++x) + { + results.Add(buildTile(geom, cfg, bmin, bmax, x, y, counter, tw * th)); + } + } + + return Task.CompletedTask; + } + + private Task buildMultiThreadAsync(InputGeomProvider geom, RecastConfig cfg, float[] bmin, float[] bmax, + int tw, int th, List results, TaskFactory taskFactory, CancellationToken cancellationToken) + { + AtomicInteger counter = new AtomicInteger(0); + CountdownEvent latch = new CountdownEvent(tw * th); + List tasks = new(); + + for (int x = 0; x < tw; ++x) + { + for (int y = 0; y < th; ++y) + { + int tx = x; + int ty = y; + var task = taskFactory.StartNew(() => + { + if (cancellationToken.IsCancellationRequested) + return; + + try + { + RecastBuilderResult tile = buildTile(geom, cfg, bmin, bmax, tx, ty, counter, tw * th); + lock (results) + { + results.Add(tile); + } + } + catch (Exception e) + { + Console.WriteLine(e); + } + + + latch.Signal(); + }, cancellationToken); + + tasks.Add(task); + } + } + + try + { + latch.Wait(); + } + catch (ThreadInterruptedException e) + { + } + + return Task.WhenAll(tasks.ToArray()); + } + + private RecastBuilderResult buildTile(InputGeomProvider geom, RecastConfig cfg, float[] bmin, float[] bmax, int tx, + int ty, AtomicInteger counter, int total) + { + RecastBuilderResult result = build(geom, new RecastBuilderConfig(cfg, bmin, bmax, tx, ty)); + if (progressListener != null) + { + progressListener.onProgress(counter.IncrementAndGet(), total); + } + + return result; + } + + public RecastBuilderResult build(InputGeomProvider geom, RecastBuilderConfig builderCfg) + { + RecastConfig cfg = builderCfg.cfg; + Telemetry ctx = new Telemetry(); + // + // Step 1. Rasterize input polygon soup. + // + Heightfield solid = RecastVoxelization.buildSolidHeightfield(geom, builderCfg, ctx); + return build(builderCfg.tileX, builderCfg.tileZ, geom, cfg, solid, ctx); + } + + public RecastBuilderResult build(int tileX, int tileZ, ConvexVolumeProvider geom, RecastConfig cfg, Heightfield solid, + Telemetry ctx) + { + filterHeightfield(solid, cfg, ctx); + CompactHeightfield chf = buildCompactHeightfield(geom, cfg, ctx, solid); + + // Partition the heightfield so that we can use simple algorithm later + // to triangulate the walkable areas. + // There are 3 martitioning methods, each with some pros and cons: + // 1) Watershed partitioning + // - the classic Recast partitioning + // - creates the nicest tessellation + // - usually slowest + // - partitions the heightfield into nice regions without holes or + // overlaps + // - the are some corner cases where this method creates produces holes + // and overlaps + // - holes may appear when a small obstacles is close to large open area + // (triangulation can handle this) + // - overlaps may occur if you have narrow spiral corridors (i.e + // stairs), this make triangulation to fail + // * generally the best choice if you precompute the nacmesh, use this + // if you have large open areas + // 2) Monotone partioning + // - fastest + // - partitions the heightfield into regions without holes and overlaps + // (guaranteed) + // - creates long thin polygons, which sometimes causes paths with + // detours + // * use this if you want fast navmesh generation + // 3) Layer partitoining + // - quite fast + // - partitions the heighfield into non-overlapping regions + // - relies on the triangulation code to cope with holes (thus slower + // than monotone partitioning) + // - produces better triangles than monotone partitioning + // - does not have the corner cases of watershed partitioning + // - can be slow and create a bit ugly tessellation (still better than + // monotone) + // if you have large open areas with small obstacles (not a problem if + // you use tiles) + // * good choice to use for tiled navmesh with medium and small sized + // tiles + + if (cfg.partitionType == PartitionType.WATERSHED) + { + // Prepare for region partitioning, by calculating distance field + // along the walkable surface. + RecastRegion.buildDistanceField(ctx, chf); + // Partition the walkable surface into simple regions without holes. + RecastRegion.buildRegions(ctx, chf, cfg.minRegionArea, cfg.mergeRegionArea); + } + else if (cfg.partitionType == PartitionType.MONOTONE) + { + // Partition the walkable surface into simple regions without holes. + // Monotone partitioning does not need distancefield. + RecastRegion.buildRegionsMonotone(ctx, chf, cfg.minRegionArea, cfg.mergeRegionArea); + } + else + { + // Partition the walkable surface into simple regions without holes. + RecastRegion.buildLayerRegions(ctx, chf, cfg.minRegionArea); + } + + // + // Step 5. Trace and simplify region contours. + // + + // Create contours. + ContourSet cset = RecastContour.buildContours(ctx, chf, cfg.maxSimplificationError, cfg.maxEdgeLen, + RecastConstants.RC_CONTOUR_TESS_WALL_EDGES); + + // + // Step 6. Build polygons mesh from contours. + // + + PolyMesh pmesh = RecastMesh.buildPolyMesh(ctx, cset, cfg.maxVertsPerPoly); + + // + // Step 7. Create detail mesh which allows to access approximate height + // on each polygon. + // + PolyMeshDetail dmesh = cfg.buildMeshDetail + ? RecastMeshDetail.buildPolyMeshDetail(ctx, pmesh, chf, cfg.detailSampleDist, cfg.detailSampleMaxError) + : null; + return new RecastBuilderResult(tileX, tileZ, solid, chf, cset, pmesh, dmesh, ctx); + } + + /* + * Step 2. Filter walkable surfaces. + */ + private void filterHeightfield(Heightfield solid, RecastConfig cfg, Telemetry ctx) + { + // Once all geometry is rasterized, we do initial pass of filtering to + // remove unwanted overhangs caused by the conservative rasterization + // as well as filter spans where the character cannot possibly stand. + if (cfg.filterLowHangingObstacles) + { + RecastFilter.filterLowHangingWalkableObstacles(ctx, cfg.walkableClimb, solid); + } + + if (cfg.filterLedgeSpans) + { + RecastFilter.filterLedgeSpans(ctx, cfg.walkableHeight, cfg.walkableClimb, solid); + } + + if (cfg.filterWalkableLowHeightSpans) + { + RecastFilter.filterWalkableLowHeightSpans(ctx, cfg.walkableHeight, solid); + } + } + + /* + * Step 3. Partition walkable surface to simple regions. + */ + private CompactHeightfield buildCompactHeightfield(ConvexVolumeProvider volumeProvider, RecastConfig cfg, Telemetry ctx, + Heightfield solid) + { + // Compact the heightfield so that it is faster to handle from now on. + // This will result more cache coherent data as well as the neighbours + // between walkable cells will be calculated. + CompactHeightfield chf = RecastCompact.buildCompactHeightfield(ctx, cfg.walkableHeight, cfg.walkableClimb, solid); + + // Erode the walkable area by agent radius. + RecastArea.erodeWalkableArea(ctx, cfg.walkableRadius, chf); + // (Optional) Mark areas. + if (volumeProvider != null) + { + foreach (ConvexVolume vol in volumeProvider.convexVolumes()) + { + RecastArea.markConvexPolyArea(ctx, vol.verts, vol.hmin, vol.hmax, vol.areaMod, chf); + } + } + + return chf; + } + + public HeightfieldLayerSet buildLayers(InputGeomProvider geom, RecastBuilderConfig builderCfg) + { + Telemetry ctx = new Telemetry(); + Heightfield solid = RecastVoxelization.buildSolidHeightfield(geom, builderCfg, ctx); + filterHeightfield(solid, builderCfg.cfg, ctx); + CompactHeightfield chf = buildCompactHeightfield(geom, builderCfg.cfg, ctx, solid); + return RecastLayers.buildHeightfieldLayers(ctx, chf, builderCfg.cfg.walkableHeight); + } +} \ No newline at end of file diff --git a/src/DotRecast.Recast/RecastBuilderConfig.cs b/src/DotRecast.Recast/RecastBuilderConfig.cs new file mode 100644 index 0000000..2669493 --- /dev/null +++ b/src/DotRecast.Recast/RecastBuilderConfig.cs @@ -0,0 +1,99 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +namespace DotRecast.Recast; + +using static RecastVectors; + +public class RecastBuilderConfig +{ + public readonly RecastConfig cfg; + + public readonly int tileX; + public readonly int tileZ; + + /** The width of the field along the x-axis. [Limit: >= 0] [Units: vx] **/ + public readonly int width; + + /** The height of the field along the z-axis. [Limit: >= 0] [Units: vx] **/ + public readonly int height; + + /** The minimum bounds of the field's AABB. [(x, y, z)] [Units: wu] **/ + public readonly float[] bmin = new float[3]; + + /** The maximum bounds of the field's AABB. [(x, y, z)] [Units: wu] **/ + public readonly float[] bmax = new float[3]; + + public RecastBuilderConfig(RecastConfig cfg, float[] bmin, float[] bmax) : this(cfg, bmin, bmax, 0, 0) + { + } + + public RecastBuilderConfig(RecastConfig cfg, float[] bmin, float[] bmax, int tileX, int tileZ) + { + this.tileX = tileX; + this.tileZ = tileZ; + this.cfg = cfg; + copy(this.bmin, bmin); + copy(this.bmax, bmax); + if (cfg.useTiles) + { + float tsx = cfg.tileSizeX * cfg.cs; + float tsz = cfg.tileSizeZ * cfg.cs; + this.bmin[0] += tileX * tsx; + this.bmin[2] += tileZ * tsz; + this.bmax[0] = this.bmin[0] + tsx; + this.bmax[2] = this.bmin[2] + tsz; + // Expand the heighfield bounding box by border size to find the extents of geometry we need to build this + // tile. + // + // This is done in order to make sure that the navmesh tiles connect correctly at the borders, + // and the obstacles close to the border work correctly with the dilation process. + // No polygons (or contours) will be created on the border area. + // + // IMPORTANT! + // + // :''''''''': + // : +-----+ : + // : | | : + // : | |<--- tile to build + // : | | : + // : +-----+ :<-- geometry needed + // :.........: + // + // You should use this bounding box to query your input geometry. + // + // For example if you build a navmesh for terrain, and want the navmesh tiles to match the terrain tile size + // you will need to pass in data from neighbour terrain tiles too! In a simple case, just pass in all the 8 + // neighbours, + // or use the bounding box below to only pass in a sliver of each of the 8 neighbours. + this.bmin[0] -= cfg.borderSize * cfg.cs; + this.bmin[2] -= cfg.borderSize * cfg.cs; + this.bmax[0] += cfg.borderSize * cfg.cs; + this.bmax[2] += cfg.borderSize * cfg.cs; + width = cfg.tileSizeX + cfg.borderSize * 2; + height = cfg.tileSizeZ + cfg.borderSize * 2; + } + else + { + int[] wh = Recast.calcGridSize(this.bmin, this.bmax, cfg.cs); + width = wh[0]; + height = wh[1]; + } + } +} \ No newline at end of file diff --git a/src/DotRecast.Recast/RecastBuilderResult.cs b/src/DotRecast.Recast/RecastBuilderResult.cs new file mode 100644 index 0000000..deb7c18 --- /dev/null +++ b/src/DotRecast.Recast/RecastBuilderResult.cs @@ -0,0 +1,56 @@ +namespace DotRecast.Recast; + +public class RecastBuilderResult +{ + public readonly int tileX; + public readonly int tileZ; + private readonly CompactHeightfield chf; + private readonly ContourSet cs; + private readonly PolyMesh pmesh; + private readonly PolyMeshDetail dmesh; + private readonly Heightfield solid; + private readonly Telemetry telemetry; + + public RecastBuilderResult(int tileX, int tileZ, Heightfield solid, CompactHeightfield chf, ContourSet cs, PolyMesh pmesh, + PolyMeshDetail dmesh, Telemetry ctx) + { + this.tileX = tileX; + this.tileZ = tileZ; + this.solid = solid; + this.chf = chf; + this.cs = cs; + this.pmesh = pmesh; + this.dmesh = dmesh; + telemetry = ctx; + } + + public PolyMesh getMesh() + { + return pmesh; + } + + public PolyMeshDetail getMeshDetail() + { + return dmesh; + } + + public CompactHeightfield getCompactHeightfield() + { + return chf; + } + + public ContourSet getContourSet() + { + return cs; + } + + public Heightfield getSolidHeightfield() + { + return solid; + } + + public Telemetry getTelemetry() + { + return telemetry; + } +} diff --git a/src/DotRecast.Recast/RecastCommon.cs b/src/DotRecast.Recast/RecastCommon.cs new file mode 100644 index 0000000..fffb438 --- /dev/null +++ b/src/DotRecast.Recast/RecastCommon.cs @@ -0,0 +1,84 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; + +namespace DotRecast.Recast; + +public class RecastCommon +{ + /// Gets neighbor connection data for the specified direction. + /// @param[in] s The span to check. + /// @param[in] dir The direction to check. [Limits: 0 <= value < 4] + /// @return The neighbor connection data for the specified direction, + /// or #RC_NOT_CONNECTED if there is no connection. + public static int GetCon(CompactSpan s, int dir) + { + int shift = dir * 6; + return (s.con >> shift) & 0x3f; + } + + /// Gets the standard width (x-axis) offset for the specified direction. + /// @param[in] dir The direction. [Limits: 0 <= value < 4] + /// @return The width offset to apply to the current cell position to move + /// in the direction. + public static int GetDirOffsetX(int dir) + { + int[] offset = { -1, 0, 1, 0, }; + return offset[dir & 0x03]; + } + + /// Gets the standard height (z-axis) offset for the specified direction. + /// @param[in] dir The direction. [Limits: 0 <= value < 4] + /// @return The height offset to apply to the current cell position to move + /// in the direction. + public static int GetDirOffsetY(int dir) + { + int[] offset = { 0, 1, 0, -1 }; + return offset[dir & 0x03]; + } + + /// Gets the direction for the specified offset. One of x and y should be 0. + /// @param[in] x The x offset. [Limits: -1 <= value <= 1] + /// @param[in] y The y offset. [Limits: -1 <= value <= 1] + /// @return The direction that represents the offset. + public static int rcGetDirForOffset(int x, int y) + { + int[] dirs = { 3, 0, -1, 2, 1 }; + return dirs[((y + 1) << 1) + x]; + } + + /// Sets the neighbor connection data for the specified direction. + /// @param[in] s The span to update. + /// @param[in] dir The direction to set. [Limits: 0 <= value < 4] + /// @param[in] i The index of the neighbor span. + public static void SetCon(CompactSpan s, int dir, int i) + { + int shift = dir * 6; + int con = s.con; + s.con = (con & ~(0x3f << shift)) | ((i & 0x3f) << shift); + } + + public static int clamp(int v, int min, int max) + { + return Math.Max(Math.Min(max, v), min); + } + + +} \ No newline at end of file diff --git a/src/DotRecast.Recast/RecastCompact.cs b/src/DotRecast.Recast/RecastCompact.cs new file mode 100644 index 0000000..5871ffd --- /dev/null +++ b/src/DotRecast.Recast/RecastCompact.cs @@ -0,0 +1,186 @@ +/* +recast4j copyright (c) 2021 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; + +namespace DotRecast.Recast; + +using static RecastConstants; +using static RecastVectors; + +public class RecastCompact +{ + private const int MAX_LAYERS = RC_NOT_CONNECTED - 1; + private const int MAX_HEIGHT = RecastConstants.SPAN_MAX_HEIGHT; + + /// @par + /// + /// This is just the beginning of the process of fully building a compact heightfield. + /// Various filters may be applied, then the distance field and regions built. + /// E.g: #rcBuildDistanceField and #rcBuildRegions + /// + /// See the #rcConfig documentation for more information on the configuration parameters. + /// + /// @see rcAllocCompactHeightfield, rcHeightfield, rcCompactHeightfield, rcConfig + public static CompactHeightfield buildCompactHeightfield(Telemetry ctx, int walkableHeight, int walkableClimb, + Heightfield hf) + { + ctx.startTimer("BUILD_COMPACTHEIGHTFIELD"); + + CompactHeightfield chf = new CompactHeightfield(); + int w = hf.width; + int h = hf.height; + int spanCount = getHeightFieldSpanCount(hf); + + // Fill in header. + chf.width = w; + chf.height = h; + chf.borderSize = hf.borderSize; + chf.spanCount = spanCount; + chf.walkableHeight = walkableHeight; + chf.walkableClimb = walkableClimb; + chf.maxRegions = 0; + copy(chf.bmin, hf.bmin); + copy(chf.bmax, hf.bmax); + chf.bmax[1] += walkableHeight * hf.ch; + chf.cs = hf.cs; + chf.ch = hf.ch; + chf.cells = new CompactCell[w * h]; + chf.spans = new CompactSpan[spanCount]; + chf.areas = new int[spanCount]; + for (int i = 0; i < chf.cells.Length; i++) + { + chf.cells[i] = new CompactCell(); + } + + for (int i = 0; i < chf.spans.Length; i++) + { + chf.spans[i] = new CompactSpan(); + } + + // Fill in cells and spans. + int idx = 0; + for (int y = 0; y < h; ++y) + { + for (int x = 0; x < w; ++x) + { + Span s = hf.spans[x + y * w]; + // If there are no spans at this cell, just leave the data to index=0, count=0. + if (s == null) + continue; + CompactCell c = chf.cells[x + y * w]; + c.index = idx; + c.count = 0; + while (s != null) + { + if (s.area != RC_NULL_AREA) + { + int bot = s.smax; + int top = s.next != null ? (int)s.next.smin : MAX_HEIGHT; + chf.spans[idx].y = RecastCommon.clamp(bot, 0, MAX_HEIGHT); + chf.spans[idx].h = RecastCommon.clamp(top - bot, 0, MAX_HEIGHT); + chf.areas[idx] = s.area; + idx++; + c.count++; + } + + s = s.next; + } + } + } + + // Find neighbour connections. + int tooHighNeighbour = 0; + for (int y = 0; y < h; ++y) + { + for (int x = 0; x < w; ++x) + { + CompactCell c = chf.cells[x + y * w]; + for (int i = c.index, ni = c.index + c.count; i < ni; ++i) + { + CompactSpan s = chf.spans[i]; + + for (int dir = 0; dir < 4; ++dir) + { + RecastCommon.SetCon(s, dir, RC_NOT_CONNECTED); + int nx = x + RecastCommon.GetDirOffsetX(dir); + int ny = y + RecastCommon.GetDirOffsetY(dir); + // First check that the neighbour cell is in bounds. + if (nx < 0 || ny < 0 || nx >= w || ny >= h) + continue; + + // Iterate over all neighbour spans and check if any of the is + // accessible from current cell. + CompactCell nc = chf.cells[nx + ny * w]; + for (int k = nc.index, nk = nc.index + nc.count; k < nk; ++k) + { + CompactSpan ns = chf.spans[k]; + int bot = Math.Max(s.y, ns.y); + int top = Math.Min(s.y + s.h, ns.y + ns.h); + + // Check that the gap between the spans is walkable, + // and that the climb height between the gaps is not too high. + if ((top - bot) >= walkableHeight && Math.Abs(ns.y - s.y) <= walkableClimb) + { + // Mark direction as walkable. + int lidx = k - nc.index; + if (lidx < 0 || lidx > MAX_LAYERS) + { + tooHighNeighbour = Math.Max(tooHighNeighbour, lidx); + continue; + } + + RecastCommon.SetCon(s, dir, lidx); + break; + } + } + } + } + } + } + + if (tooHighNeighbour > MAX_LAYERS) + { + throw new Exception("rcBuildCompactHeightfield: Heightfield has too many layers " + tooHighNeighbour + + " (max: " + MAX_LAYERS + ")"); + } + + ctx.stopTimer("BUILD_COMPACTHEIGHTFIELD"); + return chf; + } + + private static int getHeightFieldSpanCount(Heightfield hf) + { + int w = hf.width; + int h = hf.height; + int spanCount = 0; + for (int y = 0; y < h; ++y) + { + for (int x = 0; x < w; ++x) + { + for (Span s = hf.spans[x + y * w]; s != null; s = s.next) + { + if (s.area != RC_NULL_AREA) + spanCount++; + } + } + } + + return spanCount; + } +} \ No newline at end of file diff --git a/src/DotRecast.Recast/RecastConfig.cs b/src/DotRecast.Recast/RecastConfig.cs new file mode 100644 index 0000000..de01a56 --- /dev/null +++ b/src/DotRecast.Recast/RecastConfig.cs @@ -0,0 +1,185 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; + +namespace DotRecast.Recast; + +using static RecastConstants; + +public class RecastConfig +{ + public readonly PartitionType partitionType; + + public readonly bool useTiles; + + /** The width/depth size of tile's on the xz-plane. [Limit: >= 0] [Units: vx] **/ + public readonly int tileSizeX; + + public readonly int tileSizeZ; + + /** The xz-plane cell size to use for fields. [Limit: > 0] [Units: wu] **/ + public readonly float cs; + + /** The y-axis cell size to use for fields. [Limit: > 0] [Units: wu] **/ + public readonly float ch; + + /** The maximum slope that is considered walkable. [Limits: 0 <= value < 90] [Units: Degrees] **/ + public readonly float walkableSlopeAngle; + + /** + * Minimum floor to 'ceiling' height that will still allow the floor area to be considered walkable. [Limit: >= 3] + * [Units: vx] + **/ + public readonly int walkableHeight; + + /** Maximum ledge height that is considered to still be traversable. [Limit: >=0] [Units: vx] **/ + public readonly int walkableClimb; + + /** + * The distance to erode/shrink the walkable area of the heightfield away from obstructions. [Limit: >=0] [Units: + * vx] + **/ + public readonly int walkableRadius; + + /** The maximum allowed length for contour edges along the border of the mesh. [Limit: >=0] [Units: vx] **/ + public readonly int maxEdgeLen; + + /** + * The maximum distance a simplfied contour's border edges should deviate the original raw contour. [Limit: >=0] + * [Units: vx] + **/ + public readonly float maxSimplificationError; + + /** The minimum number of cells allowed to form isolated island areas. [Limit: >=0] [Units: vx] **/ + public readonly int minRegionArea; + + /** + * Any regions with a span count smaller than this value will, if possible, be merged with larger regions. [Limit: + * >=0] [Units: vx] + **/ + public readonly int mergeRegionArea; + + /** + * The maximum number of vertices allowed for polygons generated during the contour to polygon conversion process. + * [Limit: >= 3] + **/ + public readonly int maxVertsPerPoly; + + /** + * Sets the sampling distance to use when generating the detail mesh. (For height detail only.) [Limits: 0 or >= + * 0.9] [Units: wu] + **/ + public readonly float detailSampleDist; + + /** + * The maximum distance the detail mesh surface should deviate from heightfield data. (For height detail only.) + * [Limit: >=0] [Units: wu] + **/ + public readonly float detailSampleMaxError; + + public readonly AreaModification walkableAreaMod; + public readonly bool filterLowHangingObstacles; + public readonly bool filterLedgeSpans; + public readonly bool filterWalkableLowHeightSpans; + + /** Set to false to disable building detailed mesh **/ + public readonly bool buildMeshDetail; + + /** The size of the non-navigable border around the heightfield. [Limit: >=0] [Units: vx] **/ + public readonly int borderSize; + + /** Set of original settings passed in world units */ + public readonly float minRegionAreaWorld; + + public readonly float mergeRegionAreaWorld; + public readonly float walkableHeightWorld; + public readonly float walkableClimbWorld; + public readonly float walkableRadiusWorld; + public readonly float maxEdgeLenWorld; + + /** + * Non-tiled build configuration + */ + public RecastConfig(PartitionType partitionType, float cellSize, float cellHeight, float agentHeight, float agentRadius, + float agentMaxClimb, float agentMaxSlope, int regionMinSize, int regionMergeSize, float edgeMaxLen, + float edgeMaxError, int vertsPerPoly, float detailSampleDist, float detailSampleMaxError, + AreaModification walkableAreaMod) : this(partitionType, cellSize, cellHeight, agentMaxSlope, true, true, true, agentHeight, agentRadius, agentMaxClimb, + regionMinSize, regionMergeSize, edgeMaxLen, edgeMaxError, vertsPerPoly, detailSampleDist, detailSampleMaxError, + walkableAreaMod, true) + { + } + + /** + * Non-tiled build configuration + */ + public RecastConfig(PartitionType partitionType, float cellSize, float cellHeight, float agentMaxSlope, + bool filterLowHangingObstacles, bool filterLedgeSpans, bool filterWalkableLowHeightSpans, float agentHeight, + float agentRadius, float agentMaxClimb, int regionMinSize, int regionMergeSize, float edgeMaxLen, float edgeMaxError, + int vertsPerPoly, float detailSampleDist, float detailSampleMaxError, AreaModification walkableAreaMod, + bool buildMeshDetail) : this(false, 0, 0, 0, partitionType, cellSize, cellHeight, agentMaxSlope, filterLowHangingObstacles, filterLedgeSpans, + filterWalkableLowHeightSpans, agentHeight, agentRadius, agentMaxClimb, + regionMinSize * regionMinSize * cellSize * cellSize, regionMergeSize * regionMergeSize * cellSize * cellSize, + edgeMaxLen, edgeMaxError, vertsPerPoly, buildMeshDetail, detailSampleDist, detailSampleMaxError, walkableAreaMod) + { + // Note: area = size*size in [Units: wu] + } + + public RecastConfig(bool useTiles, int tileSizeX, int tileSizeZ, int borderSize, PartitionType partitionType, + float cellSize, float cellHeight, float agentMaxSlope, bool filterLowHangingObstacles, bool filterLedgeSpans, + bool filterWalkableLowHeightSpans, float agentHeight, float agentRadius, float agentMaxClimb, float minRegionArea, + float mergeRegionArea, float edgeMaxLen, float edgeMaxError, int vertsPerPoly, bool buildMeshDetail, + float detailSampleDist, float detailSampleMaxError, AreaModification walkableAreaMod) + { + this.useTiles = useTiles; + this.tileSizeX = tileSizeX; + this.tileSizeZ = tileSizeZ; + this.borderSize = borderSize; + this.partitionType = partitionType; + cs = cellSize; + ch = cellHeight; + walkableSlopeAngle = agentMaxSlope; + walkableHeight = (int)Math.Ceiling(agentHeight / ch); + walkableHeightWorld = agentHeight; + walkableClimb = (int)Math.Floor(agentMaxClimb / ch); + walkableClimbWorld = agentMaxClimb; + walkableRadius = (int)Math.Ceiling(agentRadius / cs); + walkableRadiusWorld = agentRadius; + this.minRegionArea = (int)Math.Round(minRegionArea / (cs * cs)); + minRegionAreaWorld = minRegionArea; + this.mergeRegionArea = (int)Math.Round(mergeRegionArea / (cs * cs)); + mergeRegionAreaWorld = mergeRegionArea; + maxEdgeLen = (int)(edgeMaxLen / cellSize); + maxEdgeLenWorld = edgeMaxLen; + maxSimplificationError = edgeMaxError; + maxVertsPerPoly = vertsPerPoly; + this.detailSampleDist = detailSampleDist < 0.9f ? 0 : cellSize * detailSampleDist; + this.detailSampleMaxError = cellHeight * detailSampleMaxError; + this.walkableAreaMod = walkableAreaMod; + this.filterLowHangingObstacles = filterLowHangingObstacles; + this.filterLedgeSpans = filterLedgeSpans; + this.filterWalkableLowHeightSpans = filterWalkableLowHeightSpans; + this.buildMeshDetail = buildMeshDetail; + } + + public static int calcBorder(float agentRadius, float cs) + { + return 3 + (int)Math.Ceiling(agentRadius / cs); + } +} \ No newline at end of file diff --git a/src/DotRecast.Recast/RecastConstants.cs b/src/DotRecast.Recast/RecastConstants.cs new file mode 100644 index 0000000..fe1821f --- /dev/null +++ b/src/DotRecast.Recast/RecastConstants.cs @@ -0,0 +1,84 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +namespace DotRecast.Recast; + +public static class RecastConstants +{ + public const int RC_NULL_AREA = 0; + public const int RC_NOT_CONNECTED = 0x3f; + + /// Defines the number of bits allocated to rcSpan::smin and rcSpan::smax. + public const int SPAN_HEIGHT_BITS = 20; + + /// Defines the maximum value for rcSpan::smin and rcSpan::smax. + public const int SPAN_MAX_HEIGHT = (1 << SPAN_HEIGHT_BITS) - 1; + + /// Heighfield border flag. + /// If a heightfield region ID has this bit set, then the region is a border + /// region and its spans are considered unwalkable. + /// (Used during the region and contour build process.) + /// @see rcCompactSpan::reg + public const int RC_BORDER_REG = 0x8000; + + /// Polygon touches multiple regions. + /// If a polygon has this region ID it was merged with or created + /// from polygons of different regions during the polymesh + /// build step that removes redundant border vertices. + /// (Used during the polymesh and detail polymesh build processes) + /// @see rcPolyMesh::regs + public const int RC_MULTIPLE_REGS = 0; + + // Border vertex flag. + /// If a region ID has this bit set, then the associated element lies on + /// a tile border. If a contour vertex's region ID has this bit set, the + /// vertex will later be removed in order to match the segments and vertices + /// at tile boundaries. + /// (Used during the build process.) + /// @see rcCompactSpan::reg, #rcContour::verts, #rcContour::rverts + public const int RC_BORDER_VERTEX = 0x10000; + + /// Area border flag. + /// If a region ID has this bit set, then the associated element lies on + /// the border of an area. + /// (Used during the region and contour build process.) + /// @see rcCompactSpan::reg, #rcContour::verts, #rcContour::rverts + public const int RC_AREA_BORDER = 0x20000; + + /// Applied to the region id field of contour vertices in order to extract the region id. + /// The region id field of a vertex may have several flags applied to it. So the + /// fields value can't be used directly. + /// @see rcContour::verts, rcContour::rverts + public const int RC_CONTOUR_REG_MASK = 0xffff; + + /// A value which indicates an invalid index within a mesh. + /// @note This does not necessarily indicate an error. + /// @see rcPolyMesh::polys + public const int RC_MESH_NULL_IDX = 0xffff; + + public const int RC_CONTOUR_TESS_WALL_EDGES = 0x01; + + /// < Tessellate solid (impassable) edges during contour + /// simplification. + public const int RC_CONTOUR_TESS_AREA_EDGES = 0x02; + + + + public const int RC_LOG_WARNING = 1; +} \ No newline at end of file diff --git a/src/DotRecast.Recast/RecastContour.cs b/src/DotRecast.Recast/RecastContour.cs new file mode 100644 index 0000000..2445c74 --- /dev/null +++ b/src/DotRecast.Recast/RecastContour.cs @@ -0,0 +1,1019 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.Collections.Generic; + +namespace DotRecast.Recast; + +using static RecastConstants; + +public class RecastContour +{ + private class ContourRegion + { + public Contour outline; + public ContourHole[] holes; + public int nholes; + } + + private class ContourHole + { + public int leftmost; + public int minx; + public int minz; + public Contour contour; + } + + private class PotentialDiagonal + { + public int dist; + public int vert; + } + + private class CornerHeight + { + public readonly int height; + public readonly bool borderVertex; + + public CornerHeight(int height, bool borderVertex) + { + this.height = height; + this.borderVertex = borderVertex; + } + } + + private static CornerHeight getCornerHeight(int x, int y, int i, int dir, CompactHeightfield chf) + { + bool isBorderVertex = false; + CompactSpan s = chf.spans[i]; + int ch = s.y; + int dirp = (dir + 1) & 0x3; + + int[] regs = + { + 0, 0, 0, 0 + }; + + // Combine region and area codes in order to prevent + // border vertices which are in between two areas to be removed. + regs[0] = chf.spans[i].reg | (chf.areas[i] << 16); + + if (RecastCommon.GetCon(s, dir) != RC_NOT_CONNECTED) + { + int ax = x + RecastCommon.GetDirOffsetX(dir); + int ay = y + RecastCommon.GetDirOffsetY(dir); + int ai = chf.cells[ax + ay * chf.width].index + RecastCommon.GetCon(s, dir); + CompactSpan @as = chf.spans[ai]; + ch = Math.Max(ch, @as.y); + regs[1] = chf.spans[ai].reg | (chf.areas[ai] << 16); + if (RecastCommon.GetCon(@as, dirp) != RC_NOT_CONNECTED) + { + int ax2 = ax + RecastCommon.GetDirOffsetX(dirp); + int ay2 = ay + RecastCommon.GetDirOffsetY(dirp); + int ai2 = chf.cells[ax2 + ay2 * chf.width].index + RecastCommon.GetCon(@as, dirp); + CompactSpan as2 = chf.spans[ai2]; + ch = Math.Max(ch, as2.y); + regs[2] = chf.spans[ai2].reg | (chf.areas[ai2] << 16); + } + } + + if (RecastCommon.GetCon(s, dirp) != RC_NOT_CONNECTED) + { + int ax = x + RecastCommon.GetDirOffsetX(dirp); + int ay = y + RecastCommon.GetDirOffsetY(dirp); + int ai = chf.cells[ax + ay * chf.width].index + RecastCommon.GetCon(s, dirp); + CompactSpan @as = chf.spans[ai]; + ch = Math.Max(ch, @as.y); + regs[3] = chf.spans[ai].reg | (chf.areas[ai] << 16); + if (RecastCommon.GetCon(@as, dir) != RC_NOT_CONNECTED) + { + int ax2 = ax + RecastCommon.GetDirOffsetX(dir); + int ay2 = ay + RecastCommon.GetDirOffsetY(dir); + int ai2 = chf.cells[ax2 + ay2 * chf.width].index + RecastCommon.GetCon(@as, dir); + CompactSpan as2 = chf.spans[ai2]; + ch = Math.Max(ch, as2.y); + regs[2] = chf.spans[ai2].reg | (chf.areas[ai2] << 16); + } + } + + // Check if the vertex is special edge vertex, these vertices will be removed later. + for (int j = 0; j < 4; ++j) + { + int a = j; + int b = (j + 1) & 0x3; + int c = (j + 2) & 0x3; + int d = (j + 3) & 0x3; + + // The vertex is a border vertex there are two same exterior cells in a row, + // followed by two interior cells and none of the regions are out of bounds. + bool twoSameExts = (regs[a] & regs[b] & RC_BORDER_REG) != 0 && regs[a] == regs[b]; + bool twoInts = ((regs[c] | regs[d]) & RC_BORDER_REG) == 0; + bool intsSameArea = (regs[c] >> 16) == (regs[d] >> 16); + bool noZeros = regs[a] != 0 && regs[b] != 0 && regs[c] != 0 && regs[d] != 0; + if (twoSameExts && twoInts && intsSameArea && noZeros) + { + isBorderVertex = true; + break; + } + } + + return new CornerHeight(ch, isBorderVertex); + } + + private static void walkContour(int x, int y, int i, CompactHeightfield chf, int[] flags, List points) + { + // Choose the first non-connected edge + int dir = 0; + while ((flags[i] & (1 << dir)) == 0) + dir++; + + int startDir = dir; + int starti = i; + + int area = chf.areas[i]; + + int iter = 0; + while (++iter < 40000) + { + if ((flags[i] & (1 << dir)) != 0) + { + // Choose the edge corner + bool isAreaBorder = false; + CornerHeight cornerHeight = getCornerHeight(x, y, i, dir, chf); + bool isBorderVertex = cornerHeight.borderVertex; + int px = x; + int py = cornerHeight.height; + int pz = y; + switch (dir) + { + case 0: + pz++; + break; + case 1: + px++; + pz++; + break; + case 2: + px++; + break; + } + + int r = 0; + CompactSpan s = chf.spans[i]; + if (RecastCommon.GetCon(s, dir) != RC_NOT_CONNECTED) + { + int ax = x + RecastCommon.GetDirOffsetX(dir); + int ay = y + RecastCommon.GetDirOffsetY(dir); + int ai = chf.cells[ax + ay * chf.width].index + RecastCommon.GetCon(s, dir); + r = chf.spans[ai].reg; + if (area != chf.areas[ai]) + isAreaBorder = true; + } + + if (isBorderVertex) + r |= RC_BORDER_VERTEX; + if (isAreaBorder) + r |= RC_AREA_BORDER; + points.Add(px); + points.Add(py); + points.Add(pz); + points.Add(r); + + flags[i] &= ~(1 << dir); // Remove visited edges + dir = (dir + 1) & 0x3; // Rotate CW + } + else + { + int ni = -1; + int nx = x + RecastCommon.GetDirOffsetX(dir); + int ny = y + RecastCommon.GetDirOffsetY(dir); + CompactSpan s = chf.spans[i]; + if (RecastCommon.GetCon(s, dir) != RC_NOT_CONNECTED) + { + CompactCell nc = chf.cells[nx + ny * chf.width]; + ni = nc.index + RecastCommon.GetCon(s, dir); + } + + if (ni == -1) + { + // Should not happen. + return; + } + + x = nx; + y = ny; + i = ni; + dir = (dir + 3) & 0x3; // Rotate CCW + } + + if (starti == i && startDir == dir) + { + break; + } + } + } + + private static float distancePtSeg(int x, int z, int px, int pz, int qx, int qz) + { + float pqx = qx - px; + float pqz = qz - pz; + float dx = x - px; + float dz = z - pz; + float d = pqx * pqx + pqz * pqz; + float t = pqx * dx + pqz * dz; + if (d > 0) + t /= d; + if (t < 0) + t = 0; + else if (t > 1) + t = 1; + + dx = px + t * pqx - x; + dz = pz + t * pqz - z; + + return dx * dx + dz * dz; + } + + private static void simplifyContour(List points, List simplified, float maxError, int maxEdgeLen, int buildFlags) + { + // Add initial points. + bool hasConnections = false; + for (int i = 0; i < points.Count; i += 4) + { + if ((points[i + 3] & RC_CONTOUR_REG_MASK) != 0) + { + hasConnections = true; + break; + } + } + + if (hasConnections) + { + // The contour has some portals to other regions. + // Add a new point to every location where the region changes. + for (int i = 0, ni = points.Count / 4; i < ni; ++i) + { + int ii = (i + 1) % ni; + bool differentRegs = (points[i * 4 + 3] & RC_CONTOUR_REG_MASK) != (points[ii * 4 + 3] & RC_CONTOUR_REG_MASK); + bool areaBorders = (points[i * 4 + 3] & RC_AREA_BORDER) != (points[ii * 4 + 3] & RC_AREA_BORDER); + if (differentRegs || areaBorders) + { + simplified.Add(points[i * 4 + 0]); + simplified.Add(points[i * 4 + 1]); + simplified.Add(points[i * 4 + 2]); + simplified.Add(i); + } + } + } + + if (simplified.Count == 0) + { + // If there is no connections at all, + // create some initial points for the simplification process. + // Find lower-left and upper-right vertices of the contour. + int llx = points[0]; + int lly = points[1]; + int llz = points[2]; + int lli = 0; + int urx = points[0]; + int ury = points[1]; + int urz = points[2]; + int uri = 0; + for (int i = 0; i < points.Count; i += 4) + { + int x = points[i + 0]; + int y = points[i + 1]; + int z = points[i + 2]; + if (x < llx || (x == llx && z < llz)) + { + llx = x; + lly = y; + llz = z; + lli = i / 4; + } + + if (x > urx || (x == urx && z > urz)) + { + urx = x; + ury = y; + urz = z; + uri = i / 4; + } + } + + simplified.Add(llx); + simplified.Add(lly); + simplified.Add(llz); + simplified.Add(lli); + + simplified.Add(urx); + simplified.Add(ury); + simplified.Add(urz); + simplified.Add(uri); + } + + // Add points until all raw points are within + // error tolerance to the simplified shape. + int pn = points.Count / 4; + for (int i = 0; i < simplified.Count / 4;) + { + int ii = (i + 1) % (simplified.Count / 4); + + int ax = simplified[i * 4 + 0]; + int az = simplified[i * 4 + 2]; + int ai = simplified[i * 4 + 3]; + + int bx = simplified[ii * 4 + 0]; + int bz = simplified[ii * 4 + 2]; + int bi = simplified[ii * 4 + 3]; + + // Find maximum deviation from the segment. + float maxd = 0; + int maxi = -1; + int ci, cinc, endi; + + // Traverse the segment in lexilogical order so that the + // max deviation is calculated similarly when traversing + // opposite segments. + if (bx > ax || (bx == ax && bz > az)) + { + cinc = 1; + ci = (ai + cinc) % pn; + endi = bi; + } + else + { + cinc = pn - 1; + ci = (bi + cinc) % pn; + endi = ai; + int temp = ax; + ax = bx; + bx = temp; + temp = az; + az = bz; + bz = temp; + } + + // Tessellate only outer edges or edges between areas. + if ((points[ci * 4 + 3] & RC_CONTOUR_REG_MASK) == 0 || (points[ci * 4 + 3] & RC_AREA_BORDER) != 0) + { + while (ci != endi) + { + float d = distancePtSeg(points[ci * 4 + 0], points[ci * 4 + 2], ax, az, bx, bz); + if (d > maxd) + { + maxd = d; + maxi = ci; + } + + ci = (ci + cinc) % pn; + } + } + + // If the max deviation is larger than accepted error, + // add new point, else continue to next segment. + if (maxi != -1 && maxd > (maxError * maxError)) + { + // Add the point. + simplified.Insert((i + 1) * 4 + 0, points[maxi * 4 + 0]); + simplified.Insert((i + 1) * 4 + 1, points[maxi * 4 + 1]); + simplified.Insert((i + 1) * 4 + 2, points[maxi * 4 + 2]); + simplified.Insert((i + 1) * 4 + 3, maxi); + } + else + { + ++i; + } + } + + // Split too long edges. + if (maxEdgeLen > 0 && (buildFlags & (RC_CONTOUR_TESS_WALL_EDGES | RC_CONTOUR_TESS_AREA_EDGES)) != 0) + { + for (int i = 0; i < simplified.Count / 4;) + { + int ii = (i + 1) % (simplified.Count / 4); + + int ax = simplified[i * 4 + 0]; + int az = simplified[i * 4 + 2]; + int ai = simplified[i * 4 + 3]; + + int bx = simplified[ii * 4 + 0]; + int bz = simplified[ii * 4 + 2]; + int bi = simplified[ii * 4 + 3]; + + // Find maximum deviation from the segment. + int maxi = -1; + int ci = (ai + 1) % pn; + + // Tessellate only outer edges or edges between areas. + bool tess = false; + // Wall edges. + if ((buildFlags & RC_CONTOUR_TESS_WALL_EDGES) != 0 && (points[ci * 4 + 3] & RC_CONTOUR_REG_MASK) == 0) + tess = true; + // Edges between areas. + if ((buildFlags & RC_CONTOUR_TESS_AREA_EDGES) != 0 && (points[ci * 4 + 3] & RC_AREA_BORDER) != 0) + tess = true; + + if (tess) + { + int dx = bx - ax; + int dz = bz - az; + if (dx * dx + dz * dz > maxEdgeLen * maxEdgeLen) + { + // Round based on the segments in lexilogical order so that the + // max tesselation is consistent regardles in which direction + // segments are traversed. + int n = bi < ai ? (bi + pn - ai) : (bi - ai); + if (n > 1) + { + if (bx > ax || (bx == ax && bz > az)) + maxi = (ai + n / 2) % pn; + else + maxi = (ai + (n + 1) / 2) % pn; + } + } + } + + // If the max deviation is larger than accepted error, + // add new point, else continue to next segment. + if (maxi != -1) + { + // Add the point. + simplified.Insert((i + 1) * 4 + 0, points[maxi * 4 + 0]); + simplified.Insert((i + 1) * 4 + 1, points[maxi * 4 + 1]); + simplified.Insert((i + 1) * 4 + 2, points[maxi * 4 + 2]); + simplified.Insert((i + 1) * 4 + 3, maxi); + } + else + { + ++i; + } + } + } + + for (int i = 0; i < simplified.Count / 4; ++i) + { + // The edge vertex flag is take from the current raw point, + // and the neighbour region is take from the next raw point. + int ai = (simplified[i * 4 + 3] + 1) % pn; + int bi = simplified[i * 4 + 3]; + simplified[i * 4 + 3] = (points[ai * 4 + 3] & (RC_CONTOUR_REG_MASK | RC_AREA_BORDER)) + | points[bi * 4 + 3] & RC_BORDER_VERTEX; + } + } + + private static int calcAreaOfPolygon2D(int[] verts, int nverts) + { + int area = 0; + for (int i = 0, j = nverts - 1; i < nverts; j = i++) + { + int vi = i * 4; + int vj = j * 4; + area += verts[vi + 0] * verts[vj + 2] - verts[vj + 0] * verts[vi + 2]; + } + + return (area + 1) / 2; + } + + private static bool intersectSegCountour(int d0, int d1, int i, int n, int[] verts, int[] d0verts, + int[] d1verts) + { + // For each edge (k,k+1) of P + int[] pverts = new int[4 * 4]; + for (int g = 0; g < 4; g++) + { + pverts[g] = d0verts[d0 + g]; + pverts[4 + g] = d1verts[d1 + g]; + } + + d0 = 0; + d1 = 4; + for (int k = 0; k < n; k++) + { + int k1 = RecastMesh.next(k, n); + // Skip edges incident to i. + if (i == k || i == k1) + continue; + int p0 = k * 4; + int p1 = k1 * 4; + for (int g = 0; g < 4; g++) + { + pverts[8 + g] = verts[p0 + g]; + pverts[12 + g] = verts[p1 + g]; + } + + p0 = 8; + p1 = 12; + if (RecastMesh.vequal(pverts, d0, p0) || RecastMesh.vequal(pverts, d1, p0) + || RecastMesh.vequal(pverts, d0, p1) || RecastMesh.vequal(pverts, d1, p1)) + continue; + + if (RecastMesh.intersect(pverts, d0, d1, p0, p1)) + return true; + } + + return false; + } + + private static bool inCone(int i, int n, int[] verts, int pj, int[] vertpj) + { + int pi = i * 4; + int pi1 = RecastMesh.next(i, n) * 4; + int pin1 = RecastMesh.prev(i, n) * 4; + int[] pverts = new int[4 * 4]; + for (int g = 0; g < 4; g++) + { + pverts[g] = verts[pi + g]; + pverts[4 + g] = verts[pi1 + g]; + pverts[8 + g] = verts[pin1 + g]; + pverts[12 + g] = vertpj[pj + g]; + } + + pi = 0; + pi1 = 4; + pin1 = 8; + pj = 12; + // If P[i] is a convex vertex [ i+1 left or on (i-1,i) ]. + if (RecastMesh.leftOn(pverts, pin1, pi, pi1)) + return RecastMesh.left(pverts, pi, pj, pin1) && RecastMesh.left(pverts, pj, pi, pi1); + // Assume (i-1,i,i+1) not collinear. + // else P[i] is reflex. + return !(RecastMesh.leftOn(pverts, pi, pj, pi1) && RecastMesh.leftOn(pverts, pj, pi, pin1)); + } + + private static void removeDegenerateSegments(List simplified) + { + // Remove adjacent vertices which are equal on xz-plane, + // or else the triangulator will get confused. + int npts = simplified.Count / 4; + for (int i = 0; i < npts; ++i) + { + int ni = RecastMesh.next(i, npts); + + // if (vequal(&simplified[i*4], &simplified[ni*4])) + if (simplified[i * 4] == simplified[ni * 4] + && simplified[i * 4 + 2] == simplified[ni * 4 + 2]) + { + // Degenerate segment, remove. + simplified.RemoveAt(i * 4); + simplified.RemoveAt(i * 4); + simplified.RemoveAt(i * 4); + simplified.RemoveAt(i * 4); + npts--; + } + } + } + + private static void mergeContours(Contour ca, Contour cb, int ia, int ib) + { + int maxVerts = ca.nverts + cb.nverts + 2; + int[] verts = new int[maxVerts * 4]; + + int nv = 0; + + // Copy contour A. + for (int i = 0; i <= ca.nverts; ++i) + { + int dst = nv * 4; + int src = ((ia + i) % ca.nverts) * 4; + verts[dst + 0] = ca.verts[src + 0]; + verts[dst + 1] = ca.verts[src + 1]; + verts[dst + 2] = ca.verts[src + 2]; + verts[dst + 3] = ca.verts[src + 3]; + nv++; + } + + // Copy contour B + for (int i = 0; i <= cb.nverts; ++i) + { + int dst = nv * 4; + int src = ((ib + i) % cb.nverts) * 4; + verts[dst + 0] = cb.verts[src + 0]; + verts[dst + 1] = cb.verts[src + 1]; + verts[dst + 2] = cb.verts[src + 2]; + verts[dst + 3] = cb.verts[src + 3]; + nv++; + } + + ca.verts = verts; + ca.nverts = nv; + + cb.verts = null; + cb.nverts = 0; + } + + // Finds the lowest leftmost vertex of a contour. + private static int[] findLeftMostVertex(Contour contour) + { + int minx = contour.verts[0]; + int minz = contour.verts[2]; + int leftmost = 0; + for (int i = 1; i < contour.nverts; i++) + { + int x = contour.verts[i * 4 + 0]; + int z = contour.verts[i * 4 + 2]; + if (x < minx || (x == minx && z < minz)) + { + minx = x; + minz = z; + leftmost = i; + } + } + + return new int[] { minx, minz, leftmost }; + } + + private class CompareHoles : IComparer + { + public int Compare(ContourHole a, ContourHole b) + { + if (a.minx == b.minx) + { + return a.minz.CompareTo(b.minz); + } + else + { + return a.minx.CompareTo(b.minx); + } + } + } + + private class CompareDiagDist : IComparer + { + public int Compare(PotentialDiagonal va, PotentialDiagonal vb) + { + PotentialDiagonal a = va; + PotentialDiagonal b = vb; + return a.dist.CompareTo(b.dist); + } + } + + private static void mergeRegionHoles(Telemetry ctx, ContourRegion region) + { + // Sort holes from left to right. + for (int i = 0; i < region.nholes; i++) + { + int[] minleft = findLeftMostVertex(region.holes[i].contour); + region.holes[i].minx = minleft[0]; + region.holes[i].minz = minleft[1]; + region.holes[i].leftmost = minleft[2]; + } + + Array.Sort(region.holes, new CompareHoles()); + + int maxVerts = region.outline.nverts; + for (int i = 0; i < region.nholes; i++) + maxVerts += region.holes[i].contour.nverts; + + PotentialDiagonal[] diags = new PotentialDiagonal[maxVerts]; + for (int pd = 0; pd < maxVerts; pd++) + { + diags[pd] = new PotentialDiagonal(); + } + + Contour outline = region.outline; + + // Merge holes into the outline one by one. + for (int i = 0; i < region.nholes; i++) + { + Contour hole = region.holes[i].contour; + + int index = -1; + int bestVertex = region.holes[i].leftmost; + for (int iter = 0; iter < hole.nverts; iter++) + { + // Find potential diagonals. + // The 'best' vertex must be in the cone described by 3 cosequtive vertices of the outline. + // ..o j-1 + // | + // | * best + // | + // j o-----o j+1 + // : + int ndiags = 0; + int corner = bestVertex * 4; + for (int j = 0; j < outline.nverts; j++) + { + if (inCone(j, outline.nverts, outline.verts, corner, hole.verts)) + { + int dx = outline.verts[j * 4 + 0] - hole.verts[corner + 0]; + int dz = outline.verts[j * 4 + 2] - hole.verts[corner + 2]; + diags[ndiags].vert = j; + diags[ndiags].dist = dx * dx + dz * dz; + ndiags++; + } + } + + // Sort potential diagonals by distance, we want to make the connection as short as possible. + Array.Sort(diags, 0, ndiags, new CompareDiagDist()); + + // Find a diagonal that is not intersecting the outline not the remaining holes. + index = -1; + for (int j = 0; j < ndiags; j++) + { + int pt = diags[j].vert * 4; + bool intersect = intersectSegCountour(pt, corner, diags[j].vert, outline.nverts, outline.verts, + outline.verts, hole.verts); + for (int k = i; k < region.nholes && !intersect; k++) + intersect |= intersectSegCountour(pt, corner, -1, region.holes[k].contour.nverts, + region.holes[k].contour.verts, outline.verts, hole.verts); + if (!intersect) + { + index = diags[j].vert; + break; + } + } + + // If found non-intersecting diagonal, stop looking. + if (index != -1) + break; + // All the potential diagonals for the current vertex were intersecting, try next vertex. + bestVertex = (bestVertex + 1) % hole.nverts; + } + + if (index == -1) + { + ctx.warn("mergeHoles: Failed to find merge points for"); + continue; + } + + mergeContours(region.outline, hole, index, bestVertex); + } + } + + /// @par + /// + /// The raw contours will match the region outlines exactly. The @p maxError and @p maxEdgeLen + /// parameters control how closely the simplified contours will match the raw contours. + /// + /// Simplified contours are generated such that the vertices for portals between areas match up. + /// (They are considered mandatory vertices.) + /// + /// Setting @p maxEdgeLength to zero will disabled the edge length feature. + /// + /// See the #rcConfig documentation for more information on the configuration parameters. + /// + /// @see rcAllocContourSet, rcCompactHeightfield, rcContourSet, rcConfig + public static ContourSet buildContours(Telemetry ctx, CompactHeightfield chf, float maxError, int maxEdgeLen, + int buildFlags) + { + int w = chf.width; + int h = chf.height; + int borderSize = chf.borderSize; + ContourSet cset = new ContourSet(); + + ctx.startTimer("CONTOURS"); + RecastVectors.copy(cset.bmin, chf.bmin, 0); + RecastVectors.copy(cset.bmax, chf.bmax, 0); + if (borderSize > 0) + { + // If the heightfield was build with bordersize, remove the offset. + float pad = borderSize * chf.cs; + cset.bmin[0] += pad; + cset.bmin[2] += pad; + cset.bmax[0] -= pad; + cset.bmax[2] -= pad; + } + + cset.cs = chf.cs; + cset.ch = chf.ch; + cset.width = chf.width - chf.borderSize * 2; + cset.height = chf.height - chf.borderSize * 2; + cset.borderSize = chf.borderSize; + cset.maxError = maxError; + + int[] flags = new int[chf.spanCount]; + + ctx.startTimer("CONTOURS_TRACE"); + + // Mark boundaries. + for (int y = 0; y < h; ++y) + { + for (int x = 0; x < w; ++x) + { + CompactCell c = chf.cells[x + y * w]; + for (int i = c.index, ni = c.index + c.count; i < ni; ++i) + { + int res = 0; + CompactSpan s = chf.spans[i]; + if (chf.spans[i].reg == 0 || (chf.spans[i].reg & RC_BORDER_REG) != 0) + { + flags[i] = 0; + continue; + } + + for (int dir = 0; dir < 4; ++dir) + { + int r = 0; + if (RecastCommon.GetCon(s, dir) != RC_NOT_CONNECTED) + { + int ax = x + RecastCommon.GetDirOffsetX(dir); + int ay = y + RecastCommon.GetDirOffsetY(dir); + int ai = chf.cells[ax + ay * w].index + RecastCommon.GetCon(s, dir); + r = chf.spans[ai].reg; + } + + if (r == chf.spans[i].reg) + res |= (1 << dir); + } + + flags[i] = res ^ 0xf; // Inverse, mark non connected edges. + } + } + } + + ctx.stopTimer("CONTOURS_TRACE"); + + List verts = new(256); + List simplified = new(64); + + for (int y = 0; y < h; ++y) + { + for (int x = 0; x < w; ++x) + { + CompactCell c = chf.cells[x + y * w]; + for (int i = c.index, ni = c.index + c.count; i < ni; ++i) + { + if (flags[i] == 0 || flags[i] == 0xf) + { + flags[i] = 0; + continue; + } + + int reg = chf.spans[i].reg; + if (reg == 0 || (reg & RC_BORDER_REG) != 0) + continue; + int area = chf.areas[i]; + + verts.Clear(); + simplified.Clear(); + + ctx.startTimer("CONTOURS_WALK"); + walkContour(x, y, i, chf, flags, verts); + ctx.stopTimer("CONTOURS_WALK"); + + ctx.startTimer("CONTOURS_SIMPLIFY"); + simplifyContour(verts, simplified, maxError, maxEdgeLen, buildFlags); + removeDegenerateSegments(simplified); + ctx.stopTimer("CONTOURS_SIMPLIFY"); + + // Store region->contour remap info. + // Create contour. + if (simplified.Count / 4 >= 3) + { + Contour cont = new Contour(); + cset.conts.Add(cont); + + cont.nverts = simplified.Count / 4; + cont.verts = new int[simplified.Count]; + for (int l = 0; l < cont.verts.Length; l++) + { + cont.verts[l] = simplified[l]; + } + + if (borderSize > 0) + { + // If the heightfield was build with bordersize, remove the offset. + for (int j = 0; j < cont.nverts; ++j) + { + cont.verts[j * 4] -= borderSize; + cont.verts[j * 4 + 2] -= borderSize; + } + } + + cont.nrverts = verts.Count / 4; + cont.rverts = new int[verts.Count]; + for (int l = 0; l < cont.rverts.Length; l++) + { + cont.rverts[l] = verts[l]; + } + + if (borderSize > 0) + { + // If the heightfield was build with bordersize, remove the offset. + for (int j = 0; j < cont.nrverts; ++j) + { + cont.rverts[j * 4] -= borderSize; + cont.rverts[j * 4 + 2] -= borderSize; + } + } + + cont.reg = reg; + cont.area = area; + } + } + } + } + + // Merge holes if needed. + if (cset.conts.Count > 0) + { + // Calculate winding of all polygons. + int[] winding = new int[cset.conts.Count]; + int nholes = 0; + for (int i = 0; i < cset.conts.Count; ++i) + { + Contour cont = cset.conts[i]; + // If the contour is wound backwards, it is a hole. + winding[i] = calcAreaOfPolygon2D(cont.verts, cont.nverts) < 0 ? -1 : 1; + if (winding[i] < 0) + nholes++; + } + + if (nholes > 0) + { + // Collect outline contour and holes contours per region. + // We assume that there is one outline and multiple holes. + int nregions = chf.maxRegions + 1; + ContourRegion[] regions = new ContourRegion[nregions]; + for (int i = 0; i < nregions; i++) + { + regions[i] = new ContourRegion(); + } + + for (int i = 0; i < cset.conts.Count; ++i) + { + Contour cont = cset.conts[i]; + // Positively would contours are outlines, negative holes. + if (winding[i] > 0) + { + if (regions[cont.reg].outline != null) + { + throw new Exception( + "rcBuildContours: Multiple outlines for region " + cont.reg + "."); + } + + regions[cont.reg].outline = cont; + } + else + { + regions[cont.reg].nholes++; + } + } + + for (int i = 0; i < nregions; i++) + { + if (regions[i].nholes > 0) + { + regions[i].holes = new ContourHole[regions[i].nholes]; + for (int nh = 0; nh < regions[i].nholes; nh++) + { + regions[i].holes[nh] = new ContourHole(); + } + + regions[i].nholes = 0; + } + } + + for (int i = 0; i < cset.conts.Count; ++i) + { + Contour cont = cset.conts[i]; + ContourRegion reg = regions[cont.reg]; + if (winding[i] < 0) + reg.holes[reg.nholes++].contour = cont; + } + + // Finally merge each regions holes into the outline. + for (int i = 0; i < nregions; i++) + { + ContourRegion reg = regions[i]; + if (reg.nholes == 0) + continue; + + if (reg.outline != null) + { + mergeRegionHoles(ctx, reg); + } + else + { + // The region does not have an outline. + // This can happen if the contour becaomes selfoverlapping because of + // too aggressive simplification settings. + throw new Exception("rcBuildContours: Bad outline for region " + i + + ", contour simplification is likely too aggressive."); + } + } + } + } + + ctx.stopTimer("CONTOURS"); + return cset; + } +} \ No newline at end of file diff --git a/src/DotRecast.Recast/RecastFilledVolumeRasterization.cs b/src/DotRecast.Recast/RecastFilledVolumeRasterization.cs new file mode 100644 index 0000000..89fd202 --- /dev/null +++ b/src/DotRecast.Recast/RecastFilledVolumeRasterization.cs @@ -0,0 +1,802 @@ +/* ++recast4j copyright (c) 2021 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using DotRecast.Core; + +namespace DotRecast.Recast; + +using static RecastConstants; +using static RecastVectors; +using static RecastCommon; + +public class RecastFilledVolumeRasterization +{ + private const float EPSILON = 0.00001f; + private static readonly int[] BOX_EDGES = new[] { 0, 1, 0, 2, 0, 4, 1, 3, 1, 5, 2, 3, 2, 6, 3, 7, 4, 5, 4, 6, 5, 7, 6, 7 }; + + public static void rasterizeSphere(Heightfield hf, float[] center, float radius, int area, int flagMergeThr, Telemetry ctx) + { + ctx.startTimer("RASTERIZE_SPHERE"); + float[] bounds = + { + center[0] - radius, center[1] - radius, center[2] - radius, center[0] + radius, center[1] + radius, + center[2] + radius + }; + rasterizationFilledShape(hf, bounds, area, flagMergeThr, + rectangle => intersectSphere(rectangle, center, radius * radius)); + ctx.stopTimer("RASTERIZE_SPHERE"); + } + + public static void rasterizeCapsule(Heightfield hf, float[] start, float[] end, float radius, int area, int flagMergeThr, + Telemetry ctx) + { + ctx.startTimer("RASTERIZE_CAPSULE"); + float[] bounds = + { + Math.Min(start[0], end[0]) - radius, Math.Min(start[1], end[1]) - radius, + Math.Min(start[2], end[2]) - radius, Math.Max(start[0], end[0]) + radius, Math.Max(start[1], end[1]) + radius, + Math.Max(start[2], end[2]) + radius + }; + float[] axis = { end[0] - start[0], end[1] - start[1], end[2] - start[2] }; + rasterizationFilledShape(hf, bounds, area, flagMergeThr, + rectangle => intersectCapsule(rectangle, start, end, axis, radius * radius)); + ctx.stopTimer("RASTERIZE_CAPSULE"); + } + + public static void rasterizeCylinder(Heightfield hf, float[] start, float[] end, float radius, int area, int flagMergeThr, + Telemetry ctx) + { + ctx.startTimer("RASTERIZE_CYLINDER"); + float[] bounds = + { + Math.Min(start[0], end[0]) - radius, Math.Min(start[1], end[1]) - radius, + Math.Min(start[2], end[2]) - radius, Math.Max(start[0], end[0]) + radius, Math.Max(start[1], end[1]) + radius, + Math.Max(start[2], end[2]) + radius + }; + float[] axis = { end[0] - start[0], end[1] - start[1], end[2] - start[2] }; + rasterizationFilledShape(hf, bounds, area, flagMergeThr, + rectangle => intersectCylinder(rectangle, start, end, axis, radius * radius)); + ctx.stopTimer("RASTERIZE_CYLINDER"); + } + + public static void rasterizeBox(Heightfield hf, float[] center, float[][] halfEdges, int area, int flagMergeThr, + Telemetry ctx) + { + ctx.startTimer("RASTERIZE_BOX"); + float[][] normals = + { + new[] { halfEdges[0][0], halfEdges[0][1], halfEdges[0][2] }, + new[] { halfEdges[1][0], halfEdges[1][1], halfEdges[1][2] }, + new[] { halfEdges[2][0], halfEdges[2][1], halfEdges[2][2] } + }; + normalize(normals[0]); + normalize(normals[1]); + normalize(normals[2]); + + float[] vertices = new float[8 * 3]; + float[] bounds = new float[] + { + float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity, + float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity + }; + for (int i = 0; i < 8; ++i) + { + float s0 = (i & 1) != 0 ? 1f : -1f; + float s1 = (i & 2) != 0 ? 1f : -1f; + float s2 = (i & 4) != 0 ? 1f : -1f; + vertices[i * 3 + 0] = center[0] + s0 * halfEdges[0][0] + s1 * halfEdges[1][0] + s2 * halfEdges[2][0]; + vertices[i * 3 + 1] = center[1] + s0 * halfEdges[0][1] + s1 * halfEdges[1][1] + s2 * halfEdges[2][1]; + vertices[i * 3 + 2] = center[2] + s0 * halfEdges[0][2] + s1 * halfEdges[1][2] + s2 * halfEdges[2][2]; + bounds[0] = Math.Min(bounds[0], vertices[i * 3 + 0]); + bounds[1] = Math.Min(bounds[1], vertices[i * 3 + 1]); + bounds[2] = Math.Min(bounds[2], vertices[i * 3 + 2]); + bounds[3] = Math.Max(bounds[3], vertices[i * 3 + 0]); + bounds[4] = Math.Max(bounds[4], vertices[i * 3 + 1]); + bounds[5] = Math.Max(bounds[5], vertices[i * 3 + 2]); + } + + float[][] planes = ArrayUtils.Of(6, 4); + for (int i = 0; i < 6; i++) + { + float m = i < 3 ? -1 : 1; + int vi = i < 3 ? 0 : 7; + planes[i][0] = m * normals[i % 3][0]; + planes[i][1] = m * normals[i % 3][1]; + planes[i][2] = m * normals[i % 3][2]; + planes[i][3] = vertices[vi * 3] * planes[i][0] + vertices[vi * 3 + 1] * planes[i][1] + + vertices[vi * 3 + 2] * planes[i][2]; + } + + rasterizationFilledShape(hf, bounds, area, flagMergeThr, rectangle => intersectBox(rectangle, vertices, planes)); + ctx.stopTimer("RASTERIZE_BOX"); + } + + public static void rasterizeConvex(Heightfield hf, float[] vertices, int[] triangles, int area, int flagMergeThr, + Telemetry ctx) + { + ctx.startTimer("RASTERIZE_CONVEX"); + float[] bounds = new float[] { vertices[0], vertices[1], vertices[2], vertices[0], vertices[1], vertices[2] }; + for (int i = 0; i < vertices.Length; i += 3) + { + bounds[0] = Math.Min(bounds[0], vertices[i + 0]); + bounds[1] = Math.Min(bounds[1], vertices[i + 1]); + bounds[2] = Math.Min(bounds[2], vertices[i + 2]); + bounds[3] = Math.Max(bounds[3], vertices[i + 0]); + bounds[4] = Math.Max(bounds[4], vertices[i + 1]); + bounds[5] = Math.Max(bounds[5], vertices[i + 2]); + } + + + float[][] planes = ArrayUtils.Of(triangles.Length, 4); + float[][] triBounds = ArrayUtils.Of(triangles.Length / 3, 4); + for (int i = 0, j = 0; i < triangles.Length; i += 3, j++) + { + int a = triangles[i] * 3; + int b = triangles[i + 1] * 3; + int c = triangles[i + 2] * 3; + float[] ab = { vertices[b] - vertices[a], vertices[b + 1] - vertices[a + 1], vertices[b + 2] - vertices[a + 2] }; + float[] ac = { vertices[c] - vertices[a], vertices[c + 1] - vertices[a + 1], vertices[c + 2] - vertices[a + 2] }; + float[] bc = { vertices[c] - vertices[b], vertices[c + 1] - vertices[b + 1], vertices[c + 2] - vertices[b + 2] }; + float[] ca = { vertices[a] - vertices[c], vertices[a + 1] - vertices[c + 1], vertices[a + 2] - vertices[c + 2] }; + plane(planes, i, ab, ac, vertices, a); + plane(planes, i + 1, planes[i], bc, vertices, b); + plane(planes, i + 2, planes[i], ca, vertices, c); + + float s = 1.0f / (vertices[a] * planes[i + 1][0] + vertices[a + 1] * planes[i + 1][1] + + vertices[a + 2] * planes[i + 1][2] - planes[i + 1][3]); + planes[i + 1][0] *= s; + planes[i + 1][1] *= s; + planes[i + 1][2] *= s; + planes[i + 1][3] *= s; + + s = 1.0f / (vertices[b] * planes[i + 2][0] + vertices[b + 1] * planes[i + 2][1] + vertices[b + 2] * planes[i + 2][2] + - planes[i + 2][3]); + planes[i + 2][0] *= s; + planes[i + 2][1] *= s; + planes[i + 2][2] *= s; + planes[i + 2][3] *= s; + + triBounds[j][0] = Math.Min(Math.Min(vertices[a], vertices[b]), vertices[c]); + triBounds[j][1] = Math.Min(Math.Min(vertices[a + 2], vertices[b + 2]), vertices[c + 2]); + triBounds[j][2] = Math.Max(Math.Max(vertices[a], vertices[b]), vertices[c]); + triBounds[j][3] = Math.Max(Math.Max(vertices[a + 2], vertices[b + 2]), vertices[c + 2]); + } + + rasterizationFilledShape(hf, bounds, area, flagMergeThr, + rectangle => intersectConvex(rectangle, triangles, vertices, planes, triBounds)); + ctx.stopTimer("RASTERIZE_CONVEX"); + } + + private static void plane(float[][] planes, int p, float[] v1, float[] v2, float[] vertices, int vert) + { + RecastVectors.cross(planes[p], v1, v2); + planes[p][3] = planes[p][0] * vertices[vert] + planes[p][1] * vertices[vert + 1] + planes[p][2] * vertices[vert + 2]; + } + + private static void rasterizationFilledShape(Heightfield hf, float[] bounds, int area, int flagMergeThr, + Func intersection) + { + if (!overlapBounds(hf.bmin, hf.bmax, bounds)) + { + return; + } + + bounds[3] = Math.Min(bounds[3], hf.bmax[0]); + bounds[5] = Math.Min(bounds[5], hf.bmax[2]); + bounds[0] = Math.Max(bounds[0], hf.bmin[0]); + bounds[2] = Math.Max(bounds[2], hf.bmin[2]); + + if (bounds[3] <= bounds[0] || bounds[4] <= bounds[1] || bounds[5] <= bounds[2]) + { + return; + } + + float ics = 1.0f / hf.cs; + float ich = 1.0f / hf.ch; + int xMin = (int)((bounds[0] - hf.bmin[0]) * ics); + int zMin = (int)((bounds[2] - hf.bmin[2]) * ics); + int xMax = Math.Min(hf.width - 1, (int)((bounds[3] - hf.bmin[0]) * ics)); + int zMax = Math.Min(hf.height - 1, (int)((bounds[5] - hf.bmin[2]) * ics)); + float[] rectangle = new float[5]; + rectangle[4] = hf.bmin[1]; + for (int x = xMin; x <= xMax; x++) + { + for (int z = zMin; z <= zMax; z++) + { + rectangle[0] = x * hf.cs + hf.bmin[0]; + rectangle[1] = z * hf.cs + hf.bmin[2]; + rectangle[2] = rectangle[0] + hf.cs; + rectangle[3] = rectangle[1] + hf.cs; + float[] h = intersection.Invoke(rectangle); + if (h != null) + { + int smin = (int)Math.Floor((h[0] - hf.bmin[1]) * ich); + int smax = (int)Math.Ceiling((h[1] - hf.bmin[1]) * ich); + if (smin != smax) + { + int ismin = RecastCommon.clamp(smin, 0, SPAN_MAX_HEIGHT); + int ismax = RecastCommon.clamp(smax, ismin + 1, SPAN_MAX_HEIGHT); + RecastRasterization.addSpan(hf, x, z, ismin, ismax, area, flagMergeThr); + } + } + } + } + } + + private static float[] intersectSphere(float[] rectangle, float[] center, float radiusSqr) + { + float x = Math.Max(rectangle[0], Math.Min(center[0], rectangle[2])); + float y = rectangle[4]; + float z = Math.Max(rectangle[1], Math.Min(center[2], rectangle[3])); + + float mx = x - center[0]; + float my = y - center[1]; + float mz = z - center[2]; + + float b = my; // dot(m, d) d = (0, 1, 0) + float c = lenSqr(mx, my, mz) - radiusSqr; + if (c > 0.0f && b > 0.0f) + { + return null; + } + + float discr = b * b - c; + if (discr < 0.0f) + { + return null; + } + + float discrSqrt = (float)Math.Sqrt(discr); + float tmin = -b - discrSqrt; + float tmax = -b + discrSqrt; + + if (tmin < 0.0f) + { + tmin = 0.0f; + } + + return new float[] { y + tmin, y + tmax }; + } + + private static float[] intersectCapsule(float[] rectangle, float[] start, float[] end, float[] axis, float radiusSqr) + { + float[] s = mergeIntersections(intersectSphere(rectangle, start, radiusSqr), intersectSphere(rectangle, end, radiusSqr)); + float axisLen2dSqr = axis[0] * axis[0] + axis[2] * axis[2]; + if (axisLen2dSqr > EPSILON) + { + s = slabsCylinderIntersection(rectangle, start, end, axis, radiusSqr, s); + } + + return s; + } + + private static float[] intersectCylinder(float[] rectangle, float[] start, float[] end, float[] axis, float radiusSqr) + { + float[] s = mergeIntersections( + rayCylinderIntersection(new float[] + { + clamp(start[0], rectangle[0], rectangle[2]), rectangle[4], + clamp(start[2], rectangle[1], rectangle[3]) + }, start, axis, radiusSqr), + rayCylinderIntersection(new float[] + { + clamp(end[0], rectangle[0], rectangle[2]), rectangle[4], + clamp(end[2], rectangle[1], rectangle[3]) + }, start, axis, radiusSqr)); + float axisLen2dSqr = axis[0] * axis[0] + axis[2] * axis[2]; + if (axisLen2dSqr > EPSILON) + { + s = slabsCylinderIntersection(rectangle, start, end, axis, radiusSqr, s); + } + + if (axis[1] * axis[1] > EPSILON) + { + float[][] rectangleOnStartPlane = ArrayUtils.Of(4, 3); + float[][] rectangleOnEndPlane = ArrayUtils.Of(4, 3); + float ds = dot(axis, start); + float de = dot(axis, end); + for (int i = 0; i < 4; i++) + { + float x = rectangle[(i + 1) & 2]; + float z = rectangle[(i & 2) + 1]; + float[] a = { x, rectangle[4], z }; + float dotAxisA = dot(axis, a); + float t = (ds - dotAxisA) / axis[1]; + rectangleOnStartPlane[i][0] = x; + rectangleOnStartPlane[i][1] = rectangle[4] + t; + rectangleOnStartPlane[i][2] = z; + t = (de - dotAxisA) / axis[1]; + rectangleOnEndPlane[i][0] = x; + rectangleOnEndPlane[i][1] = rectangle[4] + t; + rectangleOnEndPlane[i][2] = z; + } + + for (int i = 0; i < 4; i++) + { + s = cylinderCapIntersection(start, radiusSqr, s, i, rectangleOnStartPlane); + s = cylinderCapIntersection(end, radiusSqr, s, i, rectangleOnEndPlane); + } + } + + return s; + } + + private static float[] cylinderCapIntersection(float[] start, float radiusSqr, float[] s, int i, float[][] rectangleOnPlane) + { + int j = (i + 1) % 4; + // Ray against sphere intersection + float[] m = { rectangleOnPlane[i][0] - start[0], rectangleOnPlane[i][1] - start[1], rectangleOnPlane[i][2] - start[2] }; + float[] d = + { + rectangleOnPlane[j][0] - rectangleOnPlane[i][0], rectangleOnPlane[j][1] - rectangleOnPlane[i][1], + rectangleOnPlane[j][2] - rectangleOnPlane[i][2] + }; + float dl = dot(d, d); + float b = dot(m, d) / dl; + float c = (dot(m, m) - radiusSqr) / dl; + float discr = b * b - c; + if (discr > EPSILON) + { + float discrSqrt = (float)Math.Sqrt(discr); + float t1 = -b - discrSqrt; + float t2 = -b + discrSqrt; + if (t1 <= 1 && t2 >= 0) + { + t1 = Math.Max(0, t1); + t2 = Math.Min(1, t2); + float y1 = rectangleOnPlane[i][1] + t1 * d[1]; + float y2 = rectangleOnPlane[i][1] + t2 * d[1]; + float[] y = { Math.Min(y1, y2), Math.Max(y1, y2) }; + s = mergeIntersections(s, y); + } + } + + return s; + } + + private static float[] slabsCylinderIntersection(float[] rectangle, float[] start, float[] end, float[] axis, float radiusSqr, + float[] s) + { + if (Math.Min(start[0], end[0]) < rectangle[0]) + { + s = mergeIntersections(s, xSlabCylinderIntersection(rectangle, start, axis, radiusSqr, rectangle[0])); + } + + if (Math.Max(start[0], end[0]) > rectangle[2]) + { + s = mergeIntersections(s, xSlabCylinderIntersection(rectangle, start, axis, radiusSqr, rectangle[2])); + } + + if (Math.Min(start[2], end[2]) < rectangle[1]) + { + s = mergeIntersections(s, zSlabCylinderIntersection(rectangle, start, axis, radiusSqr, rectangle[1])); + } + + if (Math.Max(start[2], end[2]) > rectangle[3]) + { + s = mergeIntersections(s, zSlabCylinderIntersection(rectangle, start, axis, radiusSqr, rectangle[3])); + } + + return s; + } + + private static float[] xSlabCylinderIntersection(float[] rectangle, float[] start, float[] axis, float radiusSqr, float x) + { + return rayCylinderIntersection(xSlabRayIntersection(rectangle, start, axis, x), start, axis, radiusSqr); + } + + private static float[] xSlabRayIntersection(float[] rectangle, float[] start, float[] direction, float x) + { + // 2d intersection of plane and segment + float t = (x - start[0]) / direction[0]; + float z = clamp(start[2] + t * direction[2], rectangle[1], rectangle[3]); + return new float[] { x, rectangle[4], z }; + } + + private static float[] zSlabCylinderIntersection(float[] rectangle, float[] start, float[] axis, float radiusSqr, float z) + { + return rayCylinderIntersection(zSlabRayIntersection(rectangle, start, axis, z), start, axis, radiusSqr); + } + + private static float[] zSlabRayIntersection(float[] rectangle, float[] start, float[] direction, float z) + { + // 2d intersection of plane and segment + float t = (z - start[2]) / direction[2]; + float x = clamp(start[0] + t * direction[0], rectangle[0], rectangle[2]); + return new float[] { x, rectangle[4], z }; + } + + // Based on Christer Ericsons's "Real-Time Collision Detection" + private static float[] rayCylinderIntersection(float[] point, float[] start, float[] axis, float radiusSqr) + { + float[] d = axis; + float[] m = { point[0] - start[0], point[1] - start[1], point[2] - start[2] }; + // float[] n = { 0, 1, 0 }; + float md = dot(m, d); + // float nd = dot(n, d); + float nd = axis[1]; + float dd = dot(d, d); + + // float nn = dot(n, n); + float nn = 1; + // float mn = dot(m, n); + float mn = m[1]; + // float a = dd * nn - nd * nd; + float a = dd - nd * nd; + float k = dot(m, m) - radiusSqr; + float c = dd * k - md * md; + if (Math.Abs(a) < EPSILON) + { + // Segment runs parallel to cylinder axis + if (c > 0.0f) + { + return null; // ’a’ and thus the segment lie outside cylinder + } + + // Now known that segment intersects cylinder; figure out how it intersects + float tt1 = -mn / nn; // Intersect segment against ’p’ endcap + float tt2 = (nd - mn) / nn; // Intersect segment against ’q’ endcap + return new float[] { point[1] + Math.Min(tt1, tt2), point[1] + Math.Max(tt1, tt2) }; + } + + float b = dd * mn - nd * md; + float discr = b * b - a * c; + if (discr < 0.0f) + { + return null; // No real roots; no intersection + } + + float discSqrt = (float)Math.Sqrt(discr); + float t1 = (-b - discSqrt) / a; + float t2 = (-b + discSqrt) / a; + + if (md + t1 * nd < 0.0f) + { + // Intersection outside cylinder on ’p’ side + t1 = -md / nd; + if (k + t1 * (2 * mn + t1 * nn) > 0.0f) + { + return null; + } + } + else if (md + t1 * nd > dd) + { + // Intersection outside cylinder on ’q’ side + t1 = (dd - md) / nd; + if (k + dd - 2 * md + t1 * (2 * (mn - nd) + t1 * nn) > 0.0f) + { + return null; + } + } + + if (md + t2 * nd < 0.0f) + { + // Intersection outside cylinder on ’p’ side + t2 = -md / nd; + if (k + t2 * (2 * mn + t2 * nn) > 0.0f) + { + return null; + } + } + else if (md + t2 * nd > dd) + { + // Intersection outside cylinder on ’q’ side + t2 = (dd - md) / nd; + if (k + dd - 2 * md + t2 * (2 * (mn - nd) + t2 * nn) > 0.0f) + { + return null; + } + } + + return new float[] { point[1] + Math.Min(t1, t2), point[1] + Math.Max(t1, t2) }; + } + + private static float[] intersectBox(float[] rectangle, float[] vertices, float[][] planes) + { + float yMin = float.PositiveInfinity; + float yMax = float.NegativeInfinity; + // check intersection with rays starting in box vertices first + for (int i = 0; i < 8; i++) + { + int vi = i * 3; + if (vertices[vi] >= rectangle[0] && vertices[vi] < rectangle[2] && vertices[vi + 2] >= rectangle[1] + && vertices[vi + 2] < rectangle[3]) + { + yMin = Math.Min(yMin, vertices[vi + 1]); + yMax = Math.Max(yMax, vertices[vi + 1]); + } + } + + // check intersection with rays starting in rectangle vertices + float[] point = new float[] { 0, rectangle[1], 0 }; + for (int i = 0; i < 4; i++) + { + point[0] = ((i & 1) == 0) ? rectangle[0] : rectangle[2]; + point[2] = ((i & 2) == 0) ? rectangle[1] : rectangle[3]; + for (int j = 0; j < 6; j++) + { + if (Math.Abs(planes[j][1]) > EPSILON) + { + float dotNormalPoint = dot(planes[j], point); + float t = (planes[j][3] - dotNormalPoint) / planes[j][1]; + float y = point[1] + t; + bool valid = true; + for (int k = 0; k < 6; k++) + { + if (k != j) + { + if (point[0] * planes[k][0] + y * planes[k][1] + point[2] * planes[k][2] > planes[k][3]) + { + valid = false; + break; + } + } + } + + if (valid) + { + yMin = Math.Min(yMin, y); + yMax = Math.Max(yMax, y); + } + } + } + } + + // check intersection with box edges + for (int i = 0; i < BOX_EDGES.Length; i += 2) + { + int vi = BOX_EDGES[i] * 3; + int vj = BOX_EDGES[i + 1] * 3; + float x = vertices[vi]; + float z = vertices[vi + 2]; + // edge slab intersection + float y = vertices[vi + 1]; + float dx = vertices[vj] - x; + float dy = vertices[vj + 1] - y; + float dz = vertices[vj + 2] - z; + if (Math.Abs(dx) > EPSILON) + { + float? iy = xSlabSegmentIntersection(rectangle, x, y, z, dx, dy, dz, rectangle[0]); + if (iy != null) + { + yMin = Math.Min(yMin, iy.Value); + yMax = Math.Max(yMax, iy.Value); + } + + iy = xSlabSegmentIntersection(rectangle, x, y, z, dx, dy, dz, rectangle[2]); + if (iy != null) + { + yMin = Math.Min(yMin, iy.Value); + yMax = Math.Max(yMax, iy.Value); + } + } + + if (Math.Abs(dz) > EPSILON) + { + float? iy = zSlabSegmentIntersection(rectangle, x, y, z, dx, dy, dz, rectangle[1]); + if (iy != null) + { + yMin = Math.Min(yMin, iy.Value); + yMax = Math.Max(yMax, iy.Value); + } + + iy = zSlabSegmentIntersection(rectangle, x, y, z, dx, dy, dz, rectangle[3]); + if (iy != null) + { + yMin = Math.Min(yMin, iy.Value); + yMax = Math.Max(yMax, iy.Value); + } + } + } + + if (yMin <= yMax) + { + return new float[] { yMin, yMax }; + } + + return null; + } + + private static float[] intersectConvex(float[] rectangle, int[] triangles, float[] verts, float[][] planes, + float[][] triBounds) + { + float imin = float.PositiveInfinity; + float imax = float.NegativeInfinity; + for (int tr = 0, tri = 0; tri < triangles.Length; tr++, tri += 3) + { + if (triBounds[tr][0] > rectangle[2] || triBounds[tr][2] < rectangle[0] || triBounds[tr][1] > rectangle[3] + || triBounds[tr][3] < rectangle[1]) + { + continue; + } + + if (Math.Abs(planes[tri][1]) < EPSILON) + { + continue; + } + + for (int i = 0; i < 3; i++) + { + int vi = triangles[tri + i] * 3; + int vj = triangles[tri + (i + 1) % 3] * 3; + float x = verts[vi]; + float z = verts[vi + 2]; + // triangle vertex + if (x >= rectangle[0] && x <= rectangle[2] && z >= rectangle[1] && z <= rectangle[3]) + { + imin = Math.Min(imin, verts[vi + 1]); + imax = Math.Max(imax, verts[vi + 1]); + } + + // triangle slab intersection + float y = verts[vi + 1]; + float dx = verts[vj] - x; + float dy = verts[vj + 1] - y; + float dz = verts[vj + 2] - z; + if (Math.Abs(dx) > EPSILON) + { + float? iy = xSlabSegmentIntersection(rectangle, x, y, z, dx, dy, dz, rectangle[0]); + if (iy != null) + { + imin = Math.Min(imin, iy.Value); + imax = Math.Max(imax, iy.Value); + } + + iy = xSlabSegmentIntersection(rectangle, x, y, z, dx, dy, dz, rectangle[2]); + if (iy != null) + { + imin = Math.Min(imin, iy.Value); + imax = Math.Max(imax, iy.Value); + } + } + + if (Math.Abs(dz) > EPSILON) + { + float? iy = zSlabSegmentIntersection(rectangle, x, y, z, dx, dy, dz, rectangle[1]); + if (iy != null) + { + imin = Math.Min(imin, iy.Value); + imax = Math.Max(imax, iy.Value); + } + + iy = zSlabSegmentIntersection(rectangle, x, y, z, dx, dy, dz, rectangle[3]); + if (iy != null) + { + imin = Math.Min(imin, iy.Value); + imax = Math.Max(imax, iy.Value); + } + } + } + + // rectangle vertex + float[] point = new float[] { 0, rectangle[1], 0 }; + for (int i = 0; i < 4; i++) + { + point[0] = ((i & 1) == 0) ? rectangle[0] : rectangle[2]; + point[2] = ((i & 2) == 0) ? rectangle[1] : rectangle[3]; + float? y = rayTriangleIntersection(point, tri, planes); + if (y != null) + { + imin = Math.Min(imin, y.Value); + imax = Math.Max(imax, y.Value); + } + } + } + + if (imin < imax) + { + return new float[] { imin, imax }; + } + + return null; + } + + private static float? xSlabSegmentIntersection(float[] rectangle, float x, float y, float z, float dx, float dy, float dz, + float slabX) + { + float x2 = x + dx; + if ((x < slabX && x2 > slabX) || (x > slabX && x2 < slabX)) + { + float t = (slabX - x) / dx; + float iz = z + dz * t; + if (iz >= rectangle[1] && iz <= rectangle[3]) + { + return y + dy * t; + } + } + + return null; + } + + private static float? zSlabSegmentIntersection(float[] rectangle, float x, float y, float z, float dx, float dy, float dz, + float slabZ) + { + float z2 = z + dz; + if ((z < slabZ && z2 > slabZ) || (z > slabZ && z2 < slabZ)) + { + float t = (slabZ - z) / dz; + float ix = x + dx * t; + if (ix >= rectangle[0] && ix <= rectangle[2]) + { + return y + dy * t; + } + } + + return null; + } + + private static float? rayTriangleIntersection(float[] point, int plane, float[][] planes) + { + float t = (planes[plane][3] - dot(planes[plane], point)) / planes[plane][1]; + float[] s = { point[0], point[1] + t, point[2] }; + float u = dot(s, planes[plane + 1]) - planes[plane + 1][3]; + if (u < 0.0f || u > 1.0f) + { + return null; + } + + float v = dot(s, planes[plane + 2]) - planes[plane + 2][3]; + if (v < 0.0f) + { + return null; + } + + float w = 1f - u - v; + if (w < 0.0f) + { + return null; + } + + return s[1]; + } + + private static float[] mergeIntersections(float[] s1, float[] s2) + { + if (s1 == null) + { + return s2; + } + + if (s2 == null) + { + return s1; + } + + return new float[] { Math.Min(s1[0], s2[0]), Math.Max(s1[1], s2[1]) }; + } + + private static float lenSqr(float dx, float dy, float dz) + { + return dx * dx + dy * dy + dz * dz; + } + + public static float clamp(float v, float min, float max) + { + return Math.Max(Math.Min(max, v), min); + } + + private static bool overlapBounds(float[] amin, float[] amax, float[] bounds) + { + bool overlap = true; + overlap = (amin[0] > bounds[3] || amax[0] < bounds[0]) ? false : overlap; + overlap = (amin[1] > bounds[4]) ? false : overlap; + overlap = (amin[2] > bounds[5] || amax[2] < bounds[2]) ? false : overlap; + return overlap; + } +} \ No newline at end of file diff --git a/src/DotRecast.Recast/RecastFilter.cs b/src/DotRecast.Recast/RecastFilter.cs new file mode 100644 index 0000000..9b2e7d4 --- /dev/null +++ b/src/DotRecast.Recast/RecastFilter.cs @@ -0,0 +1,204 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; + +namespace DotRecast.Recast; + +using static RecastConstants; + +public class RecastFilter +{ + /// @par + /// + /// Allows the formation of walkable regions that will flow over low lying + /// objects such as curbs, and up structures such as stairways. + /// + /// Two neighboring spans are walkable if: rcAbs(currentSpan.smax - neighborSpan.smax) < waklableClimb + /// + /// @warning Will override the effect of #rcFilterLedgeSpans. So if both filters are used, call + /// #rcFilterLedgeSpans after calling this filter. + /// + /// @see rcHeightfield, rcConfig + public static void filterLowHangingWalkableObstacles(Telemetry ctx, int walkableClimb, Heightfield solid) + { + ctx.startTimer("FILTER_LOW_OBSTACLES"); + + int w = solid.width; + int h = solid.height; + + for (int y = 0; y < h; ++y) + { + for (int x = 0; x < w; ++x) + { + Span ps = null; + bool previousWalkable = false; + int previousArea = RC_NULL_AREA; + + for (Span s = solid.spans[x + y * w]; s != null; ps = s, s = s.next) + { + bool walkable = s.area != RC_NULL_AREA; + // If current span is not walkable, but there is walkable + // span just below it, mark the span above it walkable too. + if (!walkable && previousWalkable) + { + if (Math.Abs(s.smax - ps.smax) <= walkableClimb) + s.area = previousArea; + } + + // Copy walkable flag so that it cannot propagate + // past multiple non-walkable objects. + previousWalkable = walkable; + previousArea = s.area; + } + } + } + + ctx.stopTimer("FILTER_LOW_OBSTACLES"); + } + + /// @par + /// + /// A ledge is a span with one or more neighbors whose maximum is further away than @p walkableClimb + /// from the current span's maximum. + /// This method removes the impact of the overestimation of conservative voxelization + /// so the resulting mesh will not have regions hanging in the air over ledges. + /// + /// A span is a ledge if: rcAbs(currentSpan.smax - neighborSpan.smax) > walkableClimb + /// + /// @see rcHeightfield, rcConfig + public static void filterLedgeSpans(Telemetry ctx, int walkableHeight, int walkableClimb, Heightfield solid) + { + ctx.startTimer("FILTER_LEDGE"); + + int w = solid.width; + int h = solid.height; + + // Mark border spans. + for (int y = 0; y < h; ++y) + { + for (int x = 0; x < w; ++x) + { + for (Span s = solid.spans[x + y * w]; s != null; s = s.next) + { + // Skip non walkable spans. + if (s.area == RC_NULL_AREA) + continue; + + int bot = (s.smax); + int top = s.next != null ? s.next.smin : SPAN_MAX_HEIGHT; + + // Find neighbours minimum height. + int minh = SPAN_MAX_HEIGHT; + + // Min and max height of accessible neighbours. + int asmin = s.smax; + int asmax = s.smax; + + for (int dir = 0; dir < 4; ++dir) + { + int dx = x + RecastCommon.GetDirOffsetX(dir); + int dy = y + RecastCommon.GetDirOffsetY(dir); + // Skip neighbours which are out of bounds. + if (dx < 0 || dy < 0 || dx >= w || dy >= h) + { + minh = Math.Min(minh, -walkableClimb - bot); + continue; + } + + // From minus infinity to the first span. + Span ns = solid.spans[dx + dy * w]; + int nbot = -walkableClimb; + int ntop = ns != null ? ns.smin : SPAN_MAX_HEIGHT; + // Skip neightbour if the gap between the spans is too small. + if (Math.Min(top, ntop) - Math.Max(bot, nbot) > walkableHeight) + minh = Math.Min(minh, nbot - bot); + + // Rest of the spans. + for (ns = solid.spans[dx + dy * w]; ns != null; ns = ns.next) + { + nbot = ns.smax; + ntop = ns.next != null ? ns.next.smin : SPAN_MAX_HEIGHT; + // Skip neightbour if the gap between the spans is too small. + if (Math.Min(top, ntop) - Math.Max(bot, nbot) > walkableHeight) + { + minh = Math.Min(minh, nbot - bot); + + // Find min/max accessible neighbour height. + if (Math.Abs(nbot - bot) <= walkableClimb) + { + if (nbot < asmin) + asmin = nbot; + if (nbot > asmax) + asmax = nbot; + } + } + } + } + + // The current span is close to a ledge if the drop to any + // neighbour span is less than the walkableClimb. + if (minh < -walkableClimb) + s.area = RC_NULL_AREA; + + // If the difference between all neighbours is too large, + // we are at steep slope, mark the span as ledge. + if ((asmax - asmin) > walkableClimb) + { + s.area = RC_NULL_AREA; + } + } + } + } + + ctx.stopTimer("FILTER_LEDGE"); + } + + /// @par + /// + /// For this filter, the clearance above the span is the distance from the span's + /// maximum to the next higher span's minimum. (Same grid column.) + /// + /// @see rcHeightfield, rcConfig + public static void filterWalkableLowHeightSpans(Telemetry ctx, int walkableHeight, Heightfield solid) + { + ctx.startTimer("FILTER_WALKABLE"); + + int w = solid.width; + int h = solid.height; + + // Remove walkable flag from spans which do not have enough + // space above them for the agent to stand there. + for (int y = 0; y < h; ++y) + { + for (int x = 0; x < w; ++x) + { + for (Span s = solid.spans[x + y * w]; s != null; s = s.next) + { + int bot = (s.smax); + int top = s.next != null ? s.next.smin : SPAN_MAX_HEIGHT; + if ((top - bot) <= walkableHeight) + s.area = RC_NULL_AREA; + } + } + } + + ctx.stopTimer("FILTER_WALKABLE"); + } +} \ No newline at end of file diff --git a/src/DotRecast.Recast/RecastLayers.cs b/src/DotRecast.Recast/RecastLayers.cs new file mode 100644 index 0000000..bb9fbaf --- /dev/null +++ b/src/DotRecast.Recast/RecastLayers.cs @@ -0,0 +1,506 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.Collections.Generic; + +namespace DotRecast.Recast; + +using static RecastCommon; +using static RecastConstants; +using static RecastVectors; +using static RecastRegion; + +public class RecastLayers { + + const int RC_MAX_LAYERS = RecastConstants.RC_NOT_CONNECTED; + const int RC_MAX_NEIS = 16; + + private class LayerRegion { + public int id; + public int layerId; + public bool @base; + public int ymin, ymax; + public List layers; + public List neis; + + public LayerRegion(int i) { + id = i; + ymin = 0xFFFF; + layerId = 0xff; + layers = new(); + neis = new(); + } + + }; + + private static void addUnique(List a, int v) { + if (!a.Contains(v)) { + a.Add(v); + } + } + + private static bool contains(List a, int v) { + return a.Contains(v); + } + + private static bool overlapRange(int amin, int amax, int bmin, int bmax) { + return (amin > bmax || amax < bmin) ? false : true; + } + + public static HeightfieldLayerSet buildHeightfieldLayers(Telemetry ctx, CompactHeightfield chf, int walkableHeight) { + + ctx.startTimer("RC_TIMER_BUILD_LAYERS"); + int w = chf.width; + int h = chf.height; + int borderSize = chf.borderSize; + int[] srcReg = new int[chf.spanCount]; + Array.Fill(srcReg, 0xFF); + int nsweeps = chf.width;// Math.Max(chf.width, chf.height); + SweepSpan[] sweeps = new SweepSpan[nsweeps]; + for (int i = 0; i < sweeps.Length; i++) { + sweeps[i] = new SweepSpan(); + } + // Partition walkable area into monotone regions. + int[] prevCount = new int[256]; + int regId = 0; + // Sweep one line at a time. + for (int y = borderSize; y < h - borderSize; ++y) { + // Collect spans from this row. + Array.Fill(prevCount, 0, 0, (regId) - (0)); + int sweepId = 0; + + for (int x = borderSize; x < w - borderSize; ++x) { + CompactCell c = chf.cells[x + y * w]; + + for (int i = c.index, ni = c.index + c.count; i < ni; ++i) { + CompactSpan s = chf.spans[i]; + if (chf.areas[i] == RC_NULL_AREA) + continue; + int sid = 0xFF; + // -x + + if (GetCon(s, 0) != RC_NOT_CONNECTED) { + int ax = x + GetDirOffsetX(0); + int ay = y + GetDirOffsetY(0); + int ai = chf.cells[ax + ay * w].index + GetCon(s, 0); + if (chf.areas[ai] != RC_NULL_AREA && srcReg[ai] != 0xff) + sid = srcReg[ai]; + } + + if (sid == 0xff) { + sid = sweepId++; + sweeps[sid].nei = 0xff; + sweeps[sid].ns = 0; + } + + // -y + if (GetCon(s, 3) != RC_NOT_CONNECTED) { + int ax = x + GetDirOffsetX(3); + int ay = y + GetDirOffsetY(3); + int ai = chf.cells[ax + ay * w].index + GetCon(s, 3); + int nr = srcReg[ai]; + if (nr != 0xff) { + // Set neighbour when first valid neighbour is + // encoutered. + if (sweeps[sid].ns == 0) + sweeps[sid].nei = nr; + + if (sweeps[sid].nei == nr) { + // Update existing neighbour + sweeps[sid].ns++; + prevCount[nr]++; + } else { + // This is hit if there is nore than one + // neighbour. + // Invalidate the neighbour. + sweeps[sid].nei = 0xff; + } + } + } + + srcReg[i] = sid; + } + } + + // Create unique ID. + for (int i = 0; i < sweepId; ++i) { + // If the neighbour is set and there is only one continuous + // connection to it, + // the sweep will be merged with the previous one, else new + // region is created. + if (sweeps[i].nei != 0xff && prevCount[sweeps[i].nei] == sweeps[i].ns) { + sweeps[i].id = sweeps[i].nei; + } else { + if (regId == 255) { + throw new Exception("rcBuildHeightfieldLayers: Region ID overflow."); + } + sweeps[i].id = regId++; + } + } + + // Remap local sweep ids to region ids. + for (int x = borderSize; x < w - borderSize; ++x) { + CompactCell c = chf.cells[x + y * w]; + for (int i = c.index, ni = c.index + c.count; i < ni; ++i) { + if (srcReg[i] != 0xff) + srcReg[i] = sweeps[srcReg[i]].id; + } + } + } + int nregs = regId; + LayerRegion[] regs = new LayerRegion[nregs]; + + // Construct regions + for (int i = 0; i < nregs; ++i) { + regs[i] = new LayerRegion(i); + } + + // Find region neighbours and overlapping regions. + List lregs = new(); + for (int y = 0; y < h; ++y) { + for (int x = 0; x < w; ++x) { + CompactCell c = chf.cells[x + y * w]; + + lregs.Clear(); + + for (int i = c.index, ni = c.index + c.count; i < ni; ++i) { + CompactSpan s = chf.spans[i]; + int ri = srcReg[i]; + if (ri == 0xff) + continue; + + regs[ri].ymin = Math.Min(regs[ri].ymin, s.y); + regs[ri].ymax = Math.Max(regs[ri].ymax, s.y); + + // Collect all region layers. + lregs.Add(ri); + + // Update neighbours + for (int dir = 0; dir < 4; ++dir) { + if (GetCon(s, dir) != RC_NOT_CONNECTED) { + int ax = x + GetDirOffsetX(dir); + int ay = y + GetDirOffsetY(dir); + int ai = chf.cells[ax + ay * w].index + GetCon(s, dir); + int rai = srcReg[ai]; + if (rai != 0xff && rai != ri) + addUnique(regs[ri].neis, rai); + } + } + + } + + // Update overlapping regions. + for (int i = 0; i < lregs.Count - 1; ++i) { + for (int j = i + 1; j < lregs.Count; ++j) { + if (lregs[i] != lregs[j]) { + LayerRegion ri = regs[lregs[i]]; + LayerRegion rj = regs[lregs[j]]; + addUnique(ri.layers, lregs[j]); + addUnique(rj.layers, lregs[i]); + } + } + } + + } + } + + // Create 2D layers from regions. + int layerId = 0; + + List stack = new(); + + for (int i = 0; i < nregs; ++i) { + LayerRegion root = regs[i]; + // Skip already visited. + if (root.layerId != 0xff) + continue; + + // Start search. + root.layerId = layerId; + root.@base = true; + + stack.Add(i); + + while (stack.Count != 0) { + // Pop front + int pop = stack[0]; // TODO : 여기에 stack 처럼 작동하게 했는데, 스택인지는 모르겠음 + stack.RemoveAt(0); + LayerRegion reg = regs[pop]; + + foreach (int nei in reg.neis) { + LayerRegion regn = regs[nei]; + // Skip already visited. + if (regn.layerId != 0xff) + continue; + // Skip if the neighbour is overlapping root region. + if (contains(root.layers, nei)) + continue; + // Skip if the height range would become too large. + int ymin = Math.Min(root.ymin, regn.ymin); + int ymax = Math.Max(root.ymax, regn.ymax); + if ((ymax - ymin) >= 255) + continue; + + // Deepen + stack.Add(nei); + + // Mark layer id + regn.layerId = layerId; + // Merge current layers to root. + foreach (int layer in regn.layers) + addUnique(root.layers, layer); + root.ymin = Math.Min(root.ymin, regn.ymin); + root.ymax = Math.Max(root.ymax, regn.ymax); + } + } + + layerId++; + } + + // Merge non-overlapping regions that are close in height. + int mergeHeight = walkableHeight * 4; + + for (int i = 0; i < nregs; ++i) { + LayerRegion ri = regs[i]; + if (!ri.@base) + continue; + + int newId = ri.layerId; + + for (;;) { + int oldId = 0xff; + + for (int j = 0; j < nregs; ++j) { + if (i == j) + continue; + LayerRegion rj = regs[j]; + if (!rj.@base) + continue; + + // Skip if the regions are not close to each other. + if (!overlapRange(ri.ymin, ri.ymax + mergeHeight, rj.ymin, rj.ymax + mergeHeight)) + continue; + // Skip if the height range would become too large. + int ymin = Math.Min(ri.ymin, rj.ymin); + int ymax = Math.Max(ri.ymax, rj.ymax); + if ((ymax - ymin) >= 255) + continue; + + // Make sure that there is no overlap when merging 'ri' and + // 'rj'. + bool overlap = false; + // Iterate over all regions which have the same layerId as + // 'rj' + for (int k = 0; k < nregs; ++k) { + if (regs[k].layerId != rj.layerId) + continue; + // Check if region 'k' is overlapping region 'ri' + // Index to 'regs' is the same as region id. + if (contains(ri.layers, k)) { + overlap = true; + break; + } + } + // Cannot merge of regions overlap. + if (overlap) + continue; + + // Can merge i and j. + oldId = rj.layerId; + break; + } + + // Could not find anything to merge with, stop. + if (oldId == 0xff) + break; + + // Merge + for (int j = 0; j < nregs; ++j) { + LayerRegion rj = regs[j]; + if (rj.layerId == oldId) { + rj.@base = false; + // Remap layerIds. + rj.layerId = newId; + // Add overlaid layers from 'rj' to 'ri'. + foreach (int layer in rj.layers) + addUnique(ri.layers, layer); + // Update height bounds. + ri.ymin = Math.Min(ri.ymin, rj.ymin); + ri.ymax = Math.Max(ri.ymax, rj.ymax); + } + } + } + } + + // Compact layerIds + int[] remap = new int[256]; + + // Find number of unique layers. + layerId = 0; + for (int i = 0; i < nregs; ++i) + remap[regs[i].layerId] = 1; + for (int i = 0; i < 256; ++i) { + if (remap[i] != 0) + remap[i] = layerId++; + else + remap[i] = 0xff; + } + // Remap ids. + for (int i = 0; i < nregs; ++i) + regs[i].layerId = remap[regs[i].layerId]; + + // No layers, return empty. + if (layerId == 0) { + // ctx.stopTimer(RC_TIMER_BUILD_LAYERS); + return null; + } + + // Create layers. + // rcAssert(lset.layers == 0); + + int lw = w - borderSize * 2; + int lh = h - borderSize * 2; + + // Build contracted bbox for layers. + float[] bmin = new float[3]; + float[] bmax = new float[3]; + copy(bmin, chf.bmin); + copy(bmax, chf.bmax); + bmin[0] += borderSize * chf.cs; + bmin[2] += borderSize * chf.cs; + bmax[0] -= borderSize * chf.cs; + bmax[2] -= borderSize * chf.cs; + + HeightfieldLayerSet lset = new HeightfieldLayerSet(); + lset.layers = new HeightfieldLayerSet.HeightfieldLayer[layerId]; + for (int i = 0; i < lset.layers.Length; i++) { + lset.layers[i] = new HeightfieldLayerSet.HeightfieldLayer(); + } + + // Store layers. + for (int i = 0; i < lset.layers.Length; ++i) { + int curId = i; + + HeightfieldLayerSet.HeightfieldLayer layer = lset.layers[i]; + + int gridSize = lw * lh; + + layer.heights = new int[gridSize]; + Array.Fill(layer.heights, 0xFF); + layer.areas = new int[gridSize]; + layer.cons = new int[gridSize]; + + // Find layer height bounds. + int hmin = 0, hmax = 0; + for (int j = 0; j < nregs; ++j) { + if (regs[j].@base && regs[j].layerId == curId) { + hmin = regs[j].ymin; + hmax = regs[j].ymax; + } + } + + layer.width = lw; + layer.height = lh; + layer.cs = chf.cs; + layer.ch = chf.ch; + + // Adjust the bbox to fit the heightfield. + copy(layer.bmin, bmin); + copy(layer.bmax, bmax); + layer.bmin[1] = bmin[1] + hmin * chf.ch; + layer.bmax[1] = bmin[1] + hmax * chf.ch; + layer.hmin = hmin; + layer.hmax = hmax; + + // Update usable data region. + layer.minx = layer.width; + layer.maxx = 0; + layer.miny = layer.height; + layer.maxy = 0; + + // Copy height and area from compact heightfield. + for (int y = 0; y < lh; ++y) { + for (int x = 0; x < lw; ++x) { + int cx = borderSize + x; + int cy = borderSize + y; + CompactCell c = chf.cells[cx + cy * w]; + for (int j = c.index, nj = c.index + c.count; j < nj; ++j) { + CompactSpan s = chf.spans[j]; + // Skip unassigned regions. + if (srcReg[j] == 0xff) + continue; + // Skip of does nto belong to current layer. + int lid = regs[srcReg[j]].layerId; + if (lid != curId) + continue; + + // Update data bounds. + layer.minx = Math.Min(layer.minx, x); + layer.maxx = Math.Max(layer.maxx, x); + layer.miny = Math.Min(layer.miny, y); + layer.maxy = Math.Max(layer.maxy, y); + + // Store height and area type. + int idx = x + y * lw; + layer.heights[idx] = (char) (s.y - hmin); + layer.areas[idx] = chf.areas[j]; + + // Check connection. + char portal = (char)0; + char con = (char)0; + for (int dir = 0; dir < 4; ++dir) { + if (GetCon(s, dir) != RC_NOT_CONNECTED) { + int ax = cx + GetDirOffsetX(dir); + int ay = cy + GetDirOffsetY(dir); + int ai = chf.cells[ax + ay * w].index + GetCon(s, dir); + int alid = srcReg[ai] != 0xff ? regs[srcReg[ai]].layerId : 0xff; + // Portal mask + if (chf.areas[ai] != RC_NULL_AREA && lid != alid) { + portal |= (char)(1 << dir); + // Update height so that it matches on both + // sides of the portal. + CompactSpan @as = chf.spans[ai]; + if (@as.y > hmin) + layer.heights[idx] = Math.Max(layer.heights[idx], (char) (@as.y - hmin)); + } + // Valid connection mask + if (chf.areas[ai] != RC_NULL_AREA && lid == alid) { + int nx = ax - borderSize; + int ny = ay - borderSize; + if (nx >= 0 && ny >= 0 && nx < lw && ny < lh) + con |= (char)(1 << dir); + } + } + } + layer.cons[idx] = (portal << 4) | con; + } + } + } + + if (layer.minx > layer.maxx) + layer.minx = layer.maxx = 0; + if (layer.miny > layer.maxy) + layer.miny = layer.maxy = 0; + } + + // ctx->stopTimer(RC_TIMER_BUILD_LAYERS); + return lset; + } +} diff --git a/src/DotRecast.Recast/RecastMesh.cs b/src/DotRecast.Recast/RecastMesh.cs new file mode 100644 index 0000000..558c3eb --- /dev/null +++ b/src/DotRecast.Recast/RecastMesh.cs @@ -0,0 +1,1213 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; + +namespace DotRecast.Recast; + +using static RecastConstants; + +public class RecastMesh { + + public const int MAX_MESH_VERTS_POLY = 0xffff; + public const int VERTEX_BUCKET_COUNT = (1 << 12); + + private class Edge { + public int[] vert = new int[2]; + public int[] polyEdge = new int[2]; + public int[] poly = new int[2]; + + } + + private static void buildMeshAdjacency(int[] polys, int npolys, int nverts, int vertsPerPoly) { + // Based on code by Eric Lengyel from: + // http://www.terathon.com/code/edges.php + + int maxEdgeCount = npolys * vertsPerPoly; + int[] firstEdge = new int[nverts + maxEdgeCount]; + int nextEdge = nverts; + int edgeCount = 0; + + Edge[] edges = new Edge[maxEdgeCount]; + + for (int i = 0; i < nverts; i++) + firstEdge[i] = RC_MESH_NULL_IDX; + + for (int i = 0; i < npolys; ++i) { + int t = i * vertsPerPoly * 2; + for (int j = 0; j < vertsPerPoly; ++j) { + if (polys[t + j] == RC_MESH_NULL_IDX) + break; + int v0 = polys[t + j]; + int v1 = (j + 1 >= vertsPerPoly || polys[t + j + 1] == RC_MESH_NULL_IDX) ? polys[t + 0] + : polys[t + j + 1]; + if (v0 < v1) { + Edge edge = new Edge(); + edges[edgeCount] = edge; + edge.vert[0] = v0; + edge.vert[1] = v1; + edge.poly[0] = i; + edge.polyEdge[0] = j; + edge.poly[1] = i; + edge.polyEdge[1] = 0; + // Insert edge + firstEdge[nextEdge + edgeCount] = firstEdge[v0]; + firstEdge[v0] = edgeCount; + edgeCount++; + } + } + } + + for (int i = 0; i < npolys; ++i) { + int t = i * vertsPerPoly * 2; + for (int j = 0; j < vertsPerPoly; ++j) { + if (polys[t + j] == RC_MESH_NULL_IDX) + break; + int v0 = polys[t + j]; + int v1 = (j + 1 >= vertsPerPoly || polys[t + j + 1] == RC_MESH_NULL_IDX) ? polys[t + 0] + : polys[t + j + 1]; + if (v0 > v1) { + for (int e = firstEdge[v1]; e != RC_MESH_NULL_IDX; e = firstEdge[nextEdge + e]) { + Edge edge = edges[e]; + if (edge.vert[1] == v0 && edge.poly[0] == edge.poly[1]) { + edge.poly[1] = i; + edge.polyEdge[1] = j; + break; + } + } + } + } + } + + // Store adjacency + for (int i = 0; i < edgeCount; ++i) { + Edge e = edges[i]; + if (e.poly[0] != e.poly[1]) { + int p0 = e.poly[0] * vertsPerPoly * 2; + int p1 = e.poly[1] * vertsPerPoly * 2; + polys[p0 + vertsPerPoly + e.polyEdge[0]] = e.poly[1]; + polys[p1 + vertsPerPoly + e.polyEdge[1]] = e.poly[0]; + } + } + + } + + private static int computeVertexHash(int x, int y, int z) { + uint h1 = 0x8da6b343; // Large multiplicative constants; + uint h2 = 0xd8163841; // here arbitrarily chosen primes + uint h3 = 0xcb1ab31f; + + uint n = h1 * (uint)x + h2 * (uint)y + h3 * (uint)z; + return (int)(n & (VERTEX_BUCKET_COUNT - 1)); + } + + private static int[] addVertex(int x, int y, int z, int[] verts, int[] firstVert, int[] nextVert, int nv) { + int bucket = computeVertexHash(x, 0, z); + int i = firstVert[bucket]; + + while (i != -1) { + int v = i * 3; + if (verts[v + 0] == x && (Math.Abs(verts[v + 1] - y) <= 2) && verts[v + 2] == z) + return new int[] { i, nv }; + i = nextVert[i]; // next + } + + // Could not find, create new. + i = nv; + nv++; + int v2 = i * 3; + verts[v2 + 0] = x; + verts[v2 + 1] = y; + verts[v2 + 2] = z; + nextVert[i] = firstVert[bucket]; + firstVert[bucket] = i; + + return new int[] { i, nv }; + } + + public static int prev(int i, int n) { + return i - 1 >= 0 ? i - 1 : n - 1; + } + + public static int next(int i, int n) { + return i + 1 < n ? i + 1 : 0; + } + + private static int area2(int[] verts, int a, int b, int c) { + return (verts[b + 0] - verts[a + 0]) * (verts[c + 2] - verts[a + 2]) + - (verts[c + 0] - verts[a + 0]) * (verts[b + 2] - verts[a + 2]); + } + + // Returns true iff c is strictly to the left of the directed + // line through a to b. + public static bool left(int[] verts, int a, int b, int c) { + return area2(verts, a, b, c) < 0; + } + + public static bool leftOn(int[] verts, int a, int b, int c) { + return area2(verts, a, b, c) <= 0; + } + + private static bool collinear(int[] verts, int a, int b, int c) { + return area2(verts, a, b, c) == 0; + } + + // Returns true iff ab properly intersects cd: they share + // a point interior to both segments. The properness of the + // intersection is ensured by using strict leftness. + private static bool intersectProp(int[] verts, int a, int b, int c, int d) { + // Eliminate improper cases. + if (collinear(verts, a, b, c) || collinear(verts, a, b, d) || collinear(verts, c, d, a) + || collinear(verts, c, d, b)) + return false; + + return (left(verts, a, b, c) ^ left(verts, a, b, d)) && (left(verts, c, d, a) ^ left(verts, c, d, b)); + } + + // Returns T iff (a,b,c) are collinear and point c lies + // on the closed segement ab. + private static bool between(int[] verts, int a, int b, int c) { + if (!collinear(verts, a, b, c)) + return false; + // If ab not vertical, check betweenness on x; else on y. + if (verts[a + 0] != verts[b + 0]) + return ((verts[a + 0] <= verts[c + 0]) && (verts[c + 0] <= verts[b + 0])) + || ((verts[a + 0] >= verts[c + 0]) && (verts[c + 0] >= verts[b + 0])); + else + return ((verts[a + 2] <= verts[c + 2]) && (verts[c + 2] <= verts[b + 2])) + || ((verts[a + 2] >= verts[c + 2]) && (verts[c + 2] >= verts[b + 2])); + } + + // Returns true iff segments ab and cd intersect, properly or improperly. + public static bool intersect(int[] verts, int a, int b, int c, int d) { + if (intersectProp(verts, a, b, c, d)) + return true; + else if (between(verts, a, b, c) || between(verts, a, b, d) || between(verts, c, d, a) + || between(verts, c, d, b)) + return true; + else + return false; + } + + public static bool vequal(int[] verts, int a, int b) { + return verts[a + 0] == verts[b + 0] && verts[a + 2] == verts[b + 2]; + } + + // Returns T iff (v_i, v_j) is a proper internal *or* external + // diagonal of P, *ignoring edges incident to v_i and v_j*. + private static bool diagonalie(int i, int j, int n, int[] verts, int[] indices) { + int d0 = (indices[i] & 0x0fffffff) * 4; + int d1 = (indices[j] & 0x0fffffff) * 4; + + // For each edge (k,k+1) of P + for (int k = 0; k < n; k++) { + int k1 = next(k, n); + // Skip edges incident to i or j + if (!((k == i) || (k1 == i) || (k == j) || (k1 == j))) { + int p0 = (indices[k] & 0x0fffffff) * 4; + int p1 = (indices[k1] & 0x0fffffff) * 4; + + if (vequal(verts, d0, p0) || vequal(verts, d1, p0) || vequal(verts, d0, p1) || vequal(verts, d1, p1)) + continue; + + if (intersect(verts, d0, d1, p0, p1)) + return false; + } + } + return true; + } + + // Returns true iff the diagonal (i,j) is strictly internal to the + // polygon P in the neighborhood of the i endpoint. + private static bool inCone(int i, int j, int n, int[] verts, int[] indices) { + int pi = (indices[i] & 0x0fffffff) * 4; + int pj = (indices[j] & 0x0fffffff) * 4; + int pi1 = (indices[next(i, n)] & 0x0fffffff) * 4; + int pin1 = (indices[prev(i, n)] & 0x0fffffff) * 4; + // If P[i] is a convex vertex [ i+1 left or on (i-1,i) ]. + if (leftOn(verts, pin1, pi, pi1)) { + return left(verts, pi, pj, pin1) && left(verts, pj, pi, pi1); + } + // Assume (i-1,i,i+1) not collinear. + // else P[i] is reflex. + return !(leftOn(verts, pi, pj, pi1) && leftOn(verts, pj, pi, pin1)); + } + + // Returns T iff (v_i, v_j) is a proper internal + // diagonal of P. + private static bool diagonal(int i, int j, int n, int[] verts, int[] indices) { + return inCone(i, j, n, verts, indices) && diagonalie(i, j, n, verts, indices); + } + + private static bool diagonalieLoose(int i, int j, int n, int[] verts, int[] indices) { + int d0 = (indices[i] & 0x0fffffff) * 4; + int d1 = (indices[j] & 0x0fffffff) * 4; + + // For each edge (k,k+1) of P + for (int k = 0; k < n; k++) { + int k1 = next(k, n); + // Skip edges incident to i or j + if (!((k == i) || (k1 == i) || (k == j) || (k1 == j))) { + int p0 = (indices[k] & 0x0fffffff) * 4; + int p1 = (indices[k1] & 0x0fffffff) * 4; + + if (vequal(verts, d0, p0) || vequal(verts, d1, p0) || vequal(verts, d0, p1) || vequal(verts, d1, p1)) + continue; + + if (intersectProp(verts, d0, d1, p0, p1)) + return false; + } + } + return true; + } + + private static bool inConeLoose(int i, int j, int n, int[] verts, int[] indices) { + int pi = (indices[i] & 0x0fffffff) * 4; + int pj = (indices[j] & 0x0fffffff) * 4; + int pi1 = (indices[next(i, n)] & 0x0fffffff) * 4; + int pin1 = (indices[prev(i, n)] & 0x0fffffff) * 4; + + // If P[i] is a convex vertex [ i+1 left or on (i-1,i) ]. + if (leftOn(verts, pin1, pi, pi1)) + return leftOn(verts, pi, pj, pin1) && leftOn(verts, pj, pi, pi1); + // Assume (i-1,i,i+1) not collinear. + // else P[i] is reflex. + return !(leftOn(verts, pi, pj, pi1) && leftOn(verts, pj, pi, pin1)); + } + + private static bool diagonalLoose(int i, int j, int n, int[] verts, int[] indices) { + return inConeLoose(i, j, n, verts, indices) && diagonalieLoose(i, j, n, verts, indices); + } + + private static int triangulate(int n, int[] verts, int[] indices, int[] tris) { + int ntris = 0; + + // The last bit of the index is used to indicate if the vertex can be removed. + for (int i = 0; i < n; i++) { + int i1 = next(i, n); + int i2 = next(i1, n); + if (diagonal(i, i2, n, verts, indices)) { + indices[i1] |= int.MinValue; // TODO : 체크 필요 + } + } + + while (n > 3) { + int minLen = -1; + int mini = -1; + for (int minIdx = 0; minIdx < n; minIdx++) { + int nextIdx1 = next(minIdx, n); + if ((indices[nextIdx1] & 0x80000000) != 0) { + int p0 = (indices[minIdx] & 0x0fffffff) * 4; + int p2 = (indices[next(nextIdx1, n)] & 0x0fffffff) * 4; + + int dx = verts[p2 + 0] - verts[p0 + 0]; + int dy = verts[p2 + 2] - verts[p0 + 2]; + int len = dx * dx + dy * dy; + + if (minLen < 0 || len < minLen) { + minLen = len; + mini = minIdx; + } + } + } + + if (mini == -1) { + // We might get here because the contour has overlapping segments, like this: + // + // A o-o=====o---o B + // / |C D| \ + // o o o o + // : : : : + // We'll try to recover by loosing up the inCone test a bit so that a diagonal + // like A-B or C-D can be found and we can continue. + minLen = -1; + mini = -1; + for (int minIdx = 0; minIdx < n; minIdx++) { + int nextIdx1 = next(minIdx, n); + int nextIdx2 = next(nextIdx1, n); + if (diagonalLoose(minIdx, nextIdx2, n, verts, indices)) { + int p0 = (indices[minIdx] & 0x0fffffff) * 4; + int p2 = (indices[next(nextIdx2, n)] & 0x0fffffff) * 4; + int dx = verts[p2 + 0] - verts[p0 + 0]; + int dy = verts[p2 + 2] - verts[p0 + 2]; + int len = dx * dx + dy * dy; + + if (minLen < 0 || len < minLen) { + minLen = len; + mini = minIdx; + } + } + } + if (mini == -1) { + // The contour is messed up. This sometimes happens + // if the contour simplification is too aggressive. + return -ntris; + } + } + + int i = mini; + int i1 = next(i, n); + int i2 = next(i1, n); + + tris[ntris * 3] = indices[i] & 0x0fffffff; + tris[ntris * 3 + 1] = indices[i1] & 0x0fffffff; + tris[ntris * 3 + 2] = indices[i2] & 0x0fffffff; + ntris++; + + // Removes P[i1] by copying P[i+1]...P[n-1] left one index. + n--; + for (int k = i1; k < n; k++) + indices[k] = indices[k + 1]; + + if (i1 >= n) + i1 = 0; + i = prev(i1, n); + // Update diagonal flags. + if (diagonal(prev(i, n), i1, n, verts, indices)) + indices[i] |= int.MinValue; + else + indices[i] &= 0x0fffffff; + + if (diagonal(i, next(i1, n), n, verts, indices)) + indices[i1] |= int.MinValue; + else + indices[i1] &= 0x0fffffff; + } + + // Append the remaining triangle. + tris[ntris * 3] = indices[0] & 0x0fffffff; + tris[ntris * 3 + 1] = indices[1] & 0x0fffffff; + tris[ntris * 3 + 2] = indices[2] & 0x0fffffff; + ntris++; + + return ntris; + } + + private static int countPolyVerts(int[] p, int j, int nvp) { + for (int i = 0; i < nvp; ++i) + if (p[i + j] == RC_MESH_NULL_IDX) + return i; + return nvp; + } + + private static bool uleft(int[] verts, int a, int b, int c) { + return (verts[b + 0] - verts[a + 0]) * (verts[c + 2] - verts[a + 2]) + - (verts[c + 0] - verts[a + 0]) * (verts[b + 2] - verts[a + 2]) < 0; + } + + private static int[] getPolyMergeValue(int[] polys, int pa, int pb, int[] verts, int nvp) { + int ea = -1; + int eb = -1; + int na = countPolyVerts(polys, pa, nvp); + int nb = countPolyVerts(polys, pb, nvp); + + // If the merged polygon would be too big, do not merge. + if (na + nb - 2 > nvp) + return new int[] { -1, ea, eb }; + + // Check if the polygons share an edge. + + for (int i = 0; i < na; ++i) { + int va0 = polys[pa + i]; + int va1 = polys[pa + (i + 1) % na]; + if (va0 > va1) { + int temp = va0; + va0 = va1; + va1 = temp; + } + for (int j = 0; j < nb; ++j) { + int vb0 = polys[pb + j]; + int vb1 = polys[pb + (j + 1) % nb]; + if (vb0 > vb1) { + int temp = vb0; + vb0 = vb1; + vb1 = temp; + } + if (va0 == vb0 && va1 == vb1) { + ea = i; + eb = j; + break; + } + } + } + + // No common edge, cannot merge. + if (ea == -1 || eb == -1) + return new int[] { -1, ea, eb }; + + // Check to see if the merged polygon would be convex. + int va, vb, vc; + + va = polys[pa + (ea + na - 1) % na]; + vb = polys[pa + ea]; + vc = polys[pb + (eb + 2) % nb]; + if (!uleft(verts, va * 3, vb * 3, vc * 3)) + return new int[] { -1, ea, eb }; + + va = polys[pb + (eb + nb - 1) % nb]; + vb = polys[pb + eb]; + vc = polys[pa + (ea + 2) % na]; + if (!uleft(verts, va * 3, vb * 3, vc * 3)) + return new int[] { -1, ea, eb }; + + va = polys[pa + ea]; + vb = polys[pa + (ea + 1) % na]; + + int dx = verts[va * 3 + 0] - verts[vb * 3 + 0]; + int dy = verts[va * 3 + 2] - verts[vb * 3 + 2]; + + return new int[] { dx * dx + dy * dy, ea, eb }; + } + + private static void mergePolyVerts(int[] polys, int pa, int pb, int ea, int eb, int tmp, int nvp) { + int na = countPolyVerts(polys, pa, nvp); + int nb = countPolyVerts(polys, pb, nvp); + + // Merge polygons. + Array.Fill(polys, RC_MESH_NULL_IDX, tmp, (tmp + nvp) - (tmp)); + int n = 0; + // Add pa + for (int i = 0; i < na - 1; ++i) { + polys[tmp + n] = polys[pa + (ea + 1 + i) % na]; + n++; + } + // Add pb + for (int i = 0; i < nb - 1; ++i) { + polys[tmp + n] = polys[pb + (eb + 1 + i) % nb]; + n++; + } + Array.Copy(polys, tmp, polys, pa, nvp); + } + + private static int pushFront(int v, int[] arr, int an) { + an++; + for (int i = an - 1; i > 0; --i) + arr[i] = arr[i - 1]; + arr[0] = v; + return an; + } + + private static int pushBack(int v, int[] arr, int an) { + arr[an] = v; + an++; + return an; + } + + private static bool canRemoveVertex(Telemetry ctx, PolyMesh mesh, int rem) { + int nvp = mesh.nvp; + + // Count number of polygons to remove. + int numTouchedVerts = 0; + int numRemainingEdges = 0; + for (int i = 0; i < mesh.npolys; ++i) { + int p = i * nvp * 2; + int nv = countPolyVerts(mesh.polys, p, nvp); + int numRemoved = 0; + int numVerts = 0; + for (int j = 0; j < nv; ++j) { + if (mesh.polys[p + j] == rem) { + numTouchedVerts++; + numRemoved++; + } + numVerts++; + } + if (numRemoved != 0) { + numRemainingEdges += numVerts - (numRemoved + 1); + } + } + // There would be too few edges remaining to create a polygon. + // This can happen for example when a tip of a triangle is marked + // as deletion, but there are no other polys that share the vertex. + // In this case, the vertex should not be removed. + if (numRemainingEdges <= 2) + return false; + + // Find edges which share the removed vertex. + int maxEdges = numTouchedVerts * 2; + int nedges = 0; + int[] edges = new int[maxEdges * 3]; + + for (int i = 0; i < mesh.npolys; ++i) { + int p = i * nvp * 2; + int nv = countPolyVerts(mesh.polys, p, nvp); + + // Collect edges which touches the removed vertex. + for (int j = 0, k = nv - 1; j < nv; k = j++) { + if (mesh.polys[p + j] == rem || mesh.polys[p + k] == rem) { + // Arrange edge so that a=rem. + int a = mesh.polys[p + j], b = mesh.polys[p + k]; + if (b == rem) { + int temp = a; + a = b; + b = temp; + } + // Check if the edge exists + bool exists = false; + for (int m = 0; m < nedges; ++m) { + int e = m * 3; + if (edges[e + 1] == b) { + // Exists, increment vertex share count. + edges[e + 2]++; + exists = true; + } + } + // Add new edge. + if (!exists) { + int e = nedges * 3; + edges[e + 0] = a; + edges[e + 1] = b; + edges[e + 2] = 1; + nedges++; + } + } + } + } + + // There should be no more than 2 open edges. + // This catches the case that two non-adjacent polygons + // share the removed vertex. In that case, do not remove the vertex. + int numOpenEdges = 0; + for (int i = 0; i < nedges; ++i) { + if (edges[i * 3 + 2] < 2) + numOpenEdges++; + } + if (numOpenEdges > 2) + return false; + + return true; + } + + private static void removeVertex(Telemetry ctx, PolyMesh mesh, int rem, int maxTris) { + int nvp = mesh.nvp; + + // Count number of polygons to remove. + int numRemovedVerts = 0; + for (int i = 0; i < mesh.npolys; ++i) { + int p = i * nvp * 2; + int nv = countPolyVerts(mesh.polys, p, nvp); + for (int j = 0; j < nv; ++j) { + if (mesh.polys[p + j] == rem) + numRemovedVerts++; + } + } + + int nedges = 0; + int[] edges = new int[numRemovedVerts * nvp * 4]; + + int nhole = 0; + int[] hole = new int[numRemovedVerts * nvp]; + + int nhreg = 0; + int[] hreg = new int[numRemovedVerts * nvp]; + + int nharea = 0; + int[] harea = new int[numRemovedVerts * nvp]; + + for (int i = 0; i < mesh.npolys; ++i) { + int p = i * nvp * 2; + int nv = countPolyVerts(mesh.polys, p, nvp); + bool hasRem = false; + for (int j = 0; j < nv; ++j) + if (mesh.polys[p + j] == rem) + hasRem = true; + if (hasRem) { + // Collect edges which does not touch the removed vertex. + for (int j = 0, k = nv - 1; j < nv; k = j++) { + if (mesh.polys[p + j] != rem && mesh.polys[p + k] != rem) { + int e = nedges * 4; + edges[e + 0] = mesh.polys[p + k]; + edges[e + 1] = mesh.polys[p + j]; + edges[e + 2] = mesh.regs[i]; + edges[e + 3] = mesh.areas[i]; + nedges++; + } + } + // Remove the polygon. + int p2 = (mesh.npolys - 1) * nvp * 2; + if (p != p2) { + Array.Copy(mesh.polys, p2, mesh.polys, p, nvp); + } + Array.Fill(mesh.polys, RC_MESH_NULL_IDX, p + nvp, (p + nvp + nvp) - (p + nvp)); + mesh.regs[i] = mesh.regs[mesh.npolys - 1]; + mesh.areas[i] = mesh.areas[mesh.npolys - 1]; + mesh.npolys--; + --i; + } + } + + // Remove vertex. + for (int i = rem; i < mesh.nverts - 1; ++i) { + mesh.verts[i * 3 + 0] = mesh.verts[(i + 1) * 3 + 0]; + mesh.verts[i * 3 + 1] = mesh.verts[(i + 1) * 3 + 1]; + mesh.verts[i * 3 + 2] = mesh.verts[(i + 1) * 3 + 2]; + } + mesh.nverts--; + + // Adjust indices to match the removed vertex layout. + for (int i = 0; i < mesh.npolys; ++i) { + int p = i * nvp * 2; + int nv = countPolyVerts(mesh.polys, p, nvp); + for (int j = 0; j < nv; ++j) + if (mesh.polys[p + j] > rem) + mesh.polys[p + j]--; + } + for (int i = 0; i < nedges; ++i) { + if (edges[i * 4 + 0] > rem) + edges[i * 4 + 0]--; + if (edges[i * 4 + 1] > rem) + edges[i * 4 + 1]--; + } + + if (nedges == 0) + return; + + // Start with one vertex, keep appending connected + // segments to the start and end of the hole. + nhole = pushBack(edges[0], hole, nhole); + nhreg = pushBack(edges[2], hreg, nhreg); + nharea = pushBack(edges[3], harea, nharea); + + while (nedges != 0) { + bool match = false; + + for (int i = 0; i < nedges; ++i) { + int ea = edges[i * 4 + 0]; + int eb = edges[i * 4 + 1]; + int r = edges[i * 4 + 2]; + int a = edges[i * 4 + 3]; + bool add = false; + if (hole[0] == eb) { + // The segment matches the beginning of the hole boundary. + nhole = pushFront(ea, hole, nhole); + nhreg = pushFront(r, hreg, nhreg); + nharea = pushFront(a, harea, nharea); + add = true; + } else if (hole[nhole - 1] == ea) { + // The segment matches the end of the hole boundary. + nhole = pushBack(eb, hole, nhole); + nhreg = pushBack(r, hreg, nhreg); + nharea = pushBack(a, harea, nharea); + add = true; + } + if (add) { + // The edge segment was added, remove it. + edges[i * 4 + 0] = edges[(nedges - 1) * 4 + 0]; + edges[i * 4 + 1] = edges[(nedges - 1) * 4 + 1]; + edges[i * 4 + 2] = edges[(nedges - 1) * 4 + 2]; + edges[i * 4 + 3] = edges[(nedges - 1) * 4 + 3]; + --nedges; + match = true; + --i; + } + } + + if (!match) + break; + } + + int[] tris = new int[nhole * 3]; + + int[] tverts = new int[nhole * 4]; + + int[] thole = new int[nhole]; + + // Generate temp vertex array for triangulation. + for (int i = 0; i < nhole; ++i) { + int pi = hole[i]; + tverts[i * 4 + 0] = mesh.verts[pi * 3 + 0]; + tverts[i * 4 + 1] = mesh.verts[pi * 3 + 1]; + tverts[i * 4 + 2] = mesh.verts[pi * 3 + 2]; + tverts[i * 4 + 3] = 0; + thole[i] = i; + } + + // Triangulate the hole. + int ntris = triangulate(nhole, tverts, thole, tris); + if (ntris < 0) { + ntris = -ntris; + ctx.warn("removeVertex: triangulate() returned bad results."); + } + + // Merge the hole triangles back to polygons. + int[] polys = new int[(ntris + 1) * nvp]; + int[] pregs = new int[ntris]; + int[] pareas = new int[ntris]; + + int tmpPoly = ntris * nvp; + + // Build initial polygons. + int npolys = 0; + Array.Fill(polys, RC_MESH_NULL_IDX, 0, (ntris * nvp) - (0)); + for (int j = 0; j < ntris; ++j) { + int t = j * 3; + if (tris[t + 0] != tris[t + 1] && tris[t + 0] != tris[t + 2] && tris[t + 1] != tris[t + 2]) { + polys[npolys * nvp + 0] = hole[tris[t + 0]]; + polys[npolys * nvp + 1] = hole[tris[t + 1]]; + polys[npolys * nvp + 2] = hole[tris[t + 2]]; + + // If this polygon covers multiple region types then + // mark it as such + if (hreg[tris[t + 0]] != hreg[tris[t + 1]] || hreg[tris[t + 1]] != hreg[tris[t + 2]]) + pregs[npolys] = RC_MULTIPLE_REGS; + else + pregs[npolys] = hreg[tris[t + 0]]; + + pareas[npolys] = harea[tris[t + 0]]; + npolys++; + } + } + if (npolys == 0) + return; + + // Merge polygons. + if (nvp > 3) { + for (;;) { + // Find best polygons to merge. + int bestMergeVal = 0; + int bestPa = 0, bestPb = 0, bestEa = 0, bestEb = 0; + + for (int j = 0; j < npolys - 1; ++j) { + int pj = j * nvp; + for (int k = j + 1; k < npolys; ++k) { + int pk = k * nvp; + int[] veaeb = getPolyMergeValue(polys, pj, pk, mesh.verts, nvp); + int v = veaeb[0]; + int ea = veaeb[1]; + int eb = veaeb[2]; + if (v > bestMergeVal) { + bestMergeVal = v; + bestPa = j; + bestPb = k; + bestEa = ea; + bestEb = eb; + } + } + } + + if (bestMergeVal > 0) { + // Found best, merge. + int pa = bestPa * nvp; + int pb = bestPb * nvp; + mergePolyVerts(polys, pa, pb, bestEa, bestEb, tmpPoly, nvp); + if (pregs[bestPa] != pregs[bestPb]) + pregs[bestPa] = RC_MULTIPLE_REGS; + int last = (npolys - 1) * nvp; + if (pb != last) { + Array.Copy(polys, last, polys, pb, nvp); + } + pregs[bestPb] = pregs[npolys - 1]; + pareas[bestPb] = pareas[npolys - 1]; + npolys--; + } else { + // Could not merge any polygons, stop. + break; + } + } + } + + // Store polygons. + for (int i = 0; i < npolys; ++i) { + if (mesh.npolys >= maxTris) + break; + int p = mesh.npolys * nvp * 2; + Array.Fill(mesh.polys, RC_MESH_NULL_IDX, p, (p + nvp * 2) - (p)); + for (int j = 0; j < nvp; ++j) + mesh.polys[p + j] = polys[i * nvp + j]; + mesh.regs[mesh.npolys] = pregs[i]; + mesh.areas[mesh.npolys] = pareas[i]; + mesh.npolys++; + if (mesh.npolys > maxTris) { + throw new Exception("removeVertex: Too many polygons " + mesh.npolys + " (max:" + maxTris + "."); + } + } + + } + + /// @par + /// + /// @note If the mesh data is to be used to construct a Detour navigation mesh, then the upper + /// limit must be retricted to <= #DT_VERTS_PER_POLYGON. + /// + /// @see rcAllocPolyMesh, rcContourSet, rcPolyMesh, rcConfig + public static PolyMesh buildPolyMesh(Telemetry ctx, ContourSet cset, int nvp) { + ctx.startTimer("POLYMESH"); + PolyMesh mesh = new PolyMesh(); + RecastVectors.copy(mesh.bmin, cset.bmin, 0); + RecastVectors.copy(mesh.bmax, cset.bmax, 0); + mesh.cs = cset.cs; + mesh.ch = cset.ch; + mesh.borderSize = cset.borderSize; + mesh.maxEdgeError = cset.maxError; + + int maxVertices = 0; + int maxTris = 0; + int maxVertsPerCont = 0; + for (int i = 0; i < cset.conts.Count; ++i) { + // Skip null contours. + if (cset.conts[i].nverts < 3) + continue; + maxVertices += cset.conts[i].nverts; + maxTris += cset.conts[i].nverts - 2; + maxVertsPerCont = Math.Max(maxVertsPerCont, cset.conts[i].nverts); + } + if (maxVertices >= 0xfffe) { + throw new Exception("rcBuildPolyMesh: Too many vertices " + maxVertices); + } + int[] vflags = new int[maxVertices]; + + mesh.verts = new int[maxVertices * 3]; + mesh.polys = new int[maxTris * nvp * 2]; + Array.Fill(mesh.polys, RC_MESH_NULL_IDX); + mesh.regs = new int[maxTris]; + mesh.areas = new int[maxTris]; + + mesh.nverts = 0; + mesh.npolys = 0; + mesh.nvp = nvp; + mesh.maxpolys = maxTris; + + int[] nextVert = new int[maxVertices]; + + int[] firstVert = new int[VERTEX_BUCKET_COUNT]; + for (int i = 0; i < VERTEX_BUCKET_COUNT; ++i) + firstVert[i] = -1; + + int[] indices = new int[maxVertsPerCont]; + int[] tris = new int[maxVertsPerCont * 3]; + int[] polys = new int[(maxVertsPerCont + 1) * nvp]; + + int tmpPoly = maxVertsPerCont * nvp; + + for (int i = 0; i < cset.conts.Count; ++i) + { + Contour cont = cset.conts[i]; + + // Skip null contours. + if (cont.nverts < 3) + continue; + + // Triangulate contour + for (int j = 0; j < cont.nverts; ++j) + indices[j] = j; + int ntris = triangulate(cont.nverts, cont.verts, indices, tris); + if (ntris <= 0) { + // Bad triangulation, should not happen. + ctx.warn("buildPolyMesh: Bad triangulation Contour " + i + "."); + ntris = -ntris; + } + + // Add and merge vertices. + for (int j = 0; j < cont.nverts; ++j) { + int v = j * 4; + int[] inv = addVertex(cont.verts[v + 0], cont.verts[v + 1], cont.verts[v + 2], mesh.verts, firstVert, + nextVert, mesh.nverts); + indices[j] = inv[0]; + mesh.nverts = inv[1]; + if ((cont.verts[v + 3] & RC_BORDER_VERTEX) != 0) { + // This vertex should be removed. + vflags[indices[j]] = 1; + } + } + + // Build initial polygons. + int npolys = 0; + Array.Fill(polys, RC_MESH_NULL_IDX); + for (int j = 0; j < ntris; ++j) { + int t = j * 3; + if (tris[t + 0] != tris[t + 1] && tris[t + 0] != tris[t + 2] && tris[t + 1] != tris[t + 2]) { + polys[npolys * nvp + 0] = indices[tris[t + 0]]; + polys[npolys * nvp + 1] = indices[tris[t + 1]]; + polys[npolys * nvp + 2] = indices[tris[t + 2]]; + npolys++; + } + } + if (npolys == 0) + continue; + + // Merge polygons. + if (nvp > 3) { + for (;;) { + // Find best polygons to merge. + int bestMergeVal = 0; + int bestPa = 0, bestPb = 0, bestEa = 0, bestEb = 0; + + for (int j = 0; j < npolys - 1; ++j) { + int pj = j * nvp; + for (int k = j + 1; k < npolys; ++k) { + int pk = k * nvp; + int[] veaeb = getPolyMergeValue(polys, pj, pk, mesh.verts, nvp); + int v = veaeb[0]; + int ea = veaeb[1]; + int eb = veaeb[2]; + if (v > bestMergeVal) { + bestMergeVal = v; + bestPa = j; + bestPb = k; + bestEa = ea; + bestEb = eb; + } + } + } + + if (bestMergeVal > 0) { + // Found best, merge. + int pa = bestPa * nvp; + int pb = bestPb * nvp; + mergePolyVerts(polys, pa, pb, bestEa, bestEb, tmpPoly, nvp); + int lastPoly = (npolys - 1) * nvp; + if (pb != lastPoly) { + Array.Copy(polys, lastPoly, polys, pb, nvp); + } + npolys--; + } else { + // Could not merge any polygons, stop. + break; + } + } + } + + // Store polygons. + for (int j = 0; j < npolys; ++j) { + int p = mesh.npolys * nvp * 2; + int q = j * nvp; + for (int k = 0; k < nvp; ++k) + mesh.polys[p + k] = polys[q + k]; + mesh.regs[mesh.npolys] = cont.reg; + mesh.areas[mesh.npolys] = cont.area; + mesh.npolys++; + if (mesh.npolys > maxTris) { + throw new Exception( + "rcBuildPolyMesh: Too many polygons " + mesh.npolys + " (max:" + maxTris + ")."); + } + } + } + + // Remove edge vertices. + for (int i = 0; i < mesh.nverts; ++i) { + if (vflags[i] != 0) { + if (!canRemoveVertex(ctx, mesh, i)) + continue; + removeVertex(ctx, mesh, i, maxTris); + // Remove vertex + // Note: mesh.nverts is already decremented inside removeVertex()! + // Fixup vertex flags + for (int j = i; j < mesh.nverts; ++j) + vflags[j] = vflags[j + 1]; + --i; + } + } + + // Calculate adjacency. + buildMeshAdjacency(mesh.polys, mesh.npolys, mesh.nverts, nvp); + + // Find portal edges + if (mesh.borderSize > 0) { + int w = cset.width; + int h = cset.height; + for (int i = 0; i < mesh.npolys; ++i) { + int p = i * 2 * nvp; + for (int j = 0; j < nvp; ++j) { + if (mesh.polys[p + j] == RC_MESH_NULL_IDX) + break; + // Skip connected edges. + if (mesh.polys[p + nvp + j] != RC_MESH_NULL_IDX) + continue; + int nj = j + 1; + if (nj >= nvp || mesh.polys[p + nj] == RC_MESH_NULL_IDX) + nj = 0; + int va = mesh.polys[p + j] * 3; + int vb = mesh.polys[p + nj] * 3; + + if (mesh.verts[va + 0] == 0 && mesh.verts[vb + 0] == 0) + mesh.polys[p + nvp + j] = 0x8000 | 0; + else if (mesh.verts[va + 2] == h && mesh.verts[vb + 2] == h) + mesh.polys[p + nvp + j] = 0x8000 | 1; + else if (mesh.verts[va + 0] == w && mesh.verts[vb + 0] == w) + mesh.polys[p + nvp + j] = 0x8000 | 2; + else if (mesh.verts[va + 2] == 0 && mesh.verts[vb + 2] == 0) + mesh.polys[p + nvp + j] = 0x8000 | 3; + } + } + } + + // Just allocate the mesh flags array. The user is resposible to fill it. + mesh.flags = new int[mesh.npolys]; + + if (mesh.nverts > MAX_MESH_VERTS_POLY) { + throw new Exception("rcBuildPolyMesh: The resulting mesh has too many vertices " + mesh.nverts + + " (max " + MAX_MESH_VERTS_POLY + "). Data can be corrupted."); + } + if (mesh.npolys > MAX_MESH_VERTS_POLY) { + throw new Exception("rcBuildPolyMesh: The resulting mesh has too many polygons " + mesh.npolys + + " (max " + MAX_MESH_VERTS_POLY + "). Data can be corrupted."); + } + + ctx.stopTimer("POLYMESH"); + return mesh; + + } + + /// @see rcAllocPolyMesh, rcPolyMesh + public static PolyMesh mergePolyMeshes(Telemetry ctx, PolyMesh[] meshes, int nmeshes) { + + if (nmeshes == 0 || meshes == null) + return null; + + ctx.startTimer("MERGE_POLYMESH"); + PolyMesh mesh = new PolyMesh(); + mesh.nvp = meshes[0].nvp; + mesh.cs = meshes[0].cs; + mesh.ch = meshes[0].ch; + RecastVectors.copy(mesh.bmin, meshes[0].bmin, 0); + RecastVectors.copy(mesh.bmax, meshes[0].bmax, 0); + + int maxVerts = 0; + int maxPolys = 0; + int maxVertsPerMesh = 0; + for (int i = 0; i < nmeshes; ++i) { + RecastVectors.min(mesh.bmin, meshes[i].bmin, 0); + RecastVectors.max(mesh.bmax, meshes[i].bmax, 0); + maxVertsPerMesh = Math.Max(maxVertsPerMesh, meshes[i].nverts); + maxVerts += meshes[i].nverts; + maxPolys += meshes[i].npolys; + } + + mesh.nverts = 0; + mesh.verts = new int[maxVerts * 3]; + + mesh.npolys = 0; + mesh.polys = new int[maxPolys * 2 * mesh.nvp]; + Array.Fill(mesh.polys, RC_MESH_NULL_IDX, 0, (mesh.polys.Length) - (0)); + mesh.regs = new int[maxPolys]; + mesh.areas = new int[maxPolys]; + mesh.flags = new int[maxPolys]; + + int[] nextVert = new int[maxVerts]; + + int[] firstVert = new int[VERTEX_BUCKET_COUNT]; + for (int i = 0; i < VERTEX_BUCKET_COUNT; ++i) + firstVert[i] = -1; + + int[] vremap = new int[maxVertsPerMesh]; + + for (int i = 0; i < nmeshes; ++i) { + PolyMesh pmesh = meshes[i]; + + int ox = (int) Math.Floor((pmesh.bmin[0] - mesh.bmin[0]) / mesh.cs + 0.5f); + int oz = (int) Math.Floor((pmesh.bmin[2] - mesh.bmin[2]) / mesh.cs + 0.5f); + + bool isMinX = (ox == 0); + bool isMinZ = (oz == 0); + bool isMaxX = (Math.Floor((mesh.bmax[0] - pmesh.bmax[0]) / mesh.cs + 0.5f)) == 0; + bool isMaxZ = (Math.Floor((mesh.bmax[2] - pmesh.bmax[2]) / mesh.cs + 0.5f)) == 0; + bool isOnBorder = (isMinX || isMinZ || isMaxX || isMaxZ); + + for (int j = 0; j < pmesh.nverts; ++j) { + int v = j * 3; + int[] inv = addVertex(pmesh.verts[v + 0] + ox, pmesh.verts[v + 1], pmesh.verts[v + 2] + oz, mesh.verts, + firstVert, nextVert, mesh.nverts); + + vremap[j] = inv[0]; + mesh.nverts = inv[1]; + } + + for (int j = 0; j < pmesh.npolys; ++j) { + int tgt = mesh.npolys * 2 * mesh.nvp; + int src = j * 2 * mesh.nvp; + mesh.regs[mesh.npolys] = pmesh.regs[j]; + mesh.areas[mesh.npolys] = pmesh.areas[j]; + mesh.flags[mesh.npolys] = pmesh.flags[j]; + mesh.npolys++; + for (int k = 0; k < mesh.nvp; ++k) { + if (pmesh.polys[src + k] == RC_MESH_NULL_IDX) + break; + mesh.polys[tgt + k] = vremap[pmesh.polys[src + k]]; + } + + if (isOnBorder) { + for (int k = mesh.nvp; k < mesh.nvp * 2; ++k) { + if ((pmesh.polys[src + k] & 0x8000) != 0 && pmesh.polys[src + k] != 0xffff) { + int dir = pmesh.polys[src + k] & 0xf; + switch (dir) { + case 0: // Portal x- + if (isMinX) + mesh.polys[tgt + k] = pmesh.polys[src + k]; + break; + case 1: // Portal z+ + if (isMaxZ) + mesh.polys[tgt + k] = pmesh.polys[src + k]; + break; + case 2: // Portal x+ + if (isMaxX) + mesh.polys[tgt + k] = pmesh.polys[src + k]; + break; + case 3: // Portal z- + if (isMinZ) + mesh.polys[tgt + k] = pmesh.polys[src + k]; + break; + } + } + } + } + } + } + + // Calculate adjacency. + buildMeshAdjacency(mesh.polys, mesh.npolys, mesh.nverts, mesh.nvp); + if (mesh.nverts > MAX_MESH_VERTS_POLY) { + throw new Exception("rcBuildPolyMesh: The resulting mesh has too many vertices " + mesh.nverts + + " (max " + MAX_MESH_VERTS_POLY + "). Data can be corrupted."); + } + if (mesh.npolys > MAX_MESH_VERTS_POLY) { + throw new Exception("rcBuildPolyMesh: The resulting mesh has too many polygons " + mesh.npolys + + " (max " + MAX_MESH_VERTS_POLY + "). Data can be corrupted."); + } + + ctx.stopTimer("MERGE_POLYMESH"); + + return mesh; + } + + public static PolyMesh copyPolyMesh(Telemetry ctx, PolyMesh src) { + PolyMesh dst = new PolyMesh(); + + dst.nverts = src.nverts; + dst.npolys = src.npolys; + dst.maxpolys = src.npolys; + dst.nvp = src.nvp; + RecastVectors.copy(dst.bmin, src.bmin, 0); + RecastVectors.copy(dst.bmax, src.bmax, 0); + dst.cs = src.cs; + dst.ch = src.ch; + dst.borderSize = src.borderSize; + dst.maxEdgeError = src.maxEdgeError; + + dst.verts = new int[src.nverts * 3]; + Array.Copy(src.verts, 0, dst.verts, 0, dst.verts.Length); + dst.polys = new int[src.npolys * 2 * src.nvp]; + Array.Copy(src.polys, 0, dst.polys, 0, dst.polys.Length); + dst.regs = new int[src.npolys]; + Array.Copy(src.regs, 0, dst.regs, 0, dst.regs.Length); + dst.areas = new int[src.npolys]; + Array.Copy(src.areas, 0, dst.areas, 0, dst.areas.Length); + dst.flags = new int[src.npolys]; + Array.Copy(src.flags, 0, dst.flags, 0, dst.flags.Length); + return dst; + } +} diff --git a/src/DotRecast.Recast/RecastMeshDetail.cs b/src/DotRecast.Recast/RecastMeshDetail.cs new file mode 100644 index 0000000..c9ccdd5 --- /dev/null +++ b/src/DotRecast.Recast/RecastMeshDetail.cs @@ -0,0 +1,1334 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4J copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.Collections.Generic; +using DotRecast.Core; + +namespace DotRecast.Recast; + +using static RecastCommon; +using static RecastConstants; + +public class RecastMeshDetail { + + public const int MAX_VERTS = 127; + public const int MAX_TRIS = 255; // Max tris for delaunay is 2n-2-k (n=num verts, k=num hull verts). + public const int MAX_VERTS_PER_EDGE = 32; + + public const int RC_UNSET_HEIGHT = RecastConstants.SPAN_MAX_HEIGHT; + public const int EV_UNDEF = -1; + public const int EV_HULL = -2; + + private class HeightPatch { + public int xmin; + public int ymin; + public int width; + public int height; + public int[] data; + } + + private static float vdot2(float[] a, float[] b) { + return a[0] * b[0] + a[2] * b[2]; + } + + private static float vdistSq2(float[] verts, int p, int q) { + float dx = verts[q + 0] - verts[p + 0]; + float dy = verts[q + 2] - verts[p + 2]; + return dx * dx + dy * dy; + } + + private static float vdist2(float[] verts, int p, int q) { + return (float) Math.Sqrt(vdistSq2(verts, p, q)); + } + + private static float vdistSq2(float[] p, float[] q) { + float dx = q[0] - p[0]; + float dy = q[2] - p[2]; + return dx * dx + dy * dy; + } + + private static float vdist2(float[] p, float[] q) { + return (float) Math.Sqrt(vdistSq2(p, q)); + } + + private static float vdistSq2(float[] p, float[] verts, int q) { + float dx = verts[q + 0] - p[0]; + float dy = verts[q + 2] - p[2]; + return dx * dx + dy * dy; + } + + private static float vdist2(float[] p, float[] verts, int q) { + return (float) Math.Sqrt(vdistSq2(p, verts, q)); + } + + private static float vcross2(float[] verts, int p1, int p2, int p3) { + float u1 = verts[p2 + 0] - verts[p1 + 0]; + float v1 = verts[p2 + 2] - verts[p1 + 2]; + float u2 = verts[p3 + 0] - verts[p1 + 0]; + float v2 = verts[p3 + 2] - verts[p1 + 2]; + return u1 * v2 - v1 * u2; + } + + private static float vcross2(float[] p1, float[] p2, float[] p3) { + float u1 = p2[0] - p1[0]; + float v1 = p2[2] - p1[2]; + float u2 = p3[0] - p1[0]; + float v2 = p3[2] - p1[2]; + return u1 * v2 - v1 * u2; + } + + private static bool circumCircle(float[] verts, int p1, int p2, int p3, float[] c, AtomicFloat r) { + float EPS = 1e-6f; + // Calculate the circle relative to p1, to avoid some precision issues. + float[] v1 = new float[3]; + float[] v2 = new float[3]; + float[] v3 = new float[3]; + RecastVectors.sub(v2, verts, p2, p1); + RecastVectors.sub(v3, verts, p3, p1); + + float cp = vcross2(v1, v2, v3); + if (Math.Abs(cp) > EPS) { + float v1Sq = vdot2(v1, v1); + float v2Sq = vdot2(v2, v2); + float v3Sq = vdot2(v3, v3); + c[0] = (v1Sq * (v2[2] - v3[2]) + v2Sq * (v3[2] - v1[2]) + v3Sq * (v1[2] - v2[2])) / (2 * cp); + c[1] = 0; + c[2] = (v1Sq * (v3[0] - v2[0]) + v2Sq * (v1[0] - v3[0]) + v3Sq * (v2[0] - v1[0])) / (2 * cp); + r.Exchange(vdist2(c, v1)); + RecastVectors.add(c, c, verts, p1); + return true; + } + RecastVectors.copy(c, verts, p1); + r.Exchange(0f); + return false; + } + + private static float distPtTri(float[] p, float[] verts, int a, int b, int c) { + float[] v0 = new float[3]; + float[] v1 = new float[3]; + float[] v2 = new float[3]; + RecastVectors.sub(v0, verts, c, a); + RecastVectors.sub(v1, verts, b, a); + RecastVectors.sub(v2, p, verts, a); + + float dot00 = vdot2(v0, v0); + float dot01 = vdot2(v0, v1); + float dot02 = vdot2(v0, v2); + float dot11 = vdot2(v1, v1); + float dot12 = vdot2(v1, v2); + + // Compute barycentric coordinates + float invDenom = 1.0f / (dot00 * dot11 - dot01 * dot01); + float u = (dot11 * dot02 - dot01 * dot12) * invDenom; + float v = (dot00 * dot12 - dot01 * dot02) * invDenom; + + // If point lies inside the triangle, return interpolated y-coord. + float EPS = 1e-4f; + if (u >= -EPS && v >= -EPS && (u + v) <= 1 + EPS) { + float y = verts[a + 1] + v0[1] * u + v1[1] * v; + return Math.Abs(y - p[1]); + } + return float.MaxValue; + } + + private static float distancePtSeg(float[] verts, int pt, int p, int q) { + float pqx = verts[q + 0] - verts[p + 0]; + float pqy = verts[q + 1] - verts[p + 1]; + float pqz = verts[q + 2] - verts[p + 2]; + float dx = verts[pt + 0] - verts[p + 0]; + float dy = verts[pt + 1] - verts[p + 1]; + float dz = verts[pt + 2] - verts[p + 2]; + float d = pqx * pqx + pqy * pqy + pqz * pqz; + float t = pqx * dx + pqy * dy + pqz * dz; + if (d > 0) { + t /= d; + } + if (t < 0) { + t = 0; + } else if (t > 1) { + t = 1; + } + + dx = verts[p + 0] + t * pqx - verts[pt + 0]; + dy = verts[p + 1] + t * pqy - verts[pt + 1]; + dz = verts[p + 2] + t * pqz - verts[pt + 2]; + + return dx * dx + dy * dy + dz * dz; + } + + private static float distancePtSeg2d(float[] verts, int pt, float[] poly, int p, int q) { + float pqx = poly[q + 0] - poly[p + 0]; + float pqz = poly[q + 2] - poly[p + 2]; + float dx = verts[pt + 0] - poly[p + 0]; + float dz = verts[pt + 2] - poly[p + 2]; + float d = pqx * pqx + pqz * pqz; + float t = pqx * dx + pqz * dz; + if (d > 0) { + t /= d; + } + if (t < 0) { + t = 0; + } else if (t > 1) { + t = 1; + } + + dx = poly[p + 0] + t * pqx - verts[pt + 0]; + dz = poly[p + 2] + t * pqz - verts[pt + 2]; + + return dx * dx + dz * dz; + } + + private static float distToTriMesh(float[] p, float[] verts, int nverts, List tris, int ntris) { + float dmin = float.MaxValue; + for (int i = 0; i < ntris; ++i) { + int va = tris[i * 4 + 0] * 3; + int vb = tris[i * 4 + 1] * 3; + int vc = tris[i * 4 + 2] * 3; + float d = distPtTri(p, verts, va, vb, vc); + if (d < dmin) { + dmin = d; + } + } + if (dmin == float.MaxValue) { + return -1; + } + return dmin; + } + + private static float distToPoly(int nvert, float[] verts, float[] p) { + + float dmin = float.MaxValue; + int i, j; + bool c = false; + for (i = 0, j = nvert - 1; i < nvert; j = i++) { + int vi = i * 3; + int vj = j * 3; + if (((verts[vi + 2] > p[2]) != (verts[vj + 2] > p[2])) && (p[0] < (verts[vj + 0] - verts[vi + 0]) + * (p[2] - verts[vi + 2]) / (verts[vj + 2] - verts[vi + 2]) + verts[vi + 0])) { + c = !c; + } + dmin = Math.Min(dmin, distancePtSeg2d(p, 0, verts, vj, vi)); + } + return c ? -dmin : dmin; + } + + private static int getHeight(float fx, float fy, float fz, float cs, float ics, float ch, int radius, + HeightPatch hp) { + int ix = (int) Math.Floor(fx * ics + 0.01f); + int iz = (int) Math.Floor(fz * ics + 0.01f); + ix = RecastCommon.clamp(ix - hp.xmin, 0, hp.width - 1); + iz = RecastCommon.clamp(iz - hp.ymin, 0, hp.height - 1); + int h = hp.data[ix + iz * hp.width]; + if (h == RC_UNSET_HEIGHT) { + // Special case when data might be bad. + // Walk adjacent cells in a spiral up to 'radius', and look + // for a pixel which has a valid height. + int x = 1, z = 0, dx = 1, dz = 0; + int maxSize = radius * 2 + 1; + int maxIter = maxSize * maxSize - 1; + + int nextRingIterStart = 8; + int nextRingIters = 16; + + float dmin = float.MaxValue; + for (int i = 0; i < maxIter; ++i) { + int nx = ix + x; + int nz = iz + z; + + if (nx >= 0 && nz >= 0 && nx < hp.width && nz < hp.height) { + int nh = hp.data[nx + nz * hp.width]; + if (nh != RC_UNSET_HEIGHT) { + float d = Math.Abs(nh * ch - fy); + if (d < dmin) { + h = nh; + dmin = d; + } + } + } + + // We are searching in a grid which looks approximately like this: + // __________ + // |2 ______ 2| + // | |1 __ 1| | + // | | |__| | | + // | |______| | + // |__________| + // We want to find the best height as close to the center cell as possible. This means that + // if we find a height in one of the neighbor cells to the center, we don't want to + // expand further out than the 8 neighbors - we want to limit our search to the closest + // of these "rings", but the best height in the ring. + // For example, the center is just 1 cell. We checked that at the entrance to the function. + // The next "ring" contains 8 cells (marked 1 above). Those are all the neighbors to the center cell. + // The next one again contains 16 cells (marked 2). In general each ring has 8 additional cells, which + // can be thought of as adding 2 cells around the "center" of each side when we expand the ring. + // Here we detect if we are about to enter the next ring, and if we are and we have found + // a height, we abort the search. + if (i + 1 == nextRingIterStart) { + if (h != RC_UNSET_HEIGHT) { + break; + } + + nextRingIterStart += nextRingIters; + nextRingIters += 8; + } + + if ((x == z) || ((x < 0) && (x == -z)) || ((x > 0) && (x == 1 - z))) { + int tmp = dx; + dx = -dz; + dz = tmp; + } + x += dx; + z += dz; + } + } + return h; + } + + private static int findEdge(List edges, int s, int t) { + for (int i = 0; i < edges.Count / 4; i++) { + int e = i * 4; + if ((edges[e + 0] == s && edges[e + 1] == t) || (edges[e + 0] == t && edges[e + 1] == s)) { + return i; + } + } + return EV_UNDEF; + } + + private static void addEdge(Telemetry ctx, List edges, int maxEdges, int s, int t, int l, int r) { + if (edges.Count / 4 >= maxEdges) { + throw new Exception("addEdge: Too many edges (" + edges.Count / 4 + "/" + maxEdges + ")."); + } + + // Add edge if not already in the triangulation. + int e = findEdge(edges, s, t); + if (e == EV_UNDEF) { + edges.Add(s); + edges.Add(t); + edges.Add(l); + edges.Add(r); + } + } + + private static void updateLeftFace(List edges, int e, int s, int t, int f) { + if (edges[e + 0] == s && edges[e + 1] == t && edges[e + 2] == EV_UNDEF) + { + edges[e + 2] = f; + } else if (edges[e + 1] == s && edges[e + 0] == t && edges[e + 3] == EV_UNDEF) + { + edges[e + 3] = f; + } + } + + private static bool overlapSegSeg2d(float[] verts, int a, int b, int c, int d) { + float a1 = vcross2(verts, a, b, d); + float a2 = vcross2(verts, a, b, c); + if (a1 * a2 < 0.0f) { + float a3 = vcross2(verts, c, d, a); + float a4 = a3 + a2 - a1; + if (a3 * a4 < 0.0f) { + return true; + } + } + return false; + } + + private static bool overlapEdges(float[] pts, List edges, int s1, int t1) { + for (int i = 0; i < edges.Count / 4; ++i) { + int s0 = edges[i * 4 + 0]; + int t0 = edges[i * 4 + 1]; + // Same or connected edges do not overlap. + if (s0 == s1 || s0 == t1 || t0 == s1 || t0 == t1) { + continue; + } + if (overlapSegSeg2d(pts, s0 * 3, t0 * 3, s1 * 3, t1 * 3)) { + return true; + } + } + return false; + } + + static int completeFacet(Telemetry ctx, float[] pts, int npts, List edges, int maxEdges, int nfaces, int e) { + float EPS = 1e-5f; + + int edge = e * 4; + + // Cache s and t. + int s, t; + if (edges[edge + 2] == EV_UNDEF) { + s = edges[edge + 0]; + t = edges[edge + 1]; + } else if (edges[edge + 3] == EV_UNDEF) { + s = edges[edge + 1]; + t = edges[edge + 0]; + } else { + // Edge already completed. + return nfaces; + } + + // Find best point on left of edge. + int pt = npts; + float[] c = new float[3]; + AtomicFloat r = new AtomicFloat(-1f); + for (int u = 0; u < npts; ++u) { + if (u == s || u == t) { + continue; + } + if (vcross2(pts, s * 3, t * 3, u * 3) > EPS) { + if (r.Get() < 0) { + // The circle is not updated yet, do it now. + pt = u; + circumCircle(pts, s * 3, t * 3, u * 3, c, r); + continue; + } + float d = vdist2(c, pts, u * 3); + float tol = 0.001f; + if (d > r.Get() * (1 + tol)) { + // Outside current circumcircle, skip. + continue; + } else if (d < r.Get() * (1 - tol)) { + // Inside safe circumcircle, update circle. + pt = u; + circumCircle(pts, s * 3, t * 3, u * 3, c, r); + } else { + // Inside epsilon circum circle, do extra tests to make sure the edge is valid. + // s-u and t-u cannot overlap with s-pt nor t-pt if they exists. + if (overlapEdges(pts, edges, s, u)) { + continue; + } + if (overlapEdges(pts, edges, t, u)) { + continue; + } + // Edge is valid. + pt = u; + circumCircle(pts, s * 3, t * 3, u * 3, c, r); + } + } + } + + // Add new triangle or update edge info if s-t is on hull. + if (pt < npts) { + // Update face information of edge being completed. + updateLeftFace(edges, e * 4, s, t, nfaces); + + // Add new edge or update face info of old edge. + e = findEdge(edges, pt, s); + if (e == EV_UNDEF) { + addEdge(ctx, edges, maxEdges, pt, s, nfaces, EV_UNDEF); + } else { + updateLeftFace(edges, e * 4, pt, s, nfaces); + } + + // Add new edge or update face info of old edge. + e = findEdge(edges, t, pt); + if (e == EV_UNDEF) { + addEdge(ctx, edges, maxEdges, t, pt, nfaces, EV_UNDEF); + } else { + updateLeftFace(edges, e * 4, t, pt, nfaces); + } + + nfaces++; + } else { + updateLeftFace(edges, e * 4, s, t, EV_HULL); + } + return nfaces; + } + + private static void delaunayHull(Telemetry ctx, int npts, float[] pts, int nhull, int[] hull, List tris) { + int nfaces = 0; + int maxEdges = npts * 10; + List edges = new(64); + for (int i = 0, j = nhull - 1; i < nhull; j = i++) { + addEdge(ctx, edges, maxEdges, hull[j], hull[i], EV_HULL, EV_UNDEF); + } + int currentEdge = 0; + while (currentEdge < edges.Count / 4) { + if (edges[currentEdge * 4 + 2] == EV_UNDEF) { + nfaces = completeFacet(ctx, pts, npts, edges, maxEdges, nfaces, currentEdge); + } + if (edges[currentEdge * 4 + 3] == EV_UNDEF) { + nfaces = completeFacet(ctx, pts, npts, edges, maxEdges, nfaces, currentEdge); + } + currentEdge++; + } + // Create tris + tris.Clear(); + for (int i = 0; i < nfaces * 4; ++i) { + tris.Add(-1); + } + + for (int i = 0; i < edges.Count / 4; ++i) { + int e = i * 4; + if (edges[e + 3] >= 0) { + // Left face + int t = edges[e + 3] * 4; + if (tris[t + 0] == -1) { + tris[t + 0] = edges[e + 0]; + tris[t + 1] = edges[e + 1]; + } else if (tris[t + 0] == edges[e + 1]) + { + tris[t + 2] = edges[e + 0]; + } else if (tris[t + 1] == edges[e + 0]) + { + tris[t + 2] = edges[e + 1]; + } + } + if (edges[e + 2] >= 0) { + // Right + int t = edges[e + 2] * 4; + if (tris[t + 0] == -1) { + tris[t + 0] =edges[e + 1]; + tris[t + 1] =edges[e + 0]; + } else if (tris[t + 0] == edges[e + 0]) + { + tris[t + 2] = edges[e + 1]; + } else if (tris[t + 1] == edges[e + 1]) + { + tris[t + 2] = edges[e + 0]; + } + } + } + + for (int i = 0; i < tris.Count / 4; ++i) { + int t = i * 4; + if (tris[t + 0] == -1 || tris[t + 1] == -1 || tris[t + 2] == -1) { + Console.Error.WriteLine("Dangling! " + tris[t] + " " + tris[t + 1] + " " + tris[t + 2]); + // ctx.log(RC_LOG_WARNING, "delaunayHull: Removing dangling face %d [%d,%d,%d].", i, t[0],t[1],t[2]); + tris[t + 0] = tris[tris.Count - 4]; + tris[t + 1] = tris[tris.Count - 3]; + tris[t + 2] = tris[tris.Count - 2]; + tris[t + 3] = tris[tris.Count - 1]; + tris.RemoveAt(tris.Count - 1); + tris.RemoveAt(tris.Count - 1); + tris.RemoveAt(tris.Count - 1); + tris.RemoveAt(tris.Count - 1); + --i; + } + } + } + + // Calculate minimum extend of the polygon. + private static float polyMinExtent(float[] verts, int nverts) { + float minDist = float.MaxValue; + for (int i = 0; i < nverts; i++) { + int ni = (i + 1) % nverts; + int p1 = i * 3; + int p2 = ni * 3; + float maxEdgeDist = 0; + for (int j = 0; j < nverts; j++) { + if (j == i || j == ni) { + continue; + } + float d = distancePtSeg2d(verts, j * 3, verts, p1, p2); + maxEdgeDist = Math.Max(maxEdgeDist, d); + } + minDist = Math.Min(minDist, maxEdgeDist); + } + return (float) Math.Sqrt(minDist); + } + + private static void triangulateHull(int nverts, float[] verts, int nhull, int[] hull, int nin, List tris) { + int start = 0, left = 1, right = nhull - 1; + + // Start from an ear with shortest perimeter. + // This tends to favor well formed triangles as starting point. + float dmin = float.MaxValue; + for (int i = 0; i < nhull; i++) { + if (hull[i] >= nin) { + continue; // Ears are triangles with original vertices as middle vertex while others are actually line + } + // segments on edges + int pi = RecastMesh.prev(i, nhull); + int ni = RecastMesh.next(i, nhull); + int pv = hull[pi] * 3; + int cv = hull[i] * 3; + int nv = hull[ni] * 3; + float d = vdist2(verts, pv, cv) + vdist2(verts, cv, nv) + vdist2(verts, nv, pv); + if (d < dmin) { + start = i; + left = ni; + right = pi; + dmin = d; + } + } + + // Add first triangle + tris.Add(hull[start]); + tris.Add(hull[left]); + tris.Add(hull[right]); + tris.Add(0); + + // Triangulate the polygon by moving left or right, + // depending on which triangle has shorter perimeter. + // This heuristic was chose emprically, since it seems + // handle tesselated straight edges well. + while (RecastMesh.next(left, nhull) != right) { + // Check to see if se should advance left or right. + int nleft = RecastMesh.next(left, nhull); + int nright = RecastMesh.prev(right, nhull); + + int cvleft = hull[left] * 3; + int nvleft = hull[nleft] * 3; + int cvright = hull[right] * 3; + int nvright = hull[nright] * 3; + float dleft = vdist2(verts, cvleft, nvleft) + vdist2(verts, nvleft, cvright); + float dright = vdist2(verts, cvright, nvright) + vdist2(verts, cvleft, nvright); + + if (dleft < dright) { + tris.Add(hull[left]); + tris.Add(hull[nleft]); + tris.Add(hull[right]); + tris.Add(0); + left = nleft; + } else { + tris.Add(hull[left]); + tris.Add(hull[nright]); + tris.Add(hull[right]); + tris.Add(0); + right = nright; + } + } + } + + private static float getJitterX(int i) { + return (((i * 0x8da6b343) & 0xffff) / 65535.0f * 2.0f) - 1.0f; + } + + private static float getJitterY(int i) { + return (((i * 0xd8163841) & 0xffff) / 65535.0f * 2.0f) - 1.0f; + } + + static int buildPolyDetail(Telemetry ctx, float[] @in, int nin, float sampleDist, float sampleMaxError, + int heightSearchRadius, CompactHeightfield chf, HeightPatch hp, float[] verts, List tris) { + + List samples = new(512); + + int nverts = 0; + float[] edge = new float[(MAX_VERTS_PER_EDGE + 1) * 3]; + int[] hull = new int[MAX_VERTS]; + int nhull = 0; + + nverts = nin; + + for (int i = 0; i < nin; ++i) { + RecastVectors.copy(verts, i * 3, @in, i * 3); + } + tris.Clear(); + + float cs = chf.cs; + float ics = 1.0f / cs; + + // Calculate minimum extents of the polygon based on input data. + float minExtent = polyMinExtent(verts, nverts); + + // Tessellate outlines. + // This is done in separate pass in order to ensure + // seamless height values across the ply boundaries. + if (sampleDist > 0) { + for (int i = 0, j = nin - 1; i < nin; j = i++) { + int vj = j * 3; + int vi = i * 3; + bool swapped = false; + // Make sure the segments are always handled in same order + // using lexological sort or else there will be seams. + if (Math.Abs(@in[vj + 0] - @in[vi + 0]) < 1e-6f) { + if (@in[vj + 2] > @in[vi + 2]) { + int temp = vi; + vi = vj; + vj = temp; + swapped = true; + } + } else { + if (@in[vj + 0] > @in[vi + 0]) { + int temp = vi; + vi = vj; + vj = temp; + swapped = true; + } + } + // Create samples along the edge. + float dx = @in[vi + 0] - @in[vj + 0]; + float dy = @in[vi + 1] - @in[vj + 1]; + float dz = @in[vi + 2] - @in[vj + 2]; + float d = (float) Math.Sqrt(dx * dx + dz * dz); + int nn = 1 + (int) Math.Floor(d / sampleDist); + if (nn >= MAX_VERTS_PER_EDGE) { + nn = MAX_VERTS_PER_EDGE - 1; + } + if (nverts + nn >= MAX_VERTS) { + nn = MAX_VERTS - 1 - nverts; + } + + for (int k = 0; k <= nn; ++k) { + float u = (float) k / (float) nn; + int pos = k * 3; + edge[pos + 0] = @in[vj + 0] + dx * u; + edge[pos + 1] = @in[vj + 1] + dy * u; + edge[pos + 2] = @in[vj + 2] + dz * u; + edge[pos + 1] = getHeight(edge[pos + 0], edge[pos + 1], edge[pos + 2], cs, ics, chf.ch, + heightSearchRadius, hp) * chf.ch; + } + // Simplify samples. + int[] idx = new int[MAX_VERTS_PER_EDGE]; + idx[0] = 0; + idx[1] = nn; + int nidx = 2; + for (int k = 0; k < nidx - 1;) { + int a = idx[k]; + int b = idx[k + 1]; + int va = a * 3; + int vb = b * 3; + // Find maximum deviation along the segment. + float maxd = 0; + int maxi = -1; + for (int m = a + 1; m < b; ++m) { + float dev = distancePtSeg(edge, m * 3, va, vb); + if (dev > maxd) { + maxd = dev; + maxi = m; + } + } + // If the max deviation is larger than accepted error, + // add new point, else continue to next segment. + if (maxi != -1 && maxd > sampleMaxError * sampleMaxError) { + for (int m = nidx; m > k; --m) { + idx[m] = idx[m - 1]; + } + idx[k + 1] = maxi; + nidx++; + } else { + ++k; + } + } + + hull[nhull++] = j; + // Add new vertices. + if (swapped) { + for (int k = nidx - 2; k > 0; --k) { + RecastVectors.copy(verts, nverts * 3, edge, idx[k] * 3); + hull[nhull++] = nverts; + nverts++; + } + } else { + for (int k = 1; k < nidx - 1; ++k) { + RecastVectors.copy(verts, nverts * 3, edge, idx[k] * 3); + hull[nhull++] = nverts; + nverts++; + } + } + } + } + + // If the polygon minimum extent is small (sliver or small triangle), do not try to add internal points. + if (minExtent < sampleDist * 2) { + triangulateHull(nverts, verts, nhull, hull, nin, tris); + return nverts; + } + + // Tessellate the base mesh. + // We're using the triangulateHull instead of delaunayHull as it tends to + // create a bit better triangulation for long thin triangles when there + // are no internal points. + triangulateHull(nverts, verts, nhull, hull, nin, tris); + + if (tris.Count == 0) { + // Could not triangulate the poly, make sure there is some valid data there. + throw new Exception("buildPolyDetail: Could not triangulate polygon (" + nverts + ") verts)."); + } + + if (sampleDist > 0) { + // Create sample locations in a grid. + float[] bmin = new float[3]; + float[] bmax = new float[3]; + RecastVectors.copy(bmin, @in, 0); + RecastVectors.copy(bmax, @in, 0); + for (int i = 1; i < nin; ++i) { + RecastVectors.min(bmin, @in, i * 3); + RecastVectors.max(bmax, @in, i * 3); + } + int x0 = (int) Math.Floor(bmin[0] / sampleDist); + int x1 = (int) Math.Ceiling(bmax[0] / sampleDist); + int z0 = (int) Math.Floor(bmin[2] / sampleDist); + int z1 = (int) Math.Ceiling(bmax[2] / sampleDist); + samples.Clear(); + for (int z = z0; z < z1; ++z) { + for (int x = x0; x < x1; ++x) { + float[] pt = new float[3]; + pt[0] = x * sampleDist; + pt[1] = (bmax[1] + bmin[1]) * 0.5f; + pt[2] = z * sampleDist; + // Make sure the samples are not too close to the edges. + if (distToPoly(nin, @in, pt) > -sampleDist / 2) { + continue; + } + samples.Add(x); + samples.Add(getHeight(pt[0], pt[1], pt[2], cs, ics, chf.ch, heightSearchRadius, hp)); + samples.Add(z); + samples.Add(0); // Not added + } + } + + // Add the samples starting from the one that has the most + // error. The procedure stops when all samples are added + // or when the max error is within treshold. + int nsamples = samples.Count / 4; + for (int iter = 0; iter < nsamples; ++iter) { + if (nverts >= MAX_VERTS) { + break; + } + + // Find sample with most error. + float[] bestpt = new float[3]; + float bestd = 0; + int besti = -1; + for (int i = 0; i < nsamples; ++i) { + int s = i * 4; + if (samples[s + 3] != 0) { + continue; // skip added. + } + float[] pt = new float[3]; + // The sample location is jittered to get rid of some bad triangulations + // which are cause by symmetrical data from the grid structure. + pt[0] = samples[s + 0] * sampleDist + getJitterX(i) * cs * 0.1f; + pt[1] = samples[s + 1] * chf.ch; + pt[2] = samples[s + 2] * sampleDist + getJitterY(i) * cs * 0.1f; + float d = distToTriMesh(pt, verts, nverts, tris, tris.Count / 4); + if (d < 0) { + continue; // did not hit the mesh. + } + if (d > bestd) { + bestd = d; + besti = i; + bestpt = pt; + } + } + // If the max error is within accepted threshold, stop tesselating. + if (bestd <= sampleMaxError || besti == -1) { + break; + } + // Mark sample as added. + samples[besti * 4 + 3] = 1; + // Add the new sample point. + RecastVectors.copy(verts, nverts * 3, bestpt, 0); + nverts++; + + // Create new triangulation. + // TODO: Incremental add instead of full rebuild. + delaunayHull(ctx, nverts, verts, nhull, hull, tris); + } + } + + int ntris = tris.Count / 4; + if (ntris > MAX_TRIS) { + List subList = tris.GetRange(0, MAX_TRIS * 4); + tris.Clear(); + tris.AddRange(subList); + throw new Exception( + "rcBuildPolyMeshDetail: Shrinking triangle count from " + ntris + " to max " + MAX_TRIS); + } + return nverts; + } + + static void seedArrayWithPolyCenter(Telemetry ctx, CompactHeightfield chf, int[] meshpoly, int poly, int npoly, + int[] verts, int bs, HeightPatch hp, List array) { + // Note: Reads to the compact heightfield are offset by border size (bs) + // since border size offset is already removed from the polymesh vertices. + + int[] offset = { 0, 0, -1, -1, 0, -1, 1, -1, 1, 0, 1, 1, 0, 1, -1, 1, -1, 0, }; + + // Find cell closest to a poly vertex + int startCellX = 0, startCellY = 0, startSpanIndex = -1; + int dmin = RC_UNSET_HEIGHT; + for (int j = 0; j < npoly && dmin > 0; ++j) { + for (int k = 0; k < 9 && dmin > 0; ++k) { + int ax = verts[meshpoly[poly + j] * 3 + 0] + offset[k * 2 + 0]; + int ay = verts[meshpoly[poly + j] * 3 + 1]; + int az = verts[meshpoly[poly + j] * 3 + 2] + offset[k * 2 + 1]; + if (ax < hp.xmin || ax >= hp.xmin + hp.width || az < hp.ymin || az >= hp.ymin + hp.height) { + continue; + } + + CompactCell c = chf.cells[(ax + bs) + (az + bs) * chf.width]; + for (int i = c.index, ni = c.index + c.count; i < ni && dmin > 0; ++i) { + CompactSpan s = chf.spans[i]; + int d = Math.Abs(ay - s.y); + if (d < dmin) { + startCellX = ax; + startCellY = az; + startSpanIndex = i; + dmin = d; + } + } + } + } + + // Find center of the polygon + int pcx = 0, pcy = 0; + for (int j = 0; j < npoly; ++j) { + pcx += verts[meshpoly[poly + j] * 3 + 0]; + pcy += verts[meshpoly[poly + j] * 3 + 2]; + } + pcx /= npoly; + pcy /= npoly; + + array.Clear(); + array.Add(startCellX); + array.Add(startCellY); + array.Add(startSpanIndex); + int[] dirs = { 0, 1, 2, 3 }; + Array.Fill(hp.data, 0, 0, (hp.width * hp.height) - (0)); + // DFS to move to the center. Note that we need a DFS here and can not just move + // directly towards the center without recording intermediate nodes, even though the polygons + // are convex. In very rare we can get stuck due to contour simplification if we do not + // record nodes. + int cx = -1, cy = -1, ci = -1; + while (true) { + if (array.Count < 3) { + ctx.warn("Walk towards polygon center failed to reach center"); + break; + } + ci = array[array.Count - 1]; + array.RemoveAt(array.Count - 1); + + cy = array[array.Count - 1]; + array.RemoveAt(array.Count - 1); + + cx = array[array.Count - 1]; + array.RemoveAt(array.Count - 1); + + // Check if close to center of the polygon. + if (cx == pcx && cy == pcy) { + break; + } + // If we are already at the correct X-position, prefer direction + // directly towards the center in the Y-axis; otherwise prefer + // direction in the X-axis + int directDir; + if (cx == pcx) { + directDir = rcGetDirForOffset(0, pcy > cy ? 1 : -1); + } else { + directDir = rcGetDirForOffset(pcx > cx ? 1 : -1, 0); + } + + // Push the direct dir last so we start with this on next iteration + int tmp = dirs[3]; + dirs[3] = dirs[directDir]; + dirs[directDir] = tmp; + + CompactSpan cs = chf.spans[ci]; + + for (int i = 0; i < 4; ++i) { + int dir = dirs[i]; + if (GetCon(cs, dir) == RC_NOT_CONNECTED) { + continue; + } + + int newX = cx + GetDirOffsetX(dir); + int newY = cy + GetDirOffsetY(dir); + + int hpx = newX - hp.xmin; + int hpy = newY - hp.ymin; + if (hpx < 0 || hpx >= hp.width || hpy < 0 || hpy >= hp.height) { + continue; + } + if (hp.data[hpx + hpy * hp.width] != 0) { + continue; + } + + hp.data[hpx + hpy * hp.width] = 1; + + array.Add(newX); + array.Add(newY); + array.Add(chf.cells[(newX + bs) + (newY + bs) * chf.width].index + GetCon(cs, dir)); + } + + tmp = dirs[3]; + dirs[3] = dirs[directDir]; + dirs[directDir] = tmp; + + } + + array.Clear(); + // getHeightData seeds are given in coordinates with borders + array.Add(cx + bs); + array.Add(cy + bs); + array.Add(ci); + Array.Fill(hp.data, RC_UNSET_HEIGHT, 0, (hp.width * hp.height) - (0)); + CompactSpan cs2 = chf.spans[ci]; + hp.data[cx - hp.xmin + (cy - hp.ymin) * hp.width] = cs2.y; + } + + const int RETRACT_SIZE = 256; + + static void push3(List queue, int v1, int v2, int v3) { + queue.Add(v1); + queue.Add(v2); + queue.Add(v3); + } + + static void getHeightData(Telemetry ctx, CompactHeightfield chf, int[] meshpolys, int poly, int npoly, int[] verts, + int bs, HeightPatch hp, int region) { + // Note: Reads to the compact heightfield are offset by border size (bs) + // since border size offset is already removed from the polymesh vertices. + + List queue = new(512); + Array.Fill(hp.data, RC_UNSET_HEIGHT, 0, (hp.width * hp.height) - (0)); + + bool empty = true; + + // We cannot sample from this poly if it was created from polys + // of different regions. If it was then it could potentially be overlapping + // with polys of that region and the heights sampled here could be wrong. + if (region != RC_MULTIPLE_REGS) { + // Copy the height from the same region, and mark region borders + // as seed points to fill the rest. + for (int hy = 0; hy < hp.height; hy++) { + int y = hp.ymin + hy + bs; + for (int hx = 0; hx < hp.width; hx++) { + int x = hp.xmin + hx + bs; + CompactCell c = chf.cells[x + y * chf.width]; + for (int i = c.index, ni = c.index + c.count; i < ni; ++i) { + CompactSpan s = chf.spans[i]; + if (s.reg == region) { + // Store height + hp.data[hx + hy * hp.width] = s.y; + empty = false; + // If any of the neighbours is not in same region, + // add the current location as flood fill start + bool border = false; + for (int dir = 0; dir < 4; ++dir) { + if (GetCon(s, dir) != RC_NOT_CONNECTED) { + int ax = x + GetDirOffsetX(dir); + int ay = y + GetDirOffsetY(dir); + int ai = chf.cells[ax + ay * chf.width].index + GetCon(s, dir); + CompactSpan @as = chf.spans[ai]; + if (@as.reg != region) { + border = true; + break; + } + } + } + if (border) { + push3(queue, x, y, i); + } + break; + } + } + } + } + } + + // if the polygon does not contain any points from the current region (rare, but happens) + // or if it could potentially be overlapping polygons of the same region, + // then use the center as the seed point. + if (empty) { + seedArrayWithPolyCenter(ctx, chf, meshpolys, poly, npoly, verts, bs, hp, queue); + } + + int head = 0; + + // We assume the seed is centered in the polygon, so a BFS to collect + // height data will ensure we do not move onto overlapping polygons and + // sample wrong heights. + while (head * 3 < queue.Count) { + int cx = queue[head * 3 + 0]; + int cy = queue[head * 3 + 1]; + int ci = queue[head * 3 + 2]; + head++; + if (head >= RETRACT_SIZE) { + head = 0; + queue = queue.GetRange(RETRACT_SIZE * 3, queue.Count - (RETRACT_SIZE * 3)); + } + + CompactSpan cs = chf.spans[ci]; + for (int dir = 0; dir < 4; ++dir) { + if (GetCon(cs, dir) == RC_NOT_CONNECTED) { + continue; + } + + int ax = cx + GetDirOffsetX(dir); + int ay = cy + GetDirOffsetY(dir); + int hx = ax - hp.xmin - bs; + int hy = ay - hp.ymin - bs; + + if (hx < 0 || hx >= hp.width || hy < 0 || hy >= hp.height) { + continue; + } + + if (hp.data[hx + hy * hp.width] != RC_UNSET_HEIGHT) { + continue; + } + + int ai = chf.cells[ax + ay * chf.width].index + GetCon(cs, dir); + CompactSpan @as = chf.spans[ai]; + + hp.data[hx + hy * hp.width] = @as.y; + push3(queue, ax, ay, ai); + } + } + } + + static int getEdgeFlags(float[] verts, int va, int vb, float[] vpoly, int npoly) { + // The flag returned by this function matches getDetailTriEdgeFlags in Detour. + // Figure out if edge (va,vb) is part of the polygon boundary. + float thrSqr = 0.001f * 0.001f; + for (int i = 0, j = npoly - 1; i < npoly; j = i++) { + if (distancePtSeg2d(verts, va, vpoly, j * 3, i * 3) < thrSqr + && distancePtSeg2d(verts, vb, vpoly, j * 3, i * 3) < thrSqr) { + return 1; + } + } + return 0; + } + + static int getTriFlags(float[] verts, int va, int vb, int vc, float[] vpoly, int npoly) { + int flags = 0; + flags |= getEdgeFlags(verts, va, vb, vpoly, npoly) << 0; + flags |= getEdgeFlags(verts, vb, vc, vpoly, npoly) << 2; + flags |= getEdgeFlags(verts, vc, va, vpoly, npoly) << 4; + return flags; + } + + /// @par + /// + /// See the #rcConfig documentation for more information on the configuration parameters. + /// + /// @see rcAllocPolyMeshDetail, rcPolyMesh, rcCompactHeightfield, rcPolyMeshDetail, rcConfig + public static PolyMeshDetail buildPolyMeshDetail(Telemetry ctx, PolyMesh mesh, CompactHeightfield chf, + float sampleDist, float sampleMaxError) { + + ctx.startTimer("POLYMESHDETAIL"); + if (mesh.nverts == 0 || mesh.npolys == 0) { + return null; + } + + PolyMeshDetail dmesh = new PolyMeshDetail(); + int nvp = mesh.nvp; + float cs = mesh.cs; + float ch = mesh.ch; + float[] orig = mesh.bmin; + int borderSize = mesh.borderSize; + int heightSearchRadius = (int) Math.Max(1, Math.Ceiling(mesh.maxEdgeError)); + + List tris = new(512); + float[] verts = new float[256 * 3]; + HeightPatch hp = new HeightPatch(); + int nPolyVerts = 0; + int maxhw = 0, maxhh = 0; + + int[] bounds = new int[mesh.npolys * 4]; + float[] poly = new float[nvp * 3]; + + // Find max size for a polygon area. + for (int i = 0; i < mesh.npolys; ++i) { + int p = i * nvp * 2; + bounds[i * 4 + 0] = chf.width; + bounds[i * 4 + 1] = 0; + bounds[i * 4 + 2] = chf.height; + bounds[i * 4 + 3] = 0; + for (int j = 0; j < nvp; ++j) { + if (mesh.polys[p + j] == RC_MESH_NULL_IDX) { + break; + } + int v = mesh.polys[p + j] * 3; + bounds[i * 4 + 0] = Math.Min(bounds[i * 4 + 0], mesh.verts[v + 0]); + bounds[i * 4 + 1] = Math.Max(bounds[i * 4 + 1], mesh.verts[v + 0]); + bounds[i * 4 + 2] = Math.Min(bounds[i * 4 + 2], mesh.verts[v + 2]); + bounds[i * 4 + 3] = Math.Max(bounds[i * 4 + 3], mesh.verts[v + 2]); + nPolyVerts++; + } + bounds[i * 4 + 0] = Math.Max(0, bounds[i * 4 + 0] - 1); + bounds[i * 4 + 1] = Math.Min(chf.width, bounds[i * 4 + 1] + 1); + bounds[i * 4 + 2] = Math.Max(0, bounds[i * 4 + 2] - 1); + bounds[i * 4 + 3] = Math.Min(chf.height, bounds[i * 4 + 3] + 1); + if (bounds[i * 4 + 0] >= bounds[i * 4 + 1] || bounds[i * 4 + 2] >= bounds[i * 4 + 3]) { + continue; + } + maxhw = Math.Max(maxhw, bounds[i * 4 + 1] - bounds[i * 4 + 0]); + maxhh = Math.Max(maxhh, bounds[i * 4 + 3] - bounds[i * 4 + 2]); + } + hp.data = new int[maxhw * maxhh]; + + dmesh.nmeshes = mesh.npolys; + dmesh.nverts = 0; + dmesh.ntris = 0; + dmesh.meshes = new int[dmesh.nmeshes * 4]; + + int vcap = nPolyVerts + nPolyVerts / 2; + int tcap = vcap * 2; + + dmesh.nverts = 0; + dmesh.verts = new float[vcap * 3]; + dmesh.ntris = 0; + dmesh.tris = new int[tcap * 4]; + + for (int i = 0; i < mesh.npolys; ++i) { + int p = i * nvp * 2; + + // Store polygon vertices for processing. + int npoly = 0; + for (int j = 0; j < nvp; ++j) { + if (mesh.polys[p + j] == RC_MESH_NULL_IDX) { + break; + } + int v = mesh.polys[p + j] * 3; + poly[j * 3 + 0] = mesh.verts[v + 0] * cs; + poly[j * 3 + 1] = mesh.verts[v + 1] * ch; + poly[j * 3 + 2] = mesh.verts[v + 2] * cs; + npoly++; + } + + // Get the height data from the area of the polygon. + hp.xmin = bounds[i * 4 + 0]; + hp.ymin = bounds[i * 4 + 2]; + hp.width = bounds[i * 4 + 1] - bounds[i * 4 + 0]; + hp.height = bounds[i * 4 + 3] - bounds[i * 4 + 2]; + getHeightData(ctx, chf, mesh.polys, p, npoly, mesh.verts, borderSize, hp, mesh.regs[i]); + + // Build detail mesh. + int nverts = buildPolyDetail(ctx, poly, npoly, sampleDist, sampleMaxError, heightSearchRadius, chf, hp, + verts, tris); + + // Move detail verts to world space. + for (int j = 0; j < nverts; ++j) { + verts[j * 3 + 0] += orig[0]; + verts[j * 3 + 1] += orig[1] + chf.ch; // Is this offset necessary? See + // https://groups.google.com/d/msg/recastnavigation/UQFN6BGCcV0/-1Ny4koOBpkJ + verts[j * 3 + 2] += orig[2]; + } + // Offset poly too, will be used to flag checking. + for (int j = 0; j < npoly; ++j) { + poly[j * 3 + 0] += orig[0]; + poly[j * 3 + 1] += orig[1]; + poly[j * 3 + 2] += orig[2]; + } + + // Store detail submesh. + int ntris = tris.Count / 4; + + dmesh.meshes[i * 4 + 0] = dmesh.nverts; + dmesh.meshes[i * 4 + 1] = nverts; + dmesh.meshes[i * 4 + 2] = dmesh.ntris; + dmesh.meshes[i * 4 + 3] = ntris; + + // Store vertices, allocate more memory if necessary. + if (dmesh.nverts + nverts > vcap) { + while (dmesh.nverts + nverts > vcap) { + vcap += 256; + } + + float[] newv = new float[vcap * 3]; + if (dmesh.nverts != 0) { + Array.Copy(dmesh.verts, 0, newv, 0, 3 * dmesh.nverts); + } + dmesh.verts = newv; + } + for (int j = 0; j < nverts; ++j) { + dmesh.verts[dmesh.nverts * 3 + 0] = verts[j * 3 + 0]; + dmesh.verts[dmesh.nverts * 3 + 1] = verts[j * 3 + 1]; + dmesh.verts[dmesh.nverts * 3 + 2] = verts[j * 3 + 2]; + dmesh.nverts++; + } + + // Store triangles, allocate more memory if necessary. + if (dmesh.ntris + ntris > tcap) { + while (dmesh.ntris + ntris > tcap) { + tcap += 256; + } + int[] newt = new int[tcap * 4]; + if (dmesh.ntris != 0) { + Array.Copy(dmesh.tris, 0, newt, 0, 4 * dmesh.ntris); + } + dmesh.tris = newt; + } + for (int j = 0; j < ntris; ++j) { + int t = j * 4; + dmesh.tris[dmesh.ntris * 4 + 0] = tris[t + 0]; + dmesh.tris[dmesh.ntris * 4 + 1] = tris[t + 1]; + dmesh.tris[dmesh.ntris * 4 + 2] = tris[t + 2]; + dmesh.tris[dmesh.ntris * 4 + 3] = getTriFlags(verts, tris[t + 0] * 3, tris[t + 1] * 3, + tris[t + 2] * 3, poly, npoly); + dmesh.ntris++; + } + } + + ctx.stopTimer("POLYMESHDETAIL"); + return dmesh; + + } + + /// @see rcAllocPolyMeshDetail, rcPolyMeshDetail + PolyMeshDetail mergePolyMeshDetails(Telemetry ctx, PolyMeshDetail[] meshes, int nmeshes) { + PolyMeshDetail mesh = new PolyMeshDetail(); + + ctx.startTimer("MERGE_POLYMESHDETAIL"); + + int maxVerts = 0; + int maxTris = 0; + int maxMeshes = 0; + + for (int i = 0; i < nmeshes; ++i) { + if (meshes[i] == null) { + continue; + } + maxVerts += meshes[i].nverts; + maxTris += meshes[i].ntris; + maxMeshes += meshes[i].nmeshes; + } + + mesh.nmeshes = 0; + mesh.meshes = new int[maxMeshes * 4]; + mesh.ntris = 0; + mesh.tris = new int[maxTris * 4]; + mesh.nverts = 0; + mesh.verts = new float[maxVerts * 3]; + + // Merge datas. + for (int i = 0; i < nmeshes; ++i) { + PolyMeshDetail dm = meshes[i]; + if (dm == null) { + continue; + } + for (int j = 0; j < dm.nmeshes; ++j) { + int dst = mesh.nmeshes * 4; + int src = j * 4; + mesh.meshes[dst + 0] = mesh.nverts + dm.meshes[src + 0]; + mesh.meshes[dst + 1] = dm.meshes[src + 1]; + mesh.meshes[dst + 2] = mesh.ntris + dm.meshes[src + 2]; + mesh.meshes[dst + 3] = dm.meshes[src + 3]; + mesh.nmeshes++; + } + + for (int k = 0; k < dm.nverts; ++k) { + RecastVectors.copy(mesh.verts, mesh.nverts * 3, dm.verts, k * 3); + mesh.nverts++; + } + for (int k = 0; k < dm.ntris; ++k) { + mesh.tris[mesh.ntris * 4 + 0] = dm.tris[k * 4 + 0]; + mesh.tris[mesh.ntris * 4 + 1] = dm.tris[k * 4 + 1]; + mesh.tris[mesh.ntris * 4 + 2] = dm.tris[k * 4 + 2]; + mesh.tris[mesh.ntris * 4 + 3] = dm.tris[k * 4 + 3]; + mesh.ntris++; + } + } + ctx.stopTimer("MERGE_POLYMESHDETAIL"); + return mesh; + } + +} diff --git a/src/DotRecast.Recast/RecastRasterization.cs b/src/DotRecast.Recast/RecastRasterization.cs new file mode 100644 index 0000000..2e32cfe --- /dev/null +++ b/src/DotRecast.Recast/RecastRasterization.cs @@ -0,0 +1,483 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; + +namespace DotRecast.Recast; + +using static RecastConstants; + +public class RecastRasterization +{ + /** + * Check whether two bounding boxes overlap + * + * @param amin + * Min axis extents of bounding box A + * @param amax + * Max axis extents of bounding box A + * @param bmin + * Min axis extents of bounding box B + * @param bmax + * Max axis extents of bounding box B + * @returns true if the two bounding boxes overlap. False otherwise + */ + private static bool overlapBounds(float[] amin, float[] amax, float[] bmin, float[] bmax) + { + bool overlap = true; + overlap = (amin[0] > bmax[0] || amax[0] < bmin[0]) ? false : overlap; + overlap = (amin[1] > bmax[1] || amax[1] < bmin[1]) ? false : overlap; + overlap = (amin[2] > bmax[2] || amax[2] < bmin[2]) ? false : overlap; + return overlap; + } + + /** + * Adds a span to the heightfield. If the new span overlaps existing spans, it will merge the new span with the + * existing ones. The span addition can be set to favor flags. If the span is merged to another span and the new + * spanMax is within flagMergeThreshold units from the existing span, the span flags are merged. + * + * @param heightfield + * An initialized heightfield. + * @param x + * The width index where the span is to be added. [Limits: 0 <= value < Heightfield::width] + * @param y + * The height index where the span is to be added. [Limits: 0 <= value < Heightfield::height] + * @param spanMin + * The minimum height of the span. [Limit: < spanMax] [Units: vx] + * @param spanMax + * The minimum height of the span. [Limit: <= RecastConstants.SPAN_MAX_HEIGHT] [Units: vx] + * @param areaId + * The area id of the span. [Limit: <= WALKABLE_AREA) + * @param flagMergeThreshold + * The merge theshold. [Limit: >= 0] [Units: vx] + * @see Heightfield, Span. + */ + public static void addSpan(Heightfield heightfield, int x, int y, int spanMin, int spanMax, int areaId, + int flagMergeThreshold) + { + int idx = x + y * heightfield.width; + + Span s = new Span(); + s.smin = spanMin; + s.smax = spanMax; + s.area = areaId; + s.next = null; + + // Empty cell, add the first span. + if (heightfield.spans[idx] == null) + { + heightfield.spans[idx] = s; + return; + } + + Span prev = null; + Span cur = heightfield.spans[idx]; + + // Insert and merge spans. + while (cur != null) + { + if (cur.smin > s.smax) + { + // Current span is further than the new span, break. + break; + } + else if (cur.smax < s.smin) + { + // Current span is before the new span advance. + prev = cur; + cur = cur.next; + } + else + { + // Merge spans. + if (cur.smin < s.smin) + s.smin = cur.smin; + if (cur.smax > s.smax) + s.smax = cur.smax; + + // Merge flags. + if (Math.Abs(s.smax - cur.smax) <= flagMergeThreshold) + s.area = Math.Max(s.area, cur.area); + + // Remove current span. + Span next = cur.next; + if (prev != null) + prev.next = next; + else + heightfield.spans[idx] = next; + cur = next; + } + } + + // Insert new span. + if (prev != null) + { + s.next = prev.next; + prev.next = s; + } + else + { + s.next = heightfield.spans[idx]; + heightfield.spans[idx] = s; + } + } + + /** + * Divides a convex polygon of max 12 vertices into two convex polygons across a separating axis. + * + * @param inVerts + * The input polygon vertices + * @param inVertsOffset + * The offset of the first polygon vertex + * @param inVertsCount + * The number of input polygon vertices + * @param outVerts1 + * The offset of the resulting polygon 1's vertices + * @param outVerts2 + * The offset of the resulting polygon 2's vertices + * @param axisOffset + * The offset along the specified axis + * @param axis + * The separating axis + * @return The number of resulting polygon 1 and polygon 2 vertices + */ + private static int[] dividePoly(float[] inVerts, int inVertsOffset, int inVertsCount, int outVerts1, int outVerts2, float axisOffset, + int axis) + { + float[] d = new float[12]; + for (int i = 0; i < inVertsCount; ++i) + d[i] = axisOffset - inVerts[inVertsOffset + i * 3 + axis]; + + int m = 0, n = 0; + for (int i = 0, j = inVertsCount - 1; i < inVertsCount; j = i, ++i) + { + bool ina = d[j] >= 0; + bool inb = d[i] >= 0; + if (ina != inb) + { + float s = d[j] / (d[j] - d[i]); + inVerts[outVerts1 + m * 3 + 0] = inVerts[inVertsOffset + j * 3 + 0] + + (inVerts[inVertsOffset + i * 3 + 0] - inVerts[inVertsOffset + j * 3 + 0]) * s; + inVerts[outVerts1 + m * 3 + 1] = inVerts[inVertsOffset + j * 3 + 1] + + (inVerts[inVertsOffset + i * 3 + 1] - inVerts[inVertsOffset + j * 3 + 1]) * s; + inVerts[outVerts1 + m * 3 + 2] = inVerts[inVertsOffset + j * 3 + 2] + + (inVerts[inVertsOffset + i * 3 + 2] - inVerts[inVertsOffset + j * 3 + 2]) * s; + RecastVectors.copy(inVerts, outVerts2 + n * 3, inVerts, outVerts1 + m * 3); + m++; + n++; + // add the i'th point to the right polygon. Do NOT add points that are on the dividing line + // since these were already added above + if (d[i] > 0) + { + RecastVectors.copy(inVerts, outVerts1 + m * 3, inVerts, inVertsOffset + i * 3); + m++; + } + else if (d[i] < 0) + { + RecastVectors.copy(inVerts, outVerts2 + n * 3, inVerts, inVertsOffset + i * 3); + n++; + } + } + else // same side + { + // add the i'th point to the right polygon. Addition is done even for points on the dividing line + if (d[i] >= 0) + { + RecastVectors.copy(inVerts, outVerts1 + m * 3, inVerts, inVertsOffset + i * 3); + m++; + if (d[i] != 0) + continue; + } + + RecastVectors.copy(inVerts, outVerts2 + n * 3, inVerts, inVertsOffset + i * 3); + n++; + } + } + + return new int[] { m, n }; + } + + /** + * Rasterize a single triangle to the heightfield. This code is extremely hot, so much care should be given to + * maintaining maximum perf here. + * + * @param verts + * An array with vertex coordinates [(x, y, z) * N] + * @param v0 + * Index of triangle vertex 0, will be multiplied by 3 to get vertex coordinates + * @param v1 + * Triangle vertex 1 index + * @param v2 + * Triangle vertex 2 index + * @param area + * The area ID to assign to the rasterized spans + * @param hf + * Heightfield to rasterize into + * @param hfBBMin + * The min extents of the heightfield bounding box + * @param hfBBMax + * The max extents of the heightfield bounding box + * @param cellSize + * The x and z axis size of a voxel in the heightfield + * @param inverseCellSize + * 1 / cellSize + * @param inverseCellHeight + * 1 / cellHeight + * @param flagMergeThreshold + * The threshold in which area flags will be merged + */ + private static void rasterizeTri(float[] verts, int v0, int v1, int v2, int area, Heightfield hf, float[] hfBBMin, + float[] hfBBMax, float cellSize, float inverseCellSize, float inverseCellHeight, int flagMergeThreshold) + { + float[] tmin = new float[3]; + float[] tmax = new float[3]; + float by = hfBBMax[1] - hfBBMin[1]; + + // Calculate the bounding box of the triangle. + RecastVectors.copy(tmin, verts, v0 * 3); + RecastVectors.copy(tmax, verts, v0 * 3); + RecastVectors.min(tmin, verts, v1 * 3); + RecastVectors.min(tmin, verts, v2 * 3); + RecastVectors.max(tmax, verts, v1 * 3); + RecastVectors.max(tmax, verts, v2 * 3); + + // If the triangle does not touch the bbox of the heightfield, skip the triagle. + if (!overlapBounds(hfBBMin, hfBBMax, tmin, tmax)) + return; + + // Calculate the footprint of the triangle on the grid's y-axis + int z0 = (int)((tmin[2] - hfBBMin[2]) * inverseCellSize); + int z1 = (int)((tmax[2] - hfBBMin[2]) * inverseCellSize); + + int w = hf.width; + int h = hf.height; + // use -1 rather than 0 to cut the polygon properly at the start of the tile + z0 = RecastCommon.clamp(z0, -1, h - 1); + z1 = RecastCommon.clamp(z1, 0, h - 1); + + // Clip the triangle into all grid cells it touches. + float[] buf = new float[7 * 3 * 4]; + int @in = 0; + int inRow = 7 * 3; + int p1 = inRow + 7 * 3; + int p2 = p1 + 7 * 3; + + RecastVectors.copy(buf, 0, verts, v0 * 3); + RecastVectors.copy(buf, 3, verts, v1 * 3); + RecastVectors.copy(buf, 6, verts, v2 * 3); + int nvRow, nvIn = 3; + + for (int z = z0; z <= z1; ++z) + { + // Clip polygon to row. Store the remaining polygon as well + float cellZ = hfBBMin[2] + z * cellSize; + int[] nvrowin = dividePoly(buf, @in, nvIn, inRow, p1, cellZ + cellSize, 2); + nvRow = nvrowin[0]; + nvIn = nvrowin[1]; + { + int temp = @in; + @in = p1; + p1 = temp; + } + if (nvRow < 3) + continue; + + if (z < 0) + { + continue; + } + + // find the horizontal bounds in the row + float minX = buf[inRow], maxX = buf[inRow]; + for (int i = 1; i < nvRow; ++i) + { + float v = buf[inRow + i * 3]; + minX = Math.Min(minX, v); + maxX = Math.Max(maxX, v); + } + + int x0 = (int)((minX - hfBBMin[0]) * inverseCellSize); + int x1 = (int)((maxX - hfBBMin[0]) * inverseCellSize); + if (x1 < 0 || x0 >= w) + { + continue; + } + + x0 = RecastCommon.clamp(x0, -1, w - 1); + x1 = RecastCommon.clamp(x1, 0, w - 1); + + int nv, nv2 = nvRow; + for (int x = x0; x <= x1; ++x) + { + // Clip polygon to column. store the remaining polygon as well + float cx = hfBBMin[0] + x * cellSize; + int[] nvnv2 = dividePoly(buf, inRow, nv2, p1, p2, cx + cellSize, 0); + nv = nvnv2[0]; + nv2 = nvnv2[1]; + { + int temp = inRow; + inRow = p2; + p2 = temp; + } + if (nv < 3) + continue; + if (x < 0) + { + continue; + } + + // Calculate min and max of the span. + float spanMin = buf[p1 + 1]; + float spanMax = buf[p1 + 1]; + for (int i = 1; i < nv; ++i) + { + spanMin = Math.Min(spanMin, buf[p1 + i * 3 + 1]); + spanMax = Math.Max(spanMax, buf[p1 + i * 3 + 1]); + } + + spanMin -= hfBBMin[1]; + spanMax -= hfBBMin[1]; + // Skip the span if it is outside the heightfield bbox + if (spanMax < 0.0f) + continue; + if (spanMin > by) + continue; + // Clamp the span to the heightfield bbox. + if (spanMin < 0.0f) + spanMin = 0; + if (spanMax > by) + spanMax = by; + + // Snap the span to the heightfield height grid. + int spanMinCellIndex = RecastCommon.clamp((int)Math.Floor(spanMin * inverseCellHeight), 0, SPAN_MAX_HEIGHT); + int spanMaxCellIndex = RecastCommon.clamp((int)Math.Ceiling(spanMax * inverseCellHeight), spanMinCellIndex + 1, SPAN_MAX_HEIGHT); + + addSpan(hf, x, z, spanMinCellIndex, spanMaxCellIndex, area, flagMergeThreshold); + } + } + } + + /** + * Rasterizes a single triangle into the specified heightfield. Calling this for each triangle in a mesh is less + * efficient than calling rasterizeTriangles. No spans will be added if the triangle does not overlap the + * heightfield grid. + * + * @param heightfield + * An initialized heightfield. + * @param verts + * An array with vertex coordinates [(x, y, z) * N] + * @param v0 + * Index of triangle vertex 0, will be multiplied by 3 to get vertex coordinates + * @param v1 + * Triangle vertex 1 index + * @param v2 + * Triangle vertex 2 index + * @param areaId + * The area id of the triangle. [Limit: <= WALKABLE_AREA) + * @param flagMergeThreshold + * The distance where the walkable flag is favored over the non-walkable flag. [Limit: >= 0] [Units: vx] + * @see Heightfield + */ + public static void rasterizeTriangle(Heightfield heightfield, float[] verts, int v0, int v1, int v2, int area, + int flagMergeThreshold, Telemetry ctx) + { + ctx.startTimer("RASTERIZE_TRIANGLES"); + + float inverseCellSize = 1.0f / heightfield.cs; + float inverseCellHeight = 1.0f / heightfield.ch; + rasterizeTri(verts, v0, v1, v2, area, heightfield, heightfield.bmin, heightfield.bmax, heightfield.cs, inverseCellSize, + inverseCellHeight, flagMergeThreshold); + + ctx.stopTimer("RASTERIZE_TRIANGLES"); + } + + /** + * Rasterizes an indexed triangle mesh into the specified heightfield. Spans will only be added for triangles that + * overlap the heightfield grid. + * + * @param heightfield + * An initialized heightfield. + * @param verts + * The vertices. [(x, y, z) * N] + * @param tris + * The triangle indices. [(vertA, vertB, vertC) * nt] + * @param areaIds + * The area id's of the triangles. [Limit: <= WALKABLE_AREA] [Size: numTris] + * @param numTris + * The number of triangles. + * @param flagMergeThreshold + * The distance where the walkable flag is favored over the non-walkable flag. [Limit: >= 0] [Units: vx] + * @see Heightfield + */ + public static void rasterizeTriangles(Heightfield heightfield, float[] verts, int[] tris, int[] areaIds, int numTris, + int flagMergeThreshold, Telemetry ctx) + { + ctx.startTimer("RASTERIZE_TRIANGLES"); + + float inverseCellSize = 1.0f / heightfield.cs; + float inverseCellHeight = 1.0f / heightfield.ch; + for (int triIndex = 0; triIndex < numTris; ++triIndex) + { + int v0 = tris[triIndex * 3 + 0]; + int v1 = tris[triIndex * 3 + 1]; + int v2 = tris[triIndex * 3 + 2]; + rasterizeTri(verts, v0, v1, v2, areaIds[triIndex], heightfield, heightfield.bmin, heightfield.bmax, heightfield.cs, + inverseCellSize, inverseCellHeight, flagMergeThreshold); + } + + ctx.stopTimer("RASTERIZE_TRIANGLES"); + } + + /** + * Rasterizes a triangle list into the specified heightfield. Expects each triangle to be specified as three + * sequential vertices of 3 floats. Spans will only be added for triangles that overlap the heightfield grid. + * + * @param heightfield + * An initialized heightfield. + * @param verts + * The vertices. [(x, y, z) * numVerts] + * @param areaIds + * The area id's of the triangles. [Limit: <= WALKABLE_AREA] [Size: numTris] + * @param tris + * The triangle indices. [(vertA, vertB, vertC) * nt] + * @param numTris + * The number of triangles. + * @param flagMergeThreshold + * The distance where the walkable flag is favored over the non-walkable flag. [Limit: >= 0] [Units: vx] + * @see Heightfield + */ + public static void rasterizeTriangles(Heightfield heightfield, float[] verts, int[] areaIds, int numTris, + int flagMergeThreshold, Telemetry ctx) + { + ctx.startTimer("RASTERIZE_TRIANGLES"); + + float inverseCellSize = 1.0f / heightfield.cs; + float inverseCellHeight = 1.0f / heightfield.ch; + for (int triIndex = 0; triIndex < numTris; ++triIndex) + { + int v0 = (triIndex * 3 + 0); + int v1 = (triIndex * 3 + 1); + int v2 = (triIndex * 3 + 2); + rasterizeTri(verts, v0, v1, v2, areaIds[triIndex], heightfield, heightfield.bmin, heightfield.bmax, heightfield.cs, + inverseCellSize, inverseCellHeight, flagMergeThreshold); + } + + ctx.stopTimer("RASTERIZE_TRIANGLES"); + } +} \ No newline at end of file diff --git a/src/DotRecast.Recast/RecastRegion.cs b/src/DotRecast.Recast/RecastRegion.cs new file mode 100644 index 0000000..3d130b9 --- /dev/null +++ b/src/DotRecast.Recast/RecastRegion.cs @@ -0,0 +1,1618 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace DotRecast.Recast; + +using static RecastConstants; + +public class RecastRegion { + + const int RC_NULL_NEI = 0xffff; + + public class SweepSpan { + public int rid; // row id + public int id; // region id + public int ns; // number samples + public int nei; // neighbour id + } + + public static int calculateDistanceField(CompactHeightfield chf, int[] src) { + int maxDist; + int w = chf.width; + int h = chf.height; + + // Init distance and points. + for (int i = 0; i < chf.spanCount; ++i) { + src[i] = 0xffff; + } + + // Mark boundary cells. + for (int y = 0; y < h; ++y) { + for (int x = 0; x < w; ++x) { + CompactCell c = chf.cells[x + y * w]; + for (int i = c.index, ni = c.index + c.count; i < ni; ++i) { + CompactSpan s = chf.spans[i]; + int area = chf.areas[i]; + + int nc = 0; + for (int dir = 0; dir < 4; ++dir) { + if (RecastCommon.GetCon(s, dir) != RC_NOT_CONNECTED) { + int ax = x + RecastCommon.GetDirOffsetX(dir); + int ay = y + RecastCommon.GetDirOffsetY(dir); + int ai = chf.cells[ax + ay * w].index + RecastCommon.GetCon(s, dir); + if (area == chf.areas[ai]) { + nc++; + } + } + } + if (nc != 4) { + src[i] = 0; + } + } + } + } + + // Pass 1 + for (int y = 0; y < h; ++y) { + for (int x = 0; x < w; ++x) { + CompactCell c = chf.cells[x + y * w]; + for (int i = c.index, ni = c.index + c.count; i < ni; ++i) { + CompactSpan s = chf.spans[i]; + + if (RecastCommon.GetCon(s, 0) != RC_NOT_CONNECTED) { + // (-1,0) + int ax = x + RecastCommon.GetDirOffsetX(0); + int ay = y + RecastCommon.GetDirOffsetY(0); + int ai = chf.cells[ax + ay * w].index + RecastCommon.GetCon(s, 0); + CompactSpan @as = chf.spans[ai]; + if (src[ai] + 2 < src[i]) { + src[i] = src[ai] + 2; + } + + // (-1,-1) + if (RecastCommon.GetCon(@as, 3) != RC_NOT_CONNECTED) { + int aax = ax + RecastCommon.GetDirOffsetX(3); + int aay = ay + RecastCommon.GetDirOffsetY(3); + int aai = chf.cells[aax + aay * w].index + RecastCommon.GetCon(@as, 3); + if (src[aai] + 3 < src[i]) { + src[i] = src[aai] + 3; + } + } + } + if (RecastCommon.GetCon(s, 3) != RC_NOT_CONNECTED) { + // (0,-1) + int ax = x + RecastCommon.GetDirOffsetX(3); + int ay = y + RecastCommon.GetDirOffsetY(3); + int ai = chf.cells[ax + ay * w].index + RecastCommon.GetCon(s, 3); + CompactSpan @as = chf.spans[ai]; + if (src[ai] + 2 < src[i]) { + src[i] = src[ai] + 2; + } + + // (1,-1) + if (RecastCommon.GetCon(@as, 2) != RC_NOT_CONNECTED) { + int aax = ax + RecastCommon.GetDirOffsetX(2); + int aay = ay + RecastCommon.GetDirOffsetY(2); + int aai = chf.cells[aax + aay * w].index + RecastCommon.GetCon(@as, 2); + if (src[aai] + 3 < src[i]) { + src[i] = src[aai] + 3; + } + } + } + } + } + } + + // Pass 2 + for (int y = h - 1; y >= 0; --y) { + for (int x = w - 1; x >= 0; --x) { + CompactCell c = chf.cells[x + y * w]; + for (int i = c.index, ni = c.index + c.count; i < ni; ++i) { + CompactSpan s = chf.spans[i]; + + if (RecastCommon.GetCon(s, 2) != RC_NOT_CONNECTED) { + // (1,0) + int ax = x + RecastCommon.GetDirOffsetX(2); + int ay = y + RecastCommon.GetDirOffsetY(2); + int ai = chf.cells[ax + ay * w].index + RecastCommon.GetCon(s, 2); + CompactSpan @as = chf.spans[ai]; + if (src[ai] + 2 < src[i]) { + src[i] = src[ai] + 2; + } + + // (1,1) + if (RecastCommon.GetCon(@as, 1) != RC_NOT_CONNECTED) { + int aax = ax + RecastCommon.GetDirOffsetX(1); + int aay = ay + RecastCommon.GetDirOffsetY(1); + int aai = chf.cells[aax + aay * w].index + RecastCommon.GetCon(@as, 1); + if (src[aai] + 3 < src[i]) { + src[i] = src[aai] + 3; + } + } + } + if (RecastCommon.GetCon(s, 1) != RC_NOT_CONNECTED) { + // (0,1) + int ax = x + RecastCommon.GetDirOffsetX(1); + int ay = y + RecastCommon.GetDirOffsetY(1); + int ai = chf.cells[ax + ay * w].index + RecastCommon.GetCon(s, 1); + CompactSpan @as = chf.spans[ai]; + if (src[ai] + 2 < src[i]) { + src[i] = src[ai] + 2; + } + + // (-1,1) + if (RecastCommon.GetCon(@as, 0) != RC_NOT_CONNECTED) { + int aax = ax + RecastCommon.GetDirOffsetX(0); + int aay = ay + RecastCommon.GetDirOffsetY(0); + int aai = chf.cells[aax + aay * w].index + RecastCommon.GetCon(@as, 0); + if (src[aai] + 3 < src[i]) { + src[i] = src[aai] + 3; + } + } + } + } + } + } + + maxDist = 0; + for (int i = 0; i < chf.spanCount; ++i) { + maxDist = Math.Max(src[i], maxDist); + } + + return maxDist; + } + + private static int[] boxBlur(CompactHeightfield chf, int thr, int[] src) { + int w = chf.width; + int h = chf.height; + int[] dst = new int[chf.spanCount]; + + thr *= 2; + + for (int y = 0; y < h; ++y) { + for (int x = 0; x < w; ++x) { + CompactCell c = chf.cells[x + y * w]; + for (int i = c.index, ni = c.index + c.count; i < ni; ++i) { + CompactSpan s = chf.spans[i]; + int cd = src[i]; + if (cd <= thr) { + dst[i] = cd; + continue; + } + + int d = cd; + for (int dir = 0; dir < 4; ++dir) { + if (RecastCommon.GetCon(s, dir) != RC_NOT_CONNECTED) { + int ax = x + RecastCommon.GetDirOffsetX(dir); + int ay = y + RecastCommon.GetDirOffsetY(dir); + int ai = chf.cells[ax + ay * w].index + RecastCommon.GetCon(s, dir); + d += src[ai]; + + CompactSpan @as = chf.spans[ai]; + int dir2 = (dir + 1) & 0x3; + if (RecastCommon.GetCon(@as, dir2) != RC_NOT_CONNECTED) { + int ax2 = ax + RecastCommon.GetDirOffsetX(dir2); + int ay2 = ay + RecastCommon.GetDirOffsetY(dir2); + int ai2 = chf.cells[ax2 + ay2 * w].index + RecastCommon.GetCon(@as, dir2); + d += src[ai2]; + } else { + d += cd; + } + } else { + d += cd * 2; + } + } + dst[i] = ((d + 5) / 9); + } + } + } + return dst; + } + + private static bool floodRegion(int x, int y, int i, int level, int r, CompactHeightfield chf, int[] srcReg, + int[] srcDist, List stack) { + int w = chf.width; + + int area = chf.areas[i]; + + // Flood fill mark region. + stack.Clear(); + stack.Add(x); + stack.Add(y); + stack.Add(i); + srcReg[i] = r; + srcDist[i] = 0; + + int lev = level >= 2 ? level - 2 : 0; + int count = 0; + + while (stack.Count > 0) + { + int ci = stack[^1]; + stack.RemoveAt(stack.Count - 1); + + int cy = stack[^1]; + stack.RemoveAt(stack.Count - 1); + + int cx = stack[^1]; + stack.RemoveAt(stack.Count - 1); + + + CompactSpan cs = chf.spans[ci]; + + // Check if any of the neighbours already have a valid region set. + int ar = 0; + for (int dir = 0; dir < 4; ++dir) { + // 8 connected + if (RecastCommon.GetCon(cs, dir) != RC_NOT_CONNECTED) { + int ax = cx + RecastCommon.GetDirOffsetX(dir); + int ay = cy + RecastCommon.GetDirOffsetY(dir); + int ai = chf.cells[ax + ay * w].index + RecastCommon.GetCon(cs, dir); + if (chf.areas[ai] != area) { + continue; + } + int nr = srcReg[ai]; + if ((nr & RC_BORDER_REG) != 0) { + continue; + } + if (nr != 0 && nr != r) { + ar = nr; + break; + } + + CompactSpan @as = chf.spans[ai]; + + int dir2 = (dir + 1) & 0x3; + if (RecastCommon.GetCon(@as, dir2) != RC_NOT_CONNECTED) { + int ax2 = ax + RecastCommon.GetDirOffsetX(dir2); + int ay2 = ay + RecastCommon.GetDirOffsetY(dir2); + int ai2 = chf.cells[ax2 + ay2 * w].index + RecastCommon.GetCon(@as, dir2); + if (chf.areas[ai2] != area) { + continue; + } + int nr2 = srcReg[ai2]; + if (nr2 != 0 && nr2 != r) { + ar = nr2; + break; + } + } + } + } + if (ar != 0) { + srcReg[ci] = 0; + continue; + } + + count++; + + // Expand neighbours. + for (int dir = 0; dir < 4; ++dir) { + if (RecastCommon.GetCon(cs, dir) != RC_NOT_CONNECTED) { + int ax = cx + RecastCommon.GetDirOffsetX(dir); + int ay = cy + RecastCommon.GetDirOffsetY(dir); + int ai = chf.cells[ax + ay * w].index + RecastCommon.GetCon(cs, dir); + if (chf.areas[ai] != area) { + continue; + } + if (chf.dist[ai] >= lev && srcReg[ai] == 0) { + srcReg[ai] = r; + srcDist[ai] = 0; + stack.Add(ax); + stack.Add(ay); + stack.Add(ai); + } + } + } + } + + return count > 0; + } + + private static int[] expandRegions(int maxIter, int level, CompactHeightfield chf, int[] srcReg, int[] srcDist, + List stack, bool fillStack) { + int w = chf.width; + int h = chf.height; + + if (fillStack) { + // Find cells revealed by the raised level. + stack.Clear(); + for (int y = 0; y < h; ++y) { + for (int x = 0; x < w; ++x) { + CompactCell c = chf.cells[x + y * w]; + for (int i = c.index, ni = c.index + c.count; i < ni; ++i) { + if (chf.dist[i] >= level && srcReg[i] == 0 && chf.areas[i] != RC_NULL_AREA) { + stack.Add(x); + stack.Add(y); + stack.Add(i); + } + } + } + } + } else // use cells in the input stack + { + // mark all cells which already have a region + for (int j = 0; j < stack.Count; j += 3) { + int i = stack[j + 2]; + if (srcReg[i] != 0) { + stack[j + 2] = -1; + } + } + } + + List dirtyEntries = new(); + int iter = 0; + while (stack.Count > 0) { + int failed = 0; + dirtyEntries.Clear(); + + for (int j = 0; j < stack.Count; j += 3) + { + int x = stack[j + 0]; + int y = stack[j + 1]; + int i = stack[j + 2]; + if (i < 0) { + failed++; + continue; + } + + int r = srcReg[i]; + int d2 = 0xffff; + int area = chf.areas[i]; + CompactSpan s = chf.spans[i]; + for (int dir = 0; dir < 4; ++dir) { + if (RecastCommon.GetCon(s, dir) == RC_NOT_CONNECTED) { + continue; + } + int ax = x + RecastCommon.GetDirOffsetX(dir); + int ay = y + RecastCommon.GetDirOffsetY(dir); + int ai = chf.cells[ax + ay * w].index + RecastCommon.GetCon(s, dir); + if (chf.areas[ai] != area) { + continue; + } + if (srcReg[ai] > 0 && (srcReg[ai] & RC_BORDER_REG) == 0) { + if (srcDist[ai] + 2 < d2) { + r = srcReg[ai]; + d2 = srcDist[ai] + 2; + } + } + } + if (r != 0) { + stack[j + 2] = -1; // mark as used + dirtyEntries.Add(i); + dirtyEntries.Add(r); + dirtyEntries.Add(d2); + } else { + failed++; + } + } + + // Copy entries that differ between src and dst to keep them in sync. + for (int i = 0; i < dirtyEntries.Count; i += 3) { + int idx = dirtyEntries[i]; + srcReg[idx] = dirtyEntries[i + 1]; + srcDist[idx] = dirtyEntries[i + 2]; + } + + if (failed * 3 == stack.Count()) { + break; + } + + if (level > 0) { + ++iter; + if (iter >= maxIter) { + break; + } + } + } + + return srcReg; + } + + private static void sortCellsByLevel(int startLevel, CompactHeightfield chf, int[] srcReg, int nbStacks, + List> stacks, int loglevelsPerStack) // the levels per stack (2 in our case) as a bit shift + { + int w = chf.width; + int h = chf.height; + startLevel = startLevel >> loglevelsPerStack; + + for (int j = 0; j < nbStacks; ++j) { + stacks[j].Clear(); + } + + // put all cells in the level range into the appropriate stacks + for (int y = 0; y < h; ++y) { + for (int x = 0; x < w; ++x) { + CompactCell c = chf.cells[x + y * w]; + for (int i = c.index, ni = c.index + c.count; i < ni; ++i) { + if (chf.areas[i] == RC_NULL_AREA || srcReg[i] != 0) { + continue; + } + + int level = chf.dist[i] >> loglevelsPerStack; + int sId = startLevel - level; + if (sId >= nbStacks) { + continue; + } + if (sId < 0) { + sId = 0; + } + + stacks[sId].Add(x); + stacks[sId].Add(y); + stacks[sId].Add(i); + } + } + } + } + + private static void appendStacks(List srcStack, List dstStack, int[] srcReg) { + for (int j = 0; j < srcStack.Count; j += 3) { + int i = srcStack[j + 2]; + if ((i < 0) || (srcReg[i] != 0)) { + continue; + } + dstStack.Add(srcStack[j]); + dstStack.Add(srcStack[j + 1]); + dstStack.Add(srcStack[j + 2]); + } + } + + private class Region { + public int spanCount; // Number of spans belonging to this region + public int id; // ID of the region + public int areaType; // Are type. + public bool remap; + public bool visited; + public bool overlap; + public bool connectsToBorder; + public int ymin, ymax; + public List connections; + public List floors; + + public Region(int i) { + id = i; + ymin = 0xFFFF; + connections = new(); + floors = new(); + } + + } + + private static void removeAdjacentNeighbours(Region reg) { + // Remove adjacent duplicates. + for (int i = 0; i < reg.connections.Count && reg.connections.Count > 1;) { + int ni = (i + 1) % reg.connections.Count; + if (reg.connections[i] == reg.connections[ni]) { + reg.connections.RemoveAt(i); + } else { + ++i; + } + } + } + + private static void replaceNeighbour(Region reg, int oldId, int newId) { + bool neiChanged = false; + for (int i = 0; i < reg.connections.Count; ++i) { + if (reg.connections[i] == oldId) + { + reg.connections[i] = newId; + neiChanged = true; + } + } + for (int i = 0; i < reg.floors.Count; ++i) { + if (reg.floors[i] == oldId) + { + reg.floors[i] = newId; + } + } + if (neiChanged) { + removeAdjacentNeighbours(reg); + } + } + + private static bool canMergeWithRegion(Region rega, Region regb) { + if (rega.areaType != regb.areaType) { + return false; + } + int n = 0; + for (int i = 0; i < rega.connections.Count; ++i) { + if (rega.connections[i] == regb.id) { + n++; + } + } + if (n > 1) { + return false; + } + for (int i = 0; i < rega.floors.Count; ++i) { + if (rega.floors[i] == regb.id) { + return false; + } + } + return true; + } + + private static void addUniqueFloorRegion(Region reg, int n) { + if (!reg.floors.Contains(n)) { + reg.floors.Add(n); + } + } + + private static bool mergeRegions(Region rega, Region regb) { + int aid = rega.id; + int bid = regb.id; + + // Duplicate current neighbourhood. + List acon = new(rega.connections); + List bcon = regb.connections; + + // Find insertion point on A. + int insa = -1; + for (int i = 0; i < acon.Count; ++i) { + if (acon[i] == bid) { + insa = i; + break; + } + } + if (insa == -1) { + return false; + } + + // Find insertion point on B. + int insb = -1; + for (int i = 0; i < bcon.Count; ++i) { + if (bcon[i] == aid) { + insb = i; + break; + } + } + if (insb == -1) { + return false; + } + + // Merge neighbours. + rega.connections.Clear(); + for (int i = 0, ni = acon.Count; i < ni - 1; ++i) { + rega.connections.Add(acon[(insa + 1 + i) % ni]); + } + + for (int i = 0, ni = bcon.Count; i < ni - 1; ++i) { + rega.connections.Add(bcon[(insb + 1 + i) % ni]); + } + + removeAdjacentNeighbours(rega); + + for (int j = 0; j < regb.floors.Count; ++j) { + addUniqueFloorRegion(rega, regb.floors[j]); + } + rega.spanCount += regb.spanCount; + regb.spanCount = 0; + regb.connections.Clear(); + + return true; + } + + private static bool isRegionConnectedToBorder(Region reg) { + // Region is connected to border if + // one of the neighbours is null id. + return reg.connections.Contains(0); + } + + private static bool isSolidEdge(CompactHeightfield chf, int[] srcReg, int x, int y, int i, int dir) { + CompactSpan s = chf.spans[i]; + int r = 0; + if (RecastCommon.GetCon(s, dir) != RC_NOT_CONNECTED) { + int ax = x + RecastCommon.GetDirOffsetX(dir); + int ay = y + RecastCommon.GetDirOffsetY(dir); + int ai = chf.cells[ax + ay * chf.width].index + RecastCommon.GetCon(s, dir); + r = srcReg[ai]; + } + if (r == srcReg[i]) { + return false; + } + return true; + } + + private static void walkContour(int x, int y, int i, int dir, CompactHeightfield chf, int[] srcReg, + List cont) { + int startDir = dir; + int starti = i; + + CompactSpan ss = chf.spans[i]; + int curReg = 0; + if (RecastCommon.GetCon(ss, dir) != RC_NOT_CONNECTED) { + int ax = x + RecastCommon.GetDirOffsetX(dir); + int ay = y + RecastCommon.GetDirOffsetY(dir); + int ai = chf.cells[ax + ay * chf.width].index + RecastCommon.GetCon(ss, dir); + curReg = srcReg[ai]; + } + cont.Add(curReg); + + int iter = 0; + while (++iter < 40000) { + CompactSpan s = chf.spans[i]; + + if (isSolidEdge(chf, srcReg, x, y, i, dir)) { + // Choose the edge corner + int r = 0; + if (RecastCommon.GetCon(s, dir) != RC_NOT_CONNECTED) { + int ax = x + RecastCommon.GetDirOffsetX(dir); + int ay = y + RecastCommon.GetDirOffsetY(dir); + int ai = chf.cells[ax + ay * chf.width].index + RecastCommon.GetCon(s, dir); + r = srcReg[ai]; + } + if (r != curReg) { + curReg = r; + cont.Add(curReg); + } + + dir = (dir + 1) & 0x3; // Rotate CW + } else { + int ni = -1; + int nx = x + RecastCommon.GetDirOffsetX(dir); + int ny = y + RecastCommon.GetDirOffsetY(dir); + if (RecastCommon.GetCon(s, dir) != RC_NOT_CONNECTED) { + CompactCell nc = chf.cells[nx + ny * chf.width]; + ni = nc.index + RecastCommon.GetCon(s, dir); + } + if (ni == -1) { + // Should not happen. + return; + } + x = nx; + y = ny; + i = ni; + dir = (dir + 3) & 0x3; // Rotate CCW + } + + if (starti == i && startDir == dir) { + break; + } + } + + // Remove adjacent duplicates. + if (cont.Count > 1) { + for (int j = 0; j < cont.Count;) { + int nj = (j + 1) % cont.Count; + if (cont[j] == cont[nj]) { + cont.RemoveAt(j); + } else { + ++j; + } + } + } + } + + private static int mergeAndFilterRegions(Telemetry ctx, int minRegionArea, int mergeRegionSize, int maxRegionId, + CompactHeightfield chf, int[] srcReg, List overlaps) { + int w = chf.width; + int h = chf.height; + + int nreg = maxRegionId + 1; + Region[] regions = new Region[nreg]; + + // Construct regions + for (int i = 0; i < nreg; ++i) { + regions[i] = new Region(i); + } + + // Find edge of a region and find connections around the contour. + for (int y = 0; y < h; ++y) { + for (int x = 0; x < w; ++x) { + CompactCell c = chf.cells[x + y * w]; + for (int i = c.index, ni = c.index + c.count; i < ni; ++i) { + int r = srcReg[i]; + if (r == 0 || r >= nreg) { + continue; + } + + Region reg = regions[r]; + reg.spanCount++; + + // Update floors. + for (int j = c.index; j < ni; ++j) { + if (i == j) { + continue; + } + int floorId = srcReg[j]; + if (floorId == 0 || floorId >= nreg) { + continue; + } + if (floorId == r) { + reg.overlap = true; + } + addUniqueFloorRegion(reg, floorId); + } + + // Have found contour + if (reg.connections.Count > 0) { + continue; + } + + reg.areaType = chf.areas[i]; + + // Check if this cell is next to a border. + int ndir = -1; + for (int dir = 0; dir < 4; ++dir) { + if (isSolidEdge(chf, srcReg, x, y, i, dir)) { + ndir = dir; + break; + } + } + + if (ndir != -1) { + // The cell is at border. + // Walk around the contour to find all the neighbours. + walkContour(x, y, i, ndir, chf, srcReg, reg.connections); + } + } + } + } + + // Remove too small regions. + List stack = new(32); + List trace = new(32); + for (int i = 0; i < nreg; ++i) { + Region reg = regions[i]; + if (reg.id == 0 || (reg.id & RC_BORDER_REG) != 0) { + continue; + } + if (reg.spanCount == 0) { + continue; + } + if (reg.visited) { + continue; + } + + // Count the total size of all the connected regions. + // Also keep track of the regions connects to a tile border. + bool connectsToBorder = false; + int spanCount = 0; + stack.Clear(); + trace.Clear(); + + reg.visited = true; + stack.Add(i); + + while (stack.Count > 0) { + // Pop + int ri = stack[^1]; + stack.RemoveAt(stack.Count - 1); + + Region creg = regions[ri]; + + spanCount += creg.spanCount; + trace.Add(ri); + + for (int j = 0; j < creg.connections.Count; ++j) { + if ((creg.connections[j] & RC_BORDER_REG) != 0) { + connectsToBorder = true; + continue; + } + Region neireg = regions[creg.connections[j]]; + if (neireg.visited) { + continue; + } + if (neireg.id == 0 || (neireg.id & RC_BORDER_REG) != 0) { + continue; + } + // Visit + stack.Add(neireg.id); + neireg.visited = true; + } + } + + // If the accumulated regions size is too small, remove it. + // Do not remove areas which connect to tile borders + // as their size cannot be estimated correctly and removing them + // can potentially remove necessary areas. + if (spanCount < minRegionArea && !connectsToBorder) { + // Kill all visited regions. + for (int j = 0; j < trace.Count; ++j) { + regions[trace[j]].spanCount = 0; + regions[trace[j]].id = 0; + } + } + } + + // Merge too small regions to neighbour regions. + int mergeCount = 0; + do { + mergeCount = 0; + for (int i = 0; i < nreg; ++i) { + Region reg = regions[i]; + if (reg.id == 0 || (reg.id & RC_BORDER_REG) != 0) { + continue; + } + if (reg.overlap) { + continue; + } + if (reg.spanCount == 0) { + continue; + } + + // Check to see if the region should be merged. + if (reg.spanCount > mergeRegionSize && isRegionConnectedToBorder(reg)) { + continue; + } + + // Small region with more than 1 connection. + // Or region which is not connected to a border at all. + // Find smallest neighbour region that connects to this one. + int smallest = 0xfffffff; + int mergeId = reg.id; + for (int j = 0; j < reg.connections.Count; ++j) { + if ((reg.connections[j] & RC_BORDER_REG) != 0) { + continue; + } + Region mreg = regions[reg.connections[j]]; + if (mreg.id == 0 || (mreg.id & RC_BORDER_REG) != 0 || mreg.overlap) { + continue; + } + if (mreg.spanCount < smallest && canMergeWithRegion(reg, mreg) && canMergeWithRegion(mreg, reg)) { + smallest = mreg.spanCount; + mergeId = mreg.id; + } + } + // Found new id. + if (mergeId != reg.id) { + int oldId = reg.id; + Region target = regions[mergeId]; + + // Merge neighbours. + if (mergeRegions(target, reg)) { + // Fixup regions pointing to current region. + for (int j = 0; j < nreg; ++j) { + if (regions[j].id == 0 || (regions[j].id & RC_BORDER_REG) != 0) { + continue; + } + // If another region was already merged into current region + // change the nid of the previous region too. + if (regions[j].id == oldId) { + regions[j].id = mergeId; + } + // Replace the current region with the new one if the + // current regions is neighbour. + replaceNeighbour(regions[j], oldId, mergeId); + } + mergeCount++; + } + } + } + } while (mergeCount > 0); + + // Compress region Ids. + for (int i = 0; i < nreg; ++i) { + regions[i].remap = false; + if (regions[i].id == 0) { + continue; // Skip nil regions. + } + if ((regions[i].id & RC_BORDER_REG) != 0) { + continue; // Skip external regions. + } + regions[i].remap = true; + } + + int regIdGen = 0; + for (int i = 0; i < nreg; ++i) { + if (!regions[i].remap) { + continue; + } + int oldId = regions[i].id; + int newId = ++regIdGen; + for (int j = i; j < nreg; ++j) { + if (regions[j].id == oldId) { + regions[j].id = newId; + regions[j].remap = false; + } + } + } + maxRegionId = regIdGen; + + // Remap regions. + for (int i = 0; i < chf.spanCount; ++i) { + if ((srcReg[i] & RC_BORDER_REG) == 0) { + srcReg[i] = regions[srcReg[i]].id; + } + } + + // Return regions that we found to be overlapping. + for (int i = 0; i < nreg; ++i) { + if (regions[i].overlap) { + overlaps.Add(regions[i].id); + } + } + + return maxRegionId; + } + + private static void addUniqueConnection(Region reg, int n) { + if (!reg.connections.Contains(n)) { + reg.connections.Add(n); + } + } + + private static int mergeAndFilterLayerRegions(Telemetry ctx, int minRegionArea, int maxRegionId, + CompactHeightfield chf, int[] srcReg, List overlaps) { + int w = chf.width; + int h = chf.height; + + int nreg = maxRegionId + 1; + Region[] regions = new Region[nreg]; + + // Construct regions + for (int i = 0; i < nreg; ++i) { + regions[i] = new Region(i); + } + + // Find region neighbours and overlapping regions. + List lregs = new(32); + for (int y = 0; y < h; ++y) { + for (int x = 0; x < w; ++x) { + CompactCell c = chf.cells[x + y * w]; + + lregs.Clear(); + + for (int i = c.index, ni = c.index + c.count; i < ni; ++i) { + CompactSpan s = chf.spans[i]; + int ri = srcReg[i]; + if (ri == 0 || ri >= nreg) { + continue; + } + Region reg = regions[ri]; + + reg.spanCount++; + reg.areaType = chf.areas[i]; + reg.ymin = Math.Min(reg.ymin, s.y); + reg.ymax = Math.Max(reg.ymax, s.y); + // Collect all region layers. + lregs.Add(ri); + + // Update neighbours + for (int dir = 0; dir < 4; ++dir) { + if (RecastCommon.GetCon(s, dir) != RC_NOT_CONNECTED) { + int ax = x + RecastCommon.GetDirOffsetX(dir); + int ay = y + RecastCommon.GetDirOffsetY(dir); + int ai = chf.cells[ax + ay * w].index + RecastCommon.GetCon(s, dir); + int rai = srcReg[ai]; + if (rai > 0 && rai < nreg && rai != ri) { + addUniqueConnection(reg, rai); + } + if ((rai & RC_BORDER_REG) != 0) { + reg.connectsToBorder = true; + } + } + } + + } + + // Update overlapping regions. + for (int i = 0; i < lregs.Count - 1; ++i) { + for (int j = i + 1; j < lregs.Count; ++j) { + if (lregs[i] != lregs[j]) { + Region ri = regions[lregs[i]]; + Region rj = regions[lregs[j]]; + addUniqueFloorRegion(ri, lregs[j]); + addUniqueFloorRegion(rj, lregs[i]); + } + } + } + + } + } + + // Create 2D layers from regions. + int layerId = 1; + + for (int i = 0; i < nreg; ++i) { + regions[i].id = 0; + } + + // Merge montone regions to create non-overlapping areas. + List stack = new(32); + for (int i = 1; i < nreg; ++i) { + Region root = regions[i]; + // Skip already visited. + if (root.id != 0) { + continue; + } + + // Start search. + root.id = layerId; + + stack.Clear(); + stack.Add(i); + + while (stack.Count > 0) { + // Pop front + var idx = stack[0]; + stack.RemoveAt(0); + Region reg = regions[idx]; + + int ncons = reg.connections.Count; + for (int j = 0; j < ncons; ++j) { + int nei = reg.connections[j]; + Region regn = regions[nei]; + // Skip already visited. + if (regn.id != 0) { + continue; + } + // Skip if different area type, do not connect regions with different area type. + if (reg.areaType != regn.areaType) { + continue; + } + // Skip if the neighbour is overlapping root region. + bool overlap = false; + for (int k = 0; k < root.floors.Count; k++) { + if (root.floors[k] == nei) { + overlap = true; + break; + } + } + if (overlap) { + continue; + } + + // Deepen + stack.Add(nei); + + // Mark layer id + regn.id = layerId; + // Merge current layers to root. + for (int k = 0; k < regn.floors.Count; ++k) { + addUniqueFloorRegion(root, regn.floors[k]); + } + root.ymin = Math.Min(root.ymin, regn.ymin); + root.ymax = Math.Max(root.ymax, regn.ymax); + root.spanCount += regn.spanCount; + regn.spanCount = 0; + root.connectsToBorder = root.connectsToBorder || regn.connectsToBorder; + } + } + + layerId++; + } + + // Remove small regions + for (int i = 0; i < nreg; ++i) { + if (regions[i].spanCount > 0 && regions[i].spanCount < minRegionArea && !regions[i].connectsToBorder) { + int reg = regions[i].id; + for (int j = 0; j < nreg; ++j) { + if (regions[j].id == reg) { + regions[j].id = 0; + } + } + } + } + + // Compress region Ids. + for (int i = 0; i < nreg; ++i) { + regions[i].remap = false; + if (regions[i].id == 0) { + continue; // Skip nil regions. + } + if ((regions[i].id & RC_BORDER_REG) != 0) { + continue; // Skip external regions. + } + regions[i].remap = true; + } + + int regIdGen = 0; + for (int i = 0; i < nreg; ++i) { + if (!regions[i].remap) { + continue; + } + int oldId = regions[i].id; + int newId = ++regIdGen; + for (int j = i; j < nreg; ++j) { + if (regions[j].id == oldId) { + regions[j].id = newId; + regions[j].remap = false; + } + } + } + maxRegionId = regIdGen; + + // Remap regions. + for (int i = 0; i < chf.spanCount; ++i) { + if ((srcReg[i] & RC_BORDER_REG) == 0) { + srcReg[i] = regions[srcReg[i]].id; + } + } + + return maxRegionId; + } + + /// @par + /// + /// This is usually the second to the last step in creating a fully built + /// compact heightfield. This step is required before regions are built + /// using #rcBuildRegions or #rcBuildRegionsMonotone. + /// + /// After this step, the distance data is available via the rcCompactHeightfield::maxDistance + /// and rcCompactHeightfield::dist fields. + /// + /// @see rcCompactHeightfield, rcBuildRegions, rcBuildRegionsMonotone + public static void buildDistanceField(Telemetry ctx, CompactHeightfield chf) { + + ctx.startTimer("DISTANCEFIELD"); + int[] src = new int[chf.spanCount]; + ctx.startTimer("DISTANCEFIELD_DIST"); + + int maxDist = calculateDistanceField(chf, src); + chf.maxDistance = maxDist; + + ctx.stopTimer("DISTANCEFIELD_DIST"); + + ctx.startTimer("DISTANCEFIELD_BLUR"); + + // Blur + src = boxBlur(chf, 1, src); + + // Store distance. + chf.dist = src; + + ctx.stopTimer("DISTANCEFIELD_BLUR"); + + ctx.stopTimer("DISTANCEFIELD"); + + } + + private static void paintRectRegion(int minx, int maxx, int miny, int maxy, int regId, CompactHeightfield chf, + int[] srcReg) { + int w = chf.width; + for (int y = miny; y < maxy; ++y) { + for (int x = minx; x < maxx; ++x) { + CompactCell c = chf.cells[x + y * w]; + for (int i = c.index, ni = c.index + c.count; i < ni; ++i) { + if (chf.areas[i] != RC_NULL_AREA) { + srcReg[i] = regId; + } + } + } + } + } + + /// @par + /// + /// Non-null regions will consist of connected, non-overlapping walkable spans that form a single contour. + /// Contours will form simple polygons. + /// + /// If multiple regions form an area that is smaller than @p minRegionArea, then all spans will be + /// re-assigned to the zero (null) region. + /// + /// Partitioning can result in smaller than necessary regions. @p mergeRegionArea helps + /// reduce unecessarily small regions. + /// + /// See the #rcConfig documentation for more information on the configuration parameters. + /// + /// The region data will be available via the rcCompactHeightfield::maxRegions + /// and rcCompactSpan::reg fields. + /// + /// @warning The distance field must be created using #rcBuildDistanceField before attempting to build regions. + /// + /// @see rcCompactHeightfield, rcCompactSpan, rcBuildDistanceField, rcBuildRegionsMonotone, rcConfig + public static void buildRegionsMonotone(Telemetry ctx, CompactHeightfield chf, int minRegionArea, + int mergeRegionArea) { + ctx.startTimer("REGIONS"); + + int w = chf.width; + int h = chf.height; + int borderSize = chf.borderSize; + int id = 1; + + int[] srcReg = new int[chf.spanCount]; + + int nsweeps = Math.Max(chf.width, chf.height); + SweepSpan[] sweeps = new SweepSpan[nsweeps]; + for (int i = 0; i < sweeps.Length; i++) { + sweeps[i] = new SweepSpan(); + } + + // Mark border regions. + if (borderSize > 0) { + // Make sure border will not overflow. + int bw = Math.Min(w, borderSize); + int bh = Math.Min(h, borderSize); + // Paint regions + paintRectRegion(0, bw, 0, h, id | RC_BORDER_REG, chf, srcReg); + id++; + paintRectRegion(w - bw, w, 0, h, id | RC_BORDER_REG, chf, srcReg); + id++; + paintRectRegion(0, w, 0, bh, id | RC_BORDER_REG, chf, srcReg); + id++; + paintRectRegion(0, w, h - bh, h, id | RC_BORDER_REG, chf, srcReg); + id++; + + } + + int[] prev = new int[1024]; + + // Sweep one line at a time. + for (int y = borderSize; y < h - borderSize; ++y) { + // Collect spans from this row. + if (prev.Length < id * 2) { + prev = new int[id * 2]; + } else { + Array.Fill(prev, 0, 0, (id) - (0)); + } + int rid = 1; + + for (int x = borderSize; x < w - borderSize; ++x) { + CompactCell c = chf.cells[x + y * w]; + + for (int i = c.index, ni = c.index + c.count; i < ni; ++i) { + CompactSpan s = chf.spans[i]; + if (chf.areas[i] == RC_NULL_AREA) { + continue; + } + + // -x + int previd = 0; + if (RecastCommon.GetCon(s, 0) != RC_NOT_CONNECTED) { + int ax = x + RecastCommon.GetDirOffsetX(0); + int ay = y + RecastCommon.GetDirOffsetY(0); + int ai = chf.cells[ax + ay * w].index + RecastCommon.GetCon(s, 0); + if ((srcReg[ai] & RC_BORDER_REG) == 0 && chf.areas[i] == chf.areas[ai]) { + previd = srcReg[ai]; + } + } + + if (previd == 0) { + previd = rid++; + sweeps[previd].rid = previd; + sweeps[previd].ns = 0; + sweeps[previd].nei = 0; + } + + // -y + if (RecastCommon.GetCon(s, 3) != RC_NOT_CONNECTED) { + int ax = x + RecastCommon.GetDirOffsetX(3); + int ay = y + RecastCommon.GetDirOffsetY(3); + int ai = chf.cells[ax + ay * w].index + RecastCommon.GetCon(s, 3); + if (srcReg[ai] != 0 && (srcReg[ai] & RC_BORDER_REG) == 0 && chf.areas[i] == chf.areas[ai]) { + int nr = srcReg[ai]; + if (sweeps[previd].nei == 0 || sweeps[previd].nei == nr) { + sweeps[previd].nei = nr; + sweeps[previd].ns++; + if (prev.Length <= nr) { + Array.Resize(ref prev, prev.Length * 2); + } + prev[nr]++; + } else { + sweeps[previd].nei = RC_NULL_NEI; + } + } + } + + srcReg[i] = previd; + } + } + + // Create unique ID. + for (int i = 1; i < rid; ++i) { + if (sweeps[i].nei != RC_NULL_NEI && sweeps[i].nei != 0 && prev[sweeps[i].nei] == sweeps[i].ns) { + sweeps[i].id = sweeps[i].nei; + } else { + sweeps[i].id = id++; + } + } + + // Remap IDs + for (int x = borderSize; x < w - borderSize; ++x) { + CompactCell c = chf.cells[x + y * w]; + + for (int i = c.index, ni = c.index + c.count; i < ni; ++i) { + if (srcReg[i] > 0 && srcReg[i] < rid) { + srcReg[i] = sweeps[srcReg[i]].id; + } + } + } + } + + ctx.startTimer("REGIONS_FILTER"); + + // Merge regions and filter out small regions. + List overlaps = new(); + chf.maxRegions = mergeAndFilterRegions(ctx, minRegionArea, mergeRegionArea, id, chf, srcReg, overlaps); + + // Monotone partitioning does not generate overlapping regions. + + ctx.stopTimer("REGIONS_FILTER"); + + // Store the result out. + for (int i = 0; i < chf.spanCount; ++i) { + chf.spans[i].reg = srcReg[i]; + } + + ctx.stopTimer("REGIONS"); + + } + + /// @par + /// + /// Non-null regions will consist of connected, non-overlapping walkable spans that form a single contour. + /// Contours will form simple polygons. + /// + /// If multiple regions form an area that is smaller than @p minRegionArea, then all spans will be + /// re-assigned to the zero (null) region. + /// + /// Watershed partitioning can result in smaller than necessary regions, especially in diagonal corridors. + /// @p mergeRegionArea helps reduce unecessarily small regions. + /// + /// See the #rcConfig documentation for more information on the configuration parameters. + /// + /// The region data will be available via the rcCompactHeightfield::maxRegions + /// and rcCompactSpan::reg fields. + /// + /// @warning The distance field must be created using #rcBuildDistanceField before attempting to build regions. + /// + /// @see rcCompactHeightfield, rcCompactSpan, rcBuildDistanceField, rcBuildRegionsMonotone, rcConfig + public static void buildRegions(Telemetry ctx, CompactHeightfield chf, int minRegionArea, + int mergeRegionArea) { + ctx.startTimer("REGIONS"); + + int w = chf.width; + int h = chf.height; + int borderSize = chf.borderSize; + + ctx.startTimer("REGIONS_WATERSHED"); + + int LOG_NB_STACKS = 3; + int NB_STACKS = 1 << LOG_NB_STACKS; + List> lvlStacks = new(); + for (int i = 0; i < NB_STACKS; ++i) { + lvlStacks.Add(new (1024)); + } + + List stack = new(1024); + + int[] srcReg = new int[chf.spanCount]; + int[] srcDist = new int[chf.spanCount]; + + int regionId = 1; + int level = (chf.maxDistance + 1) & ~1; + + // TODO: Figure better formula, expandIters defines how much the + // watershed "overflows" and simplifies the regions. Tying it to + // agent radius was usually good indication how greedy it could be. + // readonly int expandIters = 4 + walkableRadius * 2; + int expandIters = 8; + + if (borderSize > 0) { + // Make sure border will not overflow. + int bw = Math.Min(w, borderSize); + int bh = Math.Min(h, borderSize); + // Paint regions + paintRectRegion(0, bw, 0, h, regionId | RC_BORDER_REG, chf, srcReg); + regionId++; + paintRectRegion(w - bw, w, 0, h, regionId | RC_BORDER_REG, chf, srcReg); + regionId++; + paintRectRegion(0, w, 0, bh, regionId | RC_BORDER_REG, chf, srcReg); + regionId++; + paintRectRegion(0, w, h - bh, h, regionId | RC_BORDER_REG, chf, srcReg); + regionId++; + + } + + chf.borderSize = borderSize; + + int sId = -1; + while (level > 0) { + level = level >= 2 ? level - 2 : 0; + sId = (sId + 1) & (NB_STACKS - 1); + + // ctx->startTimer(RC_TIMER_DIVIDE_TO_LEVELS); + + if (sId == 0) { + sortCellsByLevel(level, chf, srcReg, NB_STACKS, lvlStacks, 1); + } else { + appendStacks(lvlStacks[sId - 1], lvlStacks[sId], srcReg); // copy left overs from last level + } + + // ctx->stopTimer(RC_TIMER_DIVIDE_TO_LEVELS); + + ctx.startTimer("REGIONS_EXPAND"); + + // Expand current regions until no empty connected cells found. + expandRegions(expandIters, level, chf, srcReg, srcDist, lvlStacks[sId], false); + + ctx.stopTimer("REGIONS_EXPAND"); + + ctx.startTimer("REGIONS_FLOOD"); + + // Mark new regions with IDs. + for (int j = 0; j < lvlStacks[sId].Count; j += 3) { + int x = lvlStacks[sId][j]; + int y = lvlStacks[sId][j + 1]; + int i = lvlStacks[sId][j + 2]; + if (i >= 0 && srcReg[i] == 0) { + if (floodRegion(x, y, i, level, regionId, chf, srcReg, srcDist, stack)) { + regionId++; + } + } + } + + ctx.stopTimer("REGIONS_FLOOD"); + } + + // Expand current regions until no empty connected cells found. + expandRegions(expandIters * 8, 0, chf, srcReg, srcDist, stack, true); + + ctx.stopTimer("REGIONS_WATERSHED"); + + ctx.startTimer("REGIONS_FILTER"); + + // Merge regions and filter out smalle regions. + List overlaps = new(); + chf.maxRegions = mergeAndFilterRegions(ctx, minRegionArea, mergeRegionArea, regionId, chf, srcReg, overlaps); + + // If overlapping regions were found during merging, split those regions. + if (overlaps.Count > 0) { + ctx.warn("rcBuildRegions: " + overlaps.Count + " overlapping regions."); + } + + ctx.stopTimer("REGIONS_FILTER"); + + // Write the result out. + for (int i = 0; i < chf.spanCount; ++i) { + chf.spans[i].reg = srcReg[i]; + } + + ctx.stopTimer("REGIONS"); + + } + + public static void buildLayerRegions(Telemetry ctx, CompactHeightfield chf, int minRegionArea) { + + ctx.startTimer("REGIONS"); + + int w = chf.width; + int h = chf.height; + int borderSize = chf.borderSize; + int id = 1; + + int[] srcReg = new int[chf.spanCount]; + int nsweeps = Math.Max(chf.width, chf.height); + SweepSpan[] sweeps = new SweepSpan[nsweeps]; + for (int i = 0; i < sweeps.Length; i++) { + sweeps[i] = new SweepSpan(); + } + + // Mark border regions. + if (borderSize > 0) { + // Make sure border will not overflow. + int bw = Math.Min(w, borderSize); + int bh = Math.Min(h, borderSize); + // Paint regions + paintRectRegion(0, bw, 0, h, id | RC_BORDER_REG, chf, srcReg); + id++; + paintRectRegion(w - bw, w, 0, h, id | RC_BORDER_REG, chf, srcReg); + id++; + paintRectRegion(0, w, 0, bh, id | RC_BORDER_REG, chf, srcReg); + id++; + paintRectRegion(0, w, h - bh, h, id | RC_BORDER_REG, chf, srcReg); + id++; + + } + + int[] prev = new int[1024]; + + // Sweep one line at a time. + for (int y = borderSize; y < h - borderSize; ++y) { + // Collect spans from this row. + if (prev.Length <= id * 2) { + prev = new int[id * 2]; + } else { + Array.Fill(prev, 0, 0, (id) - (0)); + } + int rid = 1; + + for (int x = borderSize; x < w - borderSize; ++x) { + CompactCell c = chf.cells[x + y * w]; + + for (int i = c.index, ni = c.index + c.count; i < ni; ++i) { + CompactSpan s = chf.spans[i]; + if (chf.areas[i] == RC_NULL_AREA) { + continue; + } + + // -x + int previd = 0; + if (RecastCommon.GetCon(s, 0) != RC_NOT_CONNECTED) { + int ax = x + RecastCommon.GetDirOffsetX(0); + int ay = y + RecastCommon.GetDirOffsetY(0); + int ai = chf.cells[ax + ay * w].index + RecastCommon.GetCon(s, 0); + if ((srcReg[ai] & RC_BORDER_REG) == 0 && chf.areas[i] == chf.areas[ai]) { + previd = srcReg[ai]; + } + } + + if (previd == 0) { + previd = rid++; + sweeps[previd].rid = previd; + sweeps[previd].ns = 0; + sweeps[previd].nei = 0; + } + + // -y + if (RecastCommon.GetCon(s, 3) != RC_NOT_CONNECTED) { + int ax = x + RecastCommon.GetDirOffsetX(3); + int ay = y + RecastCommon.GetDirOffsetY(3); + int ai = chf.cells[ax + ay * w].index + RecastCommon.GetCon(s, 3); + if (srcReg[ai] != 0 && (srcReg[ai] & RC_BORDER_REG) == 0 && chf.areas[i] == chf.areas[ai]) { + int nr = srcReg[ai]; + if (sweeps[previd].nei == 0 || sweeps[previd].nei == nr) { + sweeps[previd].nei = nr; + sweeps[previd].ns++; + if (prev.Length <= nr) { + Array.Resize(ref prev, prev.Length * 2); + } + prev[nr]++; + } else { + sweeps[previd].nei = RC_NULL_NEI; + } + } + } + + srcReg[i] = previd; + } + } + + // Create unique ID. + for (int i = 1; i < rid; ++i) { + if (sweeps[i].nei != RC_NULL_NEI && sweeps[i].nei != 0 && prev[sweeps[i].nei] == sweeps[i].ns) { + sweeps[i].id = sweeps[i].nei; + } else { + sweeps[i].id = id++; + } + } + + // Remap IDs + for (int x = borderSize; x < w - borderSize; ++x) { + CompactCell c = chf.cells[x + y * w]; + + for (int i = c.index, ni = c.index + c.count; i < ni; ++i) { + if (srcReg[i] > 0 && srcReg[i] < rid) { + srcReg[i] = sweeps[srcReg[i]].id; + } + } + } + } + + ctx.startTimer("REGIONS_FILTER"); + + // Merge monotone regions to layers and remove small regions. + List overlaps = new(); + chf.maxRegions = mergeAndFilterLayerRegions(ctx, minRegionArea, id, chf, srcReg, overlaps); + + ctx.stopTimer("REGIONS_FILTER"); + + // Store the result out. + for (int i = 0; i < chf.spanCount; ++i) { + chf.spans[i].reg = srcReg[i]; + } + + ctx.stopTimer("REGIONS"); + + } +} diff --git a/src/DotRecast.Recast/RecastVectors.cs b/src/DotRecast.Recast/RecastVectors.cs new file mode 100644 index 0000000..619d45f --- /dev/null +++ b/src/DotRecast.Recast/RecastVectors.cs @@ -0,0 +1,97 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; + +namespace DotRecast.Recast; + +public static class RecastVectors +{ + public static void min(float[] a, float[] b, int i) + { + a[0] = Math.Min(a[0], b[i + 0]); + a[1] = Math.Min(a[1], b[i + 1]); + a[2] = Math.Min(a[2], b[i + 2]); + } + + public static void max(float[] a, float[] b, int i) + { + a[0] = Math.Max(a[0], b[i + 0]); + a[1] = Math.Max(a[1], b[i + 1]); + a[2] = Math.Max(a[2], b[i + 2]); + } + + public static void copy(float[] @out, float[] @in, int i) + { + copy(@out, 0, @in, i); + } + + public static void copy(float[] @out, float[] @in) + { + copy(@out, 0, @in, 0); + } + + public static void copy(float[] @out, int n, float[] @in, int m) + { + @out[n] = @in[m]; + @out[n + 1] = @in[m + 1]; + @out[n + 2] = @in[m + 2]; + } + + public static void add(float[] e0, float[] a, float[] verts, int i) + { + e0[0] = a[0] + verts[i]; + e0[1] = a[1] + verts[i + 1]; + e0[2] = a[2] + verts[i + 2]; + } + + public static void sub(float[] e0, float[] verts, int i, int j) + { + e0[0] = verts[i] - verts[j]; + e0[1] = verts[i + 1] - verts[j + 1]; + e0[2] = verts[i + 2] - verts[j + 2]; + } + + public static void sub(float[] e0, float[] i, float[] verts, int j) + { + e0[0] = i[0] - verts[j]; + e0[1] = i[1] - verts[j + 1]; + e0[2] = i[2] - verts[j + 2]; + } + + public static void cross(float[] dest, float[] v1, float[] v2) + { + dest[0] = v1[1] * v2[2] - v1[2] * v2[1]; + dest[1] = v1[2] * v2[0] - v1[0] * v2[2]; + dest[2] = v1[0] * v2[1] - v1[1] * v2[0]; + } + + public static void normalize(float[] v) + { + float d = (float)(1.0f / Math.Sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2])); + v[0] *= d; + v[1] *= d; + v[2] *= d; + } + + public static float dot(float[] v1, float[] v2) + { + return v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2]; + } +} \ No newline at end of file diff --git a/src/DotRecast.Recast/RecastVoxelization.cs b/src/DotRecast.Recast/RecastVoxelization.cs new file mode 100644 index 0000000..2f12ecf --- /dev/null +++ b/src/DotRecast.Recast/RecastVoxelization.cs @@ -0,0 +1,73 @@ +/* +recast4j copyright (c) 2021 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.Collections.Generic; +using DotRecast.Recast.Geom; + +namespace DotRecast.Recast; + +public class RecastVoxelization { + + public static Heightfield buildSolidHeightfield(InputGeomProvider geomProvider, RecastBuilderConfig builderCfg, + Telemetry ctx) { + RecastConfig cfg = builderCfg.cfg; + + // Allocate voxel heightfield where we rasterize our input data to. + Heightfield solid = new Heightfield(builderCfg.width, builderCfg.height, builderCfg.bmin, builderCfg.bmax, cfg.cs, + cfg.ch, cfg.borderSize); + + // Allocate array that can hold triangle area types. + // If you have multiple meshes you need to process, allocate + // and array which can hold the max number of triangles you need to + // process. + + // Find triangles which are walkable based on their slope and rasterize + // them. + // If your input data is multiple meshes, you can transform them here, + // calculate + // the are type for each of the meshes and rasterize them. + foreach (TriMesh geom in geomProvider.meshes()) { + float[] verts = geom.getVerts(); + if (cfg.useTiles) { + float[] tbmin = new float[2]; + float[] tbmax = new float[2]; + tbmin[0] = builderCfg.bmin[0]; + tbmin[1] = builderCfg.bmin[2]; + tbmax[0] = builderCfg.bmax[0]; + tbmax[1] = builderCfg.bmax[2]; + List nodes = geom.getChunksOverlappingRect(tbmin, tbmax); + foreach (ChunkyTriMeshNode node in nodes) { + int[] tris = node.tris; + int ntris = tris.Length / 3; + int[] m_triareas = Recast.markWalkableTriangles(ctx, cfg.walkableSlopeAngle, verts, tris, ntris, + cfg.walkableAreaMod); + RecastRasterization.rasterizeTriangles(solid, verts, tris, m_triareas, ntris, cfg.walkableClimb, ctx); + } + } else { + int[] tris = geom.getTris(); + int ntris = tris.Length / 3; + int[] m_triareas = Recast.markWalkableTriangles(ctx, cfg.walkableSlopeAngle, verts, tris, ntris, + cfg.walkableAreaMod); + RecastRasterization.rasterizeTriangles(solid, verts, tris, m_triareas, ntris, cfg.walkableClimb, ctx); + } + } + + return solid; + } + +} diff --git a/src/DotRecast.Recast/Span.cs b/src/DotRecast.Recast/Span.cs new file mode 100644 index 0000000..7a738e7 --- /dev/null +++ b/src/DotRecast.Recast/Span.cs @@ -0,0 +1,33 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ +namespace DotRecast.Recast; + +/** Represents a span in a heightfield. */ +public class Span { + + /** The lower limit of the span. [Limit: < smax] */ + public int smin; + /** The upper limit of the span. [Limit: <= SPAN_MAX_HEIGHT] */ + public int smax; + /** The area id assigned to the span. */ + public int area; + /** The next span higher up in column. */ + public Span next; + +} diff --git a/src/DotRecast.Recast/Telemetry.cs b/src/DotRecast.Recast/Telemetry.cs new file mode 100644 index 0000000..ee82460 --- /dev/null +++ b/src/DotRecast.Recast/Telemetry.cs @@ -0,0 +1,57 @@ +/* +recast4j copyright (c) 2021 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using DotRecast.Core; + +namespace DotRecast.Recast; + +public class Telemetry +{ + private readonly ThreadLocal> timerStart = new(() => new Dictionary()); + private readonly ConcurrentDictionary timerAccum = new(); + + public void startTimer(string name) + { + timerStart.Value[name] = new AtomicLong(Stopwatch.GetTimestamp()); + } + + public void stopTimer(string name) + { + timerAccum + .GetOrAdd(name, _ => new AtomicLong(0)) + .AddAndGet(Stopwatch.GetTimestamp() - timerStart.Value?[name].Read() ?? 0); + } + + public void warn(string @string) + { + Console.WriteLine(@string); + } + + public void print() + { + foreach (var (n, v) in timerAccum) + { + Console.WriteLine(n + ": " + v.Read() / 1000000); + } + } +} \ No newline at end of file diff --git a/test/DotRecast.Detour.Crowd.Test/AbstractCrowdTest.cs b/test/DotRecast.Detour.Crowd.Test/AbstractCrowdTest.cs new file mode 100644 index 0000000..3c7a0b9 --- /dev/null +++ b/test/DotRecast.Detour.Crowd.Test/AbstractCrowdTest.cs @@ -0,0 +1,149 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.Collections.Generic; +using NUnit.Framework; + +namespace DotRecast.Detour.Crowd.Test; + +using static DetourCommon; + +public class AbstractCrowdTest { + + protected readonly long[] startRefs = { 281474976710696L, 281474976710773L, 281474976710680L, 281474976710753L, + 281474976710733L }; + + protected readonly long[] endRefs = { 281474976710721L, 281474976710767L, 281474976710758L, 281474976710731L, 281474976710772L }; + + protected readonly float[][] startPoss = { + new[] { 22.60652f, 10.197294f, -45.918674f }, + new[] { 22.331268f, 10.197294f, -1.0401875f }, + new[] { 18.694363f, 15.803535f, -73.090416f }, + new[] { 0.7453353f, 10.197294f, -5.94005f }, + new[] { -20.651257f, 5.904126f, -13.712508f } }; + + protected readonly float[][] endPoss = { + new[] { 6.4576626f, 10.197294f, -18.33406f }, + new[] { -5.8023443f, 0.19729415f, 3.008419f }, + new[] { 38.423977f, 10.197294f, -0.116066754f }, + new[] { 0.8635526f, 10.197294f, -10.31032f }, + new[] { 18.784092f, 10.197294f, 3.0543678f } }; + + protected MeshData nmd; + protected NavMeshQuery query; + protected NavMesh navmesh; + protected Crowd crowd; + protected List agents; + + [SetUp] + public void setUp() { + nmd = new RecastTestMeshBuilder().getMeshData(); + navmesh = new NavMesh(nmd, 6, 0); + query = new NavMeshQuery(navmesh); + CrowdConfig config = new CrowdConfig(0.6f); + crowd = new Crowd(config, navmesh); + ObstacleAvoidanceQuery.ObstacleAvoidanceParams option = new ObstacleAvoidanceQuery.ObstacleAvoidanceParams(); + option.velBias = 0.5f; + option.adaptiveDivs = 5; + option.adaptiveRings = 2; + option.adaptiveDepth = 1; + crowd.setObstacleAvoidanceParams(0, option); + option = new ObstacleAvoidanceQuery.ObstacleAvoidanceParams(); + option.velBias = 0.5f; + option.adaptiveDivs = 5; + option.adaptiveRings = 2; + option.adaptiveDepth = 2; + crowd.setObstacleAvoidanceParams(1, option); + option = new ObstacleAvoidanceQuery.ObstacleAvoidanceParams(); + option.velBias = 0.5f; + option.adaptiveDivs = 7; + option.adaptiveRings = 2; + option.adaptiveDepth = 3; + crowd.setObstacleAvoidanceParams(2, option); + option = new ObstacleAvoidanceQuery.ObstacleAvoidanceParams(); + option.velBias = 0.5f; + option.adaptiveDivs = 7; + option.adaptiveRings = 3; + option.adaptiveDepth = 3; + crowd.setObstacleAvoidanceParams(3, option); + agents = new(); + } + + protected CrowdAgentParams getAgentParams(int updateFlags, int obstacleAvoidanceType) { + CrowdAgentParams ap = new CrowdAgentParams(); + ap.radius = 0.6f; + ap.height = 2f; + ap.maxAcceleration = 8.0f; + ap.maxSpeed = 3.5f; + ap.collisionQueryRange = ap.radius * 12f; + ap.pathOptimizationRange = ap.radius * 30f; + ap.updateFlags = updateFlags; + ap.obstacleAvoidanceType = obstacleAvoidanceType; + ap.separationWeight = 2f; + return ap; + } + + protected void addAgentGrid(int size, float distance, int updateFlags, int obstacleAvoidanceType, float[] startPos) { + CrowdAgentParams ap = getAgentParams(updateFlags, obstacleAvoidanceType); + for (int i = 0; i < size; i++) { + for (int j = 0; j < size; j++) { + float[] pos = new float[3]; + pos[0] = startPos[0] + i * distance; + pos[1] = startPos[1]; + pos[2] = startPos[2] + j * distance; + agents.Add(crowd.addAgent(pos, ap)); + } + } + } + + protected void setMoveTarget(float[] pos, bool adjust) { + float[] ext = crowd.getQueryExtents(); + QueryFilter filter = crowd.getFilter(0); + if (adjust) { + foreach (CrowdAgent ag in crowd.getActiveAgents()) { + float[] vel = calcVel(ag.npos, pos, ag.option.maxSpeed); + crowd.requestMoveVelocity(ag, vel); + } + } else { + Result nearest = query.findNearestPoly(pos, ext, filter); + foreach (CrowdAgent ag in crowd.getActiveAgents()) { + crowd.requestMoveTarget(ag, nearest.result.getNearestRef(), nearest.result.getNearestPos()); + } + } + } + + protected float[] calcVel(float[] pos, float[] tgt, float speed) { + float[] vel = vSub(tgt, pos); + vel[1] = 0.0f; + vNormalize(vel); + vel = vScale(vel, speed); + return vel; + } + + protected void dumpActiveAgents(int i) { + Console.WriteLine(crowd.getActiveAgents().Count); + foreach (CrowdAgent ag in crowd.getActiveAgents()) { + Console.WriteLine(ag.state + ", " + ag.targetState); + Console.WriteLine(ag.npos[0] + ", " + ag.npos[1] + ", " + ag.npos[2]); + Console.WriteLine(ag.nvel[0] + ", " + ag.nvel[1] + ", " + ag.nvel[2]); + } + } + +} diff --git a/test/DotRecast.Detour.Crowd.Test/Crowd1Test.cs b/test/DotRecast.Detour.Crowd.Test/Crowd1Test.cs new file mode 100644 index 0000000..20ba66e --- /dev/null +++ b/test/DotRecast.Detour.Crowd.Test/Crowd1Test.cs @@ -0,0 +1,706 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using NUnit.Framework; + +namespace DotRecast.Detour.Crowd.Test; + +public class Crowd1Test : AbstractCrowdTest { + + static readonly float[][] EXPECTED_A1Q0TVTA = { + new[] { 22.322426f, 10.197294f, -45.771397f, -3.107285f, 0.000000f, 1.610831f }, + new[] { 21.754303f, 10.197294f, -45.476715f, -3.106887f, 0.000000f, 1.611599f }, + new[] { 21.133097f, 10.197294f, -45.154068f, -3.106033f, 0.000000f, 1.613245f }, + new[] { 20.512094f, 10.197294f, -44.831028f, -3.105014f, 0.000000f, 1.615205f }, + new[] { 19.891315f, 10.197294f, -44.507557f, -3.103898f, 0.000000f, 1.617350f }, + new[] { 19.270782f, 10.197294f, -44.183617f, -3.102670f, 0.000000f, 1.619704f }, + new[] { 18.650520f, 10.197294f, -43.859158f, -3.101315f, 0.000000f, 1.622296f }, + new[] { 18.030558f, 10.197294f, -43.534126f, -3.099816f, 0.000000f, 1.625158f }, + new[] { 17.410927f, 10.197294f, -43.208462f, -3.098152f, 0.000000f, 1.628329f }, + new[] { 16.791668f, 10.197294f, -42.882092f, -3.096297f, 0.000000f, 1.631854f }, + new[] { 16.172823f, 10.197294f, -42.554935f, -3.094221f, 0.000000f, 1.635786f }, + new[] { 15.554445f, 10.197294f, -42.226898f, -3.091888f, 0.000000f, 1.640191f }, + new[] { 14.936594f, 10.197294f, -41.897869f, -3.089255f, 0.000000f, 1.645145f }, + new[] { 14.319340f, 10.197294f, -41.567722f, -3.086269f, 0.000000f, 1.650741f }, + new[] { 13.702767f, 10.197294f, -41.236305f, -3.082862f, 0.000000f, 1.657094f }, + new[] { 13.086977f, 10.197294f, -40.903435f, -3.078954f, 0.000000f, 1.664345f }, + new[] { 12.472089f, 10.197294f, -40.568901f, -3.074442f, 0.000000f, 1.672665f }, + new[] { 11.858250f, 10.197294f, -40.232449f, -3.069196f, 0.000000f, 1.682271f }, + new[] { 11.245640f, 10.197294f, -39.893761f, -3.063048f, 0.000000f, 1.693439f }, + new[] { 10.634483f, 10.197294f, -39.552460f, -3.055783f, 0.000000f, 1.706514f }, + new[] { 10.025061f, 10.197294f, -39.208069f, -3.047112f, 0.000000f, 1.721948f }, + new[] { 9.417730f, 10.197294f, -38.860004f, -3.036654f, 0.000000f, 1.740326f }, + new[] { 8.812954f, 10.197294f, -38.507519f, -3.023880f, 0.000000f, 1.762429f }, + new[] { 8.211343f, 10.197294f, -38.149658f, -3.008055f, 0.000000f, 1.789302f }, + new[] { 7.613717f, 10.197294f, -37.785183f, -2.988129f, 0.000000f, 1.822385f }, + new[] { 7.021209f, 10.197294f, -37.412445f, -2.962540f, 0.000000f, 1.863695f }, + new[] { 6.435429f, 10.197294f, -37.029221f, -2.928901f, 0.000000f, 1.916126f }, + new[] { 5.858753f, 10.197294f, -36.632427f, -2.883380f, 0.000000f, 1.983965f }, + new[] { 5.294861f, 10.197294f, -36.217667f, -2.819461f, 0.000000f, 2.073798f }, + new[] { 4.749827f, 10.197294f, -35.778419f, -2.725170f, 0.000000f, 2.196236f }, + new[] { 4.234636f, 10.197294f, -35.304523f, -2.575954f, 0.000000f, 2.369486f }, + new[] { 3.772264f, 10.197294f, -34.778965f, -2.311863f, 0.000000f, 2.627792f }, + new[] { 3.428036f, 10.197294f, -34.169453f, -1.721137f, 0.000000f, 3.047571f }, + new[] { 3.033963f, 10.197294f, -33.590916f, -1.970364f, 0.000000f, 2.892692f }, + new[] { 2.656040f, 10.197294f, -33.001701f, -1.889616f, 0.000000f, 2.946074f }, + new[] { 2.307335f, 10.197294f, -32.394737f, -1.743529f, 0.000000f, 3.034816f }, + new[] { 2.021275f, 10.197294f, -31.755856f, -1.430300f, 0.000000f, 3.194408f }, + new[] { 1.935164f, 10.197294f, -31.061172f, -0.430555f, 0.000000f, 3.473417f }, + new[] { 1.847493f, 10.197294f, -30.366684f, -0.438351f, 0.000000f, 3.472441f }, + new[] { 1.943488f, 10.197294f, -29.673298f, 0.479975f, 0.000000f, 3.466933f }, + new[] { 2.170555f, 10.197294f, -29.011150f, 1.135336f, 0.000000f, 3.310742f }, + new[] { 2.415170f, 10.197294f, -28.355282f, 1.223072f, 0.000000f, 3.279344f }, + new[] { 2.677041f, 10.197294f, -27.706110f, 1.309357f, 0.000000f, 3.245857f }, + new[] { 2.938912f, 10.197294f, -27.056938f, 1.309357f, 0.000000f, 3.245857f }, + new[] { 3.200784f, 10.197294f, -26.407766f, 1.309357f, 0.000000f, 3.245856f }, + new[] { 3.462655f, 10.197294f, -25.758595f, 1.309357f, 0.000000f, 3.245857f }, + new[] { 3.724526f, 10.197294f, -25.109423f, 1.309357f, 0.000000f, 3.245857f }, + new[] { 3.986398f, 10.197294f, -24.460251f, 1.309357f, 0.000000f, 3.245857f }, + new[] { 4.248269f, 10.197294f, -23.811079f, 1.309357f, 0.000000f, 3.245856f }, + new[] { 4.510140f, 10.197294f, -23.161907f, 1.309357f, 0.000000f, 3.245856f }, + new[] { 4.772012f, 10.197294f, -22.512735f, 1.309357f, 0.000000f, 3.245856f }, + new[] { 5.033883f, 10.197294f, -21.863564f, 1.309358f, 0.000000f, 3.245856f }, + new[] { 5.295755f, 10.197294f, -21.214392f, 1.309358f, 0.000000f, 3.245856f }, + new[] { 5.557627f, 10.197294f, -20.565220f, 1.309358f, 0.000000f, 3.245856f }, + new[] { 5.819499f, 10.197294f, -19.916048f, 1.309358f, 0.000000f, 3.245856f }, + new[] { 6.081370f, 10.197294f, -19.266876f, 1.309358f, 0.000000f, 3.245856f }, + new[] { 6.322058f, 10.197294f, -18.670219f, 1.203439f, 0.000000f, 2.983284f }, + new[] { 6.372280f, 10.197294f, -18.330706f, -0.014387f, 0.000000f, 1.339125f }, + new[] { 6.398773f, 10.197294f, -18.310312f, 0.124516f, 0.000000f, -0.004893f }, + new[] { 6.415949f, 10.197294f, -18.317240f, 0.085881f, 0.000000f, -0.034633f }, + new[] { 6.428115f, 10.197294f, -18.322145f, 0.060833f, 0.000000f, -0.024530f }, + new[] { 6.436733f, 10.197294f, -18.325621f, 0.043090f, 0.000000f, -0.017376f }, + new[] { 6.442838f, 10.197294f, -18.328083f, 0.030522f, 0.000000f, -0.012308f }, + new[] { 6.447162f, 10.197294f, -18.329826f, 0.021620f, 0.000000f, -0.008717f }, + new[] { 6.450224f, 10.197294f, -18.331062f, 0.015314f, 0.000000f, -0.006175f }, + new[] { 6.450224f, 10.197294f, -18.331062f, 0.000000f, 0.000000f, 0.000000f } }; + + static readonly float[][] EXPECTED_A1Q0TVT = { + new[] { 22.322426f, 10.197294f, -45.771397f, -3.107285f, 0.000000f, 1.610831f }, + new[] { 21.754303f, 10.197294f, -45.476715f, -3.106887f, 0.000000f, 1.611599f }, + new[] { 21.133097f, 10.197294f, -45.154068f, -3.106033f, 0.000000f, 1.613245f }, + new[] { 20.512094f, 10.197294f, -44.831028f, -3.105014f, 0.000000f, 1.615205f }, + new[] { 19.891315f, 10.197294f, -44.507557f, -3.103898f, 0.000000f, 1.617350f }, + new[] { 19.270782f, 10.197294f, -44.183617f, -3.102670f, 0.000000f, 1.619704f }, + new[] { 18.650520f, 10.197294f, -43.859158f, -3.101315f, 0.000000f, 1.622296f }, + new[] { 18.030558f, 10.197294f, -43.534126f, -3.099816f, 0.000000f, 1.625158f }, + new[] { 17.410927f, 10.197294f, -43.208462f, -3.098152f, 0.000000f, 1.628329f }, + new[] { 16.791668f, 10.197294f, -42.882092f, -3.096297f, 0.000000f, 1.631854f }, + new[] { 16.172823f, 10.197294f, -42.554935f, -3.094221f, 0.000000f, 1.635786f }, + new[] { 15.554445f, 10.197294f, -42.226898f, -3.091888f, 0.000000f, 1.640191f }, + new[] { 14.936594f, 10.197294f, -41.897869f, -3.089255f, 0.000000f, 1.645145f }, + new[] { 14.319340f, 10.197294f, -41.567722f, -3.086269f, 0.000000f, 1.650741f }, + new[] { 13.702767f, 10.197294f, -41.236305f, -3.082862f, 0.000000f, 1.657094f }, + new[] { 13.086977f, 10.197294f, -40.903435f, -3.078953f, 0.000000f, 1.664345f }, + new[] { 12.472089f, 10.197294f, -40.568901f, -3.074442f, 0.000000f, 1.672665f }, + new[] { 11.858250f, 10.197294f, -40.232449f, -3.069196f, 0.000000f, 1.682271f }, + new[] { 11.245640f, 10.197294f, -39.893761f, -3.063048f, 0.000000f, 1.693439f }, + new[] { 10.634483f, 10.197294f, -39.552460f, -3.055783f, 0.000000f, 1.706514f }, + new[] { 10.025061f, 10.197294f, -39.208069f, -3.047112f, 0.000000f, 1.721948f }, + new[] { 9.417730f, 10.197294f, -38.860004f, -3.036654f, 0.000000f, 1.740326f }, + new[] { 8.812955f, 10.197294f, -38.507519f, -3.023879f, 0.000000f, 1.762429f }, + new[] { 8.211344f, 10.197294f, -38.149658f, -3.008055f, 0.000000f, 1.789302f }, + new[] { 7.613718f, 10.197294f, -37.785183f, -2.988129f, 0.000000f, 1.822385f }, + new[] { 7.021210f, 10.197294f, -37.412445f, -2.962540f, 0.000000f, 1.863695f }, + new[] { 6.435430f, 10.197294f, -37.029221f, -2.928901f, 0.000000f, 1.916126f }, + new[] { 5.858754f, 10.197294f, -36.632427f, -2.883381f, 0.000000f, 1.983965f }, + new[] { 5.294862f, 10.197294f, -36.217667f, -2.819461f, 0.000000f, 2.073798f }, + new[] { 4.749828f, 10.197294f, -35.778419f, -2.725170f, 0.000000f, 2.196235f }, + new[] { 4.234637f, 10.197294f, -35.304523f, -2.575955f, 0.000000f, 2.369484f }, + new[] { 3.772264f, 10.197294f, -34.778965f, -2.311864f, 0.000000f, 2.627791f }, + new[] { 3.428036f, 10.197294f, -34.169453f, -1.721139f, 0.000000f, 3.047570f }, + new[] { 3.033963f, 10.197294f, -33.590916f, -1.970364f, 0.000000f, 2.892692f }, + new[] { 2.656040f, 10.197294f, -33.001701f, -1.889616f, 0.000000f, 2.946074f }, + new[] { 2.307335f, 10.197294f, -32.394737f, -1.743529f, 0.000000f, 3.034816f }, + new[] { 2.021275f, 10.197294f, -31.755856f, -1.430300f, 0.000000f, 3.194408f }, + new[] { 1.935164f, 10.197294f, -31.061172f, -0.430555f, 0.000000f, 3.473417f }, + new[] { 1.847493f, 10.197294f, -30.366684f, -0.438351f, 0.000000f, 3.472441f }, + new[] { 1.943488f, 10.197294f, -29.673298f, 0.479975f, 0.000000f, 3.466933f }, + new[] { 2.170555f, 10.197294f, -29.011150f, 1.135336f, 0.000000f, 3.310742f }, + new[] { 2.415170f, 10.197294f, -28.355282f, 1.223072f, 0.000000f, 3.279344f }, + new[] { 2.677041f, 10.197294f, -27.706110f, 1.309357f, 0.000000f, 3.245857f }, + new[] { 2.938912f, 10.197294f, -27.056938f, 1.309357f, 0.000000f, 3.245857f }, + new[] { 3.200784f, 10.197294f, -26.407766f, 1.309357f, 0.000000f, 3.245856f }, + new[] { 3.462655f, 10.197294f, -25.758595f, 1.309357f, 0.000000f, 3.245857f }, + new[] { 3.724526f, 10.197294f, -25.109423f, 1.309357f, 0.000000f, 3.245857f }, + new[] { 3.986398f, 10.197294f, -24.460251f, 1.309357f, 0.000000f, 3.245857f }, + new[] { 4.248269f, 10.197294f, -23.811079f, 1.309357f, 0.000000f, 3.245856f }, + new[] { 4.510140f, 10.197294f, -23.161907f, 1.309357f, 0.000000f, 3.245856f }, + new[] { 4.772012f, 10.197294f, -22.512735f, 1.309358f, 0.000000f, 3.245856f }, + new[] { 5.033883f, 10.197294f, -21.863564f, 1.309358f, 0.000000f, 3.245856f }, + new[] { 5.295755f, 10.197294f, -21.214392f, 1.309358f, 0.000000f, 3.245856f }, + new[] { 5.557627f, 10.197294f, -20.565220f, 1.309358f, 0.000000f, 3.245856f }, + new[] { 5.819499f, 10.197294f, -19.916048f, 1.309358f, 0.000000f, 3.245856f }, + new[] { 6.081370f, 10.197294f, -19.266876f, 1.309358f, 0.000000f, 3.245856f }, + new[] { 6.300874f, 10.197294f, -18.722734f, 1.097519f, 0.000000f, 2.720711f }, + new[] { 6.400665f, 10.197294f, -18.475355f, 0.457299f, 0.000000f, 1.133632f }, + new[] { 6.433914f, 10.197294f, -18.392933f, 0.166243f, 0.000000f, 0.412109f }, + new[] { 6.447767f, 10.197294f, -18.358591f, 0.069268f, 0.000000f, 0.171711f }, + new[] { 6.453539f, 10.197294f, -18.344282f, 0.028861f, 0.000000f, 0.071547f }, + new[] { 6.455945f, 10.197294f, -18.338320f, 0.012026f, 0.000000f, 0.029813f }, + new[] { 6.455945f, 10.197294f, -18.338320f, 0.000000f, 0.000000f, 0.000000f } }; + + static readonly float[][] EXPECTED_A1Q0TV = { + new[] { 22.333418f, 10.197294f, -45.751896f, -2.987050f, 0.000000f, 1.824153f }, + new[] { 21.787214f, 10.197294f, -45.418335f, -2.987049f, 0.000000f, 1.824153f }, + new[] { 21.189804f, 10.197294f, -45.053505f, -2.987050f, 0.000000f, 1.824153f }, + new[] { 20.592394f, 10.197294f, -44.688675f, -2.987049f, 0.000000f, 1.824153f }, + new[] { 19.994984f, 10.197294f, -44.323845f, -2.987049f, 0.000000f, 1.824153f }, + new[] { 19.397573f, 10.197294f, -43.959015f, -2.987049f, 0.000000f, 1.824154f }, + new[] { 18.800163f, 10.197294f, -43.594185f, -2.987049f, 0.000000f, 1.824154f }, + new[] { 18.202753f, 10.197294f, -43.229355f, -2.987049f, 0.000000f, 1.824154f }, + new[] { 17.605343f, 10.197294f, -42.864525f, -2.987049f, 0.000000f, 1.824154f }, + new[] { 17.007933f, 10.197294f, -42.499695f, -2.987049f, 0.000000f, 1.824154f }, + new[] { 16.410522f, 10.197294f, -42.134865f, -2.987049f, 0.000000f, 1.824155f }, + new[] { 15.813112f, 10.197294f, -41.770035f, -2.987049f, 0.000000f, 1.824155f }, + new[] { 15.215703f, 10.197294f, -41.405205f, -2.987048f, 0.000000f, 1.824155f }, + new[] { 14.618294f, 10.197294f, -41.040375f, -2.987048f, 0.000000f, 1.824155f }, + new[] { 14.020885f, 10.197294f, -40.675545f, -2.987048f, 0.000000f, 1.824155f }, + new[] { 13.423475f, 10.197294f, -40.310715f, -2.987048f, 0.000000f, 1.824155f }, + new[] { 12.826066f, 10.197294f, -39.945885f, -2.987048f, 0.000000f, 1.824156f }, + new[] { 12.228657f, 10.197294f, -39.581055f, -2.987048f, 0.000000f, 1.824156f }, + new[] { 11.631248f, 10.197294f, -39.216225f, -2.987048f, 0.000000f, 1.824156f }, + new[] { 11.033838f, 10.197294f, -38.851395f, -2.987048f, 0.000000f, 1.824156f }, + new[] { 10.436429f, 10.197294f, -38.486565f, -2.987047f, 0.000000f, 1.824157f }, + new[] { 9.839020f, 10.197294f, -38.121735f, -2.987047f, 0.000000f, 1.824157f }, + new[] { 9.241611f, 10.197294f, -37.756905f, -2.987047f, 0.000000f, 1.824157f }, + new[] { 8.644201f, 10.197294f, -37.392075f, -2.987047f, 0.000000f, 1.824158f }, + new[] { 8.046792f, 10.197294f, -37.027245f, -2.987046f, 0.000000f, 1.824158f }, + new[] { 7.449383f, 10.197294f, -36.662415f, -2.987046f, 0.000000f, 1.824160f }, + new[] { 6.851974f, 10.197294f, -36.297581f, -2.987045f, 0.000000f, 1.824160f }, + new[] { 6.254564f, 10.197294f, -35.932751f, -2.987046f, 0.000000f, 1.824159f }, + new[] { 5.657155f, 10.197294f, -35.567917f, -2.987045f, 0.000000f, 1.824161f }, + new[] { 5.059746f, 10.197294f, -35.203087f, -2.987046f, 0.000000f, 1.824160f }, + new[] { 4.462337f, 10.197294f, -34.838253f, -2.987044f, 0.000000f, 1.824162f }, + new[] { 3.864928f, 10.197294f, -34.473423f, -2.987046f, 0.000000f, 1.824159f }, + new[] { 3.267520f, 10.197294f, -34.108589f, -2.987040f, 0.000000f, 1.824169f }, + new[] { 2.910351f, 10.197294f, -33.532368f, -1.429270f, 0.000000f, 3.194869f }, + new[] { 2.648150f, 10.197294f, -32.883331f, -1.311005f, 0.000000f, 3.245191f }, + new[] { 2.385949f, 10.197294f, -32.234291f, -1.311003f, 0.000000f, 3.245192f }, + new[] { 2.123748f, 10.197294f, -31.585253f, -1.311006f, 0.000000f, 3.245191f }, + new[] { 1.861547f, 10.197294f, -30.936214f, -1.311005f, 0.000000f, 3.245191f }, + new[] { 1.916189f, 10.197294f, -30.242338f, 0.359062f, 0.000000f, 3.481533f }, + new[] { 2.011134f, 10.197294f, -29.548807f, 0.474725f, 0.000000f, 3.467656f }, + new[] { 2.246398f, 10.197294f, -28.889526f, 1.176323f, 0.000000f, 3.296402f }, + new[] { 2.481663f, 10.197294f, -28.230246f, 1.176324f, 0.000000f, 3.296402f }, + new[] { 2.716928f, 10.197294f, -27.570965f, 1.176325f, 0.000000f, 3.296401f }, + new[] { 2.979683f, 10.197294f, -26.922150f, 1.313774f, 0.000000f, 3.244071f }, + new[] { 3.242438f, 10.197294f, -26.273335f, 1.313774f, 0.000000f, 3.244071f }, + new[] { 3.505193f, 10.197294f, -25.624521f, 1.313775f, 0.000000f, 3.244071f }, + new[] { 3.767948f, 10.197294f, -24.975708f, 1.313774f, 0.000000f, 3.244071f }, + new[] { 4.030703f, 10.197294f, -24.326893f, 1.313774f, 0.000000f, 3.244071f }, + new[] { 4.293458f, 10.197294f, -23.678078f, 1.313775f, 0.000000f, 3.244071f }, + new[] { 4.556212f, 10.197294f, -23.029263f, 1.313775f, 0.000000f, 3.244071f }, + new[] { 4.818967f, 10.197294f, -22.380449f, 1.313775f, 0.000000f, 3.244071f }, + new[] { 5.081722f, 10.197294f, -21.731636f, 1.313775f, 0.000000f, 3.244071f }, + new[] { 5.344477f, 10.197294f, -21.082821f, 1.313775f, 0.000000f, 3.244071f }, + new[] { 5.607232f, 10.197294f, -20.434008f, 1.313775f, 0.000000f, 3.244071f }, + new[] { 5.869987f, 10.197294f, -19.785194f, 1.313775f, 0.000000f, 3.244071f }, + new[] { 6.132742f, 10.197294f, -19.136379f, 1.313774f, 0.000000f, 3.244071f }, + new[] { 6.322279f, 10.197294f, -18.668360f, 0.947685f, 0.000000f, 2.340096f }, + new[] { 6.401253f, 10.197294f, -18.473352f, 0.394869f, 0.000000f, 0.975039f }, + new[] { 6.434158f, 10.197294f, -18.392099f, 0.164529f, 0.000000f, 0.406268f }, + new[] { 6.447869f, 10.197294f, -18.358244f, 0.068554f, 0.000000f, 0.169280f }, + new[] { 6.453582f, 10.197294f, -18.344137f, 0.028564f, 0.000000f, 0.070535f }, + new[] { 6.455962f, 10.197294f, -18.338259f, 0.011902f, 0.000000f, 0.029390f }, + new[] { 6.455962f, 10.197294f, -18.338259f, 0.000000f, 0.000000f, 0.000000f } }; + + static readonly float[][] EXPECTED_A1Q0T = { + new[] { 22.333418f, 10.197294f, -45.751896f, -2.987050f, 0.000000f, 1.824153f }, + new[] { 21.787214f, 10.197294f, -45.418335f, -2.987049f, 0.000000f, 1.824153f }, + new[] { 21.189804f, 10.197294f, -45.053505f, -2.987050f, 0.000000f, 1.824153f }, + new[] { 20.592394f, 10.197294f, -44.688675f, -2.987049f, 0.000000f, 1.824153f }, + new[] { 19.994984f, 10.197294f, -44.323845f, -2.987049f, 0.000000f, 1.824153f }, + new[] { 19.397573f, 10.197294f, -43.959015f, -2.987049f, 0.000000f, 1.824154f }, + new[] { 18.800163f, 10.197294f, -43.594185f, -2.987049f, 0.000000f, 1.824154f }, + new[] { 18.202753f, 10.197294f, -43.229355f, -2.987049f, 0.000000f, 1.824154f }, + new[] { 17.605343f, 10.197294f, -42.864525f, -2.987049f, 0.000000f, 1.824154f }, + new[] { 17.007933f, 10.197294f, -42.499695f, -2.987049f, 0.000000f, 1.824154f }, + new[] { 16.410522f, 10.197294f, -42.134865f, -2.987049f, 0.000000f, 1.824155f }, + new[] { 15.813112f, 10.197294f, -41.770035f, -2.987049f, 0.000000f, 1.824155f }, + new[] { 15.215703f, 10.197294f, -41.405205f, -2.987048f, 0.000000f, 1.824155f }, + new[] { 14.618294f, 10.197294f, -41.040375f, -2.987048f, 0.000000f, 1.824155f }, + new[] { 14.020885f, 10.197294f, -40.675545f, -2.987048f, 0.000000f, 1.824155f }, + new[] { 13.423475f, 10.197294f, -40.310715f, -2.987048f, 0.000000f, 1.824155f }, + new[] { 12.826066f, 10.197294f, -39.945885f, -2.987048f, 0.000000f, 1.824156f }, + new[] { 12.228657f, 10.197294f, -39.581055f, -2.987048f, 0.000000f, 1.824156f }, + new[] { 11.631248f, 10.197294f, -39.216225f, -2.987048f, 0.000000f, 1.824156f }, + new[] { 11.033838f, 10.197294f, -38.851395f, -2.987048f, 0.000000f, 1.824156f }, + new[] { 10.436429f, 10.197294f, -38.486565f, -2.987047f, 0.000000f, 1.824157f }, + new[] { 9.839020f, 10.197294f, -38.121735f, -2.987047f, 0.000000f, 1.824157f }, + new[] { 9.241611f, 10.197294f, -37.756905f, -2.987047f, 0.000000f, 1.824157f }, + new[] { 8.644201f, 10.197294f, -37.392075f, -2.987047f, 0.000000f, 1.824158f }, + new[] { 8.046792f, 10.197294f, -37.027245f, -2.987046f, 0.000000f, 1.824158f }, + new[] { 7.449383f, 10.197294f, -36.662415f, -2.987046f, 0.000000f, 1.824160f }, + new[] { 6.851974f, 10.197294f, -36.297581f, -2.987045f, 0.000000f, 1.824160f }, + new[] { 6.254564f, 10.197294f, -35.932751f, -2.987046f, 0.000000f, 1.824159f }, + new[] { 5.657155f, 10.197294f, -35.567917f, -2.987045f, 0.000000f, 1.824161f }, + new[] { 5.059746f, 10.197294f, -35.203087f, -2.987046f, 0.000000f, 1.824160f }, + new[] { 4.462337f, 10.197294f, -34.838253f, -2.987044f, 0.000000f, 1.824162f }, + new[] { 3.864928f, 10.197294f, -34.473423f, -2.987046f, 0.000000f, 1.824159f }, + new[] { 3.267520f, 10.197294f, -34.108589f, -2.987040f, 0.000000f, 1.824169f }, + new[] { 2.910351f, 10.197294f, -33.532368f, -1.429270f, 0.000000f, 3.194869f }, + new[] { 2.648150f, 10.197294f, -32.883331f, -1.311005f, 0.000000f, 3.245191f }, + new[] { 2.385949f, 10.197294f, -32.234291f, -1.311003f, 0.000000f, 3.245192f }, + new[] { 2.123748f, 10.197294f, -31.585253f, -1.311006f, 0.000000f, 3.245191f }, + new[] { 1.861547f, 10.197294f, -30.936214f, -1.311005f, 0.000000f, 3.245191f }, + new[] { 1.916189f, 10.197294f, -30.242338f, 0.359062f, 0.000000f, 3.481533f }, + new[] { 2.011134f, 10.197294f, -29.548807f, 0.474725f, 0.000000f, 3.467656f }, + new[] { 2.246398f, 10.197294f, -28.889526f, 1.176323f, 0.000000f, 3.296402f }, + new[] { 2.481663f, 10.197294f, -28.230246f, 1.176324f, 0.000000f, 3.296402f }, + new[] { 2.716928f, 10.197294f, -27.570965f, 1.176325f, 0.000000f, 3.296401f }, + new[] { 2.979683f, 10.197294f, -26.922150f, 1.313774f, 0.000000f, 3.244071f }, + new[] { 3.242438f, 10.197294f, -26.273335f, 1.313774f, 0.000000f, 3.244071f }, + new[] { 3.505193f, 10.197294f, -25.624521f, 1.313775f, 0.000000f, 3.244071f }, + new[] { 3.767948f, 10.197294f, -24.975708f, 1.313774f, 0.000000f, 3.244071f }, + new[] { 4.030703f, 10.197294f, -24.326893f, 1.313774f, 0.000000f, 3.244071f }, + new[] { 4.293458f, 10.197294f, -23.678078f, 1.313775f, 0.000000f, 3.244071f }, + new[] { 4.556212f, 10.197294f, -23.029263f, 1.313775f, 0.000000f, 3.244071f }, + new[] { 4.818967f, 10.197294f, -22.380449f, 1.313775f, 0.000000f, 3.244071f }, + new[] { 5.081722f, 10.197294f, -21.731636f, 1.313775f, 0.000000f, 3.244071f }, + new[] { 5.344477f, 10.197294f, -21.082821f, 1.313775f, 0.000000f, 3.244071f }, + new[] { 5.607232f, 10.197294f, -20.434008f, 1.313775f, 0.000000f, 3.244071f }, + new[] { 5.869987f, 10.197294f, -19.785194f, 1.313775f, 0.000000f, 3.244071f }, + new[] { 6.132742f, 10.197294f, -19.136379f, 1.313774f, 0.000000f, 3.244071f }, + new[] { 6.322279f, 10.197294f, -18.668360f, 0.947685f, 0.000000f, 2.340096f }, + new[] { 6.401253f, 10.197294f, -18.473352f, 0.394869f, 0.000000f, 0.975039f }, + new[] { 6.434158f, 10.197294f, -18.392099f, 0.164529f, 0.000000f, 0.406268f }, + new[] { 6.447869f, 10.197294f, -18.358244f, 0.068554f, 0.000000f, 0.169280f }, + new[] { 6.453582f, 10.197294f, -18.344137f, 0.028564f, 0.000000f, 0.070535f }, + new[] { 6.455962f, 10.197294f, -18.338259f, 0.011902f, 0.000000f, 0.029390f }, + new[] { 6.455962f, 10.197294f, -18.338259f, 0.000000f, 0.000000f, 0.000000f } }; + + static readonly float[][] EXPECTED_A1Q1TVTA = { + new[] { 22.322426f, 10.197294f, -45.771397f, -3.107285f, 0.000000f, 1.610831f }, + new[] { 21.754303f, 10.197294f, -45.476715f, -3.106887f, 0.000000f, 1.611599f }, + new[] { 21.133097f, 10.197294f, -45.154068f, -3.106033f, 0.000000f, 1.613245f }, + new[] { 20.512094f, 10.197294f, -44.831028f, -3.105014f, 0.000000f, 1.615205f }, + new[] { 19.891315f, 10.197294f, -44.507557f, -3.103898f, 0.000000f, 1.617350f }, + new[] { 19.270782f, 10.197294f, -44.183617f, -3.102670f, 0.000000f, 1.619704f }, + new[] { 18.650520f, 10.197294f, -43.859158f, -3.101315f, 0.000000f, 1.622296f }, + new[] { 18.030558f, 10.197294f, -43.534126f, -3.099816f, 0.000000f, 1.625158f }, + new[] { 17.410927f, 10.197294f, -43.208462f, -3.098152f, 0.000000f, 1.628329f }, + new[] { 16.791668f, 10.197294f, -42.882092f, -3.096297f, 0.000000f, 1.631854f }, + new[] { 16.172823f, 10.197294f, -42.554935f, -3.094221f, 0.000000f, 1.635786f }, + new[] { 15.554445f, 10.197294f, -42.226898f, -3.091888f, 0.000000f, 1.640191f }, + new[] { 14.936594f, 10.197294f, -41.897869f, -3.089255f, 0.000000f, 1.645145f }, + new[] { 14.319340f, 10.197294f, -41.567722f, -3.086269f, 0.000000f, 1.650741f }, + new[] { 13.702767f, 10.197294f, -41.236305f, -3.082862f, 0.000000f, 1.657094f }, + new[] { 13.086977f, 10.197294f, -40.903435f, -3.078954f, 0.000000f, 1.664345f }, + new[] { 12.472089f, 10.197294f, -40.568901f, -3.074442f, 0.000000f, 1.672665f }, + new[] { 11.858250f, 10.197294f, -40.232449f, -3.069196f, 0.000000f, 1.682271f }, + new[] { 11.245640f, 10.197294f, -39.893761f, -3.063048f, 0.000000f, 1.693439f }, + new[] { 10.634483f, 10.197294f, -39.552460f, -3.055783f, 0.000000f, 1.706514f }, + new[] { 10.025061f, 10.197294f, -39.208069f, -3.047112f, 0.000000f, 1.721948f }, + new[] { 9.417730f, 10.197294f, -38.860004f, -3.036654f, 0.000000f, 1.740326f }, + new[] { 8.812954f, 10.197294f, -38.507519f, -3.023880f, 0.000000f, 1.762429f }, + new[] { 8.211343f, 10.197294f, -38.149658f, -3.008055f, 0.000000f, 1.789302f }, + new[] { 7.613717f, 10.197294f, -37.785183f, -2.988129f, 0.000000f, 1.822385f }, + new[] { 7.021209f, 10.197294f, -37.412445f, -2.962540f, 0.000000f, 1.863695f }, + new[] { 6.435429f, 10.197294f, -37.029221f, -2.928901f, 0.000000f, 1.916126f }, + new[] { 5.858753f, 10.197294f, -36.632427f, -2.883380f, 0.000000f, 1.983965f }, + new[] { 5.294861f, 10.197294f, -36.217667f, -2.819461f, 0.000000f, 2.073798f }, + new[] { 4.749827f, 10.197294f, -35.778419f, -2.725170f, 0.000000f, 2.196236f }, + new[] { 4.234636f, 10.197294f, -35.304523f, -2.575954f, 0.000000f, 2.369486f }, + new[] { 3.772264f, 10.197294f, -34.778965f, -2.311863f, 0.000000f, 2.627792f }, + new[] { 3.428036f, 10.197294f, -34.169453f, -1.721137f, 0.000000f, 3.047571f }, + new[] { 3.033963f, 10.197294f, -33.590916f, -1.970364f, 0.000000f, 2.892692f }, + new[] { 2.656040f, 10.197294f, -33.001701f, -1.889616f, 0.000000f, 2.946074f }, + new[] { 2.307335f, 10.197294f, -32.394737f, -1.743529f, 0.000000f, 3.034816f }, + new[] { 2.021275f, 10.197294f, -31.755856f, -1.430300f, 0.000000f, 3.194408f }, + new[] { 1.935164f, 10.197294f, -31.061172f, -0.430555f, 0.000000f, 3.473417f }, + new[] { 1.847493f, 10.197294f, -30.366684f, -0.438351f, 0.000000f, 3.472441f }, + new[] { 1.943488f, 10.197294f, -29.673298f, 0.479975f, 0.000000f, 3.466933f }, + new[] { 2.170555f, 10.197294f, -29.011150f, 1.135336f, 0.000000f, 3.310742f }, + new[] { 2.415170f, 10.197294f, -28.355282f, 1.223072f, 0.000000f, 3.279344f }, + new[] { 2.677041f, 10.197294f, -27.706110f, 1.309357f, 0.000000f, 3.245857f }, + new[] { 2.938912f, 10.197294f, -27.056938f, 1.309357f, 0.000000f, 3.245857f }, + new[] { 3.200784f, 10.197294f, -26.407766f, 1.309357f, 0.000000f, 3.245856f }, + new[] { 3.462655f, 10.197294f, -25.758595f, 1.309357f, 0.000000f, 3.245857f }, + new[] { 3.724526f, 10.197294f, -25.109423f, 1.309357f, 0.000000f, 3.245857f }, + new[] { 3.986398f, 10.197294f, -24.460251f, 1.309357f, 0.000000f, 3.245857f }, + new[] { 4.248269f, 10.197294f, -23.811079f, 1.309357f, 0.000000f, 3.245856f }, + new[] { 4.510140f, 10.197294f, -23.161907f, 1.309357f, 0.000000f, 3.245856f }, + new[] { 4.772012f, 10.197294f, -22.512735f, 1.309357f, 0.000000f, 3.245856f }, + new[] { 5.033883f, 10.197294f, -21.863564f, 1.309358f, 0.000000f, 3.245856f }, + new[] { 5.295755f, 10.197294f, -21.214392f, 1.309358f, 0.000000f, 3.245856f }, + new[] { 5.557627f, 10.197294f, -20.565220f, 1.309358f, 0.000000f, 3.245856f }, + new[] { 5.819499f, 10.197294f, -19.916048f, 1.309358f, 0.000000f, 3.245856f }, + new[] { 6.081370f, 10.197294f, -19.266876f, 1.309358f, 0.000000f, 3.245856f }, + new[] { 6.322058f, 10.197294f, -18.670219f, 1.203439f, 0.000000f, 2.983284f }, + new[] { 6.429398f, 10.197294f, -18.364454f, 0.320910f, 0.000000f, 1.058088f }, + new[] { 6.437642f, 10.197294f, -18.355589f, 0.041219f, 0.000000f, 0.044324f }, + new[] { 6.443481f, 10.197294f, -18.349310f, 0.029197f, 0.000000f, 0.031395f }, + new[] { 6.447618f, 10.197294f, -18.344862f, 0.020681f, 0.000000f, 0.022238f }, + new[] { 6.450547f, 10.197294f, -18.341711f, 0.014649f, 0.000000f, 0.015752f }, + new[] { 6.452622f, 10.197294f, -18.339479f, 0.010377f, 0.000000f, 0.011157f }, + new[] { 6.452622f, 10.197294f, -18.339479f, 0.000000f, 0.000000f, 0.000000f }, }; + + static readonly float[][] EXPECTED_A1Q2TVTA = { + new[] { 22.322426f, 10.197294f, -45.771397f, -3.107285f, 0.000000f, 1.610831f }, + new[] { 21.754303f, 10.197294f, -45.476715f, -3.106887f, 0.000000f, 1.611599f }, + new[] { 21.133097f, 10.197294f, -45.154068f, -3.106033f, 0.000000f, 1.613245f }, + new[] { 20.512094f, 10.197294f, -44.831028f, -3.105014f, 0.000000f, 1.615205f }, + new[] { 19.891315f, 10.197294f, -44.507557f, -3.103898f, 0.000000f, 1.617350f }, + new[] { 19.270782f, 10.197294f, -44.183617f, -3.102670f, 0.000000f, 1.619704f }, + new[] { 18.650520f, 10.197294f, -43.859158f, -3.101315f, 0.000000f, 1.622296f }, + new[] { 18.030558f, 10.197294f, -43.534126f, -3.099816f, 0.000000f, 1.625158f }, + new[] { 17.410927f, 10.197294f, -43.208462f, -3.098152f, 0.000000f, 1.628329f }, + new[] { 16.791668f, 10.197294f, -42.882092f, -3.096297f, 0.000000f, 1.631854f }, + new[] { 16.172823f, 10.197294f, -42.554935f, -3.094221f, 0.000000f, 1.635786f }, + new[] { 15.554445f, 10.197294f, -42.226898f, -3.091888f, 0.000000f, 1.640191f }, + new[] { 14.936594f, 10.197294f, -41.897869f, -3.089255f, 0.000000f, 1.645145f }, + new[] { 14.319340f, 10.197294f, -41.567722f, -3.086269f, 0.000000f, 1.650741f }, + new[] { 13.702767f, 10.197294f, -41.236305f, -3.082862f, 0.000000f, 1.657094f }, + new[] { 13.086977f, 10.197294f, -40.903435f, -3.078954f, 0.000000f, 1.664345f }, + new[] { 12.472089f, 10.197294f, -40.568901f, -3.074442f, 0.000000f, 1.672665f }, + new[] { 11.858250f, 10.197294f, -40.232449f, -3.069196f, 0.000000f, 1.682271f }, + new[] { 11.245640f, 10.197294f, -39.893761f, -3.063048f, 0.000000f, 1.693439f }, + new[] { 10.634483f, 10.197294f, -39.552460f, -3.055783f, 0.000000f, 1.706514f }, + new[] { 10.025061f, 10.197294f, -39.208069f, -3.047112f, 0.000000f, 1.721948f }, + new[] { 9.417730f, 10.197294f, -38.860004f, -3.036654f, 0.000000f, 1.740326f }, + new[] { 8.812954f, 10.197294f, -38.507519f, -3.023880f, 0.000000f, 1.762429f }, + new[] { 8.211343f, 10.197294f, -38.149658f, -3.008055f, 0.000000f, 1.789302f }, + new[] { 7.613717f, 10.197294f, -37.785183f, -2.988129f, 0.000000f, 1.822385f }, + new[] { 7.021209f, 10.197294f, -37.412445f, -2.962540f, 0.000000f, 1.863695f }, + new[] { 6.435429f, 10.197294f, -37.029221f, -2.928901f, 0.000000f, 1.916126f }, + new[] { 5.858753f, 10.197294f, -36.632427f, -2.883380f, 0.000000f, 1.983965f }, + new[] { 5.294861f, 10.197294f, -36.217667f, -2.819461f, 0.000000f, 2.073798f }, + new[] { 4.749827f, 10.197294f, -35.778419f, -2.725170f, 0.000000f, 2.196236f }, + new[] { 4.234636f, 10.197294f, -35.304523f, -2.575954f, 0.000000f, 2.369486f }, + new[] { 3.772264f, 10.197294f, -34.778965f, -2.311863f, 0.000000f, 2.627792f }, + new[] { 3.428036f, 10.197294f, -34.169453f, -1.721137f, 0.000000f, 3.047571f }, + new[] { 3.033963f, 10.197294f, -33.590916f, -1.970364f, 0.000000f, 2.892692f }, + new[] { 2.656040f, 10.197294f, -33.001701f, -1.889616f, 0.000000f, 2.946074f }, + new[] { 2.307335f, 10.197294f, -32.394737f, -1.743529f, 0.000000f, 3.034816f }, + new[] { 2.021275f, 10.197294f, -31.755856f, -1.430300f, 0.000000f, 3.194408f }, + new[] { 1.935164f, 10.197294f, -31.061172f, -0.430555f, 0.000000f, 3.473417f }, + new[] { 1.847493f, 10.197294f, -30.366684f, -0.438351f, 0.000000f, 3.472441f }, + new[] { 1.943488f, 10.197294f, -29.673298f, 0.479975f, 0.000000f, 3.466933f }, + new[] { 2.170555f, 10.197294f, -29.011150f, 1.135336f, 0.000000f, 3.310742f }, + new[] { 2.415170f, 10.197294f, -28.355282f, 1.223072f, 0.000000f, 3.279344f }, + new[] { 2.677041f, 10.197294f, -27.706110f, 1.309357f, 0.000000f, 3.245857f }, + new[] { 2.938912f, 10.197294f, -27.056938f, 1.309357f, 0.000000f, 3.245857f }, + new[] { 3.200784f, 10.197294f, -26.407766f, 1.309357f, 0.000000f, 3.245856f }, + new[] { 3.462655f, 10.197294f, -25.758595f, 1.309357f, 0.000000f, 3.245857f }, + new[] { 3.724526f, 10.197294f, -25.109423f, 1.309357f, 0.000000f, 3.245857f }, + new[] { 3.986398f, 10.197294f, -24.460251f, 1.309357f, 0.000000f, 3.245857f }, + new[] { 4.248269f, 10.197294f, -23.811079f, 1.309357f, 0.000000f, 3.245856f }, + new[] { 4.510140f, 10.197294f, -23.161907f, 1.309357f, 0.000000f, 3.245856f }, + new[] { 4.772012f, 10.197294f, -22.512735f, 1.309357f, 0.000000f, 3.245856f }, + new[] { 5.033883f, 10.197294f, -21.863564f, 1.309358f, 0.000000f, 3.245856f }, + new[] { 5.295755f, 10.197294f, -21.214392f, 1.309358f, 0.000000f, 3.245856f }, + new[] { 5.557627f, 10.197294f, -20.565220f, 1.309358f, 0.000000f, 3.245856f }, + new[] { 5.819499f, 10.197294f, -19.916048f, 1.309358f, 0.000000f, 3.245856f }, + new[] { 6.081370f, 10.197294f, -19.266876f, 1.309358f, 0.000000f, 3.245856f }, + new[] { 6.305691f, 10.197294f, -18.710793f, 1.121604f, 0.000000f, 2.780417f }, + new[] { 6.420408f, 10.197294f, -18.455353f, 0.541385f, 0.000000f, 1.188869f }, + new[] { 6.450006f, 10.197294f, -18.371796f, 0.147987f, 0.000000f, 0.417789f }, + new[] { 6.452239f, 10.197294f, -18.360790f, 0.011167f, 0.000000f, 0.055030f }, + new[] { 6.453821f, 10.197294f, -18.352995f, 0.007909f, 0.000000f, 0.038981f }, + new[] { 6.454941f, 10.197294f, -18.347473f, 0.005603f, 0.000000f, 0.027612f }, + new[] { 6.455735f, 10.197294f, -18.343561f, 0.003969f, 0.000000f, 0.019560f }, + new[] { 6.455735f, 10.197294f, -18.343561f, 0.000000f, 0.000000f, 0.000000f }, }; + + static readonly float[][] EXPECTED_A1Q3TVTA = { + new[] { 22.322426f, 10.197294f, -45.771397f, -3.107285f, 0.000000f, 1.610831f }, + new[] { 21.754303f, 10.197294f, -45.476715f, -3.106887f, 0.000000f, 1.611599f }, + new[] { 21.133097f, 10.197294f, -45.154068f, -3.106033f, 0.000000f, 1.613245f }, + new[] { 20.512094f, 10.197294f, -44.831028f, -3.105014f, 0.000000f, 1.615205f }, + new[] { 19.891315f, 10.197294f, -44.507557f, -3.103898f, 0.000000f, 1.617350f }, + new[] { 19.270782f, 10.197294f, -44.183617f, -3.102670f, 0.000000f, 1.619704f }, + new[] { 18.650520f, 10.197294f, -43.859158f, -3.101315f, 0.000000f, 1.622296f }, + new[] { 18.030558f, 10.197294f, -43.534126f, -3.099816f, 0.000000f, 1.625158f }, + new[] { 17.410927f, 10.197294f, -43.208462f, -3.098152f, 0.000000f, 1.628329f }, + new[] { 16.791668f, 10.197294f, -42.882092f, -3.096297f, 0.000000f, 1.631854f }, + new[] { 16.172823f, 10.197294f, -42.554935f, -3.094221f, 0.000000f, 1.635786f }, + new[] { 15.554445f, 10.197294f, -42.226898f, -3.091888f, 0.000000f, 1.640191f }, + new[] { 14.936594f, 10.197294f, -41.897869f, -3.089255f, 0.000000f, 1.645145f }, + new[] { 14.319340f, 10.197294f, -41.567722f, -3.086269f, 0.000000f, 1.650741f }, + new[] { 13.702767f, 10.197294f, -41.236305f, -3.082862f, 0.000000f, 1.657094f }, + new[] { 13.086977f, 10.197294f, -40.903435f, -3.078954f, 0.000000f, 1.664345f }, + new[] { 12.472089f, 10.197294f, -40.568901f, -3.074442f, 0.000000f, 1.672665f }, + new[] { 11.858250f, 10.197294f, -40.232449f, -3.069196f, 0.000000f, 1.682271f }, + new[] { 11.245640f, 10.197294f, -39.893761f, -3.063048f, 0.000000f, 1.693439f }, + new[] { 10.634483f, 10.197294f, -39.552460f, -3.055783f, 0.000000f, 1.706514f }, + new[] { 10.025061f, 10.197294f, -39.208069f, -3.047112f, 0.000000f, 1.721948f }, + new[] { 9.417730f, 10.197294f, -38.860004f, -3.036654f, 0.000000f, 1.740326f }, + new[] { 8.812954f, 10.197294f, -38.507519f, -3.023880f, 0.000000f, 1.762429f }, + new[] { 8.211343f, 10.197294f, -38.149658f, -3.008055f, 0.000000f, 1.789302f }, + new[] { 7.613717f, 10.197294f, -37.785183f, -2.988129f, 0.000000f, 1.822385f }, + new[] { 7.021209f, 10.197294f, -37.412445f, -2.962540f, 0.000000f, 1.863695f }, + new[] { 6.435429f, 10.197294f, -37.029221f, -2.928901f, 0.000000f, 1.916126f }, + new[] { 5.858753f, 10.197294f, -36.632427f, -2.883380f, 0.000000f, 1.983965f }, + new[] { 5.294861f, 10.197294f, -36.217667f, -2.819461f, 0.000000f, 2.073798f }, + new[] { 4.749827f, 10.197294f, -35.778419f, -2.725170f, 0.000000f, 2.196236f }, + new[] { 4.234636f, 10.197294f, -35.304523f, -2.575954f, 0.000000f, 2.369486f }, + new[] { 3.772264f, 10.197294f, -34.778965f, -2.311863f, 0.000000f, 2.627792f }, + new[] { 3.428036f, 10.197294f, -34.169453f, -1.721137f, 0.000000f, 3.047571f }, + new[] { 3.033963f, 10.197294f, -33.590916f, -1.970364f, 0.000000f, 2.892692f }, + new[] { 2.656040f, 10.197294f, -33.001701f, -1.889616f, 0.000000f, 2.946074f }, + new[] { 2.307335f, 10.197294f, -32.394737f, -1.743529f, 0.000000f, 3.034816f }, + new[] { 2.021275f, 10.197294f, -31.755856f, -1.430300f, 0.000000f, 3.194408f }, + new[] { 1.935164f, 10.197294f, -31.061172f, -0.430555f, 0.000000f, 3.473417f }, + new[] { 1.847493f, 10.197294f, -30.366684f, -0.438351f, 0.000000f, 3.472441f }, + new[] { 1.943488f, 10.197294f, -29.673298f, 0.479975f, 0.000000f, 3.466933f }, + new[] { 2.170555f, 10.197294f, -29.011150f, 1.135336f, 0.000000f, 3.310742f }, + new[] { 2.415170f, 10.197294f, -28.355282f, 1.223072f, 0.000000f, 3.279344f }, + new[] { 2.677041f, 10.197294f, -27.706110f, 1.309357f, 0.000000f, 3.245857f }, + new[] { 2.938912f, 10.197294f, -27.056938f, 1.309357f, 0.000000f, 3.245857f }, + new[] { 3.200784f, 10.197294f, -26.407766f, 1.309357f, 0.000000f, 3.245856f }, + new[] { 3.462655f, 10.197294f, -25.758595f, 1.309357f, 0.000000f, 3.245857f }, + new[] { 3.724526f, 10.197294f, -25.109423f, 1.309357f, 0.000000f, 3.245857f }, + new[] { 3.986398f, 10.197294f, -24.460251f, 1.309357f, 0.000000f, 3.245857f }, + new[] { 4.248269f, 10.197294f, -23.811079f, 1.309357f, 0.000000f, 3.245856f }, + new[] { 4.510140f, 10.197294f, -23.161907f, 1.309357f, 0.000000f, 3.245856f }, + new[] { 4.772012f, 10.197294f, -22.512735f, 1.309357f, 0.000000f, 3.245856f }, + new[] { 5.033883f, 10.197294f, -21.863564f, 1.309358f, 0.000000f, 3.245856f }, + new[] { 5.295755f, 10.197294f, -21.214392f, 1.309358f, 0.000000f, 3.245856f }, + new[] { 5.557627f, 10.197294f, -20.565220f, 1.309358f, 0.000000f, 3.245856f }, + new[] { 5.819499f, 10.197294f, -19.916048f, 1.309358f, 0.000000f, 3.245856f }, + new[] { 6.081370f, 10.197294f, -19.266876f, 1.309358f, 0.000000f, 3.245856f }, + new[] { 6.297070f, 10.197294f, -18.723810f, 1.078498f, 0.000000f, 2.715334f }, + new[] { 6.397289f, 10.197294f, -18.479179f, 0.456429f, 0.000000f, 1.107728f }, + new[] { 6.439916f, 10.197294f, -18.384853f, 0.213137f, 0.000000f, 0.471627f }, + new[] { 6.454712f, 10.197294f, -18.342505f, 0.073981f, 0.000000f, 0.211745f }, + new[] { 6.454712f, 10.197294f, -18.342505f, 0.000000f, 0.000000f, 0.000000f } }; + + static readonly float[][] EXPECTED_A1Q3TVTAS = { + new[] { 22.322426f, 10.197294f, -45.771397f, -3.107285f, 0.000000f, 1.610831f }, + new[] { 21.754303f, 10.197294f, -45.476715f, -3.106887f, 0.000000f, 1.611599f }, + new[] { 21.133097f, 10.197294f, -45.154068f, -3.106033f, 0.000000f, 1.613245f }, + new[] { 20.512094f, 10.197294f, -44.831028f, -3.105014f, 0.000000f, 1.615205f }, + new[] { 19.891315f, 10.197294f, -44.507557f, -3.103898f, 0.000000f, 1.617350f }, + new[] { 19.270782f, 10.197294f, -44.183617f, -3.102670f, 0.000000f, 1.619704f }, + new[] { 18.650520f, 10.197294f, -43.859158f, -3.101315f, 0.000000f, 1.622296f }, + new[] { 18.030558f, 10.197294f, -43.534126f, -3.099816f, 0.000000f, 1.625158f }, + new[] { 17.410927f, 10.197294f, -43.208462f, -3.098152f, 0.000000f, 1.628329f }, + new[] { 16.791668f, 10.197294f, -42.882092f, -3.096297f, 0.000000f, 1.631854f }, + new[] { 16.172823f, 10.197294f, -42.554935f, -3.094221f, 0.000000f, 1.635786f }, + new[] { 15.554445f, 10.197294f, -42.226898f, -3.091888f, 0.000000f, 1.640191f }, + new[] { 14.936594f, 10.197294f, -41.897869f, -3.089255f, 0.000000f, 1.645145f }, + new[] { 14.319340f, 10.197294f, -41.567722f, -3.086269f, 0.000000f, 1.650741f }, + new[] { 13.702767f, 10.197294f, -41.236305f, -3.082862f, 0.000000f, 1.657094f }, + new[] { 13.086977f, 10.197294f, -40.903435f, -3.078954f, 0.000000f, 1.664345f }, + new[] { 12.472089f, 10.197294f, -40.568901f, -3.074442f, 0.000000f, 1.672665f }, + new[] { 11.858250f, 10.197294f, -40.232449f, -3.069196f, 0.000000f, 1.682271f }, + new[] { 11.245640f, 10.197294f, -39.893761f, -3.063048f, 0.000000f, 1.693439f }, + new[] { 10.634483f, 10.197294f, -39.552460f, -3.055783f, 0.000000f, 1.706514f }, + new[] { 10.025061f, 10.197294f, -39.208069f, -3.047112f, 0.000000f, 1.721948f }, + new[] { 9.417730f, 10.197294f, -38.860004f, -3.036654f, 0.000000f, 1.740326f }, + new[] { 8.812954f, 10.197294f, -38.507519f, -3.023880f, 0.000000f, 1.762429f }, + new[] { 8.211343f, 10.197294f, -38.149658f, -3.008055f, 0.000000f, 1.789302f }, + new[] { 7.613717f, 10.197294f, -37.785183f, -2.988129f, 0.000000f, 1.822385f }, + new[] { 7.021209f, 10.197294f, -37.412445f, -2.962540f, 0.000000f, 1.863695f }, + new[] { 6.435429f, 10.197294f, -37.029221f, -2.928901f, 0.000000f, 1.916126f }, + new[] { 5.858753f, 10.197294f, -36.632427f, -2.883380f, 0.000000f, 1.983965f }, + new[] { 5.294861f, 10.197294f, -36.217667f, -2.819461f, 0.000000f, 2.073798f }, + new[] { 4.749827f, 10.197294f, -35.778419f, -2.725170f, 0.000000f, 2.196236f }, + new[] { 4.234636f, 10.197294f, -35.304523f, -2.575954f, 0.000000f, 2.369486f }, + new[] { 3.772264f, 10.197294f, -34.778965f, -2.311863f, 0.000000f, 2.627792f }, + new[] { 3.428036f, 10.197294f, -34.169453f, -1.721137f, 0.000000f, 3.047571f }, + new[] { 3.033963f, 10.197294f, -33.590916f, -1.970364f, 0.000000f, 2.892692f }, + new[] { 2.656040f, 10.197294f, -33.001701f, -1.889616f, 0.000000f, 2.946074f }, + new[] { 2.307335f, 10.197294f, -32.394737f, -1.743529f, 0.000000f, 3.034816f }, + new[] { 2.021275f, 10.197294f, -31.755856f, -1.430300f, 0.000000f, 3.194408f }, + new[] { 1.935164f, 10.197294f, -31.061172f, -0.430555f, 0.000000f, 3.473417f }, + new[] { 1.847493f, 10.197294f, -30.366684f, -0.438351f, 0.000000f, 3.472441f }, + new[] { 1.943488f, 10.197294f, -29.673298f, 0.479975f, 0.000000f, 3.466933f }, + new[] { 2.170555f, 10.197294f, -29.011150f, 1.135336f, 0.000000f, 3.310742f }, + new[] { 2.415170f, 10.197294f, -28.355282f, 1.223072f, 0.000000f, 3.279344f }, + new[] { 2.677041f, 10.197294f, -27.706110f, 1.309357f, 0.000000f, 3.245857f }, + new[] { 2.938912f, 10.197294f, -27.056938f, 1.309357f, 0.000000f, 3.245857f }, + new[] { 3.200784f, 10.197294f, -26.407766f, 1.309357f, 0.000000f, 3.245856f }, + new[] { 3.462655f, 10.197294f, -25.758595f, 1.309357f, 0.000000f, 3.245857f }, + new[] { 3.724526f, 10.197294f, -25.109423f, 1.309357f, 0.000000f, 3.245857f }, + new[] { 3.986398f, 10.197294f, -24.460251f, 1.309357f, 0.000000f, 3.245857f }, + new[] { 4.248269f, 10.197294f, -23.811079f, 1.309357f, 0.000000f, 3.245856f }, + new[] { 4.510140f, 10.197294f, -23.161907f, 1.309357f, 0.000000f, 3.245856f }, + new[] { 4.772012f, 10.197294f, -22.512735f, 1.309357f, 0.000000f, 3.245856f }, + new[] { 5.033883f, 10.197294f, -21.863564f, 1.309358f, 0.000000f, 3.245856f }, + new[] { 5.295755f, 10.197294f, -21.214392f, 1.309358f, 0.000000f, 3.245856f }, + new[] { 5.557627f, 10.197294f, -20.565220f, 1.309358f, 0.000000f, 3.245856f }, + new[] { 5.819499f, 10.197294f, -19.916048f, 1.309358f, 0.000000f, 3.245856f }, + new[] { 6.081370f, 10.197294f, -19.266876f, 1.309358f, 0.000000f, 3.245856f }, + new[] { 6.297070f, 10.197294f, -18.723810f, 1.078498f, 0.000000f, 2.715334f }, + new[] { 6.397289f, 10.197294f, -18.479179f, 0.456429f, 0.000000f, 1.107728f }, + new[] { 6.439916f, 10.197294f, -18.384853f, 0.213137f, 0.000000f, 0.471627f }, + new[] { 6.454712f, 10.197294f, -18.342505f, 0.073981f, 0.000000f, 0.211745f }, + new[] { 6.454712f, 10.197294f, -18.342505f, 0.000000f, 0.000000f, 0.000000f } }; + + [Test] + public void testAgent1Quality0TVTA() { + int updateFlags = CrowdAgentParams.DT_CROWD_ANTICIPATE_TURNS | CrowdAgentParams.DT_CROWD_OPTIMIZE_VIS + | CrowdAgentParams.DT_CROWD_OPTIMIZE_TOPO | CrowdAgentParams.DT_CROWD_OBSTACLE_AVOIDANCE; + + addAgentGrid(1, 0.4f, updateFlags, 0, startPoss[0]); + setMoveTarget(endPoss[0], false); + for (int i = 0; i < EXPECTED_A1Q0TVTA.Length; i++) { + crowd.update(1 / 5f, null); + foreach (CrowdAgent ag in crowd.getActiveAgents()) { + Assert.That(ag.npos[0], Is.EqualTo(EXPECTED_A1Q0TVTA[i][0]).Within(0.001f)); + Assert.That(ag.npos[1], Is.EqualTo(EXPECTED_A1Q0TVTA[i][1]).Within(0.001f)); + Assert.That(ag.npos[2], Is.EqualTo(EXPECTED_A1Q0TVTA[i][2]).Within(0.001f)); + Assert.That(ag.nvel[0], Is.EqualTo(EXPECTED_A1Q0TVTA[i][3]).Within(0.001f)); + Assert.That(ag.nvel[1], Is.EqualTo(EXPECTED_A1Q0TVTA[i][4]).Within(0.001f)); + Assert.That(ag.nvel[2], Is.EqualTo(EXPECTED_A1Q0TVTA[i][5]).Within(0.001f)); + } + } + } + + [Test] + public void testAgent1Quality0TVT() { + int updateFlags = CrowdAgentParams.DT_CROWD_ANTICIPATE_TURNS | CrowdAgentParams.DT_CROWD_OPTIMIZE_VIS + | CrowdAgentParams.DT_CROWD_OPTIMIZE_TOPO; + + addAgentGrid(1, 0.4f, updateFlags, 0, startPoss[0]); + setMoveTarget(endPoss[0], false); + for (int i = 0; i < EXPECTED_A1Q0TVT.Length; i++) { + crowd.update(1 / 5f, null); + foreach (CrowdAgent ag in crowd.getActiveAgents()) { + Assert.That(ag.npos[0], Is.EqualTo(EXPECTED_A1Q0TVT[i][0]).Within(0.001f)); + Assert.That(ag.npos[1], Is.EqualTo(EXPECTED_A1Q0TVT[i][1]).Within(0.001f)); + Assert.That(ag.npos[2], Is.EqualTo(EXPECTED_A1Q0TVT[i][2]).Within(0.001f)); + Assert.That(ag.nvel[0], Is.EqualTo(EXPECTED_A1Q0TVT[i][3]).Within(0.001f)); + Assert.That(ag.nvel[1], Is.EqualTo(EXPECTED_A1Q0TVT[i][4]).Within(0.001f)); + Assert.That(ag.nvel[2], Is.EqualTo(EXPECTED_A1Q0TVT[i][5]).Within(0.001f)); + } + } + } + + [Test] + public void testAgent1Quality0TV() { + int updateFlags = CrowdAgentParams.DT_CROWD_OPTIMIZE_TOPO | CrowdAgentParams.DT_CROWD_OPTIMIZE_VIS; + + addAgentGrid(1, 0.4f, updateFlags, 0, startPoss[0]); + setMoveTarget(endPoss[0], false); + for (int i = 0; i < EXPECTED_A1Q0TV.Length; i++) { + crowd.update(1 / 5f, null); + foreach (CrowdAgent ag in crowd.getActiveAgents()) { + Assert.That(ag.npos[0], Is.EqualTo(EXPECTED_A1Q0TV[i][0]).Within(0.001f)); + Assert.That(ag.npos[1], Is.EqualTo(EXPECTED_A1Q0TV[i][1]).Within(0.001f)); + Assert.That(ag.npos[2], Is.EqualTo(EXPECTED_A1Q0TV[i][2]).Within(0.001f)); + Assert.That(ag.nvel[0], Is.EqualTo(EXPECTED_A1Q0TV[i][3]).Within(0.001f)); + Assert.That(ag.nvel[1], Is.EqualTo(EXPECTED_A1Q0TV[i][4]).Within(0.001f)); + Assert.That(ag.nvel[2], Is.EqualTo(EXPECTED_A1Q0TV[i][5]).Within(0.001f)); + } + } + } + + [Test] + public void testAgent1Quality0T() { + int updateFlags = CrowdAgentParams.DT_CROWD_OPTIMIZE_TOPO; + + addAgentGrid(1, 0.4f, updateFlags, 0, startPoss[0]); + setMoveTarget(endPoss[0], false); + for (int i = 0; i < EXPECTED_A1Q0T.Length; i++) { + crowd.update(1 / 5f, null); + foreach (CrowdAgent ag in crowd.getActiveAgents()) { + Assert.That(ag.npos[0], Is.EqualTo(EXPECTED_A1Q0T[i][0]).Within(0.001)); + Assert.That(ag.npos[1], Is.EqualTo(EXPECTED_A1Q0T[i][1]).Within(0.001)); + Assert.That(ag.npos[2], Is.EqualTo(EXPECTED_A1Q0T[i][2]).Within(0.001)); + Assert.That(ag.nvel[0], Is.EqualTo(EXPECTED_A1Q0T[i][3]).Within(0.001)); + Assert.That(ag.nvel[1], Is.EqualTo(EXPECTED_A1Q0T[i][4]).Within(0.001)); + Assert.That(ag.nvel[2], Is.EqualTo(EXPECTED_A1Q0T[i][5]).Within(0.001)); + } + } + } + + [Test] + public void testAgent1Quality1TVTA() { + int updateFlags = CrowdAgentParams.DT_CROWD_ANTICIPATE_TURNS | CrowdAgentParams.DT_CROWD_OPTIMIZE_VIS + | CrowdAgentParams.DT_CROWD_OPTIMIZE_TOPO | CrowdAgentParams.DT_CROWD_OBSTACLE_AVOIDANCE; + + addAgentGrid(1, 0.4f, updateFlags, 1, startPoss[0]); + setMoveTarget(endPoss[0], false); + for (int i = 0; i < EXPECTED_A1Q1TVTA.Length; i++) { + crowd.update(1 / 5f, null); + foreach (CrowdAgent ag in crowd.getActiveAgents()) { + Assert.That(ag.npos[0], Is.EqualTo(EXPECTED_A1Q1TVTA[i][0]).Within(0.001f)); + Assert.That(ag.npos[1], Is.EqualTo(EXPECTED_A1Q1TVTA[i][1]).Within(0.001f)); + Assert.That(ag.npos[2], Is.EqualTo(EXPECTED_A1Q1TVTA[i][2]).Within(0.001f)); + Assert.That(ag.nvel[0], Is.EqualTo(EXPECTED_A1Q1TVTA[i][3]).Within(0.001f)); + Assert.That(ag.nvel[1], Is.EqualTo(EXPECTED_A1Q1TVTA[i][4]).Within(0.001f)); + Assert.That(ag.nvel[2], Is.EqualTo(EXPECTED_A1Q1TVTA[i][5]).Within(0.001f)); + } + } + } + + [Test] + public void testAgent1Quality2TVTA() { + int updateFlags = CrowdAgentParams.DT_CROWD_ANTICIPATE_TURNS | CrowdAgentParams.DT_CROWD_OPTIMIZE_VIS + | CrowdAgentParams.DT_CROWD_OPTIMIZE_TOPO | CrowdAgentParams.DT_CROWD_OBSTACLE_AVOIDANCE; + + addAgentGrid(1, 0.4f, updateFlags, 2, startPoss[0]); + setMoveTarget(endPoss[0], false); + for (int i = 0; i < EXPECTED_A1Q2TVTA.Length; i++) { + crowd.update(1 / 5f, null); + foreach (CrowdAgent ag in crowd.getActiveAgents()) { + Assert.That(ag.npos[0], Is.EqualTo(EXPECTED_A1Q2TVTA[i][0]).Within(0.001f)); + Assert.That(ag.npos[1], Is.EqualTo(EXPECTED_A1Q2TVTA[i][1]).Within(0.001f)); + Assert.That(ag.npos[2], Is.EqualTo(EXPECTED_A1Q2TVTA[i][2]).Within(0.001f)); + Assert.That(ag.nvel[0], Is.EqualTo(EXPECTED_A1Q2TVTA[i][3]).Within(0.001f)); + Assert.That(ag.nvel[1], Is.EqualTo(EXPECTED_A1Q2TVTA[i][4]).Within(0.001f)); + Assert.That(ag.nvel[2], Is.EqualTo(EXPECTED_A1Q2TVTA[i][5]).Within(0.001f)); + } + } + } + + [Test] + public void testAgent1Quality3TVTA() { + int updateFlags = CrowdAgentParams.DT_CROWD_ANTICIPATE_TURNS | CrowdAgentParams.DT_CROWD_OPTIMIZE_VIS + | CrowdAgentParams.DT_CROWD_OPTIMIZE_TOPO | CrowdAgentParams.DT_CROWD_OBSTACLE_AVOIDANCE; + + addAgentGrid(1, 0.4f, updateFlags, 3, startPoss[0]); + setMoveTarget(endPoss[0], false); + for (int i = 0; i < EXPECTED_A1Q3TVTA.Length; i++) { + crowd.update(1 / 5f, null); + foreach (CrowdAgent ag in crowd.getActiveAgents()) { + Assert.That(ag.npos[0], Is.EqualTo(EXPECTED_A1Q3TVTA[i][0]).Within(0.001f)); + Assert.That(ag.npos[1], Is.EqualTo(EXPECTED_A1Q3TVTA[i][1]).Within(0.001f)); + Assert.That(ag.npos[2], Is.EqualTo(EXPECTED_A1Q3TVTA[i][2]).Within(0.001f)); + Assert.That(ag.nvel[0], Is.EqualTo(EXPECTED_A1Q3TVTA[i][3]).Within(0.001f)); + Assert.That(ag.nvel[1], Is.EqualTo(EXPECTED_A1Q3TVTA[i][4]).Within(0.001f)); + Assert.That(ag.nvel[2], Is.EqualTo(EXPECTED_A1Q3TVTA[i][5]).Within(0.001f)); + } + } + } + + [Test] + public void testAgent1Quality3TVTAS() { + int updateFlags = CrowdAgentParams.DT_CROWD_ANTICIPATE_TURNS | CrowdAgentParams.DT_CROWD_OPTIMIZE_VIS + | CrowdAgentParams.DT_CROWD_OPTIMIZE_TOPO | CrowdAgentParams.DT_CROWD_OBSTACLE_AVOIDANCE + | CrowdAgentParams.DT_CROWD_SEPARATION; + + addAgentGrid(1, 0.4f, updateFlags, 3, startPoss[0]); + setMoveTarget(endPoss[0], false); + for (int i = 0; i < EXPECTED_A1Q3TVTAS.Length; i++) { + crowd.update(1 / 5f, null); + foreach (CrowdAgent ag in crowd.getActiveAgents()) { + Assert.That(ag.npos[0], Is.EqualTo(EXPECTED_A1Q3TVTAS[i][0]).Within(0.001f)); + Assert.That(ag.npos[1], Is.EqualTo(EXPECTED_A1Q3TVTAS[i][1]).Within(0.001f)); + Assert.That(ag.npos[2], Is.EqualTo(EXPECTED_A1Q3TVTAS[i][2]).Within(0.001f)); + Assert.That(ag.nvel[0], Is.EqualTo(EXPECTED_A1Q3TVTAS[i][3]).Within(0.001f)); + Assert.That(ag.nvel[1], Is.EqualTo(EXPECTED_A1Q3TVTAS[i][4]).Within(0.001f)); + Assert.That(ag.nvel[2], Is.EqualTo(EXPECTED_A1Q3TVTAS[i][5]).Within(0.001f)); + } + } + } + +} diff --git a/test/DotRecast.Detour.Crowd.Test/Crowd4Test.cs b/test/DotRecast.Detour.Crowd.Test/Crowd4Test.cs new file mode 100644 index 0000000..c2ccd05 --- /dev/null +++ b/test/DotRecast.Detour.Crowd.Test/Crowd4Test.cs @@ -0,0 +1,363 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.Threading; +using NUnit.Framework; + +namespace DotRecast.Detour.Crowd.Test; + +public class Crowd4Test : AbstractCrowdTest { + + static readonly float[][] EXPECTED_A1Q2TVTA = { + new[] { 23.275612f, 10.197294f, -46.233074f, 0.061640f, 0.000000f, 0.073828f }, + new[] { 23.350517f, 10.197294f, -46.304905f, 0.030557f, 0.000000f, 0.118703f }, + new[] { 23.347885f, 10.197294f, -46.331837f, -0.024102f, 0.000000f, -0.093108f }, + new[] { 23.338102f, 10.197294f, -46.372726f, -0.048912f, 0.000000f, -0.204439f }, + new[] { 23.158630f, 10.197294f, -46.386150f, -0.897364f, 0.000000f, -0.067119f }, + new[] { 22.750568f, 10.197294f, -46.389927f, -2.040308f, 0.000000f, -0.018881f }, + new[] { 22.173302f, 10.197294f, -46.272018f, -2.886330f, 0.000000f, 0.589539f }, + new[] { 21.579432f, 10.197294f, -46.052692f, -2.969354f, 0.000000f, 1.096630f }, + new[] { 20.957502f, 10.197294f, -45.761692f, -3.109650f, 0.000000f, 1.455011f }, + new[] { 20.365976f, 10.197294f, -45.476425f, -2.957630f, 0.000000f, 1.426327f }, + new[] { 19.775288f, 10.197294f, -45.189430f, -2.953444f, 0.000000f, 1.434977f }, + new[] { 19.185482f, 10.197294f, -44.900627f, -2.949032f, 0.000000f, 1.444020f }, + new[] { 18.596607f, 10.197294f, -44.609928f, -2.944376f, 0.000000f, 1.453491f }, + new[] { 18.008717f, 10.197294f, -44.317242f, -2.939449f, 0.000000f, 1.463430f }, + new[] { 17.421871f, 10.197294f, -44.022465f, -2.934223f, 0.000000f, 1.473880f }, + new[] { 16.836138f, 10.197294f, -43.725487f, -2.928665f, 0.000000f, 1.484893f }, + new[] { 16.237518f, 10.197294f, -43.413433f, -2.993098f, 0.000000f, 1.560270f }, + new[] { 15.662568f, 10.197294f, -43.098225f, -2.874751f, 0.000000f, 1.576043f }, + new[] { 15.094946f, 10.197294f, -42.777035f, -2.838109f, 0.000000f, 1.605944f }, + new[] { 14.528272f, 10.197294f, -42.454178f, -2.833370f, 0.000000f, 1.614288f }, + new[] { 13.962630f, 10.197294f, -42.129513f, -2.828207f, 0.000000f, 1.623317f }, + new[] { 13.391782f, 10.197294f, -41.806934f, -2.854241f, 0.000000f, 1.612890f }, + new[] { 12.799945f, 10.197294f, -41.482201f, -2.959186f, 0.000000f, 1.623668f }, + new[] { 12.197092f, 10.197294f, -41.139999f, -3.014262f, 0.000000f, 1.711000f }, + new[] { 11.594053f, 10.197294f, -40.811634f, -3.015195f, 0.000000f, 1.641821f }, + new[] { 10.996500f, 10.197294f, -40.460262f, -2.987766f, 0.000000f, 1.756859f }, + new[] { 10.401724f, 10.197294f, -40.104210f, -2.973881f, 0.000000f, 1.780260f }, + new[] { 9.810114f, 10.197294f, -39.742920f, -2.958048f, 0.000000f, 1.806445f }, + new[] { 9.222152f, 10.197294f, -39.375725f, -2.939809f, 0.000000f, 1.835979f }, + new[] { 8.638441f, 10.197294f, -39.001808f, -2.918552f, 0.000000f, 1.869585f }, + new[] { 8.059751f, 10.197294f, -38.620167f, -2.893450f, 0.000000f, 1.908203f }, + new[] { 7.536088f, 10.197294f, -38.213108f, -2.618314f, 0.000000f, 2.035301f }, + new[] { 7.008043f, 10.197294f, -37.798481f, -2.640225f, 0.000000f, 2.073141f }, + new[] { 6.484523f, 10.197294f, -37.378155f, -2.617601f, 0.000000f, 2.101635f }, + new[] { 5.957591f, 10.197294f, -36.962112f, -2.634658f, 0.000000f, 2.080211f }, + new[] { 5.458399f, 10.197294f, -36.525387f, -2.495958f, 0.000000f, 2.183624f }, + new[] { 4.952830f, 10.197294f, -36.083633f, -2.527847f, 0.000000f, 2.208776f }, + new[] { 4.445582f, 10.197294f, -35.638187f, -2.536238f, 0.000000f, 2.227235f }, + new[] { 3.997866f, 10.197294f, -35.100090f, -2.238582f, 0.000000f, 2.690493f }, + new[] { 3.604921f, 10.197294f, -34.520786f, -1.964725f, 0.000000f, 2.896524f }, + new[] { 3.216267f, 10.197294f, -33.938595f, -1.943270f, 0.000000f, 2.910962f }, + new[] { 2.839496f, 10.197294f, -33.348644f, -1.883856f, 0.000000f, 2.949760f }, + new[] { 2.482899f, 10.197294f, -32.746284f, -1.782983f, 0.000000f, 3.011806f }, + new[] { 2.165045f, 10.197294f, -32.122612f, -1.589272f, 0.000000f, 3.118367f }, + new[] { 1.968905f, 10.197294f, -31.542366f, -0.980699f, 0.000000f, 2.901230f }, + new[] { 1.830858f, 10.197294f, -30.899487f, -0.690238f, 0.000000f, 3.214399f }, + new[] { 1.790072f, 10.197294f, -30.240873f, -0.203930f, 0.000000f, 3.293068f }, + new[] { 1.860591f, 10.197294f, -29.561634f, 0.352598f, 0.000000f, 3.396194f }, + new[] { 2.125831f, 10.197294f, -28.913832f, 1.326197f, 0.000000f, 3.239013f }, + new[] { 2.391070f, 10.197294f, -28.266029f, 1.326197f, 0.000000f, 3.239013f }, + new[] { 2.656309f, 10.197294f, -27.618227f, 1.326197f, 0.000000f, 3.239013f }, + new[] { 2.921549f, 10.197294f, -26.970425f, 1.326197f, 0.000000f, 3.239013f }, + new[] { 3.186788f, 10.197294f, -26.322622f, 1.326197f, 0.000000f, 3.239013f }, + new[] { 3.452027f, 10.197294f, -25.674820f, 1.326197f, 0.000000f, 3.239013f }, + new[] { 3.717267f, 10.197294f, -25.027018f, 1.326197f, 0.000000f, 3.239013f }, + new[] { 3.982506f, 10.197294f, -24.379215f, 1.326197f, 0.000000f, 3.239013f }, + new[] { 4.247745f, 10.197294f, -23.731413f, 1.326197f, 0.000000f, 3.239013f }, + new[] { 4.512984f, 10.197294f, -23.083611f, 1.326197f, 0.000000f, 3.239013f }, + new[] { 4.778224f, 10.197294f, -22.435808f, 1.326197f, 0.000000f, 3.239013f }, + new[] { 5.043463f, 10.197294f, -21.788006f, 1.326197f, 0.000000f, 3.239013f }, + new[] { 5.308702f, 10.197294f, -21.140203f, 1.326197f, 0.000000f, 3.239013f }, + new[] { 5.573941f, 10.197294f, -20.492401f, 1.326197f, 0.000000f, 3.239013f }, + new[] { 5.839180f, 10.197294f, -19.844599f, 1.326197f, 0.000000f, 3.239012f }, + new[] { 6.173420f, 10.197294f, -19.342373f, 1.706390f, 0.000000f, 2.813494f }, + new[] { 6.501637f, 10.197294f, -19.076807f, 1.451270f, 0.000000f, 2.463549f }, + new[] { 6.803561f, 10.197294f, -18.729990f, 1.002026f, 0.000000f, 2.679962f }, + new[] { 6.799414f, 10.197294f, -18.366598f, -0.993883f, 0.000000f, 1.390529f }, + new[] { 6.990967f, 10.197294f, -18.240343f, 2.109758f, 0.000000f, 0.180499f }, + new[] { 7.157530f, 10.197294f, -18.320511f, 0.758936f, 0.000000f, 0.228570f }, + new[] { 7.349619f, 10.197294f, -18.347782f, 0.926001f, 0.000000f, 0.008640f }, + new[] { 7.448954f, 10.197294f, -18.390112f, 0.496676f, 0.000000f, -0.211644f }, + new[] { 7.524421f, 10.197294f, -18.477961f, 0.377335f, 0.000000f, -0.439247f }, + new[] { 7.628986f, 10.197294f, -18.484478f, 0.522823f, 0.000000f, -0.032589f }, + new[] { 7.751089f, 10.197294f, -18.466608f, 0.610513f, 0.000000f, 0.089349f }, + new[] { 7.891870f, 10.197294f, -18.490461f, 0.703905f, 0.000000f, -0.119263f }, + new[] { 8.032493f, 10.197294f, -18.515228f, 0.703115f, 0.000000f, -0.123836f }, + new[] { 8.172967f, 10.1862135f, -18.540827f, 0.702371f, 0.000000f, -0.127992f }, + new[] { 8.182732f, 10.1862135f, -18.626057f, 0.048824f, 0.000000f, -0.426151f }, + new[] { 8.165586f, 10.188671f, -18.846558f, -0.085729f, 0.000000f, -1.102502f }, + new[] { 8.048562f, 10.197294f, -18.763044f, -0.785845f, 0.000000f, 1.028553f }, + new[] { 7.802718f, 10.197294f, -18.943884f, -1.229222f, 0.000000f, -0.904202f }, + new[] { 7.655485f, 10.197294f, -19.137533f, -0.736162f, 0.000000f, -0.968250f }, + new[] { 7.542105f, 10.197294f, -19.244339f, -0.566899f, 0.000000f, -0.534033f }, + new[] { 7.427822f, 10.197294f, -19.031147f, -0.572634f, 0.000000f, 1.497748f }, + new[] { 7.339287f, 10.197294f, -18.757444f, -0.442677f, 0.000000f, 1.368514f }, + new[] { 7.255370f, 10.197294f, -18.471191f, -0.419582f, 0.000000f, 1.431268f }, + new[] { 7.153419f, 10.197294f, -18.314838f, -0.509755f, 0.000000f, 0.781767f }, + new[] { 7.129016f, 10.197294f, -18.194052f, -0.122013f, 0.000000f, 0.603930f }, + new[] { 7.099013f, 10.197294f, -18.064573f, -0.150015f, 0.000000f, 0.647393f }, + new[] { 7.085871f, 10.197294f, -17.946556f, -0.065714f, 0.000000f, 0.590090f }, + new[] { 7.067722f, 10.197294f, -17.801628f, -0.090745f, 0.000000f, 0.724639f }, + new[] { 7.048226f, 10.197294f, -17.673695f, -0.097479f, 0.000000f, 0.639666f } }; + + static readonly float[][] EXPECTED_A1Q2TVTAS = { + new[] { 23.253357f, 10.197294f, -46.279934f, 0.074597f, 0.000000f, -0.017069f }, + new[] { 23.336805f, 10.197294f, -46.374985f, 0.119401f, 0.000000f, -0.031009f }, + new[] { 23.351542f, 10.197294f, -46.410728f, 0.053426f, 0.000000f, -0.093556f }, + new[] { 23.362417f, 10.197294f, -46.429638f, 0.054377f, 0.000000f, -0.094549f }, + new[] { 23.216995f, 10.197294f, -46.446442f, -0.727108f, 0.000000f, -0.084018f }, + new[] { 22.902132f, 10.197294f, -46.426094f, -1.574317f, 0.000000f, 0.101733f }, + new[] { 22.491152f, 10.197294f, -46.376247f, -2.054897f, 0.000000f, 0.249239f }, + new[] { 22.058567f, 10.197294f, -46.289536f, -2.162925f, 0.000000f, 0.433569f }, + new[] { 21.588501f, 10.197294f, -46.183846f, -2.350327f, 0.000000f, 0.528449f }, + new[] { 21.116070f, 10.197294f, -46.072765f, -2.362152f, 0.000000f, 0.555402f }, + new[] { 20.642447f, 10.197294f, -45.954391f, -2.368116f, 0.000000f, 0.591867f }, + new[] { 20.168785f, 10.197294f, -45.827782f, -2.368307f, 0.000000f, 0.633050f }, + new[] { 19.695480f, 10.197294f, -45.692959f, -2.366527f, 0.000000f, 0.674118f }, + new[] { 19.221388f, 10.197294f, -45.549664f, -2.370462f, 0.000000f, 0.716482f }, + new[] { 18.747715f, 10.197294f, -45.401028f, -2.368369f, 0.000000f, 0.743186f }, + new[] { 18.274532f, 10.197294f, -45.247219f, -2.365911f, 0.000000f, 0.769049f }, + new[] { 17.801916f, 10.197294f, -45.088371f, -2.363083f, 0.000000f, 0.794239f }, + new[] { 17.329977f, 10.197294f, -44.924515f, -2.359692f, 0.000000f, 0.819281f }, + new[] { 16.858923f, 10.197294f, -44.755489f, -2.355273f, 0.000000f, 0.845136f }, + new[] { 16.389112f, 10.197294f, -44.580818f, -2.349057f, 0.000000f, 0.873354f }, + new[] { 15.904601f, 10.197294f, -44.397808f, -2.422558f, 0.000000f, 0.915057f }, + new[] { 15.422479f, 10.197294f, -44.204182f, -2.410610f, 0.000000f, 0.968130f }, + new[] { 14.943066f, 10.197294f, -44.000332f, -2.397065f, 0.000000f, 1.019240f }, + new[] { 14.466599f, 10.197294f, -43.786751f, -2.382330f, 0.000000f, 1.067903f }, + new[] { 13.993264f, 10.197294f, -43.563339f, -2.366675f, 0.000000f, 1.117066f }, + new[] { 13.539879f, 10.197294f, -43.333740f, -2.266925f, 0.000000f, 1.147989f }, + new[] { 13.089582f, 10.197294f, -43.096500f, -2.251482f, 0.000000f, 1.186200f }, + new[] { 12.642441f, 10.197294f, -42.852135f, -2.235710f, 0.000000f, 1.221827f }, + new[] { 12.198518f, 10.197294f, -42.601135f, -2.219616f, 0.000000f, 1.254998f }, + new[] { 11.757890f, 10.197294f, -42.343952f, -2.203139f, 0.000000f, 1.285906f }, + new[] { 11.320659f, 10.197294f, -42.080994f, -2.186156f, 0.000000f, 1.314789f }, + new[] { 10.886962f, 10.197294f, -41.812611f, -2.168484f, 0.000000f, 1.341919f }, + new[] { 10.456985f, 10.197294f, -41.539093f, -2.149882f, 0.000000f, 1.367595f }, + new[] { 10.030977f, 10.197294f, -41.260666f, -2.130040f, 0.000000f, 1.392133f }, + new[] { 9.609450f, 10.197294f, -40.977509f, -2.107634f, 0.000000f, 1.415778f }, + new[] { 9.193022f, 10.197294f, -40.690060f, -2.082145f, 0.000000f, 1.437238f }, + new[] { 8.782290f, 10.197294f, -40.398022f, -2.053656f, 0.000000f, 1.460193f }, + new[] { 8.378143f, 10.197294f, -40.101349f, -2.020734f, 0.000000f, 1.483373f }, + new[] { 7.981577f, 10.197294f, -39.799294f, -1.982833f, 0.000000f, 1.510276f }, + new[] { 7.592501f, 10.197294f, -39.491383f, -1.945382f, 0.000000f, 1.539564f }, + new[] { 7.235472f, 10.197294f, -39.191612f, -1.785145f, 0.000000f, 1.498847f }, + new[] { 6.856690f, 10.197294f, -38.869774f, -1.893909f, 0.000000f, 1.609192f }, + new[] { 6.501227f, 10.197294f, -38.570526f, -1.777313f, 0.000000f, 1.496231f }, + new[] { 6.224775f, 10.197294f, -38.301174f, -1.382261f, 0.000000f, 1.346764f }, + new[] { 5.929498f, 10.197294f, -37.995701f, -1.476386f, 0.000000f, 1.527363f }, + new[] { 5.629647f, 10.197294f, -37.639618f, -1.499255f, 0.000000f, 1.780412f }, + new[] { 5.328491f, 10.197294f, -37.296764f, -1.505784f, 0.000000f, 1.714271f }, + new[] { 5.023998f, 10.197294f, -36.908840f, -1.522466f, 0.000000f, 1.939620f }, + new[] { 4.744634f, 10.197294f, -36.502831f, -1.396819f, 0.000000f, 2.030053f }, + new[] { 4.492529f, 10.197294f, -36.078068f, -1.260521f, 0.000000f, 2.123809f }, + new[] { 4.239073f, 10.197294f, -35.631050f, -1.267281f, 0.000000f, 2.235098f }, + new[] { 3.989349f, 10.197294f, -35.178169f, -1.248621f, 0.000000f, 2.264413f }, + new[] { 3.736778f, 10.197294f, -34.723938f, -1.262857f, 0.000000f, 2.271152f }, + new[] { 3.491473f, 10.197294f, -34.264828f, -1.226526f, 0.000000f, 2.295557f }, + new[] { 3.177553f, 10.197294f, -33.848518f, -1.569597f, 0.000000f, 2.081553f }, + new[] { 2.866278f, 10.197294f, -33.427658f, -1.556375f, 0.000000f, 2.104302f }, + new[] { 2.566121f, 10.197294f, -32.995960f, -1.500786f, 0.000000f, 2.158491f }, + new[] { 2.291139f, 10.197294f, -32.544334f, -1.374910f, 0.000000f, 2.258132f }, + new[] { 2.066509f, 10.197294f, -32.063156f, -1.123153f, 0.000000f, 2.405891f }, + new[] { 1.949471f, 10.197294f, -31.544592f, -0.585186f, 0.000000f, 2.592823f }, + new[] { 1.841830f, 10.197294f, -31.020411f, -0.538206f, 0.000000f, 2.620903f }, + new[] { 1.804908f, 10.197294f, -30.484823f, -0.184613f, 0.000000f, 2.677938f }, + new[] { 1.848811f, 10.197294f, -29.937574f, 0.219515f, 0.000000f, 2.736246f }, + new[] { 1.946300f, 10.197294f, -29.417610f, 0.487449f, 0.000000f, 2.599819f }, + new[] { 2.111085f, 10.197294f, -28.884066f, 0.823926f, 0.000000f, 2.667721f }, + new[] { 2.288109f, 10.197294f, -28.358007f, 0.885119f, 0.000000f, 2.630293f }, + new[] { 2.499196f, 10.197294f, -27.812208f, 1.055435f, 0.000000f, 2.728995f }, + new[] { 2.709832f, 10.197294f, -27.265604f, 1.053180f, 0.000000f, 2.733017f }, + new[] { 2.920712f, 10.197294f, -26.718000f, 1.054401f, 0.000000f, 2.738015f }, + new[] { 3.166102f, 10.197294f, -26.183590f, 1.226950f, 0.000000f, 2.672051f }, + new[] { 3.409404f, 10.197294f, -25.647873f, 1.216508f, 0.000000f, 2.678585f }, + new[] { 3.604466f, 10.197294f, -25.169361f, 0.975311f, 0.000000f, 2.392557f }, + new[] { 3.799465f, 10.197294f, -24.688942f, 0.974994f, 0.000000f, 2.402100f }, + new[] { 4.008917f, 10.197294f, -24.179743f, 1.047259f, 0.000000f, 2.545998f }, + new[] { 4.254181f, 10.197294f, -23.667099f, 1.226322f, 0.000000f, 2.563215f }, + new[] { 4.411407f, 10.197294f, -23.188858f, 0.786131f, 0.000000f, 2.391205f }, + new[] { 4.630627f, 10.197294f, -22.706358f, 1.096096f, 0.000000f, 2.412497f }, + new[] { 4.733336f, 10.197294f, -22.348816f, 0.513548f, 0.000000f, 1.787710f }, + new[] { 4.886934f, 10.197294f, -22.007650f, 0.767987f, 0.000000f, 1.705831f }, + new[] { 4.930230f, 10.197294f, -21.656166f, 0.216483f, 0.000000f, 1.757425f }, + new[] { 5.044151f, 10.197294f, -21.343338f, 0.569606f, 0.000000f, 1.564138f }, + new[] { 5.169365f, 10.197294f, -21.074562f, 0.626069f, 0.000000f, 1.343884f }, + new[] { 5.241940f, 10.197294f, -20.714607f, 0.362874f, 0.000000f, 1.799778f }, + new[] { 5.323618f, 10.197294f, -20.576834f, 0.408392f, 0.000000f, 0.688868f }, + new[] { 5.316272f, 10.197294f, -20.384502f, -0.036732f, 0.000000f, 0.961652f }, + new[] { 5.301373f, 10.197294f, -20.182859f, -0.074500f, 0.000000f, 1.008217f }, + new[] { 5.295803f, 10.197294f, -19.982578f, -0.027847f, 0.000000f, 1.001405f }, + new[] { 5.286469f, 10.197294f, -19.742945f, -0.046671f, 0.000000f, 1.198163f }, + new[] { 5.275438f, 10.197294f, -19.603636f, -0.055155f, 0.000000f, 0.696540f }, + new[] { 5.288567f, 10.197294f, -19.521788f, 0.065643f, 0.000000f, 0.409236f }, + new[] { 5.440337f, 10.197294f, -19.479048f, 0.758851f, 0.000000f, 0.213696f }, + new[] { 5.633360f, 10.197294f, -19.464310f, 0.965118f, 0.000000f, 0.073694f }, + new[] { 5.825089f, 10.197294f, -19.461432f, 0.958646f, 0.000000f, 0.014389f }, + new[] { 5.962496f, 10.197294f, -19.404169f, 0.687035f, 0.000000f, 0.286309f }, + new[] { 6.051316f, 10.197294f, -19.417604f, 0.444099f, 0.000000f, -0.067175f }, + new[] { 6.134418f, 10.197294f, -19.466887f, 0.415510f, 0.000000f, -0.246414f }, + new[] { 6.195343f, 10.197294f, -19.520611f, 0.304628f, 0.000000f, -0.268624f }, + new[] { 6.233473f, 10.197294f, -19.589584f, 0.190648f, 0.000000f, -0.344869f }, + new[] { 6.143032f, 10.197294f, -19.644495f, -0.452208f, 0.000000f, -0.274552f }, + new[] { 6.066758f, 10.197294f, -19.669443f, -0.381368f, 0.000000f, -0.124736f }, + new[] { 5.988646f, 10.197294f, -19.691917f, -0.390560f, 0.000000f, -0.112371f }, + new[] { 5.880603f, 10.197294f, -19.641851f, -0.540214f, 0.000000f, 0.250327f }, + new[] { 5.768869f, 10.197294f, -19.553535f, -0.558670f, 0.000000f, 0.441585f }, + new[] { 5.773450f, 10.197294f, -19.545095f, 0.022906f, 0.000000f, 0.042200f }, + new[] { 5.764818f, 10.197294f, -19.506109f, -0.043161f, 0.000000f, 0.194928f }, + new[] { 5.769928f, 10.197294f, -19.497072f, 0.025550f, 0.000000f, 0.045183f }, + new[] { 5.753877f, 10.197294f, -19.524942f, -0.080258f, 0.000000f, -0.139354f }, + new[] { 5.731219f, 10.197294f, -19.533819f, -0.113286f, 0.000000f, -0.044384f } }; + + static readonly float[][] EXPECTED_A1Q2T = { + new[] { 22.990597f, 10.197294f, -46.112606f, -2.999564f, 0.000000f, 1.803501f }, + new[] { 22.524744f, 10.197294f, -45.867702f, -2.989815f, 0.000000f, 1.819617f }, + new[] { 21.946421f, 10.197294f, -45.530769f, -2.987121f, 0.000000f, 1.824035f }, + new[] { 21.357445f, 10.197294f, -45.183083f, -2.985955f, 0.000000f, 1.825945f }, + new[] { 20.766855f, 10.197294f, -44.833454f, -2.985027f, 0.000000f, 1.827461f }, + new[] { 20.176287f, 10.197294f, -44.483444f, -2.984109f, 0.000000f, 1.828960f }, + new[] { 19.586048f, 10.197294f, -44.133289f, -2.983157f, 0.000000f, 1.830513f }, + new[] { 18.996214f, 10.197294f, -43.783005f, -2.982163f, 0.000000f, 1.832130f }, + new[] { 18.406816f, 10.197294f, -43.432552f, -2.981127f, 0.000000f, 1.833817f }, + new[] { 17.817883f, 10.197294f, -43.081890f, -2.980047f, 0.000000f, 1.835571f }, + new[] { 17.229431f, 10.197294f, -42.730961f, -2.978924f, 0.000000f, 1.837393f }, + new[] { 16.641487f, 10.197294f, -42.379704f, -2.977759f, 0.000000f, 1.839280f }, + new[] { 16.054066f, 10.197294f, -42.028053f, -2.976555f, 0.000000f, 1.841228f }, + new[] { 15.467182f, 10.197294f, -41.675930f, -2.975313f, 0.000000f, 1.843233f }, + new[] { 14.880149f, 10.197294f, -41.325222f, -2.974040f, 0.000000f, 1.845288f }, + new[] { 14.292006f, 10.197294f, -40.972252f, -2.972459f, 0.000000f, 1.847835f }, + new[] { 13.702817f, 10.197294f, -40.616268f, -2.970898f, 0.000000f, 1.850342f }, + new[] { 13.113760f, 10.197294f, -40.258976f, -2.969465f, 0.000000f, 1.852640f }, + new[] { 12.525526f, 10.197294f, -39.902176f, -2.968050f, 0.000000f, 1.854907f }, + new[] { 11.938206f, 10.197294f, -39.545849f, -2.966453f, 0.000000f, 1.857459f }, + new[] { 11.351962f, 10.197294f, -39.190098f, -2.964650f, 0.000000f, 1.860337f }, + new[] { 10.767020f, 10.197294f, -38.835106f, -2.962592f, 0.000000f, 1.863611f }, + new[] { 10.183690f, 10.197294f, -38.481129f, -2.960215f, 0.000000f, 1.867385f }, + new[] { 9.602149f, 10.197294f, -38.128098f, -2.957422f, 0.000000f, 1.871805f }, + new[] { 9.023255f, 10.197294f, -37.777180f, -2.954136f, 0.000000f, 1.876987f }, + new[] { 8.448158f, 10.197294f, -37.429573f, -2.950055f, 0.000000f, 1.883394f }, + new[] { 7.879329f, 10.197294f, -37.087776f, -2.944782f, 0.000000f, 1.891628f }, + new[] { 7.319548f, 10.197294f, -36.753658f, -2.937588f, 0.000000f, 1.902782f }, + new[] { 6.776391f, 10.197294f, -36.434063f, -2.927629f, 0.000000f, 1.918068f }, + new[] { 6.269217f, 10.197294f, -36.142403f, -2.912834f, 0.000000f, 1.940462f }, + new[] { 5.846630f, 10.197294f, -35.881496f, -2.890513f, 0.000000f, 1.973559f }, + new[] { 5.411777f, 10.197294f, -35.547710f, -2.874764f, 0.000000f, 1.996430f }, + new[] { 4.896369f, 10.197294f, -35.184120f, -2.896978f, 0.000000f, 1.964057f }, + new[] { 4.367188f, 10.197294f, -34.833569f, -2.910470f, 0.000000f, 1.944008f }, + new[] { 3.807542f, 10.197294f, -34.472218f, -2.906039f, 0.000000f, 1.950624f }, + new[] { 3.238266f, 10.197294f, -34.064877f, -2.846376f, 0.000000f, 2.036700f }, + new[] { 2.917057f, 10.197294f, -33.455391f, -1.420109f, 0.000000f, 3.198952f }, + new[] { 2.645415f, 10.197294f, -32.810249f, -1.358214f, 0.000000f, 3.225718f }, + new[] { 2.373772f, 10.197294f, -32.165104f, -1.358212f, 0.000000f, 3.225718f }, + new[] { 2.103753f, 10.197294f, -31.530035f, -1.358215f, 0.000000f, 3.225717f }, + new[] { 1.837103f, 10.197294f, -30.882812f, -1.333248f, 0.000000f, 3.236116f }, + new[] { 1.887709f, 10.197294f, -30.193766f, 0.449056f, 0.000000f, 3.471073f }, + new[] { 2.034543f, 10.197294f, -29.509338f, 0.734173f, 0.000000f, 3.422132f }, + new[] { 2.266320f, 10.197294f, -28.848824f, 1.158885f, 0.000000f, 3.302572f }, + new[] { 2.498098f, 10.197294f, -28.188309f, 1.158885f, 0.000000f, 3.302572f }, + new[] { 2.729875f, 10.197294f, -27.527794f, 1.158886f, 0.000000f, 3.302572f }, + new[] { 2.992905f, 10.197294f, -26.879091f, 1.315149f, 0.000000f, 3.243514f }, + new[] { 3.255934f, 10.197294f, -26.230389f, 1.315149f, 0.000000f, 3.243514f }, + new[] { 3.518964f, 10.197294f, -25.581686f, 1.315149f, 0.000000f, 3.243514f }, + new[] { 3.781994f, 10.197294f, -24.932983f, 1.315149f, 0.000000f, 3.243514f }, + new[] { 4.045024f, 10.197294f, -24.284281f, 1.315149f, 0.000000f, 3.243514f }, + new[] { 4.308054f, 10.197294f, -23.635578f, 1.315149f, 0.000000f, 3.243514f }, + new[] { 4.571084f, 10.197294f, -22.986876f, 1.315149f, 0.000000f, 3.243514f }, + new[] { 4.834114f, 10.197294f, -22.338173f, 1.315149f, 0.000000f, 3.243514f }, + new[] { 5.097065f, 10.197294f, -21.689661f, 1.315149f, 0.000000f, 3.243514f }, + new[] { 5.349040f, 10.197294f, -21.068123f, 1.315149f, 0.000000f, 3.243514f }, + new[] { 5.564819f, 10.197294f, -20.535385f, 1.315191f, 0.000000f, 3.243497f }, + new[] { 5.742907f, 10.197294f, -20.094234f, 1.315492f, 0.000000f, 3.243375f }, + new[] { 5.844197f, 10.197294f, -19.830488f, 1.316821f, 0.000000f, 3.242836f }, + new[] { 5.890507f, 10.197294f, -19.651239f, 1.327608f, 0.000000f, 3.238434f }, + new[] { 5.895338f, 10.197294f, -19.433264f, 1.384180f, 0.000000f, 3.214661f }, + new[] { 5.917773f, 10.197294f, -19.188416f, 1.594034f, 0.000000f, 3.115936f }, + new[] { 5.947361f, 10.197294f, -19.047245f, 1.574677f, 0.000000f, 2.491868f }, + new[] { 5.960892f, 10.197294f, -18.990824f, 1.488381f, 0.000000f, 2.080121f }, + new[] { 5.971087f, 10.197294f, -18.963556f, 1.448915f, 0.000000f, 1.915559f }, + new[] { 5.977089f, 10.197294f, -18.950905f, 1.419177f, 0.000000f, 1.836029f } }; + + [Test] + public void testAgent1Quality2TVTA() { + int updateFlags = CrowdAgentParams.DT_CROWD_ANTICIPATE_TURNS | CrowdAgentParams.DT_CROWD_OPTIMIZE_VIS + | CrowdAgentParams.DT_CROWD_OPTIMIZE_TOPO | CrowdAgentParams.DT_CROWD_OBSTACLE_AVOIDANCE; + + addAgentGrid(2, 0.3f, updateFlags, 2, startPoss[0]); + setMoveTarget(endPoss[0], false); + for (int i = 0; i < EXPECTED_A1Q2TVTA.Length; i++) { + crowd.update(1 / 5f, null); + CrowdAgent ag = agents[2]; + Assert.That(ag.npos[0], Is.EqualTo(EXPECTED_A1Q2TVTA[i][0]).Within(0.001f), $"{i}"); + Assert.That(ag.npos[1], Is.EqualTo(EXPECTED_A1Q2TVTA[i][1]).Within(0.001f), $"{i}"); + Assert.That(ag.npos[2], Is.EqualTo(EXPECTED_A1Q2TVTA[i][2]).Within(0.001f), $"{i}"); + Assert.That(ag.nvel[0], Is.EqualTo(EXPECTED_A1Q2TVTA[i][3]).Within(0.001f), $"{i}"); + Assert.That(ag.nvel[1], Is.EqualTo(EXPECTED_A1Q2TVTA[i][4]).Within(0.001f), $"{i}"); + Assert.That(ag.nvel[2], Is.EqualTo(EXPECTED_A1Q2TVTA[i][5]).Within(0.001f), $"{i}"); + } + } + + [Test] + public void testAgent1Quality2TVTAS() { + int updateFlags = CrowdAgentParams.DT_CROWD_ANTICIPATE_TURNS | CrowdAgentParams.DT_CROWD_OPTIMIZE_VIS + | CrowdAgentParams.DT_CROWD_OPTIMIZE_TOPO | CrowdAgentParams.DT_CROWD_OBSTACLE_AVOIDANCE + | CrowdAgentParams.DT_CROWD_SEPARATION; + + addAgentGrid(2, 0.3f, updateFlags, 2, startPoss[0]); + setMoveTarget(endPoss[0], false); + for (int i = 0; i < EXPECTED_A1Q2TVTAS.Length; i++) { + crowd.update(1 / 5f, null); + CrowdAgent ag = agents[2]; + Assert.That(ag.npos[0], Is.EqualTo(EXPECTED_A1Q2TVTAS[i][0]).Within(0.001f), $"{i}"); + Assert.That(ag.npos[1], Is.EqualTo(EXPECTED_A1Q2TVTAS[i][1]).Within(0.001f), $"{i}"); + Assert.That(ag.npos[2], Is.EqualTo(EXPECTED_A1Q2TVTAS[i][2]).Within(0.001f), $"{i}"); + Assert.That(ag.nvel[0], Is.EqualTo(EXPECTED_A1Q2TVTAS[i][3]).Within(0.001f), $"{i}"); + Assert.That(ag.nvel[1], Is.EqualTo(EXPECTED_A1Q2TVTAS[i][4]).Within(0.001f), $"{i}"); + Assert.That(ag.nvel[2], Is.EqualTo(EXPECTED_A1Q2TVTAS[i][5]).Within(0.001f), $"{i}"); + } + } + + [Test] + public void testAgent1Quality2T() { + int updateFlags = CrowdAgentParams.DT_CROWD_OPTIMIZE_TOPO; + + addAgentGrid(2, 0.3f, updateFlags, 2, startPoss[0]); + setMoveTarget(endPoss[0], false); + for (int i = 0; i < EXPECTED_A1Q2T.Length; i++) + { + if (i == 37) + { + int a = 3; + } + + crowd.update(1 / 5f, null); + CrowdAgent ag = agents[2]; + Assert.That(ag.npos[0], Is.EqualTo(EXPECTED_A1Q2T[i][0]).Within(0.00001f), $"{i} - {ag.npos[0]} {EXPECTED_A1Q2T[i][0]}"); Console.WriteLine($"{i} - {ag.npos[0]} {EXPECTED_A1Q2T[i][0]}"); + Assert.That(ag.npos[1], Is.EqualTo(EXPECTED_A1Q2T[i][1]).Within(0.00001f), $"{i} - {ag.npos[1]} {EXPECTED_A1Q2T[i][1]}"); Console.WriteLine($"{i} - {ag.npos[1]} {EXPECTED_A1Q2T[i][1]}"); + Assert.That(ag.npos[2], Is.EqualTo(EXPECTED_A1Q2T[i][2]).Within(0.00001f), $"{i} - {ag.npos[2]} {EXPECTED_A1Q2T[i][2]}"); Console.WriteLine($"{i} - {ag.npos[2]} {EXPECTED_A1Q2T[i][2]}"); + Assert.That(ag.nvel[0], Is.EqualTo(EXPECTED_A1Q2T[i][3]).Within(0.00001f), $"{i} - {ag.nvel[0]} {EXPECTED_A1Q2T[i][3]}"); Console.WriteLine($"{i} - {ag.nvel[0]} {EXPECTED_A1Q2T[i][3]}"); + Assert.That(ag.nvel[1], Is.EqualTo(EXPECTED_A1Q2T[i][4]).Within(0.00001f), $"{i} - {ag.nvel[1]} {EXPECTED_A1Q2T[i][4]}"); Console.WriteLine($"{i} - {ag.nvel[1]} {EXPECTED_A1Q2T[i][4]}"); + Assert.That(ag.nvel[2], Is.EqualTo(EXPECTED_A1Q2T[i][5]).Within(0.00001f), $"{i} - {ag.nvel[2]} {EXPECTED_A1Q2T[i][5]}"); Console.WriteLine($"{i} - {ag.nvel[2]} {EXPECTED_A1Q2T[i][5]}"); + Thread.Sleep(1); + } + } +} diff --git a/test/DotRecast.Detour.Crowd.Test/Crowd4VelocityTest.cs b/test/DotRecast.Detour.Crowd.Test/Crowd4VelocityTest.cs new file mode 100644 index 0000000..d565e60 --- /dev/null +++ b/test/DotRecast.Detour.Crowd.Test/Crowd4VelocityTest.cs @@ -0,0 +1,118 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using NUnit.Framework; + +namespace DotRecast.Detour.Crowd.Test; + +public class Crowd4VelocityTest : AbstractCrowdTest { + + static readonly float[][] EXPECTED_A1Q3TVTA = { + + new[] { 6.101694f, 10.197294f, -17.678480f, 0.000000f, 0.000000f, 0.000000f }, + new[] { 6.024141f, 10.197294f, -17.589798f, -0.107331f, 0.000000f, 0.098730f }, + new[] { 6.004839f, 10.197294f, -17.554886f, -0.096506f, 0.000000f, 0.174561f }, + new[] { 5.744515f, 10.197294f, -17.309479f, -2.590961f, 0.000000f, 2.353066f }, + new[] { 5.253671f, 10.197294f, -16.842125f, -2.534360f, 0.000000f, 2.413922f }, + new[] { 4.789658f, 10.197294f, -16.318014f, -2.320063f, 0.000000f, 2.620555f }, + new[] { 4.407527f, 10.197294f, -15.731520f, -1.910654f, 0.000000f, 2.932474f }, + new[] { 4.023476f, 10.197294f, -15.146280f, -1.920256f, 0.000000f, 2.926195f }, + new[] { 3.645756f, 10.197294f, -14.556935f, -1.888601f, 0.000000f, 2.946725f }, + new[] { 3.277534f, 10.197294f, -13.961610f, -1.841108f, 0.000000f, 2.976629f }, + new[] { 2.924562f, 10.197294f, -13.357118f, -1.764861f, 0.000000f, 3.022460f }, + new[] { 2.598673f, 10.197294f, -12.737605f, -1.629447f, 0.000000f, 3.097564f }, + new[] { 2.330214f, 10.197294f, -12.091130f, -1.342291f, 0.000000f, 3.232376f }, + new[] { 2.174726f, 10.197294f, -11.408617f, -0.777438f, 0.000000f, 3.412564f }, + new[] { 2.040282f, 10.197294f, -10.721649f, -0.672225f, 0.000000f, 3.434838f }, + new[] { 1.958181f, 10.197294f, -10.026481f, -0.410503f, 0.000000f, 3.475843f }, + new[] { 1.653389f, 10.197294f, -9.396320f, -1.523958f, 0.000000f, 3.150802f }, + new[] { 1.642715f, 10.197294f, -8.696402f, -0.053371f, 0.000000f, 3.499593f }, + new[] { 1.937517f, 10.197294f, -8.091795f, 2.033996f, 0.000000f, 2.848308f }, + new[] { 2.364934f, 10.197294f, -7.537435f, 2.137084f, 0.000000f, 2.771799f }, + new[] { 2.802262f, 10.197294f, -6.990860f, 2.186641f, 0.000000f, 2.732875f }, + new[] { 3.186367f, 10.197294f, -6.759828f, 1.617082f, 0.000000f, -0.643841f }, + new[] { 3.460433f, 10.197294f, -6.829281f, 1.002684f, 0.000000f, -1.351205f }, + new[] { 3.605715f, 10.197294f, -6.794649f, 0.726412f, 0.000000f, 0.173160f }, + new[] { 3.796394f, 10.197294f, -6.840563f, 0.953395f, 0.000000f, -0.229571f }, + new[] { 3.882745f, 10.197294f, -6.956440f, 0.431757f, 0.000000f, -0.579388f }, + new[] { 3.983807f, 10.197294f, -7.160242f, 0.505308f, 0.000000f, -1.019009f }, + new[] { 4.031534f, 10.197294f, -7.358752f, 0.238635f, 0.000000f, -0.992549f }, + new[] { 4.081295f, 10.197294f, -7.517536f, 0.248804f, 0.000000f, -0.793922f }, + new[] { 4.108567f, 10.197294f, -7.630970f, 0.136363f, 0.000000f, -0.567171f }, + new[] { 4.092495f, 10.197294f, -7.727181f, -0.080361f, 0.000000f, -0.481056f }, + new[] { 4.096027f, 10.197294f, -7.807384f, 0.017662f, 0.000000f, -0.401016f }, + new[] { 4.131466f, 10.197294f, -7.874563f, 0.177196f, 0.000000f, -0.335895f }, + new[] { 4.102508f, 10.197294f, -7.917174f, -0.144795f, 0.000000f, -0.213056f }, + new[] { 4.073549f, 10.197294f, -7.959785f, -0.144795f, 0.000000f, -0.213056f }, + new[] { 4.044590f, 10.197294f, -8.002397f, -0.144795f, 0.000000f, -0.213056f }, + new[] { 3.983432f, 10.197294f, -8.032723f, -0.305791f, 0.000000f, -0.151636f }, + new[] { 3.948404f, 10.197294f, -8.050093f, -0.175139f, 0.000000f, -0.086848f }, + new[] { 3.935988f, 10.197294f, -8.078673f, -0.062080f, 0.000000f, -0.142903f }, + new[] { 3.943177f, 10.197294f, -8.091246f, 0.035943f, 0.000000f, -0.062864f }, + new[] { 3.950365f, 10.197294f, -8.103818f, 0.035943f, 0.000000f, -0.062864f }, + new[] { 3.957554f, 10.197294f, -8.116390f, 0.035943f, 0.000000f, -0.062864f }, + new[] { 3.964742f, 10.197294f, -8.128963f, 0.035943f, 0.000000f, -0.062864f }, + new[] { 4.003838f, 10.197294f, -8.128510f, 0.195477f, 0.000000f, 0.002258f }, + new[] { 4.042933f, 10.197294f, -8.128058f, 0.195477f, 0.000000f, 0.002258f }, + new[] { 4.082028f, 10.197294f, -8.127606f, 0.195477f, 0.000000f, 0.002258f }, + new[] { 4.121124f, 10.197294f, -8.127154f, 0.195477f, 0.000000f, 0.002258f }, + new[] { 4.160219f, 10.197294f, -8.126702f, 0.195477f, 0.000000f, 0.002258f }, + new[] { 4.199315f, 10.197294f, -8.126250f, 0.195477f, 0.000000f, 0.002258f }, + new[] { 4.206211f, 10.197294f, -8.113515f, 0.034481f, 0.000000f, 0.063677f }, + new[] { 4.213107f, 10.197294f, -8.100780f, 0.034481f, 0.000000f, 0.063677f }, + new[] { 4.230340f, 10.197294f, -8.092234f, 0.086165f, 0.000000f, 0.042728f }, + new[] { 4.247572f, 10.197294f, -8.083688f, 0.086165f, 0.000000f, 0.042728f }, + new[] { 4.246885f, 10.197294f, -8.098154f, -0.003438f, 0.000000f, -0.072332f }, + new[] { 4.264118f, 10.197294f, -8.089608f, 0.086165f, 0.000000f, 0.042728f }, + new[] { 4.281351f, 10.197294f, -8.081062f, 0.086165f, 0.000000f, 0.042728f }, + new[] { 4.280663f, 10.197294f, -8.095529f, -0.003438f, 0.000000f, -0.072332f }, + new[] { 4.297896f, 10.197294f, -8.086983f, 0.086165f, 0.000000f, 0.042728f }, + new[] { 4.315129f, 10.197294f, -8.078437f, 0.086165f, 0.000000f, 0.042728f }, + new[] { 4.332362f, 10.197294f, -8.069891f, 0.086165f, 0.000000f, 0.042728f }, + new[] { 4.322824f, 10.197294f, -8.046370f, -0.047688f, 0.000000f, 0.117607f }, + new[] { 4.322136f, 10.197294f, -8.060836f, -0.003438f, 0.000000f, -0.072332f }, + new[] { 4.321449f, 10.197294f, -8.075302f, -0.003438f, 0.000000f, -0.072332f }, + new[] { 4.320761f, 10.197294f, -8.089768f, -0.003438f, 0.000000f, -0.072332f }, + new[] { 4.337994f, 10.197294f, -8.081223f, 0.086165f, 0.000000f, 0.042728f }, + new[] { 4.328456f, 10.197294f, -8.057701f, -0.047688f, 0.000000f, 0.117607f }, + new[] { 4.327769f, 10.197294f, -8.072167f, -0.003438f, 0.000000f, -0.072332f } }; + + [Test] + public void testAgent1Quality3TVTA() { + int updateFlags = CrowdAgentParams.DT_CROWD_ANTICIPATE_TURNS | CrowdAgentParams.DT_CROWD_OPTIMIZE_VIS + | CrowdAgentParams.DT_CROWD_OPTIMIZE_TOPO | CrowdAgentParams.DT_CROWD_OBSTACLE_AVOIDANCE; + + addAgentGrid(2, 0.3f, updateFlags, 3, endPoss[0]); + setMoveTarget(endPoss[4], false); + for (int i = 0; i < EXPECTED_A1Q3TVTA.Length; i++) { + crowd.update(1 / 5f, null); + if (i == 20) { + setMoveTarget(startPoss[2], true); + } + CrowdAgent ag = agents[1]; + Assert.That(ag.npos[0], Is.EqualTo(EXPECTED_A1Q3TVTA[i][0]).Within(0.001f)); + Assert.That(ag.npos[1], Is.EqualTo(EXPECTED_A1Q3TVTA[i][1]).Within(0.001f)); + Assert.That(ag.npos[2], Is.EqualTo(EXPECTED_A1Q3TVTA[i][2]).Within(0.001f)); + Assert.That(ag.nvel[0], Is.EqualTo(EXPECTED_A1Q3TVTA[i][3]).Within(0.001f)); + Assert.That(ag.nvel[1], Is.EqualTo(EXPECTED_A1Q3TVTA[i][4]).Within(0.001f)); + Assert.That(ag.nvel[2], Is.EqualTo(EXPECTED_A1Q3TVTA[i][5]).Within(0.001f)); + } + } + +} diff --git a/test/DotRecast.Detour.Crowd.Test/DotRecast.Detour.Crowd.Test.csproj b/test/DotRecast.Detour.Crowd.Test/DotRecast.Detour.Crowd.Test.csproj new file mode 100644 index 0000000..183d983 --- /dev/null +++ b/test/DotRecast.Detour.Crowd.Test/DotRecast.Detour.Crowd.Test.csproj @@ -0,0 +1,28 @@ + + + + net7.0 + + + false + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + diff --git a/test/DotRecast.Detour.Crowd.Test/PathCorridorTest.cs b/test/DotRecast.Detour.Crowd.Test/PathCorridorTest.cs new file mode 100644 index 0000000..f68f7a7 --- /dev/null +++ b/test/DotRecast.Detour.Crowd.Test/PathCorridorTest.cs @@ -0,0 +1,80 @@ +/* +recast4j copyright (c) 2021 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.Collections.Generic; +using Moq; +using NUnit.Framework; + +namespace DotRecast.Detour.Crowd.Test; + +public class PathCorridorTest { + + private readonly PathCorridor corridor = new PathCorridor(); + private readonly QueryFilter filter = new DefaultQueryFilter(); + + [SetUp] + public void setUp() { + corridor.reset(0, new float[] {10,20,30}); + } + + [Test] + public void shouldKeepOriginalPathInFindCornersWhenNothingCanBePruned() { + List straightPath = new(); + straightPath.Add(new StraightPathItem(new float[] { 11, 20, 30.00001f }, 0, 0)); + straightPath.Add(new StraightPathItem(new float[] { 12, 20, 30.00002f }, 0, 0)); + straightPath.Add(new StraightPathItem(new float[] { 11f, 21, 32f }, 0, 0)); + straightPath.Add(new StraightPathItem(new float[] { 11f, 21, 32f }, 0, 0)); + Result> result = Results.success(straightPath); + var mockQuery = new Mock(It.IsAny()); + mockQuery.Setup(q => q.findStraightPath( + It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny()) + ).Returns(result); + List path = corridor.findCorners(int.MaxValue, mockQuery.Object, filter); + Assert.That(path.Count, Is.EqualTo(4)); + Assert.That(path, Is.EqualTo(straightPath)); + } + + [Test] + public void shouldPrunePathInFindCorners() { + List straightPath = new(); + straightPath.Add(new StraightPathItem(new float[] { 10, 20, 30.00001f }, 0, 0)); // too close + straightPath.Add(new StraightPathItem(new float[] { 10, 20, 30.00002f }, 0, 0)); // too close + straightPath.Add(new StraightPathItem(new float[] { 11f, 21, 32f }, 0, 0)); + straightPath.Add(new StraightPathItem(new float[] { 12f, 22, 33f }, NavMeshQuery.DT_STRAIGHTPATH_OFFMESH_CONNECTION, 0)); // offmesh + straightPath.Add(new StraightPathItem(new float[] { 11f, 21, 32f }, NavMeshQuery.DT_STRAIGHTPATH_OFFMESH_CONNECTION, 0)); // offmesh + Result> result = Results.success(straightPath); + + var mockQuery = new Mock(It.IsAny()); + var s = mockQuery.Setup(q => q.findStraightPath( + It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny()) + ).Returns(result); + + List path = corridor.findCorners(int.MaxValue, mockQuery.Object, filter); + Assert.That(path.Count, Is.EqualTo(2)); + Assert.That(path, Is.EqualTo(new List { straightPath[2], straightPath[3] })); + } + +} diff --git a/test/DotRecast.Detour.Crowd.Test/RecastTestMeshBuilder.cs b/test/DotRecast.Detour.Crowd.Test/RecastTestMeshBuilder.cs new file mode 100644 index 0000000..4acd865 --- /dev/null +++ b/test/DotRecast.Detour.Crowd.Test/RecastTestMeshBuilder.cs @@ -0,0 +1,110 @@ +/* +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using DotRecast.Core; +using DotRecast.Recast; +using DotRecast.Recast.Geom; + +namespace DotRecast.Detour.Crowd.Test; + +public class RecastTestMeshBuilder { + + private readonly MeshData meshData; + public const float m_cellSize = 0.3f; + public const float m_cellHeight = 0.2f; + public const float m_agentHeight = 2.0f; + public const float m_agentRadius = 0.6f; + public const float m_agentMaxClimb = 0.9f; + public const float m_agentMaxSlope = 45.0f; + public const int m_regionMinSize = 8; + public const int m_regionMergeSize = 20; + public const float m_edgeMaxLen = 12.0f; + public const float m_edgeMaxError = 1.3f; + public const int m_vertsPerPoly = 6; + public const float m_detailSampleDist = 6.0f; + public const float m_detailSampleMaxError = 1.0f; + + public RecastTestMeshBuilder() : this(ObjImporter.load(Loader.ToBytes("dungeon.obj")), + PartitionType.WATERSHED, m_cellSize, m_cellHeight, m_agentHeight, m_agentRadius, m_agentMaxClimb, m_agentMaxSlope, + m_regionMinSize, m_regionMergeSize, m_edgeMaxLen, m_edgeMaxError, m_vertsPerPoly, m_detailSampleDist, + m_detailSampleMaxError) + { + } + + public RecastTestMeshBuilder(InputGeomProvider m_geom, PartitionType m_partitionType, float m_cellSize, + float m_cellHeight, float m_agentHeight, float m_agentRadius, float m_agentMaxClimb, float m_agentMaxSlope, + int m_regionMinSize, int m_regionMergeSize, float m_edgeMaxLen, float m_edgeMaxError, int m_vertsPerPoly, + float m_detailSampleDist, float m_detailSampleMaxError) { + RecastConfig cfg = new RecastConfig(m_partitionType, m_cellSize, m_cellHeight, m_agentHeight, m_agentRadius, + m_agentMaxClimb, m_agentMaxSlope, m_regionMinSize, m_regionMergeSize, m_edgeMaxLen, m_edgeMaxError, + m_vertsPerPoly, m_detailSampleDist, m_detailSampleMaxError, SampleAreaModifications.SAMPLE_AREAMOD_GROUND); + RecastBuilderConfig bcfg = new RecastBuilderConfig(cfg, m_geom.getMeshBoundsMin(), m_geom.getMeshBoundsMax()); + RecastBuilder rcBuilder = new RecastBuilder(); + RecastBuilderResult rcResult = rcBuilder.build(m_geom, bcfg); + PolyMesh m_pmesh = rcResult.getMesh(); + for (int i = 0; i < m_pmesh.npolys; ++i) { + m_pmesh.flags[i] = 1; + } + PolyMeshDetail m_dmesh = rcResult.getMeshDetail(); + NavMeshDataCreateParams option = new NavMeshDataCreateParams(); + option.verts = m_pmesh.verts; + option.vertCount = m_pmesh.nverts; + option.polys = m_pmesh.polys; + option.polyAreas = m_pmesh.areas; + option.polyFlags = m_pmesh.flags; + option.polyCount = m_pmesh.npolys; + option.nvp = m_pmesh.nvp; + option.detailMeshes = m_dmesh.meshes; + option.detailVerts = m_dmesh.verts; + option.detailVertsCount = m_dmesh.nverts; + option.detailTris = m_dmesh.tris; + option.detailTriCount = m_dmesh.ntris; + option.walkableHeight = m_agentHeight; + option.walkableRadius = m_agentRadius; + option.walkableClimb = m_agentMaxClimb; + option.bmin = m_pmesh.bmin; + option.bmax = m_pmesh.bmax; + option.cs = m_cellSize; + option.ch = m_cellHeight; + option.buildBvTree = true; + + option.offMeshConVerts = new float[6]; + option.offMeshConVerts[0] = 0.1f; + option.offMeshConVerts[1] = 0.2f; + option.offMeshConVerts[2] = 0.3f; + option.offMeshConVerts[3] = 0.4f; + option.offMeshConVerts[4] = 0.5f; + option.offMeshConVerts[5] = 0.6f; + option.offMeshConRad = new float[1]; + option.offMeshConRad[0] = 0.1f; + option.offMeshConDir = new int[1]; + option.offMeshConDir[0] = 1; + option.offMeshConAreas = new int[1]; + option.offMeshConAreas[0] = 2; + option.offMeshConFlags = new int[1]; + option.offMeshConFlags[0] = 12; + option.offMeshConUserID = new int[1]; + option.offMeshConUserID[0] = 0x4567; + option.offMeshConCount = 1; + meshData = NavMeshBuilder.createNavMeshData(option); + } + + public MeshData getMeshData() { + return meshData; + } +} diff --git a/test/DotRecast.Detour.Crowd.Test/SampleAreaModifications.cs b/test/DotRecast.Detour.Crowd.Test/SampleAreaModifications.cs new file mode 100644 index 0000000..070bcc3 --- /dev/null +++ b/test/DotRecast.Detour.Crowd.Test/SampleAreaModifications.cs @@ -0,0 +1,52 @@ +/* +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using DotRecast.Recast; + +namespace DotRecast.Detour.Crowd.Test; + +public class SampleAreaModifications { + + public const int SAMPLE_POLYAREA_TYPE_MASK = 0x07; + public const int SAMPLE_POLYAREA_TYPE_GROUND = 0x1; + public const int SAMPLE_POLYAREA_TYPE_WATER = 0x2; + public const int SAMPLE_POLYAREA_TYPE_ROAD = 0x3; + public const int SAMPLE_POLYAREA_TYPE_DOOR = 0x4; + public const int SAMPLE_POLYAREA_TYPE_GRASS = 0x5; + public const int SAMPLE_POLYAREA_TYPE_JUMP = 0x6; + + public static AreaModification SAMPLE_AREAMOD_GROUND = new AreaModification(SAMPLE_POLYAREA_TYPE_GROUND, + SAMPLE_POLYAREA_TYPE_MASK); + public static AreaModification SAMPLE_AREAMOD_WATER = new AreaModification(SAMPLE_POLYAREA_TYPE_WATER, + SAMPLE_POLYAREA_TYPE_MASK); + public static AreaModification SAMPLE_AREAMOD_ROAD = new AreaModification(SAMPLE_POLYAREA_TYPE_ROAD, + SAMPLE_POLYAREA_TYPE_MASK); + public static AreaModification SAMPLE_AREAMOD_GRASS = new AreaModification(SAMPLE_POLYAREA_TYPE_GRASS, + SAMPLE_POLYAREA_TYPE_MASK); + public static AreaModification SAMPLE_AREAMOD_DOOR = new AreaModification(SAMPLE_POLYAREA_TYPE_DOOR, + SAMPLE_POLYAREA_TYPE_DOOR); + public static AreaModification SAMPLE_AREAMOD_JUMP = new AreaModification(SAMPLE_POLYAREA_TYPE_JUMP, + SAMPLE_POLYAREA_TYPE_JUMP); + + public const int SAMPLE_POLYFLAGS_WALK = 0x01; // Ability to walk (ground, grass, road) + public const int SAMPLE_POLYFLAGS_SWIM = 0x02; // Ability to swim (water). + public const int SAMPLE_POLYFLAGS_DOOR = 0x04; // Ability to move through doors. + public const int SAMPLE_POLYFLAGS_JUMP = 0x08; // Ability to jump. + public const int SAMPLE_POLYFLAGS_DISABLED = 0x10; // Disabled polygon + public const int SAMPLE_POLYFLAGS_ALL = 0xffff; // All abilities. +} diff --git a/test/DotRecast.Detour.Dynamic.Test/DotRecast.Detour.Dynamic.Test.csproj b/test/DotRecast.Detour.Dynamic.Test/DotRecast.Detour.Dynamic.Test.csproj new file mode 100644 index 0000000..19ae8b4 --- /dev/null +++ b/test/DotRecast.Detour.Dynamic.Test/DotRecast.Detour.Dynamic.Test.csproj @@ -0,0 +1,26 @@ + + + + net7.0 + false + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + diff --git a/test/DotRecast.Detour.Dynamic.Test/DynamicNavMeshTest.cs b/test/DotRecast.Detour.Dynamic.Test/DynamicNavMeshTest.cs new file mode 100644 index 0000000..0448f9d --- /dev/null +++ b/test/DotRecast.Detour.Dynamic.Test/DynamicNavMeshTest.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using DotRecast.Core; +using DotRecast.Detour.Dynamic.Colliders; +using DotRecast.Detour.Dynamic.Io; +using NUnit.Framework; + +namespace DotRecast.Detour.Dynamic.Test; + +public class DynamicNavMeshTest { + + private static readonly float[] START_POS = new float[] { 70.87453f, 0.0010070801f, 86.69021f }; + private static readonly float[] END_POS = new float[] { -50.22061f, 0.0010070801f, -70.761444f }; + private static readonly float[] EXTENT = new float[] { 0.1f, 0.1f, 0.1f }; + private static readonly float[] SPHERE_POS = new float[] { 45.381645f, 0.0010070801f, 52.68981f }; + + + [Test] + public void e2eTest() { + byte[] bytes = Loader.ToBytes("test_tiles.voxels"); + using var ms = new MemoryStream(bytes); + using var bis = new BinaryReader(ms); + + // load voxels from file + VoxelFileReader reader = new VoxelFileReader(); + VoxelFile f = reader.read(bis); + // create dynamic navmesh + DynamicNavMesh mesh = new DynamicNavMesh(f); + // build navmesh asynchronously using multiple threads + Task future = mesh.build(Task.Factory); + // wait for build to complete + bool _ = future.Result; + // create new query + NavMeshQuery query = new NavMeshQuery(mesh.navMesh()); + QueryFilter filter = new DefaultQueryFilter(); + // find path + FindNearestPolyResult start = query.findNearestPoly(START_POS, EXTENT, filter).result; + FindNearestPolyResult end = query.findNearestPoly(END_POS, EXTENT, filter).result; + List path = query.findPath(start.getNearestRef(), end.getNearestRef(), start.getNearestPos(), + end.getNearestPos(), filter, NavMeshQuery.DT_FINDPATH_ANY_ANGLE, float.MaxValue).result; + // check path length without any obstacles + Assert.That(path.Count, Is.EqualTo(16)); + // place obstacle + Collider colldier = new SphereCollider(SPHERE_POS, 20, SampleAreaModifications.SAMPLE_POLYAREA_TYPE_GROUND, 0.1f); + long colliderId = mesh.addCollider(colldier); + // update navmesh asynchronously + future = mesh.update(Task.Factory); + // wait for update to complete + _ = future.Result; + // create new query + query = new NavMeshQuery(mesh.navMesh()); + // find path again + start = query.findNearestPoly(START_POS, EXTENT, filter).result; + end = query.findNearestPoly(END_POS, EXTENT, filter).result; + path = query.findPath(start.getNearestRef(), end.getNearestRef(), start.getNearestPos(), end.getNearestPos(), filter, + NavMeshQuery.DT_FINDPATH_ANY_ANGLE, float.MaxValue).result; + // check path length with obstacles + Assert.That(path.Count, Is.EqualTo(19)); + // remove obstacle + mesh.removeCollider(colliderId); + // update navmesh asynchronously + future = mesh.update(Task.Factory); + // wait for update to complete + _ = future.Result; + // create new query + query = new NavMeshQuery(mesh.navMesh()); + // find path one more time + start = query.findNearestPoly(START_POS, EXTENT, filter).result; + end = query.findNearestPoly(END_POS, EXTENT, filter).result; + path = query.findPath(start.getNearestRef(), end.getNearestRef(), start.getNearestPos(), end.getNearestPos(), filter, + NavMeshQuery.DT_FINDPATH_ANY_ANGLE, float.MaxValue).result; + // path length should be back to the initial value + Assert.That(path.Count, Is.EqualTo(16)); + } +} diff --git a/test/DotRecast.Detour.Dynamic.Test/Io/VoxelFileReaderTest.cs b/test/DotRecast.Detour.Dynamic.Test/Io/VoxelFileReaderTest.cs new file mode 100644 index 0000000..59035ad --- /dev/null +++ b/test/DotRecast.Detour.Dynamic.Test/Io/VoxelFileReaderTest.cs @@ -0,0 +1,78 @@ +/* +recast4j copyright (c) 2021 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.IO; +using DotRecast.Core; +using DotRecast.Detour.Dynamic.Io; +using NUnit.Framework; + +namespace DotRecast.Detour.Dynamic.Test.Io; + +public class VoxelFileReaderTest { + + [Test] + public void shouldReadSingleTileFile() { + byte[] bytes = Loader.ToBytes("test.voxels"); + using var ms = new MemoryStream(bytes); + using var bis = new BinaryReader(ms); + + VoxelFileReader reader = new VoxelFileReader(); + VoxelFile f = reader.read(bis); + Assert.That(f.useTiles, Is.False); + Assert.That(f.bounds, Is.EqualTo(new float[] {-100.0f, 0f, -100f, 100f, 5f, 100f})); + Assert.That(f.cellSize, Is.EqualTo(0.25f)); + Assert.That(f.walkableRadius, Is.EqualTo(0.5f)); + Assert.That(f.walkableHeight, Is.EqualTo(2f)); + Assert.That(f.walkableClimb, Is.EqualTo(0.5f)); + Assert.That(f.maxEdgeLen, Is.EqualTo(20f)); + Assert.That(f.maxSimplificationError, Is.EqualTo(2f)); + Assert.That(f.minRegionArea, Is.EqualTo(2f)); + Assert.That(f.tiles.Count, Is.EqualTo(1)); + Assert.That(f.tiles[0].cellHeight, Is.EqualTo(0.001f)); + Assert.That(f.tiles[0].width, Is.EqualTo(810)); + Assert.That(f.tiles[0].depth, Is.EqualTo(810)); + Assert.That(f.tiles[0].boundsMin, Is.EqualTo(new float[] {-101.25f, 0f, -101.25f})); + Assert.That(f.tiles[0].boundsMax, Is.EqualTo(new float[] {101.25f, 5.0f, 101.25f})); + } + + [Test] + public void shouldReadMultiTileFile() { + byte[] bytes = Loader.ToBytes("test_tiles.voxels"); + using var ms = new MemoryStream(bytes); + using var bis = new BinaryReader(ms); + + VoxelFileReader reader = new VoxelFileReader(); + VoxelFile f = reader.read(bis); + + Assert.That(f.useTiles, Is.True); + Assert.That(f.bounds, Is.EqualTo(new float[] { -100.0f, 0f, -100f, 100f, 5f, 100f })); + Assert.That(f.cellSize, Is.EqualTo(0.25f)); + Assert.That(f.walkableRadius, Is.EqualTo(0.5f)); + Assert.That(f.walkableHeight, Is.EqualTo(2f)); + Assert.That(f.walkableClimb, Is.EqualTo(0.5f)); + Assert.That(f.maxEdgeLen, Is.EqualTo(20f)); + Assert.That(f.maxSimplificationError, Is.EqualTo(2f)); + Assert.That(f.minRegionArea, Is.EqualTo(2f)); + Assert.That(f.tiles.Count, Is.EqualTo(100)); + Assert.That(f.tiles[0].cellHeight, Is.EqualTo(0.001f)); + Assert.That(f.tiles[0].width, Is.EqualTo(90)); + Assert.That(f.tiles[0].depth, Is.EqualTo(90)); + Assert.That(f.tiles[0].boundsMin, Is.EqualTo(new float[] { -101.25f, 0f, -101.25f })); + Assert.That(f.tiles[0].boundsMax, Is.EqualTo(new float[] { -78.75f, 5.0f, -78.75f })); + } +} diff --git a/test/DotRecast.Detour.Dynamic.Test/Io/VoxelFileReaderWriterTest.cs b/test/DotRecast.Detour.Dynamic.Test/Io/VoxelFileReaderWriterTest.cs new file mode 100644 index 0000000..a43719b --- /dev/null +++ b/test/DotRecast.Detour.Dynamic.Test/Io/VoxelFileReaderWriterTest.cs @@ -0,0 +1,100 @@ +/* +recast4j copyright (c) 2021 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.IO; +using DotRecast.Core; +using DotRecast.Detour.Dynamic.Io; +using NUnit.Framework; + +namespace DotRecast.Detour.Dynamic.Test.Io; + +public class VoxelFileReaderWriterTest { + + [TestCase(false)] + [TestCase(true)] + public void shouldReadSingleTileFile(bool compression) { + byte[] bytes = Loader.ToBytes("test.voxels"); + using var ms = new MemoryStream(bytes); + using var bis = new BinaryReader(ms); + + VoxelFile f = readWriteRead(bis, compression); + Assert.That(f.useTiles, Is.False); + Assert.That(f.bounds, Is.EqualTo(new[] { -100.0f, 0f, -100f, 100f, 5f, 100f })); + Assert.That(f.cellSize, Is.EqualTo(0.25f)); + Assert.That(f.walkableRadius, Is.EqualTo(0.5f)); + Assert.That(f.walkableHeight, Is.EqualTo(2f)); + Assert.That(f.walkableClimb, Is.EqualTo(0.5f)); + Assert.That(f.maxEdgeLen, Is.EqualTo(20f)); + Assert.That(f.maxSimplificationError, Is.EqualTo(2f)); + Assert.That(f.minRegionArea, Is.EqualTo(2f)); + Assert.That(f.regionMergeArea, Is.EqualTo(12f)); + Assert.That(f.tiles.Count, Is.EqualTo(1)); + Assert.That(f.tiles[0].cellHeight, Is.EqualTo(0.001f)); + Assert.That(f.tiles[0].width, Is.EqualTo(810)); + Assert.That(f.tiles[0].depth, Is.EqualTo(810)); + Assert.That(f.tiles[0].spanData.Length, Is.EqualTo(9021024)); + Assert.That(f.tiles[0].boundsMin, Is.EqualTo(new[] { -101.25f, 0f, -101.25f })); + Assert.That(f.tiles[0].boundsMax, Is.EqualTo(new[] { 101.25f, 5.0f, 101.25f })); + } + + [TestCase(false)] + [TestCase(true)] + public void shouldReadMultiTileFile(bool compression) { + byte[] bytes = Loader.ToBytes("test_tiles.voxels"); + using var ms = new MemoryStream(bytes); + using var bis = new BinaryReader(ms); + + VoxelFile f = readWriteRead(bis, compression); + + Assert.That(f.useTiles, Is.True); + Assert.That(f.bounds, Is.EqualTo(new[] {-100.0f, 0f, -100f, 100f, 5f, 100f})); + Assert.That(f.cellSize, Is.EqualTo(0.25f)); + Assert.That(f.walkableRadius, Is.EqualTo(0.5f)); + Assert.That(f.walkableHeight, Is.EqualTo(2f)); + Assert.That(f.walkableClimb, Is.EqualTo(0.5f)); + Assert.That(f.maxEdgeLen, Is.EqualTo(20f)); + Assert.That(f.maxSimplificationError, Is.EqualTo(2f)); + Assert.That(f.minRegionArea, Is.EqualTo(2f)); + Assert.That(f.regionMergeArea, Is.EqualTo(12f)); + Assert.That(f.tiles.Count, Is.EqualTo(100)); + Assert.That(f.tiles[0].cellHeight, Is.EqualTo(0.001f)); + Assert.That(f.tiles[0].width, Is.EqualTo(90)); + Assert.That(f.tiles[0].depth, Is.EqualTo(90)); + Assert.That(f.tiles[0].spanData.Length, Is.EqualTo(104952)); + Assert.That(f.tiles[5].spanData.Length, Is.EqualTo(109080)); + Assert.That(f.tiles[18].spanData.Length, Is.EqualTo(113400)); + Assert.That(f.tiles[0].boundsMin, Is.EqualTo(new[] {-101.25f, 0f, -101.25f})); + Assert.That(f.tiles[0].boundsMax, Is.EqualTo(new[] {-78.75f, 5.0f, -78.75f})); + + } + + private VoxelFile readWriteRead(BinaryReader bis, bool compression) { + VoxelFileReader reader = new VoxelFileReader(); + VoxelFile f = reader.read(bis); + + using var msOut = new MemoryStream(); + using var bwOut = new BinaryWriter(msOut); + VoxelFileWriter writer = new VoxelFileWriter(); + writer.write(bwOut, f, compression); + + using var msIn = new MemoryStream(msOut.ToArray()); + using var brIn = new BinaryReader(msIn); + return reader.read(brIn); + } + +} diff --git a/test/DotRecast.Detour.Dynamic.Test/SampleAreaModifications.cs b/test/DotRecast.Detour.Dynamic.Test/SampleAreaModifications.cs new file mode 100644 index 0000000..dab5705 --- /dev/null +++ b/test/DotRecast.Detour.Dynamic.Test/SampleAreaModifications.cs @@ -0,0 +1,52 @@ +/* +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using DotRecast.Recast; + +namespace DotRecast.Detour.Dynamic.Test; + +public class SampleAreaModifications { + + public const int SAMPLE_POLYAREA_TYPE_MASK = 0x07; + public const int SAMPLE_POLYAREA_TYPE_GROUND = 0x1; + public const int SAMPLE_POLYAREA_TYPE_WATER = 0x2; + public const int SAMPLE_POLYAREA_TYPE_ROAD = 0x3; + public const int SAMPLE_POLYAREA_TYPE_DOOR = 0x4; + public const int SAMPLE_POLYAREA_TYPE_GRASS = 0x5; + public const int SAMPLE_POLYAREA_TYPE_JUMP = 0x6; + + public static AreaModification SAMPLE_AREAMOD_GROUND = new AreaModification(SAMPLE_POLYAREA_TYPE_GROUND, + SAMPLE_POLYAREA_TYPE_MASK); + public static AreaModification SAMPLE_AREAMOD_WATER = new AreaModification(SAMPLE_POLYAREA_TYPE_WATER, + SAMPLE_POLYAREA_TYPE_MASK); + public static AreaModification SAMPLE_AREAMOD_ROAD = new AreaModification(SAMPLE_POLYAREA_TYPE_ROAD, + SAMPLE_POLYAREA_TYPE_MASK); + public static AreaModification SAMPLE_AREAMOD_GRASS = new AreaModification(SAMPLE_POLYAREA_TYPE_GRASS, + SAMPLE_POLYAREA_TYPE_MASK); + public static AreaModification SAMPLE_AREAMOD_DOOR = new AreaModification(SAMPLE_POLYAREA_TYPE_DOOR, + SAMPLE_POLYAREA_TYPE_DOOR); + public static AreaModification SAMPLE_AREAMOD_JUMP = new AreaModification(SAMPLE_POLYAREA_TYPE_JUMP, + SAMPLE_POLYAREA_TYPE_JUMP); + + public const int SAMPLE_POLYFLAGS_WALK = 0x01; // Ability to walk (ground, grass, road) + public const int SAMPLE_POLYFLAGS_SWIM = 0x02; // Ability to swim (water). + public const int SAMPLE_POLYFLAGS_DOOR = 0x04; // Ability to move through doors. + public const int SAMPLE_POLYFLAGS_JUMP = 0x08; // Ability to jump. + public const int SAMPLE_POLYFLAGS_DISABLED = 0x10; // Disabled polygon + public const int SAMPLE_POLYFLAGS_ALL = 0xffff; // All abilities. +} diff --git a/test/DotRecast.Detour.Dynamic.Test/VoxelQueryTest.cs b/test/DotRecast.Detour.Dynamic.Test/VoxelQueryTest.cs new file mode 100644 index 0000000..7cce115 --- /dev/null +++ b/test/DotRecast.Detour.Dynamic.Test/VoxelQueryTest.cs @@ -0,0 +1,105 @@ +/* +recast4j copyright (c) 2021 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.IO; +using System.Threading.Tasks; +using System.Collections.Generic; +using DotRecast.Core; +using DotRecast.Detour.Dynamic.Io; +using DotRecast.Recast; +using Moq; +using NUnit.Framework; + +namespace DotRecast.Detour.Dynamic.Test; + +public class VoxelQueryTest { + + private const int TILE_WIDTH = 100; + private const int TILE_DEPTH = 90; + private static readonly float[] ORIGIN = new float[] { 50, 10, 40 }; + + + [Test] + public void shouldTraverseTiles() + { + var hfProvider = new Mock>(); + + // Given + List captorX = new(); + List captorZ = new(); + + hfProvider + .Setup(e => e.Invoke(It.IsAny(), It.IsAny())) + .Returns((Heightfield)null) + .Callback((x, z) => + { + captorX.Add(x); + captorZ.Add(z); + }); + + VoxelQuery query = new VoxelQuery(ORIGIN, TILE_WIDTH, TILE_DEPTH, hfProvider.Object); + float[] start = { 120, 10, 365 }; + float[] end = { 320, 10, 57 }; + + // When + query.raycast(start, end); + // Then + hfProvider.Verify(mock => mock.Invoke(It.IsAny(), It.IsAny()), Times.Exactly(6)); + Assert.That(captorX, Is.EqualTo(new[] { 0, 1, 1, 1, 2, 2})); + Assert.That(captorZ, Is.EqualTo(new[] { 3, 3, 2, 1, 1, 0})); + } + + [Test] + public void shouldHandleRaycastWithoutObstacles() { + DynamicNavMesh mesh = createDynaMesh(); + VoxelQuery query = mesh.voxelQuery(); + float[] start = { 7.4f, 0.5f, -64.8f }; + float[] end = { 31.2f, 0.5f, -75.3f }; + float? hit = query.raycast(start, end); + Assert.That(hit, Is.Null); + } + + [Test] + public void shouldHandleRaycastWithObstacles() { + DynamicNavMesh mesh = createDynaMesh(); + VoxelQuery query = mesh.voxelQuery(); + float[] start = { 32.3f, 0.5f, 47.9f }; + float[] end = { -31.2f, 0.5f, -29.8f }; + float? hit = query.raycast(start, end); + Assert.That(hit, Is.Not.Null); + Assert.That(hit.Value, Is.EqualTo(0.5263836f).Within(1e-7f)); + } + + private DynamicNavMesh createDynaMesh() { + var bytes = Loader.ToBytes("test_tiles.voxels"); + using var ms = new MemoryStream(bytes); + using var bis = new BinaryReader(ms); + + // load voxels from file + VoxelFileReader reader = new VoxelFileReader(); + VoxelFile f = reader.read(bis); + // create dynamic navmesh + var mesh = new DynamicNavMesh(f); + // build navmesh asynchronously using multiple threads + Task future = mesh.build(Task.Factory); + // wait for build to complete + var _ = future.Result; + return mesh; + } +} diff --git a/test/DotRecast.Detour.Extras.Test/DotRecast.Detour.Extras.Test.csproj b/test/DotRecast.Detour.Extras.Test/DotRecast.Detour.Extras.Test.csproj new file mode 100644 index 0000000..7bf542b --- /dev/null +++ b/test/DotRecast.Detour.Extras.Test/DotRecast.Detour.Extras.Test.csproj @@ -0,0 +1,26 @@ + + + + net7.0 + false + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + diff --git a/test/DotRecast.Detour.Extras.Test/Unity/Astar/UnityAStarPathfindingImporterTest.cs b/test/DotRecast.Detour.Extras.Test/Unity/Astar/UnityAStarPathfindingImporterTest.cs new file mode 100644 index 0000000..df4a51b --- /dev/null +++ b/test/DotRecast.Detour.Extras.Test/Unity/Astar/UnityAStarPathfindingImporterTest.cs @@ -0,0 +1,128 @@ +/* +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using DotRecast.Core; +using DotRecast.Detour.Extras.Unity.Astar; +using DotRecast.Detour.Io; +using NUnit.Framework; + +namespace DotRecast.Detour.Extras.Test.Unity.Astar; + +public class UnityAStarPathfindingImporterTest { + + [Test] + public void test_v4_0_6() { + NavMesh mesh = loadNavMesh("graph.zip"); + float[] startPos = new float[] { 8.200293f, 2.155071f, -26.176147f }; + float[] endPos = new float[] { 11.971109f, 0.000000f, 8.663261f }; + Result> path = findPath(mesh, startPos, endPos); + Assert.That(path.status, Is.EqualTo(Status.SUCCSESS)); + Assert.That(path.result.Count, Is.EqualTo(57)); + saveMesh(mesh, "v4_0_6"); + } + + [Test] + public void test_v4_1_16() { + NavMesh mesh = loadNavMesh("graph_v4_1_16.zip"); + float[] startPos = new float[] { 22.93f, -2.37f, -5.11f }; + float[] endPos = new float[] { 16.81f, -2.37f, 25.52f }; + Result> path = findPath(mesh, startPos, endPos); + Assert.That(path.status.isSuccess(), Is.True); + Assert.That(path.result.Count, Is.EqualTo(15)); + saveMesh(mesh, "v4_1_16"); + } + + [Test] + public void testBoundsTree() { + NavMesh mesh = loadNavMesh("test_boundstree.zip"); + float[] position = { 387.52988f, 19.997f, 368.86282f }; + + int[] tilePos = mesh.calcTileLoc(position); + long tileRef = mesh.getTileRefAt(tilePos[0], tilePos[1], 0); + MeshTile tile = mesh.getTileByRef(tileRef); + MeshData data = tile.data; + BVNode[] bvNodes = data.bvTree; + data.bvTree = null; // set BV-Tree empty to get 'clear' search poly without BV + FindNearestPolyResult clearResult = getNearestPolys(mesh, position)[0]; // check poly to exists + + // restore BV-Tree and try search again + // important aspect in that test: BV result must equals result without BV + // if poly not found or found other poly - tile bounds is wrong! + data.bvTree = bvNodes; + FindNearestPolyResult bvResult = getNearestPolys(mesh, position)[0]; + + Assert.That(bvResult.getNearestRef(), Is.EqualTo(clearResult.getNearestRef())); + } + + private NavMesh loadNavMesh(string filename) { + var filepath = Loader.ToRPath(filename); + using var fs = new FileStream(filepath, FileMode.Open); + + // Import the graphs + UnityAStarPathfindingImporter importer = new UnityAStarPathfindingImporter(); + + NavMesh[] meshes = importer.load(fs); + return meshes[0]; + } + + private Result> findPath(NavMesh mesh, float[] startPos, float[] endPos) { + // Perform a simple pathfinding + NavMeshQuery query = new NavMeshQuery(mesh); + QueryFilter filter = new DefaultQueryFilter(); + + FindNearestPolyResult[] polys = getNearestPolys(mesh, startPos, endPos); + return query.findPath(polys[0].getNearestRef(), polys[1].getNearestRef(), startPos, endPos, filter); + } + + private FindNearestPolyResult[] getNearestPolys(NavMesh mesh, params float[][] positions) { + NavMeshQuery query = new NavMeshQuery(mesh); + QueryFilter filter = new DefaultQueryFilter(); + float[] extents = new float[] { 0.1f, 0.1f, 0.1f }; + + FindNearestPolyResult[] results = new FindNearestPolyResult[positions.Length]; + for (int i = 0; i < results.Length; i++) { + float[] position = positions[i]; + Result result = query.findNearestPoly(position, extents, filter); + Assert.That(result.succeeded(), Is.True); + Assert.That(result.result.getNearestPos(), Is.Not.Null, "Nearest start position is null!"); + results[i] = result.result; + } + return results; + } + + private void saveMesh(NavMesh mesh, string filePostfix) { + // Set the flag to RecastDemo work properly + for (int i = 0; i < mesh.getTileCount(); i++) { + foreach (Poly p in mesh.getTile(i).data.polys) { + p.flags = 1; + } + } + + // Save the mesh as recast file, + MeshSetWriter writer = new MeshSetWriter(); + string filename = $"all_tiles_navmesh_{filePostfix}.bin"; + string filepath = Path.Combine("test-output", filename); + using var fs = new FileStream(filename, FileMode.Create); + using var os = new BinaryWriter(fs); + writer.write(os, mesh, ByteOrder.LITTLE_ENDIAN, true); + } + +} diff --git a/test/DotRecast.Detour.Test/AbstractDetourTest.cs b/test/DotRecast.Detour.Test/AbstractDetourTest.cs new file mode 100644 index 0000000..be78dd1 --- /dev/null +++ b/test/DotRecast.Detour.Test/AbstractDetourTest.cs @@ -0,0 +1,67 @@ +/* +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using NUnit.Framework; + +namespace DotRecast.Detour.Test; + +public abstract class AbstractDetourTest +{ + protected static readonly long[] startRefs = + { + 281474976710696L, 281474976710773L, 281474976710680L, 281474976710753L, 281474976710733L + }; + + protected static readonly long[] endRefs = + { + 281474976710721L, 281474976710767L, 281474976710758L, 281474976710731L, 281474976710772L + }; + + protected static readonly float[][] startPoss = + { + new[] { 22.60652f, 10.197294f, -45.918674f }, + new[] { 22.331268f, 10.197294f, -1.0401875f }, + new[] { 18.694363f, 15.803535f, -73.090416f }, + new[] { 0.7453353f, 10.197294f, -5.94005f }, + new[] { -20.651257f, 5.904126f, -13.712508f } + }; + + protected static readonly float[][] endPoss = + { + new[] { 6.4576626f, 10.197294f, -18.33406f }, + new[] { -5.8023443f, 0.19729415f, 3.008419f }, + new[] { 38.423977f, 10.197294f, -0.116066754f }, + new[] { 0.8635526f, 10.197294f, -10.31032f }, + new[] { 18.784092f, 10.197294f, 3.0543678f } + }; + + protected NavMeshQuery query; + protected NavMesh navmesh; + + [SetUp] + public void setUp() + { + navmesh = createNavMesh(); + query = new NavMeshQuery(navmesh); + } + + protected NavMesh createNavMesh() + { + return new NavMesh(new RecastTestMeshBuilder().getMeshData(), 6, 0); + } +} \ No newline at end of file diff --git a/test/DotRecast.Detour.Test/ConvexConvexIntersectionTest.cs b/test/DotRecast.Detour.Test/ConvexConvexIntersectionTest.cs new file mode 100644 index 0000000..269a53d --- /dev/null +++ b/test/DotRecast.Detour.Test/ConvexConvexIntersectionTest.cs @@ -0,0 +1,43 @@ +/* +recast4j copyright (c) 2021 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using NUnit.Framework; + +namespace DotRecast.Detour.Test; + +public class ConvexConvexIntersectionTest { + + [Test] + public void shouldHandleSamePolygonIntersection() { + float[] p = { -4, 0, 0, -3, 0, 3, 2, 0, 3, 3, 0, -3, -2, 0, -4 }; + float[] q = { -4, 0, 0, -3, 0, 3, 2, 0, 3, 3, 0, -3, -2, 0, -4 }; + float[] intersection = ConvexConvexIntersection.intersect(p, q); + Assert.That(intersection.Length, Is.EqualTo(5 * 3)); + Assert.That(intersection, Is.EqualTo(p)); + } + + [Test] + public void shouldHandleIntersection() { + float[] p = { -5, 0, -5, -5, 0, 4, 1, 0, 4, 1, 0, -5 }; + float[] q = { -4, 0, 0, -3, 0, 3, 2, 0, 3, 3, 0, -3, -2, 0, -4 }; + float[] intersection = ConvexConvexIntersection.intersect(p, q); + Assert.That(intersection.Length, Is.EqualTo(5 * 3)); + Assert.That(intersection, Is.EqualTo(new[] { 1, 0, 3, 1, 0, -3.4f, -2, 0, -4, -4, 0, 0, -3, 0, 3 })); + } + +} diff --git a/test/DotRecast.Detour.Test/DotRecast.Detour.Test.csproj b/test/DotRecast.Detour.Test/DotRecast.Detour.Test.csproj new file mode 100644 index 0000000..4752647 --- /dev/null +++ b/test/DotRecast.Detour.Test/DotRecast.Detour.Test.csproj @@ -0,0 +1,26 @@ + + + + net7.0 + + + false + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/test/DotRecast.Detour.Test/FindDistanceToWallTest.cs b/test/DotRecast.Detour.Test/FindDistanceToWallTest.cs new file mode 100644 index 0000000..004567f --- /dev/null +++ b/test/DotRecast.Detour.Test/FindDistanceToWallTest.cs @@ -0,0 +1,66 @@ +/* +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using NUnit.Framework; + +namespace DotRecast.Detour.Test; + +public class FindDistanceToWallTest : AbstractDetourTest +{ + private static readonly float[] DISTANCES_TO_WALL = { 0.597511f, 3.201085f, 0.603713f, 2.791475f, 2.815544f }; + + private static readonly float[][] HIT_POSITION = + { + new[] { 23.177608f, 10.197294f, -45.742954f }, + new[] { 22.331268f, 10.197294f, -4.241272f }, + new[] { 18.108675f, 15.743596f, -73.236839f }, + new[] { 1.984785f, 10.197294f, -8.441269f }, + new[] { -22.315216f, 4.997294f, -11.441269f } + }; + + private static readonly float[][] HIT_NORMAL = + { + new[] { -0.955779f, 0.0f, -0.29408592f }, + new[] { 0.0f, 0.0f, 1.0f }, + new[] { 0.97014254f, 0.0f, 0.24253564f }, + new[] { -1.0f, 0.0f, 0.0f }, + new[] { 1.0f, 0.0f, 0.0f } + }; + + [Test] + public void testFindDistanceToWall() + { + QueryFilter filter = new DefaultQueryFilter(); + for (int i = 0; i < startRefs.Length; i++) + { + float[] startPos = startPoss[i]; + Result result = query.findDistanceToWall(startRefs[i], startPos, 3.5f, filter); + FindDistanceToWallResult hit = result.result; + Assert.That(hit.getDistance(), Is.EqualTo(DISTANCES_TO_WALL[i]).Within(0.001f)); + for (int v = 0; v < 3; v++) + { + Assert.That(hit.getPosition()[v], Is.EqualTo(HIT_POSITION[i][v]).Within(0.001f)); + } + + for (int v = 0; v < 3; v++) + { + Assert.That(hit.getNormal()[v], Is.EqualTo(HIT_NORMAL[i][v]).Within(0.001f)); + } + } + } +} \ No newline at end of file diff --git a/test/DotRecast.Detour.Test/FindLocalNeighbourhoodTest.cs b/test/DotRecast.Detour.Test/FindLocalNeighbourhoodTest.cs new file mode 100644 index 0000000..482648e --- /dev/null +++ b/test/DotRecast.Detour.Test/FindLocalNeighbourhoodTest.cs @@ -0,0 +1,67 @@ +/* +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using NUnit.Framework; + +namespace DotRecast.Detour.Test; + +public class FindLocalNeighbourhoodTest : AbstractDetourTest +{ + private static readonly long[][] REFS = + { + new[] { 281474976710696L, 281474976710695L, 281474976710691L, 281474976710697L }, + new[] { 281474976710773L, 281474976710769L, 281474976710772L }, + new[] + { + 281474976710680L, 281474976710674L, 281474976710679L, 281474976710684L, 281474976710683L, + 281474976710678L, 281474976710677L, 281474976710676L + }, + new[] { 281474976710753L, 281474976710748L, 281474976710750L, 281474976710752L }, + new[] { 281474976710733L, 281474976710735L, 281474976710736L } + }; + + private static readonly long[][] PARENT_REFS = + { + new[] { 0L, 281474976710696L, 281474976710695L, 281474976710695L }, + new[] { 0L, 281474976710773L, 281474976710773L }, + new[] + { + 0L, 281474976710680L, 281474976710680L, 281474976710680L, 281474976710680L, 281474976710679L, + 281474976710683L, 281474976710678L + }, + new[] { 0L, 281474976710753L, 281474976710753L, 281474976710748L }, + new[] { 0L, 281474976710733L, 281474976710733L } + }; + + [Test] + public void testFindNearestPoly() + { + QueryFilter filter = new DefaultQueryFilter(); + for (int i = 0; i < startRefs.Length; i++) + { + float[] startPos = startPoss[i]; + Result poly = query.findLocalNeighbourhood(startRefs[i], startPos, 3.5f, + filter); + Assert.That(poly.result.getRefs().Count, Is.EqualTo(REFS[i].Length)); + for (int v = 0; v < REFS[i].Length; v++) + { + Assert.That(poly.result.getRefs()[v], Is.EqualTo(REFS[i][v])); + } + } + } +} \ No newline at end of file diff --git a/test/DotRecast.Detour.Test/FindNearestPolyTest.cs b/test/DotRecast.Detour.Test/FindNearestPolyTest.cs new file mode 100644 index 0000000..54d7395 --- /dev/null +++ b/test/DotRecast.Detour.Test/FindNearestPolyTest.cs @@ -0,0 +1,77 @@ +/* +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using NUnit.Framework; + +namespace DotRecast.Detour.Test; + + +public class FindNearestPolyTest : AbstractDetourTest { + + private static readonly long[] POLY_REFS = { 281474976710696L, 281474976710773L, 281474976710680L, 281474976710753L, 281474976710733L }; + private static readonly float[][] POLY_POS = { + new[] { 22.606520f, 10.197294f, -45.918674f }, new[] { 22.331268f, 10.197294f, -1.040187f }, + new[] { 18.694363f, 15.803535f, -73.090416f }, new[] { 0.745335f, 10.197294f, -5.940050f }, + new[] { -20.651257f, 5.904126f, -13.712508f } }; + + [Test] + public void testFindNearestPoly() { + QueryFilter filter = new DefaultQueryFilter(); + float[] extents = { 2, 4, 2 }; + for (int i = 0; i < startRefs.Length; i++) { + float[] startPos = startPoss[i]; + Result poly = query.findNearestPoly(startPos, extents, filter); + Assert.That(poly.succeeded(), Is.True); + Assert.That(poly.result.getNearestRef(), Is.EqualTo(POLY_REFS[i])); + for (int v = 0; v < POLY_POS[i].Length; v++) { + Assert.That(poly.result.getNearestPos()[v], Is.EqualTo(POLY_POS[i][v]).Within(0.001f)); + } + } + + } + + public class EmptyQueryFilter : QueryFilter + { + public bool passFilter(long refs, MeshTile tile, Poly poly) + { + return false; + } + + public float getCost(float[] pa, float[] pb, long prevRef, MeshTile prevTile, Poly prevPoly, long curRef, MeshTile curTile, + Poly curPoly, long nextRef, MeshTile nextTile, Poly nextPoly) + { + return 0; + } + } + + [Test] + public void shouldReturnStartPosWhenNoPolyIsValid() { + var filter = new EmptyQueryFilter(); + float[] extents = { 2, 4, 2 }; + for (int i = 0; i < startRefs.Length; i++) { + float[] startPos = startPoss[i]; + Result poly = query.findNearestPoly(startPos, extents, filter); + Assert.That(poly.succeeded(), Is.True); + Assert.That(0L, Is.EqualTo(poly.result.getNearestRef())); + for (int v = 0; v < POLY_POS[i].Length; v++) { + Assert.That(poly.result.getNearestPos()[v], Is.EqualTo(startPos[v]).Within(0.001f)); + } + } + + } +} diff --git a/test/DotRecast.Detour.Test/FindPathTest.cs b/test/DotRecast.Detour.Test/FindPathTest.cs new file mode 100644 index 0000000..4eb0dbb --- /dev/null +++ b/test/DotRecast.Detour.Test/FindPathTest.cs @@ -0,0 +1,158 @@ +/* +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.Collections.Generic; +using NUnit.Framework; + +namespace DotRecast.Detour.Test; + + +public class FindPathTest : AbstractDetourTest { + + private static readonly Status[] STATUSES = { Status.SUCCSESS, Status.PARTIAL_RESULT, Status.SUCCSESS, Status.SUCCSESS, + Status.SUCCSESS }; + private static readonly long[][] RESULTS = { + new[] { 281474976710696L, 281474976710695L, 281474976710694L, 281474976710703L, 281474976710706L, + 281474976710705L, 281474976710702L, 281474976710701L, 281474976710714L, 281474976710713L, + 281474976710712L, 281474976710727L, 281474976710730L, 281474976710717L, 281474976710721L }, + new[] { 281474976710773L, 281474976710772L, 281474976710768L, 281474976710754L, 281474976710755L, + 281474976710753L, 281474976710748L, 281474976710752L, 281474976710731L, 281474976710729L, + 281474976710717L, 281474976710724L, 281474976710728L, 281474976710737L, 281474976710738L, + 281474976710736L, 281474976710733L, 281474976710735L, 281474976710742L, 281474976710740L, + 281474976710746L, 281474976710745L, 281474976710744L }, + new[] { 281474976710680L, 281474976710684L, 281474976710688L, 281474976710687L, 281474976710686L, + 281474976710697L, 281474976710695L, 281474976710694L, 281474976710703L, 281474976710706L, + 281474976710705L, 281474976710702L, 281474976710701L, 281474976710714L, 281474976710713L, + 281474976710712L, 281474976710727L, 281474976710730L, 281474976710717L, 281474976710729L, + 281474976710731L, 281474976710752L, 281474976710748L, 281474976710753L, 281474976710755L, + 281474976710754L, 281474976710768L, 281474976710772L, 281474976710773L, 281474976710770L, + 281474976710757L, 281474976710761L, 281474976710758L }, + new[] { 281474976710753L, 281474976710748L, 281474976710752L, 281474976710731L }, + new[] { 281474976710733L, 281474976710736L, 281474976710738L, 281474976710737L, 281474976710728L, + 281474976710724L, 281474976710717L, 281474976710729L, 281474976710731L, 281474976710752L, + 281474976710748L, 281474976710753L, 281474976710755L, 281474976710754L, 281474976710768L, + 281474976710772L } }; + + private static readonly StraightPathItem[][] STRAIGHT_PATHS = { + new[] { new StraightPathItem(new float[] { 22.606520f, 10.197294f, -45.918674f }, 1, 281474976710696L), + new StraightPathItem(new float[] { 3.484785f, 10.197294f, -34.241272f }, 0, 281474976710713L), + new StraightPathItem(new float[] { 1.984785f, 10.197294f, -31.241272f }, 0, 281474976710712L), + new StraightPathItem(new float[] { 1.984785f, 10.197294f, -29.741272f }, 0, 281474976710727L), + new StraightPathItem(new float[] { 2.584784f, 10.197294f, -27.941273f }, 0, 281474976710730L), + new StraightPathItem(new float[] { 6.457663f, 10.197294f, -18.334061f }, 2, 0L) }, + + new[] { new StraightPathItem(new float[] { 22.331268f, 10.197294f, -1.040187f }, 1, 281474976710773L), + new StraightPathItem(new float[] { 9.784786f, 10.197294f, -2.141273f }, 0, 281474976710755L), + new StraightPathItem(new float[] { 7.984783f, 10.197294f, -2.441269f }, 0, 281474976710753L), + new StraightPathItem(new float[] { 1.984785f, 10.197294f, -8.441269f }, 0, 281474976710752L), + new StraightPathItem(new float[] { -4.315216f, 10.197294f, -15.341270f }, 0, 281474976710724L), + new StraightPathItem(new float[] { -8.215216f, 10.197294f, -17.441269f }, 0, 281474976710728L), + new StraightPathItem(new float[] { -10.015216f, 10.197294f, -17.741272f }, 0, 281474976710738L), + new StraightPathItem(new float[] { -11.815216f, 9.997294f, -17.441269f }, 0, 281474976710736L), + new StraightPathItem(new float[] { -17.815216f, 5.197294f, -11.441269f }, 0, 281474976710735L), + new StraightPathItem(new float[] { -17.815216f, 5.197294f, -8.441269f }, 0, 281474976710746L), + new StraightPathItem(new float[] { -11.815216f, 0.197294f, 3.008419f }, 2, 0L) }, + + new[] { new StraightPathItem(new float[] { 18.694363f, 15.803535f, -73.090416f }, 1, 281474976710680L), + new StraightPathItem(new float[] { 17.584785f, 10.197294f, -49.841274f }, 0, 281474976710697L), + new StraightPathItem(new float[] { 17.284786f, 10.197294f, -48.041275f }, 0, 281474976710695L), + new StraightPathItem(new float[] { 16.084785f, 10.197294f, -45.341274f }, 0, 281474976710694L), + new StraightPathItem(new float[] { 3.484785f, 10.197294f, -34.241272f }, 0, 281474976710713L), + new StraightPathItem(new float[] { 1.984785f, 10.197294f, -31.241272f }, 0, 281474976710712L), + new StraightPathItem(new float[] { 1.984785f, 10.197294f, -8.441269f }, 0, 281474976710753L), + new StraightPathItem(new float[] { 7.984783f, 10.197294f, -2.441269f }, 0, 281474976710755L), + new StraightPathItem(new float[] { 9.784786f, 10.197294f, -2.141273f }, 0, 281474976710768L), + new StraightPathItem(new float[] { 38.423977f, 10.197294f, -0.116067f }, 2, 0L) }, + + new[] { new StraightPathItem(new float[] { 0.745335f, 10.197294f, -5.940050f }, 1, 281474976710753L), + new StraightPathItem(new float[] { 0.863553f, 10.197294f, -10.310320f }, 2, 0L) }, + + new[] { new StraightPathItem(new float[] { -20.651257f, 5.904126f, -13.712508f }, 1, 281474976710733L), + new StraightPathItem(new float[] { -11.815216f, 9.997294f, -17.441269f }, 0, 281474976710738L), + new StraightPathItem(new float[] { -10.015216f, 10.197294f, -17.741272f }, 0, 281474976710728L), + new StraightPathItem(new float[] { -8.215216f, 10.197294f, -17.441269f }, 0, 281474976710724L), + new StraightPathItem(new float[] { -4.315216f, 10.197294f, -15.341270f }, 0, 281474976710729L), + new StraightPathItem(new float[] { 1.984785f, 10.197294f, -8.441269f }, 0, 281474976710753L), + new StraightPathItem(new float[] { 7.984783f, 10.197294f, -2.441269f }, 0, 281474976710755L), + new StraightPathItem(new float[] { 18.784092f, 10.197294f, 3.054368f }, 2, 0L) } }; + + [Test] + public void testFindPath() { + QueryFilter filter = new DefaultQueryFilter(); + for (int i = 0; i < startRefs.Length; i++) { + long startRef = startRefs[i]; + long endRef = endRefs[i]; + float[] startPos = startPoss[i]; + float[] endPos = endPoss[i]; + Result> path = query.findPath(startRef, endRef, startPos, endPos, filter); + Assert.That(path.status, Is.EqualTo(STATUSES[i])); + Assert.That(path.result.Count, Is.EqualTo(RESULTS[i].Length)); + for (int j = 0; j < RESULTS[i].Length; j++) { + Assert.That(path.result[j], Is.EqualTo(RESULTS[i][j])); + } + } + } + + [Test] + public void testFindPathSliced() { + QueryFilter filter = new DefaultQueryFilter(); + for (int i = 0; i < startRefs.Length; i++) { + long startRef = startRefs[i]; + long endRef = endRefs[i]; + float[] startPos = startPoss[i]; + float[] endPos = endPoss[i]; + query.initSlicedFindPath(startRef, endRef, startPos, endPos, filter, NavMeshQuery.DT_FINDPATH_ANY_ANGLE); + Status status = Status.IN_PROGRESS; + while (status == Status.IN_PROGRESS) { + Result res = query.updateSlicedFindPath(10); + status = res.status; + } + Result> path = query.finalizeSlicedFindPath(); + Assert.That(path.status, Is.EqualTo(STATUSES[i])); + Assert.That(path.result.Count, Is.EqualTo(RESULTS[i].Length)); + for (int j = 0; j < RESULTS[i].Length; j++) { + Assert.That(path.result[j], Is.EqualTo(RESULTS[i][j])); + } + + } + } + + [Test] + public void testFindPathStraight() { + QueryFilter filter = new DefaultQueryFilter(); + for (int i = 0; i < STRAIGHT_PATHS.Length; i++) {// startRefs.Length; i++) { + long startRef = startRefs[i]; + long endRef = endRefs[i]; + float[] startPos = startPoss[i]; + float[] endPos = endPoss[i]; + Result> path = query.findPath(startRef, endRef, startPos, endPos, filter); + Result> result = query.findStraightPath(startPos, endPos, path.result, + int.MaxValue, 0); + List straightPath = result.result; + Assert.That(straightPath.Count, Is.EqualTo(STRAIGHT_PATHS[i].Length)); + for (int j = 0; j < STRAIGHT_PATHS[i].Length; j++) { + Assert.That(straightPath[j].refs, Is.EqualTo(STRAIGHT_PATHS[i][j].refs)); + for (int v = 0; v < 3; v++) { + Assert.That(straightPath[j].pos[v], Is.EqualTo(STRAIGHT_PATHS[i][j].pos[v]).Within(0.01f)); + } + Assert.That(straightPath[j].flags, Is.EqualTo(STRAIGHT_PATHS[i][j].flags)); + } + } + } + +} diff --git a/test/DotRecast.Detour.Test/FindPolysAroundCircleTest.cs b/test/DotRecast.Detour.Test/FindPolysAroundCircleTest.cs new file mode 100644 index 0000000..700903a --- /dev/null +++ b/test/DotRecast.Detour.Test/FindPolysAroundCircleTest.cs @@ -0,0 +1,83 @@ +/* +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using NUnit.Framework; + +namespace DotRecast.Detour.Test; + + +public class FindPolysAroundCircleTest : AbstractDetourTest { + + private static readonly long[][] REFS = { + new[] { 281474976710696L, 281474976710695L, 281474976710694L, 281474976710691L, 281474976710697L, 281474976710693L, + 281474976710686L, 281474976710687L, 281474976710692L, 281474976710703L, 281474976710689L }, + new[] { 281474976710773L, 281474976710770L, 281474976710769L, 281474976710772L, 281474976710771L }, + new[] { 281474976710680L, 281474976710674L, 281474976710679L, 281474976710684L, 281474976710683L, 281474976710678L, + 281474976710682L, 281474976710677L, 281474976710676L, 281474976710688L, 281474976710687L, 281474976710675L, + 281474976710685L, 281474976710672L, 281474976710666L, 281474976710668L, 281474976710681L, 281474976710673L }, + new[] { 281474976710753L, 281474976710748L, 281474976710755L, 281474976710756L, 281474976710750L, 281474976710752L, + 281474976710731L, 281474976710729L, 281474976710749L, 281474976710719L, 281474976710717L, 281474976710726L }, + new[] { 281474976710733L, 281474976710735L, 281474976710736L, 281474976710734L, 281474976710739L, 281474976710742L, + 281474976710740L, 281474976710746L, 281474976710747L, } }; + private static readonly long[][] PARENT_REFS = { + new[] { 0L, 281474976710696L, 281474976710695L, 281474976710695L, 281474976710695L, 281474976710695L, 281474976710697L, + 281474976710686L, 281474976710693L, 281474976710694L, 281474976710687L }, + new[] { 0L, 281474976710773L, 281474976710773L, 281474976710773L, 281474976710772L }, + new[] { 0L, 281474976710680L, 281474976710680L, 281474976710680L, 281474976710680L, 281474976710679L, 281474976710683L, + 281474976710683L, 281474976710678L, 281474976710684L, 281474976710688L, 281474976710677L, 281474976710687L, + 281474976710682L, 281474976710672L, 281474976710672L, 281474976710675L, 281474976710666L }, + new[] { 0L, 281474976710753L, 281474976710753L, 281474976710753L, 281474976710753L, 281474976710748L, 281474976710752L, + 281474976710731L, 281474976710756L, 281474976710729L, 281474976710729L, 281474976710717L }, + new[] { 0L, 281474976710733L, 281474976710733L, 281474976710736L, 281474976710736L, 281474976710735L, 281474976710742L, + 281474976710740L, 281474976710746L } }; + private static readonly float[][] COSTS = { + new[] { 0.000000f, 0.391453f, 6.764245f, 4.153431f, 3.721995f, 6.109188f, 5.378797f, 7.178796f, 7.009186f, 7.514245f, + 12.655564f }, + new[] { 0.000000f, 6.161580f, 2.824478f, 2.828730f, 8.035697f }, + new[] { 0.000000f, 1.162604f, 1.954029f, 2.776051f, 2.046001f, 2.428367f, 6.429493f, 6.032851f, 2.878368f, 5.333885f, + 6.394545f, 9.596563f, 12.457960f, 7.096575f, 10.413582f, 10.362305f, 10.665442f, 10.593861f }, + new[] { 0.000000f, 2.483205f, 6.723722f, 5.727250f, 3.126022f, 3.543865f, 5.043865f, 6.843868f, 7.212173f, 10.602858f, + 8.793867f, 13.146453f }, + new[] { 0.000000f, 2.480514f, 0.823685f, 5.002500f, 8.229258f, 3.983844f, 5.483844f, 6.655379f, 11.996962f } }; + + [Test] + public void testFindPolysAroundCircle() { + QueryFilter filter = new DefaultQueryFilter(); + for (int i = 0; i < startRefs.Length; i++) { + long startRef = startRefs[i]; + float[] startPos = startPoss[i]; + Result result = query.findPolysAroundCircle(startRef, startPos, 7.5f, filter); + Assert.That(result.succeeded(), Is.True); + FindPolysAroundResult polys = result.result; + Assert.That(polys.getRefs().Count, Is.EqualTo(REFS[i].Length)); + for (int v = 0; v < REFS[i].Length; v++) { + bool found = false; + for (int w = 0; w < REFS[i].Length; w++) { + if (REFS[i][v] == polys.getRefs()[w]) { + Assert.That(polys.getParentRefs()[w], Is.EqualTo(PARENT_REFS[i][v])); + Assert.That(polys.getCosts()[w], Is.EqualTo(COSTS[i][v]).Within(0.01f)); + found = true; + } + } + Assert.That(found, Is.True, $"Ref not found {REFS[i][v]}"); + } + } + + } + +} diff --git a/test/DotRecast.Detour.Test/FindPolysAroundShapeTest.cs b/test/DotRecast.Detour.Test/FindPolysAroundShapeTest.cs new file mode 100644 index 0000000..a1eb728 --- /dev/null +++ b/test/DotRecast.Detour.Test/FindPolysAroundShapeTest.cs @@ -0,0 +1,131 @@ +/* +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using NUnit.Framework; + +namespace DotRecast.Detour.Test; + + +public class FindPolysAroundShapeTest : AbstractDetourTest { + + private static readonly long[][] REFS = { + new[] { 281474976710696L, 281474976710695L, 281474976710694L, 281474976710691L, 281474976710697L, + 281474976710693L, 281474976710692L, 281474976710703L, 281474976710706L, 281474976710699L, + 281474976710705L, 281474976710698L, 281474976710700L, 281474976710704L }, + new[] { 281474976710773L, 281474976710769L, 281474976710772L, 281474976710768L, 281474976710771L, + 281474976710754L, 281474976710755L, 281474976710753L, 281474976710751L, 281474976710756L, + 281474976710749L }, + new[] { 281474976710680L, 281474976710679L, 281474976710684L, 281474976710683L, 281474976710688L, + 281474976710678L, 281474976710676L, 281474976710687L, 281474976710690L, 281474976710686L, + 281474976710689L, 281474976710685L, 281474976710697L, 281474976710695L, 281474976710694L, + 281474976710691L, 281474976710696L, 281474976710693L, 281474976710692L, 281474976710703L, + 281474976710706L, 281474976710699L, 281474976710705L, 281474976710700L, 281474976710704L }, + new[] { 281474976710753L, 281474976710748L, 281474976710752L, 281474976710731L }, + new[] { 281474976710733L, 281474976710735L, 281474976710736L, 281474976710742L, 281474976710734L, + 281474976710739L, 281474976710738L, 281474976710740L, 281474976710746L, 281474976710743L, + 281474976710745L, 281474976710741L, 281474976710747L, 281474976710737L, 281474976710732L, + 281474976710728L, 281474976710724L, 281474976710744L, 281474976710725L, 281474976710717L, + 281474976710729L, 281474976710726L, 281474976710721L, 281474976710719L, 281474976710731L, + 281474976710720L, 281474976710752L, 281474976710748L, 281474976710753L, 281474976710755L, + 281474976710756L, 281474976710750L, 281474976710749L, 281474976710754L, 281474976710751L, + 281474976710768L, 281474976710772L, 281474976710773L, 281474976710771L, 281474976710769L } }; + private static readonly long[][] PARENT_REFS = { + new[] { 0L, 281474976710696L, 281474976710695L, 281474976710695L, 281474976710695L, 281474976710695L, + 281474976710693L, 281474976710694L, 281474976710703L, 281474976710706L, 281474976710706L, + 281474976710705L, 281474976710705L, 281474976710705L }, + new[] { 0L, 281474976710773L, 281474976710773L, 281474976710772L, 281474976710772L, 281474976710768L, + 281474976710754L, 281474976710755L, 281474976710755L, 281474976710753L, 281474976710756L }, + new[] { 0L, 281474976710680L, 281474976710680L, 281474976710680L, 281474976710684L, 281474976710679L, + 281474976710678L, 281474976710688L, 281474976710687L, 281474976710687L, 281474976710687L, + 281474976710687L, 281474976710686L, 281474976710697L, 281474976710695L, 281474976710695L, + 281474976710695L, 281474976710695L, 281474976710693L, 281474976710694L, 281474976710703L, + 281474976710706L, 281474976710706L, 281474976710705L, 281474976710705L }, + new[] { 0L, 281474976710753L, 281474976710748L, 281474976710752L }, + new[] { 0L, 281474976710733L, 281474976710733L, 281474976710735L, 281474976710736L, 281474976710736L, + 281474976710736L, 281474976710742L, 281474976710740L, 281474976710746L, 281474976710746L, + 281474976710746L, 281474976710746L, 281474976710738L, 281474976710738L, 281474976710737L, + 281474976710728L, 281474976710745L, 281474976710724L, 281474976710724L, 281474976710717L, + 281474976710717L, 281474976710717L, 281474976710729L, 281474976710729L, 281474976710721L, + 281474976710731L, 281474976710752L, 281474976710748L, 281474976710753L, 281474976710753L, + 281474976710753L, 281474976710756L, 281474976710755L, 281474976710755L, 281474976710754L, + 281474976710768L, 281474976710772L, 281474976710772L, 281474976710773L } }; + private static readonly float[][] COSTS = { + new[] { 0.000000f, 16.188787f, 22.561579f, 19.950766f, 19.519329f, 21.906523f, 22.806520f, 23.311579f, 25.124035f, + 28.454576f, 26.084503f, 36.438854f, 30.526634f, 31.942192f }, + new[] { 0.000000f, 16.618738f, 12.136283f, 20.387646f, 17.343250f, 22.037645f, 22.787645f, 27.178831f, 26.501472f, + 31.691311f, 33.176235f }, + new[] { 0.000000f, 36.657764f, 35.197689f, 37.484924f, 37.755524f, 37.132103f, 37.582104f, 38.816185f, 52.426109f, + 55.945839f, 51.882935f, 44.879601f, 57.745838f, 59.402641f, 65.063034f, 64.934372f, 62.733185f, + 62.756744f, 63.656742f, 65.813034f, 67.625488f, 70.956032f, 68.585960f, 73.028091f, 74.443649f }, + new[] { 0.000000f, 2.097958f, 3.158618f, 4.658618f }, + new[] { 0.000000f, 20.495766f, 21.352942f, 21.999096f, 25.531757f, 28.758514f, 30.264732f, 23.499096f, 24.670631f, + 33.166218f, 35.651184f, 34.371792f, 30.012215f, 33.886887f, 33.855347f, 34.643524f, 36.300327f, + 38.203144f, 40.339203f, 40.203213f, 47.254810f, 50.043945f, 49.054485f, 49.804810f, 49.204811f, + 52.813477f, 51.004814f, 52.504814f, 53.565475f, 62.748611f, 61.504147f, 57.915474f, 62.989071f, + 67.139801f, 66.507599f, 67.889801f, 69.539803f, 77.791168f, 75.186256f, 83.111412f } }; + + [Test] + public void testFindPolysAroundShape() { + QueryFilter filter = new DefaultQueryFilter(); + for (int i = 0; i < startRefs.Length; i++) { + long startRef = startRefs[i]; + float[] startPos = startPoss[i]; + Result polys = query.findPolysAroundShape(startRef, + getQueryPoly(startPos, endPoss[i]), filter); + Assert.That(polys.result.getRefs().Count, Is.EqualTo(REFS[i].Length)); + for (int v = 0; v < REFS[i].Length; v++) { + bool found = false; + for (int w = 0; w < REFS[i].Length; w++) { + if (REFS[i][v] == polys.result.getRefs()[w]) { + Assert.That(polys.result.getParentRefs()[w], Is.EqualTo(PARENT_REFS[i][v])); + Assert.That(polys.result.getCosts()[w], Is.EqualTo(COSTS[i][v]).Within(0.01f)); + found = true; + } + } + Assert.That(found, Is.True); + } + } + + } + + private float[] getQueryPoly(float[] m_spos, float[] m_epos) { + + float nx = (m_epos[2] - m_spos[2]) * 0.25f; + float nz = -(m_epos[0] - m_spos[0]) * 0.25f; + float agentHeight = 2.0f; + + float[] m_queryPoly = new float[12]; + m_queryPoly[0] = m_spos[0] + nx * 1.2f; + m_queryPoly[1] = m_spos[1] + agentHeight / 2; + m_queryPoly[2] = m_spos[2] + nz * 1.2f; + + m_queryPoly[3] = m_spos[0] - nx * 1.3f; + m_queryPoly[4] = m_spos[1] + agentHeight / 2; + m_queryPoly[5] = m_spos[2] - nz * 1.3f; + + m_queryPoly[6] = m_epos[0] - nx * 0.8f; + m_queryPoly[7] = m_epos[1] + agentHeight / 2; + m_queryPoly[8] = m_epos[2] - nz * 0.8f; + + m_queryPoly[9] = m_epos[0] + nx; + m_queryPoly[10] = m_epos[1] + agentHeight / 2; + m_queryPoly[11] = m_epos[2] + nz; + return m_queryPoly; + } + +} diff --git a/test/DotRecast.Detour.Test/GetPolyWallSegmentsTest.cs b/test/DotRecast.Detour.Test/GetPolyWallSegmentsTest.cs new file mode 100644 index 0000000..69f3049 --- /dev/null +++ b/test/DotRecast.Detour.Test/GetPolyWallSegmentsTest.cs @@ -0,0 +1,75 @@ +/* +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using NUnit.Framework; + +namespace DotRecast.Detour.Test; + + +public class GetPolyWallSegmentsTest : AbstractDetourTest { + + private static readonly float[][] VERTICES = { + new[] { 22.084785f, 10.197294f, -48.341274f, 22.684784f, 10.197294f, -44.141273f, 22.684784f, 10.197294f, + -44.141273f, 23.884785f, 10.197294f, -48.041275f, 23.884785f, 10.197294f, -48.041275f, 22.084785f, + 10.197294f, -48.341274f }, + new[] { 27.784786f, 10.197294f, 4.158730f, 28.384785f, 10.197294f, 2.358727f, 28.384785f, 10.197294f, 2.358727f, + 28.384785f, 10.197294f, -2.141273f, 28.384785f, 10.197294f, -2.141273f, 27.784786f, 10.197294f, + -2.741272f, 27.784786f, 10.197294f, -2.741272f, 19.684784f, 10.197294f, -4.241272f, 19.684784f, + 10.197294f, -4.241272f, 19.684784f, 10.197294f, 4.158730f, 19.684784f, 10.197294f, 4.158730f, + 27.784786f, 10.197294f, 4.158730f }, + new[] { 22.384785f, 14.997294f, -71.741272f, 19.084785f, 16.597294f, -74.741272f, 19.084785f, 16.597294f, + -74.741272f, 18.184784f, 15.997294f, -73.541275f, 18.184784f, 15.997294f, -73.541275f, 17.884785f, + 14.997294f, -72.341278f, 17.884785f, 14.997294f, -72.341278f, 17.584785f, 14.997294f, -70.841278f, + 17.584785f, 14.997294f, -70.841278f, 22.084785f, 14.997294f, -70.541275f, 22.084785f, 14.997294f, + -70.541275f, 22.384785f, 14.997294f, -71.741272f }, + new[] { 4.684784f, 10.197294f, -6.941269f, 1.984785f, 10.197294f, -8.441269f, 1.984785f, 10.197294f, -8.441269f, + -4.015217f, 10.197294f, -6.941269f, -4.015217f, 10.197294f, -6.941269f, -1.615215f, 10.197294f, + -1.541275f, -1.615215f, 10.197294f, -1.541275f, 1.384785f, 10.197294f, 1.458725f, 1.384785f, + 10.197294f, 1.458725f, 7.984783f, 10.197294f, -2.441269f, 7.984783f, 10.197294f, -2.441269f, + 4.684784f, 10.197294f, -6.941269f }, + new[] { -22.315216f, 6.597294f, -17.141273f, -23.815216f, 5.397294f, -13.841270f, -23.815216f, 5.397294f, + -13.841270f, -24.115217f, 4.997294f, -12.041275f, -24.115217f, 4.997294f, -12.041275f, -22.315216f, + 4.997294f, -11.441269f, -22.315216f, 4.997294f, -11.441269f, -17.815216f, 5.197294f, -11.441269f, + -17.815216f, 5.197294f, -11.441269f, -22.315216f, 6.597294f, -17.141273f } }; + private static readonly long[][] REFS = { + new[] { 281474976710695L, 0L, 0L }, + new[] { 0L, 281474976710770L, 0L, 281474976710769L, 281474976710772L, 0L }, + new[] { 281474976710683L, 281474976710674L, 0L, 281474976710679L, 281474976710684L, 0L }, + new[] { 281474976710750L, 281474976710748L, 0L, 0L, 281474976710755L, 281474976710756L }, + new[] { 0L, 0L, 0L, 281474976710735L, 281474976710736L } }; + + [Test] + public void testFindDistanceToWall() { + QueryFilter filter = new DefaultQueryFilter(); + for (int i = 0; i < startRefs.Length; i++) { + Result result = query.getPolyWallSegments(startRefs[i], true, filter); + GetPolyWallSegmentsResult segments = result.result; + Assert.That(segments.getSegmentVerts().Count, Is.EqualTo(VERTICES[i].Length / 6)); + Assert.That(segments.getSegmentRefs().Count, Is.EqualTo(REFS[i].Length)); + for (int v = 0; v < VERTICES[i].Length / 6; v++) { + for (int n = 0; n < 6; n++) { + Assert.That(segments.getSegmentVerts()[v][n], Is.EqualTo(VERTICES[i][v * 6 + n]).Within(0.001f)); + } + } + for (int v = 0; v < REFS[i].Length; v++) { + Assert.That(segments.getSegmentRefs()[v], Is.EqualTo(REFS[i][v])); + } + } + + } +} diff --git a/test/DotRecast.Detour.Test/Io/MeshDataReaderWriterTest.cs b/test/DotRecast.Detour.Test/Io/MeshDataReaderWriterTest.cs new file mode 100644 index 0000000..6b15f63 --- /dev/null +++ b/test/DotRecast.Detour.Test/Io/MeshDataReaderWriterTest.cs @@ -0,0 +1,118 @@ +/* +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.IO; +using DotRecast.Core; +using DotRecast.Detour.Io; +using NUnit.Framework; + +namespace DotRecast.Detour.Test.Io; + + +public class MeshDataReaderWriterTest { + + private const int VERTS_PER_POLYGON = 6; + private MeshData meshData; + + [SetUp] + public void setUp() { + RecastTestMeshBuilder rcBuilder = new RecastTestMeshBuilder(); + meshData = rcBuilder.getMeshData(); + } + + [Test] + public void testCCompatibility() { + test(true, ByteOrder.BIG_ENDIAN); + } + + [Test] + public void testCompact() { + test(false, ByteOrder.BIG_ENDIAN); + } + + [Test] + public void testCCompatibilityLE() { + test(true, ByteOrder.LITTLE_ENDIAN); + } + + [Test] + public void testCompactLE() { + test(false, ByteOrder.LITTLE_ENDIAN); + } + + public void test(bool cCompatibility, ByteOrder order) { + using var ms = new MemoryStream(); + using var bwos = new BinaryWriter(ms); + + MeshDataWriter writer = new MeshDataWriter(); + writer.write(bwos, meshData, order, cCompatibility); + ms.Seek(0, SeekOrigin.Begin); + + using var bris = new BinaryReader(ms); + MeshDataReader reader = new MeshDataReader(); + MeshData readData = reader.read(bris, VERTS_PER_POLYGON); + + Assert.That(readData.header.vertCount, Is.EqualTo(meshData.header.vertCount)); + Assert.That(readData.header.polyCount, Is.EqualTo(meshData.header.polyCount)); + Assert.That(readData.header.detailMeshCount, Is.EqualTo(meshData.header.detailMeshCount)); + Assert.That(readData.header.detailTriCount, Is.EqualTo(meshData.header.detailTriCount)); + Assert.That(readData.header.detailVertCount, Is.EqualTo(meshData.header.detailVertCount)); + Assert.That(readData.header.bvNodeCount, Is.EqualTo(meshData.header.bvNodeCount)); + Assert.That(readData.header.offMeshConCount, Is.EqualTo(meshData.header.offMeshConCount)); + for (int i = 0; i < meshData.header.vertCount; i++) { + Assert.That(readData.verts[i], Is.EqualTo(meshData.verts[i])); + } + for (int i = 0; i < meshData.header.polyCount; i++) { + Assert.That(readData.polys[i].vertCount, Is.EqualTo(meshData.polys[i].vertCount)); + Assert.That(readData.polys[i].areaAndtype, Is.EqualTo(meshData.polys[i].areaAndtype)); + for (int j = 0; j < meshData.polys[i].vertCount; j++) { + Assert.That(readData.polys[i].verts[j], Is.EqualTo(meshData.polys[i].verts[j])); + Assert.That(readData.polys[i].neis[j], Is.EqualTo(meshData.polys[i].neis[j])); + } + } + for (int i = 0; i < meshData.header.detailMeshCount; i++) { + Assert.That(readData.detailMeshes[i].vertBase, Is.EqualTo(meshData.detailMeshes[i].vertBase)); + Assert.That(readData.detailMeshes[i].vertCount, Is.EqualTo(meshData.detailMeshes[i].vertCount)); + Assert.That(readData.detailMeshes[i].triBase, Is.EqualTo(meshData.detailMeshes[i].triBase)); + Assert.That(readData.detailMeshes[i].triCount, Is.EqualTo(meshData.detailMeshes[i].triCount)); + } + for (int i = 0; i < meshData.header.detailVertCount; i++) { + Assert.That(readData.detailVerts[i], Is.EqualTo(meshData.detailVerts[i])); + } + for (int i = 0; i < meshData.header.detailTriCount; i++) { + Assert.That(readData.detailTris[i], Is.EqualTo(meshData.detailTris[i])); + } + for (int i = 0; i < meshData.header.bvNodeCount; i++) { + Assert.That(readData.bvTree[i].i, Is.EqualTo(meshData.bvTree[i].i)); + for (int j = 0; j < 3; j++) { + Assert.That(readData.bvTree[i].bmin[j], Is.EqualTo(meshData.bvTree[i].bmin[j])); + Assert.That(readData.bvTree[i].bmax[j], Is.EqualTo(meshData.bvTree[i].bmax[j])); + } + } + for (int i = 0; i < meshData.header.offMeshConCount; i++) { + Assert.That(readData.offMeshCons[i].flags, Is.EqualTo(meshData.offMeshCons[i].flags)); + Assert.That(readData.offMeshCons[i].rad, Is.EqualTo(meshData.offMeshCons[i].rad)); + Assert.That(readData.offMeshCons[i].poly, Is.EqualTo(meshData.offMeshCons[i].poly)); + Assert.That(readData.offMeshCons[i].side, Is.EqualTo(meshData.offMeshCons[i].side)); + Assert.That(readData.offMeshCons[i].userId, Is.EqualTo(meshData.offMeshCons[i].userId)); + for (int j = 0; j < 6; j++) { + Assert.That(readData.offMeshCons[i].pos[j], Is.EqualTo(meshData.offMeshCons[i].pos[j])); + } + } + } +} diff --git a/test/DotRecast.Detour.Test/Io/MeshSetReaderTest.cs b/test/DotRecast.Detour.Test/Io/MeshSetReaderTest.cs new file mode 100644 index 0000000..d31bf13 --- /dev/null +++ b/test/DotRecast.Detour.Test/Io/MeshSetReaderTest.cs @@ -0,0 +1,113 @@ +/* +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.Collections.Generic; +using System.IO; +using DotRecast.Core; +using DotRecast.Detour.Io; +using NUnit.Framework; + +namespace DotRecast.Detour.Test.Io; + +public class MeshSetReaderTest { + + private readonly MeshSetReader reader = new MeshSetReader(); + + [Test] + public void testNavmesh() { + byte[] @is = Loader.ToBytes("all_tiles_navmesh.bin"); + using var ms = new MemoryStream(@is); + using var bris = new BinaryReader(ms); + NavMesh mesh = reader.read(bris, 6); + Assert.That(mesh.getMaxTiles(), Is.EqualTo(128)); + Assert.That(mesh.getParams().maxPolys, Is.EqualTo(0x8000)); + Assert.That(mesh.getParams().tileWidth, Is.EqualTo(9.6f).Within(0.001f)); + List tiles = mesh.getTilesAt(4, 7); + Assert.That(tiles.Count, Is.EqualTo(1)); + Assert.That(tiles[0].data.polys.Length, Is.EqualTo(7)); + Assert.That(tiles[0].data.verts.Length, Is.EqualTo(22 * 3)); + tiles = mesh.getTilesAt(1, 6); + Assert.That(tiles.Count, Is.EqualTo(1)); + Assert.That(tiles[0].data.polys.Length, Is.EqualTo(7)); + Assert.That(tiles[0].data.verts.Length, Is.EqualTo(26 * 3)); + tiles = mesh.getTilesAt(6, 2); + Assert.That(tiles.Count, Is.EqualTo(1)); + Assert.That(tiles[0].data.polys.Length, Is.EqualTo(1)); + Assert.That(tiles[0].data.verts.Length, Is.EqualTo(4 * 3)); + tiles = mesh.getTilesAt(7, 6); + Assert.That(tiles.Count, Is.EqualTo(1)); + Assert.That(tiles[0].data.polys.Length, Is.EqualTo(8)); + Assert.That(tiles[0].data.verts.Length, Is.EqualTo(24 * 3)); + } + + [Test] + public void testDungeon() { + byte[] @is = Loader.ToBytes("dungeon_all_tiles_navmesh.bin"); + using var ms = new MemoryStream(@is); + using var bris = new BinaryReader(ms); + + NavMesh mesh = reader.read(bris, 6); + Assert.That(mesh.getMaxTiles(), Is.EqualTo(128)); + Assert.That(mesh.getParams().maxPolys, Is.EqualTo(0x8000)); + Assert.That(mesh.getParams().tileWidth, Is.EqualTo(9.6f).Within(0.001f)); + List tiles = mesh.getTilesAt(6, 9); + Assert.That(tiles.Count, Is.EqualTo(1)); + Assert.That(tiles[0].data.polys.Length, Is.EqualTo(2)); + Assert.That(tiles[0].data.verts.Length, Is.EqualTo(7 * 3)); + tiles = mesh.getTilesAt(2, 9); + Assert.That(tiles.Count, Is.EqualTo(1)); + Assert.That(tiles[0].data.polys.Length, Is.EqualTo(2)); + Assert.That(tiles[0].data.verts.Length, Is.EqualTo(9 * 3)); + tiles = mesh.getTilesAt(4, 3); + Assert.That(tiles.Count, Is.EqualTo(1)); + Assert.That(tiles[0].data.polys.Length, Is.EqualTo(3)); + Assert.That(tiles[0].data.verts.Length, Is.EqualTo(6 * 3)); + tiles = mesh.getTilesAt(2, 8); + Assert.That(tiles.Count, Is.EqualTo(1)); + Assert.That(tiles[0].data.polys.Length, Is.EqualTo(5)); + Assert.That(tiles[0].data.verts.Length, Is.EqualTo(17 * 3)); + } + + [Test] + public void testDungeon32Bit() { + byte[] @is = Loader.ToBytes("dungeon_all_tiles_navmesh_32bit.bin"); + using var ms = new MemoryStream(@is); + using var bris = new BinaryReader(ms); + + NavMesh mesh = reader.read32Bit(bris, 6); + Assert.That(mesh.getMaxTiles(), Is.EqualTo(128)); + Assert.That(mesh.getParams().maxPolys, Is.EqualTo(0x8000)); + Assert.That(mesh.getParams().tileWidth, Is.EqualTo(9.6f).Within(0.001f)); + List tiles = mesh.getTilesAt(6, 9); + Assert.That(tiles.Count, Is.EqualTo(1)); + Assert.That(tiles[0].data.polys.Length, Is.EqualTo(2)); + Assert.That(tiles[0].data.verts.Length, Is.EqualTo(7 * 3)); + tiles = mesh.getTilesAt(2, 9); + Assert.That(tiles.Count, Is.EqualTo(1)); + Assert.That(tiles[0].data.polys.Length, Is.EqualTo(2)); + Assert.That(tiles[0].data.verts.Length, Is.EqualTo(9 * 3)); + tiles = mesh.getTilesAt(4, 3); + Assert.That(tiles.Count, Is.EqualTo(1)); + Assert.That(tiles[0].data.polys.Length, Is.EqualTo(3)); + Assert.That(tiles[0].data.verts.Length, Is.EqualTo(6 * 3)); + tiles = mesh.getTilesAt(2, 8); + Assert.That(tiles.Count, Is.EqualTo(1)); + Assert.That(tiles[0].data.polys.Length, Is.EqualTo(5)); + Assert.That(tiles[0].data.verts.Length, Is.EqualTo(17 * 3)); + } +} diff --git a/test/DotRecast.Detour.Test/Io/MeshSetReaderWriterTest.cs b/test/DotRecast.Detour.Test/Io/MeshSetReaderWriterTest.cs new file mode 100644 index 0000000..1de6cc1 --- /dev/null +++ b/test/DotRecast.Detour.Test/Io/MeshSetReaderWriterTest.cs @@ -0,0 +1,120 @@ +/* +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.Collections.Generic; +using System.IO; +using DotRecast.Core; +using DotRecast.Detour.Io; +using DotRecast.Recast; +using DotRecast.Recast.Geom; +using NUnit.Framework; + +using static DotRecast.Detour.DetourCommon; + +namespace DotRecast.Detour.Test.Io; + +public class MeshSetReaderWriterTest { + + private readonly MeshSetWriter writer = new MeshSetWriter(); + private readonly MeshSetReader reader = new MeshSetReader(); + private const float m_cellSize = 0.3f; + private const float m_cellHeight = 0.2f; + private const float m_agentHeight = 2.0f; + private const float m_agentRadius = 0.6f; + private const float m_agentMaxClimb = 0.9f; + private const float m_agentMaxSlope = 45.0f; + private const int m_regionMinSize = 8; + private const int m_regionMergeSize = 20; + private const float m_regionMinArea = m_regionMinSize * m_regionMinSize * m_cellSize * m_cellSize; + private const float m_regionMergeArea = m_regionMergeSize * m_regionMergeSize * m_cellSize * m_cellSize; + private const float m_edgeMaxLen = 12.0f; + private const float m_edgeMaxError = 1.3f; + private const int m_vertsPerPoly = 6; + private const float m_detailSampleDist = 6.0f; + private const float m_detailSampleMaxError = 1.0f; + private const int m_tileSize = 32; + private const int m_maxTiles = 128; + private const int m_maxPolysPerTile = 0x8000; + + [Test] + public void test() { + + InputGeomProvider geom = ObjImporter.load(Loader.ToBytes("dungeon.obj")); + + NavMeshSetHeader header = new NavMeshSetHeader(); + header.magic = NavMeshSetHeader.NAVMESHSET_MAGIC; + header.version = NavMeshSetHeader.NAVMESHSET_VERSION; + vCopy(header.option.orig, geom.getMeshBoundsMin()); + header.option.tileWidth = m_tileSize * m_cellSize; + header.option.tileHeight = m_tileSize * m_cellSize; + header.option.maxTiles = m_maxTiles; + header.option.maxPolys = m_maxPolysPerTile; + header.numTiles = 0; + NavMesh mesh = new NavMesh(header.option, 6); + + float[] bmin = geom.getMeshBoundsMin(); + float[] bmax = geom.getMeshBoundsMax(); + int[] twh = DotRecast.Recast.Recast.calcTileCount(bmin, bmax, m_cellSize, m_tileSize, m_tileSize); + int tw = twh[0]; + int th = twh[1]; + for (int y = 0; y < th; ++y) { + for (int x = 0; x < tw; ++x) { + RecastConfig cfg = new RecastConfig(true, m_tileSize, m_tileSize, + RecastConfig.calcBorder(m_agentRadius, m_cellSize), PartitionType.WATERSHED, m_cellSize, m_cellHeight, + m_agentMaxSlope, true, true, true, m_agentHeight, m_agentRadius, m_agentMaxClimb, m_regionMinArea, + m_regionMergeArea, m_edgeMaxLen, m_edgeMaxError, m_vertsPerPoly, true, m_detailSampleDist, + m_detailSampleMaxError, SampleAreaModifications.SAMPLE_AREAMOD_GROUND); + RecastBuilderConfig bcfg = new RecastBuilderConfig(cfg, bmin, bmax, x, y); + TestDetourBuilder db = new TestDetourBuilder(); + MeshData data = db.build(geom, bcfg, m_agentHeight, m_agentRadius, m_agentMaxClimb, x, y, true); + if (data != null) { + mesh.removeTile(mesh.getTileRefAt(x, y, 0)); + mesh.addTile(data, 0, 0); + } + } + } + + using var ms = new MemoryStream(); + using var os = new BinaryWriter(ms); + writer.write(os, mesh, ByteOrder.LITTLE_ENDIAN, true); + ms.Seek(0, SeekOrigin.Begin); + + using var @is = new BinaryReader(ms); + mesh = reader.read(@is, 6); + Assert.That(mesh.getMaxTiles(), Is.EqualTo(128)); + Assert.That(mesh.getParams().maxPolys, Is.EqualTo(0x8000)); + Assert.That(mesh.getParams().tileWidth, Is.EqualTo(9.6f).Within(0.001f)); + List tiles = mesh.getTilesAt(6, 9); + Assert.That(tiles.Count, Is.EqualTo(1)); + Assert.That(tiles[0].data.polys.Length, Is.EqualTo(2)); + Assert.That(tiles[0].data.verts.Length, Is.EqualTo(7 * 3)); + tiles = mesh.getTilesAt(2, 9); + Assert.That(tiles.Count, Is.EqualTo(1)); + Assert.That(tiles[0].data.polys.Length, Is.EqualTo(2)); + Assert.That(tiles[0].data.verts.Length, Is.EqualTo(9 * 3)); + tiles = mesh.getTilesAt(4, 3); + Assert.That(tiles.Count, Is.EqualTo(1)); + Assert.That(tiles[0].data.polys.Length, Is.EqualTo(3)); + Assert.That(tiles[0].data.verts.Length, Is.EqualTo(6 * 3)); + tiles = mesh.getTilesAt(2, 8); + Assert.That(tiles.Count, Is.EqualTo(1)); + Assert.That(tiles[0].data.polys.Length, Is.EqualTo(5)); + Assert.That(tiles[0].data.verts.Length, Is.EqualTo(17 * 3)); + + } +} diff --git a/test/DotRecast.Detour.Test/MoveAlongSurfaceTest.cs b/test/DotRecast.Detour.Test/MoveAlongSurfaceTest.cs new file mode 100644 index 0000000..36a8a71 --- /dev/null +++ b/test/DotRecast.Detour.Test/MoveAlongSurfaceTest.cs @@ -0,0 +1,68 @@ +/* +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using NUnit.Framework; + +namespace DotRecast.Detour.Test; + +public class MoveAlongSurfaceTest : AbstractDetourTest { + + private static readonly long[][] VISITED = { + new[] { 281474976710696L, 281474976710695L, 281474976710694L, 281474976710703L, 281474976710706L, + 281474976710705L, 281474976710702L, 281474976710701L, 281474976710714L, 281474976710713L, + 281474976710712L, 281474976710727L, 281474976710730L, 281474976710717L, 281474976710721L }, + new[] { 281474976710773L, 281474976710772L, 281474976710768L, 281474976710754L, 281474976710755L, + 281474976710753L }, + new[] { 281474976710680L, 281474976710684L, 281474976710688L, 281474976710687L, 281474976710686L, + 281474976710697L, 281474976710695L, 281474976710694L, 281474976710703L, 281474976710706L, + 281474976710705L, 281474976710702L, 281474976710701L, 281474976710714L, 281474976710713L, + 281474976710712L, 281474976710727L, 281474976710730L, 281474976710717L, 281474976710721L, + 281474976710718L }, + new[] { 281474976710753L, 281474976710748L, 281474976710752L, 281474976710731L }, + new[] { 281474976710733L, 281474976710736L, 281474976710738L, 281474976710737L, 281474976710728L, + 281474976710724L, 281474976710717L, 281474976710729L, 281474976710731L, 281474976710752L, + 281474976710748L, 281474976710753L, 281474976710755L, 281474976710754L, 281474976710768L, + 281474976710772L } }; + private static readonly float[][] POSITION = { + new[] { 6.457663f, 10.197294f, -18.334061f }, + new[] { -1.433933f, 10.197294f, -1.359993f }, + new[] { 12.184784f, 9.997294f, -18.941269f }, + new[] { 0.863553f, 10.197294f, -10.310320f }, + new[] { 18.784092f, 10.197294f, 3.054368f } }; + + [Test] + public void testMoveAlongSurface() { + QueryFilter filter = new DefaultQueryFilter(); + for (int i = 0; i < startRefs.Length; i++) { + long startRef = startRefs[i]; + float[] startPos = startPoss[i]; + float[] endPos = endPoss[i]; + Result result = query.moveAlongSurface(startRef, startPos, endPos, filter); + Assert.That(result.succeeded(), Is.True); + MoveAlongSurfaceResult path = result.result; + for (int v = 0; v < 3; v++) { + Assert.That(path.getResultPos()[v], Is.EqualTo(POSITION[i][v]).Within(0.01f)); + } + Assert.That(path.getVisited().Count, Is.EqualTo(VISITED[i].Length)); + for (int j = 0; j < POSITION[i].Length; j++) { + Assert.That(path.getVisited()[j], Is.EqualTo(VISITED[i][j])); + } + } + } + +} diff --git a/test/DotRecast.Detour.Test/NavMeshBuilderTest.cs b/test/DotRecast.Detour.Test/NavMeshBuilderTest.cs new file mode 100644 index 0000000..ff34148 --- /dev/null +++ b/test/DotRecast.Detour.Test/NavMeshBuilderTest.cs @@ -0,0 +1,64 @@ +/* +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using NUnit.Framework; + +namespace DotRecast.Detour.Test; + + +public class NavMeshBuilderTest { + + private MeshData nmd; + + [SetUp] + public void setUp() { + nmd = new RecastTestMeshBuilder().getMeshData(); + } + + [Test] + public void testBVTree() { + Assert.That(nmd.verts.Length / 3, Is.EqualTo(225)); + Assert.That(nmd.polys.Length, Is.EqualTo(119)); + Assert.That(nmd.header.maxLinkCount, Is.EqualTo(457)); + Assert.That(nmd.detailMeshes.Length, Is.EqualTo(118)); + Assert.That(nmd.detailTris.Length / 4, Is.EqualTo(291)); + Assert.That(nmd.detailVerts.Length / 3, Is.EqualTo(60)); + Assert.That(nmd.offMeshCons.Length, Is.EqualTo(1)); + Assert.That(nmd.header.offMeshBase, Is.EqualTo(118)); + Assert.That(nmd.bvTree.Length, Is.EqualTo(236)); + Assert.That(nmd.bvTree.Length, Is.GreaterThanOrEqualTo(nmd.header.bvNodeCount)); + for (int i = 0; i < nmd.header.bvNodeCount; i++) { + Assert.That(nmd.bvTree[i], Is.Not.Null); + } + for (int i = 0; i < 6; i++) { + Assert.That(nmd.verts[223 * 3 + i], Is.EqualTo(nmd.offMeshCons[0].pos[i])); + } + Assert.That(nmd.offMeshCons[0].rad, Is.EqualTo(0.1f)); + Assert.That(nmd.offMeshCons[0].poly, Is.EqualTo(118)); + Assert.That(nmd.offMeshCons[0].flags, Is.EqualTo(NavMesh.DT_OFFMESH_CON_BIDIR)); + Assert.That(nmd.offMeshCons[0].side, Is.EqualTo(0xFF)); + Assert.That(nmd.offMeshCons[0].userId, Is.EqualTo(0x4567)); + Assert.That(nmd.polys[118].vertCount, Is.EqualTo(2)); + Assert.That(nmd.polys[118].verts[0], Is.EqualTo(223)); + Assert.That(nmd.polys[118].verts[1], Is.EqualTo(224)); + Assert.That(nmd.polys[118].flags, Is.EqualTo(12)); + Assert.That(nmd.polys[118].getArea(), Is.EqualTo(2)); + Assert.That(nmd.polys[118].getType(), Is.EqualTo(Poly.DT_POLYTYPE_OFFMESH_CONNECTION)); + + } +} \ No newline at end of file diff --git a/test/DotRecast.Detour.Test/PolygonByCircleConstraintTest.cs b/test/DotRecast.Detour.Test/PolygonByCircleConstraintTest.cs new file mode 100644 index 0000000..469dbc3 --- /dev/null +++ b/test/DotRecast.Detour.Test/PolygonByCircleConstraintTest.cs @@ -0,0 +1,87 @@ +/* +recast4j copyright (c) 2021 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using NUnit.Framework; + +namespace DotRecast.Detour.Test; + + +public class PolygonByCircleConstraintTest { + + private readonly PolygonByCircleConstraint constraint = new PolygonByCircleConstraint.StrictPolygonByCircleConstraint(); + + [Test] + public void shouldHandlePolygonFullyInsideCircle() { + float[] polygon = { -2, 0, 2, 2, 0, 2, 2, 0, -2, -2, 0, -2 }; + float[] center = { 1, 0, 1 }; + float[] constrained = constraint.aply(polygon, center, 6); + + Assert.That(constrained, Is.EqualTo(polygon)); + } + + [Test] + public void shouldHandleVerticalSegment() { + int expectedSize = 21; + float[] polygon = { -2, 0, 2, 2, 0, 2, 2, 0, -2, -2, 0, -2 }; + float[] center = { 2, 0, 0 }; + + float[] constrained = constraint.aply(polygon, center, 3); + Assert.That(constrained.Length, Is.EqualTo(expectedSize)); + Assert.That(constrained, Is.SupersetOf(new[] {2f, 0f, 2f, 2f, 0f, -2f})); + } + + [Test] + public void shouldHandleCircleFullyInsidePolygon() { + int expectedSize = 12 * 3; + float[] polygon = { -4, 0, 0, -3, 0, 3, 2, 0, 3, 3, 0, -3, -2, 0, -4 }; + float[] center = { -1, 0, -1 }; + float[] constrained = constraint.aply(polygon, center, 2); + + Assert.That(constrained.Length, Is.EqualTo(expectedSize)); + + for (int i = 0; i < expectedSize; i += 3) { + float x = constrained[i] + 1; + float z = constrained[i + 2] + 1; + Assert.That(x * x + z * z, Is.EqualTo(4).Within(1e-4f)); + } + } + + [Test] + public void shouldHandleCircleInsidePolygon() { + int expectedSize = 9 * 3; + float[] polygon = { -4, 0, 0, -3, 0, 3, 2, 0, 3, 3, 0, -3, -2, 0, -4 }; + float[] center = { -2, 0, -1 }; + float[] constrained = constraint.aply(polygon, center, 3); + + Assert.That(constrained.Length, Is.EqualTo(expectedSize)); + Assert.That(constrained, Is.SupersetOf(new[] { -2f, 0f, -4f, -4f, 0f, 0f, -3.4641016f, 0.0f, 1.6076951f, -2.0f, 0.0f, 2.0f })); + } + + [Test] + public void shouldHandleCircleOutsidePolygon() + { + int expectedSize = 7 * 3; + float[] polygon = { -4, 0, 0, -3, 0, 3, 2, 0, 3, 3, 0, -3, -2, 0, -4 }; + float[] center = { 4, 0, 0 }; + float[] constrained = constraint.aply(polygon, center, 4); + + Assert.That(constrained.Length, Is.EqualTo(expectedSize)); + Assert.That(constrained, Is.SupersetOf(new[] { 1.5358982f, 0f, 3f, 2f, 0f, 3f, 3f, 0f, -3f })); +} + +} diff --git a/test/DotRecast.Detour.Test/RandomPointTest.cs b/test/DotRecast.Detour.Test/RandomPointTest.cs new file mode 100644 index 0000000..6666dc8 --- /dev/null +++ b/test/DotRecast.Detour.Test/RandomPointTest.cs @@ -0,0 +1,123 @@ +/* +recast4j Copyright (c) 2015-2021 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.Diagnostics; +using NUnit.Framework; + +using static DotRecast.Detour.DetourCommon; + +namespace DotRecast.Detour.Test; + +public class RandomPointTest : AbstractDetourTest { + + [Test] + public void testRandom() { + NavMeshQuery.FRand f = new NavMeshQuery.FRand(1); + QueryFilter filter = new DefaultQueryFilter(); + for (int i = 0; i < 1000; i++) { + Result point = query.findRandomPoint(filter, f); + Assert.That(point.succeeded(), Is.True); + Tuple tileAndPoly = navmesh.getTileAndPolyByRef(point.result.getRandomRef()).result; + float[] bmin = new float[2]; + float[] bmax = new float[2]; + for (int j = 0; j < tileAndPoly.Item2.vertCount; j++) { + int v = tileAndPoly.Item2.verts[j] * 3; + bmin[0] = j == 0 ? tileAndPoly.Item1.data.verts[v] : Math.Min(bmin[0], tileAndPoly.Item1.data.verts[v]); + bmax[0] = j == 0 ? tileAndPoly.Item1.data.verts[v] : Math.Max(bmax[0], tileAndPoly.Item1.data.verts[v]); + bmin[1] = j == 0 ? tileAndPoly.Item1.data.verts[v + 2] : Math.Min(bmin[1], tileAndPoly.Item1.data.verts[v + 2]); + bmax[1] = j == 0 ? tileAndPoly.Item1.data.verts[v + 2] : Math.Max(bmax[1], tileAndPoly.Item1.data.verts[v + 2]); + } + Assert.That(point.result.getRandomPt()[0] >= bmin[0], Is.True); + Assert.That(point.result.getRandomPt()[0] <= bmax[0], Is.True); + Assert.That(point.result.getRandomPt()[2] >= bmin[1], Is.True); + Assert.That(point.result.getRandomPt()[2] <= bmax[1], Is.True); + } + } + + [Test] + public void testRandomAroundCircle() { + NavMeshQuery.FRand f = new NavMeshQuery.FRand(1); + QueryFilter filter = new DefaultQueryFilter(); + FindRandomPointResult point = query.findRandomPoint(filter, f).result; + for (int i = 0; i < 1000; i++) { + Result result = query.findRandomPointAroundCircle(point.getRandomRef(), point.getRandomPt(), + 5f, filter, f); + Assert.That(result.failed(), Is.False); + point = result.result; + Tuple tileAndPoly = navmesh.getTileAndPolyByRef(point.getRandomRef()).result; + float[] bmin = new float[2]; + float[] bmax = new float[2]; + for (int j = 0; j < tileAndPoly.Item2.vertCount; j++) { + int v = tileAndPoly.Item2.verts[j] * 3; + bmin[0] = j == 0 ? tileAndPoly.Item1.data.verts[v] : Math.Min(bmin[0], tileAndPoly.Item1.data.verts[v]); + bmax[0] = j == 0 ? tileAndPoly.Item1.data.verts[v] : Math.Max(bmax[0], tileAndPoly.Item1.data.verts[v]); + bmin[1] = j == 0 ? tileAndPoly.Item1.data.verts[v + 2] : Math.Min(bmin[1], tileAndPoly.Item1.data.verts[v + 2]); + bmax[1] = j == 0 ? tileAndPoly.Item1.data.verts[v + 2] : Math.Max(bmax[1], tileAndPoly.Item1.data.verts[v + 2]); + } + Assert.That(point.getRandomPt()[0] >= bmin[0], Is.True); + Assert.That(point.getRandomPt()[0] <= bmax[0], Is.True); + Assert.That(point.getRandomPt()[2] >= bmin[1], Is.True); + Assert.That(point.getRandomPt()[2] <= bmax[1], Is.True); + } + } + + [Test] + public void testRandomWithinCircle() { + NavMeshQuery.FRand f = new NavMeshQuery.FRand(1); + QueryFilter filter = new DefaultQueryFilter(); + FindRandomPointResult point = query.findRandomPoint(filter, f).result; + float radius = 5f; + for (int i = 0; i < 1000; i++) { + Result result = query.findRandomPointWithinCircle(point.getRandomRef(), point.getRandomPt(), + radius, filter, f); + Assert.That(result.failed(), Is.False); + float distance = vDist2D(point.getRandomPt(), result.result.getRandomPt()); + Assert.That(distance <= radius, Is.True); + point = result.result; + } + } + + [Test] + public void testPerformance() { + NavMeshQuery.FRand f = new NavMeshQuery.FRand(1); + QueryFilter filter = new DefaultQueryFilter(); + FindRandomPointResult point = query.findRandomPoint(filter, f).result; + float radius = 5f; + // jvm warmup + for (int i = 0; i < 1000; i++) { + query.findRandomPointAroundCircle(point.getRandomRef(), point.getRandomPt(), radius, filter, f); + } + for (int i = 0; i < 1000; i++) { + query.findRandomPointWithinCircle(point.getRandomRef(), point.getRandomPt(), radius, filter, f); + } + + long t1 = Stopwatch.GetTimestamp(); + for (int i = 0; i < 10000; i++) { + query.findRandomPointAroundCircle(point.getRandomRef(), point.getRandomPt(), radius, filter, f); + } + long t2 = Stopwatch.GetTimestamp(); + for (int i = 0; i < 10000; i++) { + query.findRandomPointWithinCircle(point.getRandomRef(), point.getRandomPt(), radius, filter, f); + } + long t3 = Stopwatch.GetTimestamp(); + Console.WriteLine("Random point around circle: " + (t2 - t1) / 1000000 + "ms"); + Console.WriteLine("Random point within circle: " + (t3 - t2) / 1000000 + "ms"); + } + +} diff --git a/test/DotRecast.Detour.Test/RecastTestMeshBuilder.cs b/test/DotRecast.Detour.Test/RecastTestMeshBuilder.cs new file mode 100644 index 0000000..73a0faa --- /dev/null +++ b/test/DotRecast.Detour.Test/RecastTestMeshBuilder.cs @@ -0,0 +1,111 @@ +/* +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using DotRecast.Core; +using DotRecast.Recast; +using DotRecast.Recast.Geom; + +namespace DotRecast.Detour.Test; + +public class RecastTestMeshBuilder { + + private readonly MeshData meshData; + private const float m_cellSize = 0.3f; + private const float m_cellHeight = 0.2f; + private const float m_agentHeight = 2.0f; + private const float m_agentRadius = 0.6f; + private const float m_agentMaxClimb = 0.9f; + private const float m_agentMaxSlope = 45.0f; + private const int m_regionMinSize = 8; + private const int m_regionMergeSize = 20; + private const float m_edgeMaxLen = 12.0f; + private const float m_edgeMaxError = 1.3f; + private const int m_vertsPerPoly = 6; + private const float m_detailSampleDist = 6.0f; + private const float m_detailSampleMaxError = 1.0f; + + public RecastTestMeshBuilder() : + this(ObjImporter.load(Loader.ToBytes("dungeon.obj")), + PartitionType.WATERSHED, m_cellSize, m_cellHeight, m_agentHeight, m_agentRadius, m_agentMaxClimb, m_agentMaxSlope, + m_regionMinSize, m_regionMergeSize, m_edgeMaxLen, m_edgeMaxError, m_vertsPerPoly, m_detailSampleDist, + m_detailSampleMaxError) + { + } + + public RecastTestMeshBuilder(InputGeomProvider m_geom, PartitionType m_partitionType, float m_cellSize, + float m_cellHeight, float m_agentHeight, float m_agentRadius, float m_agentMaxClimb, float m_agentMaxSlope, + int m_regionMinSize, int m_regionMergeSize, float m_edgeMaxLen, float m_edgeMaxError, int m_vertsPerPoly, + float m_detailSampleDist, float m_detailSampleMaxError) { + RecastConfig cfg = new RecastConfig(m_partitionType, m_cellSize, m_cellHeight, m_agentHeight, m_agentRadius, + m_agentMaxClimb, m_agentMaxSlope, m_regionMinSize, m_regionMergeSize, m_edgeMaxLen, m_edgeMaxError, + m_vertsPerPoly, m_detailSampleDist, m_detailSampleMaxError, SampleAreaModifications.SAMPLE_AREAMOD_GROUND); + RecastBuilderConfig bcfg = new RecastBuilderConfig(cfg, m_geom.getMeshBoundsMin(), m_geom.getMeshBoundsMax()); + RecastBuilder rcBuilder = new RecastBuilder(); + RecastBuilderResult rcResult = rcBuilder.build(m_geom, bcfg); + PolyMesh m_pmesh = rcResult.getMesh(); + for (int i = 0; i < m_pmesh.npolys; ++i) { + m_pmesh.flags[i] = 1; + } + PolyMeshDetail m_dmesh = rcResult.getMeshDetail(); + NavMeshDataCreateParams option = new NavMeshDataCreateParams(); + option.verts = m_pmesh.verts; + option.vertCount = m_pmesh.nverts; + option.polys = m_pmesh.polys; + option.polyAreas = m_pmesh.areas; + option.polyFlags = m_pmesh.flags; + option.polyCount = m_pmesh.npolys; + option.nvp = m_pmesh.nvp; + option.detailMeshes = m_dmesh.meshes; + option.detailVerts = m_dmesh.verts; + option.detailVertsCount = m_dmesh.nverts; + option.detailTris = m_dmesh.tris; + option.detailTriCount = m_dmesh.ntris; + option.walkableHeight = m_agentHeight; + option.walkableRadius = m_agentRadius; + option.walkableClimb = m_agentMaxClimb; + option.bmin = m_pmesh.bmin; + option.bmax = m_pmesh.bmax; + option.cs = m_cellSize; + option.ch = m_cellHeight; + option.buildBvTree = true; + + option.offMeshConVerts = new float[6]; + option.offMeshConVerts[0] = 0.1f; + option.offMeshConVerts[1] = 0.2f; + option.offMeshConVerts[2] = 0.3f; + option.offMeshConVerts[3] = 0.4f; + option.offMeshConVerts[4] = 0.5f; + option.offMeshConVerts[5] = 0.6f; + option.offMeshConRad = new float[1]; + option.offMeshConRad[0] = 0.1f; + option.offMeshConDir = new int[1]; + option.offMeshConDir[0] = 1; + option.offMeshConAreas = new int[1]; + option.offMeshConAreas[0] = 2; + option.offMeshConFlags = new int[1]; + option.offMeshConFlags[0] = 12; + option.offMeshConUserID = new int[1]; + option.offMeshConUserID[0] = 0x4567; + option.offMeshConCount = 1; + meshData = NavMeshBuilder.createNavMeshData(option); + } + + public MeshData getMeshData() { + return meshData; + } +} diff --git a/test/DotRecast.Detour.Test/SampleAreaModifications.cs b/test/DotRecast.Detour.Test/SampleAreaModifications.cs new file mode 100644 index 0000000..b30acaf --- /dev/null +++ b/test/DotRecast.Detour.Test/SampleAreaModifications.cs @@ -0,0 +1,52 @@ +/* +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using DotRecast.Recast; + +namespace DotRecast.Detour.Test; + +public class SampleAreaModifications { + + public const int SAMPLE_POLYAREA_TYPE_MASK = 0x07; + public const int SAMPLE_POLYAREA_TYPE_GROUND = 0x1; + public const int SAMPLE_POLYAREA_TYPE_WATER = 0x2; + public const int SAMPLE_POLYAREA_TYPE_ROAD = 0x3; + public const int SAMPLE_POLYAREA_TYPE_DOOR = 0x4; + public const int SAMPLE_POLYAREA_TYPE_GRASS = 0x5; + public const int SAMPLE_POLYAREA_TYPE_JUMP = 0x6; + + public static AreaModification SAMPLE_AREAMOD_GROUND = new AreaModification(SAMPLE_POLYAREA_TYPE_GROUND, + SAMPLE_POLYAREA_TYPE_MASK); + public static AreaModification SAMPLE_AREAMOD_WATER = new AreaModification(SAMPLE_POLYAREA_TYPE_WATER, + SAMPLE_POLYAREA_TYPE_MASK); + public static AreaModification SAMPLE_AREAMOD_ROAD = new AreaModification(SAMPLE_POLYAREA_TYPE_ROAD, + SAMPLE_POLYAREA_TYPE_MASK); + public static AreaModification SAMPLE_AREAMOD_GRASS = new AreaModification(SAMPLE_POLYAREA_TYPE_GRASS, + SAMPLE_POLYAREA_TYPE_MASK); + public static AreaModification SAMPLE_AREAMOD_DOOR = new AreaModification(SAMPLE_POLYAREA_TYPE_DOOR, + SAMPLE_POLYAREA_TYPE_DOOR); + public static AreaModification SAMPLE_AREAMOD_JUMP = new AreaModification(SAMPLE_POLYAREA_TYPE_JUMP, + SAMPLE_POLYAREA_TYPE_JUMP); + + public const int SAMPLE_POLYFLAGS_WALK = 0x01; // Ability to walk (ground, grass, road) + public const int SAMPLE_POLYFLAGS_SWIM = 0x02; // Ability to swim (water). + public const int SAMPLE_POLYFLAGS_DOOR = 0x04; // Ability to move through doors. + public const int SAMPLE_POLYFLAGS_JUMP = 0x08; // Ability to jump. + public const int SAMPLE_POLYFLAGS_DISABLED = 0x10; // Disabled polygon + public const int SAMPLE_POLYFLAGS_ALL = 0xffff; // All abilities. +} diff --git a/test/DotRecast.Detour.Test/TestDetourBuilder.cs b/test/DotRecast.Detour.Test/TestDetourBuilder.cs new file mode 100644 index 0000000..326395d --- /dev/null +++ b/test/DotRecast.Detour.Test/TestDetourBuilder.cs @@ -0,0 +1,91 @@ +/* +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using DotRecast.Recast; +using DotRecast.Recast.Geom; + +namespace DotRecast.Detour.Test; + +public class TestDetourBuilder : DetourBuilder { + + public MeshData build(InputGeomProvider geom, RecastBuilderConfig rcConfig, float agentHeight, float agentRadius, + float agentMaxClimb, int x, int y, bool applyRecastDemoFlags) { + RecastBuilder rcBuilder = new RecastBuilder(); + RecastBuilderResult rcResult = rcBuilder.build(geom, rcConfig); + PolyMesh pmesh = rcResult.getMesh(); + + if (applyRecastDemoFlags) { + // Update poly flags from areas. + for (int i = 0; i < pmesh.npolys; ++i) { + if (pmesh.areas[i] == SampleAreaModifications.SAMPLE_POLYAREA_TYPE_GROUND + || pmesh.areas[i] == SampleAreaModifications.SAMPLE_POLYAREA_TYPE_GRASS + || pmesh.areas[i] == SampleAreaModifications.SAMPLE_POLYAREA_TYPE_ROAD) { + pmesh.flags[i] = SampleAreaModifications.SAMPLE_POLYFLAGS_WALK; + } else if (pmesh.areas[i] == SampleAreaModifications.SAMPLE_POLYAREA_TYPE_WATER) { + pmesh.flags[i] = SampleAreaModifications.SAMPLE_POLYFLAGS_SWIM; + } else if (pmesh.areas[i] == SampleAreaModifications.SAMPLE_POLYAREA_TYPE_DOOR) { + pmesh.flags[i] = SampleAreaModifications.SAMPLE_POLYFLAGS_WALK + | SampleAreaModifications.SAMPLE_POLYFLAGS_DOOR; + } + if (pmesh.areas[i] > 0) { + pmesh.areas[i]--; + } + } + } + PolyMeshDetail dmesh = rcResult.getMeshDetail(); + NavMeshDataCreateParams option = getNavMeshCreateParams(rcConfig.cfg, pmesh, dmesh, agentHeight, agentRadius, + agentMaxClimb); + return build(option, x, y); + } + + public NavMeshDataCreateParams getNavMeshCreateParams(RecastConfig rcConfig, PolyMesh pmesh, PolyMeshDetail dmesh, + float agentHeight, float agentRadius, float agentMaxClimb) { + NavMeshDataCreateParams option = new NavMeshDataCreateParams(); + option.verts = pmesh.verts; + option.vertCount = pmesh.nverts; + option.polys = pmesh.polys; + option.polyAreas = pmesh.areas; + option.polyFlags = pmesh.flags; + option.polyCount = pmesh.npolys; + option.nvp = pmesh.nvp; + if (dmesh != null) { + option.detailMeshes = dmesh.meshes; + option.detailVerts = dmesh.verts; + option.detailVertsCount = dmesh.nverts; + option.detailTris = dmesh.tris; + option.detailTriCount = dmesh.ntris; + } + option.walkableHeight = agentHeight; + option.walkableRadius = agentRadius; + option.walkableClimb = agentMaxClimb; + option.bmin = pmesh.bmin; + option.bmax = pmesh.bmax; + option.cs = rcConfig.cs; + option.ch = rcConfig.ch; + option.buildBvTree = true; + /* + * option.offMeshConVerts = m_geom->getOffMeshConnectionVerts(); option.offMeshConRad = + * m_geom->getOffMeshConnectionRads(); option.offMeshConDir = m_geom->getOffMeshConnectionDirs(); + * option.offMeshConAreas = m_geom->getOffMeshConnectionAreas(); option.offMeshConFlags = + * m_geom->getOffMeshConnectionFlags(); option.offMeshConUserID = m_geom->getOffMeshConnectionId(); + * option.offMeshConCount = m_geom->getOffMeshConnectionCount(); + */ + return option; + + } +} diff --git a/test/DotRecast.Detour.Test/TestTiledNavMeshBuilder.cs b/test/DotRecast.Detour.Test/TestTiledNavMeshBuilder.cs new file mode 100644 index 0000000..330cd1e --- /dev/null +++ b/test/DotRecast.Detour.Test/TestTiledNavMeshBuilder.cs @@ -0,0 +1,120 @@ +/* +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.Collections.Generic; +using DotRecast.Core; +using DotRecast.Recast; +using DotRecast.Recast.Geom; + +using static DotRecast.Recast.RecastVectors; + +namespace DotRecast.Detour.Test; + +public class TestTiledNavMeshBuilder { + + private readonly NavMesh navMesh; + private const float m_cellSize = 0.3f; + private const float m_cellHeight = 0.2f; + private const float m_agentHeight = 2.0f; + private const float m_agentRadius = 0.6f; + private const float m_agentMaxClimb = 0.9f; + private const float m_agentMaxSlope = 45.0f; + private const int m_regionMinSize = 8; + private const int m_regionMergeSize = 20; + private const float m_regionMinArea = m_regionMinSize * m_regionMinSize * m_cellSize * m_cellSize; + private const float m_regionMergeArea = m_regionMergeSize * m_regionMergeSize * m_cellSize * m_cellSize; + private const float m_edgeMaxLen = 12.0f; + private const float m_edgeMaxError = 1.3f; + private const int m_vertsPerPoly = 6; + private const float m_detailSampleDist = 6.0f; + private const float m_detailSampleMaxError = 1.0f; + private const int m_tileSize = 32; + + public TestTiledNavMeshBuilder() : + this(ObjImporter.load(Loader.ToBytes("dungeon.obj")), + PartitionType.WATERSHED, m_cellSize, m_cellHeight, m_agentHeight, m_agentRadius, m_agentMaxClimb, m_agentMaxSlope, + m_regionMinSize, m_regionMergeSize, m_edgeMaxLen, m_edgeMaxError, m_vertsPerPoly, m_detailSampleDist, + m_detailSampleMaxError, m_tileSize) + { + } + + public TestTiledNavMeshBuilder(InputGeomProvider m_geom, PartitionType m_partitionType, float m_cellSize, float m_cellHeight, + float m_agentHeight, float m_agentRadius, float m_agentMaxClimb, float m_agentMaxSlope, int m_regionMinSize, + int m_regionMergeSize, float m_edgeMaxLen, float m_edgeMaxError, int m_vertsPerPoly, float m_detailSampleDist, + float m_detailSampleMaxError, int m_tileSize) { + + // Create empty nav mesh + NavMeshParams navMeshParams = new NavMeshParams(); + copy(navMeshParams.orig, m_geom.getMeshBoundsMin()); + navMeshParams.tileWidth = m_tileSize * m_cellSize; + navMeshParams.tileHeight = m_tileSize * m_cellSize; + navMeshParams.maxTiles = 128; + navMeshParams.maxPolys = 32768; + navMesh = new NavMesh(navMeshParams, 6); + + // Build all tiles + RecastConfig cfg = new RecastConfig(true, m_tileSize, m_tileSize, RecastConfig.calcBorder(m_agentRadius, m_cellSize), + m_partitionType, m_cellSize, m_cellHeight, m_agentMaxSlope, true, true, true, m_agentHeight, m_agentRadius, + m_agentMaxClimb, m_regionMinArea, m_regionMergeArea, m_edgeMaxLen, m_edgeMaxError, m_vertsPerPoly, true, + m_detailSampleDist, m_detailSampleMaxError, SampleAreaModifications.SAMPLE_AREAMOD_GROUND); + RecastBuilder rcBuilder = new RecastBuilder(); + List rcResult = rcBuilder.buildTiles(m_geom, cfg, null); + + // Add tiles to nav mesh + + foreach (RecastBuilderResult result in rcResult) { + PolyMesh pmesh = result.getMesh(); + if (pmesh.npolys == 0) { + continue; + } + for (int i = 0; i < pmesh.npolys; ++i) { + pmesh.flags[i] = 1; + } + NavMeshDataCreateParams option = new NavMeshDataCreateParams(); + option.verts = pmesh.verts; + option.vertCount = pmesh.nverts; + option.polys = pmesh.polys; + option.polyAreas = pmesh.areas; + option.polyFlags = pmesh.flags; + option.polyCount = pmesh.npolys; + option.nvp = pmesh.nvp; + PolyMeshDetail dmesh = result.getMeshDetail(); + option.detailMeshes = dmesh.meshes; + option.detailVerts = dmesh.verts; + option.detailVertsCount = dmesh.nverts; + option.detailTris = dmesh.tris; + option.detailTriCount = dmesh.ntris; + option.walkableHeight = m_agentHeight; + option.walkableRadius = m_agentRadius; + option.walkableClimb = m_agentMaxClimb; + option.bmin = pmesh.bmin; + option.bmax = pmesh.bmax; + option.cs = m_cellSize; + option.ch = m_cellHeight; + option.tileX = result.tileX; + option.tileZ = result.tileZ; + option.buildBvTree = true; + navMesh.addTile(NavMeshBuilder.createNavMeshData(option), 0, 0); + } + } + + public NavMesh getNavMesh() { + return navMesh; + } + +} diff --git a/test/DotRecast.Detour.Test/TiledFindPathTest.cs b/test/DotRecast.Detour.Test/TiledFindPathTest.cs new file mode 100644 index 0000000..638b275 --- /dev/null +++ b/test/DotRecast.Detour.Test/TiledFindPathTest.cs @@ -0,0 +1,69 @@ +/* +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.Collections.Generic; +using NUnit.Framework; + +namespace DotRecast.Detour.Test; + +public class TiledFindPathTest { + + private static readonly Status[] STATUSES = { Status.SUCCSESS }; + private static readonly long[][] RESULTS = { + new[] { 281475015507969L, 281475014459393L, 281475014459392L, 281475006070784L, + 281475005022208L, 281475003973636L, 281475012362240L, 281475012362241L, 281475012362242L, 281475003973634L, + 281475003973635L, 281475003973633L, 281475002925059L, 281475002925057L, 281475002925056L, 281474998730753L, + 281474998730754L, 281474994536450L, 281474994536451L, 281474994536452L, 281474994536448L, 281474990342146L, + 281474990342145L, 281474991390723L, 281474991390724L, 281474991390725L, 281474987196418L, 281474987196417L, + 281474988244996L, 281474988244995L, 281474988244997L, 281474985099266L } }; + protected static readonly long[] START_REFS = { 281475015507969L }; + protected static readonly long[] END_REFS = { 281474985099266L }; + protected static readonly float[][] START_POS = { new[] { 39.447338f, 9.998177f, -0.784811f } }; + protected static readonly float[][] END_POS = { new[] { 19.292645f, 11.611748f, -57.750366f } }; + + protected NavMeshQuery query; + protected NavMesh navmesh; + + [SetUp] + public void setUp() { + navmesh = createNavMesh(); + query = new NavMeshQuery(navmesh); + } + + protected NavMesh createNavMesh() { + return new TestTiledNavMeshBuilder().getNavMesh(); + } + + [Test] + public void testFindPath() { + QueryFilter filter = new DefaultQueryFilter(); + for (int i = 0; i < START_REFS.Length; i++) { + long startRef = START_REFS[i]; + long endRef = END_REFS[i]; + float[] startPos = START_POS[i]; + float[] endPos = END_POS[i]; + Result> path = query.findPath(startRef, endRef, startPos, endPos, filter); + Assert.That(path.status, Is.EqualTo(STATUSES[i])); + Assert.That(path.result.Count, Is.EqualTo(RESULTS[i].Length)); + for (int j = 0; j < RESULTS[i].Length; j++) { + Assert.That(RESULTS[i][j], Is.EqualTo(path.result[j])); + } + } + } + +} diff --git a/test/DotRecast.Detour.TileCache.Test/AbstractTileCacheTest.cs b/test/DotRecast.Detour.TileCache.Test/AbstractTileCacheTest.cs new file mode 100644 index 0000000..b8647e1 --- /dev/null +++ b/test/DotRecast.Detour.TileCache.Test/AbstractTileCacheTest.cs @@ -0,0 +1,74 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using DotRecast.Core; +using DotRecast.Detour.TileCache.Io.Compress; +using DotRecast.Recast.Geom; + +using static DotRecast.Detour.DetourCommon; +using static DotRecast.Recast.RecastVectors; + +namespace DotRecast.Detour.TileCache.Test; + +public class AbstractTileCacheTest { + + private const int EXPECTED_LAYERS_PER_TILE = 4; + private readonly float m_cellSize = 0.3f; + private readonly float m_cellHeight = 0.2f; + private readonly float m_agentHeight = 2.0f; + private readonly float m_agentRadius = 0.6f; + private readonly float m_agentMaxClimb = 0.9f; + private readonly float m_edgeMaxError = 1.3f; + private readonly int m_tileSize = 48; + + protected class TestTileCacheMeshProcess : TileCacheMeshProcess { + public void process(NavMeshDataCreateParams option) { + for (int i = 0; i < option.polyCount; ++i) { + option.polyFlags[i] = 1; + } + } + } + + public TileCache getTileCache(InputGeomProvider geom, ByteOrder order, bool cCompatibility) { + TileCacheParams option = new TileCacheParams(); + int[] twh = Recast.Recast.calcTileCount(geom.getMeshBoundsMin(), geom.getMeshBoundsMax(), m_cellSize, m_tileSize, m_tileSize); + option.ch = m_cellHeight; + option.cs = m_cellSize; + vCopy(option.orig, geom.getMeshBoundsMin()); + option.height = m_tileSize; + option.width = m_tileSize; + option.walkableHeight = m_agentHeight; + option.walkableRadius = m_agentRadius; + option.walkableClimb = m_agentMaxClimb; + option.maxSimplificationError = m_edgeMaxError; + option.maxTiles = twh[0] * twh[1] * EXPECTED_LAYERS_PER_TILE; + option.maxObstacles = 128; + NavMeshParams navMeshParams = new NavMeshParams(); + copy(navMeshParams.orig, geom.getMeshBoundsMin()); + navMeshParams.tileWidth = m_tileSize * m_cellSize; + navMeshParams.tileHeight = m_tileSize * m_cellSize; + navMeshParams.maxTiles = 256; + navMeshParams.maxPolys = 16384; + NavMesh navMesh = new NavMesh(navMeshParams, 6); + TileCache tc = new TileCache(option, new TileCacheStorageParams(order, cCompatibility), navMesh, + TileCacheCompressorFactory.get(cCompatibility), new TestTileCacheMeshProcess()); + return tc; + } + +} diff --git a/test/DotRecast.Detour.TileCache.Test/DotRecast.Detour.TileCache.Test.csproj b/test/DotRecast.Detour.TileCache.Test/DotRecast.Detour.TileCache.Test.csproj new file mode 100644 index 0000000..8ede8dd --- /dev/null +++ b/test/DotRecast.Detour.TileCache.Test/DotRecast.Detour.TileCache.Test.csproj @@ -0,0 +1,25 @@ + + + + net7.0 + false + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + diff --git a/test/DotRecast.Detour.TileCache.Test/Io/TileCacheReaderTest.cs b/test/DotRecast.Detour.TileCache.Test/Io/TileCacheReaderTest.cs new file mode 100644 index 0000000..088c68f --- /dev/null +++ b/test/DotRecast.Detour.TileCache.Test/Io/TileCacheReaderTest.cs @@ -0,0 +1,223 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.IO; +using DotRecast.Core; +using DotRecast.Detour.TileCache.Io; +using NUnit.Framework; + +namespace DotRecast.Detour.TileCache.Test.Io; + +public class TileCacheReaderTest { + + private readonly TileCacheReader reader = new TileCacheReader(); + + [Test] + public void testNavmesh() { + + using var ms = new MemoryStream(Loader.ToBytes("all_tiles_tilecache.bin")); + using var @is = new BinaryReader(ms); + TileCache tc = reader.read(@is, 6, null); + Assert.That(tc.getNavMesh().getMaxTiles(), Is.EqualTo(256)); + Assert.That(tc.getNavMesh().getParams().maxPolys, Is.EqualTo(16384)); + Assert.That(tc.getNavMesh().getParams().tileWidth, Is.EqualTo(14.4f).Within(0.001f)); + Assert.That(tc.getNavMesh().getParams().tileHeight, Is.EqualTo(14.4f).Within(0.001f)); + Assert.That(tc.getNavMesh().getMaxVertsPerPoly(), Is.EqualTo(6)); + Assert.That(tc.getParams().cs, Is.EqualTo(0.3f).Within(0.0f)); + Assert.That(tc.getParams().ch, Is.EqualTo(0.2f).Within(0.0f)); + Assert.That(tc.getParams().walkableClimb, Is.EqualTo(0.9f).Within(0.0f)); + Assert.That(tc.getParams().walkableHeight, Is.EqualTo(2f).Within(0.0f)); + Assert.That(tc.getParams().walkableRadius, Is.EqualTo(0.6f).Within(0.0f)); + Assert.That(tc.getParams().width, Is.EqualTo(48)); + Assert.That(tc.getParams().maxTiles, Is.EqualTo(6 * 7 * 4)); + Assert.That(tc.getParams().maxObstacles, Is.EqualTo(128)); + Assert.That(tc.getTileCount(), Is.EqualTo(168)); + // Tile0: Tris: 1, Verts: 4 Detail Meshed: 1 Detail Verts: 0 Detail Tris: 2 + // Verts: -2.269517, 28.710686, 28.710686 + MeshTile tile = tc.getNavMesh().getTile(0); + MeshData data = tile.data; + MeshHeader header = data.header; + Assert.That(header.vertCount, Is.EqualTo(4)); + Assert.That(header.polyCount, Is.EqualTo(1)); + Assert.That(header.detailMeshCount, Is.EqualTo(1)); + Assert.That(header.detailVertCount, Is.EqualTo(0)); + Assert.That(header.detailTriCount, Is.EqualTo(2)); + Assert.That(data.polys.Length, Is.EqualTo(1)); + Assert.That(data.verts.Length, Is.EqualTo(3 * 4)); + Assert.That(data.detailMeshes.Length, Is.EqualTo(1)); + Assert.That(data.detailVerts.Length, Is.EqualTo(0)); + Assert.That(data.detailTris.Length, Is.EqualTo(4 * 2)); + Assert.That(data.verts[1], Is.EqualTo(-2.269517f).Within(0.0001f)); + Assert.That(data.verts[6], Is.EqualTo(28.710686f).Within(0.0001f)); + Assert.That(data.verts[9], Is.EqualTo(28.710686f).Within(0.0001f)); + // Tile8: Tris: 7, Verts: 10 Detail Meshed: 7 Detail Verts: 0 Detail Tris: 10 + // Verts: 0.330483, 43.110687, 43.110687 + tile = tc.getNavMesh().getTile(8); + data = tile.data; + header = data.header; + Console.WriteLine(data.header.x + " " + data.header.y + " " + data.header.layer); + Assert.That(header.x, Is.EqualTo(4)); + Assert.That(header.y, Is.EqualTo(1)); + Assert.That(header.layer, Is.EqualTo(0)); + Assert.That(header.vertCount, Is.EqualTo(10)); + Assert.That(header.polyCount, Is.EqualTo(7)); + Assert.That(header.detailMeshCount, Is.EqualTo(7)); + Assert.That(header.detailVertCount, Is.EqualTo(0)); + Assert.That(header.detailTriCount, Is.EqualTo(10)); + Assert.That(data.polys.Length, Is.EqualTo(7)); + Assert.That(data.verts.Length, Is.EqualTo(3 * 10)); + Assert.That(data.detailMeshes.Length, Is.EqualTo(7)); + Assert.That(data.detailVerts.Length, Is.EqualTo(0)); + Assert.That(data.detailTris.Length, Is.EqualTo(4 * 10)); + Assert.That(data.verts[1], Is.EqualTo(0.330483f).Within(0.0001f)); + Assert.That(data.verts[6], Is.EqualTo(43.110687f).Within(0.0001f)); + Assert.That(data.verts[9], Is.EqualTo(43.110687f).Within(0.0001f)); + // Tile16: Tris: 13, Verts: 33 Detail Meshed: 13 Detail Verts: 0 Detail Tris: 25 + // Verts: 1.130483, 5.610685, 6.510685 + tile = tc.getNavMesh().getTile(16); + data = tile.data; + header = data.header; + Assert.That(header.vertCount, Is.EqualTo(33)); + Assert.That(header.polyCount, Is.EqualTo(13)); + Assert.That(header.detailMeshCount, Is.EqualTo(13)); + Assert.That(header.detailVertCount, Is.EqualTo(0)); + Assert.That(header.detailTriCount, Is.EqualTo(25)); + Assert.That(data.polys.Length, Is.EqualTo(13)); + Assert.That(data.verts.Length, Is.EqualTo(3 * 33)); + Assert.That(data.detailMeshes.Length, Is.EqualTo(13)); + Assert.That(data.detailVerts.Length, Is.EqualTo(0)); + Assert.That(data.detailTris.Length, Is.EqualTo(4 * 25)); + Assert.That(data.verts[1], Is.EqualTo(1.130483f).Within(0.0001f)); + Assert.That(data.verts[6], Is.EqualTo(5.610685f).Within(0.0001f)); + Assert.That(data.verts[9], Is.EqualTo(6.510685f).Within(0.0001f)); + // Tile29: Tris: 5, Verts: 15 Detail Meshed: 5 Detail Verts: 0 Detail Tris: 11 + // Verts: 10.330483, 10.110685, 10.110685 + tile = tc.getNavMesh().getTile(29); + data = tile.data; + header = data.header; + Assert.That(header.vertCount, Is.EqualTo(15)); + Assert.That(header.polyCount, Is.EqualTo(5)); + Assert.That(header.detailMeshCount, Is.EqualTo(5)); + Assert.That(header.detailVertCount, Is.EqualTo(0)); + Assert.That(header.detailTriCount, Is.EqualTo(11)); + Assert.That(data.polys.Length, Is.EqualTo(5)); + Assert.That(data.verts.Length, Is.EqualTo(3 * 15)); + Assert.That(data.detailMeshes.Length, Is.EqualTo(5)); + Assert.That(data.detailVerts.Length, Is.EqualTo(0)); + Assert.That(data.detailTris.Length, Is.EqualTo(4 * 11)); + Assert.That(data.verts[1], Is.EqualTo(10.330483f).Within(0.0001f)); + Assert.That(data.verts[6], Is.EqualTo(10.110685f).Within(0.0001f)); + Assert.That(data.verts[9], Is.EqualTo(10.110685f).Within(0.0001f)); + } + + [Test] + public void testDungeon() { + using var ms = new MemoryStream(Loader.ToBytes("dungeon_all_tiles_tilecache.bin")); + using var @is = new BinaryReader(ms); + TileCache tc = reader.read(@is, 6, null); + Assert.That(tc.getNavMesh().getMaxTiles(), Is.EqualTo(256)); + Assert.That(tc.getNavMesh().getParams().maxPolys, Is.EqualTo(16384)); + Assert.That(tc.getNavMesh().getParams().tileWidth, Is.EqualTo(14.4f).Within(0.001f)); + Assert.That(tc.getNavMesh().getParams().tileHeight, Is.EqualTo(14.4f).Within(0.001f)); + Assert.That(tc.getNavMesh().getMaxVertsPerPoly(), Is.EqualTo(6)); + Assert.That(tc.getParams().cs, Is.EqualTo(0.3f).Within(0.0f)); + Assert.That(tc.getParams().ch, Is.EqualTo(0.2f).Within(0.0f)); + Assert.That(tc.getParams().walkableClimb, Is.EqualTo(0.9f).Within(0.0f)); + Assert.That(tc.getParams().walkableHeight, Is.EqualTo(2f).Within(0.0f)); + Assert.That(tc.getParams().walkableRadius, Is.EqualTo(0.6f).Within(0.0f)); + Assert.That(tc.getParams().width, Is.EqualTo(48)); + Assert.That(tc.getParams().maxTiles, Is.EqualTo(6 * 7 * 4)); + Assert.That(tc.getParams().maxObstacles, Is.EqualTo(128)); + Assert.That(tc.getTileCount(), Is.EqualTo(168)); + // Tile0: Tris: 8, Verts: 18 Detail Meshed: 8 Detail Verts: 0 Detail Tris: 14 + // Verts: 14.997294, 15.484785, 15.484785 + MeshTile tile = tc.getNavMesh().getTile(0); + MeshData data = tile.data; + MeshHeader header = data.header; + Assert.That(header.vertCount, Is.EqualTo(18)); + Assert.That(header.polyCount, Is.EqualTo(8)); + Assert.That(header.detailMeshCount, Is.EqualTo(8)); + Assert.That(header.detailVertCount, Is.EqualTo(0)); + Assert.That(header.detailTriCount, Is.EqualTo(14)); + Assert.That(data.polys.Length, Is.EqualTo(8)); + Assert.That(data.verts.Length, Is.EqualTo(3 * 18)); + Assert.That(data.detailMeshes.Length, Is.EqualTo(8)); + Assert.That(data.detailVerts.Length, Is.EqualTo(0)); + Assert.That(data.detailTris.Length, Is.EqualTo(4 * 14)); + Assert.That(data.verts[1], Is.EqualTo(14.997294f).Within(0.0001f)); + Assert.That(data.verts[6], Is.EqualTo(15.484785f).Within(0.0001f)); + Assert.That(data.verts[9], Is.EqualTo(15.484785f).Within(0.0001f)); + // Tile8: Tris: 3, Verts: 8 Detail Meshed: 3 Detail Verts: 0 Detail Tris: 6 + // Verts: 13.597294, 17.584785, 17.584785 + tile = tc.getNavMesh().getTile(8); + data = tile.data; + header = data.header; + Assert.That(header.vertCount, Is.EqualTo(8)); + Assert.That(header.polyCount, Is.EqualTo(3)); + Assert.That(header.detailMeshCount, Is.EqualTo(3)); + Assert.That(header.detailVertCount, Is.EqualTo(0)); + Assert.That(header.detailTriCount, Is.EqualTo(6)); + Assert.That(data.polys.Length, Is.EqualTo(3)); + Assert.That(data.verts.Length, Is.EqualTo(3 * 8)); + Assert.That(data.detailMeshes.Length, Is.EqualTo(3)); + Assert.That(data.detailVerts.Length, Is.EqualTo(0)); + Assert.That(data.detailTris.Length, Is.EqualTo(4 * 6)); + Assert.That(data.verts[1], Is.EqualTo(13.597294f).Within(0.0001f)); + Assert.That(data.verts[6], Is.EqualTo(17.584785f).Within(0.0001f)); + Assert.That(data.verts[9], Is.EqualTo(17.584785f).Within(0.0001f)); + // Tile16: Tris: 10, Verts: 20 Detail Meshed: 10 Detail Verts: 0 Detail Tris: 18 + // Verts: 6.197294, -22.315216, -22.315216 + tile = tc.getNavMesh().getTile(16); + data = tile.data; + header = data.header; + Assert.That(header.vertCount, Is.EqualTo(20)); + Assert.That(header.polyCount, Is.EqualTo(10)); + Assert.That(header.detailMeshCount, Is.EqualTo(10)); + Assert.That(header.detailVertCount, Is.EqualTo(0)); + Assert.That(header.detailTriCount, Is.EqualTo(18)); + Assert.That(data.polys.Length, Is.EqualTo(10)); + Assert.That(data.verts.Length, Is.EqualTo(3 * 20)); + Assert.That(data.detailMeshes.Length, Is.EqualTo(10)); + Assert.That(data.detailVerts.Length, Is.EqualTo(0)); + Assert.That(data.detailTris.Length, Is.EqualTo(4 * 18)); + Assert.That(data.verts[1], Is.EqualTo(6.197294f).Within(0.0001f)); + Assert.That(data.verts[6], Is.EqualTo(-22.315216f).Within(0.0001f)); + Assert.That(data.verts[9], Is.EqualTo(-22.315216f).Within(0.0001f)); + // Tile29: Tris: 1, Verts: 5 Detail Meshed: 1 Detail Verts: 0 Detail Tris: 3 + // Verts: 10.197294, 48.484783, 48.484783 + tile = tc.getNavMesh().getTile(29); + data = tile.data; + header = data.header; + Assert.That(header.vertCount, Is.EqualTo(5)); + Assert.That(header.polyCount, Is.EqualTo(1)); + Assert.That(header.detailMeshCount, Is.EqualTo(1)); + Assert.That(header.detailVertCount, Is.EqualTo(0)); + Assert.That(header.detailTriCount, Is.EqualTo(3)); + Assert.That(data.polys.Length, Is.EqualTo(1)); + Assert.That(data.verts.Length, Is.EqualTo(3 * 5)); + Assert.That(data.detailMeshes.Length, Is.EqualTo(1)); + Assert.That(data.detailVerts.Length, Is.EqualTo(0)); + Assert.That(data.detailTris.Length, Is.EqualTo(4 * 3)); + Assert.That(data.verts[1], Is.EqualTo(10.197294f).Within(0.0001f)); + Assert.That(data.verts[6], Is.EqualTo(48.484783f).Within(0.0001f)); + Assert.That(data.verts[9], Is.EqualTo(48.484783f).Within(0.0001f)); + } + +} diff --git a/test/DotRecast.Detour.TileCache.Test/Io/TileCacheReaderWriterTest.cs b/test/DotRecast.Detour.TileCache.Test/Io/TileCacheReaderWriterTest.cs new file mode 100644 index 0000000..1dcf540 --- /dev/null +++ b/test/DotRecast.Detour.TileCache.Test/Io/TileCacheReaderWriterTest.cs @@ -0,0 +1,137 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.Collections.Generic; +using System.IO; +using DotRecast.Core; +using DotRecast.Detour.TileCache.Io; +using DotRecast.Recast; +using DotRecast.Recast.Geom; +using NUnit.Framework; + +namespace DotRecast.Detour.TileCache.Test.Io; + + +public class TileCacheReaderWriterTest : AbstractTileCacheTest { + + private readonly TileCacheReader reader = new TileCacheReader(); + private readonly TileCacheWriter writer = new TileCacheWriter(); + + [Test] + public void testFastLz() { + testDungeon(false); + testDungeon(true); + } + + [Test] + public void testLZ4() { + testDungeon(true); + testDungeon(false); + } + + private void testDungeon(bool cCompatibility) { + InputGeomProvider geom = ObjImporter.load(Loader.ToBytes("dungeon.obj")); + TestTileLayerBuilder layerBuilder = new TestTileLayerBuilder(geom); + List layers = layerBuilder.build(ByteOrder.LITTLE_ENDIAN, cCompatibility, 1); + TileCache tc = getTileCache(geom, ByteOrder.LITTLE_ENDIAN, cCompatibility); + foreach (byte[] layer in layers) { + long refs = tc.addTile(layer, 0); + tc.buildNavMeshTile(refs); + } + + using var msout = new MemoryStream(); + using var baos = new BinaryWriter(msout); + writer.write(baos, tc, ByteOrder.LITTLE_ENDIAN, cCompatibility); + + using var msis = new MemoryStream(msout.ToArray()); + using var bais = new BinaryReader(msis); + tc = reader.read(bais, 6, null); + Assert.That(tc.getNavMesh().getMaxTiles(), Is.EqualTo(256)); + Assert.That(tc.getNavMesh().getParams().maxPolys, Is.EqualTo(16384)); + Assert.That(tc.getNavMesh().getParams().tileWidth, Is.EqualTo(14.4f).Within(0.001f)); + Assert.That(tc.getNavMesh().getParams().tileHeight, Is.EqualTo(14.4f).Within(0.001f)); + Assert.That(tc.getNavMesh().getMaxVertsPerPoly(), Is.EqualTo(6)); + Assert.That(tc.getParams().cs, Is.EqualTo(0.3f).Within(0.0f)); + Assert.That(tc.getParams().ch, Is.EqualTo(0.2f).Within(0.0f)); + Assert.That(tc.getParams().walkableClimb, Is.EqualTo(0.9f).Within(0.0f)); + Assert.That(tc.getParams().walkableHeight, Is.EqualTo(2f).Within(0.0f)); + Assert.That(tc.getParams().walkableRadius, Is.EqualTo(0.6f).Within(0.0f)); + Assert.That(tc.getParams().width, Is.EqualTo(48)); + Assert.That(tc.getParams().maxTiles, Is.EqualTo(6 * 7 * 4)); + Assert.That(tc.getParams().maxObstacles, Is.EqualTo(128)); + Assert.That(tc.getTileCount(), Is.EqualTo(168)); + // Tile0: Tris: 8, Verts: 18 Detail Meshed: 8 Detail Verts: 0 Detail Tris: 14 + MeshTile tile = tc.getNavMesh().getTile(0); + MeshData data = tile.data; + MeshHeader header = data.header; + Assert.That(header.vertCount, Is.EqualTo(18)); + Assert.That(header.polyCount, Is.EqualTo(8)); + Assert.That(header.detailMeshCount, Is.EqualTo(8)); + Assert.That(header.detailVertCount, Is.EqualTo(0)); + Assert.That(header.detailTriCount, Is.EqualTo(14)); + Assert.That(data.polys.Length, Is.EqualTo(8)); + Assert.That(data.verts.Length, Is.EqualTo(3 * 18)); + Assert.That(data.detailMeshes.Length, Is.EqualTo(8)); + Assert.That(data.detailVerts.Length, Is.EqualTo(0)); + Assert.That(data.detailTris.Length, Is.EqualTo(4 * 14)); + // Tile8: Tris: 3, Verts: 8 Detail Meshed: 3 Detail Verts: 0 Detail Tris: 6 + tile = tc.getNavMesh().getTile(8); + data = tile.data; + header = data.header; + Assert.That(header.vertCount, Is.EqualTo(8)); + Assert.That(header.polyCount, Is.EqualTo(3)); + Assert.That(header.detailMeshCount, Is.EqualTo(3)); + Assert.That(header.detailVertCount, Is.EqualTo(0)); + Assert.That(header.detailTriCount, Is.EqualTo(6)); + Assert.That(data.polys.Length, Is.EqualTo(3)); + Assert.That(data.verts.Length, Is.EqualTo(3 * 8)); + Assert.That(data.detailMeshes.Length, Is.EqualTo(3)); + Assert.That(data.detailVerts.Length, Is.EqualTo(0)); + Assert.That(data.detailTris.Length, Is.EqualTo(4 * 6)); + // Tile16: Tris: 10, Verts: 20 Detail Meshed: 10 Detail Verts: 0 Detail Tris: 18 + tile = tc.getNavMesh().getTile(16); + data = tile.data; + header = data.header; + Assert.That(header.vertCount, Is.EqualTo(20)); + Assert.That(header.polyCount, Is.EqualTo(10)); + Assert.That(header.detailMeshCount, Is.EqualTo(10)); + Assert.That(header.detailVertCount, Is.EqualTo(0)); + Assert.That(header.detailTriCount, Is.EqualTo(18)); + Assert.That(data.polys.Length, Is.EqualTo(10)); + Assert.That(data.verts.Length, Is.EqualTo(3 * 20)); + Assert.That(data.detailMeshes.Length, Is.EqualTo(10)); + Assert.That(data.detailVerts.Length, Is.EqualTo(0)); + Assert.That(data.detailTris.Length, Is.EqualTo(4 * 18)); + // Tile29: Tris: 1, Verts: 5 Detail Meshed: 1 Detail Verts: 0 Detail Tris: 3 + tile = tc.getNavMesh().getTile(29); + data = tile.data; + header = data.header; + Assert.That(header.vertCount, Is.EqualTo(5)); + Assert.That(header.polyCount, Is.EqualTo(1)); + Assert.That(header.detailMeshCount, Is.EqualTo(1)); + Assert.That(header.detailVertCount, Is.EqualTo(0)); + Assert.That(header.detailTriCount, Is.EqualTo(3)); + Assert.That(data.polys.Length, Is.EqualTo(1)); + Assert.That(data.verts.Length, Is.EqualTo(3 * 5)); + Assert.That(data.detailMeshes.Length, Is.EqualTo(1)); + Assert.That(data.detailVerts.Length, Is.EqualTo(0)); + Assert.That(data.detailTris.Length, Is.EqualTo(4 * 3)); + } + +} diff --git a/test/DotRecast.Detour.TileCache.Test/SampleAreaModifications.cs b/test/DotRecast.Detour.TileCache.Test/SampleAreaModifications.cs new file mode 100644 index 0000000..bf3a32d --- /dev/null +++ b/test/DotRecast.Detour.TileCache.Test/SampleAreaModifications.cs @@ -0,0 +1,55 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using DotRecast.Recast; + +namespace DotRecast.Detour.TileCache.Test; + + + +public class SampleAreaModifications { + + public static int SAMPLE_POLYAREA_TYPE_MASK = 0x07; + /// Value for the kind of ceil "ground" + public static int SAMPLE_POLYAREA_TYPE_GROUND = 0x1; + /// Value for the kind of ceil "water" + public static int SAMPLE_POLYAREA_TYPE_WATER = 0x2; + /// Value for the kind of ceil "road" + public static int SAMPLE_POLYAREA_TYPE_ROAD = 0x3; + /// Value for the kind of ceil "grass" + public static int SAMPLE_POLYAREA_TYPE_GRASS = 0x4; + /// Flag for door area. Can be combined with area types and jump flag. + public static int SAMPLE_POLYAREA_FLAG_DOOR = 0x08; + /// Flag for jump area. Can be combined with area types and door flag. + public static int SAMPLE_POLYAREA_FLAG_JUMP = 0x10; + + public static AreaModification SAMPLE_AREAMOD_GROUND = new AreaModification(SAMPLE_POLYAREA_TYPE_GROUND, + SAMPLE_POLYAREA_TYPE_MASK); + public static AreaModification SAMPLE_AREAMOD_WATER = new AreaModification(SAMPLE_POLYAREA_TYPE_WATER, + SAMPLE_POLYAREA_TYPE_MASK); + public static AreaModification SAMPLE_AREAMOD_ROAD = new AreaModification(SAMPLE_POLYAREA_TYPE_ROAD, + SAMPLE_POLYAREA_TYPE_MASK); + public static AreaModification SAMPLE_AREAMOD_GRASS = new AreaModification(SAMPLE_POLYAREA_TYPE_GRASS, + SAMPLE_POLYAREA_TYPE_MASK); + public static AreaModification SAMPLE_AREAMOD_DOOR = new AreaModification(SAMPLE_POLYAREA_FLAG_DOOR, + SAMPLE_POLYAREA_FLAG_DOOR); + public static AreaModification SAMPLE_AREAMOD_JUMP = new AreaModification(SAMPLE_POLYAREA_FLAG_JUMP, + SAMPLE_POLYAREA_FLAG_JUMP); + +} diff --git a/test/DotRecast.Detour.TileCache.Test/TempObstaclesTest.cs b/test/DotRecast.Detour.TileCache.Test/TempObstaclesTest.cs new file mode 100644 index 0000000..cf6fab6 --- /dev/null +++ b/test/DotRecast.Detour.TileCache.Test/TempObstaclesTest.cs @@ -0,0 +1,92 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.Collections.Generic; +using DotRecast.Core; +using DotRecast.Recast; +using DotRecast.Recast.Geom; +using NUnit.Framework; + +namespace DotRecast.Detour.TileCache.Test; + +public class TempObstaclesTest : AbstractTileCacheTest { + + [Test] + public void testDungeon() { + bool cCompatibility = true; + InputGeomProvider geom = ObjImporter.load(Loader.ToBytes("dungeon.obj")); + TestTileLayerBuilder layerBuilder = new TestTileLayerBuilder(geom); + List layers = layerBuilder.build(ByteOrder.LITTLE_ENDIAN, cCompatibility, 1); + TileCache tc = getTileCache(geom, ByteOrder.LITTLE_ENDIAN, cCompatibility); + foreach (byte[] data in layers) { + long refs = tc.addTile(data, 0); + tc.buildNavMeshTile(refs); + } + List tiles = tc.getNavMesh().getTilesAt(1, 4); + MeshTile tile = tiles[0]; + Assert.That(tile.data.header.vertCount, Is.EqualTo(16)); + Assert.That(tile.data.header.polyCount, Is.EqualTo(6)); + long o = tc.addObstacle(new float[] { -1.815208f, 9.998184f, -20.307983f }, 1f, 2f); + bool upToDate = tc.update(); + Assert.That(upToDate, Is.True); + tiles = tc.getNavMesh().getTilesAt(1, 4); + tile = tiles[0]; + Assert.That(tile.data.header.vertCount, Is.EqualTo(22)); + Assert.That(tile.data.header.polyCount, Is.EqualTo(11)); + tc.removeObstacle(o); + upToDate = tc.update(); + Assert.That(upToDate, Is.True); + tiles = tc.getNavMesh().getTilesAt(1, 4); + tile = tiles[0]; + Assert.That(tile.data.header.vertCount, Is.EqualTo(16)); + Assert.That(tile.data.header.polyCount, Is.EqualTo(6)); + } + + [Test] + public void testDungeonBox() { + bool cCompatibility = true; + InputGeomProvider geom = ObjImporter.load(Loader.ToBytes("dungeon.obj")); + TestTileLayerBuilder layerBuilder = new TestTileLayerBuilder(geom); + List layers = layerBuilder.build(ByteOrder.LITTLE_ENDIAN, cCompatibility, 1); + TileCache tc = getTileCache(geom, ByteOrder.LITTLE_ENDIAN, cCompatibility); + foreach (byte[] data in layers) { + long refs = tc.addTile(data, 0); + tc.buildNavMeshTile(refs); + } + List tiles = tc.getNavMesh().getTilesAt(1, 4); + MeshTile tile = tiles[0]; + Assert.That(tile.data.header.vertCount, Is.EqualTo(16)); + Assert.That(tile.data.header.polyCount, Is.EqualTo(6)); + long o = tc.addBoxObstacle(new float[] { -2.315208f, 9.998184f, -20.807983f }, + new float[] { -1.315208f, 11.998184f, -19.807983f }); + bool upToDate = tc.update(); + Assert.That(upToDate, Is.True); + tiles = tc.getNavMesh().getTilesAt(1, 4); + tile = tiles[0]; + Assert.That(tile.data.header.vertCount, Is.EqualTo(22)); + Assert.That(tile.data.header.polyCount, Is.EqualTo(11)); + tc.removeObstacle(o); + upToDate = tc.update(); + Assert.That(upToDate, Is.True); + tiles = tc.getNavMesh().getTilesAt(1, 4); + tile = tiles[0]; + Assert.That(tile.data.header.vertCount, Is.EqualTo(16)); + Assert.That(tile.data.header.polyCount, Is.EqualTo(6)); + } +} diff --git a/test/DotRecast.Detour.TileCache.Test/TestTileLayerBuilder.cs b/test/DotRecast.Detour.TileCache.Test/TestTileLayerBuilder.cs new file mode 100644 index 0000000..a343a28 --- /dev/null +++ b/test/DotRecast.Detour.TileCache.Test/TestTileLayerBuilder.cs @@ -0,0 +1,120 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.Collections.Generic; +using DotRecast.Core; +using DotRecast.Recast; +using DotRecast.Recast.Geom; + +using static DotRecast.Detour.DetourCommon; + +namespace DotRecast.Detour.TileCache.Test; + +public class TestTileLayerBuilder : AbstractTileLayersBuilder { + + private const float m_cellSize = 0.3f; + private const float m_cellHeight = 0.2f; + private const float m_agentHeight = 2.0f; + private const float m_agentRadius = 0.6f; + private const float m_agentMaxClimb = 0.9f; + private const float m_agentMaxSlope = 45.0f; + private const int m_regionMinSize = 8; + private const int m_regionMergeSize = 20; + private const float m_regionMinArea = m_regionMinSize * m_regionMinSize * m_cellSize * m_cellSize; + private const float m_regionMergeArea = m_regionMergeSize * m_regionMergeSize * m_cellSize * m_cellSize; + private const float m_edgeMaxLen = 12.0f; + private const float m_edgeMaxError = 1.3f; + private const int m_vertsPerPoly = 6; + private const float m_detailSampleDist = 6.0f; + private const float m_detailSampleMaxError = 1.0f; + private readonly RecastConfig rcConfig; + private const int m_tileSize = 48; + protected readonly InputGeomProvider geom; + private readonly int tw; + private readonly int th; + + public TestTileLayerBuilder(InputGeomProvider geom) { + this.geom = geom; + rcConfig = new RecastConfig(true, m_tileSize, m_tileSize, RecastConfig.calcBorder(m_agentRadius, m_cellSize), + PartitionType.WATERSHED, m_cellSize, m_cellHeight, m_agentMaxSlope, true, true, true, m_agentHeight, + m_agentRadius, m_agentMaxClimb, m_regionMinArea, m_regionMergeArea, m_edgeMaxLen, m_edgeMaxError, m_vertsPerPoly, + true, m_detailSampleDist, m_detailSampleMaxError, SampleAreaModifications.SAMPLE_AREAMOD_GROUND); + float[] bmin = geom.getMeshBoundsMin(); + float[] bmax = geom.getMeshBoundsMax(); + int[] twh = Recast.Recast.calcTileCount(bmin, bmax, m_cellSize, m_tileSize, m_tileSize); + tw = twh[0]; + th = twh[1]; + } + + public List build(ByteOrder order, bool cCompatibility, int threads) { + return build(order, cCompatibility, threads, tw, th); + } + + public int getTw() { + return tw; + } + + public int getTh() { + return th; + } + + protected override List build(int tx, int ty, ByteOrder order, bool cCompatibility) { + HeightfieldLayerSet lset = getHeightfieldSet(tx, ty); + List result = new(); + if (lset != null) { + TileCacheBuilder builder = new TileCacheBuilder(); + for (int i = 0; i < lset.layers.Length; ++i) { + HeightfieldLayerSet.HeightfieldLayer layer = lset.layers[i]; + + // Store header + TileCacheLayerHeader header = new TileCacheLayerHeader(); + header.magic = TileCacheLayerHeader.DT_TILECACHE_MAGIC; + header.version = TileCacheLayerHeader.DT_TILECACHE_VERSION; + + // Tile layer location in the navmesh. + header.tx = tx; + header.ty = ty; + header.tlayer = i; + vCopy(header.bmin, layer.bmin); + vCopy(header.bmax, layer.bmax); + + // Tile info. + header.width = layer.width; + header.height = layer.height; + header.minx = layer.minx; + header.maxx = layer.maxx; + header.miny = layer.miny; + header.maxy = layer.maxy; + header.hmin = layer.hmin; + header.hmax = layer.hmax; + result.Add(builder.compressTileCacheLayer(header, layer.heights, layer.areas, layer.cons, order, cCompatibility)); + } + } + return result; + } + + protected HeightfieldLayerSet getHeightfieldSet(int tx, int ty) { + RecastBuilder rcBuilder = new RecastBuilder(); + float[] bmin = geom.getMeshBoundsMin(); + float[] bmax = geom.getMeshBoundsMax(); + RecastBuilderConfig cfg = new RecastBuilderConfig(rcConfig, bmin, bmax, tx, ty); + HeightfieldLayerSet lset = rcBuilder.buildLayers(geom, cfg); + return lset; + } +} diff --git a/test/DotRecast.Detour.TileCache.Test/TileCacheFindPathTest.cs b/test/DotRecast.Detour.TileCache.Test/TileCacheFindPathTest.cs new file mode 100644 index 0000000..424de1e --- /dev/null +++ b/test/DotRecast.Detour.TileCache.Test/TileCacheFindPathTest.cs @@ -0,0 +1,61 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.Collections.Generic; +using System.IO; +using DotRecast.Core; +using DotRecast.Detour.TileCache.Io; +using NUnit.Framework; + +namespace DotRecast.Detour.TileCache.Test; + +public class TileCacheFindPathTest : AbstractTileCacheTest { + + private readonly float[] start = { 39.44734f, 9.998177f, -0.784811f }; + private readonly float[] end = { 19.292645f, 11.611748f, -57.750366f }; + private readonly NavMesh navmesh; + private readonly NavMeshQuery query; + + public TileCacheFindPathTest() { + using var msis = new MemoryStream(Loader.ToBytes("dungeon_all_tiles_tilecache.bin")); + using var @is = new BinaryReader(msis); + TileCache tcC = new TileCacheReader().read(@is, 6, new TestTileCacheMeshProcess()); + navmesh = tcC.getNavMesh(); + query = new NavMeshQuery(navmesh); + } + + [Test] + public void testFindPath() { + QueryFilter filter = new DefaultQueryFilter(); + float[] extents = new float[] { 2f, 4f, 2f }; + Result findPolyStart = query.findNearestPoly(start, extents, filter); + Result findPolyEnd = query.findNearestPoly(end, extents, filter); + long startRef = findPolyStart.result.getNearestRef(); + long endRef = findPolyEnd.result.getNearestRef(); + float[] startPos = findPolyStart.result.getNearestPos(); + float[] endPos = findPolyEnd.result.getNearestPos(); + Result> path = query.findPath(startRef, endRef, startPos, endPos, filter); + int maxStraightPath = 256; + int options = 0; + Result> pathStr = query.findStraightPath(startPos, endPos, path.result, maxStraightPath, + options); + Assert.That(pathStr.result.Count, Is.EqualTo(8)); + } + +} \ No newline at end of file diff --git a/test/DotRecast.Detour.TileCache.Test/TileCacheNavigationTest.cs b/test/DotRecast.Detour.TileCache.Test/TileCacheNavigationTest.cs new file mode 100644 index 0000000..9a814c6 --- /dev/null +++ b/test/DotRecast.Detour.TileCache.Test/TileCacheNavigationTest.cs @@ -0,0 +1,103 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System.Collections.Generic; +using DotRecast.Core; +using DotRecast.Recast; +using DotRecast.Recast.Geom; +using NUnit.Framework; + +namespace DotRecast.Detour.TileCache.Test; + +public class TileCacheNavigationTest : AbstractTileCacheTest { + + protected readonly long[] startRefs = { 281475006070787L }; + protected readonly long[] endRefs = { 281474986147841L }; + protected readonly float[][] startPoss = { new[] { 39.447338f, 9.998177f, -0.784811f } }; + protected readonly float[][] endPoss = { new[] { 19.292645f, 11.611748f, -57.750366f } }; + private readonly Status[] statuses = { Status.SUCCSESS }; + private readonly long[][] results = { new[] { 281475006070787L, 281475006070785L, 281475005022208L, 281475005022209L, 281475003973633L, + 281475003973634L, 281475003973632L, 281474996633604L, 281474996633605L, 281474996633603L, 281474995585027L, + 281474995585029L, 281474995585026L, 281474995585028L, 281474995585024L, 281474991390721L, 281474991390722L, + 281474991390725L, 281474991390720L, 281474987196418L, 281474987196417L, 281474988244995L, 281474988245001L, + 281474988244997L, 281474988244998L, 281474988245002L, 281474988245000L, 281474988244999L, 281474988244994L, + 281474985099264L, 281474985099266L, 281474986147841L } }; + + protected NavMesh navmesh; + protected NavMeshQuery query; + + [SetUp] + public void setUp() { + + bool cCompatibility = true; + InputGeomProvider geom = ObjImporter.load(Loader.ToBytes("dungeon.obj")); + TestTileLayerBuilder layerBuilder = new TestTileLayerBuilder(geom); + List layers = layerBuilder.build(ByteOrder.LITTLE_ENDIAN, cCompatibility, 1); + TileCache tc = getTileCache(geom, ByteOrder.LITTLE_ENDIAN, cCompatibility); + foreach (byte[] data in layers) { + tc.addTile(data, 0); + } + for (int y = 0; y < layerBuilder.getTh(); ++y) { + for (int x = 0; x < layerBuilder.getTw(); ++x) { + foreach (long refs in tc.getTilesAt(x, y)) { + tc.buildNavMeshTile(refs); + } + } + } + navmesh = tc.getNavMesh(); + query = new NavMeshQuery(navmesh); + + } + + [Test] + public void testFindPathWithDefaultHeuristic() { + QueryFilter filter = new DefaultQueryFilter(); + for (int i = 0; i < startRefs.Length; i++) { + long startRef = startRefs[i]; + long endRef = endRefs[i]; + float[] startPos = startPoss[i]; + float[] endPos = endPoss[i]; + Result> path = query.findPath(startRef, endRef, startPos, endPos, filter); + Assert.That(path.status, Is.EqualTo(statuses[i])); + Assert.That(path.result.Count, Is.EqualTo(results[i].Length)); + for (int j = 0; j < results[i].Length; j++) { + Assert.That(path.result[j], Is.EqualTo(results[i][j])); // TODO : 확인 필요 + } + } + } + + [Test] + public void testFindPathWithNoHeuristic() { + QueryFilter filter = new DefaultQueryFilter(); + for (int i = 0; i < startRefs.Length; i++) { + long startRef = startRefs[i]; + long endRef = endRefs[i]; + float[] startPos = startPoss[i]; + float[] endPos = endPoss[i]; + Result> path = query.findPath(startRef, endRef, startPos, endPos, filter, new DefaultQueryHeuristic(0.0f), + 0, 0); + Assert.That(path.status, Is.EqualTo(statuses[i])); + Assert.That(path.result.Count, Is.EqualTo(results[i].Length)); + for (int j = 0; j < results[i].Length; j++) { + Assert.That(path.result[j], Is.EqualTo(results[i][j])); // TODO : 확인 필요 + } + } + } + +} diff --git a/test/DotRecast.Detour.TileCache.Test/TileCacheTest.cs b/test/DotRecast.Detour.TileCache.Test/TileCacheTest.cs new file mode 100644 index 0000000..cd93439 --- /dev/null +++ b/test/DotRecast.Detour.TileCache.Test/TileCacheTest.cs @@ -0,0 +1,272 @@ +/* +Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using DotRecast.Core; +using DotRecast.Recast; +using DotRecast.Recast.Geom; +using NUnit.Framework; + +namespace DotRecast.Detour.TileCache.Test; + +public class TileCacheTest : AbstractTileCacheTest { + + [Test] + public void testFastLz() { + testDungeon(ByteOrder.LITTLE_ENDIAN, false); + testDungeon(ByteOrder.LITTLE_ENDIAN, true); + testDungeon(ByteOrder.BIG_ENDIAN, false); + testDungeon(ByteOrder.BIG_ENDIAN, true); + test(ByteOrder.LITTLE_ENDIAN, false); + test(ByteOrder.LITTLE_ENDIAN, true); + test(ByteOrder.BIG_ENDIAN, false); + test(ByteOrder.BIG_ENDIAN, true); + } + + [Test] + public void testLZ4() { + testDungeon(ByteOrder.LITTLE_ENDIAN, false); + testDungeon(ByteOrder.LITTLE_ENDIAN, true); + testDungeon(ByteOrder.BIG_ENDIAN, false); + testDungeon(ByteOrder.BIG_ENDIAN, true); + test(ByteOrder.LITTLE_ENDIAN, false); + test(ByteOrder.LITTLE_ENDIAN, true); + test(ByteOrder.BIG_ENDIAN, false); + test(ByteOrder.BIG_ENDIAN, true); + } + + private void testDungeon(ByteOrder order, bool cCompatibility) { + InputGeomProvider geom = ObjImporter.load(Loader.ToBytes("dungeon.obj")); + TileCache tc = getTileCache(geom, order, cCompatibility); + TestTileLayerBuilder layerBuilder = new TestTileLayerBuilder(geom); + List layers = layerBuilder.build(order, cCompatibility, 1); + int cacheLayerCount = 0; + int cacheCompressedSize = 0; + int cacheRawSize = 0; + foreach (byte[] layer in layers) { + long refs = tc.addTile(layer, 0); + tc.buildNavMeshTile(refs); + cacheLayerCount++; + cacheCompressedSize += layer.Length; + cacheRawSize += 4 * 48 * 48 + 56; // FIXME + } + Console.WriteLine("Compressor: " + tc.getCompressor().GetType().Name + " C Compatibility: " + cCompatibility + + " Layers: " + cacheLayerCount + " Raw Size: " + cacheRawSize + " Compressed: " + cacheCompressedSize); + Assert.That(tc.getNavMesh().getMaxTiles(), Is.EqualTo(256)); + Assert.That(tc.getNavMesh().getParams().maxPolys, Is.EqualTo(16384)); + Assert.That(tc.getNavMesh().getParams().tileWidth, Is.EqualTo(14.4f).Within(0.001f)); + Assert.That(tc.getNavMesh().getParams().tileHeight, Is.EqualTo(14.4f).Within(0.001f)); + Assert.That(tc.getNavMesh().getMaxVertsPerPoly(), Is.EqualTo(6)); + Assert.That(tc.getParams().cs, Is.EqualTo(0.3f)); + Assert.That(tc.getParams().ch, Is.EqualTo(0.2f)); + Assert.That(tc.getParams().walkableClimb, Is.EqualTo(0.9f)); + Assert.That(tc.getParams().walkableHeight, Is.EqualTo(2f)); + Assert.That(tc.getParams().walkableRadius, Is.EqualTo(0.6f)); + Assert.That(tc.getParams().width, Is.EqualTo(48)); + Assert.That(tc.getParams().maxTiles, Is.EqualTo(6 * 7 * 4)); + Assert.That(tc.getParams().maxObstacles, Is.EqualTo(128)); + Assert.That(tc.getTileCount(), Is.EqualTo(168)); + + // Tile0: Tris: 8, Verts: 18 Detail Meshed: 8 Detail Verts: 0 Detail Tris: 14 + MeshTile tile = tc.getNavMesh().getTile(0); + MeshData data = tile.data; + MeshHeader header = data.header; + Assert.That(header.vertCount, Is.EqualTo(18)); + Assert.That(header.polyCount, Is.EqualTo(8)); + Assert.That(header.detailMeshCount, Is.EqualTo(8)); + Assert.That(header.detailVertCount, Is.EqualTo(0)); + Assert.That(header.detailTriCount, Is.EqualTo(14)); + Assert.That(data.polys.Length, Is.EqualTo(8)); + Assert.That(data.verts.Length, Is.EqualTo(3 * 18)); + Assert.That(data.detailMeshes.Length, Is.EqualTo(8)); + Assert.That(data.detailVerts.Length, Is.EqualTo(0)); + Assert.That(data.detailTris.Length, Is.EqualTo(4 * 14)); + Assert.That(data.verts[1], Is.EqualTo(14.997294f).Within(0.0001f)); + Assert.That(data.verts[6], Is.EqualTo(15.484785f).Within(0.0001f)); + Assert.That(data.verts[9], Is.EqualTo(15.484785f).Within(0.0001f)); + // Tile8: Tris: 3, Verts: 8 Detail Meshed: 3 Detail Verts: 0 Detail Tris: 6 + tile = tc.getNavMesh().getTile(8); + data = tile.data; + header = data.header; + Assert.That(header.vertCount, Is.EqualTo(8)); + Assert.That(header.polyCount, Is.EqualTo(3)); + Assert.That(header.detailMeshCount, Is.EqualTo(3)); + Assert.That(header.detailVertCount, Is.EqualTo(0)); + Assert.That(header.detailTriCount, Is.EqualTo(6)); + Assert.That(data.polys.Length, Is.EqualTo(3)); + Assert.That(data.verts.Length, Is.EqualTo(3 * 8)); + Assert.That(data.detailMeshes.Length, Is.EqualTo(3)); + Assert.That(data.detailVerts.Length, Is.EqualTo(0)); + Assert.That(data.detailTris.Length, Is.EqualTo(4 * 6)); + // Tile16: Tris: 10, Verts: 20 Detail Meshed: 10 Detail Verts: 0 Detail Tris: 18 + tile = tc.getNavMesh().getTile(16); + data = tile.data; + header = data.header; + Assert.That(header.vertCount, Is.EqualTo(20)); + Assert.That(header.polyCount, Is.EqualTo(10)); + Assert.That(header.detailMeshCount, Is.EqualTo(10)); + Assert.That(header.detailVertCount, Is.EqualTo(0)); + Assert.That(header.detailTriCount, Is.EqualTo(18)); + Assert.That(data.polys.Length, Is.EqualTo(10)); + Assert.That(data.verts.Length, Is.EqualTo(3 * 20)); + Assert.That(data.detailMeshes.Length, Is.EqualTo(10)); + Assert.That(data.detailVerts.Length, Is.EqualTo(0)); + Assert.That(data.detailTris.Length, Is.EqualTo(4 * 18)); + // Tile29: Tris: 1, Verts: 5 Detail Meshed: 1 Detail Verts: 0 Detail Tris: 3 + tile = tc.getNavMesh().getTile(29); + data = tile.data; + header = data.header; + Assert.That(header.vertCount, Is.EqualTo(5)); + Assert.That(header.polyCount, Is.EqualTo(1)); + Assert.That(header.detailMeshCount, Is.EqualTo(1)); + Assert.That(header.detailVertCount, Is.EqualTo(0)); + Assert.That(header.detailTriCount, Is.EqualTo(3)); + Assert.That(data.polys.Length, Is.EqualTo(1)); + Assert.That(data.verts.Length, Is.EqualTo(3 * 5)); + Assert.That(data.detailMeshes.Length, Is.EqualTo(1)); + Assert.That(data.detailVerts.Length, Is.EqualTo(0)); + Assert.That(data.detailTris.Length, Is.EqualTo(4 * 3)); + } + + private void test(ByteOrder order, bool cCompatibility) { + InputGeomProvider geom = ObjImporter.load(Loader.ToBytes("nav_test.obj")); + TileCache tc = getTileCache(geom, order, cCompatibility); + TestTileLayerBuilder layerBuilder = new TestTileLayerBuilder(geom); + List layers = layerBuilder.build(order, cCompatibility, 1); + int cacheLayerCount = 0; + int cacheCompressedSize = 0; + int cacheRawSize = 0; + foreach (byte[] layer in layers) { + long refs = tc.addTile(layer, 0); + tc.buildNavMeshTile(refs); + cacheLayerCount++; + cacheCompressedSize += layer.Length; + cacheRawSize += 4 * 48 * 48 + 56; + } + Console.WriteLine("Compressor: " + tc.getCompressor().GetType().Name + " C Compatibility: " + cCompatibility + + " Layers: " + cacheLayerCount + " Raw Size: " + cacheRawSize + " Compressed: " + cacheCompressedSize); + } + + [Test] + public void testPerformance() { + int threads = 4; + ByteOrder order = ByteOrder.LITTLE_ENDIAN; + bool cCompatibility = false; + InputGeomProvider geom = ObjImporter.load(Loader.ToBytes("dungeon.obj")); + TestTileLayerBuilder layerBuilder = new TestTileLayerBuilder(geom); + for (int i = 0; i < 4; i++) { + layerBuilder.build(order, cCompatibility, 1); + layerBuilder.build(order, cCompatibility, threads); + } + + long t1 = Stopwatch.GetTimestamp(); + List layers = null; + for (int i = 0; i < 8; i++) { + layers = layerBuilder.build(order, cCompatibility, 1); + } + long t2 = Stopwatch.GetTimestamp(); + for (int i = 0; i < 8; i++) { + layers = layerBuilder.build(order, cCompatibility, threads); + } + long t3 = Stopwatch.GetTimestamp(); + Console.WriteLine(" Time ST : " + (t2 - t1) / 1000000); + Console.WriteLine(" Time MT : " + (t3 - t2) / 1000000); + TileCache tc = getTileCache(geom, order, cCompatibility); + foreach (byte[] layer in layers) { + long refs = tc.addTile(layer, 0); + tc.buildNavMeshTile(refs); + } + Assert.That(tc.getNavMesh().getMaxTiles(), Is.EqualTo(256)); + Assert.That(tc.getNavMesh().getParams().maxPolys, Is.EqualTo(16384)); + Assert.That(tc.getNavMesh().getParams().tileWidth, Is.EqualTo(14.4f).Within(0.001f)); + Assert.That(tc.getNavMesh().getParams().tileHeight, Is.EqualTo(14.4f).Within(0.001f)); + Assert.That(tc.getNavMesh().getMaxVertsPerPoly(), Is.EqualTo(6)); + Assert.That(tc.getParams().cs, Is.EqualTo(0.3f)); + Assert.That(tc.getParams().ch, Is.EqualTo(0.2f)); + Assert.That(tc.getParams().walkableClimb, Is.EqualTo(0.9f)); + Assert.That(tc.getParams().walkableHeight, Is.EqualTo(2f)); + Assert.That(tc.getParams().walkableRadius, Is.EqualTo(0.6f)); + Assert.That(tc.getParams().width, Is.EqualTo(48)); + Assert.That(tc.getParams().maxTiles, Is.EqualTo(6 * 7 * 4)); + Assert.That(tc.getParams().maxObstacles, Is.EqualTo(128)); + Assert.That(tc.getTileCount(), Is.EqualTo(168)); + // Tile0: Tris: 8, Verts: 18 Detail Meshed: 8 Detail Verts: 0 Detail Tris: 14 + MeshTile tile = tc.getNavMesh().getTile(0); + MeshData data = tile.data; + MeshHeader header = data.header; + Assert.That(header.vertCount, Is.EqualTo(18)); + Assert.That(header.polyCount, Is.EqualTo(8)); + Assert.That(header.detailMeshCount, Is.EqualTo(8)); + Assert.That(header.detailVertCount, Is.EqualTo(0)); + Assert.That(header.detailTriCount, Is.EqualTo(14)); + Assert.That(data.polys.Length, Is.EqualTo(8)); + Assert.That(data.verts.Length, Is.EqualTo(3 * 18)); + Assert.That(data.detailMeshes.Length, Is.EqualTo(8)); + Assert.That(data.detailVerts.Length, Is.EqualTo(0)); + Assert.That(data.detailTris.Length, Is.EqualTo(4 * 14)); + Assert.That(data.verts[1], Is.EqualTo(14.997294f).Within(0.0001f)); + Assert.That(data.verts[6], Is.EqualTo(15.484785f).Within(0.0001f)); + Assert.That(data.verts[9], Is.EqualTo(15.484785f).Within(0.0001f)); + // Tile8: Tris: 3, Verts: 8 Detail Meshed: 3 Detail Verts: 0 Detail Tris: 6 + tile = tc.getNavMesh().getTile(8); + data = tile.data; + header = data.header; + Assert.That(header.vertCount, Is.EqualTo(8)); + Assert.That(header.polyCount, Is.EqualTo(3)); + Assert.That(header.detailMeshCount, Is.EqualTo(3)); + Assert.That(header.detailVertCount, Is.EqualTo(0)); + Assert.That(header.detailTriCount, Is.EqualTo(6)); + Assert.That(data.polys.Length, Is.EqualTo(3)); + Assert.That(data.verts.Length, Is.EqualTo(3 * 8)); + Assert.That(data.detailMeshes.Length, Is.EqualTo(3)); + Assert.That(data.detailVerts.Length, Is.EqualTo(0)); + Assert.That(data.detailTris.Length, Is.EqualTo(4 * 6)); + // Tile16: Tris: 10, Verts: 20 Detail Meshed: 10 Detail Verts: 0 Detail Tris: 18 + tile = tc.getNavMesh().getTile(16); + data = tile.data; + header = data.header; + Assert.That(header.vertCount, Is.EqualTo(20)); + Assert.That(header.polyCount, Is.EqualTo(10)); + Assert.That(header.detailMeshCount, Is.EqualTo(10)); + Assert.That(header.detailVertCount, Is.EqualTo(0)); + Assert.That(header.detailTriCount, Is.EqualTo(18)); + Assert.That(data.polys.Length, Is.EqualTo(10)); + Assert.That(data.verts.Length, Is.EqualTo(3 * 20)); + Assert.That(data.detailMeshes.Length, Is.EqualTo(10)); + Assert.That(data.detailVerts.Length, Is.EqualTo(0)); + Assert.That(data.detailTris.Length, Is.EqualTo(4 * 18)); + // Tile29: Tris: 1, Verts: 5 Detail Meshed: 1 Detail Verts: 0 Detail Tris: 3 + tile = tc.getNavMesh().getTile(29); + data = tile.data; + header = data.header; + Assert.That(header.vertCount, Is.EqualTo(5)); + Assert.That(header.polyCount, Is.EqualTo(1)); + Assert.That(header.detailMeshCount, Is.EqualTo(1)); + Assert.That(header.detailVertCount, Is.EqualTo(0)); + Assert.That(header.detailTriCount, Is.EqualTo(3)); + Assert.That(data.polys.Length, Is.EqualTo(1)); + Assert.That(data.verts.Length, Is.EqualTo(3 * 5)); + Assert.That(data.detailMeshes.Length, Is.EqualTo(1)); + Assert.That(data.detailVerts.Length, Is.EqualTo(0)); + Assert.That(data.detailTris.Length, Is.EqualTo(4 * 3)); + } + +} diff --git a/test/DotRecast.Recast.Test/DotRecast.Recast.Test.csproj b/test/DotRecast.Recast.Test/DotRecast.Recast.Test.csproj new file mode 100644 index 0000000..74dd6d7 --- /dev/null +++ b/test/DotRecast.Recast.Test/DotRecast.Recast.Test.csproj @@ -0,0 +1,25 @@ + + + + net7.0 + + + false + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/test/DotRecast.Recast.Test/RecastLayersTest.cs b/test/DotRecast.Recast.Test/RecastLayersTest.cs new file mode 100644 index 0000000..1f4cc4f --- /dev/null +++ b/test/DotRecast.Recast.Test/RecastLayersTest.cs @@ -0,0 +1,157 @@ +/* +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.IO; +using DotRecast.Core; +using DotRecast.Recast.Geom; +using NUnit.Framework; + +namespace DotRecast.Recast.Test; + +using static RecastConstants; + +public class RecastLayersTest +{ + private const float m_cellSize = 0.3f; + private const float m_cellHeight = 0.2f; + private const float m_agentHeight = 2.0f; + private const float m_agentRadius = 0.6f; + private const float m_agentMaxClimb = 0.9f; + private const float m_agentMaxSlope = 45.0f; + private const int m_regionMinSize = 8; + private const int m_regionMergeSize = 20; + private const float m_regionMinArea = m_regionMinSize * m_regionMinSize * m_cellSize * m_cellSize; + private const float m_regionMergeArea = m_regionMergeSize * m_regionMergeSize * m_cellSize * m_cellSize; + private const float m_edgeMaxLen = 12.0f; + private const float m_edgeMaxError = 1.3f; + private const int m_vertsPerPoly = 6; + private const float m_detailSampleDist = 6.0f; + private const float m_detailSampleMaxError = 1.0f; + private const PartitionType m_partitionType = PartitionType.WATERSHED; + private const int m_tileSize = 48; + + [Test] + public void testDungeon2() + { + } + + + [Test] + public void testDungeon() + { + HeightfieldLayerSet lset = build("dungeon.obj", 3, 2); + Assert.That(lset.layers.Length, Is.EqualTo(1)); + Assert.That(lset.layers[0].width, Is.EqualTo(48)); + Assert.That(lset.layers[0].hmin, Is.EqualTo(51)); + Assert.That(lset.layers[0].hmax, Is.EqualTo(67)); + Assert.That(lset.layers[0].heights[7], Is.EqualTo(17)); + Assert.That(lset.layers[0].heights[107], Is.EqualTo(15)); + Assert.That(lset.layers[0].heights[257], Is.EqualTo(13)); + Assert.That(lset.layers[0].heights[1814], Is.EqualTo(255)); + Assert.That(lset.layers[0].cons[12], Is.EqualTo(135)); + Assert.That(lset.layers[0].cons[109], Is.EqualTo(15)); + Assert.That(lset.layers[0].cons[530], Is.EqualTo(15)); + Assert.That(lset.layers[0].cons[1600], Is.EqualTo(0)); + } + + [Test] + public void test() + { + HeightfieldLayerSet lset = build("nav_test.obj", 3, 2); + Assert.That(lset.layers.Length, Is.EqualTo(3)); + Assert.That(lset.layers[0].width, Is.EqualTo(48)); + Assert.That(lset.layers[0].hmin, Is.EqualTo(13)); + Assert.That(lset.layers[0].hmax, Is.EqualTo(30)); + Assert.That(lset.layers[0].heights[7], Is.EqualTo(0)); + Assert.That(lset.layers[0].heights[107], Is.EqualTo(255)); + Assert.That(lset.layers[0].heights[257], Is.EqualTo(0)); + Assert.That(lset.layers[0].heights[1814], Is.EqualTo(255)); + Assert.That(lset.layers[0].cons[12], Is.EqualTo(133)); + Assert.That(lset.layers[0].cons[109], Is.EqualTo(0)); + Assert.That(lset.layers[0].cons[530], Is.EqualTo(0)); + Assert.That(lset.layers[0].cons[1600], Is.EqualTo(15)); + + Assert.That(lset.layers[1].width, Is.EqualTo(48)); + Assert.That(lset.layers[1].hmin, Is.EqualTo(13)); + Assert.That(lset.layers[1].hmax, Is.EqualTo(13)); + Assert.That(lset.layers[1].heights[7], Is.EqualTo(255)); + Assert.That(lset.layers[1].heights[107], Is.EqualTo(255)); + Assert.That(lset.layers[1].heights[257], Is.EqualTo(255)); + Assert.That(lset.layers[1].heights[1814], Is.EqualTo(255)); + Assert.That(lset.layers[1].cons[12], Is.EqualTo(0)); + Assert.That(lset.layers[1].cons[109], Is.EqualTo(0)); + Assert.That(lset.layers[1].cons[530], Is.EqualTo(0)); + Assert.That(lset.layers[1].cons[1600], Is.EqualTo(0)); + + Assert.That(lset.layers[2].width, Is.EqualTo(48)); + Assert.That(lset.layers[2].hmin, Is.EqualTo(76)); + Assert.That(lset.layers[2].hmax, Is.EqualTo(76)); + Assert.That(lset.layers[2].heights[7], Is.EqualTo(255)); + Assert.That(lset.layers[2].heights[107], Is.EqualTo(255)); + Assert.That(lset.layers[2].heights[257], Is.EqualTo(255)); + Assert.That(lset.layers[2].heights[1814], Is.EqualTo(255)); + Assert.That(lset.layers[2].cons[12], Is.EqualTo(0)); + Assert.That(lset.layers[2].cons[109], Is.EqualTo(0)); + Assert.That(lset.layers[2].cons[530], Is.EqualTo(0)); + Assert.That(lset.layers[2].cons[1600], Is.EqualTo(0)); + } + + [Test] + public void test2() + { + HeightfieldLayerSet lset = build("nav_test.obj", 2, 4); + Assert.That(lset.layers.Length, Is.EqualTo(2)); + Assert.That(lset.layers[0].width, Is.EqualTo(48)); + Assert.That(lset.layers[0].hmin, Is.EqualTo(13)); + Assert.That(lset.layers[0].hmax, Is.EqualTo(13)); + Assert.That(lset.layers[0].heights[7], Is.EqualTo(0)); + Assert.That(lset.layers[0].heights[107], Is.EqualTo(0)); + Assert.That(lset.layers[0].heights[257], Is.EqualTo(0)); + Assert.That(lset.layers[0].heights[1814], Is.EqualTo(0)); + Assert.That(lset.layers[0].cons[12], Is.EqualTo(135)); + Assert.That(lset.layers[0].cons[109], Is.EqualTo(15)); + Assert.That(lset.layers[0].cons[530], Is.EqualTo(0)); + Assert.That(lset.layers[0].cons[1600], Is.EqualTo(15)); + + Assert.That(lset.layers[1].width, Is.EqualTo(48)); + Assert.That(lset.layers[1].hmin, Is.EqualTo(68)); + Assert.That(lset.layers[1].hmax, Is.EqualTo(101)); + Assert.That(lset.layers[1].heights[7], Is.EqualTo(33)); + Assert.That(lset.layers[1].heights[107], Is.EqualTo(255)); + Assert.That(lset.layers[1].heights[257], Is.EqualTo(255)); + Assert.That(lset.layers[1].heights[1814], Is.EqualTo(3)); + Assert.That(lset.layers[1].cons[12], Is.EqualTo(0)); + Assert.That(lset.layers[1].cons[109], Is.EqualTo(0)); + Assert.That(lset.layers[1].cons[530], Is.EqualTo(15)); + Assert.That(lset.layers[1].cons[1600], Is.EqualTo(0)); + } + + private HeightfieldLayerSet build(string filename, int x, int y) + { + InputGeomProvider geom = ObjImporter.load(Loader.ToBytes(filename)); + RecastBuilder builder = new RecastBuilder(); + RecastConfig cfg = new RecastConfig(true, m_tileSize, m_tileSize, RecastConfig.calcBorder(m_agentRadius, m_cellSize), + m_partitionType, m_cellSize, m_cellHeight, m_agentMaxSlope, true, true, true, m_agentHeight, m_agentRadius, + m_agentMaxClimb, m_regionMinArea, m_regionMergeArea, m_edgeMaxLen, m_edgeMaxError, m_vertsPerPoly, true, + m_detailSampleDist, m_detailSampleMaxError, SampleAreaModifications.SAMPLE_AREAMOD_GROUND); + RecastBuilderConfig bcfg = new RecastBuilderConfig(cfg, geom.getMeshBoundsMin(), geom.getMeshBoundsMax(), x, y); + HeightfieldLayerSet lset = builder.buildLayers(geom, bcfg); + return lset; + } +} \ No newline at end of file diff --git a/test/DotRecast.Recast.Test/RecastSoloMeshTest.cs b/test/DotRecast.Recast.Test/RecastSoloMeshTest.cs new file mode 100644 index 0000000..1c96106 --- /dev/null +++ b/test/DotRecast.Recast.Test/RecastSoloMeshTest.cs @@ -0,0 +1,337 @@ +/* +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.Diagnostics; +using System.IO; +using DotRecast.Core; +using DotRecast.Recast.Geom; +using NUnit.Framework; + +namespace DotRecast.Recast.Test; + +using static RecastConstants; + +public class RecastSoloMeshTest +{ + private const float m_cellSize = 0.3f; + private const float m_cellHeight = 0.2f; + private const float m_agentHeight = 2.0f; + private const float m_agentRadius = 0.6f; + private const float m_agentMaxClimb = 0.9f; + private const float m_agentMaxSlope = 45.0f; + private const int m_regionMinSize = 8; + private const int m_regionMergeSize = 20; + private const float m_edgeMaxLen = 12.0f; + private const float m_edgeMaxError = 1.3f; + private const int m_vertsPerPoly = 6; + private const float m_detailSampleDist = 6.0f; + private const float m_detailSampleMaxError = 1.0f; + private PartitionType m_partitionType = PartitionType.WATERSHED; + + [Test] + public void testPerformance() + { + for (int i = 0; i < 10; i++) + { + testBuild("dungeon.obj", PartitionType.WATERSHED, 52, 16, 15, 223, 118, 118, 513, 291); + testBuild("dungeon.obj", PartitionType.MONOTONE, 0, 17, 16, 210, 100, 100, 453, 264); + testBuild("dungeon.obj", PartitionType.LAYERS, 0, 5, 5, 203, 97, 97, 446, 266); + } + } + + [Test] + public void testDungeonWatershed() + { + testBuild("dungeon.obj", PartitionType.WATERSHED, 52, 16, 15, 223, 118, 118, 513, 291); + } + + [Test] + public void testDungeonMonotone() + { + testBuild("dungeon.obj", PartitionType.MONOTONE, 0, 17, 16, 210, 100, 100, 453, 264); + } + + [Test] + public void testDungeonLayers() + { + testBuild("dungeon.obj", PartitionType.LAYERS, 0, 5, 5, 203, 97, 97, 446, 266); + } + + [Test] + public void testWatershed() + { + testBuild("nav_test.obj", PartitionType.WATERSHED, 60, 48, 47, 349, 153, 153, 802, 558); + } + + [Test] + public void testMonotone() + { + testBuild("nav_test.obj", PartitionType.MONOTONE, 0, 50, 49, 340, 185, 185, 871, 557); + } + + [Test] + public void testLayers() + { + testBuild("nav_test.obj", PartitionType.LAYERS, 0, 19, 32, 312, 150, 150, 764, 521); + } + + public void testBuild(string filename, PartitionType partitionType, int expDistance, int expRegions, + int expContours, int expVerts, int expPolys, int expDetMeshes, int expDetVerts, int expDetTris) + { + m_partitionType = partitionType; + InputGeomProvider geomProvider = ObjImporter.load(Loader.ToBytes(filename)); + long time = Stopwatch.GetTimestamp(); + float[] bmin = geomProvider.getMeshBoundsMin(); + float[] bmax = geomProvider.getMeshBoundsMax(); + Telemetry m_ctx = new Telemetry(); + // + // Step 1. Initialize build config. + // + + // Init build configuration from GUI + RecastConfig cfg = new RecastConfig(partitionType, m_cellSize, m_cellHeight, m_agentHeight, m_agentRadius, + m_agentMaxClimb, m_agentMaxSlope, m_regionMinSize, m_regionMergeSize, m_edgeMaxLen, m_edgeMaxError, + m_vertsPerPoly, m_detailSampleDist, m_detailSampleMaxError, SampleAreaModifications.SAMPLE_AREAMOD_GROUND); + RecastBuilderConfig bcfg = new RecastBuilderConfig(cfg, bmin, bmax); + // + // Step 2. Rasterize input polygon soup. + // + + // Allocate voxel heightfield where we rasterize our input data to. + Heightfield m_solid = new Heightfield(bcfg.width, bcfg.height, bcfg.bmin, bcfg.bmax, cfg.cs, cfg.ch, cfg.borderSize); + + foreach (TriMesh geom in geomProvider.meshes()) + { + float[] verts = geom.getVerts(); + int[] tris = geom.getTris(); + int ntris = tris.Length / 3; + + // Allocate array that can hold triangle area types. + // If you have multiple meshes you need to process, allocate + // and array which can hold the max number of triangles you need to + // process. + + // Find triangles which are walkable based on their slope and rasterize + // them. + // If your input data is multiple meshes, you can transform them here, + // calculate + // the are type for each of the meshes and rasterize them. + int[] m_triareas = Recast.markWalkableTriangles(m_ctx, cfg.walkableSlopeAngle, verts, tris, ntris, + cfg.walkableAreaMod); + RecastRasterization.rasterizeTriangles(m_solid, verts, tris, m_triareas, ntris, cfg.walkableClimb, m_ctx); + // + // Step 3. Filter walkables surfaces. + // + } + + // Once all geometry is rasterized, we do initial pass of filtering to + // remove unwanted overhangs caused by the conservative rasterization + // as well as filter spans where the character cannot possibly stand. + RecastFilter.filterLowHangingWalkableObstacles(m_ctx, cfg.walkableClimb, m_solid); + RecastFilter.filterLedgeSpans(m_ctx, cfg.walkableHeight, cfg.walkableClimb, m_solid); + RecastFilter.filterWalkableLowHeightSpans(m_ctx, cfg.walkableHeight, m_solid); + + // + // Step 4. Partition walkable surface to simple regions. + // + + // Compact the heightfield so that it is faster to handle from now on. + // This will result more cache coherent data as well as the neighbours + // between walkable cells will be calculated. + CompactHeightfield m_chf = RecastCompact.buildCompactHeightfield(m_ctx, cfg.walkableHeight, cfg.walkableClimb, + m_solid); + + // Erode the walkable area by agent radius. + RecastArea.erodeWalkableArea(m_ctx, cfg.walkableRadius, m_chf); + + // (Optional) Mark areas. + /* + * ConvexVolume vols = m_geom->getConvexVolumes(); for (int i = 0; i < m_geom->getConvexVolumeCount(); ++i) + * rcMarkConvexPolyArea(m_ctx, vols[i].verts, vols[i].nverts, vols[i].hmin, vols[i].hmax, (unsigned + * char)vols[i].area, *m_chf); + */ + + // Partition the heightfield so that we can use simple algorithm later + // to triangulate the walkable areas. + // There are 3 martitioning methods, each with some pros and cons: + // 1) Watershed partitioning + // - the classic Recast partitioning + // - creates the nicest tessellation + // - usually slowest + // - partitions the heightfield into nice regions without holes or + // overlaps + // - the are some corner cases where this method creates produces holes + // and overlaps + // - holes may appear when a small obstacles is close to large open area + // (triangulation can handle this) + // - overlaps may occur if you have narrow spiral corridors (i.e + // stairs), this make triangulation to fail + // * generally the best choice if you precompute the nacmesh, use this + // if you have large open areas + // 2) Monotone partioning + // - fastest + // - partitions the heightfield into regions without holes and overlaps + // (guaranteed) + // - creates long thin polygons, which sometimes causes paths with + // detours + // * use this if you want fast navmesh generation + // 3) Layer partitoining + // - quite fast + // - partitions the heighfield into non-overlapping regions + // - relies on the triangulation code to cope with holes (thus slower + // than monotone partitioning) + // - produces better triangles than monotone partitioning + // - does not have the corner cases of watershed partitioning + // - can be slow and create a bit ugly tessellation (still better than + // monotone) + // if you have large open areas with small obstacles (not a problem if + // you use tiles) + // * good choice to use for tiled navmesh with medium and small sized + // tiles + long time3 = Stopwatch.GetTimestamp(); + + if (m_partitionType == PartitionType.WATERSHED) + { + // Prepare for region partitioning, by calculating distance field + // along the walkable surface. + RecastRegion.buildDistanceField(m_ctx, m_chf); + // Partition the walkable surface into simple regions without holes. + RecastRegion.buildRegions(m_ctx, m_chf, cfg.minRegionArea, cfg.mergeRegionArea); + } + else if (m_partitionType == PartitionType.MONOTONE) + { + // Partition the walkable surface into simple regions without holes. + // Monotone partitioning does not need distancefield. + RecastRegion.buildRegionsMonotone(m_ctx, m_chf, cfg.minRegionArea, cfg.mergeRegionArea); + } + else + { + // Partition the walkable surface into simple regions without holes. + RecastRegion.buildLayerRegions(m_ctx, m_chf, cfg.minRegionArea); + } + + Assert.That(m_chf.maxDistance, Is.EqualTo(expDistance), "maxDistance"); + Assert.That(m_chf.maxRegions, Is.EqualTo(expRegions), "Regions"); + // + // Step 5. Trace and simplify region contours. + // + + // Create contours. + ContourSet m_cset = RecastContour.buildContours(m_ctx, m_chf, cfg.maxSimplificationError, cfg.maxEdgeLen, + RecastConstants.RC_CONTOUR_TESS_WALL_EDGES); + + Assert.That(m_cset.conts.Count, Is.EqualTo(expContours), "Contours"); + // + // Step 6. Build polygons mesh from contours. + // + + // Build polygon navmesh from the contours. + PolyMesh m_pmesh = RecastMesh.buildPolyMesh(m_ctx, m_cset, cfg.maxVertsPerPoly); + Assert.That(m_pmesh.nverts, Is.EqualTo(expVerts), "Mesh Verts"); + Assert.That(m_pmesh.npolys, Is.EqualTo(expPolys), "Mesh Polys"); + + // + // Step 7. Create detail mesh which allows to access approximate height + // on each polygon. + // + + PolyMeshDetail m_dmesh = RecastMeshDetail.buildPolyMeshDetail(m_ctx, m_pmesh, m_chf, cfg.detailSampleDist, + cfg.detailSampleMaxError); + Assert.That(m_dmesh.nmeshes, Is.EqualTo(expDetMeshes), "Mesh Detail Meshes"); + Assert.That(m_dmesh.nverts, Is.EqualTo(expDetVerts), "Mesh Detail Verts"); + Assert.That(m_dmesh.ntris, Is.EqualTo(expDetTris), "Mesh Detail Tris"); + long time2 = Stopwatch.GetTimestamp(); + Console.WriteLine(filename + " : " + partitionType + " " + (time2 - time) / 1000000 + " ms"); + Console.WriteLine(" " + (time3 - time) / 1000000 + " ms"); + saveObj(filename.Substring(0, filename.LastIndexOf('.')) + "_" + partitionType + "_detail.obj", m_dmesh); + saveObj(filename.Substring(0, filename.LastIndexOf('.')) + "_" + partitionType + ".obj", m_pmesh); + m_ctx.print(); + } + + private void saveObj(string filename, PolyMesh mesh) + { + try + { + string path = Path.Combine("test-output", filename); + Directory.CreateDirectory(Path.GetDirectoryName(path)); + using StreamWriter fw = new StreamWriter(path); + for (int v = 0; v < mesh.nverts; v++) + { + fw.Write("v " + (mesh.bmin[0] + mesh.verts[v * 3] * mesh.cs) + " " + + (mesh.bmin[1] + mesh.verts[v * 3 + 1] * mesh.ch) + " " + + (mesh.bmin[2] + mesh.verts[v * 3 + 2] * mesh.cs) + "\n"); + } + + for (int i = 0; i < mesh.npolys; i++) + { + int p = i * mesh.nvp * 2; + fw.Write("f "); + for (int j = 0; j < mesh.nvp; ++j) + { + int v = mesh.polys[p + j]; + if (v == RC_MESH_NULL_IDX) + { + break; + } + + fw.Write((v + 1) + " "); + } + + fw.Write("\n"); + } + + fw.Close(); + } + catch (Exception e) + { + } + } + + private void saveObj(string filename, PolyMeshDetail dmesh) + { + try + { + string filePath = Path.Combine("test-output", filename); + Directory.CreateDirectory(Path.GetDirectoryName(filePath)); + using StreamWriter fw = new StreamWriter(filePath); + for (int v = 0; v < dmesh.nverts; v++) + { + fw.Write( + "v " + dmesh.verts[v * 3] + " " + dmesh.verts[v * 3 + 1] + " " + dmesh.verts[v * 3 + 2] + "\n"); + } + + for (int m = 0; m < dmesh.nmeshes; m++) + { + int vfirst = dmesh.meshes[m * 4]; + int tfirst = dmesh.meshes[m * 4 + 2]; + for (int f = 0; f < dmesh.meshes[m * 4 + 3]; f++) + { + fw.Write("f " + (vfirst + dmesh.tris[(tfirst + f) * 4] + 1) + " " + + (vfirst + dmesh.tris[(tfirst + f) * 4 + 1] + 1) + " " + + (vfirst + dmesh.tris[(tfirst + f) * 4 + 2] + 1) + "\n"); + } + } + + fw.Close(); + } + catch (Exception e) + { + } + } +} \ No newline at end of file diff --git a/test/DotRecast.Recast.Test/RecastTest.cs b/test/DotRecast.Recast.Test/RecastTest.cs new file mode 100644 index 0000000..20ab4f2 --- /dev/null +++ b/test/DotRecast.Recast.Test/RecastTest.cs @@ -0,0 +1,55 @@ +/* +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using NUnit.Framework; + +namespace DotRecast.Recast.Test; + +using static RecastConstants; + +public class RecastTest +{ + [Test] + public void testClearUnwalkableTriangles() + { + float walkableSlopeAngle = 45; + float[] verts = { 0, 0, 0, 1, 0, 0, 0, 0, -1 }; + int nv = 3; + int[] walkable_tri = { 0, 1, 2 }; + int[] unwalkable_tri = { 0, 2, 1 }; + int nt = 1; + + Telemetry ctx = new Telemetry(); + { + int[] areas = { 42 }; + Recast.clearUnwalkableTriangles(ctx, walkableSlopeAngle, verts, nv, unwalkable_tri, nt, areas); + Assert.That(areas[0], Is.EqualTo(RC_NULL_AREA), "Sets area ID of unwalkable triangle to RC_NULL_AREA"); + } + { + int[] areas = { 42 }; + Recast.clearUnwalkableTriangles(ctx, walkableSlopeAngle, verts, nv, walkable_tri, nt, areas); + Assert.That(areas[0], Is.EqualTo(42), "Does not modify walkable triangle aread ID's"); + } + { + int[] areas = { 42 }; + walkableSlopeAngle = 0; + Recast.clearUnwalkableTriangles(ctx, walkableSlopeAngle, verts, nv, walkable_tri, nt, areas); + Assert.That(areas[0], Is.EqualTo(RC_NULL_AREA), "Slopes equal to the max slope are considered unwalkable."); + } + } +} \ No newline at end of file diff --git a/test/DotRecast.Recast.Test/RecastTileMeshTest.cs b/test/DotRecast.Recast.Test/RecastTileMeshTest.cs new file mode 100644 index 0000000..ddb8012 --- /dev/null +++ b/test/DotRecast.Recast.Test/RecastTileMeshTest.cs @@ -0,0 +1,167 @@ +/* +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using DotRecast.Core; +using DotRecast.Recast.Geom; +using NUnit.Framework; + +namespace DotRecast.Recast.Test; + +using static RecastConstants; + +public class RecastTileMeshTest +{ + private const float m_cellSize = 0.3f; + private const float m_cellHeight = 0.2f; + private const float m_agentHeight = 2.0f; + private const float m_agentRadius = 0.6f; + private const float m_agentMaxClimb = 0.9f; + private const float m_agentMaxSlope = 45.0f; + private const int m_regionMinSize = 8; + private const int m_regionMergeSize = 20; + private const float m_regionMinArea = m_regionMinSize * m_regionMinSize * m_cellSize * m_cellSize; + private const float m_regionMergeArea = m_regionMergeSize * m_regionMergeSize * m_cellSize * m_cellSize; + private const float m_edgeMaxLen = 12.0f; + private const float m_edgeMaxError = 1.3f; + private const int m_vertsPerPoly = 6; + private const float m_detailSampleDist = 6.0f; + private const float m_detailSampleMaxError = 1.0f; + private const PartitionType m_partitionType = PartitionType.WATERSHED; + private const int m_tileSize = 32; + + [Test] + public void testDungeon() + { + testBuild("dungeon.obj"); + } + + public void testBuild(string filename) + { + InputGeomProvider geom = ObjImporter.load(Loader.ToBytes(filename)); + RecastBuilder builder = new RecastBuilder(); + RecastConfig cfg = new RecastConfig(true, m_tileSize, m_tileSize, RecastConfig.calcBorder(m_agentRadius, m_cellSize), + m_partitionType, m_cellSize, m_cellHeight, m_agentMaxSlope, true, true, true, m_agentHeight, m_agentRadius, + m_agentMaxClimb, m_regionMinArea, m_regionMergeArea, m_edgeMaxLen, m_edgeMaxError, m_vertsPerPoly, true, + m_detailSampleDist, m_detailSampleMaxError, SampleAreaModifications.SAMPLE_AREAMOD_GROUND); + RecastBuilderConfig bcfg = new RecastBuilderConfig(cfg, geom.getMeshBoundsMin(), geom.getMeshBoundsMax(), 7, 8); + RecastBuilderResult rcResult = builder.build(geom, bcfg); + Assert.That(rcResult.getMesh().npolys, Is.EqualTo(1)); + Assert.That(rcResult.getMesh().nverts, Is.EqualTo(5)); + bcfg = new RecastBuilderConfig(cfg, geom.getMeshBoundsMin(), geom.getMeshBoundsMax(), 6, 9); + rcResult = builder.build(geom, bcfg); + Assert.That(rcResult.getMesh().npolys, Is.EqualTo(2)); + Assert.That(rcResult.getMesh().nverts, Is.EqualTo(7)); + bcfg = new RecastBuilderConfig(cfg, geom.getMeshBoundsMin(), geom.getMeshBoundsMax(), 2, 9); + rcResult = builder.build(geom, bcfg); + Assert.That(rcResult.getMesh().npolys, Is.EqualTo(2)); + Assert.That(rcResult.getMesh().nverts, Is.EqualTo(9)); + bcfg = new RecastBuilderConfig(cfg, geom.getMeshBoundsMin(), geom.getMeshBoundsMax(), 4, 3); + rcResult = builder.build(geom, bcfg); + Assert.That(rcResult.getMesh().npolys, Is.EqualTo(3)); + Assert.That(rcResult.getMesh().nverts, Is.EqualTo(6)); + bcfg = new RecastBuilderConfig(cfg, geom.getMeshBoundsMin(), geom.getMeshBoundsMax(), 2, 8); + rcResult = builder.build(geom, bcfg); + Assert.That(rcResult.getMesh().npolys, Is.EqualTo(5)); + Assert.That(rcResult.getMesh().nverts, Is.EqualTo(17)); + bcfg = new RecastBuilderConfig(cfg, geom.getMeshBoundsMin(), geom.getMeshBoundsMax(), 0, 8); + rcResult = builder.build(geom, bcfg); + Assert.That(rcResult.getMesh().npolys, Is.EqualTo(6)); + Assert.That(rcResult.getMesh().nverts, Is.EqualTo(15)); + } + + [Test] + public void testPerformance() + { + InputGeomProvider geom = ObjImporter.load(Loader.ToBytes("dungeon.obj")); + RecastBuilder builder = new RecastBuilder(); + RecastConfig cfg = new RecastConfig(true, m_tileSize, m_tileSize, RecastConfig.calcBorder(m_agentRadius, m_cellSize), + m_partitionType, m_cellSize, m_cellHeight, m_agentMaxSlope, true, true, true, m_agentHeight, m_agentRadius, + m_agentMaxClimb, m_regionMinArea, m_regionMergeArea, m_edgeMaxLen, m_edgeMaxError, m_vertsPerPoly, true, + m_detailSampleDist, m_detailSampleMaxError, SampleAreaModifications.SAMPLE_AREAMOD_GROUND); + for (int i = 0; i < 4; i++) + { + build(geom, builder, cfg, 1, true); + build(geom, builder, cfg, 4, true); + } + + long t1 = Stopwatch.GetTimestamp(); + for (int i = 0; i < 4; i++) + { + build(geom, builder, cfg, 1, false); + } + + long t2 = Stopwatch.GetTimestamp(); + for (int i = 0; i < 4; i++) + { + build(geom, builder, cfg, 4, false); + } + + long t3 = Stopwatch.GetTimestamp(); + Console.WriteLine(" Time ST : " + (t2 - t1) / 1000000); + Console.WriteLine(" Time MT : " + (t3 - t2) / 1000000); + } + + private void build(InputGeomProvider geom, RecastBuilder builder, RecastConfig cfg, int threads, bool validate) + { + CancellationTokenSource cts = new CancellationTokenSource(); + List tiles = new(); + var task = builder.buildTilesAsync(geom, cfg, threads, tiles, Task.Factory, cts.Token); + if (validate) + { + RecastBuilderResult rcResult = getTile(tiles, 7, 8); + Assert.That(rcResult.getMesh().npolys, Is.EqualTo(1)); + Assert.That(rcResult.getMesh().nverts, Is.EqualTo(5)); + rcResult = getTile(tiles, 6, 9); + Assert.That(rcResult.getMesh().npolys, Is.EqualTo(2)); + Assert.That(rcResult.getMesh().nverts, Is.EqualTo(7)); + rcResult = getTile(tiles, 2, 9); + Assert.That(rcResult.getMesh().npolys, Is.EqualTo(2)); + Assert.That(rcResult.getMesh().nverts, Is.EqualTo(9)); + rcResult = getTile(tiles, 4, 3); + Assert.That(rcResult.getMesh().npolys, Is.EqualTo(3)); + Assert.That(rcResult.getMesh().nverts, Is.EqualTo(6)); + rcResult = getTile(tiles, 2, 8); + Assert.That(rcResult.getMesh().npolys, Is.EqualTo(5)); + Assert.That(rcResult.getMesh().nverts, Is.EqualTo(17)); + rcResult = getTile(tiles, 0, 8); + Assert.That(rcResult.getMesh().npolys, Is.EqualTo(6)); + Assert.That(rcResult.getMesh().nverts, Is.EqualTo(15)); + } + + try + { + cts.Cancel(); + //executor.awaitTermination(1000, TimeUnit.HOURS); + } + catch (Exception e) + { + } + } + + private RecastBuilderResult getTile(List tiles, int x, int z) + { + return tiles.FirstOrDefault(tile => tile.tileX == x && tile.tileZ == z); + } +} \ No newline at end of file diff --git a/test/DotRecast.Recast.Test/SampleAreaModifications.cs b/test/DotRecast.Recast.Test/SampleAreaModifications.cs new file mode 100644 index 0000000..5f20bb6 --- /dev/null +++ b/test/DotRecast.Recast.Test/SampleAreaModifications.cs @@ -0,0 +1,60 @@ +/* +recast4j Copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +namespace DotRecast.Recast.Test; + +public class SampleAreaModifications +{ + public const int SAMPLE_POLYAREA_TYPE_MASK = 0x07; + + /// Value for the kind of ceil "ground" + public const int SAMPLE_POLYAREA_TYPE_GROUND = 0x1; + + /// Value for the kind of ceil "water" + public const int SAMPLE_POLYAREA_TYPE_WATER = 0x2; + + /// Value for the kind of ceil "road" + public const int SAMPLE_POLYAREA_TYPE_ROAD = 0x3; + + /// Value for the kind of ceil "grass" + public const int SAMPLE_POLYAREA_TYPE_GRASS = 0x4; + + /// Flag for door area. Can be combined with area types and jump flag. + public const int SAMPLE_POLYAREA_FLAG_DOOR = 0x08; + + /// Flag for jump area. Can be combined with area types and door flag. + public const int SAMPLE_POLYAREA_FLAG_JUMP = 0x10; + + public static readonly AreaModification SAMPLE_AREAMOD_GROUND = new AreaModification(SAMPLE_POLYAREA_TYPE_GROUND, + SAMPLE_POLYAREA_TYPE_MASK); + + public static readonly AreaModification SAMPLE_AREAMOD_WATER = new AreaModification(SAMPLE_POLYAREA_TYPE_WATER, + SAMPLE_POLYAREA_TYPE_MASK); + + public static readonly AreaModification SAMPLE_AREAMOD_ROAD = new AreaModification(SAMPLE_POLYAREA_TYPE_ROAD, + SAMPLE_POLYAREA_TYPE_MASK); + + public static readonly AreaModification SAMPLE_AREAMOD_GRASS = new AreaModification(SAMPLE_POLYAREA_TYPE_GRASS, + SAMPLE_POLYAREA_TYPE_MASK); + + public static readonly AreaModification SAMPLE_AREAMOD_DOOR = new AreaModification(SAMPLE_POLYAREA_FLAG_DOOR, + SAMPLE_POLYAREA_FLAG_DOOR); + + public static readonly AreaModification SAMPLE_AREAMOD_JUMP = new AreaModification(SAMPLE_POLYAREA_FLAG_JUMP, + SAMPLE_POLYAREA_FLAG_JUMP); +} \ No newline at end of file