forked from bit/DotRecastNetSim
505 lines
17 KiB
C#
505 lines
17 KiB
C#
/*
|
|
recast4j copyright (c) 2021 Piotr Piastucki piotr@jtilia.org
|
|
DotRecast Copyright (c) 2023 Choi Ikpil ikpil@naver.com
|
|
|
|
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 System.Threading.Tasks;
|
|
using DotRecast.Core;
|
|
using DotRecast.Detour.Dynamic;
|
|
using DotRecast.Detour.Dynamic.Io;
|
|
using DotRecast.Recast.Toolset;
|
|
using DotRecast.Recast.Toolset.Gizmos;
|
|
using DotRecast.Recast.Toolset.Tools;
|
|
using DotRecast.Recast.Demo.Draw;
|
|
using DotRecast.Recast.Demo.UI;
|
|
using DotRecast.Recast.Toolset.Geom;
|
|
using ImGuiNET;
|
|
using Serilog;
|
|
using static DotRecast.Recast.Demo.Draw.DebugDraw;
|
|
using static DotRecast.Recast.Demo.Draw.DebugDrawPrimitives;
|
|
|
|
namespace DotRecast.Recast.Demo.Tools;
|
|
|
|
public class DynamicUpdateSampleTool : ISampleTool
|
|
{
|
|
private static readonly ILogger Logger = Log.ForContext<DynamicUpdateSampleTool>();
|
|
|
|
private DemoSample _sample;
|
|
private readonly RcDynamicUpdateTool _tool;
|
|
|
|
private RcDynamicUpdateToolMode mode = RcDynamicUpdateToolMode.BUILD;
|
|
private float cellSize = 0.3f;
|
|
|
|
// build config
|
|
private int partitioning = RcPartitionType.WATERSHED.Value;
|
|
private float walkableSlopeAngle = 45f;
|
|
private float walkableHeight = 2f;
|
|
private float walkableRadius = 0.6f;
|
|
private float walkableClimb = 0.9f;
|
|
|
|
private float minRegionArea = 6f;
|
|
private float regionMergeSize = 36f;
|
|
|
|
private float maxEdgeLen = 12f;
|
|
private float maxSimplificationError = 1.3f;
|
|
private int vertsPerPoly = 6;
|
|
|
|
private float detailSampleDist = 6f;
|
|
private float detailSampleMaxError = 1f;
|
|
|
|
private bool filterLowHangingObstacles = true;
|
|
private bool filterLedgeSpans = true;
|
|
private bool filterWalkableLowHeightSpans = true;
|
|
|
|
private bool buildDetailMesh = true;
|
|
|
|
private bool compression = true;
|
|
private bool showColliders = false;
|
|
private long buildTime;
|
|
private long raycastTime;
|
|
|
|
private RcDynamicColliderShape colliderShape = RcDynamicColliderShape.SPHERE;
|
|
|
|
private readonly TaskFactory executor;
|
|
|
|
private bool sposSet;
|
|
private bool eposSet;
|
|
private RcVec3f spos;
|
|
private RcVec3f epos;
|
|
private bool raycastHit;
|
|
private RcVec3f raycastHitPos;
|
|
|
|
public DynamicUpdateSampleTool()
|
|
{
|
|
var bridgeGeom = DemoInputGeomProvider.LoadFile("bridge.obj");
|
|
var houseGeom = DemoInputGeomProvider.LoadFile("house.obj");
|
|
var convexGeom = DemoInputGeomProvider.LoadFile("convex.obj");
|
|
_tool = new(Random.Shared, bridgeGeom, houseGeom, convexGeom);
|
|
executor = Task.Factory;
|
|
}
|
|
|
|
public void Layout()
|
|
{
|
|
var prevModeIdx = mode.Idx;
|
|
|
|
ImGui.Text($"Dynamic Update Tool Modes");
|
|
ImGui.Separator();
|
|
ImGui.RadioButton(RcDynamicUpdateToolMode.BUILD.Label, ref prevModeIdx, RcDynamicUpdateToolMode.BUILD.Idx);
|
|
ImGui.RadioButton(RcDynamicUpdateToolMode.COLLIDERS.Label, ref prevModeIdx, RcDynamicUpdateToolMode.COLLIDERS.Idx);
|
|
ImGui.RadioButton(RcDynamicUpdateToolMode.RAYCAST.Label, ref prevModeIdx, RcDynamicUpdateToolMode.RAYCAST.Idx);
|
|
ImGui.NewLine();
|
|
|
|
if (prevModeIdx != mode.Idx)
|
|
{
|
|
mode = RcDynamicUpdateToolMode.Values[prevModeIdx];
|
|
}
|
|
|
|
ImGui.Text($"Selected mode - {mode.Label}");
|
|
ImGui.Separator();
|
|
|
|
if (mode == RcDynamicUpdateToolMode.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;
|
|
|
|
var dynaMesh = _tool.GetDynamicNavMesh();
|
|
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();
|
|
RcPartitionType.Values.ForEach(partition =>
|
|
{
|
|
var label = partition.Name.Substring(0, 1).ToUpper()
|
|
+ partition.Name.Substring(1).ToLower();
|
|
ImGui.RadioButton(label, ref partitioning, partition.Value);
|
|
});
|
|
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 Dynamic mesh"))
|
|
{
|
|
if (dynaMesh != null)
|
|
{
|
|
BuildDynaMesh();
|
|
_sample.SetChanged(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (mode == RcDynamicUpdateToolMode.COLLIDERS)
|
|
{
|
|
var prevColliderShape = (int)colliderShape;
|
|
|
|
ImGui.Text("Colliders");
|
|
ImGui.Separator();
|
|
ImGui.Checkbox("Show", ref showColliders);
|
|
ImGui.RadioButton("Sphere", ref prevColliderShape, (int)RcDynamicColliderShape.SPHERE);
|
|
ImGui.RadioButton("Capsule", ref prevColliderShape, (int)RcDynamicColliderShape.CAPSULE);
|
|
ImGui.RadioButton("Box", ref prevColliderShape, (int)RcDynamicColliderShape.BOX);
|
|
ImGui.RadioButton("Cylinder", ref prevColliderShape, (int)RcDynamicColliderShape.CYLINDER);
|
|
ImGui.RadioButton("Composite", ref prevColliderShape, (int)RcDynamicColliderShape.COMPOSITE);
|
|
ImGui.RadioButton("Convex Trimesh", ref prevColliderShape, (int)RcDynamicColliderShape.CONVEX);
|
|
ImGui.RadioButton("Trimesh Bridge", ref prevColliderShape, (int)RcDynamicColliderShape.TRIMESH_BRIDGE);
|
|
ImGui.RadioButton("Trimesh House", ref prevColliderShape, (int)RcDynamicColliderShape.TRIMESH_HOUSE);
|
|
ImGui.NewLine();
|
|
|
|
if (prevColliderShape != (int)colliderShape)
|
|
{
|
|
colliderShape = (RcDynamicColliderShape)prevColliderShape;
|
|
}
|
|
}
|
|
|
|
if (mode == RcDynamicUpdateToolMode.RAYCAST)
|
|
{
|
|
ImGui.Text($"Raycast Time: {raycastTime} ms");
|
|
ImGui.Separator();
|
|
if (sposSet)
|
|
{
|
|
ImGui.Text($"Start: {spos.x}, {spos.y + 1.3f}, {spos.z}");
|
|
}
|
|
|
|
if (eposSet)
|
|
{
|
|
ImGui.Text($"End: {epos.x}, {epos.y + 1.3f}, {epos.z}");
|
|
}
|
|
|
|
if (raycastHit)
|
|
{
|
|
ImGui.Text($"Hit: {raycastHitPos.x}, {raycastHitPos.y}, {raycastHitPos.z}");
|
|
}
|
|
|
|
ImGui.NewLine();
|
|
}
|
|
else
|
|
{
|
|
ImGui.Text($"Build Time: {buildTime} ms");
|
|
}
|
|
}
|
|
|
|
public void HandleRender(NavMeshRenderer renderer)
|
|
{
|
|
if (mode == RcDynamicUpdateToolMode.COLLIDERS)
|
|
{
|
|
if (showColliders)
|
|
{
|
|
foreach (var gizmo in _tool.GetGizmos())
|
|
{
|
|
GizmoRenderer.Render(renderer.GetDebugDraw(), gizmo.Gizmo);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (mode == RcDynamicUpdateToolMode.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 != RcVec3f.Zero)
|
|
{
|
|
int spathCol = raycastHit ? DuRGBA(128, 32, 16, 220) : DuRGBA(64, 128, 240, 220);
|
|
dd.Begin(LINES, 2.0f);
|
|
dd.Vertex(spos.x, spos.y + 1.3f, spos.z, spathCol);
|
|
dd.Vertex(raycastHitPos.x, raycastHitPos.y, raycastHitPos.z, spathCol);
|
|
dd.End();
|
|
}
|
|
|
|
dd.DepthMask(true);
|
|
}
|
|
}
|
|
|
|
private void DrawAgent(RecastDebugDraw dd, RcVec3f pos, int col)
|
|
{
|
|
var settings = _sample.GetSettings();
|
|
float r = settings.agentRadius;
|
|
float h = settings.agentHeight;
|
|
float c = settings.agentMaxClimb;
|
|
dd.DepthMask(false);
|
|
// Agent dimensions.
|
|
dd.DebugDrawCylinderWire(pos.x - r, pos.y + 0.02f, pos.z - r, pos.x + r, pos.y + h, pos.z + r, col, 2.0f);
|
|
dd.DebugDrawCircle(pos.x, pos.y + c, pos.z, r, DuRGBA(0, 0, 0, 64), 1.0f);
|
|
int colb = DuRGBA(0, 0, 0, 196);
|
|
dd.Begin(LINES);
|
|
dd.Vertex(pos.x, pos.y - c, pos.z, colb);
|
|
dd.Vertex(pos.x, pos.y + c, pos.z, colb);
|
|
dd.Vertex(pos.x - r / 2, pos.y + 0.02f, pos.z, colb);
|
|
dd.Vertex(pos.x + r / 2, pos.y + 0.02f, pos.z, colb);
|
|
dd.Vertex(pos.x, pos.y + 0.02f, pos.z - r / 2, colb);
|
|
dd.Vertex(pos.x, pos.y + 0.02f, pos.z + r / 2, colb);
|
|
dd.End();
|
|
dd.DepthMask(true);
|
|
}
|
|
|
|
public IRcToolable GetTool()
|
|
{
|
|
return _tool;
|
|
}
|
|
|
|
public void SetSample(DemoSample sample)
|
|
{
|
|
_sample = sample;
|
|
}
|
|
|
|
public void OnSampleChanged()
|
|
{
|
|
// ..
|
|
}
|
|
|
|
|
|
public void HandleClick(RcVec3f s, RcVec3f p, bool shift)
|
|
{
|
|
if (mode == RcDynamicUpdateToolMode.COLLIDERS)
|
|
{
|
|
if (!shift)
|
|
{
|
|
_tool.AddShape(colliderShape, p);
|
|
}
|
|
}
|
|
|
|
if (mode == RcDynamicUpdateToolMode.RAYCAST)
|
|
{
|
|
if (shift)
|
|
{
|
|
sposSet = true;
|
|
spos = p;
|
|
}
|
|
else
|
|
{
|
|
eposSet = true;
|
|
epos = p;
|
|
}
|
|
|
|
var dynaMesh = _tool.GetDynamicNavMesh();
|
|
if (sposSet && eposSet && dynaMesh != null)
|
|
{
|
|
long t1 = RcFrequency.Ticks;
|
|
bool hasHit = _tool.Raycast(spos, epos, out var hitPos, out raycastHitPos);
|
|
long t2 = RcFrequency.Ticks;
|
|
raycastTime = (t2 - t1) / TimeSpan.TicksPerMillisecond;
|
|
raycastHit = hasHit;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
public void HandleClickRay(RcVec3f start, RcVec3f dir, bool shift)
|
|
{
|
|
if (mode == RcDynamicUpdateToolMode.COLLIDERS)
|
|
{
|
|
if (shift)
|
|
{
|
|
_tool.RemoveShape(start, dir);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void HandleUpdate(float dt)
|
|
{
|
|
long t = RcFrequency.Ticks;
|
|
try
|
|
{
|
|
bool updated = _tool.UpdateDynaMesh(executor);
|
|
if (updated)
|
|
{
|
|
buildTime = (RcFrequency.Ticks - t) / TimeSpan.TicksPerMillisecond;
|
|
var dynaMesh = _tool.GetDynamicNavMesh();
|
|
_sample.Update(null, dynaMesh.RecastResults(), dynaMesh.NavMesh());
|
|
_sample.SetChanged(false);
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Logger.Error(e, "");
|
|
}
|
|
}
|
|
|
|
|
|
private void Load(string filename)
|
|
{
|
|
try
|
|
{
|
|
var dynaMesh = _tool.Load(filename, DtVoxelTileLZ4DemoCompressor.Shared);
|
|
|
|
UpdateFrom(dynaMesh.config);
|
|
BuildDynaMesh();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Logger.Error(e, "");
|
|
}
|
|
}
|
|
|
|
|
|
private void Save(string filename)
|
|
{
|
|
_tool.Save(filename, compression, DtVoxelTileLZ4DemoCompressor.Shared);
|
|
}
|
|
|
|
private void BuildDynaMesh()
|
|
{
|
|
var dynaMesh = _tool.GetDynamicNavMesh();
|
|
UpdateTo(dynaMesh.config);
|
|
long t = RcFrequency.Ticks;
|
|
try
|
|
{
|
|
var _ = dynaMesh.Build(executor).Result;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Logger.Error(e, "");
|
|
}
|
|
|
|
buildTime = (RcFrequency.Ticks - t) / TimeSpan.TicksPerMillisecond;
|
|
_sample.Update(null, dynaMesh.RecastResults(), dynaMesh.NavMesh());
|
|
}
|
|
|
|
private void UpdateTo(DtDynamicNavMeshConfig config)
|
|
{
|
|
config.partition = partitioning;
|
|
config.walkableHeight = walkableHeight;
|
|
config.walkableSlopeAngle = walkableSlopeAngle;
|
|
config.walkableRadius = walkableRadius;
|
|
config.walkableClimb = walkableClimb;
|
|
config.filterLowHangingObstacles = filterLowHangingObstacles;
|
|
config.filterLedgeSpans = filterLedgeSpans;
|
|
config.filterWalkableLowHeightSpans = filterWalkableLowHeightSpans;
|
|
config.minRegionArea = minRegionArea;
|
|
config.regionMergeArea = regionMergeSize;
|
|
config.maxEdgeLen = maxEdgeLen;
|
|
config.maxSimplificationError = maxSimplificationError;
|
|
config.vertsPerPoly = vertsPerPoly;
|
|
config.buildDetailMesh = buildDetailMesh;
|
|
config.detailSampleDistance = detailSampleDist;
|
|
config.detailSampleMaxError = detailSampleMaxError;
|
|
}
|
|
|
|
private void UpdateFrom(DtDynamicNavMeshConfig config)
|
|
{
|
|
cellSize = config.cellSize;
|
|
partitioning = config.partition;
|
|
walkableHeight = config.walkableHeight;
|
|
walkableSlopeAngle = config.walkableSlopeAngle;
|
|
walkableRadius = config.walkableRadius;
|
|
walkableClimb = config.walkableClimb;
|
|
minRegionArea = config.minRegionArea;
|
|
regionMergeSize = config.regionMergeArea;
|
|
maxEdgeLen = config.maxEdgeLen;
|
|
maxSimplificationError = config.maxSimplificationError;
|
|
vertsPerPoly = config.vertsPerPoly;
|
|
buildDetailMesh = config.buildDetailMesh;
|
|
detailSampleDist = config.detailSampleDistance;
|
|
detailSampleMaxError = config.detailSampleMaxError;
|
|
filterLowHangingObstacles = config.filterLowHangingObstacles;
|
|
filterLedgeSpans = config.filterLedgeSpans;
|
|
filterWalkableLowHeightSpans = config.filterWalkableLowHeightSpans;
|
|
}
|
|
} |