Глава четвёртая

3D Шейдеры

Простейший 3D шейдер

Shader "Tutorial/Simple3DShader"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "white" {}
	}
	SubShader
	{
		// Заметьте отсуствие строк, которые ускоряли работу 
		// шейдеров для картинок, в шейдере, накладываемом
		// на 3д объект они бы привели к артефактам
		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"

			// Ранее мы пользовались заранее подготовленными
			// структурами для шейдеров, теперь мы объявили 
			// свои собственные
			struct appdata
			{
				// Позиция вершины в мировых координатах
				float4 vertex : POSITION;
				// Координаты развёртки, соответствующие вершине
				float2 uv : TEXCOORD0;
			};

			struct v2f
			{
				// Координаты развёртки
				float2 uv : TEXCOORD0;
				// Экранные координаты вершины
				float4 vertex : POSITION;
			};
			// То, что appdata и v2f так похожи, это скорее 
			// случайность, чаще эти структуры заметно отличаются

			// Кроме структур, мы использовали и готовый 
			// вертексный шейдер, который был создан командой Unity
			v2f vert (appdata v)
			{
				v2f o;
				// Чтобы получить экранные координаты вершины, 
				// умножаем матрицу вида камеры на мировые 
				// координаты вершины
				o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
				// Координаты развёртки передаём дальше без изменений
				o.uv = v.uv;
				return o;
			}
			
			sampler2D _MainTex;

			fixed4 frag (v2f i) : COLOR
			{
				fixed4 col = tex2D(_MainTex, i.uv);
				col = 1 - col;
				return col;
			}
			ENDCG
		}
	}
}

Чуть более сложный пример шейдера:

Shader "Tutorial/LittleMoreComplex3D"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "white" {}
	}
	SubShader
	{
		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"

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

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

			v2f vert (appdata v)
			{
				v2f o;
				// Всё отличие от простейшего 3D шейдера 
				// это изменение положение вершин объекта
				// в направлении нормали.
				v.vertex = v.vertex + v.normal;
				o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
				o.uv = v.uv;
				return o;
			}
			
			sampler2D _MainTex;

			fixed4 frag (v2f i) : COLOR
			{
				fixed4 col = tex2D(_MainTex, i.uv);
				col = 1 - col;
				return col;
			}
			ENDCG
		}
	}
}

Реакция на клик мышью

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

Shader "Tutorial/ClickWave"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "white" {}
		_ClickPos("Click position",Vector) = (0,0,0,1.0)
		_WaveSpread("Wave spread", Range(0,11)) = 0.0
	}
	SubShader
	{
		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"

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

			struct v2f
			{
				float2 uv : TEXCOORD0;
				float4 vertex : POSITION;
			};
			float3 _ClickPos;
			float _WaveSpread;

			v2f vert (appdata v)
			{
				v2f o;
				
				float dist = distance(_ClickPos, mul(_Object2World, v.vertex));
				float a = step(dist-0.8, _WaveSpread);
				float b = step(_WaveSpread, dist+0.8);

				v.vertex = lerp(v.vertex, v.vertex + v.normal / 6, min(a,b)) ;
				o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
				o.uv = v.uv;
				return o;
			}
			
			sampler2D _MainTex;

			fixed4 frag (v2f i) : COLOR
			{
				fixed4 col = tex2D(_MainTex, i.uv);
				return col;
			}
			ENDCG
		}
	}
}

Принцип работы этого шейдера в том, что он каждую вершину, которая находится на определённом расстоянии от точки клика, сдвигает в направлении нормали. Расстояние определяется извне шейдера, устанавливается в C# путём SetFloat.

distance(_ClickPos, mul(_Object2World, v.vertex));

эта функция находит расстояние между точками в пространстве, но важно чтобы обе точки были в одной системе координат, а нам даны координаты вертекса в локальном пространстве модели. Чтобы это исправить мы умножаем матрицу _Object2World на локальные координаты и получаем мировые координаты. Сама матрица _Object2World создана unity без нашего участия, нам остаётся только пользоваться.

mul(_Object2World, v.vertex)

Следующие строки:

float a = step(dist-0.8, _WaveSpread);
float b = step(_WaveSpread, dist+0.8);

созданы чтобы определить ширину волны на поверхности, магическое число 0.8 умноженное на 2 и есть ширина волны.

min(a,b)

a и b, при таком подходе, одновременно будут равны 1 только тогда, когда расстояние от точки клика до вершины больше чем dist-0.8, но меньше чем dist+0.8. Чем мы и пользуемся чтобы интерполировать(а на самом деле выбрать) между v.vertex и v.vertex + v.normal, где v.vertex это обычные координаты вершины, а v.vertex + v.normal это сдвинутые в сторону нормали. Более красивого перехода можно было бы добиться при использовании не step, результатом которой может быть только 1 или 0, а smoothstep, который даёт не только 0 или 1, но и промежуточные значения.

float a = smoothstep(dist - 1.5,dist - 0.8, _WaveSpread);
float b = smoothstep(_WaveSpread - 1.5,_WaveSpread - 0.8, dist);

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

И текст C# класса, который надо прикрепить к объекту, по которому кликают:

using UnityEngine;
using System.Collections;

public class SphereClick : MonoBehaviour {

	public Material waveMaterial;
	private float waveSpread = 0;
	
	void Update () {
		if (Input.GetMouseButtonDown(0))
		{
			Vector2 clickPos = Input.mousePosition;
			Ray ray = Camera.main.ScreenPointToRay(clickPos);
			RaycastHit hit;
			if (Physics.Raycast(ray, out hit))
			{
				Debug.Log("Found an object - distance: " + hit.distance);
				waveMaterial.SetVector("_ClickPos", hit.point);
				waveSpread = 0;
			}
		}

		waveMaterial.SetFloat("_WaveSpread", waveSpread);
		waveSpread += 0.1f;
	}
}

Передача данных между вертексным и фрагментным шейдерами

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

Shader "Tutorial/ClickWaveTexture"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "white" {}
        // Заводим вторую текстуру, которая будет проявляться
		_SecondTex("Texture", 2D) = "white" {}
		_ClickPos("Click position",Vector) = (0,0,0,1.0)
		_WaveSpread("Wave spread", Range(0,11)) = 0.0
	}
	SubShader
	{
		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"

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

			struct v2f
			{
				float2 uv : TEXCOORD0;
				float4 vertex : POSITION;
                // Добавляем в выходную структуру вертексного
                // шейдера(по совместительству входную фрагментного)
                // переменную для хранения степени проявления 
                // второй текстуры. Важно помнить, что любой 
                // переменной внутри структуры нужно семантическое
                // значение.
				float appear : COLOR;
			};
			float3 _ClickPos;
			float _WaveSpread;

			v2f vert (appdata v)
			{
				v2f o;
				
				float dist = distance(_ClickPos, mul(_Object2World, v.vertex));
				float a = smoothstep(dist - 1.5,dist - 0.8, _WaveSpread);
				float b = smoothstep(_WaveSpread - 1.5,_WaveSpread - 0.8, dist);
				v.vertex = lerp(v.vertex, v.vertex + v.normal / 6, min(a,b)) ;

				o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
				o.uv = v.uv;
                // Запоминаем значение
				o.appear = b;
				return o;
			}
			
			sampler2D _MainTex;
			sampler2D _SecondTex;

			fixed4 frag (v2f i) : COLOR
			{
				fixed4 col = tex2D(_MainTex, i.uv);
				fixed4 col2 = tex2D(_SecondTex, i.uv);
                // И используем это значение во фрагментном шейдере
				col = lerp(col, col2, i.appear);
				return col;
			}
			ENDCG
		}
	}
}

Ещё понадобится сделать небольшой трюк в C#:

using UnityEngine;
using System.Collections;

public class SphereClickTexture : MonoBehaviour {

	public Material waveMaterial;
	private float waveSpread = 0;
	
	void Update () {
		if (Input.GetMouseButtonDown(0))
		{
			Vector2 clickPos = Input.mousePosition;
			Ray ray = Camera.main.ScreenPointToRay(clickPos);
			RaycastHit hit;
			if (Physics.Raycast(ray, out hit))
			{
				Debug.Log("Found an object - distance: " + hit.distance);
				waveMaterial.SetVector("_ClickPos", hit.point);
				Texture tex = waveMaterial.GetTexture("_MainTex");
				Texture tex2 = waveMaterial.GetTexture("_SecondTex");
				waveMaterial.SetTexture("_MainTex", tex2);
				waveMaterial.SetTexture("_SecondTex", tex);
				waveSpread = 0;
			}
		}

		waveMaterial.SetFloat("_WaveSpread", waveSpread);
		waveSpread += 0.2f;
	}
}

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

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

Last updated