Skip to content

Commit

Permalink
Per-zone customization + reverted some badly-behaved defaults.
Browse files Browse the repository at this point in the history
  • Loading branch information
awgil committed Apr 5, 2024
1 parent a4f4c32 commit baf30bb
Show file tree
Hide file tree
Showing 12 changed files with 150 additions and 56 deletions.
4 changes: 3 additions & 1 deletion TODO
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
- walk/fly transitions
- optimizations
-- use heightmap equivalent for voxelization/downsample
-- use 1 bit per voxel for last level (16x memory save)
- better pathfind
-- use same string-pulling idea for navvolumes
-- follow/target update
- intersectionset dirty roots?
- per-zone custom markup (tighter borders? shapes to ignore?)
- per-zone custom markup
-- tighter borders / set of regions to ignore geometry outside
- volume nav - consider building equivalent of navmesh
-- do we even need any compact representation? connectivity sounds hard for 3d spans...
-- watershed-like partitioning - should be easy
Expand Down
9 changes: 9 additions & 0 deletions vnavmesh/Customizations/Z0250WolvesDenPier.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Navmesh.Customizations;

[CustomizationTerritory(250)]
class Z0250WolvesDenPier : NavmeshCustomization
{
public override int Version => 1;

public override bool IsFlyingSupported(SceneDefinition definition) => false; // this is unflyable, despite intended use being 1
}
12 changes: 12 additions & 0 deletions vnavmesh/Customizations/Z0519LostCityOfAmdaporHard.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Navmesh.Customizations;

[CustomizationTerritory(519)]
class Z0519LostCityOfAmdaporHard : NavmeshCustomization
{
public override int Version => 1;

public Z0519LostCityOfAmdaporHard()
{
Settings.AgentMaxClimb = 0.75f; // web bridges - TODO: think about a better systemic solution
}
}
57 changes: 32 additions & 25 deletions vnavmesh/Debug/DebugNavmeshCustom.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ class DebugNavmeshCustom : IDisposable
{
private record struct HeightfieldComparison(float DurationOld, float DurationNew, bool Identical);

public class Customization : NavmeshCustomization
{
public bool Flyable;

public override int Version => 1;
public override bool IsFlyingSupported(SceneDefinition definition) => Flyable;
}

// async navmesh builder
public class AsyncBuilder : IDisposable
{
Expand Down Expand Up @@ -56,14 +64,14 @@ public void Dispose()
Clear();
}

public void Rebuild(NavmeshSettings settings, bool flyable, bool includeTiles)
public void Rebuild(Customization settings, bool includeTiles)
{
Clear();
Service.Log.Debug("[navmesh] extract from scene");
_scene = new();
_scene.FillFromActiveLayout();
Service.Log.Debug("[navmesh] schedule async build");
_task = Task.Run(() => BuildNavmesh(_scene, flyable, settings, includeTiles));
_task = Task.Run(() => BuildNavmesh(_scene, settings, includeTiles));
}

public void Clear()
Expand All @@ -82,12 +90,12 @@ public void Clear()
//GC.Collect();
}

private void BuildNavmesh(SceneDefinition scene, bool flyable, NavmeshSettings settings, bool includeTiles)
private void BuildNavmesh(SceneDefinition scene, NavmeshCustomization customization, bool includeTiles)
{
try
{
var timer = Timer.Create();
_builder = new(scene, flyable, settings);
_builder = new(scene, customization);

// create tile data and add to navmesh
_intermediates = new(_builder.NumTilesX, _builder.NumTilesZ);
Expand Down Expand Up @@ -135,7 +143,7 @@ public void Dispose()
}
}

private NavmeshSettings _settings = new();
private Customization _settings = new();
private AsyncBuilder _navmesh = new();
private UITree _tree = new();
private DebugDrawer _dd;
Expand Down Expand Up @@ -165,27 +173,26 @@ public void Dispose()
public void Draw()
{
using (var nsettings = _tree.Node("Navmesh properties"))
{
if (nsettings.Opened)
_settings.Draw();
{
ImGui.Checkbox("Support flying", ref _settings.Flyable);
_settings.Settings.Draw();
}
}

using (var d = ImRaii.Disabled(_navmesh.CurrentState == AsyncBuilder.State.InProgress))
{
if (ImGui.Button("Rebuild navmesh flyable"))
{
Clear();
_navmesh.Rebuild(_settings, true, true);
}
ImGui.SameLine();
if (ImGui.Button("Rebuild navmesh non-flyable"))
if (ImGui.Button("Rebuild navmesh"))
{
Clear();
_navmesh.Rebuild(_settings, false, true);
_navmesh.Rebuild(_settings, true);
}
ImGui.SameLine();
if (ImGui.Button("Rebuild scene extract only"))
{
Clear();
_navmesh.Rebuild(_settings, true, false);
_navmesh.Rebuild(_settings, false);
}
ImGui.SameLine();
ImGui.TextUnformatted($"State: {_navmesh.CurrentState}");
Expand Down Expand Up @@ -289,16 +296,16 @@ private HeightfieldComparison CompareHeightfields(int tx, int tz, SceneExtractor
var telemetry = new RcContext();
var boundsMin = new Vector3(-1024);
var boundsMax = new RcVec3f(1024);
var numTilesXZ = _settings.NumTiles[0];
var numTilesXZ = _settings.Settings.NumTiles[0];
var tileWidth = (boundsMax.X - boundsMin.X) / numTilesXZ;
var tileHeight = (boundsMax.Z - boundsMin.Z) / numTilesXZ;
var walkableClimbVoxels = (int)MathF.Floor(_settings.AgentMaxClimb / _settings.CellHeight);
var walkableRadiusVoxels = (int)MathF.Ceiling(_settings.AgentRadius / _settings.CellSize);
var walkableNormalThreshold = _settings.AgentMaxSlopeDeg.Degrees().Cos();
var walkableClimbVoxels = (int)MathF.Floor(_settings.Settings.AgentMaxClimb / _settings.Settings.CellHeight);
var walkableRadiusVoxels = (int)MathF.Ceiling(_settings.Settings.AgentRadius / _settings.Settings.CellSize);
var walkableNormalThreshold = _settings.Settings.AgentMaxSlopeDeg.Degrees().Cos();
var borderSizeVoxels = 3 + walkableRadiusVoxels;
var borderSizeWorld = borderSizeVoxels * _settings.CellSize;
var tileSizeXVoxels = (int)MathF.Ceiling(tileWidth / _settings.CellSize) + 2 * borderSizeVoxels;
var tileSizeZVoxels = (int)MathF.Ceiling(tileHeight / _settings.CellSize) + 2 * borderSizeVoxels;
var borderSizeWorld = borderSizeVoxels * _settings.Settings.CellSize;
var tileSizeXVoxels = (int)MathF.Ceiling(tileWidth / _settings.Settings.CellSize) + 2 * borderSizeVoxels;
var tileSizeZVoxels = (int)MathF.Ceiling(tileHeight / _settings.Settings.CellSize) + 2 * borderSizeVoxels;
var tileBoundsMin = new Vector3(boundsMin.X + tx * tileWidth, boundsMin.Y, boundsMin.Z + tz * tileHeight);
var tileBoundsMax = new Vector3(tileBoundsMin.X + tileWidth, boundsMax.Y, tileBoundsMin.Z + tileHeight);
tileBoundsMin.X -= borderSizeWorld;
Expand All @@ -307,12 +314,12 @@ private HeightfieldComparison CompareHeightfields(int tx, int tz, SceneExtractor
tileBoundsMax.Z += borderSizeWorld;

var timer = Timer.Create();
var shfOld = new RcHeightfield(tileSizeXVoxels, tileSizeZVoxels, tileBoundsMin.SystemToRecast(), tileBoundsMax.SystemToRecast(), _settings.CellSize, _settings.CellHeight, borderSizeVoxels);
var shfOld = new RcHeightfield(tileSizeXVoxels, tileSizeZVoxels, tileBoundsMin.SystemToRecast(), tileBoundsMax.SystemToRecast(), _settings.Settings.CellSize, _settings.Settings.CellHeight, borderSizeVoxels);
var rasterizerOld = new NavmeshRasterizer(shfOld, walkableNormalThreshold, walkableClimbVoxels, 0, false, null, telemetry);
rasterizerOld.RasterizeOld(scene, SceneExtractor.MeshType.All);
var dur1 = (float)timer.Value().TotalSeconds;

var shfNew = new RcHeightfield(tileSizeXVoxels, tileSizeZVoxels, tileBoundsMin.SystemToRecast(), tileBoundsMax.SystemToRecast(), _settings.CellSize, _settings.CellHeight, borderSizeVoxels);
var shfNew = new RcHeightfield(tileSizeXVoxels, tileSizeZVoxels, tileBoundsMin.SystemToRecast(), tileBoundsMax.SystemToRecast(), _settings.Settings.CellSize, _settings.Settings.CellHeight, borderSizeVoxels);
var rasterizerNew = new NavmeshRasterizer(shfNew, walkableNormalThreshold, walkableClimbVoxels, 0, false, null, telemetry);
rasterizerNew.Rasterize(scene, SceneExtractor.MeshType.All, false, false);
var dur2 = (float)timer.Value().TotalSeconds;
Expand Down Expand Up @@ -344,7 +351,7 @@ private HeightfieldComparison CompareAllHeightfields(SceneExtractor scene)
{
float dur1 = 0, dur2 = 0;
bool identical = true;
var numTilesXZ = _settings.NumTiles[0];
var numTilesXZ = _settings.Settings.NumTiles[0];
for (int tz = 0; tz < numTilesXZ; ++tz)
{
for (int tx = 0; tx < numTilesXZ; ++tx)
Expand Down
12 changes: 8 additions & 4 deletions vnavmesh/Navmesh.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,33 @@
namespace Navmesh;

// full set of data needed for navigation in the zone
public record class Navmesh(DtNavMesh Mesh, VoxelMap? Volume)
public record class Navmesh(int CustomizationVersion, DtNavMesh Mesh, VoxelMap? Volume)
{
public static readonly uint Magic = 0x444D564E; // 'NVMD'
public static readonly uint Version = 12;
public static readonly uint Version = 13;

// throws an exception on failure
public static Navmesh Deserialize(BinaryReader reader, NavmeshSettings settings)
public static Navmesh Deserialize(BinaryReader reader, int expectedCustomizationVersion)
{
var magic = reader.ReadUInt32();
var version = reader.ReadUInt32();
if (magic != Magic || version != Version)
throw new Exception("Incorrect header");
var customizationVersion = reader.ReadInt32();
if (customizationVersion != expectedCustomizationVersion)
throw new Exception("Outdated customization version");

using var compressedReader = new BinaryReader(new BrotliStream(reader.BaseStream, CompressionMode.Decompress, true));
var mesh = DeserializeMesh(reader);
var volume = DeserializeVolume(reader);
return new(mesh, volume);
return new(customizationVersion, mesh, volume);
}

public void Serialize(BinaryWriter writer)
{
writer.Write(Magic);
writer.Write(Version);
writer.Write(CustomizationVersion);

using var compressedWriter = new BinaryWriter(new BrotliStream(writer.BaseStream, CompressionLevel.Optimal, true));
SerializeMesh(writer, Mesh);
Expand Down
23 changes: 13 additions & 10 deletions vnavmesh/NavmeshBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,19 @@ public record struct Intermediates(RcHeightfield SolidHeightfield, RcCompactHeig
private int _voxelizerNumY = 1;
private int _voxelizerNumZ = 1;

public NavmeshBuilder(SceneDefinition scene, bool flyable, NavmeshSettings settings)
public NavmeshBuilder(SceneDefinition scene, NavmeshCustomization customization)
{
Settings = settings;
Settings = customization.Settings;
var flyable = customization.IsFlyingSupported(scene);

// load all meshes
Scene = new(scene);
customization.CustomizeScene(Scene);

BoundsMin = new(-1024);
BoundsMax = new(1024);
NumTilesX = NumTilesZ = settings.NumTiles[0];
Service.Log.Debug($"starting building {NumTilesX}x{NumTilesZ} navmesh");
NumTilesX = NumTilesZ = Settings.NumTiles[0];
Service.Log.Debug($"starting building {NumTilesX}x{NumTilesZ} navmesh, customization = {customization.GetType()} v{customization.Version}");

// create empty navmesh
var navmeshParams = new DtNavMeshParams();
Expand All @@ -53,9 +56,9 @@ public NavmeshBuilder(SceneDefinition scene, bool flyable, NavmeshSettings setti
navmeshParams.maxTiles = NumTilesX * NumTilesZ;
navmeshParams.maxPolys = 1 << DtNavMesh.DT_POLY_BITS;

var navmesh = new DtNavMesh(navmeshParams, settings.PolyMaxVerts);
var volume = flyable ? new VoxelMap(BoundsMin, BoundsMax, settings.NumTiles) : null;
Navmesh = new(navmesh, volume);
var navmesh = new DtNavMesh(navmeshParams, Settings.PolyMaxVerts);
var volume = flyable ? new VoxelMap(BoundsMin, BoundsMax, Settings.NumTiles) : null;
Navmesh = new(customization.Version, navmesh, volume);

// calculate derived parameters
_walkableClimbVoxels = (int)MathF.Floor(Settings.AgentMaxClimb / Settings.CellHeight);
Expand All @@ -68,10 +71,10 @@ public NavmeshBuilder(SceneDefinition scene, bool flyable, NavmeshSettings setti
_tileSizeZVoxels = (int)MathF.Ceiling(navmeshParams.tileHeight / Settings.CellSize) + 2 * _borderSizeVoxels;
if (volume != null)
{
_voxelizerNumY = settings.NumTiles[0];
for (int i = 1; i < settings.NumTiles.Length; ++i)
_voxelizerNumY = Settings.NumTiles[0];
for (int i = 1; i < Settings.NumTiles.Length; ++i)
{
var n = settings.NumTiles[i];
var n = Settings.NumTiles[i];
_voxelizerNumX *= n;
_voxelizerNumY *= n;
_voxelizerNumZ *= n;
Expand Down
59 changes: 59 additions & 0 deletions vnavmesh/NavmeshCustomization.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace Navmesh;

// base class for per-territory navmesh customizations
public class NavmeshCustomization
{
// every time defaults change, we need to bump global navmesh version - this should be kept at zero
// every time customization changes, we can bump the local version field, to avoid invalidating whole cache
// each derived class should set it to non-zero value
public virtual int Version => 0;

public NavmeshSettings Settings = new();

public virtual bool IsFlyingSupported(SceneDefinition definition) => Service.LuminaRow<Lumina.Excel.GeneratedSheets.TerritoryType>(definition.TerritoryID)?.TerritoryIntendedUse is 1 or 49 or 47; // 1 is normal outdoor, 49 is island, 47 is Diadem

// this is a customization point to add or remove colliders in the scene
public virtual void CustomizeScene(SceneExtractor scene) { }
}

// attribute that defines which territories particular customization applies to
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public class CustomizationTerritoryAttribute : Attribute
{
public uint TerritoryID;

public CustomizationTerritoryAttribute(uint territoryID) => TerritoryID = territoryID;
}

// registry containing all customizations
public static class NavmeshCustomizationRegistry
{
public static NavmeshCustomization Default = new();
public static Dictionary<uint, NavmeshCustomization> PerTerritory = new();

static NavmeshCustomizationRegistry()
{
var baseType = typeof(NavmeshCustomization);
foreach (var t in Assembly.GetExecutingAssembly().DefinedTypes.Where(t => t.IsSubclassOf(baseType)))
{
var instance = Activator.CreateInstance(t) as NavmeshCustomization;
if (instance == null)
{
Service.Log.Error($"Failed to create instance of customization class {t}");
continue;
}

foreach (var attr in t.GetCustomAttributes<CustomizationTerritoryAttribute>())
{
PerTerritory.Add(attr.TerritoryID, instance);
}
}
}

public static NavmeshCustomization ForTerritory(uint id) => PerTerritory.GetValueOrDefault(id, Default);
}
18 changes: 6 additions & 12 deletions vnavmesh/NavmeshManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ public class NavmeshManager : IDisposable
public int NumQueuedPathfindRequests => _queuedPathfindTasks.Count;

private DirectoryInfo _cacheDir;
private NavmeshSettings _settings = new();
private string _lastKey = "";
private Task<Navmesh>? _loadTask;
private volatile float _loadTaskProgress;
Expand Down Expand Up @@ -124,9 +123,8 @@ public bool Reload(bool allowLoadFromCache)
var scene = new SceneDefinition();
scene.FillFromActiveLayout();
var cacheKey = GetCacheKey(scene);
var flyable = GetFlyable();
_loadTaskProgress = 0;
_loadTask = Task.Run(() => BuildNavmesh(scene, flyable, cacheKey, allowLoadFromCache));
_loadTask = Task.Run(() => BuildNavmesh(scene, cacheKey, allowLoadFromCache));
}
return true;
}
Expand Down Expand Up @@ -191,12 +189,6 @@ private unsafe string GetCacheKey(SceneDefinition scene)
return $"{terrRow.Bg.ToString().Replace('/', '_')}__{filterKey:X}__{string.Join('.', scene.FestivalLayers.Select(id => id.ToString("X")))}";
}

private unsafe bool GetFlyable()
{
var layout = LayoutWorld.Instance()->ActiveLayout;
return layout->TerritoryTypeId != 250 && Service.LuminaRow<Lumina.Excel.GeneratedSheets.TerritoryType>(layout->TerritoryTypeId)?.TerritoryIntendedUse is 1 or 49 or 47; // exclude wolves' den pier; 1 is normal outdoor, 49 is island, 47 is Diadem
}

private void ClearState()
{
_queryCancelSource?.Cancel();
Expand All @@ -210,8 +202,10 @@ private void ClearState()
_navmesh = null;
}

private Navmesh BuildNavmesh(SceneDefinition scene, bool flyable, string cacheKey, bool allowLoadFromCache)
private Navmesh BuildNavmesh(SceneDefinition scene, string cacheKey, bool allowLoadFromCache)
{
var customization = NavmeshCustomizationRegistry.ForTerritory(scene.TerritoryID);

// try reading from cache
var cache = new FileInfo($"{_cacheDir.FullName}/{cacheKey}.navmesh");
if (allowLoadFromCache && cache.Exists)
Expand All @@ -221,7 +215,7 @@ private Navmesh BuildNavmesh(SceneDefinition scene, bool flyable, string cacheKe
Service.Log.Debug($"Loading cache: {cache.FullName}");
using var stream = cache.OpenRead();
using var reader = new BinaryReader(stream);
return Navmesh.Deserialize(reader, _settings);
return Navmesh.Deserialize(reader, customization.Version);
}
catch (Exception ex)
{
Expand All @@ -231,7 +225,7 @@ private Navmesh BuildNavmesh(SceneDefinition scene, bool flyable, string cacheKe

// cache doesn't exist or can't be used for whatever reason - build navmesh from scratch
// TODO: we can build multiple tiles concurrently
var builder = new NavmeshBuilder(scene, flyable, _settings);
var builder = new NavmeshBuilder(scene, customization);
var deltaProgress = 1.0f / (builder.NumTilesX * builder.NumTilesZ);
for (int z = 0; z < builder.NumTilesZ; ++z)
{
Expand Down
Loading

0 comments on commit baf30bb

Please sign in to comment.