Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Feedback NativeArray is terrible slow

Discussion in 'Burst' started by HellGate94, Apr 9, 2021.

  1. HellGate94

    HellGate94

    Joined:
    Sep 21, 2017
    Posts:
    132
    A co-worker sent me a repo where it compares the performance of python, c# and cpp by prime number sieve (https://github.com/davepl/Primes)

    the baseline score i got is (ignoring py):
    C# (.Net Core 3.1, Release): ~4600
    Cpp (x64, Release): ~12000

    now after quickly optimizing the c# version myself a bit (mostly removing BitArray and replacing it with a bool[]) i get a score of around ~7400

    now this is the point where i wonder how Burst would compare to that so i went ahead porting it to unity.
    first just the c# code in unity mono scored around ~6400 what is better than expected

    after porting it to a burst (1.4.6) job and testing it i got an amazing score of..... 1200 (without any checks or leak detection)
    at first i thought it was because of job scheduling so i turned it into a simple static burst function with 0 change in performance. next i thought, maybe its because of pinvoke calls to unmanaged code in c#, so i made a build using il2cpp with a bit of performance increase to ~2200

    after some more trying around i tested using an UnsafeList instead of NativeArray and it more than doubled the score to ~4600. that is still nowhere near the normal c# score but at least better

    the burst job used to test:

    Code (CSharp):
    1.     [BurstCompile]
    2.     public struct prime_sieve : IJob, IDisposable {
    3.         public int sieveSize;
    4.         public NativeArray<bool> bits;
    5.         // public bool[] bits;
    6.  
    7.         public prime_sieve(int size) {
    8.             sieveSize = size;
    9.             // bits = new bool[(int)((size + 1) / 2)];
    10.             bits = new NativeArray<bool>((int)((size + 1) / 2), Allocator.Persistent, NativeArrayOptions.UninitializedMemory);
    11.             for (int i = 0; i < bits.Length; i++) {
    12.                 bits[i] = true;
    13.             }
    14.         }
    15.  
    16.         public void Dispose() {
    17.             if (bits.IsCreated)
    18.                 bits.Dispose();
    19.         }
    20.  
    21.         public void Execute() {
    22.             int factor = 3;
    23.             int q = (int)math.sqrt(sieveSize);
    24.  
    25.             while (factor < q) {
    26.                 for (int num = factor; num <= sieveSize; num++) {
    27.                     if (GetBit(num)) {
    28.                         factor = num;
    29.                         break;
    30.                     }
    31.                 }
    32.  
    33.                 // If marking factor 3, you wouldn't mark 6 (it's a mult of 2) so start with the 3rd instance of this factor's multiple.
    34.                 // We can then step by factor * 2 because every second one is going to be even by definition
    35.  
    36.                 for (int num = factor * 3; num <= sieveSize; num += factor * 2)
    37.                     ClearBit(num);
    38.  
    39.                 factor += 2;
    40.             }
    41.         }
    42.  
    43.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    44.         public bool GetBit(int index) {
    45.             if (index % 2 == 0)
    46.                 return false;
    47.             return bits[index / 2];
    48.         }
    49.  
    50.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    51.         public void ClearBit(int index) {
    52.             if (index % 2 == 0) {
    53.                 Debug.Log("You are setting even bits, which is sub-optimal");
    54.                 return;
    55.             }
    56.             bits[index / 2] = false;
    57.         }
    58.     }
    Edit: just noticed the source repo changed quite a bit since i took a look. will try to update this post to match the new implementation / test wise

    Edit2: quick port from the new cpp implementation to c# / burst gives the following results:

    cpp: ~18000
    mono: ~8000
    nativearray burst: ~1500
    unsafelist burst: ~34000 (almost 2x faster than cpp now wtf)
    both burst test in editor with checks and stuff disabled
     
    Last edited: Apr 9, 2021
  2. sheredom

    sheredom

    Unity Technologies

    Joined:
    Jul 15, 2019
    Posts:
    300
    One thing I noticed wih a quick glance of the inspector was that the modulus operations were using sdiv - which is slow. The reason they were using this is that C# has defined integer overflow, unlike C++ which has undefined behaviour. This makes a huge change to the optimizer, because with C++ it can convert your index % 2 into index & 0x1. You can use the
    AssumeRange
    attribute of Burst to tell the compiler that index is always positive:

    Code (CSharp):
    1. [MethodImpl(MethodImplOptions.AggressiveInlining)]
    2. public bool GetBit([AssumeRange(0, int.MaxValue)] int index)
    3. {
    4.     if (index % 2 == 0)
    5.         return false;
    6.     return bits[index / 2];
    7. }
    8.  
    9. [MethodImpl(MethodImplOptions.AggressiveInlining)]
    10. public void ClearBit([AssumeRange(0, int.MaxValue)] int index)
    11. {
    12.     if (index % 2 == 0)
    13.     {
    14.         Debug.Log("You are setting even bits, which is sub-optimal");
    15.         return;
    16.     }
    17.     bits[index / 2] = false;
    18. }
     
    Slight0 and laurentlavigne like this.
  3. HellGate94

    HellGate94

    Joined:
    Sep 21, 2017
    Posts:
    132
    interesting and good to know. however that would affect the performance of the UnsafeList test just as much. also in the edit of my post i tried with a quick and dirty port of the new cpp implementation https://github.com/davepl/Primes/blob/main/PrimeCPP/PrimeCPP.cpp#L57

    this implementation does no longer use modulo and is mostly just loops with array access and results are similar

     
  4. sheredom

    sheredom

    Unity Technologies

    Joined:
    Jul 15, 2019
    Posts:
    300
    So I did some more digging with a sieve size of 100,000:
    • 0.045ms for NativeArray
    • 0.045ms for NativeList
    • 0.023ms for UnsafeList
    If I force safety checks off for the NativeArray example:
    • 0.023ms for NativeArray with safety checks disabled
    Also with Burst disabled:
    • 2.76ms for NativeArray
    • 3.74ms for NativeList
    • 0.85ms for UnsafeList
    I don't really know what the numbers above you were quoting were, but the Burst numbers here seem reasonable.
     
    forestrf likes this.
  5. HellGate94

    HellGate94

    Joined:
    Sep 21, 2017
    Posts:
    132
    yea your numbers look quite reasonable

    the numbers i posted are the number of passes / 10sec i got (creating, running, disposing)

    one thing i noticed is that safety checks and leak detection never really made a difference for me even compared to builds.
     
    sheredom likes this.
  6. laurentlavigne

    laurentlavigne

    Joined:
    Aug 16, 2012
    Posts:
    6,225
    What do you think would speed this up?
    Code (CSharp):
    1. using System;
    2. using UnityEngine;
    3. using System.Collections.Generic;
    4. using Unity.Burst;
    5. using Unity.Collections;
    6. using Unity.Jobs;
    7. using Unity.Mathematics;
    8. using Unity.Profiling;
    9. using Random = UnityEngine.Random;
    10.  
    11. [DefaultExecutionOrder(-9001)]
    12. public class BlissManager : MonoBehaviour
    13. {
    14.     public static BlissManager _instance;
    15.     public static BlissManager instance
    16.     {
    17.         get
    18.         {
    19.             if (!_instance)
    20.             {
    21.                 _instance = FindObjectOfType<BlissManager>();
    22.                 #if UNITY_EDITOR
    23.                 Debug.LogWarning($"Expensive find object in {_instance?.gameObject.name} ... scene: {_instance?.gameObject.scene.name}");
    24.                 #endif
    25.             }
    26.             return _instance;
    27.         }
    28.     }
    29.     List<Booster> _boosters = new List<Booster>();
    30.     Texture2D _tex = null;
    31.     public float boostEffectMultiplier = 2f;
    32.     List<Rect> _recalculate = new List<Rect>();
    33.     const int HALF_GRID_SIZE = 100;
    34.     public static int[] _grid;
    35.     public int baseBliss = 0;
    36.     float[] _lastMap = null;
    37.     NativeArray<half> _newTextureMapHalfPointer;
    38.     int _changeUMin = 0;
    39.     int _changeUMax = 0;
    40.     int _changeVMin = 0;
    41.     int _changeVMax = 0;
    42.     public RenderTexture blissRT;
    43.     readonly half _good = (half) 1;
    44.     readonly half _neutral = (half) 0.5;
    45.     readonly half _bad = (half) 0;
    46.     [Space] public bool testBlit = false;
    47.     bool Paused => Time.timeScale < 0.1f;
    48.     //job inout variables
    49.     JobHandle recalculateBoostJobHandle;
    50.     NativeArray<int> _gridNative;
    51.  
    52.     public void Awake()
    53.     {
    54.         if (_instance)
    55.         {
    56.             Destroy(gameObject);
    57.             return;
    58.         }
    59.         _instance = this;
    60.         transform.parent = null;
    61.         DontDestroyOnLoad(gameObject);
    62.         // _newTextureMapHalfPointer = new NativeArray<half>(HALF_GRID_SIZE * HALF_GRID_SIZE * 4, Allocator.Persistent);
    63.         _lastMap = new float[HALF_GRID_SIZE * HALF_GRID_SIZE * 4];
    64.         for (int i = 0; i < _lastMap.Length; i++) { _lastMap[i] = 0.5f; }
    65.         _tex = new Texture2D(HALF_GRID_SIZE * 2, HALF_GRID_SIZE * 2, TextureFormat.RHalf, mipChain: true);
    66.         //set the bliss map to 0.5 which is neutral
    67.         var newMap = _tex.GetRawTextureData<half>();
    68.         for (int i = 0; i < newMap.Length; i++) { newMap[i] = _neutral; }
    69.         _tex.Apply(true);
    70.         if (testBlit)
    71.             Graphics.Blit(_tex, blissRT);
    72.         else
    73.             Graphics.CopyTexture(_tex, blissRT);
    74.     }
    75.  
    76.     void OnEnable()
    77.     {
    78.         Shader.SetGlobalFloat("_BlissMapSize", HALF_GRID_SIZE * 2);
    79.     }
    80.  
    81.     ParticleSystem _corruptionParticle;
    82.  
    83.     public void Start()
    84.     {
    85.         if (beautyCorrupted)
    86.         {
    87.             _corruptionParticle = (Instantiate(beautyCorrupted.gameObject, Vector3.zero, Quaternion.identity) as GameObject).GetComponent<ParticleSystem>();
    88.             _corruptionParticle.enableEmission = false;
    89.             _corruptionParticle.transform.parent = transform;
    90.         }
    91.         _changeVMax = HALF_GRID_SIZE * 2;
    92.         _changeUMax = HALF_GRID_SIZE * 2;
    93.         CalculateEntireBlissMap();
    94.     }
    95.  
    96.     // emit particles
    97.     void FixedUpdate()
    98.     {
    99.         if (performanceBeautifyEnabled)
    100.         {
    101.             for (int u = 0; u < HALF_GRID_SIZE * 2; u++)
    102.             {
    103.                 for (int v = 0; v < HALF_GRID_SIZE * 2; v++)
    104.                 {
    105.                     if (_beautyGrid[u, v] != null)
    106.                     {
    107.                         BeautyGrid beauty = _beautyGrid[u, v];
    108.                         if (beauty.bliss <= .3f)
    109.                         {
    110.                             if (Random.value > CORRUPTIONPARTICLEPROBABILITY)
    111.                             {
    112.                                 Vector3 randomCircle = Random.insideUnitSphere;
    113.                                 randomCircle.y = 0;
    114.                                 _corruptionParticle.Emit(beauty.position + randomCircle, Vector3.zero, 1, Random.Range(0.5f, 1.5f), Color.white);
    115.                             }
    116.                         }
    117.                     }
    118.                 }
    119.             }
    120.         }
    121.     }
    122.  
    123.     //recalculate bliss
    124.     public void Update()
    125.     {
    126.         if (Paused)
    127.             return;
    128.         if (_recalculate.Count > 0)
    129.         {
    130.             Rect rmax = _recalculate[0];
    131.             for (int i = 1; i < _recalculate.Count; i++)
    132.             {
    133.                 Rect r = _recalculate[i];
    134.                 rmax.xMin = Mathf.Min(rmax.xMin, r.xMin);
    135.                 rmax.xMax = Mathf.Max(rmax.xMax, r.xMax);
    136.                 rmax.yMin = Mathf.Min(rmax.yMin, r.yMin);
    137.                 rmax.yMax = Mathf.Max(rmax.yMax, r.yMax);
    138.             }
    139.             RecalculateBoostJobified(rmax);
    140.             _recalculate = new List<Rect>();
    141.         }
    142.     }
    143.  
    144.     public void RegisterBooster(Booster b)
    145.     {
    146.         _boosters.Add(b);
    147.     }
    148.  
    149.     public void RemoveBooster(Booster b)
    150.     {
    151.         _boosters.Remove(b);
    152.     }
    153.  
    154.     public float GetBoostMultiplier(Vector3 pos, bool negative)
    155.     {
    156.         int boost = GetBoost(pos);
    157.  
    158.         // are we boosted by corruption? if so -ve the boost
    159.         if (negative) boost = -boost;
    160.  
    161.         // if boosted (i.e. positive bliss for good guys, negative for bad guys) return the multiplier
    162.         if (boost <= 0) return 1;
    163.         return boostEffectMultiplier;
    164.     }
    165.  
    166.     public void QueueRecalculateBoost(Rect r)
    167.     {
    168.         _recalculate.Add(r);
    169.     }
    170.  
    171.     public void CalculateEntireBlissMap()
    172.     {
    173.         RecalculateBoostJobified(new Rect(-HALF_GRID_SIZE, -HALF_GRID_SIZE, HALF_GRID_SIZE * 2, HALF_GRID_SIZE * 2));
    174.     }
    175.  
    176.     struct Booster4Job
    177.     {
    178.         public Vector3 pos;
    179.         public int range;
    180.         public int boostAmount;
    181.     }
    182.     public struct NativeUnit<T> where T : struct
    183.     {
    184.         NativeArray<T> internalContainer;
    185.  
    186.         public NativeUnit(Allocator allocator)
    187.         {
    188.             internalContainer = new NativeArray<T>(1, allocator);
    189.         }
    190.  
    191.         public void Dispose()
    192.         {
    193.             internalContainer.Dispose();
    194.         }
    195.  
    196.         public T Value
    197.         {
    198.             get { return internalContainer[0]; }
    199.             set { internalContainer[0] = value; }
    200.         }
    201.     }
    202.     List<Booster4Job> _b4j = new List<Booster4Job>(1000);
    203.     bool jobStarted = false;
    204.  
    205.     static readonly ProfilerMarker ___recalculateBliss = new ProfilerMarker ("-Recalculate Bliss");
    206.     static readonly ProfilerMarker ___applyTexture = new ProfilerMarker("--apply Texture");
    207.  
    208.     public void RecalculateBoostJobified(Rect r)
    209.     {
    210.         ___recalculateBliss.Begin();
    211.         if (_grid == null)
    212.             _grid = new int[HALF_GRID_SIZE * 2 * HALF_GRID_SIZE * 2];
    213.         // prep native data for the job
    214.         _newTextureMapHalfPointer = _tex.GetPixelData<half>(0);
    215.         _b4j.Clear();
    216.         foreach (Booster boost in _boosters)
    217.         {
    218.             if (!boost || !boost.gameObject.activeInHierarchy || !boost.boostEnabled)
    219.                 continue;
    220.             _b4j.Add(new Booster4Job() {pos = boost.GetIntPosition(), range = boost.range, boostAmount = boost.boostAmount});
    221.         }
    222.         _gridNative = new NativeArray<int>(_grid, Allocator.TempJob);
    223.         var recalculateBoostJob = new RecalculateBoostJob
    224.         {
    225.             r_IN = r,
    226.             boosters4job_IN = new NativeArray<Booster4Job>(_b4j.ToArray(), Allocator.TempJob),
    227.             baseBliss_IN = baseBliss,
    228.             grid_OUT = _gridNative,
    229.             newTextureMapHalfPointer_INOUT = _newTextureMapHalfPointer
    230.         };
    231.         recalculateBoostJob.Run();
    232.         //unpack
    233.         _gridNative.CopyTo(_grid);
    234.         //clean up
    235.         _gridNative.Dispose();
    236.         // texture
    237.         ___applyTexture.Begin();
    238.         _tex.Apply(true);
    239.         if (testBlit)
    240.             Graphics.Blit(_tex, blissRT);
    241.         else
    242.             Graphics.CopyTexture(_tex, blissRT);
    243.         ___applyTexture.End();
    244.         ___recalculateBliss.End();
    245.     }
    246.  
    247.     [BurstCompile]
    248.     struct RecalculateBoostJob : IJob
    249.     {
    250.         [ReadOnly] public Rect r_IN;
    251.         [DeallocateOnJobCompletion]
    252.         [ReadOnly] public NativeArray<Booster4Job> boosters4job_IN;
    253.         [ReadOnly] public int baseBliss_IN;
    254.         public NativeArray<int> grid_OUT;
    255.         public NativeArray<half> newTextureMapHalfPointer_INOUT;
    256.  
    257.         public void Execute()
    258.         {
    259.             // init the blissmap
    260.             var blissmap_OUT = new NativeArray<int>(HALF_GRID_SIZE * 2 * HALF_GRID_SIZE * 2, Allocator.Temp, NativeArrayOptions.ClearMemory);
    261.             for (int i = 0; i < blissmap_OUT.Length; i++)
    262.                 blissmap_OUT[i] = 0;
    263.             // convert to int and clamp
    264.             int xMin = Mathf.Max((int) r_IN.xMin, -HALF_GRID_SIZE);
    265.             int xMax = Mathf.Min((int) r_IN.xMax, HALF_GRID_SIZE);
    266.             int zMin = Mathf.Max((int) r_IN.yMin, -HALF_GRID_SIZE);
    267.             int zMax = Mathf.Min((int) r_IN.yMax, HALF_GRID_SIZE);
    268.             // go through each booster
    269.             int b;
    270.             for (int i = 0; i < boosters4job_IN.Length; i++)
    271.             {
    272.                 var boost = boosters4job_IN[i];
    273.                 var pos = boost.pos;
    274.                 int cx = (int) pos.x;
    275.                 int dx2 = cx + boost.range + 2;
    276.                 int dx1 = cx - boost.range - 1;
    277.                 int cz = (int) pos.z;
    278.                 int dz2 = cz + boost.range + 2;
    279.                 int dz1 = cz - boost.range - 1;
    280.                 // early exit if the booster isn't within the changed rect
    281.                 if (dx2 < xMin || dz2 < zMin || dx1 > xMax || dz1 > zMax)
    282.                     continue;
    283.                 dx1 = Mathf.Max(dx1, xMin);
    284.                 dx2 = Mathf.Min(dx2, xMax);
    285.                 dz1 = Mathf.Max(dz1, zMin);
    286.                 dz2 = Mathf.Min(dz2, zMax);
    287.                 // convert current boost [-1,+1] to binary mask
    288.                 b = Boost2BlissBitmask(boost.boostAmount);
    289.                 int rangeSqr = boost.range * boost.range;
    290.                 for (int z = dz1; z < dz2; z++)
    291.                 {
    292.                     for (int x = dx1; x < dx2; x++)
    293.                     {
    294.                         int distSqr = (x - cx) * (x - cx) + (z - cz) * (z - cz);
    295.                         if (distSqr < rangeSqr)
    296.                         {
    297.                             var boostCoord = BoostCoord(x, z);
    298.                             // bliss bitwise math
    299.                             blissmap_OUT[boostCoord] = blissmap_OUT[boostCoord] | b;
    300.                         }
    301.                     }
    302.                 }
    303.             }
    304.             // composite pass with base bliss
    305.             b = Boost2BlissBitmask(baseBliss_IN);
    306.             for (int x = xMin; x < xMax; x++)
    307.             {
    308.                 for (int z = zMin; z < zMax; z++)
    309.                 {
    310.                     var boostCoord = BoostCoord(x, z);
    311.                     grid_OUT[boostCoord] = blissmap_OUT[boostCoord] | b;
    312.                 }
    313.             }
    314.             // expand change rect for rendering
    315.             int4 uv = new int4(Mathf.Min(HALF_GRID_SIZE * 2, HALF_GRID_SIZE + xMin), Mathf.Max(0, HALF_GRID_SIZE + xMax), Mathf.Min(HALF_GRID_SIZE * 2, HALF_GRID_SIZE + zMin), Mathf.Max(0, HALF_GRID_SIZE + zMax));
    316.             // texture update
    317.             for (int u = uv.x; u < uv.y; u++)
    318.             {
    319.                 for (int v = uv.z; v < uv.w; v++)
    320.                 {
    321.                     int coord = u + v * HALF_GRID_SIZE * 2;
    322.                     int bliss = BlissBitmask2Boost(grid_OUT[coord]);
    323.                     half newf = (half) (bliss == -1 ? 0 : bliss == 0 ? 0.5 : 1);
    324.                     newTextureMapHalfPointer_INOUT[coord] = newf;
    325.                 }
    326.             }
    327.         }
    328.     }
    329.  
    330.     static int Boost2BlissBitmask(int b)
    331.     {
    332.         if (b > 0)
    333.             return 1;
    334.         if (b < 0)
    335.             return 2;
    336.         return 0;
    337.     }
    338.  
    339.     public static int BoostCoord(int x, int z)
    340.     {
    341.         if (x > HALF_GRID_SIZE - 1) x = HALF_GRID_SIZE - 1;
    342.         if (z > HALF_GRID_SIZE - 1) z = HALF_GRID_SIZE - 1;
    343.         if (x < -(HALF_GRID_SIZE - 1)) x = -(HALF_GRID_SIZE - 1);
    344.         if (z < -(HALF_GRID_SIZE - 1)) z = -(HALF_GRID_SIZE - 1);
    345.         return (x + HALF_GRID_SIZE) + (z + HALF_GRID_SIZE) * HALF_GRID_SIZE * 2;
    346.     }
    347.  
    348.     public int GetBoost(Vector3 pos)
    349.     {
    350.         int x = Mathf.FloorToInt(pos.x + 0.001f);
    351.         int z = Mathf.FloorToInt(pos.z + 0.001f);
    352.         return GetBoost(x, z);
    353.     }
    354.  
    355.     public static int GetBoost(int coord)
    356.     {
    357.         return BlissBitmask2Boost(_grid[coord]);
    358.     }
    359.  
    360.     public int GetBoost(int x, int z)
    361.     {
    362.         if (_grid == null) return 0;
    363.         return BlissBitmask2Boost(_grid[BoostCoord(x, z)]);
    364.     }
    365.  
    366.     static int BlissBitmask2Boost(int b)
    367.     {
    368.         if (b == 1) return 1;
    369.         if (b == 2) return -1;
    370.         return 0;
    371.     }
    372.  
    373.     int ConvertWorldToUVCoordinates(int2 worldCoordinates)
    374.     {
    375.         int result = 0;
    376.         Physics.Raycast(new Vector3(worldCoordinates.x, 1, worldCoordinates.y), Vector3.down, out RaycastHit hit, 2);
    377.         var uv = hit.textureCoord;
    378.         return (int) uv.x + (int) uv.y * HALF_GRID_SIZE * 2;
    379.     }
    380.  
    381.     public void OnDrawGizmosSelected()
    382.     {
    383.         Gizmos.color = Color.yellow;
    384.         Gizmos.DrawWireCube(Vector3.zero, new Vector3(HALF_GRID_SIZE * 2, 1, HALF_GRID_SIZE * 2));
    385.     }
    386.  
    387.     // Beautification
    388.     public bool performanceBeautifyEnabled;
    389.     public GameObject beautifyBliss;
    390.     public ParticleSystem beautyCorrupted;
    391.     public LayerMask beautyLayer;
    392. // what's on the terrain, optimization
    393.     class BeautyGrid
    394.     {
    395.         public Vector3 position;
    396.         public Vector3 normal;
    397.         public float bliss;
    398.         public GameObject blissObject;
    399.         public ParticleSystem corruptionParticle;
    400.     }
    401.     BeautyGrid[,] _beautyGrid = new BeautyGrid[HALF_GRID_SIZE * 2, HALF_GRID_SIZE * 2];
    402.     RaycastHit _hit = new RaycastHit();
    403.  
    404.     public void ForceRecalculateBoostAndUpdateFoliage()
    405.     {
    406.         RecalculateBoostJobified(new Rect(-HALF_GRID_SIZE, -HALF_GRID_SIZE, HALF_GRID_SIZE * 2, HALF_GRID_SIZE * 2));
    407.     }
    408.  
    409.     void InstantiateBlissObject(int u, int v)
    410.     {
    411.         Vector3 n = _beautyGrid[u, v].normal;
    412.         Quaternion rotation = Quaternion.AngleAxis(Random.value * 360, n);
    413.         Vector3 randomOffset = Vector3.Cross(n, Random.insideUnitSphere);
    414.         GameObject blissObjectInstance = Instantiate(beautifyBliss, _beautyGrid[u, v].position + randomOffset * .3f, rotation) as GameObject;
    415.         //                    Color randomGrassColor = Random.Range(.5f,1)*Color.white;
    416.         //                    randomGrassColor.a = 1;
    417.         //                    blissObjectInstance.renderer.material.color = randomGrassColor;
    418.         blissObjectInstance.transform.parent = transform;
    419.         blissObjectInstance.SetActive(false);
    420.         _beautyGrid[u, v].blissObject = blissObjectInstance;
    421.     }
    422.  
    423.     void InstantiateCorruptionParticle(int u, int v)
    424.     {
    425.         _beautyGrid[u, v].corruptionParticle = (Instantiate(beautyCorrupted.gameObject, _beautyGrid[u, v].position, Quaternion.identity) as GameObject).GetComponent<ParticleSystem>();
    426.         _beautyGrid[u, v].corruptionParticle.enableEmission = false;
    427.         _beautyGrid[u, v].corruptionParticle.gameObject.SetActive(false);
    428.         _beautyGrid[u, v].corruptionParticle.transform.parent = transform;
    429.     }
    430.  
    431. //add or animate grass or lava to match bliss
    432.     void Beautify(int u, int v, float bliss)
    433.     {
    434.         // turn off all particles and grass and skip if beautify is off
    435.         if (!performanceBeautifyEnabled)
    436.         {
    437.             if (_beautyGrid[u, v] != null)
    438.             {
    439.                 if (_beautyGrid[u, v].blissObject)
    440.                     _beautyGrid[u, v].blissObject.SetActive(false);
    441.                 if (_beautyGrid[u, v].corruptionParticle)
    442.                     _beautyGrid[u, v].corruptionParticle.enableEmission = false;
    443.                 _beautyGrid[u, v].bliss = 0;
    444.             }
    445.             return;
    446.         }
    447.         UnityEngine.Profiling.Profiler.BeginSample("beautify");
    448.         //init beautify grid element
    449.         if (_beautyGrid[u, v] == null)
    450.         {
    451.             if (Physics.Raycast(new Vector3(u - HALF_GRID_SIZE + .5f, 30, v - HALF_GRID_SIZE + .5f), -Vector3.up, out _hit, 40, beautyLayer))
    452.             {
    453.                 _beautyGrid[u, v] = new BeautyGrid();
    454.                 _beautyGrid[u, v].position = _hit.point;
    455.                 _beautyGrid[u, v].normal = _hit.normal;
    456.             }
    457.         }
    458.         if (_beautyGrid[u, v] == null)
    459.             return;
    460.         _beautyGrid[u, v].bliss = bliss;
    461.         // calculate scale of bliss grass
    462.         #if UNITY_IOS || UNITY_SWITCH
    463.         // skip every 2 beautification
    464.         if (u % 2 == 0 || v % 2 == 0)
    465.             return;
    466.         #endif
    467.         if (bliss >= 0.7f)
    468.         {
    469.             //bliss
    470.             if (!_beautyGrid[u, v].blissObject)
    471.                 InstantiateBlissObject(u, v);
    472.             _beautyGrid[u, v].blissObject.SetActive(true);
    473.             _beautyGrid[u, v].blissObject.transform.localScale = 2 * (bliss - .5f) * Vector3.one;
    474.         } else
    475.         {
    476.             if (_beautyGrid[u, v].blissObject)
    477.                 _beautyGrid[u, v].blissObject.SetActive(false);
    478.         }
    479.         UnityEngine.Profiling.Profiler.EndSample();
    480.     }
    481.     #if UNITY_IOS || unity_
    482.     const float CORRUPTIONPARTICLEPROBABILITY = 0.990f;
    483.     #else
    484.     const float CORRUPTIONPARTICLEPROBABILITY = 0.95f;
    485.     #endif
    486. }