Pull to refresh

Unity3D 3.х Terrain Bump Specular Shader

Reading time4 min
Views16K
На данный момент Unity3D не поддерживает наложение на встроенный ландшафт карты нормалей и отражения(specular). Гугление по этому поводу принесло не очень впечатляющие результаты в виде вот этого шейдера и некоторых его модификаций. Воодушевившись картинкой и скачав архив меня постигло разочарование. Во-первых для работы шейдера на ландшафт необходимо вешать скрипт которым управляется шейдер (что очень неудобно), а во-вторых в данной реализации больше 4х карт нормалей нельзя назначить.
В этой статье я опишу процесс создания собственного шейдера для ландшафта, параллельно рассказав как работает стандартный шейдер.

Механизм отрисовки ландшафта в стандартных шейдерах юнити


Для отрисовки ландшафта в юнити используется два шейдера:
Hidden/TerrainEngine/Splatmap/Lightmap-FirstPass и Hidden/TerrainEngine/Splatmap/Lightmap-AddPass, скачать их можно здесь

Первый шейдер рисует первые 4 текстуры ландшафта. Второй шейдер рисует последовально остальные текстуры по 4 за раз, пока не закончатся текстуры.

Итак что передается в шейдер из движка:

	...
	struct Input {
		float2 uv_Control : TEXCOORD0;
		float2 uv_Splat0 : TEXCOORD1;
		float2 uv_Splat1 : TEXCOORD2;
		float2 uv_Splat2 : TEXCOORD3;
		float2 uv_Splat3 : TEXCOORD4;
	};

	sampler2D _Control;
	sampler2D _Splat0,_Splat1,_Splat2,_Splat3;
	...
	


_SplatX — текстура с материалом
_Control — управляющая карта. Это текстура в которой каждый из каналов задает яркость одного из материалов в определенной точке. Управляющая карта создается на основе карты материалов ландшафта (Alphamaps) для каждой четверки материалов в недрах движка. Именно потому что у управляющей текстуры 4 канала, шейдеры рендерят не больше чем по 4 материала за раз.




Разберем что дальше происходит в шейдере:

Он имеет единственную процедуру, в которой считается цвет текущей точки (o.Albedo), и он равен сумме произведений яркости точки из управляющей карты RGBA и ее цвета из текстуры материала.

	...
	void surf (Input IN, inout SurfaceOutput o) {
		half4 splat_control = tex2D (_Control, IN.uv_Control);
		half3 col;
		col  = splat_control.r * tex2D (_Splat0, IN.uv_Splat0).rgb;
		col += splat_control.g * tex2D (_Splat1, IN.uv_Splat1).rgb;
		col += splat_control.b * tex2D (_Splat2, IN.uv_Splat2).rgb;
		col += splat_control.a * tex2D (_Splat3, IN.uv_Splat3).rgb;
		o.Albedo = col;
		o.Alpha = 0.0;
	}
	...
	


Результат его работы можно увидеть ниже:



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

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

Создание собственного шейдера

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



Новая процедура surf:

void surf (Input IN, inout SurfaceOutput o) {
	fixed4 splat_control = tex2D (_Control, IN.uv_Control);
	fixed3 col;
	fixed spec;
	
//Получим RGBA точки из первого материала
	fixed4 d1 = tex2D (_Splat0, IN.uv_Splat0);
//Получим RGBA точки из второго материала
	fixed4 d2 = tex2D (_Splat2, IN.uv_Splat2);

//Получим нормаль точки от первого материала
	fixed3 n1 = UnpackNormal( tex2D (_Splat1, IN.uv_Splat1) );
//Получим нормаль точки от второго материала
	fixed3 n2 = UnpackNormal( tex2D (_Splat3, IN.uv_Splat3) );
	
//Меняем цвет точки в соответствии с управляющей картой
	col = splat_control.r * d1.rgb;
//Интерполируем нормаль (ниже описано подробней)
	o.Normal =  normalize(lerp(fixed3(0.5,0.5,1), n1, clamp(splat_control.r + 0.3,0,1)));
//Меняем степень отблеска в обратно пропорционально альфа каналу материала, чтобы текстура без альфы не бликовала. "0.1" - максимальная степень отблеска, поменяйте на свой вкус.
	spec = (1 - d1.a) * splat_control.r * 0.1;
	
//Повторяем для второго материала и складываем с первым
	col += splat_control.b * d2.rgb;
	o.Normal += normalize(lerp(fixed3(0.5,0.5,1), n2, clamp(splat_control.b + 0.3,0,1)));
	spec += (1 - d2.a) * splat_control.b * 0.1;

//Немного убавляем яркость, чтобы соответствовало basemap'у
	o.Albedo = col * 0.5;
//Задаем отблеск
	o.Specular = spec;
	o.Gloss = spec;
	o.Alpha = 0.0;
}


В процедуре все должно быть понятно, так как кроме арифметических операций там больше почти ничего нет. Единственное, что хотелось бы разобрать — вот эту строчку:
o.Normal = lerp(fixed3(0.5,0.5,1), n1, clamp(splat_control.r + 0.3,0,1));
Нормаль — это единичный вектор перпендикулярный к поверхности. И так как нам нужно ее плавно уменьшать, мы не можем просто умножать ее на какой-то коэффициент. Для решения этой задачи я интерполирую текущую нормаль в т.н. «нулевую нормаль», при которой на текстуре не будет никакого рельефа.

Чтобы карта нормалей могла примениться, на мехе должны быть посчитанные тангенсы (вектор перпендикулярный нормали и параллельный поверхности, направленный в сторону увеличения координаты U на развертке). Обычно их считает ПО, в котором разрабатывается модель, но так как ландшафт строится в юнити «на лету», то тангенсов там нет.
Придется посчитать тангенсы внутри шейдера самим:

void vert (inout appdata_full v) {
	fixed3 T1 = float3(1, 0, 0);
	if (dot(T1,v.normal) > 0.99) {
		T1 = float3(0,1,0); //workaround
	}
	fixed3 Bi = cross(T1, v.normal);
	fixed3 newTangent = normalize(cross(v.normal, Bi));
	v.tangent.xyz = newTangent.xyz;
	if (dot(cross(v.normal,newTangent),Bi) < 0)
		v.tangent.w = -1.0f;
	else
		v.tangent.w = 1.0f;
}


Шейдер для последующих проходов практически идентичен.

Для тех кто создает ландшафт динамически, не забудьте исправить индексы материалов в коде. Они должны быть умножены на два, т.к. по нечётным индексам лежат нормали.

materials[x, z, material_number*2] = 1;

Минусы данного метода:
  • Незначительно повышается Draw Calls из-за большего количества проходов шейдера
  • Надо следить за тем чтобы не нарисовать на ландшафте неправильным материалом (нормалью)
  • Использует более 8 регистров, что делает невозможным компиляцию под Flash
  • Использует модель Shader 3.0, что делает невозможной работу на старом железе (из-за 64+ операций)

Кратко, для тех кому лень читать:
  • Шейдер рисует ландшафт с картами нормалей и спекулара
  • Текстуры на ландшафте должны чередоваться через одну. Диффузка / Нормаль / Диффузка / Нормаль и т. д.
  • Спекулар карта хранится в альфа канале диффузки

Результат со стандартной текстурой, и картами снятыми с нее (стало / было):


Скачать шейдер
Tags:
Hubs:
Total votes 22: ↑19 and ↓3+16
Comments24

Articles