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



Всем привет. Как известно, 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.
Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 20

    –3
    Интересно, готов купить. К тому же обещаете такую производительность!

    Хотелось бы больше примеров использования/реализации, а не только готовые картинки. С удовольствием бы посмотрел более мелкую сетку в действие.
      0
      Пока существует только демо с ракетой из поста. Дальше планирую сделать простенький платформер.
        0
        Как раз хочу использовать в платформере, реализуйте комнату с многочисленными колонами, с однотипными и разнотипными формами, для проверки, падения и угла наклона света при разных ракурсах.

        Надеюсь поняли меня, если что могу накидать прототип.
          0
          Пишет, что сайт вышел за процессорный лимит
            0
            Перезалил на другой хостинг.
              0
              Спасибо. Исходников, видно, не будет?
        –3
        Тоже заинтересован, выкладывайте в стор. Только с ценой не наглейте :-)
          +18
          Когда зашел — ожидал увидеть нормальный алгоритм, а не алгоритм в двух строчках (хотя-бы вставки кода делайте) с готовыми картинками.

          P.S.:
          На хабре приятно делиться знаниями, а не продавать их на ассет-сторе.
            0
            Аналогично, коллега! Читал статью и надеялся увидеть исходники и побольше теории.
              0
              Исходники очень легко вытаскиваются из билда, автор даже не озаботился обфускацией, так что пусть не расстраивается, если какой-то шустрый китаец выложит этот код в стор первее самого автора =)
              i.imgur.com/B04anKA.png
                0
                Проще уж самому написать, чем выковыривать и разбираться в зависимостях :)
                  0
                  Нам с вами — да, но среднестатистическому хитрому китайцу (как показывает практика) — нет. Он ещё и демку эту продавать будет заодно, где-нибудь на локальных китайских ресурсах.
              0
              Освещение (да и стиль ваших игр) чем-то напоминает тёмные локации Knytt Underground. Вы случайно им не руководствовались?
                –2
                Нет. В основном смотрел на террарию.
                +1
                8мс даже при 30фпс это 25% общего бюджета на кадр(а при 60 так и вообще все 50%). С другой стороны, если все действительно выполняется на GPU и он все равно недогружен работой, это может быть и без разницы.А выглядит, судя по демо очень клево.
                И конечно было бы интересно взглянуть на шейдер трассировки.
                  0
                  Шейдер трассировки достаточно простой. Ниже одна из его версий. А насчет 8 мс на рисование я немного не правильно написал. В 8 мс/кадр укладывается вся представленная мной демка, содержащая 30-40 Drawcalls и около 8000 треугольников.

                  Трассировка на 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
                  	}
                  }
                  
                  }
                  

                  +1
                  Вообще сама система крайне интересная, пускай и применение ее достаточно узкое. Было бы неплохо доработать это и выложить в Asset Store — бесплатно или платно сами уж решайте.

                  Кстати ссылки на демо битые :(
                    0
                    Вы можете булетами сравнить ваше решение с 2D Light System и 2D Dynamic Lights and Shadows PRO? Хочется понять, чем такой подход лучше/хуже
                      +1
                      2D Dynamic Lights and Shadows PRO судя по всему работает через колайдеры и генерацию мешей. Свет не мягкий, четко видны грани. При небольшом количестве источников освещения и колайдеров на сцене должно работать быстрее, чем у меня. Возможно, присутствует та же проблема что и у 2DVLS, когда источник света больших размеров считается очень не точно из-за того, что используются рейкасты физики.
                      2D Light System по идее работает схожим образом с моей т.е. на GPU. Только там используется плоскость XZ вместо XY и, судя по всему, нет поддержки от автора.
                      Сам я не пробовал использовать ни одну из этих двух систем, сравниваю только по описанию и скриншотам.
                        0
                        Спасибо! Если есть возможность, поделись пакетом. Если в итоге понравится, то к выходу в релиз обязательно приобрету лицензию

                    Only users with full accounts can post comments. Log in, please.