Главная » Хабрахабр » [Из песочницы] Анимированный эффект щита космического корабля в 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 Интересное!

[Перевод] Python Developer Tools от Microsoft. Начало работы

Последние несколько лет специалисты Microsoft трудились над тем, чтобы добавить поддержку инструментов разработчика Python в одни из наших самых популярных продуктов: Visual Studio Code и Visual Studio. В этом году все заработало. В статье мы познакомимся с инструментами разработчика Python ...

[Перевод] Вселенная, соответствующая нашим текущим представлениям, может оказаться невозможной

Новая физическая гипотеза бросает вызов лидирующей «теории всего» Один заголовок поразил его так, что он сбросил все остатки сна. 25 июня физик Тимм Вразе [Timm Wrase], живущий в Вене, проснулся, и сонно листал в онлайне список недавно опубликованных физических работ. ...