Как стать автором
Обновить

2D система освещения для Unity3D, работающая на GPU

Время на прочтение5 мин
Количество просмотров34K


Всем привет. Как известно, Unity3D отсутствует поддержка освещения для 2D игр. В Asset Store можно найти такую систему, но у неё есть один недостаток — она работает на CPU и потребляет весьма много ресурсов (64-4096 рейкастов за кадр на каждый источник света). Поэтому я решил сделать своё освещение, производительности которого хватило бы для мобильных устройств. Для этого вычисления были перенесены на GPU. Получилось что-то похожее свет Terraria или Starbound.

Ссылка на демку. Стрелки — движение, пробел — шасси, R — перезапуск. Скриншоты взяты из неё.

Всё освещение считается на небольших текстурах, в примере используются 160х88 пикселей. При повышении разрешения можно добиться очень мелкой сетки, которую будет сложно заметить, правда это уже не для мобильных платформ. Из-за того, что вычисления производятся на таких небольших текстурах можно использовать довольно тяжелые шейдеры.

Для работы освещения используются 3 камеры, каждая из которых отвечает за свою часть системы: источники света, препятствия света, свет окружения. Затем источники света и свет окружения смешиваются и накладываются на игровую камеру.

Теперь подробнее, в порядке отрисовки.

Препятствия света



Текстура препятствий света. RGB каналы. Эта и последующие похожие текстуры имеют масштаб 400%

Вот такую текстуру выдает камера. Черные области полностью прозрачны, белые — полностью непрозрачные. Также поддерживаются цветные области, например, полностью красный пиксель будет блокировать красную часть света и пропускать зеленую и синюю.

Свет окружения



Источники света окружения


Источники света окружения. Альфа канал


Итеративно генерируемая текстура света окружения


Так выглядит немного усиленный свет окружения, без обычных источников освещения

Тут всё несколько сложнее. Такой тип освещения я реализовал для того, чтобы добавить слабый свет на то пространство, где нет источников света. В примере с помощью него реализована неяркая подсветка всего свободного пространства. RGB канал управляет цветом, альфа канал силой свечения. Главное отличие этого типа света от обычных источников состоит в том, что он считается итеративно и не имеет направления.

Алгоритм расчета для одного пикселя:

  1. Берем начальное значение пикселя из предыдущей итеративной текстуры.
  2. Вычитаем из пикселя силу препятствия из текстуры препятствий.
  3. Прибавляем к пикселю силу свечения из текстуры источников света окружения.
  4. Прибавляем к пикселю усредненное значение соседних пикселей


Источники света



Источники света

Обычные источники — главная часть системы освещения. Для их рисования используется нечто похожее на спрайты. Весь цвет исходит из центра, хотя при желании точку можно перенести куда угодно.
Для источников света доступно несколько шейдеров с трассировкой пути и один без неё. Шейдеры с трассировкой различаются по количеству трассируемых точек. Я использую два таких — один на 9 точек, работающий с Shader Model 2.0, другой на 20 точек для Shader Model 3.0. Шейдер без трассировки пути используется для систем частиц, так как ему не нужна какая-либо дополнительная информация для работы.

Алгоритм трассировки путя:

  1. Берем яркость пикселя из текстуры.
  2. Находим позицию источника света и текущего пикселя в текстуре препятствий.
  3. Уменьшаем текущую яркость на значения пикселей препятствий, которые лежат между двумя точками из предыдущего шага.

Шейдер трассировки на 9 точек
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
	}
}

}



Смешивание и наложение света



Свет источников + свет окружения

После того, как свет источников и свет окружения отрендерен можно смешивать их друг с другом. Для этого текстуры умножаются на свою альфу и складываются. Затем всё это накладывается на изображение игры и выводится на экран.


Скриншоты результата, большее разрешение по клику.

И напоследок плюсы и минусы


Плюсы:
  • Вычисления происходят на GPU.
  • Источниками света являются обычные спрайты, соответственно можно делать источник света любой формы.
  • Каждый источник света потребляет очень мало ресурсов.
  • Работает на мобильный устройствах, потребляя ~8 мс за кадр на Nexus 4.
  • Полностью динамическое освещение. Препятствия можно создавать и уничтожать на лету без всякой потери производительности.
  • Поддержка света окружения.
  • Сама по себе система генерирует 6 DrawCalls, все источники света можно уместить один плюс еще один для света окружения.
  • Разноцветные источники света и препятствия.
  • Возможность эмитить источники света в системе частиц. Производительность почти не отличается от обычных партиклов.
  • Гибкие настройки качества.

Минусы:
  • Система освещает по сетке, в следствии чего мелкие препятствия могут игнорироваться. На мощных платформах можно сделать сетку очень мелкой.
  • Необходимо генерировать меши для света окружения и препятствий.
  • Размер камер, в которых создается освещение должен быть больше размера игровой камеры, чтобы корректно отображались источники света за экраном.
  • Вычислительная сложность системы почти не зависит от количества источников. Это означает, что если она потребляет 8 мс за кадр с 10 источниками света, то без источников она так и будет потреблять около 8 мс.


P.S. При наличии интереса сообщества доработаю и выложу в Asset Store.
P.P.S. Выложил, вот ссылка
P.P.P.S. Теперь бесплатно и open-source. GitHub.
Теги:
Хабы:
+33
Комментарии20

Публикации

Изменить настройки темы

Истории

Работа

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн