GPU-based 2D lighting system for Unity3D



Hello. As you know, Unity3D lacks lighting support for 2D games. You can find such a system in the Asset Store , but it has one drawback - it works on the CPU and consumes a lot of resources (64-4096 reykast per frame for each light source). Therefore, I decided to make my own lighting, the performance of which would be enough for mobile devices. For this, the calculations were transferred to the GPU. It turned out to be something similar to Terraria or Starbound light.

Link to the demo. Arrows - movement, spacebar - chassis, R - restart. Screenshots taken from it.

All lighting is considered on small textures, in the example 160x88 pixels are used. With an increase in resolution, you can achieve a very fine grid, which will be difficult to notice, though this is no longer for mobile platforms. Due to the fact that calculations are performed on such small textures, fairly heavy shaders can be used.

For the operation of lighting, 3 cameras are used, each of which is responsible for its part of the system: light sources, light obstacles, and ambient light. Then the light sources and the ambient light are mixed and superimposed on the game camera.

Now in more detail, in drawing order.

Obstacles of light



Light obstruction texture. RGB channels. This and subsequent similar textures have a scale of 400%.

This is the texture the camera gives out. Black areas are completely transparent, white areas are completely opaque. Colored areas are also supported, for example, a completely red pixel will block the red part of the light and transmit green and blue.

Environment light



Light sources of the environment


Light sources of the environment. Alpha channel


Iteratively generated texture of the ambient light


This is how a slightly amplified ambient light looks like, without the usual light sources.

Everything is somewhat more complicated. I implemented this type of lighting in order to add faint light to a space where there are no light sources. In the example, using it, a dim highlighting of the entire free space is implemented. The RGB channel controls the color, the alpha channel controls the luminosity. The main difference between this type of light and conventional sources is that it is considered iterative and has no direction.

Calculation algorithm for one pixel:

  1. We take the initial pixel value from the previous iterative texture.
  2. Subtract the obstacle strength from the obstacle texture from the pixel.
  3. We add to the pixel the luminosity from the texture of the light sources of the environment.
  4. Add the average value of neighboring pixels to the pixel


Sources of light



Light

sources Conventional sources are a major part of the lighting system. Something similar to sprites is used to draw them. All color comes from the center, although the point can be moved anywhere, if desired.
For light sources, several shaders are available with a trace of the path and one without it. Trace shaders differ in the number of traced points. I use two of these - one at 9 points, working with Shader Model 2.0, the other at 20 points for Shader Model 3.0. A shader without path tracing is used for particle systems, since it does not need any additional information to work.

Path Tracing Algorithm:

  1. We take the brightness of the pixel from the texture.
  2. Find the position of the light source and the current pixel in the obstacle texture.
  3. Reduce the current brightness by the pixel values ​​of the obstacles that lie between the two points from the previous step.

9 point trace shader
Shader "Light2D/Light 9 Points" {
Properties {
	_MainTex ("Light texture", 2D) = "white" {}
	_ObstacleMul ("Obstacle Mul", Float) = 500
	_EmissionColorMul ("Emission color mul", Float) = 1
}
SubShader {	
	Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}
	LOD 100
	Blend OneMinusDstColor One
	Cull Off
	ZWrite Off
	Lighting Off
	Pass {  
		CGPROGRAM
			// Upgrade NOTE: excluded shader from DX11 and Xbox360; has structs without semantics (struct v2f members sp)
			#pragma exclude_renderers d3d11 xbox360
			#pragma vertex vert
			#pragma fragment frag
			#pragma glsl_no_auto_normalization
			#include "UnityCG.cginc"
			struct appdata_t {
				float4 vertex : POSITION;
				float2 texcoord : TEXCOORD0;
				fixed4 color : COLOR0;
				fixed4 normal : TEXCOORD1;
			};
			struct v2f {
				float4 vertex : SV_POSITION;
				half2 texcoord : TEXCOORD0;
				fixed4 color : COLOR0;
				half4 scrPos : TEXCOORD2;
				half4 scrPosCenter : TEXCOORD1;
			};
		    sampler2D _ObstacleTex;
			sampler2D _MainTex;
		 	half _ObstacleMul;
			half _EmissionColorMul;
			v2f vert (appdata_t v)
			{
				v2f o;
				o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
				o.texcoord = v.texcoord;
				o.scrPos = ComputeScreenPos(o.vertex);
				o.scrPosCenter = v.normal;
				o.color = v.color;
				return o;
			}
			fixed3 maximize(fixed3 vec){
				vec = max(vec, fixed3(0.01, 0.01, 0.01));
				return vec/max(vec.x, max(vec.y, vec.z));
			}
			half sum(half3 vec){
				return vec.x + vec.y + vec.z;
			}
			fixed4 frag (v2f i) : COLOR
			{
                fixed2 thisPos = (i.scrPos.xy/i.scrPos.w); 
				fixed2 centerPos = i.scrPosCenter;
				const fixed sub = 0.111111111111;
				fixed m = _ObstacleMul*length((thisPos - centerPos)*fixed2(_ScreenParams.x/_ScreenParams.y, 1)*sub);
				fixed4 tex = tex2D(_MainTex, i.texcoord);
				clip(tex.a - 0.005);
				fixed4 col = i.color*fixed4(tex.rgb, 1)*tex.a;
				fixed pos = 1;
				pos -= sub; col *= saturate(1 - tex2D(_ObstacleTex, lerp(centerPos, thisPos, pos))*m);
				pos -= sub; col *= saturate(1 - tex2D(_ObstacleTex, lerp(centerPos, thisPos, pos))*m);
				pos -= sub; col *= saturate(1 - tex2D(_ObstacleTex, lerp(centerPos, thisPos, pos))*m);
				pos -= sub; col *= saturate(1 - tex2D(_ObstacleTex, lerp(centerPos, thisPos, pos))*m);
				pos -= sub; col *= saturate(1 - tex2D(_ObstacleTex, lerp(centerPos, thisPos, pos))*m);
				pos -= sub; col *= saturate(1 - tex2D(_ObstacleTex, lerp(centerPos, thisPos, pos))*m);
				pos -= sub; col *= saturate(1 - tex2D(_ObstacleTex, lerp(centerPos, thisPos, pos))*m);
				pos -= sub; col *= saturate(1 - tex2D(_ObstacleTex, lerp(centerPos, thisPos, pos))*m);
				pos -= sub; col *= saturate(1 - tex2D(_ObstacleTex, lerp(centerPos, thisPos, pos))*m);
				col.rgb *= _EmissionColorMul;
                return col;
			}
		ENDCG
	}
}
}



Blending and blending light



Light of sources + light of environment

After light of sources and light of environment are rendered it is possible to mix them with each other. To do this, the textures are multiplied by their alpha and added. Then all this is superimposed on the image of the game and displayed on the screen. Screenshots of the result, higher resolution on a click.




And finally, the pros and cons


Pros:
  • Computations occur on the GPU.
  • The light sources are ordinary sprites, respectively, you can make a light source of any shape.
  • Each light source consumes very few resources.
  • Works on mobile devices, consuming ~ 8 ms per frame on Nexus 4.
  • Fully dynamic lighting. Obstacles can be created and destroyed on the fly without any loss of productivity.
  • Support for ambient light.
  • The system itself generates 6 DrawCalls, all light sources can fit one plus one more for ambient light.
  • Multi-colored light sources and obstacles.
  • The ability to emit light sources in a particle system. Performance is almost no different from ordinary particles.
  • Flexible quality settings.

Minuses:
  • The system illuminates on a grid, as a result of which small obstacles can be ignored. On powerful platforms, you can make the grid very small.
  • It is necessary to generate meshes for the light of the environment and obstacles.
  • The size of the cameras in which the lighting is created must be larger than the size of the game camera so that the light sources behind the screen are displayed correctly.
  • The computational complexity of the system is almost independent of the number of sources. This means that if it consumes 8 ms per frame with 10 light sources, then without sources it will consume about 8 ms.


PS In the presence of community interest, I will finalize and put it in the Asset Store.
PPS I posted, here is the PPPS link
Now free and open-source. Github

Also popular now: