Solved it. Still missing AxisFlags usage and whatnot, but it’s enough to count for a solution.
Working component code:
using Unity.Burst;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Rendering;
using UnityEngine;
using UnityEngine.Rendering;
namespace BlueAbyss
{
public class HollowShapeAuthoring : MonoBehaviour
{
public float Thickness = 0.2f;
public AxisFlags AxisFlags = (AxisFlags)~0;
public class Baker : Baker<HollowShapeAuthoring>
{
public override void Bake(HollowShapeAuthoring authoring)
{
var meshFilter = GetComponent<MeshFilter>();
if (!meshFilter) return;
var entity = GetEntity(TransformUsageFlags.Renderable);
var extents = meshFilter.sharedMesh.bounds.extents;
var highest = Mathf.Max(extents.x, extents.y, extents.z);
AddComponent(entity, new HollowShape
{
Thickness = authoring.Thickness,
ShapeExtents = highest,
AxisFlags = authoring.AxisFlags,
});
AddComponentObject(entity, new HollowShapeMesh
{
Mesh = meshFilter.sharedMesh,
});
}
}
}
public struct HollowShape : IComponentData
{
public float3 Thickness;
public float3 ShapeExtents;
public AxisFlags AxisFlags;
public float3 LastScale;
public BatchMeshID BatchMeshID;
}
public class HollowShapeMesh : IComponentData
{
public Mesh Mesh;
}
[System.Flags]
public enum AxisFlags
{
X = 1 << 0,
Y = 1 << 1,
Z = 1 << 2,
}
}
Working system code:
using Unity.Entities;
using Unity.Mathematics;
using Unity.Collections;
using Unity.Burst;
using Unity.Transforms;
using Unity.Rendering;
using Unity.Jobs;
using UnityEngine;
using UnityEngine.Rendering;
namespace BlueAbyss
{
[RequireMatchingQueriesForUpdate]
partial struct HollowShapeSystem : ISystem
{
EntityQuery query;
EntityTypeHandle entityType;
ComponentLookup<HollowShape> hollowShapeLookup;
ComponentLookup<MaterialMeshInfo> materialMeshInfoLookup;
ComponentTypeHandle<HollowShape> hollowShapeType;
ComponentTypeHandle<LocalToWorld> localToWorldType;
public void OnCreate(ref SystemState state)
{
query = new EntityQueryBuilder(Allocator.Temp)
.WithAllRW<HollowShape>()
.WithAll<LocalToWorld>()
.Build(ref state);
query.SetChangedVersionFilter(typeof(LocalToWorld)); // We only care if the entity changes scale.
entityType = state.GetEntityTypeHandle();
hollowShapeLookup = state.GetComponentLookup<HollowShape>();
materialMeshInfoLookup = state.GetComponentLookup<MaterialMeshInfo>();
hollowShapeType = state.GetComponentTypeHandle<HollowShape>();
localToWorldType = state.GetComponentTypeHandle<LocalToWorld>(true);
}
public void OnUpdate(ref SystemState state)
{
var chunks = query.ToArchetypeChunkArray(Allocator.TempJob);
entityType.Update(ref state);
hollowShapeType.Update(ref state);
localToWorldType.Update(ref state);
NativeList<Entity> validEntities = new(query.CalculateEntityCount(), Allocator.TempJob);
EntityValidationJob entityValidationJob = new()
{
Chunks = chunks,
ValidEntities = validEntities.AsParallelWriter(),
EntityType = entityType,
HollowShapeType = hollowShapeType,
LocalToWorldType = localToWorldType,
};
entityValidationJob.Run(chunks.Length);
if (validEntities.IsEmpty)
{
validEntities.Dispose();
return;
}
hollowShapeLookup.Update(ref state);
materialMeshInfoLookup.Update(ref state);
NativeArray<Entity> entities = validEntities.ToArray(Allocator.TempJob);
validEntities.Dispose();
var meshes = new Mesh[entities.Length];
for (int i = 0; i < entities.Length; i++)
{
var entity = entities[i];
var hollowShapeMesh = state.EntityManager.GetComponentObject<HollowShapeMesh>(entity);
meshes[i] = hollowShapeMesh.Mesh;
}
var sourceMeshDataArray = Mesh.AcquireReadOnlyMeshData(meshes);
var targetMeshDataArray = Mesh.AllocateWritableMeshData(entities.Length);
NativeArray<VertexAttributeDescriptor> vertexLayout = new(4, Allocator.TempJob);
vertexLayout[0] = new(VertexAttribute.Position, VertexAttributeFormat.Float32);
vertexLayout[1] = new(VertexAttribute.Normal, VertexAttributeFormat.Float32);
vertexLayout[2] = new(VertexAttribute.Tangent, VertexAttributeFormat.Float32, 4);
vertexLayout[3] = new(VertexAttribute.Color, VertexAttributeFormat.UNorm8, 4);
HollowShapeJob hollowShapeJob = new()
{
Entities = entities,
HollowShapeLookup = hollowShapeLookup,
SourceMeshDataArray = sourceMeshDataArray,
TargetMeshDataArray = targetMeshDataArray,
VertexLayout = vertexLayout,
};
state.Dependency = hollowShapeJob.Schedule(entities.Length, 1, state.Dependency);
state.Dependency.Complete();
sourceMeshDataArray.Dispose();
for (int i = 0; i < meshes.Length; i++)
{
meshes[i] = new();
}
Mesh.ApplyAndDisposeWritableMeshData(targetMeshDataArray, meshes, MeshUpdateFlags.DontRecalculateBounds);
var entitiesGraphicsSystem = state.World.GetExistingSystemManaged<EntitiesGraphicsSystem>();
NativeArray<BatchMeshID> meshIDs = new(meshes.Length, Allocator.TempJob);
for (int i = 0; i < meshes.Length; i++)
{
meshIDs[i] = entitiesGraphicsSystem.RegisterMesh(meshes[i]);
}
MeshApplicationJob meshApplicationJob = new()
{
Entities = entities,
MeshIDs = meshIDs,
HollowShapeLookup = hollowShapeLookup,
MaterialMeshInfoLookup = materialMeshInfoLookup,
};
meshApplicationJob.Run(meshIDs.Length);
foreach (var meshID in meshIDs) // The mesh application job swaps in the old IDs.
{
Object.Destroy(entitiesGraphicsSystem.GetMesh(meshID));
entitiesGraphicsSystem.UnregisterMesh(meshID);
}
meshIDs.Dispose();
}
[BurstCompile]
struct EntityValidationJob : IJobParallelFor
{
[ReadOnly, DeallocateOnJobCompletion]
public NativeArray<ArchetypeChunk> Chunks;
public NativeList<Entity>.ParallelWriter ValidEntities;
public EntityTypeHandle EntityType;
public ComponentTypeHandle<HollowShape> HollowShapeType;
[ReadOnly]
public ComponentTypeHandle<LocalToWorld> LocalToWorldType;
public void Execute(int index)
{
var chunk = Chunks[index];
var chunkEntities = chunk.GetNativeArray(EntityType);
var chunkHollowShape = chunk.GetNativeArray(ref HollowShapeType);
var chunkLocalToWorld = chunk.GetNativeArray(ref LocalToWorldType);
var chunkCount = chunk.Count;
for (int i = 0; i < chunkCount; i++)
{
var hollowShape = chunkHollowShape[i];
var localToWorld = chunkLocalToWorld[i];
var currentScale = localToWorld.Value.Scale();
if (currentScale.Equals(hollowShape.LastScale)) continue;
hollowShape.LastScale = currentScale;
ValidEntities.AddNoResize(chunkEntities[i]);
chunkHollowShape[i] = hollowShape;
}
}
}
struct MeshGatheringJob : IJob
{
[ReadOnly]
public NativeArray<Entity> Entities;
public EntityManager EntityManager;
public Mesh.MeshDataArray OutputMeshDataArray;
public void Execute()
{
var meshes = new Mesh[Entities.Length];
for (int i = 0; i < Entities.Length; i++)
{
var entity = Entities[i];
var hollowShapeMesh = EntityManager.GetComponentObject<HollowShapeMesh>(entity);
meshes[i] = hollowShapeMesh.Mesh;
}
OutputMeshDataArray = Mesh.AcquireReadOnlyMeshData(meshes);
}
}
struct Vertex
{
public float3 Position;
public float3 Normal;
public float4 Tangent;
public Color32 Color;
}
[BurstCompile]
struct HollowShapeJob : IJobParallelFor
{
[ReadOnly]
public NativeArray<Entity> Entities;
[ReadOnly]
public ComponentLookup<HollowShape> HollowShapeLookup;
[ReadOnly]
public Mesh.MeshDataArray SourceMeshDataArray;
public Mesh.MeshDataArray TargetMeshDataArray;
[DeallocateOnJobCompletion]
public NativeArray<VertexAttributeDescriptor> VertexLayout;
public void Execute(int index)
{
var entity = Entities[index];
var hollowShape = HollowShapeLookup[entity];
var sourceMeshData = SourceMeshDataArray[index];
var targetMeshData = TargetMeshDataArray[index];
var sourceIndices = sourceMeshData.GetIndexData<ushort>();
var sourceVertices = sourceMeshData.GetVertexData<Vertex>();
targetMeshData.SetIndexBufferParams(sourceIndices.Length, sourceMeshData.indexFormat);
targetMeshData.SetVertexBufferParams(sourceVertices.Length, VertexLayout);
var targetIndices = targetMeshData.GetIndexData<ushort>();
var targetVertices = targetMeshData.GetVertexData<Vertex>();
var insideColor = new Color32(255, 255, 255, 255);
for (int i = 0; i < sourceIndices.Length; i++)
{
targetIndices[i] = sourceIndices[i];
}
for (int i = 0; i < sourceVertices.Length; i++)
{
var vertex = sourceVertices[i];
if (IsRGBEqual(in vertex.Color, in insideColor))
{
vertex.Position = (hollowShape.ShapeExtents - hollowShape.Thickness / hollowShape.LastScale) * math.normalizesafe(vertex.Position);
}
targetVertices[i] = vertex;
}
targetMeshData.subMeshCount = sourceMeshData.subMeshCount;
for (int i = 0; i < sourceMeshData.subMeshCount; i++)
{
targetMeshData.SetSubMesh(i, sourceMeshData.GetSubMesh(i));
}
}
bool IsRGBEqual(in Color32 a, in Color32 b)
{
return a.r == b.r && a.b == b.b && a.g == b.g;
}
}
[BurstCompile]
struct MeshApplicationJob : IJobParallelFor
{
[ReadOnly, DeallocateOnJobCompletion]
public NativeArray<Entity> Entities;
public NativeArray<BatchMeshID> MeshIDs;
public ComponentLookup<HollowShape> HollowShapeLookup;
public ComponentLookup<MaterialMeshInfo> MaterialMeshInfoLookup;
public void Execute(int index)
{
var entity = Entities[index];
var hollowShape = HollowShapeLookup[entity];
var materialMeshInfo = MaterialMeshInfoLookup[entity];
var meshID = MeshIDs[index];
MeshIDs[index] = hollowShape.BatchMeshID;
hollowShape.BatchMeshID = meshID;
materialMeshInfo.MeshID = meshID;
HollowShapeLookup[entity] = hollowShape;
MaterialMeshInfoLookup[entity] = materialMeshInfo;
}
}
}
}