142 lines
5.9 KiB
C#

using UnityEngine;
using System.Collections.Generic;
using UnityEngine.Profiling;
namespace Pathfinding {
using Pathfinding.Pooling;
using Unity.Collections.LowLevel.Unsafe;
/// <summary>
/// Movement script for curved worlds.
/// This script inherits from AIPath, but adjusts its movement plane every frame using the ground normal.
/// </summary>
[HelpURL("https://arongranberg.com/astar/documentation/stable/aipathalignedtosurface.html")]
public class AIPathAlignedToSurface : AIPath {
/// <summary>Scratch dictionary used to avoid allocations every frame</summary>
static readonly Dictionary<Mesh, int> scratchDictionary = new Dictionary<Mesh, int>();
protected override void OnEnable () {
base.OnEnable();
movementPlane = new Util.SimpleMovementPlane(rotation);
}
protected override void ApplyGravity (float deltaTime) {
// Apply gravity
if (usingGravity) {
// Gravity is relative to the current surface.
// Only the normal direction is well defined however so x and z are ignored.
verticalVelocity += deltaTime * (float.IsNaN(gravity.x) ? Physics.gravity.y : gravity.y);
} else {
verticalVelocity = 0;
}
}
/// <summary>
/// Calculates smoothly interpolated normals for all raycast hits and uses that to set the movement planes of the agents.
///
/// To support meshes that change at any time, we use Mesh.AcquireReadOnlyMeshData to get a read-only view of the mesh data.
/// This is only efficient if we batch all updates and make a single call to Mesh.AcquireReadOnlyMeshData.
///
/// This method is quite convoluted due to having to read the raw vertex data streams from unity meshes to avoid allocations.
/// </summary>
public static void UpdateMovementPlanes (AIPathAlignedToSurface[] components, int count) {
Profiler.BeginSample("UpdateMovementPlanes");
var meshes = ListPool<Mesh>.Claim();
var componentsByMesh = new List<List<AIPathAlignedToSurface> >();
var meshToIndex = scratchDictionary;
for (int i = 0; i < count; i++) {
var c = components[i].lastRaycastHit.collider;
// triangleIndex can be -1 if the mesh collider is convex, and the raycast started inside it.
// This is not a documented behavior, but it seems to happen in practice.
if (c is MeshCollider mc && components[i].lastRaycastHit.triangleIndex != -1) {
var sharedMesh = mc.sharedMesh;
if (meshToIndex.TryGetValue(sharedMesh, out var meshIndex)) {
componentsByMesh[meshIndex].Add(components[i]);
} else if (sharedMesh != null && sharedMesh.isReadable) {
meshToIndex[sharedMesh] = meshes.Count;
meshes.Add(sharedMesh);
componentsByMesh.Add(ListPool<AIPathAlignedToSurface>.Claim());
componentsByMesh[meshes.Count-1].Add(components[i]);
} else {
// Unreadable mesh
components[i].SetInterpolatedNormal(components[i].lastRaycastHit.normal);
}
} else {
// Not a mesh collider, or the triangle index was -1
components[i].SetInterpolatedNormal(components[i].lastRaycastHit.normal);
}
}
var meshDatas = Mesh.AcquireReadOnlyMeshData(meshes);
for (int i = 0; i < meshes.Count; i++) {
var m = meshes[i];
var meshIndex = meshToIndex[m];
var meshData = meshDatas[meshIndex];
var componentsForMesh = componentsByMesh[meshIndex];
var stream = meshData.GetVertexAttributeStream(UnityEngine.Rendering.VertexAttribute.Normal);
if (stream == -1) {
// Mesh does not have normals
for (int j = 0; j < componentsForMesh.Count; j++) componentsForMesh[j].SetInterpolatedNormal(componentsForMesh[j].lastRaycastHit.normal);
continue;
}
var vertexData = meshData.GetVertexData<byte>(stream);
var stride = meshData.GetVertexBufferStride(stream);
var normalOffset = meshData.GetVertexAttributeOffset(UnityEngine.Rendering.VertexAttribute.Normal);
unsafe {
var normals = (byte*)vertexData.GetUnsafeReadOnlyPtr() + normalOffset;
for (int j = 0; j < componentsForMesh.Count; j++) {
var comp = componentsForMesh[j];
var hit = comp.lastRaycastHit;
int t0, t1, t2;
// Get the vertex indices corresponding to the triangle that was hit
if (meshData.indexFormat == UnityEngine.Rendering.IndexFormat.UInt16) {
var indices = meshData.GetIndexData<ushort>();
t0 = indices[hit.triangleIndex * 3 + 0];
t1 = indices[hit.triangleIndex * 3 + 1];
t2 = indices[hit.triangleIndex * 3 + 2];
} else {
var indices = meshData.GetIndexData<int>();
t0 = indices[hit.triangleIndex * 3 + 0];
t1 = indices[hit.triangleIndex * 3 + 1];
t2 = indices[hit.triangleIndex * 3 + 2];
}
// Get the normals corresponding to the 3 vertices
var n0 = *((Vector3*)(normals + t0 * stride));
var n1 = *((Vector3*)(normals + t1 * stride));
var n2 = *((Vector3*)(normals + t2 * stride));
// Interpolate the normal using the barycentric coordinates
Vector3 baryCenter = hit.barycentricCoordinate;
Vector3 interpolatedNormal = n0 * baryCenter.x + n1 * baryCenter.y + n2 * baryCenter.z;
interpolatedNormal = interpolatedNormal.normalized;
Transform hitTransform = hit.collider.transform;
interpolatedNormal = hitTransform.TransformDirection(interpolatedNormal);
comp.SetInterpolatedNormal(interpolatedNormal);
}
}
}
meshDatas.Dispose();
for (int i = 0; i < componentsByMesh.Count; i++) ListPool<AIPathAlignedToSurface>.Release(componentsByMesh[i]);
ListPool<Mesh>.Release(ref meshes);
scratchDictionary.Clear();
Profiler.EndSample();
}
void SetInterpolatedNormal (Vector3 normal) {
if (normal != Vector3.zero) {
var fwd = Vector3.Cross(movementPlane.rotation * Vector3.right, normal);
movementPlane = new Util.SimpleMovementPlane(Quaternion.LookRotation(fwd, normal));
}
if (rvoController != null) rvoController.movementPlane = movementPlane;
}
protected override void UpdateMovementPlane () {
// The UpdateMovementPlanes method will take care of this
}
}
}