0
\$\begingroup\$

To clarify, I'm an experienced 3D modeller, but there are hiccups here and there getting used to Unity's interface.

I'm working with an adjustable spotlight for a game; it can be mounted and directed just like a turret. To do this properly, I need the start diameter of the spotlight to match the aperture size of the model. To clarify that this is not the inner diameter, I've made a drawing.

Start diameter is boundary closest to source

That is, I need to be able to make a spotlight that begins further down its cone. I can't just back the light up, as the emitted light will invariably collide with its own mesh and it won't work at all. I would also have artefacts like the player standing behind it and being lit by some invisible light source...

I expected this to be a lot easier than it is. Can someone point me in the right direction? Thank you!

\$\endgroup\$
2
  • 1
    \$\begingroup\$ "I can't just back the light up, as the emitted light will invariably collide with its own mesh" You can fix that by setting the shadow near plane of the light. \$\endgroup\$
    – DMGregory
    Mar 11 at 19:55
  • \$\begingroup\$ @DMGregory Thanks, I never would have guessed that. It makes sense on a technical level, though. \$\endgroup\$ Mar 11 at 23:04

1 Answer 1

2
\$\begingroup\$

You are asking two different questions in the same post. My answer will address the tricky part of setting a spotlight that accounts for different user-defined parameters.

If you want a light to not illuminate certain GameObjects, simply assign them to a different layer and disable that layer in the Culling Mask property of your Light.


So, you want to design a Spotlight by defining its start and end diameters. However, built-in spotlights in Unity are cone-shaped, with no option for a starting area of light. My solution is a workaround on this, by conveniently setting a spotlight to behave as you want.

Slant angle of a cone

Unity only exposes the Spot Angle variable. Fortunately, we can solve this with simple trigonometry. Geometrically speaking, a Spotlight resembles a cone. If we look at a cone from the side, its silhouette becomes an isosceles triangle. We can then divide such triangle into two complementary, right triangles:

enter image description here

In this context, \$tan(\theta) = \frac{r}{h}\$ and therefore \$\theta = \arctan(\frac{r}{h})\$. The diameter is twice the radius: \$d = 2r\$. Then, the final formula is:

$$ \theta = \arctan(\frac{d}{2h}) $$.

Slant angle of a truncated cone

Until now, we only had a base diameter. Let's extend this with a new parameter: the top diameter.

The introduction of a new diameter transforms the cone into a truncated cone. A side-view of a truncated cone is an isosceles trapezoid. We can then divide such trapezoid into two complementary, right trapezoid:

enter image description here

We need some more steps before writing a new formula. Let's draw a new line inside one of the right trapezoids, so we divide it into a rectangle and a right triangle:

enter image description here

I'm now assuming you are only interested in the distance between your diameters. So, here \$h\$ represents the truncated cone height, not the whole cone height.

Due to similarity, we know that the whole cone's slant angle and the sub triangle's acute angle are the same value. However, we need the length of the opposite edge. Its value is the difference between the base and top radii: \$l = R-r\$. Let's plug this value into the formula we used before: \$\theta = arctan(\frac{R-r}{h})\$. Finally, let's swap radii for diameters. The final formula is:

$$ \theta = arctan(\frac{D-d}{2h}) $$

Be aware that \$\theta\$ is the Spotlight's cone slant angle, not the Spotlight's Spot Angle. The Spot Angle is simply twice the slant angle.

Sample source code

Let's convert all this maths into code! We are going to create a MonoBehaviour script. It will compute the values we need when changing its parameters. Once assigned to the Light GameObject, it will update the light properties automatically:

using UnityEngine;

public class CustomSpotlight : MonoBehaviour {
    public float SD {
        get { return _startDiameter; }
        set { _startDiameter = value; }
    }
    public float ED {
        get { return _endDiameter; }
        set { _endDiameter = value; }
    }
    public float H {
        get { return _length; }
        set {
            _length = value;
            float theta = Mathf.Atan2(ED-SD, 2*H);
            float H_add = SD/(2*Mathf.Tan(theta));
            _lt.range = H + H_add;
            _lt.spotAngle = 2 * theta * Mathf.Rad2Deg;
            _lt.shadowNearPlane = H_add;    //EDIT: update near plane distance
            transform.localPosition = -H_add * transform.forward;
        }
    }

    [SerializeField]
    float _startDiameter = 1f;
    [SerializeField]
    float _endDiameter = 2f;
    [SerializeField]
    float _length = 1f;

    Light _lt;

    void OnValidate() {
        _lt = GetComponent<Light>();
        _lt.type = LightType.Spot;
        
        SD = _startDiameter;
        ED = _endDiameter;
        H = _length;
    }
}

This code doesn't check against wrong values or trigonometric edge cases, you should include them if you want to use this in production.

After re-computing values, we must also update the light position. We need this offset because e.g. a constant start radius requires the light source position to move back and forth (because the "outside hull" is still a cone).

Then, we must parent the GameObject with the Light and CustomSpotlight components to an empty GameObject. Such GO will be used to move and rotate the spotlight around the scene, while its child Light will adjust its local transform to get the desired effect.

\$\endgroup\$
2
  • \$\begingroup\$ Thank you, every now and then I bump into a legit mathematician on here. I'm imagining that this math isn't taken care of automatically by Unity because it's computationally taxing. I'm also considering alternatives of having multiple less-bright spot lights, condensed to a single spotlight at low LOD (I know, not perfect); or using a mesh cylinder up close like Crysis did. Thank you for your elaborate and prompt response! \$\endgroup\$ Mar 11 at 17:34
  • 2
    \$\begingroup\$ I added a line of code to my solution that implements DMGregory's suggestion of changing the Shadow Near Plane property. Now, it updates automatically and equals the distance between the light origin and the start diameter, \$\endgroup\$
    – liggiorgio
    Mar 12 at 14:50

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .