The new 2D lighting on tilemaps, advice and suggestions?

Hey guys, I had been testing out the new system for 2D lighting on a game with tilemaps. At first obviously this doesn’t work at all for the time being, but with some hacking thanks to this thread I was able to get it working:

And this is what I end up with:

(Gif too large for forum)

I used a version of his script for ShadowCaster2D that allows it to generate the shadows based on the tilemap collision mesh, which is handy! I discovered that with that implementation there was no way for tiles to be dynamically added/removed from the world and having the lighting adjust to match - so I wrote some changes to his other script (I’ve named it “SetTilemapShadows” for lack of a better name) that allow it to recreate the shadows when the user needs to (like after adding new tiles to the tilemap). Here that is:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Experimental.Rendering.Universal;

public class SetTilemapShadows : MonoBehaviour
{
    public static SetTilemapShadows Instance;

    private CompositeCollider2D tilemapCollider;
    private GameObject shadowCasterContainer;
    private List<GameObject> shadowCasters = new List<GameObject>(), toDelete = new List<GameObject>();
    private List<PolygonCollider2D> shadowPolygons = new List<PolygonCollider2D>();
    private List<ShadowCaster2D> shadowCasterComponents = new List<ShadowCaster2D>();

    private bool doReset = false, doCleanup = false;

    public void Start()
    {
        Instance = this;
        tilemapCollider = GetComponent<CompositeCollider2D>();
        shadowCasterContainer = GameObject.Find("shadow_casters");
        for (int i = 0; i < tilemapCollider.pathCount; i++)
        {
            Vector2[] pathVertices = new Vector2[tilemapCollider.GetPathPointCount(i)];
            tilemapCollider.GetPath(i, pathVertices);
            GameObject shadowCaster = new GameObject("shadow_caster_" + i);
            shadowCasters.Add(shadowCaster);
            PolygonCollider2D shadowPolygon = (PolygonCollider2D)shadowCaster.AddComponent(typeof(PolygonCollider2D));
            shadowPolygons.Add(shadowPolygon);
            shadowCaster.transform.parent = shadowCasterContainer.transform;
            shadowPolygon.points = pathVertices;
            shadowPolygon.enabled = false;
            //if (shadowCaster.GetComponent<ShadowCaster2D>() != null) // remove existing caster?
            //    Destroy(shadowCaster.GetComponent<ShadowCaster2D>());
            ShadowCaster2D shadowCasterComponent = shadowCaster.AddComponent<ShadowCaster2D>();
            shadowCasterComponents.Add(shadowCasterComponent);
            shadowCasterComponent.selfShadows = true;
        }
    }

    private void Reset()
    {
        toDelete = new List<GameObject>(shadowCasters);
        shadowCasters.Clear();
        shadowPolygons.Clear();
        shadowCasterComponents.Clear();

        for (int i = 0; i < tilemapCollider.pathCount; i++)
        {
            Vector2[] pathVertices = new Vector2[tilemapCollider.GetPathPointCount(i)];
            tilemapCollider.GetPath(i, pathVertices);
            GameObject shadowCaster = new GameObject("shadow_caster_" + i);
            shadowCasters.Add(shadowCaster);
            PolygonCollider2D shadowPolygon = (PolygonCollider2D)shadowCaster.AddComponent(typeof(PolygonCollider2D));
            shadowPolygons.Add(shadowPolygon);
            shadowCaster.transform.parent = shadowCasterContainer.transform;
            shadowPolygon.points = pathVertices;
            shadowPolygon.enabled = false;
            //if (shadowCaster.GetComponent<ShadowCaster2D>() != null) // remove existing caster?
            //    Destroy(shadowCaster.GetComponent<ShadowCaster2D>());
            ShadowCaster2D shadowCasterComponent = shadowCaster.AddComponent<ShadowCaster2D>();
            shadowCasterComponents.Add(shadowCasterComponent);
            shadowCasterComponent.selfShadows = true;
        }
        doCleanup = true;
    }

    private void LateUpdate()
    {
        if(doReset)
        {
            Reset();
            doReset = false;
        }
        if(doCleanup)
        {
            StartCoroutine(Cleanup());
            doCleanup = false;
        }
    }

    IEnumerator Cleanup()
    {
        yield return null;
        for (int i = 0; i < toDelete.Count; i++)
        {
            Destroy(toDelete[i]);
        }
        toDelete.Clear();
    }

    public void UpdateShadows()
    {
        doReset = true;
    }
}

And while that isn’t exactly “high performance” minded in design - it works well enough to not cause any lag on a tilemap that is 128x128, so good enough for now. You’ll notice I had to use some strange timing and a coroutine to get the shadows to render right between the time the user adds the new tile(s) and the time it actually recreates the shadow casters, but by doing it that way it helps prevent a odd flickering on the shadow map, though still it does make a bit of flicker where the new shadow is added, but it doesn’t seem too annoying to use it that way. There are also some bits left in where I thought I should track existing shadow casters and just change the shape of them rather than create new - but I never got that working properly so feel free to cutout those leftover Lists<> that don’t serve a purpose now.

I just attached that script to something in my scene and when I need to update the shadows, I call the UpdateShadows() method on the static instance.

So now that I’ve shared the results of that here, hopefully others can improve upon and perhaps replace this system with a more performance friendly one that doesn’t delete then recreate all the existing shadows and such - I would love to see more clever solutions but figure it can’t hurt to get the concept I’ve made out into the hands of anybody needing dynamic 2D lights on a tilemap!

Another really annoying thing is that the package manager (or something) automatically “updates” the ShadowCaster2D script in the library files every single time I restart the editor. That means I have to keep a separate copy of the script outside the project and copy/paste it over the “updated” script unity keeps changing back… but regardless it works so I am working with that annoyance for now.

EDIT: And one last thing - UT please please please… build in a function to make the ShadowCaster2D do this with the collision mesh of the tilemap - that’ll save me all this headache :stuck_out_tongue:

2 Likes

Thanks for linking to my response! Regarding this issue:

If you have a Github account you can fork this repository: https://github.com/Unity-Technologies/ScriptableRenderPipeline and then make changes to it, then in your package manager you can add a package from a git URL by clicking the + icon on the top left. This should help anyone who might also wish to play around with various implementations of this.

1 Like

Just a note, if you don’t want to bother with editing the PackageManager files, you can add a BoxCollider2D instead of PolygonCollider2D. This means all the shadowcasters will be squares, but maybe that’s okay.

TilemapShadowCaster2D.cs

using UnityEngine;
using UnityEngine.Tilemaps;
using UnityEngine.Experimental.Rendering.Universal;

public class TilemapShadowCaster2D : MonoBehaviour
{
    public void Start() {
        var sc = Resources.Load<GameObject>("Misc/BoxShadowCaster");

        var tilemap = GetComponent<Tilemap>();
        GameObject shadowCasterContainer = GameObject.Find("shadow_casters");
        int i = 0;
        foreach (var position in tilemap.cellBounds.allPositionsWithin) {
            if (tilemap.GetTile(position) == null)
                continue;
         
            GameObject shadowCaster = GameObject.Instantiate(sc, shadowCasterContainer.transform);
            shadowCaster.transform.position = TilemapUtils.GridCoordsToWorldCoords(new Vector2Int(position.x, position.y));
            shadowCaster.name = "shadow_caster_" + i;
            i++;
        }
    }
}

Edit: Requires a prefab in your project under Assets/Resources/Misc/BoxShadowCaster that is an empty GameObject with just a BoxCollider2D that is the size of the tile and a ShadowCaster2D

1 Like

heya

thank you so much for that, this is exactly what I need. But i cant get it to work. I managed to get rid of the errors, but there are still no shadows casted. The shadowCaster2D shape doesn’t seem to get modified by your script, it just stays a a point in the middle. How can I use this the right way?

(Please dont look at the placeholder art :smile:)

again thanks for making this it is amazing :slight_smile:

5385639--545964--Bildschirmfoto 2020-01-18 um 23.48.55.png

Not too certain what appears to be causing that issue, from the sounds of it the bounds aren’t being picked up. Have you modified the ShadowCaster2D.cs file with the changes specified in this post (the first script): https://discussions.unity.com/t/769178 ? To use the script in this post (SetTilemapShadows) you attach it to a tilemap that has a tilemap collider 2D component (with Used By Composite checked) and a composite collider 2D component attached.

I actually did that first haha, a much simpler solution but became very performance intensive :confused:

That is why I stumbled upon other ideas and started trying those.

Anyway now my issue is performance with tons of tiles on a 128x128 tilemap, and with many small tiles scattered about it has to make many shadow casters, and the way I update the entire set anytime I change one tile - it causes some slowdowns, eventually to the point its not playable.

It is unfortunate, as I thought that might be a good solution for 2D shadows, but for the time being it doesn’t seem to be, not at least until unity updates it with perhaps better performance, as well as a built in way to handle tilemaps that isn’t so costly on performance. I myself am not willing to dive that deep down the rabbit hole to solve it myself, and would rather look around at some of the commercial options on the asset store, to see if I can spend a little money and save a lot of time, ya know? Plus I’m just not that good at shadows and heavy maf’s… so I’ve already moved onto trying to find other solutions :frowning:

Thanks for your reply. I got it to work, it’s really Simple. Thank you so much for that - this really got me back to work on my game.
Anyhow, there is still one little problem:
The shadowcaster behaves a little bit weird - i doesn’t cast a shadow the right way when the light is on top or below the shadowcaster (see picture).
the bottom should not be lit up

is this a general problem with shadowcasters at the moment?

thanks

5391141--546738--Bildschirmfoto 2020-01-20 um 22.04.07.png

I noticed this too - I think its a bit of a problem with the 2D lighting in general - and to avoid it just try not to allow lights to enter objects, which is probably the only real solution for now. :confused:

1 Like

Thank you both so much for working this out for everyone else.

3 Likes

I’m having trouble with this. I forked it and made the changes, but how can I add the sub-folder for URP as a package in packman?

Edit: It took some work, and I’m not sure if it’s done exactly the way you’re supposed to do it, but if anyone is interested in a fork of 7.1.7 (for Unity 2019.3) with these changes applied, you can grab it here:
https://github.com/dotaxis/com.unity.render-pipelines.universal.git

This includes the new script to cast shadows on a tilemap. use it, remove Universal RP from your project via Package Manager, then add this to your manifest.json:

"com.unity.render-pipelines.universal": "https://github.com/dotaxis/com.unity.render-pipelines.universal.git"

I recommend you fork the repo to your own GitHub instead of using mine. It’s the smart thing to do.

1 Like

What’s TilemapUtils? Sorry if I missed something!

I believe TilemapUtils is something that @japhib was using to convert his “grid space” into “world space”, if your grid coordinates align with world coordinates already, you should be able to remove this and just have new Vector3Int(position.x, position.y, 0) or something similar.

1 Like

(I’ve changed my username and profile picture :p)

Okay, thanks for the help! I should have looked more closely at the script. I did some modifications to TilemapShadowCaster2D.cs and ended up with the following:

using UnityEngine;
using UnityEngine.Tilemaps;
using UnityEngine.Experimental.Rendering.Universal;

[RequireComponent(typeof(Tilemap))]
public class TilemapShadowCaster2D : MonoBehaviour
{
    public GameObject shadowCasterPrefab;

    public void Start()
    {
        var tilemap = GetComponent<Tilemap>();
        int i = 0;
        foreach (var position in tilemap.cellBounds.allPositionsWithin)
        {
            if (tilemap.GetTile(position) == null)
                continue;

            GameObject shadowCaster = GameObject.Instantiate(shadowCasterPrefab, transform);
            shadowCaster.transform.position = new Vector3Int(position.x + 1, position.y + 1, 0);
            shadowCaster.name = "Shadow Caster " + i;
            i++;
        }
    }
}

Unfortunately, this didn’t quite work, as you can see here:

https://twitter.com/ElnuDev/status/1221894484595634176

Any thoughts on fixing this? I had to add 1 to the x and y values of the Vector3Int, otherwise the shadow casters weren’t aligned to the tilemap. I also added icons to the shadow casters and their positions checked out with the tilemap, so there isn’t any positioning issue. My shadow caster prefab has only an unmodified ShadowCaster2D component attached. Thanks for your help in advance!

It’s kind of difficult to say what the problem is, I can suggest some things that might be the problem though? Have you ticked “selfShadows” in the ShadowCaster2D component on the prefab? Do the prefabs also have colliders? My original implementation differed to this (as this script creates a new ShadowCaster2D prefab for each tile, which might reduce performance a little (especially with individual colliders)), instead what I do is have a composite collider on my tilemap, then iterate through each distinct region in the composite collider and create a ShadowCaster2D gameobject with a polygon collider with points that correspond to that shapes points. My script for this is:

using UnityEngine;
using UnityEngine.Experimental.Rendering.Universal;
using System.Collections.Generic;

[ExecuteInEditMode]
[DisallowMultipleComponent]
[RequireComponent(typeof(CompositeCollider2D))]
[AddComponentMenu("Rendering/2D/Shadow Caster 2D Tilemap")]
public class ShadowCaster2DTilemap : MonoBehaviour {
    public CompositeCollider2D tilemapCollider;
    private GameObject shadowCasterContainer;
    public string ShadowCasterContainerName;
    public List<GameObject> shadowCasters = new List<GameObject>();
    private int previousPointCount;

    public void Start() {
        tilemapCollider = GetComponent<CompositeCollider2D>();
        shadowCasterContainer = GameObject.Find(ShadowCasterContainerName);
      
        if (!GameObject.Find(ShadowCasterContainerName)) {
            shadowCasterContainer = new GameObject(ShadowCasterContainerName);
        } else {
            // Clear existing shadow casters
            if (Application.isPlaying) {
                foreach (Transform child in shadowCasterContainer.transform) {
                    Destroy(child.gameObject);
                }
            } else {
                while(shadowCasterContainer.transform.childCount != 0){
                    DestroyImmediate(shadowCasterContainer.transform.GetChild(0).gameObject);
                }
            }
        }

        GenerateShadowCasters(false);
    }

    public void Update() {
        if (previousPointCount != tilemapCollider.pointCount) {
            GenerateShadowCasters(true);
        }
    }

    public void GenerateShadowCasters(bool clearExisting) {
        if (clearExisting) {
            // Clear existing shadow casters
            if (Application.isPlaying) {
                foreach (Transform child in shadowCasterContainer.transform) {
                    Destroy(child.gameObject);
                }
            } else {
                while(shadowCasterContainer.transform.childCount != 0){
                    DestroyImmediate(shadowCasterContainer.transform.GetChild(0).gameObject);
                }
            }
        }

        previousPointCount = tilemapCollider.pointCount;

        for (int i = 0; i < tilemapCollider.pathCount; i++) {
            Vector2[] pathVertices = new Vector2[tilemapCollider.GetPathPointCount(i)];
            tilemapCollider.GetPath(i, pathVertices);
            GameObject shadowCaster = new GameObject(ShadowCasterContainerName + "_" + i);
            PolygonCollider2D shadowPolygon = (PolygonCollider2D)shadowCaster.AddComponent(typeof(PolygonCollider2D));
            shadowCaster.transform.parent = shadowCasterContainer.transform;
            shadowPolygon.points = pathVertices;
            shadowPolygon.enabled = false;
            ShadowCaster2D shadowCasterComponent = shadowCaster.AddComponent<ShadowCaster2D>();
            shadowCasterComponent.selfShadows = true;
        }
    }
}

Note the Update handler, that checks if the number of points on the tilemap collider (CompositeCollider) has changed and regenerates the shadow casters. This can be useful if you want dynamic updates to your shadows, e.g. cells get destroyed or added. Hopefully this helps? Any issues let me know.

1 Like

Aha! “Self shadows” fixed the issue. It’s working perfectly now! Thanks so much for your help!

What i am looking for is for a script to generate smth like this on button press. however, i cant find a way to access a shadowcasters shape nodes by script. the rest im confident i can hack together. id like to avoid forking the RP, that makes me feel uncomfortable. any ideas? can it be done?

edit: id guess the CompositeShadowCaster works very differently to the CompositeCollider, in that it does not actually create a new mesh; but just tinkers with the blending. However, even if i achive the aforementioned algorithmic ShadowCaster generation, i still end up with a bunch of objects with single colliders, where hypothetical CompoositeCollider with all the nodes would be so much more convenient.

edit2: there is differnt difficulties when working with shadows for a top-down game aswell; like backlighting. when i stand behind a tree, i want it to selfshadow, and when i stand in front of it, it should not selfshadow. i can hack smth together to toggle that switch for me, but what would be realy usefull would be to selfshadow by an amount instead of a hard bool-switch for smooth transitioning. in what i imagine how the lighting works, thats rather trivial :smile: i should try and make a feedback post.

2 Likes

The method I’m using spawns in a bunch of square shadow casters with “Self Shadows” turned on. Unfortunately, that really does eat up performance on larger Tilemaps. :face_with_spiral_eyes: Hopefully Unity will add built in shadow support for tilemaps soon! Until then we’ll just have to wait or make custom solutions.

yep. i didnt check out your script tbh, but the original one. it did not quite work for me so i wrote my own version that uses a simple prefab with a 1 by 1 shadowcaster attached, instantiates it a bunch and sets position and scale. the number of shadowcasters is also suboptimal, but for me at least its an improvement over the other script. i posted it to reddit with screenshot and all, check it out if you want: Reddit - Dive into anything

edit: most impotently, my version does not require you to tinker with the RP package, thats the main thing for me. i also came close to make the casters be manipulatable after generation (i use an inspector button to generate them), but the shape-thingy seems to bug out, or maybe its a problem with my script. anyways :slight_smile:

edit2: i also somewhat fixed the backlighting-point i made in edit2 of my original post here. i copied the Sprite-Lit-Default material, renamed to Sprite-Lit-Selfshadow, and added a normal map with all rgb(128,0,128) color. it makes it so that when a light approaches say a tree from the top, the tree does not get lit until the light passes it and gets below; so to achive that its not illuminated “from behind”, but only from its front. it also works with walls. only problem is that very large sprites get illuminated not at once but per pixel, showing a kind of gradient sometimes that doesnt look quite right. maybe i will tinker with the shader somewhere in the future.

edit3: you want to dynamicaly remove shadowcasters, and while my script doesnt allow for that rn, it could be made to do so. while generating, it saves prefab instance references in a 2-dimensional array that correspond to their position. you could return that and add a method to remove a shadowcaster, possibly splitting the one you want to remove a part from into to, analogous to how theyre merged in the first place - but reverse. if your performance problems come from the manipulation, this could be a solution.

1 Like

The real issue with using a shadowcaster square for any shadows is performance. Even if you don’t plan on allowing changes to the tilemap (and then changes to the shadow casters) at runtime, you still are creating potentially hundreds of unique shadowcasters in your scene, and if your trying to make a particularly large world this will become painfully slow. And if you are trying to do dynamic changes to the tilemap and shadows, this will be even worse in its effects if not done very carefully with memory concerns addressed.

I think the solution @thomasedw came up with, and my changes to allow runtime editing are more performance friendly in a situation where your map is larger, while its much easier to use your solution if your map isn’t that big and won’t allow changes. And by not using a single shadow caster for one single tile, and rather on large set of tiles that are connected, it prevents there being hundreds of “useless” shadowcasters.

But really, the best solution for dynamic large worlds that nobody has shared yet - which would be to not destroy the original set of shadow casters and recreate them like I shared - would be the best option for everybody, because if someone came up with a way to recreate the shadow casters that already exist, and only add/remove them in the event the tilemap is changed to completely remove or add a unique caster, then it would be much easier on memory and perform nicer on mobile.

I like that everybody is sharing their own ideas though - keep it up! Hopefully with enough collaboration here we can get to an optimal solution that “just works” for everybody and is performance friendly regardless of your tilemap being changed or not.

EDIT: Another thought - if you wanted to pursue the square shaped solution further and get a little better performance with less wasted shadowcasters, you could make the squares occupy many tiles in size, and only use a single shadowcaster for a single tile if there is no way to cover additional tiles, and might be worth trying out!

i too think yours is more performant becaus the square method also generates “edges” where 2 shadowcasters touch, while yours doesnt if i remember correctly. however, if the “edges” you generate technically are squares, im not so sure. im to lazy rn to check it out again :x

i already do merge adjecent tiles, my method generates a shadowcaster “per wall” - im not sure if i already implemented your edit-suggestion. here is a screenshot: 5436810--554055--screenshot_shadowcasters_small.jpg
my original goal was to generate them in a way i can edit them after generation, however that part failed. still what i achived was to not have to tinker with the RP.

i found these 2 scripts in search for a grid + shadowcaster solution, and i think my script is a valid alternative to your script, and the changed one, increasing total number of possible solutions to 3 :slight_smile: main benefit being not to have to tinker with the rp. i will leave possible future performance issues for my future-self to solve!

i too think none of these are optimal, lets hope they implement some sort of tilemap support when the feature’s development nears completion. i think a quasi-optimal solution for static shadowcasters can already be achived if we we would able to manipulate the shape of a caster; at least that would allow for a total of generated shadowcasters that equals in number what one would create by hand, and at that point id be fine with that and redirect any performance issues to the component itself rather then their method of generation.

2 Likes