From 5e44e1cb1ad8d2af7dc19be68a3a3112ed0c5b6c Mon Sep 17 00:00:00 2001 From: nathanrun1 Date: Sat, 4 Oct 2025 13:43:39 -0700 Subject: [PATCH 1/3] feat: added travel costs to integration step --- Runtime/NativeFlowField.cs | 40 +++++++++++++++++-- .../GenerateIntegrationField.compute | 15 +++++-- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/Runtime/NativeFlowField.cs b/Runtime/NativeFlowField.cs index b9bec6d..ce25047 100644 --- a/Runtime/NativeFlowField.cs +++ b/Runtime/NativeFlowField.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using Unity.Entities; using Debug = UnityEngine.Debug; +using System.Linq; namespace FlowFieldAI { @@ -99,6 +100,8 @@ private static class ShaderProperties public static readonly int Width = Shader.PropertyToID("Width"); public static readonly int Height = Shader.PropertyToID("Height"); public static readonly int DiagonalMovement = Shader.PropertyToID("DiagonalMovement"); + public static readonly int UseTravelCosts = Shader.PropertyToID("UseTravelCosts"); + public static readonly int TravelCosts = Shader.PropertyToID("TravelCosts"); public static readonly int InputCosts = Shader.PropertyToID("InputCosts"); public static readonly int OutputCosts = Shader.PropertyToID("OutputCosts"); public static readonly int OutputHeatMap = Shader.PropertyToID("OutputHeatMap"); @@ -110,6 +113,8 @@ private static class ShaderProperties // ───────────────────────────────────────────────────────────── private readonly ComputeShader integrationComputeShader; private readonly int integrationComputeShaderKernel; + private ComputeBuffer integrationTravelCostsBuffer; // NEW + private readonly bool useTravelCosts; private ComputeBuffer integrationFrontBuffer; private ComputeBuffer integrationBackBuffer; @@ -148,7 +153,7 @@ private static class ShaderProperties /// If true, allocates an additional render texture to visualize distance propagation. /// Minimum number of buffers to keep pooled. /// Maximum number of buffers allowed for async dispatching. - public NativeFlowField(int width, int height, bool generateHeatMap=false, int minBuffers=2, int maxBuffers=5) + public NativeFlowField(int width, int height, bool generateHeatMap=false, bool useTravelCosts=false, int minBuffers=2, int maxBuffers=5) { if (minBuffers < 2) { @@ -168,6 +173,9 @@ public NativeFlowField(int width, int height, bool generateHeatMap=false, int mi integrationComputeShader = Resources.Load("GenerateIntegrationField"); integrationComputeShaderKernel = integrationComputeShader.FindKernel("GenerateIntegrationField"); + integrationTravelCostsBuffer = new ComputeBuffer(Length, sizeof(float)); + this.useTravelCosts = useTravelCosts; + integrationFrontBuffer = new ComputeBuffer(Length, sizeof(float)); integrationBackBuffer = new ComputeBuffer(Length, sizeof(float)); @@ -204,6 +212,7 @@ public NativeFlowField(int width, int height, bool generateHeatMap=false, int mi /// public void Dispose() { + integrationTravelCostsBuffer.Dispose(); integrationFrontBuffer.Dispose(); integrationBackBuffer.Dispose(); flowFieldBuffer.Dispose(); @@ -223,6 +232,18 @@ public void Dispose() /// An async GPU readback request that completes when the field is ready. public AsyncGPUReadbackRequest Bake(NativeArray inputField) => Bake(inputField, BakeOptions.Default); + /// + /// Starts a new asynchronous bake using the specified bake options and 0-initialized travel costs. + /// The returned request can be used to track GPU readback completion. + /// + /// + /// A native array representing the input field. Use and for special cells, + /// and numerical values for target weights. + /// + /// Configuration options for this bake operation. + /// An async GPU readback request that completes when the field is ready. + public AsyncGPUReadbackRequest Bake(NativeArray inputField, BakeOptions bakeOptions) => Bake(inputField, new NativeArray(), bakeOptions); + /// /// Starts a new asynchronous bake using the specified bake options. /// The returned request can be used to track GPU readback completion. @@ -231,13 +252,18 @@ public void Dispose() /// A native array representing the input field. Use and for special cells, /// and numerical values for target weights. /// + /// + /// A native array representing the travel costs of each cell in the input field. + /// These costs are added to the minimum weight calculation of each target cell at the same index. + /// /// Configuration options for this bake operation. /// An async GPU readback request that completes when the field is ready. - public AsyncGPUReadbackRequest Bake(NativeArray inputField, BakeOptions bakeOptions) + public AsyncGPUReadbackRequest Bake(NativeArray inputField, NativeArray travelCosts, BakeOptions bakeOptions) { bakeTimer.Restart(); ValidateMatrixDimensions(inputField); + if (useTravelCosts) ValidateMatrixDimensions(travelCosts); if (bufferPool.IsExhausted) { @@ -246,7 +272,7 @@ public AsyncGPUReadbackRequest Bake(NativeArray inputField, BakeOptions b } // Initialize buffers - var iterationsThisFrame = InitializeBake(inputField, bakeOptions); + var iterationsThisFrame = InitializeBake(inputField, travelCosts, bakeOptions); // Generate integration field PerformIntegration(iterationsThisFrame); @@ -284,7 +310,7 @@ public AsyncGPUReadbackRequest Bake(NativeArray inputField, BakeOptions b // ───────────────────────────────────────────────────────────── // Private Methods // ───────────────────────────────────────────────────────────── - private int InitializeBake(NativeArray inputField, BakeOptions bakeOptions) + private int InitializeBake(NativeArray inputField, NativeArray travelCostsField, BakeOptions bakeOptions) { // Initialize graphics command buffer commandBuffer.Clear(); @@ -312,6 +338,8 @@ private int InitializeBake(NativeArray inputField, BakeOptions bakeOption if (bakeContext.CurrentIteration == 0 && initializeNewBake) { commandBuffer.SetBufferData(integrationFrontBuffer, inputField); + if (useTravelCosts) + commandBuffer.SetBufferData(integrationTravelCostsBuffer, travelCostsField); } return iterationsThisFrame; @@ -322,6 +350,10 @@ private void PerformIntegration(int iterationsThisFrame) commandBuffer.SetComputeIntParam(integrationComputeShader, ShaderProperties.Width, Width); commandBuffer.SetComputeIntParam(integrationComputeShader, ShaderProperties.Height, Height); commandBuffer.SetComputeIntParam(integrationComputeShader, ShaderProperties.DiagonalMovement, bakeContext.Options.DiagonalMovement ? 1 : 0); + commandBuffer.SetComputeIntParam(integrationComputeShader, ShaderProperties.UseTravelCosts, useTravelCosts ? 1 : 0); + + // Assign travel cost buffer + commandBuffer.SetComputeBufferParam(integrationComputeShader, integrationComputeShaderKernel, ShaderProperties.TravelCosts, integrationTravelCostsBuffer); for (var i = 0; i < iterationsThisFrame; i++) { diff --git a/Runtime/Resources/GenerateIntegrationField.compute b/Runtime/Resources/GenerateIntegrationField.compute index ab0a51d..db28b56 100644 --- a/Runtime/Resources/GenerateIntegrationField.compute +++ b/Runtime/Resources/GenerateIntegrationField.compute @@ -5,6 +5,8 @@ uint Width; uint Height; int DiagonalMovement; +int UseTravelCosts; +StructuredBuffer TravelCosts; StructuredBuffer InputCosts; RWStructuredBuffer OutputCosts; @@ -23,14 +25,16 @@ float getMinCost(uint x, uint y) int nx = x + offsets[i].x; int ny = y + offsets[i].y; - if (nx >= 0 && (uint)nx < Width && ny >= 0 && (uint)ny < Height) + if (nx >= 0 && (uint) nx < Width && ny >= 0 && (uint) ny < Height) { bool isDiagonal = i >= 4; if (isDiagonal) { // Both orthogonal must be free - if (InputCosts[flatten(x + offsets[i].x, y, Width)] >= FLT_MAX-1) continue; - if (InputCosts[flatten(x, y + offsets[i].y, Width)] >= FLT_MAX-1) continue; + if (InputCosts[flatten(x + offsets[i].x, y, Width)] >= FLT_MAX - 1) + continue; + if (InputCosts[flatten(x, y + offsets[i].y, Width)] >= FLT_MAX - 1) + continue; } uint neighborIndex = flatten(nx, ny, Width); float neighborCost = InputCosts[neighborIndex]; @@ -41,11 +45,16 @@ float getMinCost(uint x, uint y) if (minCost <= FLT_MIN) // Free minCost = neighborCost + stepCost; else + // minCost = min(minCost, neighborCost + stepCost); minCost = min(minCost, neighborCost + stepCost); } } } + if (UseTravelCosts) + { + minCost = min(FLT_MAX, minCost + TravelCosts[flatten(x, y, Width)]); // Add travel cost + } return minCost; } From 03ec9c61e982a2c846d6f08af6f5921967619a8a Mon Sep 17 00:00:00 2001 From: nathanrun1 Date: Mon, 6 Oct 2025 20:14:55 -0700 Subject: [PATCH 2/3] fix: changed travel costs to only apply to neighbor cost calculation --- Runtime/NativeFlowField.cs | 19 +++++++++++-------- Runtime/Resources/GenerateFlowField.compute | 3 +++ .../GenerateIntegrationField.compute | 8 ++------ 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/Runtime/NativeFlowField.cs b/Runtime/NativeFlowField.cs index ce25047..bc5b662 100644 --- a/Runtime/NativeFlowField.cs +++ b/Runtime/NativeFlowField.cs @@ -111,10 +111,11 @@ private static class ShaderProperties // ───────────────────────────────────────────────────────────── // Private Fields // ───────────────────────────────────────────────────────────── + private ComputeBuffer travelCostsBuffer; + private readonly bool useTravelCosts; + private readonly ComputeShader integrationComputeShader; private readonly int integrationComputeShaderKernel; - private ComputeBuffer integrationTravelCostsBuffer; // NEW - private readonly bool useTravelCosts; private ComputeBuffer integrationFrontBuffer; private ComputeBuffer integrationBackBuffer; @@ -170,12 +171,12 @@ public NativeFlowField(int width, int height, bool generateHeatMap=false, bool u this.generateHeatMap = generateHeatMap; + travelCostsBuffer = new ComputeBuffer(Length, sizeof(float)); + this.useTravelCosts = useTravelCosts; + integrationComputeShader = Resources.Load("GenerateIntegrationField"); integrationComputeShaderKernel = integrationComputeShader.FindKernel("GenerateIntegrationField"); - integrationTravelCostsBuffer = new ComputeBuffer(Length, sizeof(float)); - this.useTravelCosts = useTravelCosts; - integrationFrontBuffer = new ComputeBuffer(Length, sizeof(float)); integrationBackBuffer = new ComputeBuffer(Length, sizeof(float)); @@ -212,7 +213,7 @@ public NativeFlowField(int width, int height, bool generateHeatMap=false, bool u /// public void Dispose() { - integrationTravelCostsBuffer.Dispose(); + travelCostsBuffer.Dispose(); integrationFrontBuffer.Dispose(); integrationBackBuffer.Dispose(); flowFieldBuffer.Dispose(); @@ -339,7 +340,7 @@ private int InitializeBake(NativeArray inputField, NativeArray tra { commandBuffer.SetBufferData(integrationFrontBuffer, inputField); if (useTravelCosts) - commandBuffer.SetBufferData(integrationTravelCostsBuffer, travelCostsField); + commandBuffer.SetBufferData(travelCostsBuffer, travelCostsField); } return iterationsThisFrame; @@ -353,7 +354,7 @@ private void PerformIntegration(int iterationsThisFrame) commandBuffer.SetComputeIntParam(integrationComputeShader, ShaderProperties.UseTravelCosts, useTravelCosts ? 1 : 0); // Assign travel cost buffer - commandBuffer.SetComputeBufferParam(integrationComputeShader, integrationComputeShaderKernel, ShaderProperties.TravelCosts, integrationTravelCostsBuffer); + commandBuffer.SetComputeBufferParam(integrationComputeShader, integrationComputeShaderKernel, ShaderProperties.TravelCosts, travelCostsBuffer); for (var i = 0; i < iterationsThisFrame; i++) { @@ -388,8 +389,10 @@ private void GenerateFlowField() commandBuffer.SetComputeIntParam(generateFlowFieldComputeShader, ShaderProperties.Width, Width); commandBuffer.SetComputeIntParam(generateFlowFieldComputeShader, ShaderProperties.Height, Height); commandBuffer.SetComputeIntParam(generateFlowFieldComputeShader, ShaderProperties.DiagonalMovement, bakeContext.Options.DiagonalMovement ? 1 : 0); + commandBuffer.SetComputeIntParam(integrationComputeShader, ShaderProperties.UseTravelCosts, useTravelCosts ? 1 : 0); commandBuffer.SetComputeBufferParam(generateFlowFieldComputeShader, generateFlowFieldComputeShaderKernel, ShaderProperties.InputCosts, integrationFrontBuffer); commandBuffer.SetComputeBufferParam(generateFlowFieldComputeShader, generateFlowFieldComputeShaderKernel, ShaderProperties.OutputFlowField, flowFieldBuffer); + commandBuffer.SetComputeBufferParam(generateFlowFieldComputeShader, generateFlowFieldComputeShaderKernel, ShaderProperties.TravelCosts, travelCostsBuffer); commandBuffer.DispatchCompute(generateFlowFieldComputeShader, generateFlowFieldComputeShaderKernel, threadGroupsX, threadGroupsY, 1); Graphics.ExecuteCommandBufferAsync(commandBuffer, bakeContext.Options.ComputeQueueType); diff --git a/Runtime/Resources/GenerateFlowField.compute b/Runtime/Resources/GenerateFlowField.compute index b008372..2980452 100644 --- a/Runtime/Resources/GenerateFlowField.compute +++ b/Runtime/Resources/GenerateFlowField.compute @@ -5,6 +5,8 @@ uint Width; uint Height; int DiagonalMovement; +int UseTravelCosts; +StructuredBuffer TravelCosts; StructuredBuffer InputCosts; RWStructuredBuffer OutputFlowField; @@ -36,6 +38,7 @@ int getFlow(uint x, uint y) } uint neighborIndex = flatten(nx, ny, Width); float neighborCost = InputCosts[neighborIndex]; + if (UseTravelCosts) neighborCost += TravelCosts[neighborIndex]; // Add travel cost if (neighborCost < FLT_MAX && neighborCost > FLT_MIN) { diff --git a/Runtime/Resources/GenerateIntegrationField.compute b/Runtime/Resources/GenerateIntegrationField.compute index db28b56..3b60187 100644 --- a/Runtime/Resources/GenerateIntegrationField.compute +++ b/Runtime/Resources/GenerateIntegrationField.compute @@ -25,7 +25,7 @@ float getMinCost(uint x, uint y) int nx = x + offsets[i].x; int ny = y + offsets[i].y; - if (nx >= 0 && (uint) nx < Width && ny >= 0 && (uint) ny < Height) + if (nx >= 0 && (uint)nx < Width && ny >= 0 && (uint)ny < Height) { bool isDiagonal = i >= 4; if (isDiagonal) @@ -38,6 +38,7 @@ float getMinCost(uint x, uint y) } uint neighborIndex = flatten(nx, ny, Width); float neighborCost = InputCosts[neighborIndex]; + if (UseTravelCosts) neighborCost += TravelCosts[neighborIndex]; // Add travel cost if (neighborCost < FLT_MAX && neighborCost > FLT_MIN) { @@ -45,16 +46,11 @@ float getMinCost(uint x, uint y) if (minCost <= FLT_MIN) // Free minCost = neighborCost + stepCost; else - // minCost = min(minCost, neighborCost + stepCost); minCost = min(minCost, neighborCost + stepCost); } } } - if (UseTravelCosts) - { - minCost = min(FLT_MAX, minCost + TravelCosts[flatten(x, y, Width)]); // Add travel cost - } return minCost; } From 6973ea3488ec54433c73edee7d84dd7be2ac8166 Mon Sep 17 00:00:00 2001 From: nathanrun1 Date: Wed, 8 Oct 2025 07:27:28 -0700 Subject: [PATCH 3/3] fix: Wasn't properly setting UseTravelCosts on GenerateFlowField compute shader --- Runtime/NativeFlowField.cs | 6 +++--- Runtime/Resources/GenerateFlowField.compute | 6 ++++-- Runtime/Resources/GenerateIntegrationField.compute | 6 ++++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Runtime/NativeFlowField.cs b/Runtime/NativeFlowField.cs index bc5b662..3240ade 100644 --- a/Runtime/NativeFlowField.cs +++ b/Runtime/NativeFlowField.cs @@ -111,7 +111,7 @@ private static class ShaderProperties // ───────────────────────────────────────────────────────────── // Private Fields // ───────────────────────────────────────────────────────────── - private ComputeBuffer travelCostsBuffer; + private ComputeBuffer travelCostsBuffer; private readonly bool useTravelCosts; private readonly ComputeShader integrationComputeShader; @@ -335,7 +335,7 @@ private int InitializeBake(NativeArray inputField, NativeArray tra ? Mathf.Min(iterationsRemaining, bakeOptions.IterationsPerFrame) : Mathf.Min(iterationsRemaining, bakeOptions.Iterations); - // Initialize compute buffer with input field + // Initialize compute buffer with input field if (bakeContext.CurrentIteration == 0 && initializeNewBake) { commandBuffer.SetBufferData(integrationFrontBuffer, inputField); @@ -389,7 +389,7 @@ private void GenerateFlowField() commandBuffer.SetComputeIntParam(generateFlowFieldComputeShader, ShaderProperties.Width, Width); commandBuffer.SetComputeIntParam(generateFlowFieldComputeShader, ShaderProperties.Height, Height); commandBuffer.SetComputeIntParam(generateFlowFieldComputeShader, ShaderProperties.DiagonalMovement, bakeContext.Options.DiagonalMovement ? 1 : 0); - commandBuffer.SetComputeIntParam(integrationComputeShader, ShaderProperties.UseTravelCosts, useTravelCosts ? 1 : 0); + commandBuffer.SetComputeIntParam(generateFlowFieldComputeShader, ShaderProperties.UseTravelCosts, useTravelCosts ? 1 : 0); commandBuffer.SetComputeBufferParam(generateFlowFieldComputeShader, generateFlowFieldComputeShaderKernel, ShaderProperties.InputCosts, integrationFrontBuffer); commandBuffer.SetComputeBufferParam(generateFlowFieldComputeShader, generateFlowFieldComputeShaderKernel, ShaderProperties.OutputFlowField, flowFieldBuffer); commandBuffer.SetComputeBufferParam(generateFlowFieldComputeShader, generateFlowFieldComputeShaderKernel, ShaderProperties.TravelCosts, travelCostsBuffer); diff --git a/Runtime/Resources/GenerateFlowField.compute b/Runtime/Resources/GenerateFlowField.compute index 2980452..97d7ced 100644 --- a/Runtime/Resources/GenerateFlowField.compute +++ b/Runtime/Resources/GenerateFlowField.compute @@ -37,8 +37,10 @@ int getFlow(uint x, uint y) if (InputCosts[flatten(x, y + offsets[i].y, Width)] >= FLT_MAX-1) continue; } uint neighborIndex = flatten(nx, ny, Width); - float neighborCost = InputCosts[neighborIndex]; - if (UseTravelCosts) neighborCost += TravelCosts[neighborIndex]; // Add travel cost + float neighborCost; + + if (UseTravelCosts) neighborCost = InputCosts[neighborIndex] + TravelCosts[neighborIndex]; // Add travel cost + else neighborCost = InputCosts[neighborIndex]; if (neighborCost < FLT_MAX && neighborCost > FLT_MIN) { diff --git a/Runtime/Resources/GenerateIntegrationField.compute b/Runtime/Resources/GenerateIntegrationField.compute index 3b60187..eecfa64 100644 --- a/Runtime/Resources/GenerateIntegrationField.compute +++ b/Runtime/Resources/GenerateIntegrationField.compute @@ -37,8 +37,10 @@ float getMinCost(uint x, uint y) continue; } uint neighborIndex = flatten(nx, ny, Width); - float neighborCost = InputCosts[neighborIndex]; - if (UseTravelCosts) neighborCost += TravelCosts[neighborIndex]; // Add travel cost + + float neighborCost; + if (UseTravelCosts) neighborCost = InputCosts[neighborIndex] + TravelCosts[neighborIndex]; // Add travel cost + else neighborCost = InputCosts[neighborIndex]; if (neighborCost < FLT_MAX && neighborCost > FLT_MIN) {