Главная » Хабрахабр » [Из песочницы] Анимированный эффект щита космического корабля в Unity3D

[Из песочницы] Анимированный эффект щита космического корабля в Unity3D

Привет Хабр! Я хочу рассказать как сделать шейдер для отрисовки щита космического корабля в Unity3D.

Вот такой

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

Заинтересовавшихся прошу под кат. (Осторожно! Внутри тяжелые картинки и гифки).
Статья написана как набор инструкций с пояснениями, даже полный новичок сможет выполнить их и получить готовый шейдер, но для понимания того что происходит желательно ориентироваться в базовых терминах:

  • Шейдер
  • Вершинный шейдер
  • Фрагментный/пиксельный шейдер
  • UV-координаты

Эффект состоит из 3-х основных компонентов:

  • Базовый полупрозрачный шейдер, использующий текстуру как карту прозрачности и цвет как цвет щита
  • Эффект Френеля
  • Реакция на попадание по щиту
  • Анимация

Будем по порядку добавлять эти компоненты в шейдер и к концу статьи получим эффект как на КДПВ.

Базовый шейдер

Начнем со стандартного шейдера Unity3D:

Исходный код стандартного неосвещенного шейдера

Shader "Unlit/NewUnlitShader"
{ Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag // make fog work #pragma multi_compile_fog #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; UNITY_FOG_COORDS(1) float4 vertex : SV_POSITION; }; sampler2D _MainTex; float4 _MainTex_ST; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); UNITY_TRANSFER_FOG(o,o.vertex); return o; } fixed4 frag (v2f i) : SV_Target { // sample the texture fixed4 col = tex2D(_MainTex, i.uv); // apply fog UNITY_APPLY_FOG(i.fogCoord, col); return col; } ENDCG } }
}

Подготовим его для наших целей

  1. Переименуем его в Shields/Transparent для этого заменим строку Shader "Unlit/NewUnlitShader" на Shader "Shields/Transparent"
  2. Полупрозрачные элементы в юнити отрисовываются в отдельной очереди и особым способом, для этого необходимо сообщить юнити что шейдер полупрозрачный заменив Tags { "RenderType"="Opaque" } на Tags { "Queue"="Transparent" "RenderType"="Transparent" }
    Для отрисовки полупрозрачных элементов требуется задать особый режим смешивания, для этого после Tags { "Queue"="Transparent" "RenderType"="Transparent" } добавим строку Blend SrcAlpha OneMinusSrcAlpha.

    Так же необходимо отключить запись в Z-Buffer — он используется для сортировки непрозрачных объектов, а для отрисовки полупрозрачных объектов будет только мешать. Для этого добавим строку

    ZWrite Off
    

    после

    Tags { "Queue"="Transparent" "RenderType"="Transparent" }
    

  3. Эффект щита не будет использоваться вместе со встроенным в юнити эффектом тумана, поэтому удалим все упоминания о нем из шейдера — удаляем строки
    UNITY_FOG_COORDS(1)
    
    UNITY_TRANSFER_FOG(o,o.vertex)
    
    UNITY_APPLY_FOG(i.fogCoord, col)
    

Мы получили базовый неосвещенный полупрозрачный шейдер. Теперь из него нужно сделать шейдер, который использует текстуру как маску полупрозрачности и цвет заданный пользователем как цвет пикселя:

  1. Сейчас шейдер имеет лишь один входной параметр — текстуру, добавим цвет как входной параметр, а параметр текстуры переименую в Transparency Mask. В юнити входные параметры для шейдера задаются внутри блока Properties, сейчас он выглядит так:
    Properties
    { _MainTex ("Texture", 2D) = "white" {}
    }
    
    

    Добавим входной параметр цвет и переименуем текстуру:

    Properties
    { _ShieldColor("Shield Color", Color) = (1, 0, 0, 1) _MainTex ("Transparency Mask", 2D) = "white" {}
    }
    
    

    Чтобы входные параметры, заданные в блоке Properties, стали доступны в вершинном и фрагментном шейдерах, их нужно объявить как переменные внутри прохода шейдера — вставим строку

     float4 _ShieldColor; 
    

    перед строкой

    v2f vert (appdata v)
    

    Подробнее про передачу параметров в шейдер можно почитать в официальной документации.

  2. Цвет отдельного пикселя определяется возвращаемым значением фрагментного шейдера,
    сейчас он выглядит так:
    fixed4 frag (v2f i) : SV_Target
    { // sample the texture fixed4 col = tex2D(_MainTex, i.uv); return col;
    }
    

    Что такое v2f

    Здесь v2f — возвращаемое значение вершинных шейдеров интерполированное для данного пикселя на экране

    struct v2f
    { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION;
    };
    
    

    uv — текстурная координата пикселя
    vertext — координата пикселя в экранных координатах

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

    Сделаем следующее:

    fixed4 frag (v2f i) : SV_Target
    { // sample the texture fixed4 transparencyMask = tex2D(_MainTex, i.uv); return fixed4(_ShieldColor.r, _ShieldColor.g, _ShieldColor.b, transparencyMask.r);
    }
    
    

    То есть семплируем текстуру как раньше, но вместо возврата её цвета напрямую, возвращаем цвет как _ShieldColor с альфа каналом, взятым из красного цвета текстуры.

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

    Предлагаю читателю это сделать самому или заглянуть под спойлер.

    Скрытый текст

    Properties
    { _ShieldIntensity("Shield Intensity", Range(0,1)) = 1.0 _ShieldColor("Shield Color", Color) = (1, 0, 0, 1) _MainTex ("Transparency Mask", 2D) = "white" {}
    }
    
    
    float _ShieldIntensity;
    fixed4 frag (v2f i) : SV_Target
    { // sample the texture fixed4 transparencyMask = tex2D(_MainTex, i.uv); return fixed4(_ShieldColor.r, _ShieldColor.g, _ShieldColor.b, _ShieldIntensity * transparencyMask.r);
    }
    
    

Должно получиться примерно следующее:

Здесь и далее я использую вот эту бесшовную текстуру шума

Полный листинг получившегося шейдера

Shader "Shields/Transparent"
{ Properties { _ShieldIntensity("Shield Intensity", Range(0,1)) = 1.0 _ShieldColor("Shield Color", Color) = (1, 0, 0, 1) _MainTex ("Transparency Mask", 2D) = "white" {} } SubShader { Tags { "Queue"="Transparent" "RenderType"="Transparent" } ZWrite Off Blend SrcAlpha OneMinusSrcAlpha Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float3 normal: NORMAL; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; sampler2D _MainTex; float4 _MainTex_ST; float4 _ShieldColor; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); return o; } float _ShieldIntensity; fixed4 frag (v2f i) : SV_Target { // sample the texture fixed4 transparencyMask = tex2D(_MainTex, i.uv); return fixed4(_ShieldColor.r, _ShieldColor.g, _ShieldColor.b, _ShieldIntensity * transparencyMask.r); } ENDCG } }
}

Пока выглядит не очень, но это база, на которой будет строиться весь эффект.

Эффект Френеля

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

Приступим к реализации, используя приближенную формулу из cg tutorial on nvidia

где I — направление на камеру, N — нормаль поверхности в точке падения

  1. Первым делом скопируем шейдер в новый файл и переименуем его в Shields/Fresnel, чтобы иметь историю изменений
  2. Как видно из формулы, нам понадобятся 3 новых параметра для шейдера Bias,
    Scale, Power
    . Я рассчитываю, что читатель уже научился добавлять параметры в шейдер и не буду приводить детальные инструкции как это сделать. При затруднениях всегда можно посмотреть в полный код в конце раздела
  3. Рассчитаем I и N в вершинном шейдере. Вершинный шейдер в нашем шейдере это функция v2f vert (appdata v) возвращаемое значение — это описанная ранее структура v2f, а appdata это параметры вершины взятые из меша.
    Что такое appdata

    struct appdata
    { float4 vertex : POSITION; float3 normal: NORMAL; float2 uv : TEXCOORD0;
    };
    
    

    vertex — координаты вершины в локальных координатах
    normal — нормаль к поверхности заданная для этой вершины
    uv — текстурные координаты вершины

    I — направление на камеру в мировых координатах — можно посчитать, как разницу мировых координат камеры и мировых координат вершины. В шейдерах Unity матрица перехода из локальных координат в мировые доступна в переменной unity_ObjectToWorld, а мировые координаты камеры в переменной _WorldSpaceCameraPos. Зная это, можно вычислить I следующими строками в коде вершинного шейдера:

    float4 worldVertex = mul(unity_ObjectToWorld, v.vertex);
    float3 I = normalize(worldVertex - _WorldSpaceCameraPos.xyz);
    
    

    N — нормаль поверхности в мировых координатах — вычислить ещё проще:

     float3 normWorld = normalize(mul(unity_ObjectToWorld, v.normal));
    

  4. Теперь можно посчитать непрозрачность щита по формуле эффекта Френеля:
    float fresnel = _Bias + _Scale * pow(1.0 + dot(I, normWorld), _Power);
    

    Можно заметить, что значение fresnel при определенных значениях переменных может быть меньше 0, это даст цветовые артефакты при отрисовке. Ограничим значение переменной интервалом [0;1] с помощью функции saturate:

    float fresnel = saturate(_Bias + _Scale * pow(1.0 + dot(I, normWorld), _Power));
    

  5. Осталось только передать это значение в пиксельный шейдер. Для этого добавляем в структуру v2f поле intensity:
    struct v2f
    { float2 uv : TEXCOORD0; float intensity : COLOR0; float4 vertex : SV_POSITION;
    };
    
    

    ( COLOR0 — это семантика, объяснение что это такое выходит за рамки этой статьи, заинтересовавшиеся могут почитать про semantics в hlsl).

    Теперь мы можем заполнить это поле в вершинном шейдере и использовать во фрагментном:

    v2f vert (appdata v)
    { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); float4 worldVertex = mul(unity_ObjectToWorld, v.vertex); float3 normWorld = normalize(mul(unity_ObjectToWorld, v.normal)); float3 I = normalize(worldVertex - _WorldSpaceCameraPos.xyz); float fresnel = saturate(_Bias + _Scale * pow(1.0 + dot(I, normWorld), _Power)); o.intensity = fresnel; return o;
    } float _ShieldIntensity;
    fixed4 frag (v2f i) : SV_Target
    { // sample the texture fixed4 transparencyMask = tex2D(_MainTex, i.uv); return fixed4(_ShieldColor.r, _ShieldColor.g, _ShieldColor.b, (_ShieldIntensity + i.intensity) * transparencyMask.r);
    }
    
    

    Можно заметить, что теперь сложить _ShieldIntensity и i.intensity можно ещё в вершинном шейдере, так и сделаем.

Готово! Поиграв параметрами уравнения Френеля, можно получить такую картинку

Мои параметры

Bias=-0.5, Scale=1, Power=1

Полный листинг щита с эффектом Френеля

Shader "Shields/Fresnel"
{ Properties { _ShieldIntensity("Shield Intensity", Range(0,1)) = 1.0 _ShieldColor("Shield Color", Color) = (1, 0, 0, 1) _MainTex ("Transparency Mask", 2D) = "white" {} _Bias("Bias", float) = 1.0 _Scale("Scale", float) = 1.0 _Power("Power", float) = 1.0 } SubShader { Tags { "Queue"="Transparent" "RenderType"="Transparent" } ZWrite Off Blend SrcAlpha OneMinusSrcAlpha Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float3 normal: NORMAL; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float intensity : COLOR0; float4 vertex : SV_POSITION; }; sampler2D _MainTex; float4 _MainTex_ST; float4 _ShieldColor; float _ShieldIntensity; float _Bias; float _Scale; float _Power; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); float4 worldVertex = mul(unity_ObjectToWorld, v.vertex); float3 normWorld = normalize(mul(unity_ObjectToWorld, v.normal)); float3 I = normalize(worldVertex - _WorldSpaceCameraPos.xyz); float fresnel = saturate(_Bias + _Scale * pow(1.0 + dot(I, normWorld), _Power)); o.intensity = fresnel + _ShieldIntensity; return o; } fixed4 frag (v2f i) : SV_Target { // sample the texture fixed4 transparencyMask = tex2D(_MainTex, i.uv); return fixed4(_ShieldColor.r, _ShieldColor.g, _ShieldColor.b, i.intensity * transparencyMask.r); } ENDCG } }
} 

Теперь можно переходить к самому интересному — отображению попаданий по щиту.

Отрисовка попадания

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

  1. Для реализации эффекта шейдеру нужно как-то узнать в какой точке произошло попадание и в какое время. Передачей этих аргументов будет заниматься скрипт на GameObject'е щита, а так как с# скриптинг не является предметом этой статьи, я просто приведу исходные коды скриптов:
    Листинг скрипта для объекта с щитом

    public class ShieldHitter : MonoBehaviour
    { private static int[] hitInfoId = new[] { Shader.PropertyToID("_WorldHitPoint0"), Shader.PropertyToID("_WorldHitPoint1"), Shader.PropertyToID("_WorldHitPoint2") }; private static int[] hitTimeId = new[] { Shader.PropertyToID("_HitTime0"), Shader.PropertyToID("_HitTime1"), Shader.PropertyToID("_HitTime2") }; private Material material; void Start() { if (material == null) { material = this.gameObject.GetComponent<MeshRenderer>().material; } } int lastHit = 0; public void OnHit(Vector3 point, Vector3 direction) { material.SetVector(hitInfoId[lastHit], point); material.SetFloat(hitTimeId[lastHit], Time.timeSinceLevelLoad); lastHit++; if (lastHit >= hitInfoId.Length) lastHit = 0; } void OnCollisionEnter(Collision collision) { OnHit(collision.contacts[0].point, Vector3.one); }
    }
    
    

    Листинг скрипта для камеры

    using UnityEngine; [ExecuteInEditMode]
    public class CameraControls : MonoBehaviour
    { private const int minDistance = 25; private const int maxDistance = 25; private const float minTheta = 0.01f; private const float maxTheta = Mathf.PI - 0.01f; private const float minPhi = 0; private const float maxPhi = 2 * Mathf.PI ; [SerializeField] private Transform _target; [SerializeField] private Camera _camera; [SerializeField] [Range(minDistance, maxDistance)] private float _distance = 25; [SerializeField] [Range(minTheta, maxTheta)] private float _theta = 1; [SerializeField] [Range(minPhi, maxPhi)] private float _phi = 2.5f; [SerializeField] private float _angleSpeed = 2.0f; [SerializeField] private float _distanceSpeed = 2.0f; // Update is called once per frame void Update () { if (_target == null || _camera == null) { return; } if (Application.isPlaying) { if (Input.GetKey(KeyCode.Q)) { _distance += _distanceSpeed * Time.deltaTime; } if (Input.GetKey(KeyCode.E)) { _distance -= _distanceSpeed * Time.deltaTime; } Mathf.Clamp(_distance, minDistance, maxDistance); if (Input.GetKey(KeyCode.A)) { _phi += _angleSpeed * Time.deltaTime; } if (Input.GetKey(KeyCode.D)) { _phi -= _angleSpeed * Time.deltaTime; } _phi = _phi % (maxPhi); if (Input.GetKey(KeyCode.S)) { _theta += _angleSpeed * Time.deltaTime; } if (Input.GetKey(KeyCode.W)) { _theta -= _angleSpeed * Time.deltaTime; } _theta = Mathf.Clamp(_theta, minTheta, maxTheta); Vector3 newCoords = new Vector3 { x = _distance * Mathf.Sin(_theta) * Mathf.Cos(_phi), z = _distance * Mathf.Sin(_theta) * Mathf.Sin(_phi), y = _distance * Mathf.Cos(_theta) }; this.transform.position = newCoords + _target.position; this.transform.LookAt(_target); if (Input.GetMouseButtonDown(0)) { Ray ray = _camera.ScreenPointToRay(Input.mousePosition); RaycastHit hit; var isHit = Physics.Raycast(ray, out hit); if (isHit) { ShieldHitter handler = hit.collider.gameObject.GetComponent<ShieldHitter>(); Debug.Log(hit.point); if (handler != null) { handler.OnHit(hit.point, ray.direction); } } } } }
    }
    
    

  2. Как и в прошлый раз сохраним шейдер под новым именем Shields/FresnelWithHits
  3. Идея заключается в том, чтобы в каждой точке щита рассчитать возмущение щита от попаданий рядом, при этом, чем раньше попадание произошло, тем меньше его влияние на возмущение щита.

    Я выбрал следующую формулу:

    $$display$$intensity = (1 - time) * (1/distance - 1)$$display$$

    где:
    distance — доля расстояния до точки попадания от максимального, [0, 1]time — доля времени жизни от максимального, [0, 1]Таким образом, интенсивность обратно пропорциональна расстоянию до точки столкновения,
    пропорциональна времени оставшемуся до конца действия попадания, а также равна 0 при дистанции равной или больше максимальной и при оставшемся времени равному 0.

    Я бы хотел найти функцию, которая бы удовлетворяла этим условиям без необходимости ограничивать область значений времени и расстояния, но эта — всё что у меня есть.

  4. Отрисовка эффектов попадания в шейдерах неизбежно накладывает ограничения на количество одновременно обрабатываемых попаданий, для примера я выбрал 3 одновременно отображающихся попадания. Добавим в шейдер входные параметры WorldHitPoint0, WorldHitPoint1, WorldHitPoint2, HitTime0, HitTime1, HitTime2 — по паре для каждого одновременно обрабатываемого попадания. Также нам понадобятся параметры MaxDistance — на какое максимальное расстояние распространяется возмущение щита от попадания, и HitDuration — длительность возмущения щита от попадания.
  5. Для каждого попадания рассчитаем в вершинном шейдере time и distance
    float t0 = saturate((_Time.y - _HitTime0) / _HitDuration);
    float d0 = saturate(distance(worldVertex.xyz, _WorldHitPoint0.xyz) / (_MaxDistance)); float t1 = saturate((_Time.y - _HitTime1) / _HitDuration);
    float d1 = saturate(distance(worldVertex.xyz, _WorldHitPoint1.xyz) / (_MaxDistance)); float t2 = saturate((_Time.y - _HitTime2) / _HitDuration);
    float d2 = saturate(distance(worldVertex.xyz, _WorldHitPoint2.xyz) / (_MaxDistance));
    
    

    и посчитаем суммарную интенсивность попаданий по формуле:

    float hitIntensity = (1 - t0) * ((1 / (d0)) - 1) + (1 - t1) * ((1 / (d1)) - 1) + (1 - t2) * ((1 / (d2)) - 1);
    
    

    Осталось лишь сложить интенсивность щита от попаданий с интенсивностью от других эффектов:

    o.intensity = fresnel + _ShieldIntensity + hitIntensity;
    
    

  6. Настраиваем материал, выставляем правильные значения расстояния и вуаля:

    Уже достаточно хорошо, так? Но есть одна проблема. Попадания на обратной стороне щита не видны. Причина этого в том, что, по умолчанию, полигоны, нормаль которых направлена от камеры, не рисуются. Чтобы заставить графический движок их рисовать нужно добавить после ZWrite Off строку Cull off. Но и тут нас поджидает проблема:
    эффект Френеля, реализованный в прошлом разделе, подсвечивает все полигоны, смотрящие от камеры — придется менять формулу на

    float dt = dot(I, normWorld);
    fresnel = saturate(_Bias + _Scale * pow(1.0 - dt * dt, _Power));
    
    

    Так как исходная формула — уже аппроксимация, использование квадрата не оказывает значимого эффекта на результат (его можно подправить другими параметрами) и позволяет не добавлять дорогой оператор ветвления и не использовать дорогой sqrt.

    Запускаем, проверяем и:

    Теперь всё очень неплохо.

  7. Остался последний штрих: для придания эффекту «живости» можно прибавить к текстурным координатам шума текущее время для создания эффекта движения щита по сфере.
    o.uv = TRANSFORM_TEX(v.uv, _MainTex) + _Time.x / 6;
    
    

Финальный результат:

Листинг финальной версии шейдера

Shader "Shields/FresnelWithHits"
{ Properties { _ShieldIntensity("Shield Intensity", Range(0,1)) = 1.0 _ShieldColor("Shield Color", Color) = (1, 0, 0, 1) _MainTex ("Transparency Mask", 2D) = "white" {} _Bias("Bias", float) = 1.0 _Scale("Scale", float) = 1.0 _Power("Power", float) = 1.0 _WorldHitPoint0("Hit Point 0", Vector) = (0, 1, 0, 0) _WorldHitTime0("Hit Time 0", float) = -1000 _WorldHitPoint1("Hit Point 1", Vector) = (0, 1, 0, 0) _WorldHitTime1("Hit Time 1", float) = -1000 _WorldHitPoint2("Hit Point 2", Vector) = (0, 1, 0, 0) _WorldHitTime2("Hit Time 2", float) = -1000 _HitDuration("Hit Duration", float) = 10.0 _MaxDistance("MaxDistance", float) = 0.5 } SubShader { Tags { "Queue" = "Transparent" "RenderType" = "Transparent" } ZWrite Off Cull Off Blend SrcAlpha OneMinusSrcAlpha Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float3 normal: NORMAL; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float intensity : COLOR0; float4 vertex : SV_POSITION; }; sampler2D _MainTex; float4 _MainTex_ST; float4 _ShieldColor; float _ShieldIntensity; float _Bias; float _Scale; float _Power; float _MaxDistance; float _HitDuration; float _HitTime0; float4 _WorldHitPoint0; float _HitTime1; float4 _WorldHitPoint1; float _HitTime2; float4 _WorldHitPoint2; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex) + _Time.x / 6; float4 worldVertex = mul(unity_ObjectToWorld, v.vertex); float3 normWorld = normalize(mul(unity_ObjectToWorld, v.normal)); float3 I = normalize(worldVertex - _WorldSpaceCameraPos.xyz); float fresnel = 0; float dt = dot(I, normWorld); fresnel = saturate(_Bias + _Scale * pow(1.0 - dt * dt, _Power)); float t0 = saturate((_Time.y - _HitTime0) / _HitDuration); float d0 = saturate(distance(worldVertex.xyz, _WorldHitPoint0.xyz) / (_MaxDistance)); float t1 = saturate((_Time.y - _HitTime1) / _HitDuration); float d1 = saturate(distance(worldVertex.xyz, _WorldHitPoint1.xyz) / (_MaxDistance)); float t2 = saturate((_Time.y - _HitTime2) / _HitDuration); float d2 = saturate(distance(worldVertex.xyz, _WorldHitPoint2.xyz) / (_MaxDistance)); float hitIntensity = (1 - t0) * ((1 / (d0)) - 1) + (1 - t1) * ((1 / (d1)) - 1) + (1 - t2) * ((1 / (d2)) - 1); o.intensity = fresnel + _ShieldIntensity + hitIntensity; return o; } fixed4 frag (v2f i) : SV_Target { // sample the texture fixed4 transparencyMask = tex2D(_MainTex, i.uv); return fixed4(_ShieldColor.r, _ShieldColor.g, _ShieldColor.b, saturate(i.intensity * transparencyMask.r)); } ENDCG } }
} 

То что нужно.

Вот так несложно и не слишком дорого можно получить достаточно красивый эффект щита космического корабля.

Вместо послесловия: оптимизация

Обозначу основные направления возможной оптимизации:

  • Удалить неиспользуемое: эффект Френеля, базовый полупрозрачный щит — всё это не бесплатно и если некоторые из компонентов не нужны, нужно их удалить.
  • t0, t1, t2 можно считать на CPU раз за кадр для каждого щита в скрипте. Тем самым можно убрать 3 saturate и кучу вычислений.
  • Использовать числа с плавающей точкой с меньшей точностью, во многих местах можно обойтись fixed или даже half вместо float.
  • Если на экране рисуется много щитов, есть смысл рассмотреть использование инстансинга.

x

Ещё Hi-Tech Интересное!

Справочная: Яндекс.Телефон

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

Что если разделение прибыли 30/70 перестанет быть стандартом геймдева?

На середине разработки игры могут поменяться движок, жанр, сюжет и сеттинг, но одно известно точно — когда игра выйдет, магазины заберут 30% прибыли. Геймдев — индустрия полная неопределенностей. С одной стороны его диктуют закрытые платформы, вроде игровых консолей или айфонов, ...