Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice
  3. Join us on November 16th, 2023, between 1 pm and 9 pm CET for Ask the Experts Online on Discord and on Unity Discussions.
    Dismiss Notice

ECS with Texture Mipmap Streaming?

Discussion in 'Entity Component System' started by goonter, Apr 25, 2019.

  1. goonter

    goonter

    Joined:
    Aug 31, 2015
    Posts:
    89
    I have a setup similar to the megacity project where I load in entities using subscenes + HLOD + StaticOptimizeEntity. I'm testing out loading an object that is textured with mipmap streaming enabled and it only seems to load the lowest quality mip level and never any higher no matter how close the camera gets to the object. I have another similar object in the main scene that is working fine so I know mipmap streaming is working for normal gameobjects in my test build.

    Any ideas about how to fix this? I assume it's because the Hybrid Renderer is not communicating with the mipmap streaming system, but I'm hoping there is a workaround or some modifications I could make to the Hybrid Renderer package to fix it.
     
    Last edited: Apr 25, 2019
  2. goonter

    goonter

    Joined:
    Aug 31, 2015
    Posts:
    89
    Also fyi, the texture that is on the entity with mipmap streaming enabled shows up as blue in debugging view, which the docs say is "Textures that are not set to stream, or if there is no renderer calculating the mip levels"
     
  3. goonter

    goonter

    Joined:
    Aug 31, 2015
    Posts:
    89
    If anyone is interested, I ended up writing a system using ECS that uses the Texture Streaming API to change the requested streaming mips manually for Hybrid Rendered Entities. The basic structure is this:
    • StreamingTexture SharedComponent
      • Texture2D MainTex
      • Texture2D NormalTex
      • Texture2D RoughTex
    • StreamingTextureInfo Component (added during conversion if the MeshRenderer mainTex is set to stream)
      • float MeshUVDistributionMetric
      • float TexelCount
    • StreamingTextureChunkMip Component
      • int Value
    • StreamingTextureCamera Component
      • float MipMapBias
      • float FieldOfView
      • float PixelHeight
    A StreamingTextureSystem then queries by SCD StreamingTexture and StreamingTextureInfo, calculates the desired mip for each StreamingTextureInfo based on the info + WorldBounds + CameraPosition. Then I set the lowest requested mip for each chunk in StreamingTextureChunkMip.

    Then a MipChangeSystem that gets all StreamingTexture chunk, sorts them by value using NativeArraySharedValues, then for each unique value, check for the lowest StreamingTextureChunkMip, and finally GetSharedComponentData for the unique StreamingTexture and do the Texture Streaming API calls for each Texture2D (i.e. MainTex.requestedMipmapLevel = lowestChunkMip).

    With 250,000 streaming texture RenderMesh entities, the StreamingTextureSystem runs at about 0.10ms, and the MipChangeSystem at about 0.70ms.

    Hope this helps someone who is wanting to do something similar. This was really the missing piece for me with the megacity demo and adding it in makes all the tech together a really nice world streaming toolset.

    Also just want to say to the Unity team that the Texture Streaming system is amazing. I have six test 8k textures that would usually be 85.3MB each in memory in a build, and with Texture Streaming at the lowest mip, they only load 5.7KB into memory!!! It's like magic.
     
    Last edited: May 1, 2019
  4. Opeth001

    Opeth001

    Joined:
    Jan 28, 2017
    Posts:
    1,068
    Can you share some ScreenShots of Performance + your Code ?
    Thanks!!
     
  5. goonter

    goonter

    Joined:
    Aug 31, 2015
    Posts:
    89
    I'm still working out some weird bugs but I might share it once I get it in a better state! One hack for anyone who attempts something similar: You need to have at least 1 standard non-ecs game object in your scene that uses texture streaming for the system to become enabled. If you don't, I think the whole Texture Streaming system turns off and the ECS-based mip change requests don't do anything.
     
    twobob and Opeth001 like this.
  6. julian-moschuering

    julian-moschuering

    Joined:
    Apr 15, 2014
    Posts:
    529
    My current solution for this. You have to provide CameraData singleton, everything else should work out of the box.
    Does ignore transform scales, as I don't need that atm.

    Code (CSharp):
    1.  
    2. using System.Collections.Generic;
    3. using Unity.Collections;
    4. using Unity.Entities;
    5. using Unity.Mathematics;
    6. using Unity.Rendering;
    7. using Unity.Transforms;
    8. using UnityEngine;
    9.  
    10. public struct CameraData : IComponentData
    11. {
    12.     public ushort Width;
    13.     public ushort Height;
    14.     public half FovX;
    15.     public half FovY;
    16. }
    17.  
    18. internal struct TS_Init : IComponentData
    19. {
    20.     public bool IsValid;
    21. }
    22.  
    23. internal struct TS_MinDistance : IComponentData
    24. {
    25.     public float Value;
    26. }
    27.  
    28. // Buffer as ChunkComponent (which is possible by manipulating Chunk entity directly) leads to crashes on destruction atm
    29. internal struct TS_TextureIndex : IComponentData
    30. {
    31.     public byte Count;
    32.     public unsafe fixed ushort Value[8];
    33. }
    34.  
    35. [UpdateInGroup(typeof(StructuralChangePresentationSystemGroup))]
    36. [WorldSystemFilter(WorldSystemFilterFlags.Default)]
    37. public class TextureStreamingInitSystem : SystemBase
    38. {
    39.     EntityQuery missingQuery;
    40.     public NativeList<float> metric;
    41.     public NativeList<byte> currentMipLevel;
    42.     public NativeList<byte> lastMipLevel;
    43.     public NativeList<byte> maxMipLevel;
    44.     public List<Texture2D> textures;
    45.     Dictionary<Material, ushort[]> materialTextures = new Dictionary<Material, ushort[]>();
    46.  
    47.     protected override void OnCreate()
    48.     {
    49.         missingQuery = GetEntityQuery(new EntityQueryDesc
    50.         {
    51.             All = new[] {ComponentType.ReadOnly<RenderMesh>()},
    52.             Any = new[] {ComponentType.ReadOnly<RenderBounds>(), ComponentType.ReadOnly<WorldRenderBounds>()},
    53.             None = new[] {ComponentType.ChunkComponentReadOnly<TS_MinDistance>(), ComponentType.ChunkComponentReadOnly<TS_TextureIndex>()},
    54.             Options = EntityQueryOptions.IncludeDisabled | EntityQueryOptions.IncludePrefab
    55.         });
    56.         metric = new NativeList<float>(Allocator.Persistent);
    57.         currentMipLevel = new NativeList<byte>(Allocator.Persistent);
    58.         lastMipLevel = new NativeList<byte>(Allocator.Persistent);
    59.         maxMipLevel = new NativeList<byte>(Allocator.Persistent);
    60.         textures = new List<Texture2D>();
    61.     }
    62.  
    63.     protected override void OnDestroy()
    64.     {
    65.         metric.Dispose();
    66.         currentMipLevel.Dispose();
    67.         lastMipLevel.Dispose();
    68.         maxMipLevel.Dispose();
    69.         textures = null;
    70.         base.OnDestroy();
    71.     }
    72.  
    73.     protected override unsafe void OnUpdate()
    74.     {
    75.         var initTypes = new ComponentTypes(ComponentType.ChunkComponent<TS_Init>(),
    76.                                            ComponentType.ChunkComponent<TS_MinDistance>(),
    77.                                            ComponentType.ChunkComponent<TS_TextureIndex>());
    78.         EntityManager.AddComponent(missingQuery, initTypes);
    79.         var renderMeshHandle = GetSharedComponentTypeHandle<RenderMesh>();
    80.  
    81.         // todo: do a bursted parallel for to check if there is anything to do
    82.         // todo: assign by SharedComponentIndex in bursted job if known
    83.         Entities.ForEach((ref TS_TextureIndex textureIndices, ref TS_Init init, in ChunkHeader chunkHeader) =>
    84.         {
    85.             var chunk = chunkHeader.ArchetypeChunk;
    86.             if (init.IsValid && !chunk.DidChange(renderMeshHandle, LastSystemVersion))
    87.                 return;
    88.             init.IsValid = true;
    89.  
    90.             var renderMesh = chunk.GetSharedComponentData(renderMeshHandle, EntityManager);
    91.             var material = renderMesh.material;
    92.             if (material != null) // why?
    93.             {
    94.                 var indices = GetMaterialTextures(material, renderMesh.mesh, renderMesh.subMesh);
    95.                 textureIndices.Count = (byte)math.min(indices.Length, 8);
    96.                 for (var i = 0; i < textureIndices.Count; i++)
    97.                     textureIndices.Value[i] = indices[i];
    98.             }
    99.         }).WithoutBurst().Run();
    100.     }
    101.  
    102.     // todo: yes, we ignore that each texture might be used on different meshes
    103.     //       our evaluation will just always use the highest resolution
    104.     ushort[] GetMaterialTextures(Material material, Mesh mesh, int subMesh)
    105.     {
    106.         if (materialTextures.TryGetValue(material, out var indices))
    107.             return indices;
    108.         var ids = material.GetTexturePropertyNameIDs();
    109.         var indexList = new NativeList<ushort>(Allocator.Temp);
    110.         for (var i = 0; i < ids.Length; i++)
    111.         {
    112.             var texture = material.GetTexture(ids[i]) as Texture2D;
    113.             if (texture == null || !texture.streamingMipmaps)
    114.                 continue;
    115.             var index = textures.IndexOf(texture);
    116.             if (index < 0)
    117.                 index = AddNewTexture(texture, mesh, subMesh);
    118.             indexList.Add((ushort)index);
    119.         }
    120.         indices = indexList.ToArray();
    121.         indexList.Dispose();
    122.         materialTextures[material] = indices;
    123.         return indices;
    124.     }
    125.  
    126.     ushort AddNewTexture(Texture2D texture, Mesh mesh, int subMesh)
    127.     {
    128.         var index = textures.Count;
    129.         textures.Add(texture);
    130.         metric.Add((texture.width * texture.height) / mesh.GetUVDistributionMetric(0));
    131.         currentMipLevel.Add((byte)(texture.mipmapCount - 1));
    132.         maxMipLevel.Add((byte)(texture.mipmapCount - 1));
    133.         lastMipLevel.Add(0);
    134.         return (ushort)index;
    135.     }
    136. }
    137.  
    138. [UpdateInGroup(typeof(UpdatePresentationSystemGroup))]
    139. [WorldSystemFilter(WorldSystemFilterFlags.Default | WorldSystemFilterFlags.EntitySceneOptimizations)]
    140. public class TextureStreamingSystem : SystemBase
    141. {
    142.     TextureStreamingInitSystem textureStreamingInitSystem;
    143.  
    144.     protected override void OnCreate()
    145.     {
    146.         base.OnCreate();
    147.         textureStreamingInitSystem = World.GetExistingSystem<TextureStreamingInitSystem>();
    148.         // make sure everything that happened before is reset
    149.         foreach (var texture in Resources.FindObjectsOfTypeAll<Texture>())
    150.         {
    151.             if (!(texture is Texture2D t2d) || !t2d.streamingMipmaps)
    152.                 continue;
    153.             t2d.ClearRequestedMipmapLevel();
    154.         }
    155.     }
    156.  
    157.     protected override void OnUpdate()
    158.     {
    159.         var refPoint = new NativeArray<float3>(1, Allocator.TempJob);
    160.         Entities.WithAll<CameraData>().ForEach((in LocalToWorld localToWorld) =>
    161.         {
    162.             refPoint[0] = localToWorld.Position;
    163.         }).Schedule();
    164.         CalculateDistances(refPoint);
    165.         CalculateRequired(GetSingleton<CameraData>());
    166.         AssignRequired();
    167.         refPoint.Dispose(Dependency);
    168.     }
    169.  
    170.     void CalculateDistances(NativeArray<float3> refPoint)
    171.     {
    172.         var worldRenderBoundsHandle = GetComponentTypeHandle<WorldRenderBounds>(true);
    173.         var dontCareDistance = 80.0f;
    174.         Entities.WithReadOnly(refPoint).WithReadOnly(worldRenderBoundsHandle)
    175.             .ForEach((ref TS_MinDistance minCameraDistance, in ChunkHeader chunkHeader, in ChunkWorldRenderBounds chunkWorldRenderBounds) =>
    176.             {
    177.                 if (DistanceSq(chunkWorldRenderBounds.Value, refPoint[0]) > dontCareDistance * dontCareDistance)
    178.                 {
    179.                     minCameraDistance.Value = dontCareDistance;
    180.                 }
    181.                 else
    182.                 {
    183.                     // per instance distance
    184.                     var chunk = chunkHeader.ArchetypeChunk;
    185.                     var worldRenderBounds = chunk.GetNativeArray(worldRenderBoundsHandle);
    186.                     var distanceSq = dontCareDistance * dontCareDistance;
    187.                     for (var i = 0; i < worldRenderBounds.Length; i++)
    188.                     {
    189.                         distanceSq = math.min(distanceSq, DistanceSq(worldRenderBounds[i].Value, refPoint[0]));
    190.                     }
    191.                     minCameraDistance.Value = math.sqrt(distanceSq);
    192.                 }
    193.             }).ScheduleParallel();
    194.     }
    195.  
    196.     static float DistanceSq(in AABB aabb, float3 point)
    197.     {
    198.         var nearest = math.max(math.min(point, aabb.Max), aabb.Min);
    199.         return math.distancesq(point, nearest);
    200.     }
    201.  
    202.     struct MipSet
    203.     {
    204.         public ushort TextureIndex;
    205.         public byte Mip;
    206.     }
    207.  
    208.     unsafe void CalculateRequired(CameraData cameraData)
    209.     {
    210.         var metrics = textureStreamingInitSystem.metric.AsArray();
    211.         var currentMipLevel = textureStreamingInitSystem.currentMipLevel.AsArray();
    212.         var cameraHalfAngle = math.radians(cameraData.FovY) * 0.5f;
    213.         var screenHalfHeight = cameraData.Height * 0.5f;
    214.         var cameraEyeToScreenDistanceSq = math.pow(screenHalfHeight / math.tan(cameraHalfAngle), 2.0f);
    215.         var aspectRatio = (float)cameraData.Width / cameraData.Height;
    216.         if (aspectRatio > 1.0f)
    217.             cameraEyeToScreenDistanceSq *= aspectRatio;
    218.  
    219.         var results = new NativeQueue<MipSet>(Allocator.TempJob);
    220.         var resultsWriter = results.AsParallelWriter();
    221.         Entities.WithReadOnly(metrics).WithReadOnly(currentMipLevel)
    222.             .ForEach((ref TS_MinDistance minCameraDistance, in ChunkHeader chunkHeader, in TS_TextureIndex textureIndices) =>
    223.             {
    224.                 for (var i = 0; i < textureIndices.Count; i++)
    225.                 {
    226.                     // todo: scale not handled; we don't scale really
    227.                     var textureIndex = textureIndices.Value[i];
    228.                     var dSq = minCameraDistance.Value * minCameraDistance.Value;
    229.                     var v = metrics[textureIndex] * dSq / cameraEyeToScreenDistanceSq;
    230.                     var baseMip = 0.5f * math.log2(v);
    231.                     var mip = (byte)math.clamp(baseMip, 0, byte.MaxValue);
    232.                     if (mip < currentMipLevel[textureIndex])
    233.                         resultsWriter.Enqueue(new MipSet {TextureIndex = textureIndex, Mip = mip});
    234.                 }
    235.             }).ScheduleParallel();
    236.  
    237.         // reduce
    238.         Job.WithCode(() =>
    239.         {
    240.             while (results.TryDequeue(out var item))
    241.                 currentMipLevel[item.TextureIndex] = item.Mip < currentMipLevel[item.TextureIndex] ? item.Mip : currentMipLevel[item.TextureIndex];
    242.         }).Schedule();
    243.  
    244.         Dependency = results.Dispose(Dependency);
    245.     }
    246.  
    247.     void AssignRequired()
    248.     {
    249.         var count = textureStreamingInitSystem.textures.Count;
    250.         var currentMipLevel = textureStreamingInitSystem.currentMipLevel.AsArray();
    251.         var lastMipLevel = textureStreamingInitSystem.lastMipLevel.AsArray();
    252.         var maxMipLevel = textureStreamingInitSystem.maxMipLevel.AsArray();
    253.         var changeCounter = new NativeArray<int>(1, Allocator.TempJob);
    254.         var changed = new NativeArray<ushort>(count, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
    255.      
    256.         Dependency = new CollectChanged
    257.         {
    258.             ChangeCounter = changeCounter,
    259.             Changed = changed,
    260.             CurrentMipLevel = currentMipLevel,
    261.             LastMipLevel = lastMipLevel,
    262.         }.ScheduleBatch(count, JobsUtility.CacheLineSize, Dependency);
    263.      
    264.         // todo: easy fix, don't forget
    265.         Dependency.Complete();
    266.         Job.WithCode(() =>
    267.         {
    268.             lastMipLevel.CopyFrom(currentMipLevel);
    269.             currentMipLevel.CopyFrom(maxMipLevel);
    270.         }).Schedule();
    271.  
    272.         var changeCount = changeCounter[0];
    273.         for (var i = 0; i < changeCount; i++)
    274.         {
    275.             var textureIndex = changed[i];
    276.             textureStreamingInitSystem.textures[textureIndex].requestedMipmapLevel = lastMipLevel[textureIndex];
    277.         }
    278.  
    279.         changeCounter.Dispose();
    280.         changed.Dispose();
    281.     }
    282.  
    283.     struct CollectChanged : IJobParallelForBatch
    284.     {
    285.         [NativeDisableParallelForRestriction]
    286.         public NativeArray<int> ChangeCounter;
    287.         [NativeDisableParallelForRestriction]
    288.         public NativeArray<ushort> Changed;
    289.         [ReadOnly] public NativeArray<byte> CurrentMipLevel;
    290.         [ReadOnly] public NativeArray<byte> LastMipLevel;
    291.  
    292.         public unsafe void Execute(int startIndex, int count)
    293.         {
    294.             ref int counter = ref UnsafeUtility.ArrayElementAsRef<int>(ChangeCounter.GetUnsafePtr(), 0);
    295.             var end = startIndex + count;
    296.             for (var i = startIndex; i < end; i++)
    297.             {
    298.                 if (CurrentMipLevel[i] != LastMipLevel[i])
    299.                     Changed[Interlocked.Add(ref counter, 1) - 1] = (ushort)i;
    300.             }
    301.         }
    302.     }
    303. }
    304.  
    305.  
     
    Last edited: Dec 17, 2020
    apkdev and nyanpath like this.