Allow programmatic manipulation for ShadowCaster2D shape

ShadowCaster2D shadow shape is not programmatically accessible, which severely limits what can one do with shadows in 2D. Please make a public “Shape” property, which will modify internal shape variable and reset the hash.

https://github.com/Unity-Technologies/ScriptableRenderPipeline/blob/master/com.unity.render-pipelines.universal/Runtime/2D/ShadowCaster2D.cs#L32-L33

What I see can be done with this:

  • transfer shapes from one point in program to another. E.g. in my project I have single game object with sprite being switched, and thus shapes should be switched too. I can encode shapes in my ScriptableObject (game database), but I can’t set the shape programmatically on a shadow caster.
  • automatic shape generation from the sprite (by tracing edges)
  • animating shapes programmatically to produce various light effects
  • use PCG to generate objects and their shapes

It would also be cool to expose not a single path, but list of paths as a shadow outline to generate shadows for tilemap mazes and such, but that is slightly more complex story, I suppose.

3 Likes

I would love to see this. I am looking to use this lighting system in a 2D Voxel game where the shape is constantly changing and cannot be preset in the editor.

It would also be good to expose the Target Sorting Layer field. This would help when dynamically adding a ShadowCaster2D to a gameobject without having to use a prefab.

The repo moved apparently. Link is now: https://github.com/Unity-Technologies/Graphics/blob/master/com.unity.render-pipelines.universal/Runtime/2D/ShadowCaster2D.cs

I came across a somewhat backhanded way to do this, and it kills your performance if you try to do it once the game is running, but I was able to use System.Reflection

private ShadowCaster2D shadowCaster;
private static BindingFlags accessFlagsPrivate =
  BindingFlags.NonPublic | BindingFlags.Instance;
private static FieldInfo meshField =
  typeof(ShadowCaster2D).GetField("m_Mesh", accessFlagsPrivate);
private static FieldInfo shapePathField =
  typeof(ShadowCaster2D).GetField("m_ShapePath", accessFlagsPrivate);
private static MethodInfo onEnableMethod =
  typeof(ShadowCaster2D).GetMethod("OnEnable", accessFlagsPrivate);
void Start()
{
  modifyShadowShape();
}
void modifyShadowShape()
{
  shadowCaster = 
  GetComponent<UnityEngine.Experimental.Rendering.Universal.ShadowCaster2D>();
  var testPath = new Vector3[]{
    new Vector3(0,0,0),
    new Vector3(0,1,0),
    new Vector3(1,1,0),
  };
  shapePathField.SetValue(shadowCaster, testPath);
  meshField.SetValue(shadowCaster, null);
  onEnableMethod.Invoke(shadowCaster, new object[0]);
}

I’dl like to be able to modify the shadows as well, I was thinking I could preprocess the sprites to get polygons or something

1 Like

If anyone else is like me and is doing some 2D stuff with tilemaps and just wants a convenient way to get shadows going with this experimental system, without modifying the package scripts, I’ve adapted the approach from this thread & another into a component that’ll work with an arbitrary input list of CompositeCollider2Ds and create a set of children ShadowCaster2Ds that you can use with a sister CompositeShadowCaster2D script on the parent to conveniently manage a scene with stuff like tilemap colliders.

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

/**
* Usage: Make an empty game object and attach this component script to it. Drag & drop CompositeCollider2D components
* into the sourceColliders field.
*
* This script isn't ideal, but it's simple and an okay jumping off point for more advanced changes (like caching).
*
* The main caveat is that it'll regenerate the child objects that comprise the shadow group every time you enter
* edit mode. For larger scenes this might be slow or annoying. It's a rough sketch I'm using for a jam game so I'm
* not too concerned about this, future travelers may way to dig into more optimal ways of not having to recreate the
* child object shadow casters.
*/
[ExecuteInEditMode]
[RequireComponent(typeof(CompositeShadowCaster2D))]
[DisallowMultipleComponent]
public class ShadowWrangler : MonoBehaviour
{
    public CompositeCollider2D[] sourceColliders = { };

    private static BindingFlags accessFlagsPrivate =
        BindingFlags.NonPublic | BindingFlags.Instance;

    private static FieldInfo shapePathField =
        typeof(ShadowCaster2D).GetField("m_ShapePath", accessFlagsPrivate);

    private static FieldInfo meshHashField =
        typeof(ShadowCaster2D).GetField("m_ShapePathHash", accessFlagsPrivate);


    // Start is called before the first frame update
    void OnEnable()
    {
        // only refresh this stuff in edit mode since the ShadowCaster components will serialize/persist their
        // shadow mesh on their own.
        // TODO: maybe only trigger the refresh when a difference is detected
        if (Application.isEditor) RefreshWorldShadows();
    }

    private void RefreshWorldShadows()
    {
        ClearChildShadows();
        foreach (var source in sourceColliders)
        {
            CreateGroupsForCollider(source);
        }
    }

    private void ClearChildShadows()
    {
        var doomed = new List<Transform>();
        for (int i = 0; i < transform.childCount; i++)
        {
            var child = transform.GetChild(i);
            if (child.GetComponent<ShadowCaster2D>() == null || !child.name.StartsWith("_caster_")) continue;
            doomed.Add(transform.GetChild(i));
        }

        foreach (var child in doomed)
        {
            DestroyImmediate(child.gameObject);
        }
    }

    /**
     * This is adapted from strategies in these two discussion threads:
     * - https://discussions.unity.com/t/769178
     * - https://discussions.unity.com/t/776893
     * Hopefully a day comes where we're allowed to programmatically mutate the shadow caster shape
     * programmatically.
     */
    private void CreateGroupsForCollider(CompositeCollider2D source)
    {
        for (int i = 0; i < source.pathCount; i++)
        {
            // get the path data
            Vector2[] pathVertices = new Vector2[source.GetPathPointCount(i)];
            source.GetPath(i, pathVertices);
            Vector3[] finalVerts = Array.ConvertAll<Vector2, Vector3>(pathVertices, input => input);

            // make a new child
            var shadowCaster = new GameObject("_caster_" + i + "_" + source.transform.name);
            shadowCaster.transform.parent = transform;

            // create & prime the shadow caster
            var shadow = shadowCaster.AddComponent<ShadowCaster2D>();
            shadow.selfShadows = true;
            shapePathField.SetValue(shadow, finalVerts);
            // invalidate the hash so it re-generates the shadow mesh on the next Update()
            meshHashField.SetValue(shadow, -1);
        }
    }
}

Example hierarchy view of the results:

Example scene view (white outlines are the shadow mesh/path):

I hope this can be of use to other people googling for this kind of thing, and maybe Unity will consider formalizing an approach to editing the path programmatically in a way that carefully acknowledge it’s expensiveness (maybe we can only edit it in edit mode or something?)

2 Likes

Please do not cross-post as per you other post here .

Thanks.

please implement official solution haha. i realy see no harm in the crosspost since both threats discuss this topic and its relevant to both.

3 Likes

Watch out, man. They’ll call You out for necroposting XD

It wasn’t a necropost. Yours was however. :frowning:

If you want to participate in these forums, please make sure to read the Community Code of Conduct . In your case, section 1i.

Thank you.

2 Likes