diff --git a/Core/Framework/BlackboardEntity.cs b/Core/Framework/BlackboardEntity.cs index 830e2c1..8aa47d4 100644 --- a/Core/Framework/BlackboardEntity.cs +++ b/Core/Framework/BlackboardEntity.cs @@ -10,7 +10,7 @@ public unsafe struct BlackboardEntity { private Entity entity; private LatiosWorldUnmanaged latiosWorld; - private EntityManager em => latiosWorld.m_impl->m_worldUnmanaged.EntityManager; + internal EntityManager em => latiosWorld.m_impl->m_worldUnmanaged.EntityManager; /// /// Create a blackboard entity diff --git a/Core/Framework/ICustomEditorBootstrap.cs b/Core/Framework/ICustomEditorBootstrap.cs index 2cf6f56..1029c69 100644 --- a/Core/Framework/ICustomEditorBootstrap.cs +++ b/Core/Framework/ICustomEditorBootstrap.cs @@ -70,7 +70,7 @@ internal static void InitializeEditorWorld(World defaultEditorWorld) #if UNITY_EDITOR public static class UnityEditorTool { - [UnityEditor.MenuItem("Edit/Restart Editor World")] + [UnityEditor.MenuItem("Edit/Latios/Restart Editor World")] public static void RestartEditorWorld() { var previousEditorWorld = World.DefaultGameObjectInjectionWorld; diff --git a/Core/GameplayToolkit/Bits.cs b/Core/GameplayToolkit/Bits.cs new file mode 100644 index 0000000..671471b --- /dev/null +++ b/Core/GameplayToolkit/Bits.cs @@ -0,0 +1,101 @@ +using Unity.Collections; +using Unity.Entities; +using Unity.Mathematics; + +namespace Latios +{ + public static class Bits + { + public static bool GetBit(int data, int bitIndex) => new BitField32(math.asuint(data)).IsSet(bitIndex); + public static void SetBit(ref int data, int bitIndex, bool bitValue) + { + var bitfield = new BitField32(math.asuint(data)); + bitfield.SetBits(bitIndex, bitValue); + data = math.asint(bitfield.Value); + } + public static bool GetBit(uint data, int bitIndex) => new BitField32(data).IsSet(bitIndex); + public static void SetBit(ref uint data, int bitIndex, bool bitValue) + { + var bitfield = new BitField32(data); + bitfield.SetBits(bitIndex, bitValue); + data = bitfield.Value; + } + public static bool GetBit(long data, int bitIndex) => new BitField64(math.asulong(data)).IsSet(bitIndex); + public static void SetBit(ref long data, int bitIndex, bool bitValue) + { + var bitfield = new BitField64(math.asulong(data)); + bitfield.SetBits(bitIndex, bitValue); + data = math.aslong(bitfield.Value); + } + public static bool GetBit(ulong data, int bitIndex) => new BitField64(data).IsSet(bitIndex); + public static void SetBit(ref ulong data, int bitIndex, bool bitValue) + { + var bitfield = new BitField64(data); + bitfield.SetBits(bitIndex, bitValue); + data = bitfield.Value; + } + public static bool GetBit(ushort data, int bitIndex) => GetBit((uint)data, bitIndex); + public static void SetBit(ref ushort data, int bitIndex, bool bitValue) + { + uint intdata = data; + SetBit(ref intdata, bitIndex, bitValue); + data = (ushort)(intdata & 0xffff); + } + public static bool GetBit(byte data, int bitIndex) => GetBit((uint)data, bitIndex); + public static void SetBit(ref byte data, int bitIndex, bool bitValue) + { + uint intdata = data; + SetBit(ref intdata, bitIndex, bitValue); + data = (byte)(intdata & 0xff); + } + + public static int GetBits(int data, int firstBitIndex, int bitCount) + => math.asint(new BitField32(math.asuint(data)).GetBits(firstBitIndex, bitCount)); + public static void SetBits(ref int data, int firstBitIndex, int bitCount, int newValue) + { + uint udata = math.asuint(data); + SetBits(ref udata, firstBitIndex, bitCount, math.asuint(newValue)); + data = math.asint(udata); + } + public static uint GetBits(uint data, int firstBitIndex, int bitCount) => new BitField32(data).GetBits(firstBitIndex, bitCount); + public static void SetBits(ref uint data, int firstBitIndex, int bitCount, uint newValue) + { + var mask = 0xffffffffu >> (32 - bitCount); + var newPart = (newValue & mask) << firstBitIndex; + var oldPart = data & ~(mask << firstBitIndex); + data = newPart | oldPart; + } + public static long GetBits(long data, int firstBitIndex, int bitCount) + => math.aslong(new BitField64(math.asulong(data)).GetBits(firstBitIndex, bitCount)); + public static void SetBits(ref long data, int firstBitIndex, int bitCount, long newValue) + { + ulong udata = math.asulong(data); + SetBits(ref udata, firstBitIndex, bitCount, math.asulong(newValue)); + data = math.aslong(udata); + } + public static ulong GetBits(ulong data, int firstBitIndex, int bitCount) => new BitField64(data).GetBits(firstBitIndex, bitCount); + public static void SetBits(ref ulong data, int firstBitIndex, int bitCount, ulong newValue) + { + var mask = (~0x0u) >> (64 - bitCount); + var newPart = (newValue & mask) << firstBitIndex; + var oldPart = data & ~(mask << firstBitIndex); + data = newPart | oldPart; + } + public static ushort GetBits(ushort data, int firstBitIndex, int bitCount) + => (ushort)(0xffff & GetBits((uint)data, firstBitIndex, bitCount)); + public static void SetBits(ref ushort data, int firstBitIndex, int bitCount, ushort newValue) + { + uint intdata = data; + SetBits(ref intdata, firstBitIndex, bitCount, newValue); + data = (ushort)(intdata & 0xffff); + } + public static byte GetBits(byte data, int firstBitIndex, int bitCount) + => (byte)(0xff & GetBits((uint)data, firstBitIndex, bitCount)); + public static void SetBits(ref byte data, int firstBitIndex, int bitCount, byte newValue) + { + uint intdata = data; + SetBits(ref intdata, firstBitIndex, bitCount, newValue); + data = (byte)(intdata & 0xff); + } + } +} \ No newline at end of file diff --git a/Core/GameplayToolkit/Bits.cs.meta b/Core/GameplayToolkit/Bits.cs.meta new file mode 100644 index 0000000..824d229 --- /dev/null +++ b/Core/GameplayToolkit/Bits.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: de6b783df48278547a3b2ab91fded914 \ No newline at end of file diff --git a/Core/GameplayToolkit/TypePack.cs b/Core/GameplayToolkit/TypePack.cs new file mode 100644 index 0000000..9e55491 --- /dev/null +++ b/Core/GameplayToolkit/TypePack.cs @@ -0,0 +1,292 @@ +using Unity.Collections; +using Unity.Entities; +using Unity.Mathematics; + +namespace Latios +{ + public struct TypePack + { + public static implicit operator FixedList128Bytes(TypePack pack) + { + return new FixedList128Bytes() + { + ComponentType.ReadOnly() + }; + } + public static implicit operator ComponentTypeSet(TypePack pack) => new ComponentTypeSet(ComponentType.ReadOnly()); + } + + public struct TypePack + { + public static implicit operator FixedList128Bytes(TypePack pack) + { + return new FixedList128Bytes() + { + ComponentType.ReadOnly(), + ComponentType.ReadOnly() + }; + } + public static implicit operator ComponentTypeSet(TypePack pack) + { + return new ComponentTypeSet(ComponentType.ReadOnly(), ComponentType.ReadOnly()); + } + } + + public struct TypePack + { + public static implicit operator FixedList128Bytes(TypePack pack) + { + return new FixedList128Bytes() + { + ComponentType.ReadOnly(), + ComponentType.ReadOnly(), + ComponentType.ReadOnly() + }; + } + public static implicit operator ComponentTypeSet(TypePack pack) + { + return new ComponentTypeSet(ComponentType.ReadOnly(), + ComponentType.ReadOnly(), + ComponentType.ReadOnly()); + } + } + + public struct TypePack + { + public static implicit operator FixedList128Bytes(TypePack pack) + { + return new FixedList128Bytes() + { + ComponentType.ReadOnly(), + ComponentType.ReadOnly(), + ComponentType.ReadOnly(), + ComponentType.ReadOnly() + }; + } + public static implicit operator ComponentTypeSet(TypePack pack) + { + return new ComponentTypeSet(ComponentType.ReadOnly(), + ComponentType.ReadOnly(), + ComponentType.ReadOnly(), + ComponentType.ReadOnly()); + } + } + + public struct TypePack + { + public static implicit operator FixedList128Bytes(TypePack pack) + { + return new FixedList128Bytes() + { + ComponentType.ReadOnly(), + ComponentType.ReadOnly(), + ComponentType.ReadOnly(), + ComponentType.ReadOnly(), + ComponentType.ReadOnly() + }; + } + public static implicit operator ComponentTypeSet(TypePack pack) + { + return new ComponentTypeSet(ComponentType.ReadOnly(), + ComponentType.ReadOnly(), + ComponentType.ReadOnly(), + ComponentType.ReadOnly(), + ComponentType.ReadOnly()); + } + } + + public static class AddComponentExtensions + { + public static void AddComponents(this EntityManager entityManager, Entity entity, in T0 c0, in T1 c1) + where T0 : unmanaged, IComponentData + where T1 : unmanaged, IComponentData + { + entityManager.AddComponent(entity, new TypePack()); + entityManager.SetComponentData(entity, c0); + entityManager.SetComponentData(entity, c1); + } + + public static void AddComponents(this EntityManager entityManager, Entity entity, in T0 c0, in T1 c1, in T2 c2) + where T0 : unmanaged, IComponentData + where T1 : unmanaged, IComponentData + where T2 : unmanaged, IComponentData + { + entityManager.AddComponent(entity, new TypePack()); + entityManager.SetComponentData(entity, c0); + entityManager.SetComponentData(entity, c1); + entityManager.SetComponentData(entity, c2); + } + + public static void AddComponents(this EntityManager entityManager, Entity entity, in T0 c0, in T1 c1, in T2 c2, in T3 c3) + where T0 : unmanaged, IComponentData + where T1 : unmanaged, IComponentData + where T2 : unmanaged, IComponentData + where T3 : unmanaged, IComponentData + { + entityManager.AddComponent(entity, new TypePack()); + entityManager.SetComponentData(entity, c0); + entityManager.SetComponentData(entity, c1); + entityManager.SetComponentData(entity, c2); + entityManager.SetComponentData(entity, c3); + } + + public static void AddComponents(this EntityManager entityManager, Entity entity, in T0 c0, in T1 c1, in T2 c2, in T3 c3, in T4 c4) + where T0 : unmanaged, IComponentData + where T1 : unmanaged, IComponentData + where T2 : unmanaged, IComponentData + where T3 : unmanaged, IComponentData + where T4 : unmanaged, IComponentData + { + entityManager.AddComponent(entity, new TypePack()); + entityManager.SetComponentData(entity, c0); + entityManager.SetComponentData(entity, c1); + entityManager.SetComponentData(entity, c2); + entityManager.SetComponentData(entity, c3); + entityManager.SetComponentData(entity, c4); + } + + public static void AddComponents(this EntityCommandBuffer entityCommandBuffer, Entity entity, in T0 c0, in T1 c1) + where T0 : unmanaged, IComponentData + where T1 : unmanaged, IComponentData + { + entityCommandBuffer.AddComponent(entity, new TypePack()); + entityCommandBuffer.SetComponent(entity, c0); + entityCommandBuffer.SetComponent(entity, c1); + } + + public static void AddComponents(this EntityCommandBuffer entityCommandBuffer, Entity entity, in T0 c0, in T1 c1, in T2 c2) + where T0 : unmanaged, IComponentData + where T1 : unmanaged, IComponentData + where T2 : unmanaged, IComponentData + { + entityCommandBuffer.AddComponent(entity, new TypePack()); + entityCommandBuffer.SetComponent(entity, c0); + entityCommandBuffer.SetComponent(entity, c1); + entityCommandBuffer.SetComponent(entity, c2); + } + + public static void AddComponents(this EntityCommandBuffer entityCommandBuffer, Entity entity, in T0 c0, in T1 c1, in T2 c2, in T3 c3) + where T0 : unmanaged, IComponentData + where T1 : unmanaged, IComponentData + where T2 : unmanaged, IComponentData + where T3 : unmanaged, IComponentData + { + entityCommandBuffer.AddComponent(entity, new TypePack()); + entityCommandBuffer.SetComponent(entity, c0); + entityCommandBuffer.SetComponent(entity, c1); + entityCommandBuffer.SetComponent(entity, c2); + entityCommandBuffer.SetComponent(entity, c3); + } + + public static void AddComponents(this EntityCommandBuffer entityCommandBuffer, Entity entity, in T0 c0, in T1 c1, in T2 c2, in T3 c3, in T4 c4) + where T0 : unmanaged, IComponentData + where T1 : unmanaged, IComponentData + where T2 : unmanaged, IComponentData + where T3 : unmanaged, IComponentData + where T4 : unmanaged, IComponentData + { + entityCommandBuffer.AddComponent(entity, new TypePack()); + entityCommandBuffer.SetComponent(entity, c0); + entityCommandBuffer.SetComponent(entity, c1); + entityCommandBuffer.SetComponent(entity, c2); + entityCommandBuffer.SetComponent(entity, c3); + entityCommandBuffer.SetComponent(entity, c4); + } + + public static void AddComponents(this EntityCommandBuffer.ParallelWriter entityCommandBuffer, int sortKey, Entity entity, in T0 c0, in T1 c1) +where T0 : unmanaged, IComponentData +where T1 : unmanaged, IComponentData + { + entityCommandBuffer.AddComponent(sortKey, entity, new TypePack()); + entityCommandBuffer.SetComponent(sortKey, entity, c0); + entityCommandBuffer.SetComponent(sortKey, entity, c1); + } + + public static void AddComponents(this EntityCommandBuffer.ParallelWriter entityCommandBuffer, int sortKey, Entity entity, in T0 c0, in T1 c1, in T2 c2) + where T0 : unmanaged, IComponentData + where T1 : unmanaged, IComponentData + where T2 : unmanaged, IComponentData + { + entityCommandBuffer.AddComponent(sortKey, entity, new TypePack()); + entityCommandBuffer.SetComponent(sortKey, entity, c0); + entityCommandBuffer.SetComponent(sortKey, entity, c1); + entityCommandBuffer.SetComponent(sortKey, entity, c2); + } + + public static void AddComponents(this EntityCommandBuffer.ParallelWriter entityCommandBuffer, int sortKey, Entity entity, in T0 c0, in T1 c1, in T2 c2, in T3 c3) + where T0 : unmanaged, IComponentData + where T1 : unmanaged, IComponentData + where T2 : unmanaged, IComponentData + where T3 : unmanaged, IComponentData + { + entityCommandBuffer.AddComponent(sortKey, entity, new TypePack()); + entityCommandBuffer.SetComponent(sortKey, entity, c0); + entityCommandBuffer.SetComponent(sortKey, entity, c1); + entityCommandBuffer.SetComponent(sortKey, entity, c2); + entityCommandBuffer.SetComponent(sortKey, entity, c3); + } + + public static void AddComponents(this EntityCommandBuffer.ParallelWriter entityCommandBuffer, int sortKey, Entity entity, in T0 c0, in T1 c1, in T2 c2, in T3 c3, in T4 c4) + where T0 : unmanaged, IComponentData + where T1 : unmanaged, IComponentData + where T2 : unmanaged, IComponentData + where T3 : unmanaged, IComponentData + where T4 : unmanaged, IComponentData + { + entityCommandBuffer.AddComponent(sortKey, entity, new TypePack()); + entityCommandBuffer.SetComponent(sortKey, entity, c0); + entityCommandBuffer.SetComponent(sortKey, entity, c1); + entityCommandBuffer.SetComponent(sortKey, entity, c2); + entityCommandBuffer.SetComponent(sortKey, entity, c3); + entityCommandBuffer.SetComponent(sortKey, entity, c4); + } + + public static void AddComponents(this BlackboardEntity blackboardEntity, in T0 c0, in T1 c1) + where T0 : unmanaged, IComponentData + where T1 : unmanaged, IComponentData + { + blackboardEntity.em.AddComponent(blackboardEntity, new TypePack()); + blackboardEntity.em.SetComponentData(blackboardEntity, c0); + blackboardEntity.em.SetComponentData(blackboardEntity, c1); + } + + public static void AddComponents(this BlackboardEntity blackboardEntity, in T0 c0, in T1 c1, in T2 c2) + where T0 : unmanaged, IComponentData + where T1 : unmanaged, IComponentData + where T2 : unmanaged, IComponentData + { + blackboardEntity.em.AddComponent(blackboardEntity, new TypePack()); + blackboardEntity.em.SetComponentData(blackboardEntity, c0); + blackboardEntity.em.SetComponentData(blackboardEntity, c1); + blackboardEntity.em.SetComponentData(blackboardEntity, c2); + } + + public static void AddComponents(this BlackboardEntity blackboardEntity, in T0 c0, in T1 c1, in T2 c2, in T3 c3) + where T0 : unmanaged, IComponentData + where T1 : unmanaged, IComponentData + where T2 : unmanaged, IComponentData + where T3 : unmanaged, IComponentData + { + blackboardEntity.em.AddComponent(blackboardEntity, new TypePack()); + blackboardEntity.em.SetComponentData(blackboardEntity, c0); + blackboardEntity.em.SetComponentData(blackboardEntity, c1); + blackboardEntity.em.SetComponentData(blackboardEntity, c2); + blackboardEntity.em.SetComponentData(blackboardEntity, c3); + } + + public static void AddComponents(this BlackboardEntity blackboardEntity, in T0 c0, in T1 c1, in T2 c2, in T3 c3, in T4 c4) + where T0 : unmanaged, IComponentData + where T1 : unmanaged, IComponentData + where T2 : unmanaged, IComponentData + where T3 : unmanaged, IComponentData + where T4 : unmanaged, IComponentData + { + blackboardEntity.em.AddComponent(blackboardEntity, new TypePack()); + blackboardEntity.em.SetComponentData(blackboardEntity, c0); + blackboardEntity.em.SetComponentData(blackboardEntity, c1); + blackboardEntity.em.SetComponentData(blackboardEntity, c2); + blackboardEntity.em.SetComponentData(blackboardEntity, c3); + blackboardEntity.em.SetComponentData(blackboardEntity, c4); + } + } +} \ No newline at end of file diff --git a/Core/GameplayToolkit/TypePack.cs.meta b/Core/GameplayToolkit/TypePack.cs.meta new file mode 100644 index 0000000..4c56445 --- /dev/null +++ b/Core/GameplayToolkit/TypePack.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d3a36876dd109c94a8e8034bf6b7208c \ No newline at end of file diff --git a/Core/ShaderLibrary.meta b/Core/ShaderLibrary.meta new file mode 100644 index 0000000..86b561e --- /dev/null +++ b/Core/ShaderLibrary.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8537e8da4ec62c94a9e6ef496f35eabf +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Core/ShaderLibrary/RigidTransform.hlsl b/Core/ShaderLibrary/RigidTransform.hlsl new file mode 100644 index 0000000..95641c1 --- /dev/null +++ b/Core/ShaderLibrary/RigidTransform.hlsl @@ -0,0 +1,57 @@ +#include "Packages/com.latios.latiosframework/Core/ShaderLibrary/quaternion.hlsl" +//#include "quaternion.hlsl" + +struct RigidTransform +{ + quaternion rot; + float3 pos; +}; + +RigidTransform new_RigidTransform(quaternion newRot, float3 newPos) +{ + RigidTransform result; + result.rot = newRot; + result.pos = newPos; + return result; +} + +RigidTransform new_RigidTransform(float3x3 newRot, float3 newPos) +{ + return new_RigidTransform(new_quaternion(newRot), newPos); +} + +RigidTransform new_RigidTransform(float4x4 m) +{ + return new_RigidTransform(new_quaternion(m), m._m03_m13_m23); +} + +#define RigidTransform_identity new_RigidTransform(quaternion_identity, float3(0.0, 0.0, 0.0)) + +// Todo: Do we need the rotation-only and translation-only overloads? + +RigidTransform inverse(RigidTransform t) +{ + quaternion invRotation = inverse(t.rot); + float3 invTranslation = mul(invRotation, -t.pos); + return new_RigidTransform(invRotation, invTranslation); +} + +RigidTransform mul(RigidTransform a, RigidTransform b) +{ + return new_RigidTransform(mul(a.rot, b.rot), mul(a.rot, b.pos) + a.pos); +} + +float4 mul(RigidTransform a, float4 pos) +{ + return float4(mul(a.rot, pos.xyz) + a.pos * pos.w, pos.w); +} + +float3 rotate(RigidTransform a, float3 dir) +{ + return mul(a.rot, dir); +} + +float3 transform(RigidTransform a, float3 pos) +{ + return mul(a.rot, pos) + a.pos; +} \ No newline at end of file diff --git a/Core/ShaderLibrary/RigidTransform.hlsl.meta b/Core/ShaderLibrary/RigidTransform.hlsl.meta new file mode 100644 index 0000000..c2f88b1 --- /dev/null +++ b/Core/ShaderLibrary/RigidTransform.hlsl.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: e43e062b3ffb6bc479197902e19f96ce +ShaderIncludeImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Core/ShaderLibrary/math_extras.hlsl b/Core/ShaderLibrary/math_extras.hlsl new file mode 100644 index 0000000..f6f0909 --- /dev/null +++ b/Core/ShaderLibrary/math_extras.hlsl @@ -0,0 +1,22 @@ +#define FLT_MIN_NORMAL 1.175494351e-38F + +float4 select(float4 a, float4 b, bool c) +{ + return c ? b : a; +} + +float3 select(float3 a, float3 b, bool c) +{ + return c ? b : a; +} + +float4 chgsign(float4 x, float4 y) +{ + return asfloat(asuint(x) ^ (asuint(y) & 0x80000000)); +} + +float3 normalizesafe(float3 x) +{ + float len = dot(x, x); + return select(float3(0.0, 0.0, 0.0), x * rsqrt(len), len > FLT_MIN_NORMAL); +} \ No newline at end of file diff --git a/Core/ShaderLibrary/math_extras.hlsl.meta b/Core/ShaderLibrary/math_extras.hlsl.meta new file mode 100644 index 0000000..2e4e867 --- /dev/null +++ b/Core/ShaderLibrary/math_extras.hlsl.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 54bddc90cd85b3948b73777304247698 +ShaderIncludeImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Core/ShaderLibrary/quaternion.hlsl b/Core/ShaderLibrary/quaternion.hlsl new file mode 100644 index 0000000..f3da975 --- /dev/null +++ b/Core/ShaderLibrary/quaternion.hlsl @@ -0,0 +1,397 @@ +//#include "math_extras.hlsl" +#include "Packages/com.latios.latiosframework/Core/ShaderLibrary/math_extras.hlsl" + +struct quaternion +{ + float4 value; +}; + +quaternion new_quaternion(float x, float y, float z, float w) +{ + quaternion result; + result.value = float4(x, y, z, w); + return result; +} + +quaternion new_quaternion(float4 newValue) +{ + quaternion result; + result.value = newValue; + return result; +} + +quaternion new_quaternion(float3x3 m) +{ + float3 u = m._m00_m10_m20; //m.c0; + float3 v = m._m01_m11_m21; //m.c1; + float3 w = m._m02_m12_m22; //m.c2; + + uint u_sign = (asuint(u.x) & 0x80000000); + float t = v.y + asfloat(asuint(w.z) ^ u_sign); + uint4 u_mask = uint4((int) u_sign >> 31); + uint4 t_mask = uint4(asint(t) >> 31); + + float tr = 1.0f + abs(u.x); + + uint4 sign_flips = uint4(0x00000000, 0x80000000, 0x80000000, 0x80000000) ^ (u_mask & uint4(0x00000000, 0x80000000, 0x00000000, 0x80000000)) ^ (t_mask & uint4(0x80000000, 0x80000000, 0x80000000, 0x00000000)); + + float4 value = float4(tr, u.y, w.x, v.z) + asfloat(asuint(float4(t, v.x, u.z, w.y)) ^ sign_flips); // +---, +++-, ++-+, +-++ + + value = asfloat((asuint(value) & ~u_mask) | (asuint(value.zwxy) & u_mask)); + value = asfloat((asuint(value.wzyx) & ~t_mask) | (asuint(value) & t_mask)); + value = normalize(value); + return new_quaternion(value); +} + +quaternion new_quaternion(float3x4 m) +{ + float3 u = m._m00_m10_m20; //m.c0; + float3 v = m._m01_m11_m21; //m.c1; + float3 w = m._m02_m12_m22; //m.c2; + + uint u_sign = (asuint(u.x) & 0x80000000); + float t = v.y + asfloat(asuint(w.z) ^ u_sign); + uint4 u_mask = uint4((int) u_sign >> 31); + uint4 t_mask = uint4(asint(t) >> 31); + + float tr = 1.0f + abs(u.x); + + uint4 sign_flips = uint4(0x00000000, 0x80000000, 0x80000000, 0x80000000) ^ (u_mask & uint4(0x00000000, 0x80000000, 0x00000000, 0x80000000)) ^ (t_mask & uint4(0x80000000, 0x80000000, 0x80000000, 0x00000000)); + + float4 value = float4(tr, u.y, w.x, v.z) + asfloat(asuint(float4(t, v.x, u.z, w.y)) ^ sign_flips); // +---, +++-, ++-+, +-++ + + value = asfloat((asuint(value) & ~u_mask) | (asuint(value.zwxy) & u_mask)); + value = asfloat((asuint(value.wzyx) & ~t_mask) | (asuint(value) & t_mask)); + value = normalize(value); + return new_quaternion(value); +} + +quaternion new_quaternion(float4x4 m) +{ + float3 u = m._m00_m10_m20; //m.c0; + float3 v = m._m01_m11_m21; //m.c1; + float3 w = m._m02_m12_m22; //m.c2; + + uint u_sign = (asuint(u.x) & 0x80000000); + float t = v.y + asfloat(asuint(w.z) ^ u_sign); + uint4 u_mask = uint4((int) u_sign >> 31); + uint4 t_mask = uint4(asint(t) >> 31); + + float tr = 1.0f + abs(u.x); + + uint4 sign_flips = uint4(0x00000000, 0x80000000, 0x80000000, 0x80000000) ^ (u_mask & uint4(0x00000000, 0x80000000, 0x00000000, 0x80000000)) ^ (t_mask & uint4(0x80000000, 0x80000000, 0x80000000, 0x00000000)); + + float4 value = float4(tr, u.y, w.x, v.z) + asfloat(asuint(float4(t, v.x, u.z, w.y)) ^ sign_flips); // +---, +++-, ++-+, +-++ + + value = asfloat((asuint(value) & ~u_mask) | (asuint(value.zwxy) & u_mask)); + value = asfloat((asuint(value.wzyx) & ~t_mask) | (asuint(value) & t_mask)); + value = normalize(value); + return new_quaternion(value); +} + +#define quaternion_identity new_quaternion(0.0, 0.0, 0.0, 1.0) + +quaternion AxisAngle(float3 axis, float angle) +{ + float sina, cosa; + sincos(0.5f * angle, sina, cosa); + return new_quaternion(float4(axis * sina, cosa)); +} + +quaternion EulerXYZ(float3 xyz) +{ + // return mul(rotateZ(xyz.z), mul(rotateY(xyz.y), rotateX(xyz.x))); + float3 s, c; + sincos(0.5f * xyz, s, c); + return new_quaternion( + // s.x * c.y * c.z - s.y * s.z * c.x, + // s.y * c.x * c.z + s.x * s.z * c.y, + // s.z * c.x * c.y - s.x * s.y * c.z, + // c.x * c.y * c.z + s.y * s.z * s.x + float4(s.xyz, c.x) * c.yxxy * c.zzyz + s.yxxy * s.zzyz * float4(c.xyz, s.x) * float4(-1.0f, 1.0f, -1.0f, 1.0f) + ); +} + +quaternion EulerXYZ(float x, float y, float z) +{ + return EulerXYZ(float3(x, y, z)); +} + +quaternion EulerXZY(float3 xyz) +{ + // return mul(rotateY(xyz.y), mul(rotateZ(xyz.z), rotateX(xyz.x))); + float3 s, c; + sincos(0.5f * xyz, s, c); + return new_quaternion( + // s.x * c.y * c.z + s.y * s.z * c.x, + // s.y * c.x * c.z + s.x * s.z * c.y, + // s.z * c.x * c.y - s.x * s.y * c.z, + // c.x * c.y * c.z - s.y * s.z * s.x + float4(s.xyz, c.x) * c.yxxy * c.zzyz + s.yxxy * s.zzyz * float4(c.xyz, s.x) * float4(1.0f, 1.0f, -1.0f, -1.0f) + ); +} + +quaternion EulerXZY(float x, float y, float z) +{ + return EulerXZY(float3(x, y, z)); +} + +quaternion EulerYXZ(float3 xyz) +{ + // return mul(rotateZ(xyz.z), mul(rotateX(xyz.x), rotateY(xyz.y))); + float3 s, c; + sincos(0.5f * xyz, s, c); + return new_quaternion( + // s.x * c.y * c.z - s.y * s.z * c.x, + // s.y * c.x * c.z + s.x * s.z * c.y, + // s.z * c.x * c.y + s.x * s.y * c.z, + // c.x * c.y * c.z - s.y * s.z * s.x + float4(s.xyz, c.x) * c.yxxy * c.zzyz + s.yxxy * s.zzyz * float4(c.xyz, s.x) * float4(-1.0f, 1.0f, 1.0f, -1.0f) + ); +} + +quaternion EulerYXZ(float x, float y, float z) +{ + return EulerYXZ(float3(x, y, z)); +} + +quaternion EulerYZX(float3 xyz) +{ + // return mul(rotateX(xyz.x), mul(rotateZ(xyz.z), rotateY(xyz.y))); + float3 s, c; + sincos(0.5f * xyz, s, c); + return new_quaternion( + // s.x * c.y * c.z - s.y * s.z * c.x, + // s.y * c.x * c.z - s.x * s.z * c.y, + // s.z * c.x * c.y + s.x * s.y * c.z, + // c.x * c.y * c.z + s.y * s.z * s.x + float4(s.xyz, c.x) * c.yxxy * c.zzyz + s.yxxy * s.zzyz * float4(c.xyz, s.x) * float4(-1.0f, -1.0f, 1.0f, 1.0f) + ); +} + +quaternion EulerYZX(float x, float y, float z) +{ + return EulerYZX(float3(x, y, z)); +} + +quaternion EulerZXY(float3 xyz) +{ + // return mul(rotateY(xyz.y), mul(rotateX(xyz.x), rotateZ(xyz.z))); + float3 s, c; + sincos(0.5f * xyz, s, c); + return new_quaternion( + // s.x * c.y * c.z + s.y * s.z * c.x, + // s.y * c.x * c.z - s.x * s.z * c.y, + // s.z * c.x * c.y - s.x * s.y * c.z, + // c.x * c.y * c.z + s.y * s.z * s.x + float4(s.xyz, c.x) * c.yxxy * c.zzyz + s.yxxy * s.zzyz * float4(c.xyz, s.x) * float4(1.0f, -1.0f, -1.0f, 1.0f) + ); +} + +quaternion EulerZXY(float x, float y, float z) +{ + return EulerZXY(float3(x, y, z)); +} + +quaternion EulerZYX(float3 xyz) +{ + // return mul(rotateX(xyz.x), mul(rotateY(xyz.y), rotateZ(xyz.z))); + float3 s, c; + sincos(0.5f * xyz, s, c); + return new_quaternion( + // s.x * c.y * c.z + s.y * s.z * c.x, + // s.y * c.x * c.z - s.x * s.z * c.y, + // s.z * c.x * c.y + s.x * s.y * c.z, + // c.x * c.y * c.z - s.y * s.x * s.z + float4(s.xyz, c.x) * c.yxxy * c.zzyz + s.yxxy * s.zzyz * float4(c.xyz, s.x) * float4(1.0f, -1.0f, 1.0f, -1.0f) + ); +} + +quaternion EulerZYX(float x, float y, float z) +{ + return EulerZYX(float3(x, y, z)); +} + +// Todo: RotationOrder enum and functions + +quaternion RotateX(float angle) +{ + float sina, cosa; + sincos(0.5f * angle, sina, cosa); + return new_quaternion(sina, 0.0f, 0.0f, cosa); +} + +quaternion RotateY(float angle) +{ + float sina, cosa; + sincos(0.5f * angle, sina, cosa); + return new_quaternion(0.0f, sina, 0.0f, cosa); +} + +quaternion RotateZ(float angle) +{ + float sina, cosa; + sincos(0.5f * angle, sina, cosa); + return new_quaternion(0.0f, 0.0f, sina, cosa); +} + +quaternion LookRotation(float3 forward, float3 up) +{ + float3 t = normalize(cross(up, forward)); + return new_quaternion(float3x3(t, cross(forward, t), forward)); +} + +quaternion LookRotationSafe(float3 forward, float3 up) +{ + float forwardLengthSq = dot(forward, forward); + float upLengthSq = dot(up, up); + + forward *= rsqrt(forwardLengthSq); + up *= rsqrt(upLengthSq); + + float3 t = cross(up, forward); + float tLengthSq = dot(t, t); + t *= rsqrt(tLengthSq); + + float mn = min(min(forwardLengthSq, upLengthSq), tLengthSq); + float mx = max(max(forwardLengthSq, upLengthSq), tLengthSq); + + bool accept = mn > 1e-35f && mx < 1e35f && isfinite(forwardLengthSq) && isfinite(upLengthSq) && isfinite(tLengthSq); + return new_quaternion(select(float4(0.0f, 0.0f, 0.0f, 1.0f), new_quaternion(float3x3(t, cross(forward, t), forward)).value, accept)); +} + +quaternion conjugate(quaternion q) +{ + return new_quaternion(q.value * float4(-1.0f, -1.0f, -1.0f, 1.0f)); +} + +quaternion inverse(quaternion q) +{ + float4 x = q.value; + return new_quaternion(rcp(dot(x, x)) * x * float4(-1.0f, -1.0f, -1.0f, 1.0f)); +} + +float dot(quaternion a, quaternion b) +{ + return dot(a.value, b.value); +} + +float length(quaternion q) +{ + return sqrt(dot(q.value, q.value)); +} + +float lengthsq(quaternion q) +{ + return dot(q.value, q.value); +} + +quaternion normalize(quaternion q) +{ + float4 x = q.value; + return new_quaternion(rsqrt(dot(x, x)) * x); +} + +quaternion normalizesafe(quaternion q) +{ + float4 x = q.value; + float len = dot(x, x); + return quaternion(select(quaternion_identity.value, x * rsqrt(len), len > FLT_MIN_NORMAL)); +} + +quaternion normalizesafe(quaternion q, quaternion defaultvalue) +{ + float4 x = q.value; + float len = dot(x, x); + return quaternion(select(defaultvalue.value, x * rsqrt(len), len > FLT_MIN_NORMAL)); +} + +quaternion unitexp(quaternion q) +{ + float v_rcp_len = rsqrt(dot(q.value.xyz, q.value.xyz)); + float v_len = rcp(v_rcp_len); + float sin_v_len, cos_v_len; + sincos(v_len, sin_v_len, cos_v_len); + return new_quaternion(float4(q.value.xyz * v_rcp_len * sin_v_len, cos_v_len)); +} + +quaternion exp(quaternion q) +{ + float v_rcp_len = rsqrt(dot(q.value.xyz, q.value.xyz)); + float v_len = rcp(v_rcp_len); + float sin_v_len, cos_v_len; + sincos(v_len, sin_v_len, cos_v_len); + return quaternion(float4(q.value.xyz * v_rcp_len * sin_v_len, cos_v_len) * exp(q.value.w)); +} + +quaternion unitlog(quaternion q) +{ + float w = clamp(q.value.w, -1.0f, 1.0f); + float s = acos(w) * rsqrt(1.0f - w * w); + return new_quaternion(float4(q.value.xyz * s, 0.0f)); +} + +quaternion log(quaternion q) +{ + float v_len_sq = dot(q.value.xyz, q.value.xyz); + float q_len_sq = v_len_sq + q.value.w * q.value.w; + + float s = acos(clamp(q.value.w * rsqrt(q_len_sq), -1.0f, 1.0f)) * rsqrt(v_len_sq); + return new_quaternion(float4(q.value.xyz * s, 0.5f * log(q_len_sq))); +} + +quaternion mul(quaternion a, quaternion b) +{ + return new_quaternion(a.value.wwww * b.value + (a.value.xyzx * b.value.wwwx + a.value.yzxy * b.value.zxyy) * float4(1.0f, 1.0f, 1.0f, -1.0f) - a.value.zxyz * b.value.yzxz); +} + +float3 mul(quaternion q, float3 v) +{ + float3 t = 2 * cross(q.value.xyz, v); + return v + q.value.w * t + cross(q.value.xyz, t); +} + +float3 rotate(quaternion q, float3 v) +{ + float3 t = 2 * cross(q.value.xyz, v); + return v + q.value.w * t + cross(q.value.xyz, t); +} + +quaternion nlerp(quaternion q1, quaternion q2, float t) +{ + return normalize(q1.value + t * (chgsign(q2.value, dot(q1, q2)) - q1.value)); +} + +// Todo: acos is pricy +quaternion slerp(quaternion q1, quaternion q2, float t) +{ + float dt = dot(q1, q2); + if (dt < 0.0f) + { + dt = -dt; + q2.value = -q2.value; + } + + if (dt < 0.9995f) + { + float angle = acos(dt); + float s = rsqrt(1.0f - dt * dt); // 1.0f / sin(angle) + float w1 = sin(angle * (1.0f - t)) * s; + float w2 = sin(angle * t) * s; + return new_quaternion(q1.value * w1 + q2.value * w2); + } + else + { + // if the angle is small, use linear interpolation + return nlerp(q1, q2, t); + } +} + +// Todo: asin is pricy +float angle(quaternion q1, quaternion q2) +{ + float diff = asin(length(normalize(mul(conjugate(q1), q2)).value.xyz)); + return diff + diff; +} + +// Todo: quaternion from non-uniform column vectors using https://matthias-research.github.io/pages/publications/stablePolarDecomp.pdf diff --git a/Core/ShaderLibrary/quaternion.hlsl.meta b/Core/ShaderLibrary/quaternion.hlsl.meta new file mode 100644 index 0000000..2dce620 --- /dev/null +++ b/Core/ShaderLibrary/quaternion.hlsl.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 609365792e8654445a6eb241e5c198b3 +ShaderIncludeImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Core/ShaderLibrary/typed_math_extras.hlsl b/Core/ShaderLibrary/typed_math_extras.hlsl new file mode 100644 index 0000000..29f2d22 --- /dev/null +++ b/Core/ShaderLibrary/typed_math_extras.hlsl @@ -0,0 +1,37 @@ +#include "Packages/com.latios.latiosframework/Core/ShaderLibrary/RigidTransform.hlsl" +//#include "RigidTransform.hlsl" + +float3x3 new_float3x3(quaternion q) +{ + float4 v = q.value; + float4 v2 = v + v; + + uint3 npn = uint3(0x80000000, 0x00000000, 0x80000000); + uint3 nnp = uint3(0x80000000, 0x80000000, 0x00000000); + uint3 pnn = uint3(0x00000000, 0x80000000, 0x80000000); + + float3x3 result; + result._m00_m10_m20 = v2.y * asfloat(asuint(v.yxw) ^ npn) - v2.z * asfloat(asuint(v.zwx) ^ pnn) + float3(1, 0, 0); + result._m01_m11_m21 = v2.z * asfloat(asuint(v.wzy) ^ nnp) - v2.x * asfloat(asuint(v.yxw) ^ npn) + float3(0, 1, 0); + result._m02_m12_m22 = v2.x * asfloat(asuint(v.zwx) ^ pnn) - v2.y * asfloat(asuint(v.wzy) ^ nnp) + float3(0, 0, 1); + return result; +} + +float3x4 TRS(float3 translation, quaternion rotation, float3 scale) +{ + float3x3 r = new_float3x3(rotation); + return float3x4((r._m00_m10_m20 * scale.x), + (r._m01_m11_m21 * scale.y), + (r._m02_m12_m22 * scale.z), + (translation)); +} + +float3 InverseRotateFast(quaternion normalizedRotation, float3 v) +{ + return rotate(conjugate(normalizedRotation), v); +} + +float3 InverseRotateFast(quaternion normalizedRotation, quaternion q) +{ + return mul(conjugate(normalizedRotation), q); +} \ No newline at end of file diff --git a/Core/ShaderLibrary/typed_math_extras.hlsl.meta b/Core/ShaderLibrary/typed_math_extras.hlsl.meta new file mode 100644 index 0000000..e8e7038 --- /dev/null +++ b/Core/ShaderLibrary/typed_math_extras.hlsl.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 0c1cbd823393d2f43bfdc1d651809d72 +ShaderIncludeImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Kinemation/Authoring/BakingSystems/MeshDeformDataSmartBlobberSystem.cs b/Kinemation/Authoring/BakingSystems/MeshDeformDataSmartBlobberSystem.cs index b9db0c3..6192d79 100644 --- a/Kinemation/Authoring/BakingSystems/MeshDeformDataSmartBlobberSystem.cs +++ b/Kinemation/Authoring/BakingSystems/MeshDeformDataSmartBlobberSystem.cs @@ -186,25 +186,44 @@ protected unsafe override void OnUpdate() var blendShapeCount = mesh.blendShapeCount; if (requiresBlendShapes && blendShapeCount > 0) { - var gpuBuffer = mesh.GetBlendShapeBuffer(UnityEngine.Rendering.BlendShapeBufferLayout.PerShape); - builder.blendShapeNames = new UnsafeList(blendShapeCount, WorldUpdateAllocator, NativeArrayOptions.UninitializedMemory); - builder.blendShapeRanges = new UnsafeList(blendShapeCount, WorldUpdateAllocator, NativeArrayOptions.UninitializedMemory); - - uint verticesCount = 0; - for (int shapeIndex = 0; shapeIndex < blendShapeCount; shapeIndex++) + GraphicsBuffer gpuBuffer = default; + try + { + gpuBuffer = mesh.GetBlendShapeBuffer(UnityEngine.Rendering.BlendShapeBufferLayout.PerShape); + } + catch (Exception e) + { + // Yes, this can fail. I don't know why. But I reproduced it once in a user's project. It is either a Unity bug, or a corrupted mesh. + // We try to fail a little more gracefully here by just not baking blend shapes. This will result in a blend shape count of 0, + // which the runtime already is prepared for. + UnityEngine.Debug.LogException(e); + UnityEngine.Debug.LogError( + $"The mesh {mesh.name} reports having a blendShapeCount > 0 but Kinemation was unable to obtain the blend shape GraphicsBuffer from it. This is either a Unity bug, or an issue with the mesh itself."); + blendShapeRequestBuffers[index] = default; + blendShapeRequests[index] = default; + gpuBuffer = default; + } + if (gpuBuffer != default) { - builder.blendShapeNames.Add(mesh.GetBlendShapeName(shapeIndex)); - var range = mesh.GetBlendShapeBufferRange(shapeIndex); - builder.blendShapeRanges.Add(in range); - verticesCount = math.max(verticesCount, range.endIndex + 1); + builder.blendShapeNames = new UnsafeList(blendShapeCount, WorldUpdateAllocator, NativeArrayOptions.UninitializedMemory); + builder.blendShapeRanges = new UnsafeList(blendShapeCount, WorldUpdateAllocator, NativeArrayOptions.UninitializedMemory); + + uint verticesCount = 0; + for (int shapeIndex = 0; shapeIndex < blendShapeCount; shapeIndex++) + { + builder.blendShapeNames.Add(mesh.GetBlendShapeName(shapeIndex)); + var range = mesh.GetBlendShapeBufferRange(shapeIndex); + builder.blendShapeRanges.Add(in range); + verticesCount = math.max(verticesCount, range.endIndex + 1); + } + builder.blendShapeBufferSize = verticesCount; + var cpuBuffer = new NativeArray(math.max(gpuBuffer.count, (int)verticesCount * 10), + Allocator.Persistent, + NativeArrayOptions.ClearMemory); + blendShapeRequests[index] = UnityEngine.Rendering.AsyncGPUReadback.RequestIntoNativeArray(ref cpuBuffer, gpuBuffer); + blendShapeRequestBuffers[index] = cpuBuffer; + m_graphicsBufferCache.Add(gpuBuffer); } - builder.blendShapeBufferSize = verticesCount; - var cpuBuffer = new NativeArray(math.max(gpuBuffer.count, (int)verticesCount * 10), - Allocator.Persistent, - NativeArrayOptions.ClearMemory); - blendShapeRequests[index] = UnityEngine.Rendering.AsyncGPUReadback.RequestIntoNativeArray(ref cpuBuffer, gpuBuffer); - blendShapeRequestBuffers[index] = cpuBuffer; - m_graphicsBufferCache.Add(gpuBuffer); } else { diff --git a/Kinemation/Authoring/KinemationBakingBootstrap.cs b/Kinemation/Authoring/KinemationBakingBootstrap.cs index 1529cc1..ab2ccde 100644 --- a/Kinemation/Authoring/KinemationBakingBootstrap.cs +++ b/Kinemation/Authoring/KinemationBakingBootstrap.cs @@ -24,6 +24,7 @@ public static void InstallKinemation(ref CustomBakingBootstrapContext context) context.filteredBakerTypes.Add(typeof(DefaultMeshRendererBaker)); context.filteredBakerTypes.Remove(typeof(Unity.Rendering.MeshRendererBaker)); + context.filteredBakerTypes.Remove(typeof(LODGroupBaker)); context.bakingSystemTypesToInject.Add(TypeManager.GetSystemTypeIndex()); context.bakingSystemTypesToInject.Add(TypeManager.GetSystemTypeIndex()); diff --git a/Kinemation/Authoring/OverrideMeshRendererBaker.cs b/Kinemation/Authoring/OverrideMeshRendererBaker.cs index 2042bed..8632892 100644 --- a/Kinemation/Authoring/OverrideMeshRendererBaker.cs +++ b/Kinemation/Authoring/OverrideMeshRendererBaker.cs @@ -355,7 +355,7 @@ public static void GetLOD(IBaker baker, Renderer renderer, out LodSettings lodSe Debug.LogWarning( $"LOD renderer {renderer.gameObject.name} has a different world position than the LOD Group {group.gameObject.name} it belongs to. This is currently not supported and artifacts may occur. If you are seeing this message, please report it to the Latios Framework developers so that we can better understand your use case."); } - lodSettings.localHeight /= math.cmax(relativeTransform.scale * relativeTransform.stretch); + lodSettings.localHeight = group.size / math.cmax(relativeTransform.scale * relativeTransform.stretch); } } diff --git a/Kinemation/Components/CullingComponents.cs b/Kinemation/Components/CullingComponents.cs index 41439d5..e194847 100644 --- a/Kinemation/Components/CullingComponents.cs +++ b/Kinemation/Components/CullingComponents.cs @@ -36,6 +36,13 @@ public struct ChunkPerCameraCullingMask : IComponentData public BitField64 upper; public ulong GetUlongFromIndex(int index) => index == 0 ? lower.Value : upper.Value; + public void ClearBitAtIndex(int index) + { + if (index < 64) + lower.SetBits(index, false); + else + upper.SetBits(index - 64, false); + } } /// @@ -60,6 +67,14 @@ public struct ChunkPerDispatchCullingMask : IComponentData { public BitField64 lower; public BitField64 upper; + + internal void ClearBitAtIndex(int index) + { + if (index < 64) + lower.SetBits(index, false); + else + upper.SetBits(index - 64, false); + } } /// diff --git a/Kinemation/Components/InternalComponents.cs b/Kinemation/Components/InternalComponents.cs index 6eb5288..0c39c66 100644 --- a/Kinemation/Components/InternalComponents.cs +++ b/Kinemation/Components/InternalComponents.cs @@ -11,17 +11,17 @@ namespace Latios.Kinemation { #region Meshes -namespace InternalSourceGen -{ - public struct SkeletonDependent : ICleanupComponentData + namespace InternalSourceGen { - public EntityWith root; - public BlobAssetReference meshBindingBlob; - public BlobAssetReference skeletonBindingBlob; - public int boneOffsetEntryIndex; - public int indexInDependentSkinnedMeshesBuffer; + public struct SkeletonDependent : ICleanupComponentData + { + public EntityWith root; + public BlobAssetReference meshBindingBlob; + public BlobAssetReference skeletonBindingBlob; + public int boneOffsetEntryIndex; + public int indexInDependentSkinnedMeshesBuffer; + } } -} internal struct BoundMesh : ICleanupComponentData { @@ -72,25 +72,30 @@ internal struct ChunkDeformPrefixSums : IComponentData [WriteGroup(typeof(ChunkPerCameraCullingMask))] internal struct ChunkCopyDeformTag : IComponentData { } + + internal struct TrackedUniqueMesh : ICleanupComponentData + { + public UnityObjectRef mesh; + } #endregion #region Skeletons -namespace InternalSourceGen -{ - // This is system state to prevent copies on instantiate - [InternalBufferCapacity(1)] - public struct DependentSkinnedMesh : ICleanupBufferElementData - { - public EntityWith skinnedMesh; - // Todo: Store entry indices instead? - public uint meshVerticesStart; - public uint meshWeightsStart; - public uint meshBindPosesStart; - public uint boneOffsetsCount; - public uint boneOffsetsStart; - public float meshRadialOffset; + namespace InternalSourceGen + { + // This is system state to prevent copies on instantiate + [InternalBufferCapacity(1)] + public struct DependentSkinnedMesh : ICleanupBufferElementData + { + public EntityWith skinnedMesh; + // Todo: Store entry indices instead? + public uint meshVerticesStart; + public uint meshWeightsStart; + public uint meshBindPosesStart; + public uint boneOffsetsCount; + public uint boneOffsetsStart; + public float meshRadialOffset; + } } -} internal struct SkeletonBoundsOffsetFromMeshes : IComponentData { @@ -461,6 +466,33 @@ bool IEquatable.Equals(ChunkIdentifier other) // The data is owned by a world or system rewindable allocator. public JobHandle TryDispose(JobHandle inputDeps) => inputDeps; } + + internal partial struct UniqueMeshPool : ICollectionComponent + { + public NativeList > unusedMeshes; + public NativeList > allMeshes; + public NativeHashSet invalidMeshesToCull; + public NativeHashSet meshesPrevalidatedThisFrame; + public NativeHashMap, BatchMeshID> meshToIdMap; + public NativeHashMap > idToMeshMap; + + public JobHandle TryDispose(JobHandle inputDeps) + { + if (unusedMeshes.IsCreated) + { + inputDeps.Complete(); + GraphicsUnmanaged.DestroyMeshes(allMeshes.AsArray()); + unusedMeshes.Dispose(); + allMeshes.Dispose(); + invalidMeshesToCull.Dispose(); + meshesPrevalidatedThisFrame.Dispose(); + meshToIdMap.Dispose(); + idToMeshMap.Dispose(); + return default; + } + return inputDeps; + } + } #endregion } diff --git a/Kinemation/Components/MeshComponents.cs b/Kinemation/Components/MeshComponents.cs index 6752466..b6a3dca 100644 --- a/Kinemation/Components/MeshComponents.cs +++ b/Kinemation/Components/MeshComponents.cs @@ -42,6 +42,13 @@ public struct PreviousPostProcessMatrix : IComponentData public float3x4 postProcessMatrix; } + /// + /// When present on an entity with a MaterialMeshInfo that uses ranges, every mesh instance + /// in the range will be replaced with the mesh specified directly by the MaterialMeshInfo. + /// This allows you to render multiple materials in a single entity using a runtime-generated mesh. + /// + public struct OverrideMeshInRangeTag : IComponentData { } + /// /// An optional flag which specifies when a deformed mesh needs to be rebound /// Usage: Add/Enable this component whenever binding needs to occur. @@ -241,6 +248,198 @@ public enum Flags : byte #endregion + #region Unique Meshes + /// + /// When present, this component specifies that this entity has a UniqueMesh. + /// Enable this component to mark the mesh as dirty. + /// + public struct UniqueMeshConfig : IComponentData, IEnableableComponent + { + internal byte packed; + + /// + /// Commands Kinemation to automatically recalculate normals. The UniqueMeshNormal buffer does not need to be present, + /// but if it is, it will be overwritten with the result of this calculation. This operation will fail if the mesh does + /// not contain position elements. + /// + public bool calculateNormals + { + get => Bits.GetBit(packed, 0); + set => Bits.SetBit(ref packed, 0, value); + } + /// + /// Commands Kinemation to automatically recalculate tangents. The UniqueMeshTangent buffer does not need to be present, + /// but if it is, it will be overwritten with the result of this calculation. This operation will fail if the mesh does + /// not contain position and Uv0xy elements, and it will also fail if it does not contain normals and calculateNormals is false. + /// + public bool calculateTangents + { + get => Bits.GetBit(packed, 1); + set => Bits.SetBit(ref packed, 1, value); + } + /// + /// Commands Kinemation to clear and deallocate all Unique Mesh dynamic buffers after writing to the backing Mesh object. + /// This can be used to save memory. This setting is ignored if the mesh is determined to be invalid. + /// + public bool reclaimDynamicBufferMemoryAfterUpload + { + get => Bits.GetBit(packed, 2); + set => Bits.SetBit(ref packed, 2, value); + } + /// + /// Commands Kinemation to ignore the fact that a mesh may be empty or invalid and to generate a draw command anyways if the + /// entity otherwise passes culling. In Unity 6 and newer, you must set this if you modify the mesh during DispatchRoundRobinEarlyExtensionsSuperSystem. + /// + public bool disableEmptyAndInvalidMeshCulling + { + get => Bits.GetBit(packed, 3); + set => Bits.SetBit(ref packed, 3, value); + } + /// + /// Commands Kinemation to upload the mesh, even if the entity is culled from rendering. + /// + public bool forceUpload + { + get => Bits.GetBit(packed, 4); + set => Bits.SetBit(ref packed, 4, value); + } + } + /// + /// Specifies the vertex positions of the unique mesh. The RenderBounds will NOT be automatically updated. + /// It is the responsibility of the writer of the positions to update the RenderBounds (or some other system + /// if the positions are written to in DispatchRoundRobinEarlyExtensionsSuperSystem). + /// + [InternalBufferCapacity(0)] + public struct UniqueMeshPosition : IBufferElementData + { + public float3 position; + } + + /// + /// Specifies the vertex normals of the unique mesh + /// + [InternalBufferCapacity(0)] + public struct UniqueMeshNormal : IBufferElementData + { + public float3 normal; + } + + /// + /// Specifies the vertex tangents of the unique mesh + /// + [InternalBufferCapacity(0)] + public struct UniqueMeshTangent : IBufferElementData + { + public float4 tangent; + } + + /// + /// Specifies the vertex colors of the unique mesh + /// + [InternalBufferCapacity(0)] + public struct UniqueMeshColor : IBufferElementData + { + public float4 color; + } + + /// + /// Specifies the x and y components of UV0 of the unique mesh. + /// These UV values are typically used for sampling textures. + /// + [InternalBufferCapacity(0)] + public struct UniqueMeshUv0xy : IBufferElementData + { + public float2 uv; + } + + /// + /// Specifies the x, y, and z components of UV3 of the unique mesh. + /// A typically use case for these would be as an input to custom motion vectors in shader graph + /// in the case the mesh is animated. + /// + [InternalBufferCapacity(0)] + public struct UniqueMeshUv3xyz : IBufferElementData + { + public float3 uv; + } + + /// + /// Specifies the vertex indices used to define primitives in submeshes. + /// Typically, it is best to call `Reinterpret(4)` on the buffer to specify triangles. + /// If not present, the indices used will be a sequential count of the vertices. (0, 1, 2, 3, 4, ...) + /// + [InternalBufferCapacity(0)] + public struct UniqueMeshIndex : IBufferElementData + { + public int index; + } + + /// + /// Specifies the submeshes that compose the mesh. + /// If not present, it is assumed there is a single submesh using the entire mesh rendered as triangles. + /// + [InternalBufferCapacity(1)] + public struct UniqueMeshSubmesh : IBufferElementData + { + internal int packedA; + internal int packedB; + + /// + /// The first index in the submesh + /// + public int indexStart + { + get => Bits.GetBits(packedA, 0, 28); + set => Bits.SetBits(ref packedA, 0, 28, value); + } + + /// + /// The number of indices in the submesh + /// + public int indexCount + { + get => Bits.GetBits(packedB, 0, 28); + set => Bits.SetBits(ref packedB, 0, 28, value); + } + + public enum Topology : byte + { + Triangles = 0, + Lines = 1, + LineStrip = 2, + Points = 3 + } + + /// + /// The topology of the submesh + /// + public Topology topology + { + get => (Topology)Bits.GetBits(packedA, 28, 2); + set => Bits.SetBits(ref packedA, 28, 2, (int)value); + } + + public enum NormalHint : byte + { + PositiveX, + PositiveY, + PositiveZ, + NegativeX, + NegativeY, + NegativeZ, + } + + /// + /// A hint for the normal direction when not using Triangle topology + /// + public NormalHint normalHint + { + get => (NormalHint)Bits.GetBits(packedB, 28, 3); + set => Bits.SetBits(ref packedB, 28, 3, (int)value); + } + } + #endregion + #region Material Properties [MaterialProperty("_latiosCurrentVertexSkinningMatrixBase")] public struct CurrentMatrixVertexSkinningShaderIndex : IComponentData diff --git a/Kinemation/Systems/Culling/CullInvalidUniqueMeshesSystem.cs b/Kinemation/Systems/Culling/CullInvalidUniqueMeshesSystem.cs new file mode 100644 index 0000000..93dda43 --- /dev/null +++ b/Kinemation/Systems/Culling/CullInvalidUniqueMeshesSystem.cs @@ -0,0 +1,401 @@ +using Latios.Unsafe; +using Unity.Burst; +using Unity.Burst.Intrinsics; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Entities; +using Unity.Entities.Exposed; +using Unity.Jobs; +using Unity.Rendering; +using UnityEngine.Rendering; + +using static Unity.Entities.SystemAPI; + +namespace Latios.Kinemation.Systems +{ + [RequireMatchingQueriesForUpdate] + [DisableAutoCreation] + [BurstCompile] + public partial struct CullInvalidUniqueMeshesSystem : ISystem + { + LatiosWorldUnmanaged latiosWorld; + EntityQuery m_query; + + [BurstCompile] + public void OnCreate(ref SystemState state) + { + latiosWorld = state.GetLatiosWorldUnmanaged(); + m_query = state.Fluent().With(false).With(true).With(false, true).Build(); + } + + [BurstCompile] + public unsafe void OnUpdate(ref SystemState state) + { + var cullingContext = latiosWorld.worldBlackboardEntity.GetComponentData(); + var meshPool = latiosWorld.worldBlackboardEntity.GetCollectionComponent(false); + if (cullingContext.cullIndexThisFrame == 0) + state.Dependency = new NewFrameJob { meshPool = meshPool }.Schedule(state.Dependency); + + var cullingConfig = latiosWorld.worldBlackboardEntity.GetCollectionComponent(false); + var changeRequests = new UnsafeParallelBlockList(UnsafeUtility.SizeOf(), 256, cullingConfig.cullingThreadLocalAllocator.GeneralAllocator->ToAllocator); + state.Dependency = new CullJob + { + entityHandle = GetEntityTypeHandle(), + mmiHandle = GetComponentTypeHandle(true), + positionHandle = GetBufferTypeHandle(true), + normalHandle = GetBufferTypeHandle(true), + tangentHandle = GetBufferTypeHandle(true), + colorHandle = GetBufferTypeHandle(true), + uv0xyHandle = GetBufferTypeHandle(true), + uv3xyzHandle = GetBufferTypeHandle(true), + indexHandle = GetBufferTypeHandle(true), + submeshHandle = GetBufferTypeHandle(true), + meshPool = meshPool, + configHandle = GetComponentTypeHandle(false), + maskHandle = GetComponentTypeHandle(false), + changeRequests = changeRequests + }.ScheduleParallel(m_query, state.Dependency); + + state.Dependency = new UpdateRequestsJob + { + changeRequests = changeRequests, + meshPool = meshPool, + }.Schedule(state.Dependency); + } + + struct ChangeRequest + { + public BatchMeshID id; + public bool isInvalid; + } + + [BurstCompile] + struct NewFrameJob : IJob + { + public UniqueMeshPool meshPool; + + public void Execute() => meshPool.meshesPrevalidatedThisFrame.Clear(); + } + + [BurstCompile] + struct CullJob : IJobChunk + { + [ReadOnly] public EntityTypeHandle entityHandle; + [ReadOnly] public ComponentTypeHandle mmiHandle; + [ReadOnly] public BufferTypeHandle positionHandle; + [ReadOnly] public BufferTypeHandle normalHandle; + [ReadOnly] public BufferTypeHandle tangentHandle; + [ReadOnly] public BufferTypeHandle colorHandle; + [ReadOnly] public BufferTypeHandle uv0xyHandle; + [ReadOnly] public BufferTypeHandle uv3xyzHandle; + [ReadOnly] public BufferTypeHandle indexHandle; + [ReadOnly] public BufferTypeHandle submeshHandle; + [ReadOnly] public UniqueMeshPool meshPool; + + public ComponentTypeHandle configHandle; + public ComponentTypeHandle maskHandle; + public UnsafeParallelBlockList changeRequests; + + [NativeSetThreadIndex] + int threadIndex; + + public unsafe void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask) + { + ref var chunkMask = ref chunk.GetChunkComponentRefRW(ref maskHandle); + var mmis = (MaterialMeshInfo*)chunk.GetRequiredComponentDataPtrRO(ref mmiHandle); + var configurations = (UniqueMeshConfig*)chunk.GetRequiredComponentDataPtrRO(ref configHandle); + var configuredBits = chunk.GetEnabledMask(ref configHandle); + var enumerator = new ChunkEntityEnumerator(true, new v128(chunkMask.lower.Value, chunkMask.upper.Value), chunk.Count); + BitField64 furtherEvaluateLower = default, furtherEvaluateUpper = default; + while (enumerator.NextEntityIndex(out var entityIndex)) + { + if (configuredBits[entityIndex]) + { + if (!configurations[entityIndex].disableEmptyAndInvalidMeshCulling && !meshPool.meshesPrevalidatedThisFrame.Contains(mmis[entityIndex].MeshID)) + { + if (entityIndex < 64) + furtherEvaluateLower.SetBits(entityIndex, true); + else + furtherEvaluateUpper.SetBits(entityIndex - 64, true); + } + } + else if (meshPool.invalidMeshesToCull.Contains(mmis[entityIndex].MeshID)) + { + chunkMask.ClearBitAtIndex(entityIndex); + } + } + if ((furtherEvaluateLower.Value | furtherEvaluateUpper.Value) == 0) + return; + + // New configurations to validate + var validator = new UniqueMeshValidator + { + configurations = configurations, + entities = chunk.GetEntityDataPtrRO(entityHandle), + positionBuffers = chunk.GetBufferAccessor(ref positionHandle), + normalBuffers = chunk.GetBufferAccessor(ref normalHandle), + tangentBuffers = chunk.GetBufferAccessor(ref tangentHandle), + colorBuffers = chunk.GetBufferAccessor(ref colorHandle), + uv0xyBuffers = chunk.GetBufferAccessor(ref uv0xyHandle), + uv3xyzBuffers = chunk.GetBufferAccessor(ref uv3xyzHandle), + indexBuffers = chunk.GetBufferAccessor(ref indexHandle), + submeshBuffers = chunk.GetBufferAccessor(ref submeshHandle), + }; + validator.Init(); + + enumerator = new ChunkEntityEnumerator(true, new v128(furtherEvaluateLower.Value, furtherEvaluateUpper.Value), chunk.Count); + while (enumerator.NextEntityIndex(out var entityIndex)) + { + if (!validator.IsEntityIndexValidMesh(entityIndex)) + { + // Mark the entity as configured now so that we don't try to process it again until the user fixes the problem. + configuredBits[entityIndex] = false; + // Mark the entity as invalid so that we can cull it in future updates, but only if the status changed. + if (meshPool.invalidMeshesToCull.Contains(mmis[entityIndex].MeshID)) + { + changeRequests.Write(new ChangeRequest + { + id = mmis[entityIndex].MeshID, + isInvalid = true + }, threadIndex); + } + // Mark the entity as culled + chunkMask.ClearBitAtIndex(entityIndex); + } + else + { + // The mesh is now valid. Report it so that we don't cull it in the future. We must always report to add to the per-frame validation list. + changeRequests.Write(new ChangeRequest + { + id = mmis[entityIndex].MeshID, + isInvalid = false + }, threadIndex); + } + } + } + } + + [BurstCompile] + struct UpdateRequestsJob : IJob + { + public UnsafeParallelBlockList changeRequests; + public UniqueMeshPool meshPool; + + public void Execute() + { + var enumerator = changeRequests.GetEnumerator(); + while (enumerator.MoveNext()) + { + var request = enumerator.GetCurrent(); + if (request.isInvalid) + { + meshPool.invalidMeshesToCull.Add(request.id); + } + else + { + meshPool.invalidMeshesToCull.Remove(request.id); + meshPool.meshesPrevalidatedThisFrame.Add(request.id); + } + } + } + } + } + + internal unsafe struct UniqueMeshValidator + { + public UniqueMeshConfig* configurations; + public Entity* entities; + public BufferAccessor positionBuffers; + public BufferAccessor normalBuffers; + public BufferAccessor tangentBuffers; + public BufferAccessor colorBuffers; + public BufferAccessor uv0xyBuffers; + public BufferAccessor uv3xyzBuffers; + public BufferAccessor indexBuffers; + public BufferAccessor submeshBuffers; + + bool hasPositions; + bool hasNormals; + bool hasTangents; + bool hasColors; + bool hasUv0xys; + bool hasUv3xyzs; + bool hasIndices; + bool hasSubmehes; + + public void Init() + { + hasPositions = positionBuffers.Length > 0; + hasNormals = normalBuffers.Length > 0; + hasTangents = tangentBuffers.Length > 0; + hasColors = colorBuffers.Length > 0; + hasUv0xys = uv0xyBuffers.Length > 0; + hasUv3xyzs = uv3xyzBuffers.Length > 0; + hasIndices = indexBuffers.Length > 0; + hasSubmehes = submeshBuffers.Length > 0; + } + + public bool IsEntityIndexValidMesh(int entityIndex) + { + var config = configurations[entityIndex]; + bool failed = false; + + // Validate buffer size matches + int vertexCode = -1; + int vertexCount = -1; + if (hasPositions) + { + vertexCode = 0; + vertexCount = positionBuffers[entityIndex].Length; + } + if (!config.calculateNormals && hasNormals) + { + if (vertexCode < 0) + { + vertexCode = 1; + vertexCount = normalBuffers[entityIndex].Length; + } + else if (vertexCount != normalBuffers[entityIndex].Length) + { + UnityEngine.Debug.LogError( + $"{entities[entityIndex].ToFixedString()} has {vertexCount} positions and {normalBuffers[entityIndex].Length} tangents. These must match."); + failed = true; + } + } + if (!config.calculateTangents && hasTangents) + { + if (vertexCode < 0) + { + vertexCode = 2; + vertexCount = tangentBuffers[entityIndex].Length; + } + else if (vertexCount != tangentBuffers[entityIndex].Length) + { + UnityEngine.Debug.LogError( + $"{entities[entityIndex].ToFixedString()} has {vertexCount} {GetNameFromVertexCode(vertexCode)} and {tangentBuffers[entityIndex].Length} tangents. These must match."); + failed = true; + } + } + if (hasColors) + { + if (vertexCode < 0) + { + vertexCode = 3; + vertexCount = colorBuffers[entityIndex].Length; + } + else if (vertexCount != colorBuffers[entityIndex].Length) + { + UnityEngine.Debug.LogError( + $"{entities[entityIndex].ToFixedString()} has {vertexCount} {GetNameFromVertexCode(vertexCode)} and {colorBuffers[entityIndex].Length} colors. These must match."); + failed = true; + } + } + if (hasUv0xys) + { + if (vertexCode < 0) + { + vertexCode = 4; + vertexCount = uv0xyBuffers[entityIndex].Length; + } + else if (vertexCount != uv0xyBuffers[entityIndex].Length) + { + UnityEngine.Debug.LogError( + $"{entities[entityIndex].ToFixedString()} has {vertexCount} {GetNameFromVertexCode(vertexCode)} and {uv0xyBuffers[entityIndex].Length} UV0 xy values. These must match."); + failed = true; + } + } + if (hasUv3xyzs) + { + if (vertexCode < 0) + { + vertexCode = 4; + vertexCount = uv3xyzBuffers[entityIndex].Length; + } + else if (vertexCount != uv3xyzBuffers[entityIndex].Length) + { + UnityEngine.Debug.LogError( + $"{entities[entityIndex].ToFixedString()} has {vertexCount} {GetNameFromVertexCode(vertexCode)} and {uv3xyzBuffers[entityIndex].Length} UV3 xyz values. These must match."); + failed = true; + } + } + + // Validate config options + if (config.calculateNormals && !hasPositions) + { + UnityEngine.Debug.LogError($"Cannot calculate normals without positions for {entities[entityIndex].ToFixedString()}."); + failed = true; + } + if (config.calculateTangents && !hasPositions && !hasUv0xys && !(hasNormals || config.calculateNormals)) + { + UnityEngine.Debug.LogError($"Cannot calculate tangents without positions, normals, and UV0 values for {entities[entityIndex].ToFixedString()}."); + failed = true; + } + + // Validate submeshes + if (hasSubmehes) + { + int indexCount = hasIndices ? indexBuffers[entityIndex].Length : vertexCount; + int submeshIndex = 0; + foreach (var submesh in submeshBuffers[entityIndex]) + { + if (submesh.indexStart + submesh.indexCount > indexCount) + { + UnityEngine.Debug.LogError( + $"In {entities[entityIndex].ToFixedString()}, submesh {submeshIndex} with indexStart {submesh.indexStart} and indexCount {submesh.indexCount} exceeds {indexCount} total indices in the mesh."); + failed = true; + } + if (submesh.topology == UniqueMeshSubmesh.Topology.Triangles && submesh.indexCount % 3 != 0) + { + UnityEngine.Debug.LogError( + $"In {entities[entityIndex].ToFixedString()}, submesh has triangle topology and indexCount {submesh.indexCount} which is not divisible by 3."); + failed = true; + } + if (submesh.topology == UniqueMeshSubmesh.Topology.Lines && submesh.indexCount % 2 != 0) + { + UnityEngine.Debug.LogError( + $"In {entities[entityIndex].ToFixedString()}, submesh has line topology and indexCount {submesh.indexCount} which is not divisible by 2. Did you intend to use LineStrip instead?"); + failed = true; + } + submeshIndex++; + } + } + + // Validate indices only if we haven't failed already + int totalIndexCount = 0; + if (!failed && hasIndices) + { + foreach (var index in indexBuffers[entityIndex]) + { + if (index.index < 0 || index.index >= vertexCount) + { + UnityEngine.Debug.LogError( + $"In {entities[entityIndex].ToFixedString()}, index value {index.index} at position {totalIndexCount} in the index buffer is outside the vertex range [0, {vertexCount})"); + failed = true; + break; + } + totalIndexCount++; + } + } + // If the mesh is just empty, silently fail. + if (vertexCount == 0 && totalIndexCount == 0) + failed = true; + + return !failed; + } + + FixedString32Bytes GetNameFromVertexCode(int code) + { + switch (code) + { + case 0: return "positions"; + case 1: return "normals"; + case 2: return "tangents"; + case 3: return "colors"; + case 4: return "UV0 xy values"; + case 5: return "UV3 xyz values"; + default: return default; + } + } + } +} + diff --git a/Kinemation/Systems/Culling/CullInvalidUniqueMeshesSystem.cs.meta b/Kinemation/Systems/Culling/CullInvalidUniqueMeshesSystem.cs.meta new file mode 100644 index 0000000..dee4e09 --- /dev/null +++ b/Kinemation/Systems/Culling/CullInvalidUniqueMeshesSystem.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 59a64365e1fe9fe4d913e3768b382710 \ No newline at end of file diff --git a/Kinemation/Systems/Culling/CullLodsSystem.cs b/Kinemation/Systems/Culling/CullLodsSystem.cs index aca7166..b087484 100644 --- a/Kinemation/Systems/Culling/CullLodsSystem.cs +++ b/Kinemation/Systems/Culling/CullLodsSystem.cs @@ -228,7 +228,7 @@ public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useE if (i < 64) chunkInfo.CullingData.InstanceLodEnableds.Enabled[0] |= 1ul << i; else - chunkInfo.CullingData.InstanceLodEnableds.Enabled[0] |= 1ul << (i - 64); + chunkInfo.CullingData.InstanceLodEnableds.Enabled[1] |= 1ul << (i - 64); } } else @@ -247,7 +247,7 @@ public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useE if (i < 64) chunkInfo.CullingData.InstanceLodEnableds.Enabled[0] |= 1ul << i; else - chunkInfo.CullingData.InstanceLodEnableds.Enabled[0] |= 1ul << (i - 64); + chunkInfo.CullingData.InstanceLodEnableds.Enabled[1] |= 1ul << (i - 64); } } } diff --git a/Kinemation/Systems/Culling/GenerateBrgDrawCommandsSystem.cs b/Kinemation/Systems/Culling/GenerateBrgDrawCommandsSystem.cs index 39a7d17..881fd9f 100644 --- a/Kinemation/Systems/Culling/GenerateBrgDrawCommandsSystem.cs +++ b/Kinemation/Systems/Culling/GenerateBrgDrawCommandsSystem.cs @@ -120,21 +120,22 @@ public unsafe void OnUpdate(ref SystemState state) #if UNITY_EDITOR EditorDataComponentHandle = GetSharedComponentTypeHandle(), #endif - EntitiesGraphicsChunkInfo = GetComponentTypeHandle(true), - LastSystemVersion = state.LastSystemVersion, - LightMaps = ManagedAPI.GetSharedComponentTypeHandle(), - lodCrossfadeHandle = GetComponentTypeHandle(true), - motionVectorDeformQueryMask = m_motionVectorDeformQueryMask, - PostProcessMatrix = GetComponentTypeHandle(true), - MaterialMeshInfo = GetComponentTypeHandle(true), - ProceduralMotion = GetComponentTypeHandle(true), - ProfilerEmitChunk = m_profilerEmitChunk, - RenderFilterSettings = GetSharedComponentTypeHandle(), - RenderMeshArray = ManagedAPI.GetSharedComponentTypeHandle(), - SceneCullingMask = cullingContext.sceneCullingMask, - speedTreeCrossfadeTagHandle = GetComponentTypeHandle(true), - splitsAreValid = cullingContext.viewType == BatchCullingViewType.Light, - useMmiRangeLodTagHandle = GetComponentTypeHandle(true), + EntitiesGraphicsChunkInfo = GetComponentTypeHandle(true), + LastSystemVersion = state.LastSystemVersion, + LightMaps = ManagedAPI.GetSharedComponentTypeHandle(), + lodCrossfadeHandle = GetComponentTypeHandle(true), + motionVectorDeformQueryMask = m_motionVectorDeformQueryMask, + PostProcessMatrix = GetComponentTypeHandle(true), + MaterialMeshInfo = GetComponentTypeHandle(true), + ProceduralMotion = GetComponentTypeHandle(true), + ProfilerEmitChunk = m_profilerEmitChunk, + RenderFilterSettings = GetSharedComponentTypeHandle(), + RenderMeshArray = ManagedAPI.GetSharedComponentTypeHandle(), + overrideMeshInRangeTagHandle = GetComponentTypeHandle(true), + SceneCullingMask = cullingContext.sceneCullingMask, + speedTreeCrossfadeTagHandle = GetComponentTypeHandle(true), + splitsAreValid = cullingContext.viewType == BatchCullingViewType.Light, + useMmiRangeLodTagHandle = GetComponentTypeHandle(true), #if !LATIOS_TRANSFORMS_UNCACHED_QVVS && !LATIOS_TRANSFORMS_UNITY WorldTransform = GetComponentTypeHandle(true), #elif !LATIOS_TRANSFORMS_UNCACHED_QVVS && LATIOS_TRANSFORMS_UNITY @@ -310,6 +311,7 @@ unsafe struct EmitDrawCommandsJob : IJobParallelForDefer [ReadOnly] public ComponentTypeHandle lodCrossfadeHandle; [ReadOnly] public ComponentTypeHandle speedTreeCrossfadeTagHandle; [ReadOnly] public ComponentTypeHandle useMmiRangeLodTagHandle; + [ReadOnly] public ComponentTypeHandle overrideMeshInRangeTagHandle; [ReadOnly] public EntityQueryMask motionVectorDeformQueryMask; public bool splitsAreValid; @@ -386,6 +388,7 @@ void Execute(in ArchetypeChunk chunk) bool isLightMapped = chunk.GetSharedComponentIndex(LightMaps) >= 0; bool hasLodCrossfade = chunk.Has(ref lodCrossfadeHandle); bool useMmiRangeLod = chunk.Has(ref useMmiRangeLodTagHandle); + bool hasOverrideMesh = chunk.Has(ref overrideMeshInRangeTagHandle); // Check if the chunk has statically disabled motion (i.e. never in motion pass) // or enabled motion (i.e. in motion pass if there was actual motion or force-to-zero). @@ -538,6 +541,10 @@ void Execute(in ArchetypeChunk chunk) } } + BatchMeshID overrideMesh = default; + if (hasOverrideMesh) + overrideMesh = materialMeshInfo.IsRuntimeMesh ? materialMeshInfo.MeshID : brgRenderMeshArray.GetMeshID(materialMeshInfo); + for (int i = 0; i < matMeshIndexRange.length; i++) { int matMeshSubMeshIndex = matMeshIndexRange.start + i; @@ -563,6 +570,9 @@ void Execute(in ArchetypeChunk chunk) filterIndexWithLodBit &= 0x7fffffff; } + if (hasOverrideMesh) + matMeshSubMesh.Mesh = overrideMesh; + DrawCommandSettings settings = new DrawCommandSettings { FilterIndex = filterIndexWithLodBit, diff --git a/Kinemation/Systems/Culling/UploadUniqueMeshesSystem.cs b/Kinemation/Systems/Culling/UploadUniqueMeshesSystem.cs new file mode 100644 index 0000000..3df213d --- /dev/null +++ b/Kinemation/Systems/Culling/UploadUniqueMeshesSystem.cs @@ -0,0 +1,660 @@ +using Latios.Kinemation.Systems; +using Latios.Unsafe; +using Unity.Burst; +using Unity.Burst.Intrinsics; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Entities; +using Unity.Jobs; +using Unity.Mathematics; +using Unity.Rendering; +using UnityEngine.Rendering; + +using static Unity.Entities.SystemAPI; + +namespace Latios.Kinemation +{ + [RequireMatchingQueriesForUpdate] + [DisableAutoCreation] + [BurstCompile] + public partial struct UploadUniqueMeshesSystem : ISystem, ICullingComputeDispatchSystem + { + LatiosWorldUnmanaged latiosWorld; + EntityQuery m_query; + CullingComputeDispatchData m_data; + + [BurstCompile] + public void OnCreate(ref SystemState state) + { + latiosWorld = state.GetLatiosWorldUnmanaged(); + m_query = state.Fluent().With(false).Build(); + } + + [BurstCompile] + public void OnUpdate(ref SystemState state) => m_data.DoUpdate(ref state, ref this); + + public CollectState Collect(ref SystemState state) + { + var chunkCount = m_query.CalculateChunkCountWithoutFiltering(); + var collectedChunks = new NativeList(chunkCount, state.WorldUpdateAllocator); + collectedChunks.Resize(chunkCount, NativeArrayOptions.ClearMemory); + var meshIDsToInvalidate = new UnsafeParallelBlockList(UnsafeUtility.SizeOf(), 256, state.WorldUpdateAllocator); + var meshPool = latiosWorld.worldBlackboardEntity.GetCollectionComponent(false); + + state.Dependency = new FindAndValidateMeshesJob + { + collectedChunks = collectedChunks, + colorHandle = GetBufferTypeHandle(true), + configHandle = GetComponentTypeHandle(false), + entityHandle = GetEntityTypeHandle(), + indexHandle = GetBufferTypeHandle(true), + maskHandle = GetComponentTypeHandle(true), + meshIDsToInvalidate = meshIDsToInvalidate, + meshPool = meshPool, + mmiHandle = GetComponentTypeHandle(true), + normalHandle = GetBufferTypeHandle(true), + positionHandle = GetBufferTypeHandle(true), + submeshHandle = GetBufferTypeHandle(true), + tangentHandle = GetBufferTypeHandle(true), + trackedUniqueMeshHandle = GetComponentTypeHandle(true), + uv0xyHandle = GetBufferTypeHandle(true), + uv3xyzHandle = GetBufferTypeHandle(true), + }.ScheduleParallel(m_query, state.Dependency); + + var meshesNeeded = new NativeReference(state.WorldUpdateAllocator, NativeArrayOptions.UninitializedMemory); + + state.Dependency = new OrganizeMeshesJob + { + collectedChunks = collectedChunks, + meshIDsToInvalidate = meshIDsToInvalidate, + meshPool = meshPool, + meshesNeeded = meshesNeeded + }.Schedule(state.Dependency); + + return new CollectState + { + collectedChunks = collectedChunks, + meshesNeeded = meshesNeeded + }; + } + + public WriteState Write(ref SystemState state, ref CollectState collectState) + { + var meshCount = collectState.meshesNeeded.Value; + if (meshCount == 0) + return default; + var meshesToUpload = + CollectionHelper.CreateNativeArray >(meshCount, state.WorldUpdateAllocator, NativeArrayOptions.UninitializedMemory); + var meshDataArray = UnityEngine.Mesh.AllocateWritableMeshData(meshCount); + var meshPool = latiosWorld.worldBlackboardEntity.GetCollectionComponent(true); + + state.Dependency = new WriteMeshesJob + { + collectedChunks = collectState.collectedChunks.AsDeferredJobArray(), + colorHandle = GetBufferTypeHandle(false), + configHandle = GetComponentTypeHandle(false), + indexHandle = GetBufferTypeHandle(false), + meshDataArray = meshDataArray, + meshesToUpload = meshesToUpload, + meshPool = meshPool, + mmiHandle = GetComponentTypeHandle(true), + normalHandle = GetBufferTypeHandle(false), + positionHandle = GetBufferTypeHandle(false), + submeshHandle = GetBufferTypeHandle(false), + tangentHandle = GetBufferTypeHandle(false), + trackedHandle = GetComponentTypeHandle(true), + uv0xyHandle = GetBufferTypeHandle(false), + uv3xyzHandle = GetBufferTypeHandle(false), + }.ScheduleParallel(meshCount, 1, state.Dependency); + + return new WriteState + { + meshCount = meshCount, + meshDataArray = meshDataArray, + meshesToUpload = meshesToUpload + }; + } + + public void Dispatch(ref SystemState state, ref WriteState writeState) + { + if (writeState.meshCount == 0) + return; + + var flags = MeshUpdateFlags.DontValidateIndices | MeshUpdateFlags.DontResetBoneBounds | MeshUpdateFlags.DontNotifyMeshUsers | MeshUpdateFlags.DontRecalculateBounds; + GraphicsUnmanaged.ApplyAndDisposeWritableMeshData(writeState.meshDataArray, writeState.meshesToUpload, flags); + } + + public struct CollectState + { + internal NativeList collectedChunks; + internal NativeReference meshesNeeded; + } + + public struct WriteState + { + internal int meshCount; + internal UnityEngine.Mesh.MeshDataArray meshDataArray; + internal NativeArray > meshesToUpload; + } + + internal struct CollectedChunk + { + public ArchetypeChunk chunk; + public BitField64 lower; + public BitField64 upper; + public int prefixSum; + } + + [BurstCompile] + struct FindAndValidateMeshesJob : IJobChunk + { + [ReadOnly] public EntityTypeHandle entityHandle; + [ReadOnly] public ComponentTypeHandle mmiHandle; + [ReadOnly] public BufferTypeHandle positionHandle; + [ReadOnly] public BufferTypeHandle normalHandle; + [ReadOnly] public BufferTypeHandle tangentHandle; + [ReadOnly] public BufferTypeHandle colorHandle; + [ReadOnly] public BufferTypeHandle uv0xyHandle; + [ReadOnly] public BufferTypeHandle uv3xyzHandle; + [ReadOnly] public BufferTypeHandle indexHandle; + [ReadOnly] public BufferTypeHandle submeshHandle; + [ReadOnly] public ComponentTypeHandle maskHandle; + [ReadOnly] public ComponentTypeHandle trackedUniqueMeshHandle; + [ReadOnly] public UniqueMeshPool meshPool; + + public ComponentTypeHandle configHandle; + public UnsafeParallelBlockList meshIDsToInvalidate; + [NativeDisableParallelForRestriction] public NativeList collectedChunks; // Preallocated to query chunk count without filtering + + [NativeSetThreadIndex] + int threadIndex; + + public unsafe void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask) + { + // 1) Only consider meshes that have a MaterialMeshInfo or a TrackedUniqueMesh. + if (!(chunk.Has(ref mmiHandle) || chunk.Has(ref trackedUniqueMeshHandle))) + return; + + var configurations = (UniqueMeshConfig*)chunk.GetRequiredComponentDataPtrRO(ref configHandle); + + // 2) Only consider visible meshes or meshes with forced uploads + ChunkPerDispatchCullingMask maskToProcess = default; + if (chunk.HasChunkComponent(ref maskHandle)) + maskToProcess = chunk.GetChunkComponentData(ref maskHandle); + + var enumerator = new ChunkEntityEnumerator(useEnabledMask, chunkEnabledMask, chunk.Count); + while (enumerator.NextEntityIndex(out var entityIndex)) + { + if (configurations[entityIndex].forceUpload) + { + if (entityIndex < 64) + maskToProcess.lower.SetBits(entityIndex, true); + else + maskToProcess.upper.SetBits(entityIndex - 64, true); + } + } + if ((maskToProcess.lower.Value | maskToProcess.upper.Value) == 0) + return; + + // 3) Identify which meshes still need validation. + var mmis = chunk.GetComponentDataPtrRO(ref mmiHandle); + BitField64 validateLower = default, validateUpper = default; + enumerator = new ChunkEntityEnumerator(true, new v128(maskToProcess.lower.Value, maskToProcess.upper.Value), chunk.Count); + while (enumerator.NextEntityIndex(out var entityIndex)) + { + if (!meshPool.meshesPrevalidatedThisFrame.Contains(mmis[entityIndex].MeshID)) + { + if (entityIndex < 64) + validateLower.SetBits(entityIndex, true); + else + validateUpper.SetBits(entityIndex - 64, true); + } + } + + // New configurations to validate + var configuredBits = chunk.GetEnabledMask(ref configHandle); + + // 4) Validate meshes still needing validation if necessary + if ((validateLower.Value | validateUpper.Value) != 0) + { + var validator = new UniqueMeshValidator + { + configurations = configurations, + entities = chunk.GetEntityDataPtrRO(entityHandle), + positionBuffers = chunk.GetBufferAccessor(ref positionHandle), + normalBuffers = chunk.GetBufferAccessor(ref normalHandle), + tangentBuffers = chunk.GetBufferAccessor(ref tangentHandle), + colorBuffers = chunk.GetBufferAccessor(ref colorHandle), + uv0xyBuffers = chunk.GetBufferAccessor(ref uv0xyHandle), + uv3xyzBuffers = chunk.GetBufferAccessor(ref uv3xyzHandle), + indexBuffers = chunk.GetBufferAccessor(ref indexHandle), + submeshBuffers = chunk.GetBufferAccessor(ref submeshHandle), + }; + validator.Init(); + + enumerator = new ChunkEntityEnumerator(true, new v128(validateLower.Value, validateUpper.Value), chunk.Count); + while (enumerator.NextEntityIndex(out var entityIndex)) + { + if (!validator.IsEntityIndexValidMesh(entityIndex)) + { + // Mark the entity as configured now so that we don't try to process it again until the user fixes the problem. + configuredBits[entityIndex] = false; + // Mark the entity as invalid so that we can cull it in future updates, but only if the status changed. + if (meshPool.invalidMeshesToCull.Contains(mmis[entityIndex].MeshID)) + { + meshIDsToInvalidate.Write(mmis[entityIndex].MeshID, threadIndex); + } + // Mark the entity as non-processable + maskToProcess.ClearBitAtIndex(entityIndex); + } + } + } + if ((maskToProcess.lower.Value | maskToProcess.upper.Value) == 0) + return; + + // 5) Export chunk + collectedChunks[unfilteredChunkIndex] = new CollectedChunk + { + chunk = chunk, + lower = maskToProcess.lower, + upper = maskToProcess.upper, + }; + } + } + + [BurstCompile] + struct OrganizeMeshesJob : IJob + { + public NativeList collectedChunks; + public UniqueMeshPool meshPool; + public UnsafeParallelBlockList meshIDsToInvalidate; + public NativeReference meshesNeeded; + + public void Execute() + { + var enumerator = meshIDsToInvalidate.GetEnumerator(); + while (enumerator.MoveNext()) + { + var id = enumerator.GetCurrent(); + meshPool.invalidMeshesToCull.Add(id); + } + + int prefixSum = 0; + int writeIndex = 0; + for (int i = 0; i < collectedChunks.Length; i++) + { + var chunk = collectedChunks[i]; + if (chunk.chunk == default) + continue; + + chunk.prefixSum = prefixSum; + prefixSum += chunk.lower.CountBits() + chunk.upper.CountBits(); + collectedChunks[writeIndex] = chunk; + writeIndex++; + } + collectedChunks.Length = writeIndex; + meshesNeeded.Value = prefixSum; + } + } + + [BurstCompile] + struct WriteMeshesJob : IJobFor + { + [ReadOnly] public NativeArray collectedChunks; + [ReadOnly] public ComponentTypeHandle mmiHandle; + [ReadOnly] public ComponentTypeHandle trackedHandle; + [ReadOnly] public UniqueMeshPool meshPool; + + public ComponentTypeHandle configHandle; + public BufferTypeHandle positionHandle; + public BufferTypeHandle normalHandle; + public BufferTypeHandle tangentHandle; + public BufferTypeHandle colorHandle; + public BufferTypeHandle uv0xyHandle; + public BufferTypeHandle uv3xyzHandle; + public BufferTypeHandle indexHandle; + public BufferTypeHandle submeshHandle; + + [NativeDisableParallelForRestriction] public UnityEngine.Mesh.MeshDataArray meshDataArray; + [NativeDisableParallelForRestriction] public NativeArray > meshesToUpload; + + [NativeDisableContainerSafetyRestriction] NativeList tempNormals; + [NativeDisableContainerSafetyRestriction] NativeList tempTangents; + [NativeDisableContainerSafetyRestriction] NativeList tempDescriptors; + + public unsafe void Execute(int chunkIndex) + { + if (!tempDescriptors.IsCreated) + tempDescriptors = new NativeList(8, Allocator.Temp); + + var chunk = collectedChunks[chunkIndex]; + + // Assign all the meshes now for a little better cache coherency. + var mask = chunk.chunk.GetEnabledMask(ref configHandle); + var tracked = chunk.chunk.GetComponentDataPtrRO(ref trackedHandle); + if (tracked != null) + { + int dstIndex = chunk.prefixSum; + var e = new ChunkEntityEnumerator(true, new v128(chunk.lower.Value, chunk.upper.Value), chunk.chunk.Count); + while (e.NextEntityIndex(out var entityIndex)) + { + mask[entityIndex] = false; + meshesToUpload[dstIndex] = tracked[entityIndex].mesh; + dstIndex++; + } + } + else + { + var mmis = chunk.chunk.GetComponentDataPtrRO(ref mmiHandle); + int dstIndex = chunk.prefixSum; + var e = new ChunkEntityEnumerator(true, new v128(chunk.lower.Value, chunk.upper.Value), chunk.chunk.Count); + while (e.NextEntityIndex(out var entityIndex)) + { + mask[entityIndex] = false; + meshesToUpload[dstIndex] = meshPool.idToMeshMap[mmis[entityIndex].MeshID]; + dstIndex++; + } + } + + var configurations = chunk.chunk.GetComponentDataPtrRO(ref configHandle); + var positionBuffers = chunk.chunk.GetBufferAccessor(ref positionHandle); + var normalBuffers = chunk.chunk.GetBufferAccessor(ref normalHandle); + var tangentBuffers = chunk.chunk.GetBufferAccessor(ref tangentHandle); + var colorBuffers = chunk.chunk.GetBufferAccessor(ref colorHandle); + var uv0xyBuffers = chunk.chunk.GetBufferAccessor(ref uv0xyHandle); + var uv3xyzBuffers = chunk.chunk.GetBufferAccessor(ref uv3xyzHandle); + var indexBuffers = chunk.chunk.GetBufferAccessor(ref indexHandle); + var submeshBuffers = chunk.chunk.GetBufferAccessor(ref submeshHandle); + + int meshIndex = chunk.prefixSum; + var enumerator = new ChunkEntityEnumerator(true, new v128(chunk.lower.Value, chunk.upper.Value), chunk.chunk.Count); + while (enumerator.NextEntityIndex(out var entityIndex)) + { + var config = configurations[entityIndex]; + + // Capture the arrays, possibly performing normal and tangent recalculation. + var indices = indexBuffers.Length > 0 ? indexBuffers[entityIndex].AsNativeArray().Reinterpret() : default; + var submeshes = submeshBuffers.Length > 0 ? submeshBuffers[entityIndex].AsNativeArray() : default; + var positions = positionBuffers.Length > 0 ? positionBuffers[entityIndex].AsNativeArray().Reinterpret() : default; + NativeArray normals = default; + if (config.calculateNormals) + { + if (normalBuffers.Length > 0) + { + var nb = normalBuffers[entityIndex]; + nb.ResizeUninitialized(positions.Length); + normals = nb.AsNativeArray().Reinterpret(); + } + else + { + if (!tempNormals.IsCreated) + tempNormals = new NativeList(positions.Length, Allocator.Temp); + tempNormals.ResizeUninitialized(positions.Length); + normals = tempNormals.AsArray(); + } + RecalculateNormals(positions, normals, indices, submeshes); + } + else if (normalBuffers.Length > 0) + { + normals = normalBuffers[entityIndex].AsNativeArray().Reinterpret(); + } + var uv0xys = uv0xyBuffers.Length > 0 ? uv0xyBuffers[entityIndex].AsNativeArray().Reinterpret() : default; + NativeArray tangents = default; + if (config.calculateTangents) + { + if (tangentBuffers.Length > 0) + { + var nb = tangentBuffers[entityIndex]; + nb.ResizeUninitialized(positions.Length); + tangents = nb.AsNativeArray().Reinterpret(); + } + else + { + if (!tempTangents.IsCreated) + tempTangents = new NativeList(positions.Length, Allocator.Temp); + tempTangents.ResizeUninitialized(positions.Length); + tangents = tempTangents.AsArray(); + } + RecalculateTangents(positions, normals, tangents, uv0xys, indices, submeshes); + } + else if (tangentBuffers.Length > 0) + { + tangents = tangentBuffers[entityIndex].AsNativeArray().Reinterpret(); + } + var colors = colorBuffers.Length > 0 ? colorBuffers[entityIndex].AsNativeArray().Reinterpret() : default; + var uv3xyzs = uv3xyzBuffers.Length > 0 ? uv3xyzBuffers[entityIndex].AsNativeArray().Reinterpret() : default; + + // Set up vertex attributes + tempDescriptors.Clear(); + int vertexCount = 0; + if (positions.Length > 0) + { + vertexCount = positions.Length; + tempDescriptors.Add(new VertexAttributeDescriptor(VertexAttribute.Position, VertexAttributeFormat.Float32, 3, 0)); + } + if (normals.Length > 0) + { + vertexCount = normals.Length; + tempDescriptors.Add(new VertexAttributeDescriptor(VertexAttribute.Normal, VertexAttributeFormat.Float32, 3, 0)); + } + if (tangents.Length > 0) + { + vertexCount = tangents.Length; + tempDescriptors.Add(new VertexAttributeDescriptor(VertexAttribute.Tangent, VertexAttributeFormat.Float32, 4, 0)); + } + if (colors.Length > 0) + { + vertexCount = colors.Length; + tempDescriptors.Add(new VertexAttributeDescriptor(VertexAttribute.Color)); + } + if (uv0xys.Length > 0) + { + vertexCount = uv0xys.Length; + tempDescriptors.Add(new VertexAttributeDescriptor(VertexAttribute.TexCoord0, VertexAttributeFormat.Float32, 2, 0)); + } + if (uv3xyzBuffers.Length > 0) + { + vertexCount = uv3xyzs.Length; + tempDescriptors.Add(new VertexAttributeDescriptor(VertexAttribute.TexCoord3, VertexAttributeFormat.Float32, 3, 0)); + } + + if (vertexCount == 0) + { + // We have an attribute-free mesh. We need to extract the vertex count from the indices. + if (indices.Length > 0) + { + foreach (var vindex in indices) + { + vertexCount = math.max(vertexCount, vindex + 1); + } + } + else + { + // Our mesh is fully defined by the submesh. + foreach (var submesh in submeshes) + { + vertexCount = math.max(vertexCount, submesh.indexStart + submesh.indexCount); + } + } + } + + var meshData = meshDataArray[meshIndex]; + meshData.SetVertexBufferParams(vertexCount, tempDescriptors.AsArray()); + var stream0 = meshData.GetVertexData(0); + + int writeIndex = 0; + for (int i = 0; i < vertexCount; i++) + { + if (positions.Length > 0) + Write(ref writeIndex, ref stream0, positions[i]); + if (normals.Length > 0) + Write(ref writeIndex, ref stream0, normals[i]); + if (tangents.Length > 0) + Write(ref writeIndex, ref stream0, tangents[i]); + if (colors.Length > 0) + Write(ref writeIndex, ref stream0, colors[i]); + if (uv0xys.Length > 0) + Write(ref writeIndex, ref stream0, uv0xys[i]); + if (uv3xyzs.Length > 0) + Write(ref writeIndex, ref stream0, uv3xyzs[i]); + } + + // Process indices + int indexCount = indices.Length > 0 ? indices.Length : vertexCount; + if (vertexCount < ushort.MaxValue) + { + meshData.SetIndexBufferParams(indexCount, IndexFormat.UInt16); + var indexStream = meshData.GetIndexData(); + if (indices.Length > 0) + { + for (int i = 0; i < indices.Length; i++) + { + indexStream[i] = (ushort)indices[i]; + } + } + else + { + for (ushort i = 0; i < indexCount; i++) + { + indexStream[i] = i; + } + } + } + else + { + meshData.SetIndexBufferParams(indexCount, IndexFormat.UInt32); + var indexStream = meshData.GetIndexData(); + if (indices.Length > 0) + { + indexStream.CopyFrom(indices); + } + else + { + for (int i = 0; i < indexCount; i++) + { + indexStream[i] = i; + } + } + } + + // Process submeshes + meshData.subMeshCount = math.max(1, submeshes.Length); + var flags = MeshUpdateFlags.DontValidateIndices | MeshUpdateFlags.DontResetBoneBounds | MeshUpdateFlags.DontNotifyMeshUsers | + MeshUpdateFlags.DontRecalculateBounds; + if (submeshes.Length == 0) + { + meshData.SetSubMesh(0, new SubMeshDescriptor(0, indexCount, UnityEngine.MeshTopology.Triangles), flags); + } + else + { + for (int submeshIndex = 0; submeshIndex < submeshes.Length; submeshIndex++) + { + var submesh = submeshes[submeshIndex]; + var topology = submesh.topology switch + { + UniqueMeshSubmesh.Topology.Triangles => UnityEngine.MeshTopology.Triangles, + UniqueMeshSubmesh.Topology.Lines => UnityEngine.MeshTopology.Lines, + UniqueMeshSubmesh.Topology.LineStrip => UnityEngine.MeshTopology.LineStrip, + UniqueMeshSubmesh.Topology.Points => UnityEngine.MeshTopology.Points, + _ => UnityEngine.MeshTopology.Triangles + }; + meshData.SetSubMesh(submeshIndex, new SubMeshDescriptor(submesh.indexStart, submesh.indexCount, topology), flags); + } + } + + // Process buffer clearing option + if (config.reclaimDynamicBufferMemoryAfterUpload) + { + if (positionBuffers.Length > 0) + { + var b = positionBuffers[entityIndex]; + b.Clear(); + b.TrimExcess(); + } + if (normalBuffers.Length > 0) + { + var b = normalBuffers[entityIndex]; + b.Clear(); + b.TrimExcess(); + } + if (tangentBuffers.Length > 0) + { + var b = tangentBuffers[entityIndex]; + b.Clear(); + b.TrimExcess(); + } + if (colorBuffers.Length > 0) + { + var b = colorBuffers[entityIndex]; + b.Clear(); + b.TrimExcess(); + } + if (uv0xyBuffers.Length > 0) + { + var b = uv0xyBuffers[entityIndex]; + b.Clear(); + b.TrimExcess(); + } + if (uv3xyzBuffers.Length > 0) + { + var b = uv3xyzBuffers[entityIndex]; + b.Clear(); + b.TrimExcess(); + } + if (indexBuffers.Length > 0) + { + var b = indexBuffers[entityIndex]; + b.Clear(); + b.TrimExcess(); + } + if (submeshBuffers.Length > 0) + { + var b = submeshBuffers[entityIndex]; + b.Clear(); + b.TrimExcess(); + } + } + } + } + + void RecalculateNormals(NativeArray positions, NativeArray normals, NativeArray indices, NativeArray submeshes) + { + // Todo: + throw new System.NotImplementedException("Recalculate normals for UniqueMesh is not supported at this time."); + } + + void RecalculateTangents(NativeArray positions, + NativeArray normals, + NativeArray tangents, + NativeArray uvs, + NativeArray indices, + NativeArray submeshes) + { + // Todo: + throw new System.NotImplementedException("Recalculate tangents for UniqueMesh is not supported at this time."); + } + + void Write(ref int writeIndex, ref NativeArray stream, float2 value) + { + stream[writeIndex++] = value.x; + stream[writeIndex++] = value.y; + } + + void Write(ref int writeIndex, ref NativeArray stream, float3 value) + { + stream[writeIndex++] = value.x; + stream[writeIndex++] = value.y; + stream[writeIndex++] = value.z; + } + + void Write(ref int writeIndex, ref NativeArray stream, float4 value) + { + stream[writeIndex++] = value.x; + stream[writeIndex++] = value.y; + stream[writeIndex++] = value.z; + stream[writeIndex++] = value.w; + } + } + } +} + diff --git a/Kinemation/Systems/Culling/UploadUniqueMeshesSystem.cs.meta b/Kinemation/Systems/Culling/UploadUniqueMeshesSystem.cs.meta new file mode 100644 index 0000000..cfec53d --- /dev/null +++ b/Kinemation/Systems/Culling/UploadUniqueMeshesSystem.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8460643e647d2fc4fa4ea5621102ea48 \ No newline at end of file diff --git a/Kinemation/Systems/KinemationSuperSystems.cs b/Kinemation/Systems/KinemationSuperSystems.cs index 51d6298..f99a5ff 100644 --- a/Kinemation/Systems/KinemationSuperSystems.cs +++ b/Kinemation/Systems/KinemationSuperSystems.cs @@ -33,6 +33,19 @@ protected override void CreateSystems() EnableSystemSorting = true; } } + + /// + /// This super system is the first to execute within KinemationCustomGraphicsSuperSystem. + /// Use it to enable entities for custom graphics processing. + /// + [DisableAutoCreation] + public partial class KinemationCustomGraphicsSetupSuperSystem : SuperSystem + { + protected override void CreateSystems() + { + EnableSystemSorting = true; + } + } #endregion #region Update SuperSystems @@ -77,6 +90,7 @@ protected override void CreateSystems() GetOrCreateAndAddUnmanagedSystem(); GetOrCreateAndAddUnmanagedSystem(); GetOrCreateAndAddUnmanagedSystem(); + GetOrCreateAndAddUnmanagedSystem(); GetOrCreateAndAddManagedSystem(); #if UNITY_EDITOR GetOrCreateAndAddManagedSystem(); @@ -155,19 +169,6 @@ public override bool ShouldUpdateSystem() } } - /// - /// This super system is the first to execute within KinemationCustomGraphicsSuperSystem. - /// Use it to enable entities for custom graphics processing. - /// - [DisableAutoCreation] - public partial class KinemationCustomGraphicsSetupSuperSystem : SuperSystem - { - protected override void CreateSystems() - { - EnableSystemSorting = true; - } - } - /// /// This super system executes special dispatch custom graphics systems in round-robin fashion. /// This is because dispatch systems typically require two separate sync points each to @@ -217,6 +218,7 @@ protected override void CreateSystems() EnableSystemSorting = false; GetOrCreateAndAddUnmanagedSystem(); + GetOrCreateAndAddUnmanagedSystem(); GetOrCreateAndAddUnmanagedSystem(); GetOrCreateAndAddUnmanagedSystem(); GetOrCreateAndAddUnmanagedSystem(); @@ -281,6 +283,7 @@ protected override void CreateSystems() EnableSystemSorting = false; GetOrCreateAndAddManagedSystem(); + GetOrCreateAndAddUnmanagedSystem(); GetOrCreateAndAddUnmanagedSystem(); GetOrCreateAndAddUnmanagedSystem(); GetOrCreateAndAddUnmanagedSystem(); diff --git a/Kinemation/Systems/PostBatching/AllocateUniqueMeshesSystem.cs b/Kinemation/Systems/PostBatching/AllocateUniqueMeshesSystem.cs new file mode 100644 index 0000000..b4c1f93 --- /dev/null +++ b/Kinemation/Systems/PostBatching/AllocateUniqueMeshesSystem.cs @@ -0,0 +1,160 @@ +using Unity.Burst; +using Unity.Burst.Intrinsics; +using Unity.Collections; +using Unity.Entities; +using Unity.Jobs; +using Unity.Rendering; +using UnityEngine.Rendering; + +using static Unity.Entities.SystemAPI; + +namespace Latios.Kinemation.Systems +{ + [RequireMatchingQueriesForUpdate] + [DisableAutoCreation] + [BurstCompile] + public partial struct AllocateUniqueMeshesSystem : ISystem + { + LatiosWorldUnmanaged latiosWorld; + + EntityQuery m_newMeshesQuery; + EntityQuery m_deadMeshesQuery; + + [BurstCompile] + public void OnCreate(ref SystemState state) + { + latiosWorld = state.GetLatiosWorldUnmanaged(); + + m_newMeshesQuery = state.Fluent().With(true).Without().Build(); + m_deadMeshesQuery = state.Fluent().With(true).Without().Build(); + + latiosWorld.worldBlackboardEntity.AddOrSetCollectionComponentAndDisposeOld(new UniqueMeshPool + { + allMeshes = new NativeList >(64, Allocator.Persistent), + unusedMeshes = new NativeList >(64, Allocator.Persistent), + invalidMeshesToCull = new NativeHashSet(64, Allocator.Persistent), + meshesPrevalidatedThisFrame = new NativeHashSet(64, Allocator.Persistent), + meshToIdMap = new NativeHashMap, BatchMeshID>(64, Allocator.Persistent), + idToMeshMap = new NativeHashMap >(64, Allocator.Persistent) + }); + } + + [BurstCompile] + public void OnUpdate(ref SystemState state) + { + var newMeshesCount = m_newMeshesQuery.CalculateEntityCountWithoutFiltering(); + var deadMeshesCount = m_deadMeshesQuery.CalculateEntityCountWithoutFiltering(); + var neededMeshes = newMeshesCount - deadMeshesCount; + var meshPool = latiosWorld.worldBlackboardEntity.GetCollectionComponent(false); + if (neededMeshes > 0) + { + var newAllocatedMeshes = new NativeArray >(neededMeshes, state.WorldUpdateAllocator, NativeArrayOptions.UninitializedMemory); + var newNames = new NativeArray(neededMeshes, state.WorldUpdateAllocator, NativeArrayOptions.UninitializedMemory); + var newIDs = new NativeArray(neededMeshes, state.WorldUpdateAllocator, NativeArrayOptions.UninitializedMemory); + + for (int i = 0; i < neededMeshes; i++) + { + newNames[i] = $"Kinemation Unique Mesh {meshPool.allMeshes.Length + i}"; + } + + GraphicsUnmanaged.CreateMeshes(newAllocatedMeshes, newNames); + GraphicsUnmanaged.RegisterMeshes(newAllocatedMeshes, newIDs, state.WorldUnmanaged); + + state.Dependency = new AddMeshesToPoolJob + { + meshes = newAllocatedMeshes, + ids = newIDs, + meshPool = meshPool, + }.Schedule(state.Dependency); + } + + if (deadMeshesCount > 0) + { + state.Dependency = new DeadMeshesJob + { + ecb = latiosWorld.syncPoint.CreateEntityCommandBuffer(), + entityHandle = GetEntityTypeHandle(), + meshPool = meshPool, + trackedHandle = GetComponentTypeHandle(true) + }.Schedule(m_deadMeshesQuery, state.Dependency); + } + if (newMeshesCount > 0) + { + state.Dependency = new NewMeshesJob + { + accb = latiosWorld.syncPoint.CreateAddComponentsCommandBuffer(AddComponentsDestroyedEntityResolution.AddToNewEntityAndDestroy), + entityHandle = GetEntityTypeHandle(), + meshPool = meshPool, + mmiHandle = GetComponentTypeHandle(false) + }.Schedule(m_newMeshesQuery, state.Dependency); + } + } + + [BurstCompile] + struct AddMeshesToPoolJob : IJob + { + [ReadOnly] public NativeArray > meshes; + [ReadOnly] public NativeArray ids; + public UniqueMeshPool meshPool; + + public void Execute() + { + meshPool.allMeshes.AddRange(meshes); + meshPool.unusedMeshes.AddRange(meshes); + for (int i = 0; i < meshes.Length; i++) + { + meshPool.meshToIdMap.Add(meshes[i], ids[i]); + meshPool.idToMeshMap.Add(ids[i], meshes[i]); + } + } + } + + [BurstCompile] + struct DeadMeshesJob : IJobChunk + { + [ReadOnly] public EntityTypeHandle entityHandle; + [ReadOnly] public ComponentTypeHandle trackedHandle; + public EntityCommandBuffer ecb; + public UniqueMeshPool meshPool; + + public unsafe void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask) + { + var entities = chunk.GetNativeArray(entityHandle); + ecb.RemoveComponent(entities); + var tracked = (TrackedUniqueMesh*)chunk.GetRequiredComponentDataPtrRO(ref trackedHandle); + for (int i = 0; i < chunk.Count; i++) + { + var mesh = tracked[i].mesh; + meshPool.unusedMeshes.Add(mesh); + meshPool.invalidMeshesToCull.Remove(meshPool.meshToIdMap[mesh]); + } + } + } + + [BurstCompile] + struct NewMeshesJob : IJobChunk + { + [ReadOnly] public EntityTypeHandle entityHandle; + public ComponentTypeHandle mmiHandle; + public AddComponentsCommandBuffer accb; + public UniqueMeshPool meshPool; + + public unsafe void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask) + { + var entities = chunk.GetEntityDataPtrRO(entityHandle); + var mmis = chunk.GetComponentDataPtrRW(ref mmiHandle); + for (int i = 0; i < chunk.Count; i++) + { + var mesh = meshPool.unusedMeshes[meshPool.unusedMeshes.Length - 1]; + meshPool.unusedMeshes.Length--; + accb.Add(entities[i], new TrackedUniqueMesh { mesh = mesh }); + var id = meshPool.meshToIdMap[mesh]; + meshPool.invalidMeshesToCull.Add(id); + if (mmis != null) + mmis[i].MeshID = id; + } + } + } + } +} + diff --git a/Kinemation/Systems/PostBatching/AllocateUniqueMeshesSystem.cs.meta b/Kinemation/Systems/PostBatching/AllocateUniqueMeshesSystem.cs.meta new file mode 100644 index 0000000..2745f0a --- /dev/null +++ b/Kinemation/Systems/PostBatching/AllocateUniqueMeshesSystem.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8924e670ef441674fa09e90383325a37 \ No newline at end of file diff --git a/Kinemation/Utilities/GraphicsUnmanaged.cs b/Kinemation/Utilities/GraphicsUnmanaged.cs index 8e113f7..b72be21 100644 --- a/Kinemation/Utilities/GraphicsUnmanaged.cs +++ b/Kinemation/Utilities/GraphicsUnmanaged.cs @@ -3,12 +3,14 @@ using System.Diagnostics; using System.Runtime.InteropServices; using AOT; +using Latios.Kinemation.Systems; using Unity.Burst; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using Unity.Entities; using Unity.Mathematics; using UnityEngine; +using UnityEngine.Rendering; namespace Latios.Kinemation { @@ -26,6 +28,7 @@ public static void Initialize() if (initialized) return; + meshListCache = new List(); buffers = new List(); handles.Data.freeList = new UnsafeList(128, Allocator.Persistent); handles.Data.versions = new UnsafeList(128, Allocator.Persistent); @@ -63,6 +66,93 @@ public static void SetGlobalBuffer(int propertyId, GraphicsBufferUnmanaged graph throw new System.InvalidOperationException("Setting the Graphics Buffer globally failed."); #endif } + + /// + /// Creates multiple Mesh objects + /// + /// An array to store references to the mesh objects. The length of the array determines the number of Mesh instances to create. + /// An optional array of names to assign to each new Mesh. + public static void CreateMeshes(NativeArray > outputMeshes, NativeArray optionalMeshNames = default) + { + var context = new CreateMeshesContext + { + meshes = outputMeshes, + names = optionalMeshNames, + success = false + }; + DoManagedExecute((IntPtr)(&context), 13); + +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if (!context.success) + throw new System.InvalidOperationException("Creating the meshes failed."); +#endif + } + + /// + /// Destroys multiple Mesh objects + /// + /// An arrayof mesh objects to destroy + public static void DestroyMeshes(NativeArray > meshes) + { + var context = new DestroyMeshesContext + { + meshes = meshes, + success = false + }; + DoManagedExecute((IntPtr)(&context), 16); + +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if (!context.success) + throw new System.InvalidOperationException("Destroying the meshes failed."); +#endif + } + + /// + /// Registers multiple meshes with the LatiosEntitiesGraphicsSystem within the specified World. + /// + /// The meshes to register + /// The array to store the resulting BatchMeshID values corresponding to the registered meshes + /// The world which contains the LatiosEntitiesGraphicsSystem performing the rendering + public static void RegisterMeshes(NativeArray > meshes, NativeArray outputIDs, WorldUnmanaged world) + { + var context = new RegisterMeshesContext + { + meshes = meshes, + world = world, + latiosEntitiesGraphicsSystem = TypeManager.GetSystemTypeIndex(), + success = false + }; + DoManagedExecute((IntPtr)(&context), 14); + +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if (!context.success) + throw new System.InvalidOperationException("Registering the meshes failed."); +#endif + } + + /// + /// Applies data defined in MeshData structs to Mesh objects. + /// Note: You can create the MeshDataArray from Burst using Mesh.AcquireWritableMeshData(). + /// + /// The mesh data array. + /// The destination Meshes. Must match the size of the mesh data array. + /// The mesh data update flags. + public static void ApplyAndDisposeWritableMeshData(Mesh.MeshDataArray data, NativeArray > meshes, UnityEngine.Rendering.MeshUpdateFlags flags) + { + var context = new ApplyAndDisposeWritableMeshDataContext + { + data = data, + meshes = meshes, + flags = flags, + success = false + }; + DoManagedExecute((IntPtr)(&context), 15); + +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if (!context.success) + throw new System.InvalidOperationException("Applying the meshes failed."); +#endif + } #endregion #region Extensions @@ -305,6 +395,8 @@ internal static void SetGraphicsBufferName(GraphicsBufferUnmanaged unmanaged, in #endregion #region State + static List meshListCache; + static List buffers; static readonly SharedStatic handles = SharedStatic.GetOrCreate(); static bool initialized = false; @@ -442,6 +534,40 @@ struct SetGraphicsBufferNameContext public FixedString128Bytes name; public bool success; } + + // Code 13 + struct CreateMeshesContext + { + public NativeArray > meshes; + public NativeArray names; + public bool success; + } + + // Code 14 + struct RegisterMeshesContext + { + public NativeArray > meshes; + public NativeArray outputIDs; + public WorldUnmanaged world; + public SystemTypeIndex latiosEntitiesGraphicsSystem; + public bool success; + } + + // Code 15 + struct ApplyAndDisposeWritableMeshDataContext + { + public Mesh.MeshDataArray data; + public NativeArray > meshes; + public UnityEngine.Rendering.MeshUpdateFlags flags; + public bool success; + } + + // Code 16 + struct DestroyMeshesContext + { + public NativeArray > meshes; + public bool success; + } #endregion static void DoManagedExecute(IntPtr context, int operation) @@ -566,6 +692,55 @@ static void ManagedExecute(IntPtr context, int operation) ctx.success = true; break; } + case 13: + { + ref var ctx = ref *(CreateMeshesContext*)context; + bool hasNames = ctx.names.IsCreated; + for (int i = 0; i < ctx.meshes.Length; i++) + { + var mesh = new Mesh(); + if (hasNames) + mesh.name = ctx.names[i].ToString(); + ctx.meshes[i] = mesh; + } + ctx.success = true; + break; + } + case 14: + { + ref var ctx = ref *(RegisterMeshesContext*)context; + var system = ctx.world.EntityManager.World.GetExistingSystemManaged(ctx.latiosEntitiesGraphicsSystem) as LatiosEntitiesGraphicsSystem; + // Todo: Unity has a Span registration API, but it is internal. + int i = 0; + foreach (var mesh in ctx.meshes) + { + ctx.outputIDs[i] = system.RegisterMesh(mesh); + i++; + } + ctx.success = true; + break; + } + case 15: + { + ref var ctx = ref *(ApplyAndDisposeWritableMeshDataContext*)context; + meshListCache.Clear(); + foreach (var mesh in ctx.meshes) + { + meshListCache.Add(mesh); + } + Mesh.ApplyAndDisposeWritableMeshData(ctx.data, meshListCache, ctx.flags); + meshListCache.Clear(); // Clear to help GC maybe? + ctx.success = true; + break; + } + case 16: + { + ref var ctx = ref *(DestroyMeshesContext*)context; + foreach (var mesh in ctx.meshes) + mesh.Value.DestroySafelyFromAnywhere(); + ctx.success = true; + break; + } } } catch (Exception e) diff --git a/MyriAudio/Internal/DspGraph/LatiosDspGraphDriver.cs b/MyriAudio/Internal/DspGraph/LatiosDspGraphDriver.cs deleted file mode 100644 index 5c2a361..0000000 --- a/MyriAudio/Internal/DspGraph/LatiosDspGraphDriver.cs +++ /dev/null @@ -1,64 +0,0 @@ -using Unity.Audio; -using Unity.Burst; -using Unity.Collections; -using Unity.Collections.LowLevel.Unsafe; -using Unity.Media.Utilities; - -namespace Latios.Myri -{ - //This is just DefaultDSPGraphDriver from DspGraph except with the scheduling mode changed to be on-thread. - [BurstCompile(CompileSynchronously = true)] - public struct LatiosDSPGraphDriver : IAudioOutput - { - public DSPGraph Graph; - int m_ChannelCount; - [NativeDisableContainerSafetyRestriction] - NativeArray m_DeinterleavedBuffer; - private bool m_FirstMix; - - public void Initialize(int channelCount, SoundFormat format, int sampleRate, long dspBufferSize) - { - m_ChannelCount = channelCount; - m_DeinterleavedBuffer = new NativeArray((int)(dspBufferSize * channelCount), Allocator.AudioKernel, NativeArrayOptions.UninitializedMemory); - m_FirstMix = true; - } - - public void BeginMix(int frameCount) - { - if (!m_FirstMix) - return; - m_FirstMix = false; - Graph.OutputMixer.BeginMix(frameCount, DSPGraph.ExecutionMode.Synchronous); - } - - public unsafe void EndMix(NativeArray output, int frames) - { -#if UNITY_2020_2_OR_NEWER - // Interleaving happens in the output hook manager - Graph.OutputMixer.ReadMix(output, frames, m_ChannelCount); -#else - Graph.OutputMixer.ReadMix(m_DeinterleavedBuffer, frames, m_ChannelCount); - Utility.InterleaveAudioStream((float*)m_DeinterleavedBuffer.GetUnsafeReadOnlyPtr(), (float*)output.GetUnsafePtr(), frames, m_ChannelCount); -#endif - Graph.OutputMixer.BeginMix(frames, DSPGraph.ExecutionMode.Synchronous); - } - - public void Dispose() - { - //UnityEngine.Debug.Log("Driver.Dispose"); - - if (Graph.Valid) - { - //UnityEngine.Debug.Log("Disposing graph"); - Graph.Dispose(); - } - - // TODO: This currently throws, needs yet another fix in unity - if (m_DeinterleavedBuffer.IsCreated) - { - m_DeinterleavedBuffer.Dispose(); - } - } - } -} - diff --git a/MyriAudio/Internal/OutputDrivers.meta b/MyriAudio/Internal/OutputDrivers.meta new file mode 100644 index 0000000..88cfb10 --- /dev/null +++ b/MyriAudio/Internal/OutputDrivers.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 38513ef31366a5844bee911bd86404cb +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MyriAudio/Internal/OutputDrivers/DriverManager.cs b/MyriAudio/Internal/OutputDrivers/DriverManager.cs new file mode 100644 index 0000000..966c0cc --- /dev/null +++ b/MyriAudio/Internal/OutputDrivers/DriverManager.cs @@ -0,0 +1,130 @@ +using System.Collections.Generic; +using Unity.Audio; + +namespace Latios.Myri.Driver +{ + internal static class DriverManager + { + struct Index + { + public int index; + public bool isManaged; + } + + static Dictionary registeredGraphs = new Dictionary(); + static List burstDrivers = new List(); + static Stack burstDriversFreeList = new Stack(); + static List managedGraphs = new List(); + static Stack managedGraphsFreeList = new Stack(); + static int incrementingKey = 1; + + static MyriManagedDriver currentManagedDriver; + static List managedDriversListCache = new List(); + + public static int RegisterGraph(ref DSPGraph graph) + { + if (!DriverSelection.useManagedDriver) + { + var burstDriver = new MyriBurstDriver { Graph = graph }; + + if (!burstDriversFreeList.TryPop(out var freeIndex)) + { + freeIndex = burstDrivers.Count; + burstDrivers.Add(default); + } + registeredGraphs.Add(incrementingKey, new Index + { + index = freeIndex, + isManaged = false + }); + var result = incrementingKey; + incrementingKey++; + + burstDrivers[freeIndex] = burstDriver.AttachToDefaultOutput(); + return result; + } + else + { + lock(managedGraphs) + { + if (!managedGraphsFreeList.TryPop(out var freeIndex)) + { + freeIndex = managedGraphs.Count; + managedGraphs.Add(default); + } + registeredGraphs.Add(incrementingKey, new Index + { + index = freeIndex, + isManaged = true + }); + var result = incrementingKey; + incrementingKey++; + + managedGraphs[freeIndex] = graph; + return result; + } + } + } + + public static void DeregisterAndDisposeGraph(int registeredKey) + { + if (!registeredGraphs.TryGetValue(registeredKey, out var index)) + { + UnityEngine.Debug.LogError($"Myri's DriverManager is corrupted. The key does not exist."); + return; + } + if (!index.isManaged) + { + burstDrivers[index.index].Dispose(); + burstDrivers[index.index] = default; + burstDriversFreeList.Push(index.index); + } + else + { + lock(managedGraphs) + { + managedGraphs[index.index].Dispose(); + managedGraphs[index.index] = default; + managedGraphsFreeList.Push(index.index); + } + } + registeredGraphs.Remove(registeredKey); + } + + public static void Update() + { + if (!DriverSelection.useManagedDriver) + return; + + // Todo: Support creating a managed driver when there is no listener. + // The biggest challenge with this is that if a listener were to exist later, + // then we can't detect it, and Unity will complain about multiple listeners. + if (currentManagedDriver == null) + currentManagedDriver = null; + else + return; + + UnityEngine.AudioListener currentListener = null; + // Check the main camera for a listener + var mainCamera = UnityEngine.Camera.main; + if (mainCamera != null) + { + if (!mainCamera.TryGetComponent(out currentListener)) + currentListener = null; + } + if (currentListener == null) + { + // Is it on a player character? + currentListener = UnityEngine.Object.FindAnyObjectByType(); + } + + if (currentListener == null) + return; + + currentManagedDriver = currentListener.gameObject.AddComponent(); + } + + public static List GetLockableManagedGraphs() => managedGraphs; + } +} + diff --git a/MyriAudio/Internal/OutputDrivers/DriverManager.cs.meta b/MyriAudio/Internal/OutputDrivers/DriverManager.cs.meta new file mode 100644 index 0000000..fa70b70 --- /dev/null +++ b/MyriAudio/Internal/OutputDrivers/DriverManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 065d7ebda34042648a156d0b46db694d \ No newline at end of file diff --git a/MyriAudio/Internal/OutputDrivers/DriverSelection.cs b/MyriAudio/Internal/OutputDrivers/DriverSelection.cs new file mode 100644 index 0000000..c8187d6 --- /dev/null +++ b/MyriAudio/Internal/OutputDrivers/DriverSelection.cs @@ -0,0 +1,37 @@ +using Unity.Collections; +using Unity.Entities; +using Unity.Mathematics; + +namespace Latios.Myri.Driver +{ + internal static class DriverSelection + { + public static bool useManagedDriver + { +#if !UNITY_EDITOR + get => false; +#else + get => UnityEditor.EditorPrefs.GetBool(editorPrefName); + private set => UnityEditor.EditorPrefs.SetBool(editorPrefName, value); +#endif + } + +#if UNITY_EDITOR + static string editorPrefName = $"{UnityEditor.PlayerSettings.productName}_MyriEditorManagedDriver"; + const string menuPath = "Edit/Latios/Use Myri Editor Managed Driver"; + + [UnityEditor.InitializeOnLoadMethod] + public static void InitToggle() => UnityEditor.Menu.SetChecked(menuPath, useManagedDriver); + + [UnityEditor.MenuItem(menuPath)] + public static void ToggleDriver() + { + var currentState = useManagedDriver; + currentState = !currentState; + useManagedDriver = currentState; + UnityEditor.Menu.SetChecked(menuPath, currentState); + } +#endif + } +} + diff --git a/MyriAudio/Internal/OutputDrivers/DriverSelection.cs.meta b/MyriAudio/Internal/OutputDrivers/DriverSelection.cs.meta new file mode 100644 index 0000000..1b59305 --- /dev/null +++ b/MyriAudio/Internal/OutputDrivers/DriverSelection.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f6a9b5fb3b2fbb3429900c2c560a5cbf \ No newline at end of file diff --git a/MyriAudio/Internal/OutputDrivers/MyriBurstDriver.cs b/MyriAudio/Internal/OutputDrivers/MyriBurstDriver.cs new file mode 100644 index 0000000..3b4be7b --- /dev/null +++ b/MyriAudio/Internal/OutputDrivers/MyriBurstDriver.cs @@ -0,0 +1,48 @@ +using Unity.Audio; +using Unity.Burst; +using Unity.Collections; + +namespace Latios.Myri.Driver +{ + [BurstCompile(CompileSynchronously = true)] + public struct MyriBurstDriver : IAudioOutput + { + public DSPGraph Graph; + int m_ChannelCount; + private bool m_FirstMix; + + public void Initialize(int channelCount, SoundFormat format, int sampleRate, long dspBufferSize) + { + m_ChannelCount = channelCount; + m_FirstMix = true; + } + + public void BeginMix(int frameCount) + { + if (!m_FirstMix) + return; + m_FirstMix = false; + Graph.OutputMixer.BeginMix(frameCount, DSPGraph.ExecutionMode.Synchronous); + } + + public unsafe void EndMix(NativeArray output, int frames) + { + // Interleaving happens in the output hook manager + Graph.OutputMixer.ReadMix(output, frames, m_ChannelCount); + // Todo: Would we get lower latency by always calling this in BeginMix since we operate synchronously? + Graph.OutputMixer.BeginMix(frames, DSPGraph.ExecutionMode.Synchronous); + } + + public void Dispose() + { + //UnityEngine.Debug.Log("Driver.Dispose"); + + if (Graph.Valid) + { + //UnityEngine.Debug.Log("Disposing graph"); + Graph.Dispose(); + } + } + } +} + diff --git a/MyriAudio/Internal/DspGraph/LatiosDspGraphDriver.cs.meta b/MyriAudio/Internal/OutputDrivers/MyriBurstDriver.cs.meta similarity index 100% rename from MyriAudio/Internal/DspGraph/LatiosDspGraphDriver.cs.meta rename to MyriAudio/Internal/OutputDrivers/MyriBurstDriver.cs.meta diff --git a/MyriAudio/Internal/OutputDrivers/MyriManagedDriver.cs b/MyriAudio/Internal/OutputDrivers/MyriManagedDriver.cs new file mode 100644 index 0000000..421da67 --- /dev/null +++ b/MyriAudio/Internal/OutputDrivers/MyriManagedDriver.cs @@ -0,0 +1,57 @@ +using System; +using Unity.Audio; +using Unity.Burst; +using Unity.Collections; +using UnityEngine; + +namespace Latios.Myri.Driver +{ + internal class MyriManagedDriver : MonoBehaviour + { + private unsafe void OnAudioFilterRead(float[] data, int channels) + { + var span = data.AsSpan(); + int spanLength = span.Length; + fixed (float* dst = span) + { + var graphs = DriverManager.GetLockableManagedGraphs(); + lock (graphs) + { + for (int i = 0; i < graphs.Count; i++) + { + var graph = graphs[i]; + if (!graph.Valid) + continue; + MyriManagedDriverUpdater.Update(&graph, dst, spanLength, channels); + } + } + } + } + } + + [BurstCompile] + static unsafe class MyriManagedDriverUpdater + { + [BurstCompile] + public static void Update(DSPGraph* graph, float* dstBufferAdditive, int managedFloatCount, int channelCount) + { + var bufferA = stackalloc float[managedFloatCount]; + var array = CollectionHelper.ConvertExistingDataToNativeArray(bufferA, managedFloatCount, Allocator.None, true); + int samplesPerFrame = managedFloatCount / channelCount; + graph->BeginMix(samplesPerFrame, DSPGraph.ExecutionMode.Synchronous); + graph->ReadMix(array, samplesPerFrame, channelCount); + + // Interleave + for (int i = 0; i < samplesPerFrame; i++) + { + for (int c = 0; c < channelCount; c++) + { + var outputIndex = i * channelCount + c; + var inputIndex = c * samplesPerFrame + i; + dstBufferAdditive[outputIndex] += array[inputIndex]; + } + } + } + } +} + diff --git a/MyriAudio/Internal/OutputDrivers/MyriManagedDriver.cs.meta b/MyriAudio/Internal/OutputDrivers/MyriManagedDriver.cs.meta new file mode 100644 index 0000000..4b055c1 --- /dev/null +++ b/MyriAudio/Internal/OutputDrivers/MyriManagedDriver.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8149a5cff5e418f40bdfc352c0cb5322 \ No newline at end of file diff --git a/MyriAudio/Systems/AudioSystem.cs b/MyriAudio/Systems/AudioSystem.cs index e2b7178..6fa3f30 100644 --- a/MyriAudio/Systems/AudioSystem.cs +++ b/MyriAudio/Systems/AudioSystem.cs @@ -1,4 +1,5 @@ -using Latios.Transforms.Abstract; +using Latios.Myri.Driver; +using Latios.Transforms.Abstract; using Unity.Audio; using Unity.Burst; using Unity.Collections; @@ -18,11 +19,10 @@ public partial struct AudioSystem : ISystem, ISystemShouldUpdate { bool m_initialized; - private DSPGraph m_graph; - private LatiosDSPGraphDriver m_driver; - private AudioOutputHandle m_outputHandle; - private int m_sampleRate; - private int m_samplesPerFrame; + private DSPGraph m_graph; + private int m_driverKey; + private int m_sampleRate; + private int m_samplesPerFrame; private DSPNode m_mixNode; private DSPConnection m_mixToLimiterMasterConnection; @@ -89,7 +89,10 @@ public void OnCreate(ref SystemState state) public bool ShouldUpdateSystem(ref SystemState state) { if (m_initialized) + { + DriverManager.Update(); return true; + } if (m_aliveListenersQuery.IsEmptyIgnoreFilter && m_deadListenersQuery.IsEmptyIgnoreFilter && m_loopedQuery.IsEmptyIgnoreFilter && m_oneshotsQuery.IsEmptyIgnoreFilter && m_oneshotsToDestroyWhenFinishedQuery.IsEmptyIgnoreFilter) @@ -113,10 +116,9 @@ public bool ShouldUpdateSystem(ref SystemState state) var format = ChannelEnumConverter.GetSoundFormatFromSpeakerMode(UnityEngine.AudioSettings.speakerMode); var channels = ChannelEnumConverter.GetChannelCountFromSoundFormat(format); UnityEngine.AudioSettings.GetDSPBufferSize(out m_samplesPerFrame, out _); - m_sampleRate = UnityEngine.AudioSettings.outputSampleRate; - m_graph = DSPGraph.Create(format, channels, m_samplesPerFrame, m_sampleRate); - m_driver = new LatiosDSPGraphDriver { Graph = m_graph }; - m_outputHandle = m_driver.AttachToDefaultOutput(); + m_sampleRate = UnityEngine.AudioSettings.outputSampleRate; + m_graph = DSPGraph.Create(format, channels, m_samplesPerFrame, m_sampleRate); + m_driverKey = DriverManager.RegisterGraph(ref m_graph); var commandBlock = m_graph.CreateCommandBlock(); m_mixNode = commandBlock.CreateDSPNode(); @@ -149,6 +151,7 @@ public bool ShouldUpdateSystem(ref SystemState state) m_ildNode); commandBlock.Cancel(); + DriverManager.Update(); return true; } @@ -440,8 +443,7 @@ public void OnDestroy(ref SystemState state) commandBlock.ReleaseDSPNode(m_mixNode); commandBlock.ReleaseDSPNode(m_limiterMasterNode); commandBlock.Complete(); - AudioOutputExtensions.DisposeOutputHook(ref m_outputHandle); - m_driver.Dispose(); + DriverManager.DeregisterAndDisposeGraph(m_driverKey); m_lastUpdateJobHandle.Complete(); m_mixNodePortFreelist.Dispose(); diff --git a/README.md b/README.md index ff81a4d..584ffb2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ![](https://github.com/Dreaming381/Latios-Framework-Documentation/blob/554a583e217bfe5bf38ece0ed65b22c33711afc6/media/bf2cb606139bb3ca01fe1c4c9f92cdf7.png) -# Latios Framework for Unity ECS – [0.12.0-alpha.3] +# Latios Framework for Unity ECS – [0.12.0-alpha.4] **This is a prerelease version of the Latios Framework version 0.12 which is still under development. Changelogs and Documentation, including the remainder of this README, have not been updated to reflect the new features and changes in 0.12. Git hashes may not be preserved on transition to beta or official release.** diff --git a/Transforms/ShaderLibrary.meta b/Transforms/ShaderLibrary.meta new file mode 100644 index 0000000..61cabc6 --- /dev/null +++ b/Transforms/ShaderLibrary.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5744ca540c9081646b08a53ad4dd37a0 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Transforms/ShaderLibrary/TransformQvvs.hlsl b/Transforms/ShaderLibrary/TransformQvvs.hlsl new file mode 100644 index 0000000..dbe201f --- /dev/null +++ b/Transforms/ShaderLibrary/TransformQvvs.hlsl @@ -0,0 +1,235 @@ +#include "Packages/com.latios.latiosframework/Core/ShaderLibrary/typed_math_extras.hlsl" +//#include "../../Core/ShaderLibrary/typed_math_extras.hlsl" + +struct TransformQvvs +{ + quaternion rotation; + float3 position; + int worldIndex; + float3 stretch; + float scale; + + float3x4 ToMatrix3x4() + { + return TRS(position, rotation, scale * stretch); + } + + float4x4 ToMatrix4x4() + { + return TRS(position, rotation, scale * stretch); + } + + float3x4 ToInverseMatrix3x4() + { + quaternion rotationInverse = conjugate(rotation); + float3x3 rotMat = new_float3x3(rotationInverse); + float3 positionInverse = rotate(rotationInverse, -position); + float4 rcp = rcp(float4(stretch, scale)); + float3 scaleInverse = rcp.xyz * rcp.w; + float3x4 result; + result._m00_m10_m20 = rotMat._m00_m10_m20 * scaleInverse; + result._m01_m11_m21 = rotMat._m01_m11_m21 * scaleInverse; + result._m02_m12_m22 = rotMat._m02_m12_m22 * scaleInverse; + result._m03_m13_m23 = positionInverse * scaleInverse; + return result; + } + + float3x4 ToInverseMatrix3x4IgnoreStretch() + { + quaternion rotationInverse = conjugate(rotation); + float3x3 rotMat = new_float3x3(rotationInverse); + float3 positionInverse = rotate(rotationInverse, -position); + float rcp = rcp(scale); + float3x4 result; + result._m00_m10_m20 = rotMat._m00_m10_m20; + result._m01_m11_m21 = rotMat._m01_m11_m21; + result._m02_m12_m22 = rotMat._m02_m12_m22; + result._m03_m13_m23 = positionInverse; + return result * rcp; + } +}; + +TransformQvvs new_TransformQvvs(float3 position, quaternion rotation) +{ + TransformQvvs result; + result.position = position; + result.rotation = rotation; + result.worldIndex = 0; + result.stretch = float3(1.0, 1.0, 1.0); + result.scale = 1.0; + return result; +} + +TransformQvvs new_TransformQvvs(float3 position, quaternion rotation, float scale, float3 stretch) +{ + TransformQvvs result; + result.position = position; + result.rotation = rotation; + result.worldIndex = 0; + result.stretch = stretch; + result.scale = scale; + return result; +} + +TransformQvvs new_TransformQvvs(float3 position, quaternion rotation, float scale, float3 stretch, int worldIndex) +{ + TransformQvvs result; + result.position = position; + result.rotation = rotation; + result.worldIndex = worldIndex; + result.stretch = stretch; + result.scale = scale; + return result; +} + +TransformQvvs new_TransformQvvs(RigidTransform rigidTransform) +{ + return new_TransformQvvs(rigidTransform.pos, rigidTransform.rot); +} + +#define TransformQvvs_identity new_TransformQvvs(RigidTransform_identity); + +struct TransformQvs +{ + quaternion rotation; + float3 position; + float scale; +}; + +TransformQvs new_TransformQvs(float3 position, quaternion rotation) +{ + TransformQvs result; + result.rotation = rotation; + result.position = position; + result.scale = 1.0; + return result; +} + +TransformQvs new_TransformQvs(float3 position, quaternion rotation, float scale) +{ + TransformQvs result; + result.rotation = rotation; + result.position = position; + result.scale = scale; + return result; +} + +#define TransformQvs_identity new_TransformQvs(float3(0.0, 0.0, 0.0), quaternion_identity) + +TransformQvvs mul(in TransformQvvs a, in TransformQvvs b) +{ + return new_TransformQvvs(a.position + rotate(a.rotation, b.position * a.stretch * a.scale), + mul(a.rotation, b.rotation), + a.scale * b.scale, + b.stretch, + b.worldIndex); +} + +void mul(inout TransformQvvs bWorld, in TransformQvvs a, in TransformQvs b) +{ + bWorld.rotation = mul(a.rotation, b.rotation); + bWorld.position = a.position + rotate(a.rotation, b.position * a.stretch * a.scale); + // bWorld.worldIndex is preserved + // bWorld.stretch is preserved + bWorld.scale = a.scale * b.scale; +} + +TransformQvs inversemul(in TransformQvvs a, in TransformQvvs b) +{ + quaternion inverseRotation = conjugate(a.rotation); + float4 rcps = rcp(float4(a.stretch, a.scale)); + return new_TransformQvs(rotate(inverseRotation, b.position - a.position) * rcps.xyz * rcps.w, + mul(inverseRotation, b.rotation), + rcps.w * b.scale); +} + +TransformQvvs inversemulqvvs(in TransformQvvs a, in TransformQvvs b) +{ + quaternion inverseRotation = conjugate(a.rotation); + float4 rcps = rcp(float4(a.stretch, a.scale)); + return new_TransformQvvs(rotate(inverseRotation, b.position - a.position) * rcps.xyz * rcps.w, + mul(inverseRotation, b.rotation), + rcps.w * b.scale, + b.stretch, + b.worldIndex); +} + +float3 TransformPoint(in TransformQvvs qvvs, float3 p) +{ + return qvvs.position + rotate(qvvs.rotation, p * qvvs.stretch * qvvs.scale); +} + +float3 InverseTransformPoint(in TransformQvvs qvvs, float3 p) +{ + float3 localPoint = InverseRotateFast(qvvs.rotation, p - qvvs.position); + float4 rcps = rcp(float4(qvvs.stretch, qvvs.scale)); + return localPoint * rcps.xyz * rcps.w; +} + +float3 TransformDirection(in TransformQvvs qvvs, float3 direction) +{ + return rotate(qvvs.rotation, direction); +} + +float3 TransformDirectionWithStretch(in TransformQvvs qvvs, float3 direction) +{ + float magnitude = length(direction); + return normalizesafe(rotate(qvvs.rotation, direction) * qvvs.stretch) * magnitude; +} + +float3 TransformDirectionScaledAndStretched(in TransformQvvs qvvs, float3 direction) +{ + return rotate(qvvs.rotation, direction) * qvvs.stretch * qvvs.scale; +} + +float3 InverseTransformDirection(in TransformQvvs qvvs, float3 direction) +{ + return InverseRotateFast(qvvs.rotation, direction); +} + +float3 InverseTransformDirectionWithStretch(in TransformQvvs qvvs, float3 direction) +{ + float magnitude = length(direction); + float3 rcp = rcp(qvvs.stretch); + return normalizesafe(InverseRotateFast(qvvs.rotation, direction) * rcp) * magnitude; +} + +float3 InverseTransformDirectionScaledAndStretched(in TransformQvvs qvvs, float3 direction) +{ + float4 rcp = rcp(float4(qvvs.stretch, qvvs.scale)); + return InverseRotateFast(qvvs.rotation, direction) * rcp.xyz * rcp.w; +} + +quaternion TransformRotation(in TransformQvvs qvvs, quaternion rotation) +{ + return mul(qvvs.rotation, rotation); +} + +quaternion InverseTransformRotation(in TransformQvvs qvvs, quaternion rotation) +{ + return InverseRotateFast(qvvs.rotation, rotation); +} + +float TransformScale(in TransformQvvs qvvs, float scale) +{ + return qvvs.scale * scale; +} + +float InverseTransformScale(in TransformQvvs qvvs, float scale) +{ + return scale / qvvs.scale; +} + +TransformQvvs RotateAbout(in TransformQvvs qvvs, quaternion rotation, float3 pivot) +{ + float3 pivotToOldPosition = qvvs.position - pivot; + float3 pivotToNewPosition = rotate(rotation, pivotToOldPosition); + return new_TransformQvvs + ( + qvvs.position + pivotToNewPosition - pivotToOldPosition, + mul(rotation, qvvs.rotation), + qvvs.scale, + qvvs.stretch, + qvvs.worldIndex + ); +} \ No newline at end of file diff --git a/Transforms/ShaderLibrary/TransformQvvs.hlsl.meta b/Transforms/ShaderLibrary/TransformQvvs.hlsl.meta new file mode 100644 index 0000000..c52ce21 --- /dev/null +++ b/Transforms/ShaderLibrary/TransformQvvs.hlsl.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: aded65d47c687d942a1e4686b3590af4 +ShaderIncludeImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/package.json b/package.json index f8f2543..c15a4a8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "com.latios.latiosframework", "displayName": "Latios Framework for ECS", - "version": "0.12.0-alpha.3", + "version": "0.12.0-alpha.4", "unity": "2022.3", "description": "Latios Framework for ECS is a collection of tools, algorithms, and API extensions developed by a hardcore hobbyist game developer.\n\nThis package includes all of the following modules:\n\u25aa Core\n\u25aa QVVS Transforms\n\u25aa Psyshock Physics\n\u25aa Myri Audio\n\u25aa Kinemation Animation and Rendering\n\u25aa Caligraphics\n\u25aa Mimic\n\nExamples: \n\u25aa Latios Space Shooter Sample - https://github.com/Dreaming381/lsss-wip \n\u25aa Mini Demos - https://github.com/Dreaming381/LatiosFrameworkMiniDemos \n\u25aa Free Parking - https://github.com/Dreaming381/Free-Parking", "documentationURL": "https://github.com/Dreaming381/Latios-Framework-Documentation",