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) {