Sunday, February 14, 2021

Making a 2D Lighting System

We made a game called Two Opposites for the Brackeys Game Jam 2021.1. The game was made in a week and out of the 10k+ people participated worldwide, it ranked #22 in the innovation category. You can play it in your browser here.


1.0 2D Lighting System

Upon initial analysis, we decided that the atmosphere should be given the most priority while developing this game. And the visual appeal of the game played a significant role in that. Back when we started working on this project, Unity didn’t have any rendering pipeline that supported 2D lighting. So my task was to develop a 2D lighting system for the game.

1.1 2D Raycaster (GL) - 1st iteration

  • The basic idea was to draw transparent lines originating radially outwards from a sprite with negligible separation to give a sense of light coming out.
  • I used the Unity’s low level Graphics Library (gl) to draw lines between two points.
  • The raycast loop formulated--
for (float i = 0; i < theta; i += steps)
{
    GL.Begin(GL.LINES);               
    GL.Color(col); // Initializing GL Library with white color as input

    GL.Vertex3(player.transform.position.x, player.transform.position.y, 0); //strating point
    GL.Vertex3(player.transform.position.x + 
    Mathf.Cos(i * Mathf.Deg2Rad) * maxVisiblityDistance, 
    player.transform.position.y + 
    Mathf.Sin(i * Mathf.Deg2Rad) * maxVisiblityDistance, 
    0); //ending point

    GL.End(); //clearing garbage            
}
Adjusting color Adjusting max visibility distance

                   Adjusting steps                   Adjusting theta
  • This loop draws rays from the player’s position to equally spaced points around the player.
  • The angle that light covers is governed by theta.
  • The spacing between each ray is governed by steps.
  • The color of the rays is governed by col.
  • The length of light ray is governed by maxVisiblityDistance.
  • All of these variables were serialized in the inspector.
  • This script is attached to the MainCamera and the loop is called in OnPostRender() method so that the lines are rendered as soon as the camera finishes rendering the scene.

1.2 Ray Material

  • The next task to make the light rays feel more natural by introducing transparency.
  • While pondering I found that GL library by default uses the Unlit material provided by Unity to create the lines.
  • As the Unlit Material doesn’t support transparency by default, I wrote an Unlit Shader that supported both transparency and vertex colors.
  • The RGBA values of the colors of the material based upon this shader was passed as an input.        
GL.Begin(GL.LINES);
lineMat.SetPass(0);
GL.Color(new Color(lineMat.color.r,
lineMat.color.g,
lineMat.color.b,
lineMat.color.a));


With transparency controls, the rays looked more natural

1.3 Environment Lighting

  • The next step would be to make the scene react to the light rays emitted by the player.
  • This involved two steps--
  • Making the light ray stop when it hits an object. This is done by Raycasting along the light rays that GL draws and checking if we’ve hit something.
RaycastHit2D hit = Physics2D.Raycast(player.transform.position,
new Vector2(Mathf.Cos(i * Mathf.Deg2Rad),
Mathf.Sin(i * Mathf.Deg2Rad)),
maxVisiblityDistance);

if (hit)
{
    GL.Vertex3(player.transform.position.x, player.transform.position.y, 0);
    GL.Vertex3(hit.point.x, hit.point.y, 0);
}

else
{
    GL.Vertex3(player.transform.position.x, player.transform.position.y, 0);
    GL.Vertex3(player.transform.position.x +
    Mathf.Cos(i * Mathf.Deg2Rad)* maxVisiblityDistance,
    player.transform.position.y +
    Mathf.Sin(i * Mathf.Deg2Rad)* maxVisiblityDistance,
    0);
} 
  • Making the sprite color of the object depend upon its distance from the light source.
SpriteRenderer sp = hit.transform.GetComponent<SpriteRenderer>();
if (sp != null)
{
    sp.color = new Color(1 / Mathf.Pow(Vector3.Distance(
        hit.transform.position, player.transform.position),
        1.5f),
        1 / Mathf.Pow(Vector3.Distance(hit.transform.position, player.transform.position),
        1.5f),
        1 / Mathf.Pow(Vector3.Distance(hit.transform. position, player.transform. position),
        1.5f));
} 

1.4 2D Raycaster (GL) - 2nd iteration

  • The previous method for generating rays was very inefficient with time complexity of O(n) as the loop had to run 3600 times every frame with a step size of 0.1.
  • This issue was solved by detecting the edges of nearby objects and casting rays at them and then filling the space by generating mesh between them.
int numRays = Caster.LightContour.Count - 1;
LightMesh.mesh.vertices = Caster. LightContour. ToArray();
int[] triangles = new int[numRays * 3];
for (int i = 0; i < numRays * 3; i += 3)
{
triangles[i] = (i / 3 + 1) % numRays;
triangles[i + 1] = numRays;
triangles[i + 2] = (i7 3 + 2) % numRays;
}
LightMesh.mesh.triangles = triangles;

This 2D lighting system that I incorporated in the project was implemented by Unity in the later versions as a Package in URP Render Pipeline.

Popuular Posts