Force shadow on specific object

Hi, I would like to force a shadow to be cast from a specific distant object using scene directional light without degrading shadows from objects close by. It has to be able to run on an Oculus Quest 2. I’ve tried a couple of assets but no luck. I know I can set the shadow distance to a higher value but this causes shadows that are near to be very low quality. I don’t mind having to buy an affordable asset if that’s going to solve the problem.

I can’t think of an asset that does this, but I can think of a way to do what you want. But you’re going to have to write some custom c# and a custom shader to do it.

You’d have to have a c# script attached to the light, and have it list the object(s) you want to ensure get rendered into it. Then you’d create a command buffer is using CommandBuffer.DrawRenderer() to render those objects and attach it to the directional light using Light.AddCommandBuffer() on the event LightEvent.BeforeShadowMap. You just need to create and add it once on Awake(), or update the command buffer if the objects you want to cast shadows change (call Clear() and then DrawRenderer() for the objects you want).

You’d need to write a custom shader that is a copy of just the default shadow caster pass from this page with some minor modifications.

Shader "Forced Shadow Caster"
{
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_shadowcaster
            #include "UnityCG.cginc"

            struct v2f {
                V2F_SHADOW_CASTER;
            };

            v2f vert(appdata_base v)
            {
                v2f o;
                TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
            #if UNITY_REVERSED_Z
                // override the depth to be at the near plane
                o.pos.z = o.pos.w * 0.999999;
            #else // GL near plane is inverted from all other APIs
                o.pos.z = -o.pos.w * 0.999999;
            #endif

                return o;
            }

            float4 frag(v2f i) : SV_Target
            {
                SHADOW_CASTER_FRAGMENT(i)
            }
            ENDCG
        }
    }
}

Make a material that uses that shader and use that as a material override in the DrawRenderer() calls.

That should work, though I’ve not tested any of that.

1 Like

Thank you very much for your in-depth answer! I will study it carefully! I haven’t worked with shaders before but might as well learn it now :slight_smile:

Hi bgolus, and thank you for your help :slight_smile: I’ve made a script, a shader, and a material. I’ve attached the script to the directional light and that script has a reference to the light itself, the object I want a shadow drawn from, and the material with the shader. But it produces a weird shadow, it seems to be the material and shader that produces it, because if I turn of fthe script on the light, the shadow remains. I get the shadow in the red box, and it moves when I move the editor camera, it also moves when the object moves during runtime. I want the shadow in the blue box, and it shows in the image because the camera is close to the object, it doesn’t show on a distance beyond shadow draw distance, which is what I would like it to. Can you see what I am doing wrong?

public Renderer testRenderer;
public Material testMaterial;
public Light directionalLight;

void Awake()
{
    CommandBuffer mCacheCommandBuffer = new CommandBuffer();
    mCacheCommandBuffer.name = "TestCommandBuffer";
    mCacheCommandBuffer.DrawRenderer(testRenderer, testMaterial);
    directionalLight.AddCommandBuffer(LightEvent.BeforeShadowMap, mCacheCommandBuffer);
}

Whoops, that should be LightEvent.BeforeShadowMapPass

1 Like

Thank you :slight_smile: That removed the weird shadow, but I’m still not getting a cast shadow. I’ve made a basic scene with only a plane, cube, light, and camera. The cube does change color a little when pressing play but won’t cast a shadow. If you’d like to look at my example, you need to set the shadow distance to 25 or less. I’m really out of my league here, I really appreciate your help :slight_smile: I’ve attached the assets folder from the basic demo to this reply :slight_smile:

7721623–969079–Assets.zip (6.36 KB)

Ah. I misunderstood what exactly you wanted. My example code does do what you asked, but shadows will still only show within the shadow distance. It’ll just prevent objects that are very far away from degrading the shadows if you disable shadow casting and receiving on that object. Like if you have a moon you want to cast shadows over where the player is. But if you want an object to cast a shadow onto the terrain regardless of how away from the camera it is, especially if you want it to work on the Quest, that is a much more complicated request, and not one that can be easily implemented from just me describing the setup.

The short version is you want a blob shadow. The normal way to implement that is to use a projector, which you do not want to use on the Quest, especially on terrain as it’ll murder performance. The way I’ve done it on the Quest, and even the Google Daydream, is to place a quad on the terrain with some c# code that raycasts against it to find a “good enough” position above the terrain to not intersect with it, but not hover too high, and put a dark transparent material on it that uses a static image of the shadow.

1 Like

I am sorry for being unclear :slight_smile: Forcing the object that is near the camera to have a not degraded shadow would work just fine for me, and then set the shadow distance high enough to have the faraway object project a degraded shadow. I can turn off the shadow and then have it project one anyway with the code you gave me, but it is still a degraded shadow.

What you’re talking about is either cascaded shadow maps, or per-object shadow maps. Unity does not have built in support for either on the Quest, or at all in the case of per object shadow maps.

Cascaded shadow maps are on by default on desktop & consoles, but they’re not supported at all on mobile devices like the Quest. That would normally be the best way to have both sharp shadows on objects near to the camera and blurry low res shadows out into the distance. It’s how (almost) all modern games handle this. Technically the URP supports cascaded shadow maps on mobile, and mobile VR, but you might not want to go that route, especially as URP’s performance on the Quest is questionable unless you disable all of those kind of features and stick with straight baked lighting. To be fair real time shadows on the Quest 2 is kind of a tall ask to begin with.

Per-object shadows can be cheaper than full shadow cascades, if you only need a single object to be able to cast real time shadows. And they are possible in Unity. There was even an official Unity asset on the store that exists for that … which broke about 3 weeks after it was released several years ago, was never updated, and which they only just finally pulled. It also didn’t do the extra step of what you want for it to cast shadows on other objects, only better self shadowing.

To get a single “per-object” shadow you’ll basically need to write entirely custom shaders for everything in the scene you want to have receive the per-object shadow, and you need to render out your own custom shadow maps of the object you want to have cast shadows. This is all doable, and potentially even not that expensive on the Quest 2, but isn’t a small amount of work.

1 Like

Hello again bgolus :slight_smile: You have been most helpful, you have clarified a lot of things for me on this matter. With your help, I made a simple blob shadow, I just get the scene directional light direction, project a ray from the object that needs a blob shadow, move a plane with a blob shadow image with transparency to ray hit point and subtract hit.transform.up * 0.3, align plane with terrain at point of hit. I will test on my Quest later today to see if it makes a hit to performance. Thank you very much!

I will post my code here for anyone interested. Notes: 1<<6 is bit shifting a layermask to detect hit from, layer 6 is a custom layer I made for terrain.

public Light sceneDirectionalLight;
public GameObject planeAtRayEnd;

private Vector3 sceneDirectionalLightDirection;
#endregion

#region BUILTIN METHODS
// Start is called before the first frame update
void Start()
{
    sceneDirectionalLightDirection = sceneDirectionalLight.transform.forward;
}

// Update is called once per frame
void Update()
{
    RaycastHit hit;
    if (Physics.Raycast(transform.position, sceneDirectionalLightDirection, out hit, 100f, 1<<6))
    {
        planeAtRayEnd.transform.position = hit.point + (transform.up * 0.3f);
        planeAtRayEnd.transform.rotation = Quaternion.FromToRotation(hit.transform.up, hit.normal) * transform.rotation;
    }
}