diff --git a/Runtime/NativeFlowField.cs b/Runtime/NativeFlowField.cs
index b9bec6d..3240ade 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");
@@ -108,6 +111,9 @@ private static class ShaderProperties
// ─────────────────────────────────────────────────────────────
// Private Fields
// ─────────────────────────────────────────────────────────────
+ private ComputeBuffer travelCostsBuffer;
+ private readonly bool useTravelCosts;
+
private readonly ComputeShader integrationComputeShader;
private readonly int integrationComputeShaderKernel;
private ComputeBuffer integrationFrontBuffer;
@@ -148,7 +154,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)
{
@@ -165,6 +171,9 @@ public NativeFlowField(int width, int height, bool generateHeatMap=false, int mi
this.generateHeatMap = generateHeatMap;
+ travelCostsBuffer = new ComputeBuffer(Length, sizeof(float));
+ this.useTravelCosts = useTravelCosts;
+
integrationComputeShader = Resources.Load("GenerateIntegrationField");
integrationComputeShaderKernel = integrationComputeShader.FindKernel("GenerateIntegrationField");
@@ -204,6 +213,7 @@ public NativeFlowField(int width, int height, bool generateHeatMap=false, int mi
///
public void Dispose()
{
+ travelCostsBuffer.Dispose();
integrationFrontBuffer.Dispose();
integrationBackBuffer.Dispose();
flowFieldBuffer.Dispose();
@@ -223,6 +233,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 +253,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 +273,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 +311,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();
@@ -308,10 +335,12 @@ private int InitializeBake(NativeArray inputField, BakeOptions bakeOption
? 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);
+ if (useTravelCosts)
+ commandBuffer.SetBufferData(travelCostsBuffer, travelCostsField);
}
return iterationsThisFrame;
@@ -322,6 +351,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, travelCostsBuffer);
for (var i = 0; i < iterationsThisFrame; i++)
{
@@ -356,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(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);
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..97d7ced 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;
@@ -35,7 +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];
+ 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 ab0a51d..eecfa64 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;
@@ -29,11 +31,16 @@ float getMinCost(uint x, uint y)
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];
+
+ float neighborCost;
+ if (UseTravelCosts) neighborCost = InputCosts[neighborIndex] + TravelCosts[neighborIndex]; // Add travel cost
+ else neighborCost = InputCosts[neighborIndex];
if (neighborCost < FLT_MAX && neighborCost > FLT_MIN)
{