forked from mirror/DotRecast
775 lines
30 KiB
C#
775 lines
30 KiB
C#
/*
|
|
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;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Immutable;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Runtime.Serialization;
|
|
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 ImGuiNET;
|
|
using Silk.NET.OpenAL;
|
|
using Silk.NET.Windowing;
|
|
using static DotRecast.Recast.Demo.Draw.DebugDraw;
|
|
using static DotRecast.Recast.Demo.Draw.DebugDrawPrimitives;
|
|
using static DotRecast.Core.RecastMath;
|
|
|
|
namespace DotRecast.Recast.Demo.Tools;
|
|
|
|
public class DynamicUpdateToolMode
|
|
{
|
|
public static readonly DynamicUpdateToolMode BUILD = new(0, "Build");
|
|
public static readonly DynamicUpdateToolMode COLLIDERS = new(1, "Colliders");
|
|
public static readonly DynamicUpdateToolMode RAYCAST = new(2, "Raycast");
|
|
|
|
public static readonly ImmutableArray<DynamicUpdateToolMode> Values = ImmutableArray.Create(
|
|
BUILD, COLLIDERS, RAYCAST
|
|
);
|
|
|
|
public int Idx { get; }
|
|
public string Label { get; }
|
|
|
|
private DynamicUpdateToolMode(int idx, string label)
|
|
{
|
|
Idx = idx;
|
|
Label = label;
|
|
}
|
|
}
|
|
|
|
public enum ColliderShape
|
|
{
|
|
SPHERE,
|
|
CAPSULE,
|
|
BOX,
|
|
CYLINDER,
|
|
COMPOSITE,
|
|
CONVEX,
|
|
TRIMESH_BRIDGE,
|
|
TRIMESH_HOUSE
|
|
}
|
|
|
|
public class DynamicUpdateTool : Tool
|
|
{
|
|
private Sample sample;
|
|
private int toolModeIdx = DynamicUpdateToolMode.BUILD.Idx;
|
|
private DynamicUpdateToolMode mode = DynamicUpdateToolMode.BUILD;
|
|
private float cellSize = 0.3f;
|
|
|
|
private int partitioningIdx = PartitionType.WATERSHED.Idx;
|
|
private PartitionType partitioning = PartitionType.WATERSHED;
|
|
private bool filterLowHangingObstacles = true;
|
|
private bool filterLedgeSpans = true;
|
|
private bool filterWalkableLowHeightSpans = true;
|
|
private float walkableHeight = 2f;
|
|
private float walkableRadius = 0.6f;
|
|
private float walkableClimb = 0.9f;
|
|
private float walkableSlopeAngle = 45f;
|
|
private float minRegionArea = 6f;
|
|
private float regionMergeSize = 36f;
|
|
private float maxEdgeLen = 12f;
|
|
private float maxSimplificationError = 1.3f;
|
|
private int vertsPerPoly = 6;
|
|
private bool buildDetailMesh = true;
|
|
private bool compression = true;
|
|
private float detailSampleDist = 6f;
|
|
private float detailSampleMaxError = 1f;
|
|
private bool showColliders = false;
|
|
private long buildTime;
|
|
private long raycastTime;
|
|
|
|
private int colliderShapeIdx = (int)ColliderShape.SPHERE;
|
|
private ColliderShape colliderShape = ColliderShape.SPHERE;
|
|
|
|
private DynamicNavMesh dynaMesh;
|
|
private readonly TaskFactory executor;
|
|
private readonly Dictionary<long, Collider> colliders = new();
|
|
private readonly Dictionary<long, ColliderGizmo> 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 == DynamicUpdateToolMode.COLLIDERS)
|
|
{
|
|
if (!shift)
|
|
{
|
|
Tuple<Collider, ColliderGizmo> 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 == DynamicUpdateToolMode.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) / TimeSpan.TicksPerMillisecond;
|
|
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<Collider, ColliderGizmo> sphereCollider(float[] p)
|
|
{
|
|
float radius = 1 + (float)random.NextDouble() * 10;
|
|
return Tuple.Create<Collider, ColliderGizmo>(
|
|
new SphereCollider(p, radius, SampleAreaModifications.SAMPLE_POLYAREA_TYPE_WATER, dynaMesh.config.walkableClimb),
|
|
GizmoFactory.sphere(p, radius));
|
|
}
|
|
|
|
private Tuple<Collider, ColliderGizmo> 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<Collider, ColliderGizmo>(new CapsuleCollider(start, end, radius, SampleAreaModifications.SAMPLE_POLYAREA_TYPE_WATER,
|
|
dynaMesh.config.walkableClimb), GizmoFactory.capsule(start, end, radius));
|
|
}
|
|
|
|
private Tuple<Collider, ColliderGizmo> 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<Collider, ColliderGizmo>(
|
|
new BoxCollider(p, halfEdges, SampleAreaModifications.SAMPLE_POLYAREA_TYPE_WATER, dynaMesh.config.walkableClimb),
|
|
GizmoFactory.box(p, halfEdges));
|
|
}
|
|
|
|
private Tuple<Collider, ColliderGizmo> 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<Collider, ColliderGizmo>(new CylinderCollider(start, end, radius, SampleAreaModifications.SAMPLE_POLYAREA_TYPE_WATER,
|
|
dynaMesh.config.walkableClimb), GizmoFactory.cylinder(start, end, radius));
|
|
}
|
|
|
|
private Tuple<Collider, ColliderGizmo> 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 = RecastMath.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, ColliderGizmo>(collider, gizmo);
|
|
}
|
|
|
|
private Tuple<Collider, ColliderGizmo> trimeshBridge(float[] p)
|
|
{
|
|
return trimeshCollider(p, bridgeGeom);
|
|
}
|
|
|
|
private Tuple<Collider, ColliderGizmo> trimeshHouse(float[] p)
|
|
{
|
|
return trimeshCollider(p, houseGeom);
|
|
}
|
|
|
|
private Tuple<Collider, ColliderGizmo> 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, ColliderGizmo>(collider, GizmoFactory.trimesh(verts, convexGeom.faces));
|
|
}
|
|
|
|
private Tuple<Collider, ColliderGizmo> 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, ColliderGizmo>(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 == DynamicUpdateToolMode.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 == DynamicUpdateToolMode.COLLIDERS)
|
|
{
|
|
if (showColliders)
|
|
{
|
|
colliderGizmos.Values.forEach(g => g.render(renderer.getDebugDraw()));
|
|
}
|
|
}
|
|
|
|
if (mode == DynamicUpdateToolMode.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) / TimeSpan.TicksPerMillisecond;
|
|
sample.update(null, dynaMesh.recastResults(), dynaMesh.navMesh());
|
|
sample.setChanged(false);
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine(e);
|
|
}
|
|
}
|
|
|
|
public override void layout()
|
|
{
|
|
ImGui.Text($"Dynamic Update Tool Modes");
|
|
ImGui.Separator();
|
|
|
|
var prevMode = mode;
|
|
ImGui.RadioButton(DynamicUpdateToolMode.BUILD.Label, ref toolModeIdx, DynamicUpdateToolMode.BUILD.Idx);
|
|
ImGui.RadioButton(DynamicUpdateToolMode.COLLIDERS.Label, ref toolModeIdx, DynamicUpdateToolMode.COLLIDERS.Idx);
|
|
ImGui.RadioButton(DynamicUpdateToolMode.RAYCAST.Label, ref toolModeIdx, DynamicUpdateToolMode.RAYCAST.Idx);
|
|
ImGui.NewLine();
|
|
|
|
if (prevMode.Idx != toolModeIdx)
|
|
{
|
|
mode = DynamicUpdateToolMode.Values[toolModeIdx];
|
|
}
|
|
|
|
ImGui.Text($"Selected mode - {mode.Label}");
|
|
ImGui.Separator();
|
|
|
|
if (mode == DynamicUpdateToolMode.BUILD)
|
|
{
|
|
var loadVoxelPopupStrId = "Load Voxels Popup";
|
|
bool isLoadVoxelPopup = true;
|
|
if (ImGui.Button("Load Voxels..."))
|
|
{
|
|
ImGui.OpenPopup(loadVoxelPopupStrId);
|
|
}
|
|
|
|
if (ImGui.BeginPopupModal(loadVoxelPopupStrId, ref isLoadVoxelPopup, ImGuiWindowFlags.NoTitleBar))
|
|
{
|
|
var picker = ImFilePicker.GetFilePicker(loadVoxelPopupStrId, Path.Combine(Environment.CurrentDirectory), ".voxels");
|
|
if (picker.Draw())
|
|
{
|
|
load(picker.SelectedFile);
|
|
ImFilePicker.RemoveFilePicker(loadVoxelPopupStrId);
|
|
}
|
|
ImGui.EndPopup();
|
|
}
|
|
|
|
var saveVoxelPopupStrId = "Save Voxels Popup";
|
|
bool isSaveVoxelPopup = true;
|
|
if (dynaMesh != null)
|
|
{
|
|
ImGui.Checkbox("Compression", ref compression);
|
|
if (ImGui.Button("Save Voxels..."))
|
|
{
|
|
ImGui.BeginPopup(saveVoxelPopupStrId);
|
|
}
|
|
|
|
if (ImGui.BeginPopupModal(saveVoxelPopupStrId, ref isSaveVoxelPopup, ImGuiWindowFlags.NoTitleBar))
|
|
{
|
|
var picker = ImFilePicker.GetFilePicker(saveVoxelPopupStrId, Path.Combine(Environment.CurrentDirectory), ".voxels");
|
|
if (picker.Draw())
|
|
{
|
|
if (string.IsNullOrEmpty(picker.SelectedFile))
|
|
save(picker.SelectedFile);
|
|
|
|
ImFilePicker.RemoveFilePicker(saveVoxelPopupStrId);
|
|
}
|
|
ImGui.EndPopup();
|
|
}
|
|
|
|
}
|
|
|
|
ImGui.NewLine();
|
|
|
|
ImGui.Text("Rasterization");
|
|
ImGui.Separator();
|
|
ImGui.Text($"Cell Size - {cellSize}");
|
|
ImGui.NewLine();
|
|
|
|
ImGui.Text("Agent");
|
|
ImGui.Separator();
|
|
ImGui.SliderFloat("Height", ref walkableHeight, 0f, 5f, "%.2f");
|
|
ImGui.SliderFloat("Radius", ref walkableRadius, 0f, 10f, "%.2f");
|
|
ImGui.SliderFloat("Max Climb", ref walkableClimb, 0f, 10f, "%.2f");
|
|
ImGui.Text($"Max Slope : {walkableSlopeAngle}");
|
|
ImGui.NewLine();
|
|
|
|
ImGui.Text("Partitioning");
|
|
ImGui.Separator();
|
|
PartitionType.Values.forEach(partition =>
|
|
{
|
|
var label = partition.Name.Substring(0, 1).ToUpper()
|
|
+ partition.Name.Substring(1).ToLower();
|
|
ImGui.RadioButton(label, ref partitioningIdx, partition.Idx);
|
|
});
|
|
ImGui.NewLine();
|
|
|
|
ImGui.Text("Filtering");
|
|
ImGui.Separator();
|
|
ImGui.Checkbox("Low Hanging Obstacles", ref filterLowHangingObstacles);
|
|
ImGui.Checkbox("Ledge Spans", ref filterLedgeSpans);
|
|
ImGui.Checkbox("Walkable Low Height Spans", ref filterWalkableLowHeightSpans);
|
|
ImGui.NewLine();
|
|
|
|
ImGui.Text("Region");
|
|
ImGui.Separator();
|
|
ImGui.SliderFloat("Min Region Size", ref minRegionArea, 0, 150, "%.1f");
|
|
ImGui.SliderFloat("Merged Region Size", ref regionMergeSize, 0, 400, "%.1f");
|
|
ImGui.NewLine();
|
|
|
|
ImGui.Text("Polygonization");
|
|
ImGui.Separator();
|
|
ImGui.SliderFloat("Max Edge Length", ref maxEdgeLen, 0f, 50f, "%.1f");
|
|
ImGui.SliderFloat("Max Edge Error", ref maxSimplificationError, 0.1f, 10f, "%.1f");
|
|
ImGui.SliderInt("Verts Per Poly", ref vertsPerPoly, 3, 12);
|
|
ImGui.NewLine();
|
|
|
|
ImGui.Text("Detail Mesh");
|
|
ImGui.Separator();
|
|
ImGui.Checkbox("Enable", ref buildDetailMesh);
|
|
ImGui.SliderFloat("Sample Distance", ref detailSampleDist, 0f, 16f, "%.1f");
|
|
ImGui.SliderFloat("Max Sample Error", ref detailSampleMaxError, 0f, 16f, "%.1f");
|
|
ImGui.NewLine();
|
|
|
|
if (ImGui.Button("Build"))
|
|
{
|
|
if (dynaMesh != null)
|
|
{
|
|
buildDynaMesh();
|
|
sample.setChanged(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (mode == DynamicUpdateToolMode.COLLIDERS)
|
|
{
|
|
ImGui.Text("Colliders");
|
|
ImGui.Separator();
|
|
var prev = colliderShape;
|
|
ImGui.Checkbox("Show", ref showColliders);
|
|
ImGui.RadioButton("Sphere", ref colliderShapeIdx, (int)ColliderShape.SPHERE);
|
|
ImGui.RadioButton("Capsule", ref colliderShapeIdx, (int)ColliderShape.CAPSULE);
|
|
ImGui.RadioButton("Box", ref colliderShapeIdx, (int)ColliderShape.BOX);
|
|
ImGui.RadioButton("Cylinder", ref colliderShapeIdx, (int)ColliderShape.CYLINDER);
|
|
ImGui.RadioButton("Composite", ref colliderShapeIdx, (int)ColliderShape.COMPOSITE);
|
|
ImGui.RadioButton("Convex Trimesh", ref colliderShapeIdx, (int)ColliderShape.CONVEX);
|
|
ImGui.RadioButton("Trimesh Bridge", ref colliderShapeIdx, (int)ColliderShape.TRIMESH_BRIDGE);
|
|
ImGui.RadioButton("Trimesh House", ref colliderShapeIdx, (int)ColliderShape.TRIMESH_HOUSE);
|
|
ImGui.NewLine();
|
|
|
|
if ((int)prev != colliderShapeIdx)
|
|
{
|
|
colliderShape = (ColliderShape)colliderShapeIdx;
|
|
}
|
|
}
|
|
|
|
if (mode == DynamicUpdateToolMode.RAYCAST)
|
|
{
|
|
ImGui.Text($"Raycast Time: {raycastTime} ms");
|
|
ImGui.Separator();
|
|
if (sposSet)
|
|
{
|
|
ImGui.Text($"Start: {spos[0]}, {spos[1] + 1.3f}, {spos[2]}");
|
|
}
|
|
|
|
if (eposSet)
|
|
{
|
|
ImGui.Text($"End: {epos[0]}, {epos[1] + 1.3f}, {epos[2]}");
|
|
}
|
|
|
|
if (raycastHit)
|
|
{
|
|
ImGui.Text($"Hit: {raycastHitPos[0]}, {raycastHitPos[1]}, {raycastHitPos[2]}");
|
|
}
|
|
|
|
ImGui.NewLine();
|
|
}
|
|
else
|
|
{
|
|
ImGui.Text($"Build Time: {buildTime} ms");
|
|
}
|
|
}
|
|
|
|
|
|
private void load(string filename)
|
|
{
|
|
try
|
|
{
|
|
using var fs = new FileStream(filename, FileMode.Open, FileAccess.Read);
|
|
using var br = new BinaryReader(fs);
|
|
VoxelFileReader reader = new VoxelFileReader();
|
|
VoxelFile voxelFile = reader.read(br);
|
|
dynaMesh = new DynamicNavMesh(voxelFile);
|
|
dynaMesh.config.keepIntermediateResults = true;
|
|
updateUI();
|
|
buildDynaMesh();
|
|
|
|
colliders.Clear();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine(e);
|
|
dynaMesh = null;
|
|
}
|
|
}
|
|
|
|
|
|
private void save(string filename)
|
|
{
|
|
using var fs = new FileStream(filename, FileMode.CreateNew, FileAccess.Write);
|
|
using var bw = new BinaryWriter(fs);
|
|
VoxelFile voxelFile = VoxelFile.from(dynaMesh);
|
|
VoxelFileWriter writer = new VoxelFileWriter();
|
|
writer.write(bw, voxelFile, compression);
|
|
}
|
|
|
|
private void buildDynaMesh()
|
|
{
|
|
configDynaMesh();
|
|
long t = Stopwatch.GetTimestamp();
|
|
try
|
|
{
|
|
var _ = dynaMesh.build(executor).Result;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine(e);
|
|
}
|
|
|
|
buildTime = (Stopwatch.GetTimestamp() - t) / TimeSpan.TicksPerMillisecond;
|
|
sample.update(null, dynaMesh.recastResults(), dynaMesh.navMesh());
|
|
}
|
|
|
|
private void configDynaMesh()
|
|
{
|
|
dynaMesh.config.partitionType = partitioning;
|
|
dynaMesh.config.walkableHeight = walkableHeight;
|
|
dynaMesh.config.walkableSlopeAngle = walkableSlopeAngle;
|
|
dynaMesh.config.walkableRadius = walkableRadius;
|
|
dynaMesh.config.walkableClimb = walkableClimb;
|
|
dynaMesh.config.filterLowHangingObstacles = filterLowHangingObstacles;
|
|
dynaMesh.config.filterLedgeSpans = filterLedgeSpans;
|
|
dynaMesh.config.filterWalkableLowHeightSpans = filterWalkableLowHeightSpans;
|
|
dynaMesh.config.minRegionArea = minRegionArea;
|
|
dynaMesh.config.regionMergeArea = regionMergeSize;
|
|
dynaMesh.config.maxEdgeLen = maxEdgeLen;
|
|
dynaMesh.config.maxSimplificationError = maxSimplificationError;
|
|
dynaMesh.config.vertsPerPoly = vertsPerPoly;
|
|
dynaMesh.config.buildDetailMesh = buildDetailMesh;
|
|
dynaMesh.config.detailSampleDistance = detailSampleDist;
|
|
dynaMesh.config.detailSampleMaxError = detailSampleMaxError;
|
|
}
|
|
|
|
private void updateUI()
|
|
{
|
|
cellSize = dynaMesh.config.cellSize;
|
|
partitioning = dynaMesh.config.partitionType;
|
|
walkableHeight = dynaMesh.config.walkableHeight;
|
|
walkableSlopeAngle = dynaMesh.config.walkableSlopeAngle;
|
|
walkableRadius = dynaMesh.config.walkableRadius;
|
|
walkableClimb = dynaMesh.config.walkableClimb;
|
|
minRegionArea = dynaMesh.config.minRegionArea;
|
|
regionMergeSize = dynaMesh.config.regionMergeArea;
|
|
maxEdgeLen = dynaMesh.config.maxEdgeLen;
|
|
maxSimplificationError = dynaMesh.config.maxSimplificationError;
|
|
vertsPerPoly = dynaMesh.config.vertsPerPoly;
|
|
buildDetailMesh = dynaMesh.config.buildDetailMesh;
|
|
detailSampleDist = dynaMesh.config.detailSampleDistance;
|
|
detailSampleMaxError = dynaMesh.config.detailSampleMaxError;
|
|
filterLowHangingObstacles = dynaMesh.config.filterLowHangingObstacles;
|
|
filterLedgeSpans = dynaMesh.config.filterLedgeSpans;
|
|
filterWalkableLowHeightSpans = dynaMesh.config.filterWalkableLowHeightSpans;
|
|
}
|
|
|
|
public override string getName()
|
|
{
|
|
return "Dynamic Updates";
|
|
}
|
|
} |