Search Unity

  1. Looking for a job or to hire someone for a project? Check out the re-opened job forums.
    Dismiss Notice
  2. Unity 2020 LTS & Unity 2021.1 have been released.
    Dismiss Notice
  3. Good news ✨ We have more Unite Now videos available for you to watch on-demand! Come check them out and ask our experts any questions!
    Dismiss Notice

Script for generating ShadowCaster2Ds for Tilemaps

Discussion in '2D' started by ThundThund, Jun 6, 2020.

  1. ThundThund

    ThundThund

    Joined:
    Feb 7, 2017
    Posts:
    152
    Hi everybody,

    Since I did not find any built-in functionality to make shadow casters fit in the shape of the tilemaps (something that I think it is fundamental) I made my own script to achieve such thing without having to manually create and adjust every shadow caster.

    Just add the script to your project and you will see a new option in the top menu of the editor. Once you execute it, shadow casters will be created for all the tilemaps in the open scene. It requires that the tilemaps have CompositeCollider2Ds. I made it as generic as possible so it is not limited to tilemaps, it only depends on composite colliders.

    I hope it helps you to save some time.

    Other code I shared:
    Pixel perfect LineRenderer2D
    Delaunay Triangulation with constrained edges
    Target sorting layers as assets

    Regards.



    Code (CSharp):
    1. #if UNITY_EDITOR
    2.  
    3. using System.Collections.Generic;
    4. using System.Reflection;
    5. using UnityEngine;
    6. using UnityEngine.Experimental.Rendering.Universal;
    7.  
    8. /// <summary>
    9. /// It extends the ShadowCaster2D class in order to be able to modify some private data members.
    10. /// </summary>
    11. public static class ShadowCaster2DExtensions
    12. {
    13.     /// <summary>
    14.     /// Replaces the path that defines the shape of the shadow caster.
    15.     /// </summary>
    16.     /// <remarks>
    17.     /// Calling this method will change the shape but not the mesh of the shadow caster. Call SetPathHash afterwards.
    18.     /// </remarks>
    19.     /// <param name="shadowCaster">The object to modify.</param>
    20.     /// <param name="path">The new path to define the shape of the shadow caster.</param>
    21.     public static void SetPath(this ShadowCaster2D shadowCaster, Vector3[] path)
    22.     {
    23.         FieldInfo shapeField = typeof(ShadowCaster2D).GetField("m_ShapePath",
    24.                                                                BindingFlags.NonPublic |
    25.                                                                BindingFlags.Instance);
    26.         shapeField.SetValue(shadowCaster, path);
    27.     }
    28.  
    29.     /// <summary>
    30.     /// Replaces the hash key of the shadow caster, which produces an internal data rebuild.
    31.     /// </summary>
    32.     /// <remarks>
    33.     /// A change in the shape of the shadow caster will not block the light, it has to be rebuilt using this function.
    34.     /// </remarks>
    35.     /// <param name="shadowCaster">The object to modify.</param>
    36.     /// <param name="hash">The new hash key to store. It must be different from the previous key to produce the rebuild. You can use a random number.</param>
    37.     public static void SetPathHash(this ShadowCaster2D shadowCaster, int hash)
    38.     {
    39.         FieldInfo hashField = typeof(ShadowCaster2D).GetField("m_ShapePathHash",
    40.                                                               BindingFlags.NonPublic |
    41.                                                               BindingFlags.Instance);
    42.         hashField.SetValue(shadowCaster, hash);
    43.     }
    44. }
    45.  
    46. /// <summary>
    47. /// It provides a way to automatically generate shadow casters that cover the shapes of composite colliders.
    48. /// </summary>
    49. /// <remarks>
    50. /// Specially recommended for tilemaps, as there is no built-in tool that does this job at the moment.
    51. /// </remarks>
    52. public class ShadowCaster2DGenerator
    53. {
    54.     [UnityEditor.MenuItem("Generate Shadow Casters", menuItem = "Tools/Generate Shadow Casters")]
    55.     public static void GenerateShadowCasters()
    56.     {
    57.         CompositeCollider2D[] colliders = GameObject.FindObjectsOfType<CompositeCollider2D>();
    58.  
    59.         for(int i = 0; i < colliders.Length; ++i)
    60.         {
    61.             GenerateTilemapShadowCasters(colliders[i]);
    62.         }
    63.     }
    64.  
    65.     /// <summary>
    66.     /// Given a Composite Collider 2D, it replaces existing Shadow Caster 2Ds (children) with new Shadow Caster 2D objects whose
    67.     /// shapes coincide with the paths of the collider.
    68.     /// </summary>
    69.     /// <remarks>
    70.     /// It is recommended that the object that contains the collider component has a Composite Shadow Caster 2D too.
    71.     /// It is recommended to call this method in editor only.
    72.     /// </remarks>
    73.     /// <param name="collider">The collider which will be the parent of the new shadow casters.</param>
    74.     public static void GenerateTilemapShadowCasters(CompositeCollider2D collider)
    75.     {
    76.         // First, it destroys the existing shadow casters
    77.         ShadowCaster2D[] existingShadowCasters = collider.GetComponentsInChildren<ShadowCaster2D>();
    78.  
    79.         for (int i = 0; i < existingShadowCasters.Length; ++i)
    80.         {
    81.             GameObject.DestroyImmediate(existingShadowCasters[i].gameObject);
    82.         }
    83.  
    84.         // Then it creates the new shadow casters, based on the paths of the composite collider
    85.         int pathCount = collider.pathCount;
    86.         List<Vector2> pointsInPath = new List<Vector2>();
    87.         List<Vector3> pointsInPath3D = new List<Vector3>();
    88.  
    89.         for (int i = 0; i < pathCount; ++i)
    90.         {
    91.             collider.GetPath(i, pointsInPath);
    92.  
    93.             GameObject newShadowCaster = new GameObject("ShadowCaster2D");
    94.             newShadowCaster.isStatic = true;
    95.             newShadowCaster.transform.SetParent(collider.transform, false);
    96.  
    97.             for(int j = 0; j < pointsInPath.Count; ++j)
    98.             {
    99.                 pointsInPath3D.Add(pointsInPath[j]);
    100.             }
    101.  
    102.             ShadowCaster2D component = newShadowCaster.AddComponent<ShadowCaster2D>();
    103.             component.SetPath(pointsInPath3D.ToArray());
    104.             component.SetPathHash(Random.Range(int.MinValue, int.MaxValue)); // The hashing function GetShapePathHash could be copied from the LightUtility class
    105.  
    106.             pointsInPath.Clear();
    107.             pointsInPath3D.Clear();
    108.         }
    109.      
    110.         UnityEditor.SceneManagement.EditorSceneManager.MarkAllScenesDirty();
    111.     }
    112.  
    113. }
    114.  
    115. #endif
    116.  
     
    Last edited: Apr 18, 2021 at 6:39 PM
    Mindjar, id0, lilacsky824 and 14 others like this.
  2. Lo-renzo

    Lo-renzo

    Joined:
    Apr 8, 2018
    Posts:
    730
    MD_Reptile likes this.
  3. ThundThund

    ThundThund

    Joined:
    Feb 7, 2017
    Posts:
    152
    Regarding performance, it only creates a GameObject with a ShaderCaster2D component per independent block (i.e. non-continuous block) of the Tilemap. For example, in the image, it creates 14 objects. I don't know how the shadow casting system performs as the number of shadow casters increase, that's my only doubt and nobody can do anything to solve that (apart from Unity guys). Besides, I don't know if having a CompositeShadowCaster2D makes a difference or not in terms of performance.
     
    Redninja7002 likes this.
  4. japhib

    japhib

    Joined:
    May 26, 2017
    Posts:
    61
    Wow, this is awesome!
     
  5. Kashlavor

    Kashlavor

    Joined:
    Jan 10, 2014
    Posts:
    2
    Looks great, thank you.
     
  6. FarGalaxy

    FarGalaxy

    Joined:
    Jun 24, 2019
    Posts:
    1
    Hi, the script works fine, but an error occurs when building the project, because Unity tries to add this script with UnityEditor to the build.

    I solved this problem by adding the
    #if UNITY_EDITOR
    at the beginning and
    #endif
    at the end of the script.
     
  7. ThundThund

    ThundThund

    Joined:
    Feb 7, 2017
    Posts:
    152
    Sorry I forgot to add the compiler definitions. I've updated the script.
     
    thathurtabit likes this.
  8. Dannyxv8

    Dannyxv8

    Joined:
    Jun 29, 2020
    Posts:
    1
    Wow, this actually worked unlike everything else I've tried so far! You're a god!! I just hope there's not too many side effects or performance intensive...
     
  9. PaulKhill

    PaulKhill

    Joined:
    Jul 30, 2020
    Posts:
    15
    Quick addition for anyone trying to make this work : if you are using a Tilemap Collider 2D, you must add a Composite Collider 2D and check the "Used by Composite" option in the Tilemap Collider 2D for this to work. (You may also want to set the bodytype of the rigid body to static, if you don't want your whole tilemap to fly)

    Great work friend !
     
  10. Mazemace

    Mazemace

    Joined:
    Jan 4, 2013
    Posts:
    7
    Looks great!

    The light travelled through walls for me, so I added the following code

    Code (CSharp):
    1. component.selfShadows = true;
     
  11. unity_AJ5N9uSO-J1hIw

    unity_AJ5N9uSO-J1hIw

    Joined:
    Nov 9, 2020
    Posts:
    1
    Hello,
    I can't seem to find the option that is supposed to execute. Could you send a screenshot or describe more specifically to me where I can find it? Sorry for this dumb question.

    Edit:
    I found it. It's just me who had not searched properly. If someone else has the same issue that I had, here is how to access it.

    Tools -> Generate Shadow Casters

    Yep, it's that simple.
    Again, sorry for this question.
     
    Last edited: Nov 9, 2020
    Subnormal and japhib like this.
  12. Yumineko

    Yumineko

    Joined:
    May 4, 2016
    Posts:
    2
    Great! Thank you for the useful script.
     
  13. ThundThund

    ThundThund

    Joined:
    Feb 7, 2017
    Posts:
    152
    Added the line "UnityEditor.SceneManagement.EditorSceneManager.MarkAllScenesDirty();". Sometimes I just open the scene and generate the shadow casters, without making any other change. Since the editor does not realize about those changes, they are not saved. Pressing the Play button makes the scene restore its original content, as if nothing happened. Now the scenes are marked as dirty and everything works as expected.
     
  14. mega8200

    mega8200

    Joined:
    Aug 18, 2020
    Posts:
    1
    Hello, does the script work on runtime
     
  15. ThundThund

    ThundThund

    Joined:
    Feb 7, 2017
    Posts:
    152
    Never tried.
     
  16. japhib

    japhib

    Joined:
    May 26, 2017
    Posts:
    61
    I'm pretty sure I got it to work at runtime in the past. You just have to remove anything that uses a "UnityEditor" namespace, like this line at the bottom:

    Code (CSharp):
    1. UnityEditor.SceneManagement.EditorSceneManager.MarkAllScenesDirty();
     
  17. LordHobbas

    LordHobbas

    Joined:
    Dec 10, 2018
    Posts:
    3
    Hi!

    I can't seem to get this to work.
    If I understand correctly, all I need to do is
    1. Import Universal RP from Package Manager.
    2. Copy paste the script to somewhere in my project
    3. Create a Tilemap, (and edit the collisions of course).
    4. Add a Tilemap Collider 2D and Composite Collider 2D to the Tilemap. (Make sure to check "Used By Composite" under TileMap Collider 2D).
    5. Lastly, run Tools > Generate Shadow Casters.

    Nothing changes after running it. I tested creating a new project for this but it made no difference.
    I'm running Unity 2020.2.4f1

    Is there anything obvious I'm missing?
     
  18. ThundThund

    ThundThund

    Joined:
    Feb 7, 2017
    Posts:
    152
    Hi LordHobbas, let's try to find the issue. I'm not sure but could it be related to the Geometry Type in the CompositeCollider2D?
     
  19. LordHobbas

    LordHobbas

    Joined:
    Dec 10, 2018
    Posts:
    3
    Thanks for the quick response, I didn't expect that!
    It's set to "Outlines".
    I can see now that there are several "ShadowCaster2D" created as children to the Tilemap object, and they're recreated everytime I run the script. So the script seems to work perfectly. I think I've missed something else.
    Do I need to enable something to make the Tilemap receive shadows as well?
     
  20. ThundThund

    ThundThund

    Joined:
    Feb 7, 2017
    Posts:
    152
    You are welcome. If the shadow casters have the correct shape, then it all depends on the layers that are affected by the casters and the settings of the light, shadow intensity, target sorting layers, etc.
     
  21. LordHobbas

    LordHobbas

    Joined:
    Dec 10, 2018
    Posts:
    3
    I got it to work!
    I had to create a new Material. The one I already had was created before I set the custom pipeline asset as my graphics, and Unity did not like that. After creating a new one, and upping the shadow intensity on the lights, it started working perfectly!
     
    ThundThund likes this.
  22. Mindjar

    Mindjar

    Joined:
    Aug 13, 2013
    Posts:
    29
    Thanks for the script, works perfectly!
     
    ThundThund likes this.
unityunity