Реалистичное гравитационное линзование на Unity

image
Эффект гравитационной линзы вызванный скоплением галактик RCS2 032727-132623

Возникла недавно необходимость реализовать на Unity достаточно правдоподобное изображение черной дыры и, соответственно, эффект гравитационного линзирования ею вызываемого. Первой мыслью было найти готовую реализацию и подстроить под себя, однако, поскольку ни одного достаточно хорошего решения так и не нашел (что весьма странно, зная насколько популярны игры на космическую тематику), решил реализовать эффект самостоятельно, а заодно и поделиться результатом с хабросообществом.

Для начала напишем скрипт который мы повесим на камеру, и который будет применять шейдер к выводимому на экран изображению.

Скрипт
using UnityEngine;

[ExecuteInEditMode]
public class Lens: MonoBehaviour {
	public Shader  shader;
	
	public float   ratio = 1;  	//Отношение высоты к длине экрана, для правильного отображения шейдера
	public float   radius = 0; 	//Радиус черной дыры измеряемый в тех же единицах, что и остальные объекты на сцене

	public GameObject BH;  //Объект, позиция которого берется за позицию черной дыры

	private Material _material; //Материал на котором будет находится шейдер
	protected Material material {
		get {
			if (_material == null) {
				_material = new Material (shader);
				_material.hideFlags = HideFlags.HideAndDontSave;
			}
			return _material;
		} 
	}

	protected virtual void OnDisable() {
		if( _material ) {
			DestroyImmediate( _material );
		}
	}

	void OnRenderImage (RenderTexture source, RenderTexture destination) {
		if (shader && material) {
			//Находим позицию черной дыры в экранных координатах
			Vector2 pos = new Vector2(
				this.camera.WorldToScreenPoint (BH.transform.position).x / this.camera.pixelWidth,
				1-this.camera.WorldToScreenPoint (BH.transform.position).y / this.camera.pixelHeight);

			//Устанавливаем все необходимые для шейдера параметры
			material.SetVector("_Position", new Vector2(pos.x, pos.y));
			material.SetFloat("_Ratio", ratio);
			material.SetFloat("_Rad", radius);
			material.SetFloat("_Distance", Vector3.Distance(BH.transform.position, this.transform.position));
			//И применяем к полученному изображению.
			Graphics.Blit(source, destination, material);
		}
	}
}

Теперь приступим к более важной части: написанию самого шейдера.

Первым делом, нам необходимо получить радиус, в зависимости от которого будем искажать изображение:
float2 offset = i.uv - _Position; //Сдвигаем наш пиксель на нужную позицию
float2 ratio = {_Ratio,1}; //определяем соотношение сторон экрана
float rad = length(offset / ratio); //определяем расстояние

В физике, формула преломления луча света проходящего на расстоянии r от объекта с массой M имеет вид:
image
Для нас M — масса черной дыры. Зная, что радиус черной дыры определяется как
image
Получаем следующую конструкцию
float deformation = 2*_Rad*1/pow(rad*z,2); 

где deformation — сила искажения в каждой конкретной точке, при этом z — некоторая зависимость размера искажения от расстояния на котором находится камера. Что бы понять как эта зависимость выражается, обратимся к формуле кольца Эйнштейна.
image
Где
image
В данной формуле нас интересует ее зависимость от дистанции, потому, большую ее часть можно отбросить наблюдая лишь за
image
Поскольку шейдер обрабатывает 2х мерное изображение, мы не можем сказать о том, как далеко находятся объекты. И хотя это можно реализовать с помощью карты глубины, исказить их корректно не получиться, так как потребуются изображения всего что находиться за каждым из объектов. Поэтому предположим, что DL<<DS и DL<<DLS. Тогда мы видим, что размер искажения обратно пропорционален корню растояния, получаем
deformation = 2*_Rad*1/pow(rad*pow(_Distance,0.5),2);

Теперь применим нашу деформацию:
offset =offset*(1-deformation);

Вернем изображение на место и отобразим.
offset += _Position;

half4 res = tex2D(_MainTex, offset);
return res;

Полный код шейдера
Shader "Gravitation Lensing Shader" {
Properties {
	_MainTex ("Base (RGB)", 2D) = "white" {}
}

SubShader {
	Pass {
		ZTest Always Cull Off ZWrite Off
		Fog { Mode off }
				
		CGPROGRAM
		#pragma vertex vert
		#pragma fragment frag
		#pragma fragmentoption ARB_precision_hint_fastest 
		#include "UnityCG.cginc"

		uniform sampler2D _MainTex;
		uniform float2 _Position;
		uniform float _Rad;
		uniform float _Ratio;
		uniform float _Distance;

		struct v2f {
			float4 pos : POSITION;
			float2 uv : TEXCOORD0;
		};

		v2f vert( appdata_img v )
		{
			v2f o;
			o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
			o.uv = v.texcoord;
			return o;
		}
		
		float4 frag (v2f i) : COLOR
		{
			float2 offset = i.uv - _Position; //Сдвигаем наш пиксель на нужную позицию
			float2 ratio = {_Ratio,1}; //определяем соотношение сторон экрана
			float rad = length(offset / ratio); //определяем расстояние от условного "центра" экрана.

			float deformation = 1/pow(rad*pow(_Distance,0.5),2)*_Rad*2;
			
			offset =offset*(1-deformation);
			
			offset += _Position;
			
			half4 res = tex2D(_MainTex, offset);
			//if (rad*_Distance<pow(2*_Rad/_Distance,0.5)*_Distance) {res.g+=0.2;} // проверка соблюдения радиуса эйнштейна
			//if (rad*_Distance<_Rad){res.r=0;res.g=0;res.b=0;} //проверка радиуса ЧД
			return res;
		}
		ENDCG

	}
}

Fallback off

}

Вот и все! Можно насладится результатом:


Данный шейдер реализует искажение лишь для одного массивного объекта. Для отображения того, что находиться перед черной дырой я использовал еще одну камеру которая рисует поверх основной. И хотя такое решение нельзя назвать элегантным, оно неплохо работает в моем случае.

P.S. Обратите внимание, что пост эффекты работают только в Pro версии Unity.
Поделиться публикацией
Похожие публикации
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 12
  • –9
    Шейдеры для меня это темный лес.
    Выложите пожалуйста вашу работу на Assetstore.
    • +3
      Работа является частью проекта, которым я пока не готов поделится. Все что необходимо для воссоздания эффекта, есть в статье.
    • +3
      Возникла недавно необходимость реализовать на Unity достаточно правдоподобное изображение черной дыры


      Это можно прямо сейчас на clientsfromhell отправлять.

      "Как-то черная дыра недостаточно реалистично выглядит, может, стоит поиграться с сингулярностью или насытить ее джетами?"
      • +4
        «Сделайте чёрную дыру посветлее, а то плохо видно».
        «А что за ней звёзды так поплыли?»
        • 0
          Опять же джетам без материи поступающей в Черную дыру не должно быть
        • 0
          А вам тоже нехватает double для координат? Как по мне — пусть сделают вектор в дабле и жизнь удалась.
          • 0
            По-моему диск немного ярковат. Без звезды рядом его вообще не должно быть, т.к. фотоны должны быть испущены чем-то.
            • +1
              Позвольте вставить свои пять копеек. Сначала дам ссылку на вики. Кстати, там же вы можете обнаржуить уравнение записанное в угловой мере.

              Каюсь, там не описана вся область, а только общая формула, однако уже из приведенного достаточно понятно, что формула в вашей статье — лишь частный случай.

              А именно данная формула соответствует потенциалу непрозрачной точечной массы. Формула справедлива для звездо- и планетоподобных объектов, в том числе ЧД звездной и промежуточных масс. Однако, если говорить о галактиках, то наблюдаемые кресты Эйнштена и иже с ними ясно показывают, что не соответствует реальности. В модели точечной массы может быть только два изображения, наблюдаются вплоть до 8 (если мне не изменяет память). Кстати, лет 5 назад, общепризнанным методом определения реального грав потенциала галактики был метод научного тыка, как сейчас не знаю

              Ладно, я немного отвлекся. Для ЧД звездно массы, скорее всего мы получим ситуацию близкой к солнечной — единственным проявлением будет отклонения света, ни второго изображения, ни повторения сигнала мы не получим. Ну и да, при этом источник должен лежать за ЧД ( по отношению к наблюдателю)

              Ну и теперь по видео: если честно я из него ничего не понял: что за джеты, что за ореол, что за замкнутые линии, напоминающие потенциал Роша? Моделируется ТДС?
              • 0
                Не подскажете, можно ли похожее реализовать на бесплатной версии юнити? Нужно делать простенькое искажение вокруг объекта, но под ним может быть фон, возможно многослойный или скайбокс. Или вообще никак? А может быть накладывать шейдер на каждый слой? Игра 2D, делаю гиперпрыжок, хочется его оформить как открытие миниатюрной черной дыры, но в шейдерах пока плохо разбираюсь, вот и хочу понять, реально ли или идти делать эффект по-проще?
                • 0
                  P.S. не бейте за «подскажете», я случайно!
                  • 0
                    Думаю вам стоит попробовать использовать анимированную модель с наложенной на нее текстурой фона. Тогда изменяя ее полигоны можно создать подобный эффект искажения пространства. Хотя выглядеть это может и чуть хуже, но при верной подаче все получится.
                    • 0
                      Спасибо, это будет интересная задача.

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.