Как мы шейдер писали

» опубликован

Вместо предисловия

Мы с LLlypuK'ом в очередной раз затеяли захват мира долгостроящийся проект, но на этот раз мы не гарантируем его выполнение... и это скорее даже не сама цель... Целью можно назвать изучение такой среды как Unity3d, а профит в получении некоторых навыков. Если получится из этого еще и игру сделать, то мы не расстроимся, а даже наоборот. Если кому интересно, проект имеет рабочее название Voluntarium, но это все, что мы пока о нем можем сказать.
О чем же я буду говорить? О том, какие интересные (сугубо на мой личный взгляд) решения мы нашли, чем можем поделиться и так далее. Ну что-ж.... первый блин -поехали.

Медленно к сути

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

Нам понадобятся

» Текстуры
я вежливо позаимствовал их у старичка WC3... и слегка подправил
» Исходники шейдеров
Можно скачать отсюда
Или же взять здесь... нам понадобятся эти шейдеры:
» DefaultResourcesExtra\TerrainShaders\Splats\FirstPass.shader
Shader "Nature/Terrain/Diffuse" {
Properties {
	[HideInInspector] _Control ("Control (RGBA)", 2D) = "red" {}
	[HideInInspector] _Splat3 ("Layer 3 (A)", 2D) = "white" {}
	[HideInInspector] _Splat2 ("Layer 2 (B)", 2D) = "white" {}
	[HideInInspector] _Splat1 ("Layer 1 (G)", 2D) = "white" {}
	[HideInInspector] _Splat0 ("Layer 0 (R)", 2D) = "white" {}
	// used in fallback on old cards & base map
	[HideInInspector] _MainTex ("BaseMap (RGB)", 2D) = "white" {}
	[HideInInspector] _Color ("Main Color", Color) = (1,1,1,1)
}
	
SubShader {
	Tags {
		"SplatCount" = "4"
		"Queue" = "Geometry-100"
		"RenderType" = "Opaque"
	}
CGPROGRAM
#pragma surface surf Lambert
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;

void surf (Input IN, inout SurfaceOutput o) {
	fixed4 splat_control = tex2D (_Control, IN.uv_Control);
	fixed3 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;
}
ENDCG  
}

Dependency "AddPassShader" = "Hidden/TerrainEngine/Splatmap/Lightmap-AddPass"
Dependency "BaseMapShader" = "Diffuse"
Dependency "Details0"      = "Hidden/TerrainEngine/Details/Vertexlit"
Dependency "Details1"      = "Hidden/TerrainEngine/Details/WavingDoublePass"
Dependency "Details2"      = "Hidden/TerrainEngine/Details/BillboardWavingDoublePass"
Dependency "Tree0"         = "Hidden/TerrainEngine/BillboardTree"

// Fallback to Diffuse
Fallback "Diffuse"
}
» DefaultResourcesExtra\TerrainShaders\Splats\AddPass.shader
Shader "Hidden/TerrainEngine/Splatmap/Lightmap-AddPass" {
Properties {
	_Control ("Control (RGBA)", 2D) = "black" {}
	_Splat3 ("Layer 3 (A)", 2D) = "white" {}
	_Splat2 ("Layer 2 (B)", 2D) = "white" {}
	_Splat1 ("Layer 1 (G)", 2D) = "white" {}
	_Splat0 ("Layer 0 (R)", 2D) = "white" {}
}
	
SubShader {
	Tags {
		"SplatCount" = "4"
		"Queue" = "Geometry-99"
		"IgnoreProjector"="True"
		"RenderType" = "Opaque"
	}
	
CGPROGRAM
#pragma surface surf Lambert decal:add
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;

void surf (Input IN, inout SurfaceOutput o) {
	fixed4 splat_control = tex2D (_Control, IN.uv_Control);
	fixed3 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;
}
ENDCG  
}

Fallback off
}
Ну и базовое понимание, что такое шейдеры, UV координаты, и как это все работает в unity3d.

К делу

  1. Для начала создадим Terrain в Unity3d. Добавим в него три наши текстуры... и что нибудь ими нарисуем. У меня лично получилось что-то подобное:
Да... не очень красиво, но нам лишь для понимания сути.
Начнем с создания своего материала и двух шейдеров с тем кодом, который приведен выше.
Внесем в первый шейдер правки:
//заменим строчку
Shader "Nature/Terrain/Diffuse"
//на эту
Shader "MyShader/Terrain"
//а эту
Dependency "AddPassShader" = "Hidden/TerrainEngine/Splatmap/Lightmap-AddPass"
//на эту
Dependency "AddPassShader" = "Hidden/Terrain/AddPass"
а во втором шейдере правки будут такими
//заменим строчку
Shader "Hidden/TerrainEngine/Splatmap/Lightmap-AddPass"
//на эту
Shader "Hidden/Terrain/AddPass"
Этим самым мы сменим путь, по которому их будет искать Unity3d в своей библиотеки шейдеров в рамках нашего проекта.
Стоит немного рассказать о роли данных шейдеров. Первый - это основной шейдер, который занимается смешиванием текстур на основе управляющей текстуры, а точнее он занимается смешиванием первых 4х текстур, для всех последующих четверок вызывается второй шейдер. Для этого в первом и создается зависимость:
Dependency "AddPassShader" = "Hidden/Terrain/AddPass"
Как работает сам шейдер? Очень просто. Когда вы водите кистью по ландшафту, внутренний скрипт рисует в одном из каналов так называемой управляющей текстуры (в шейдере это текстура _Control). То есть красный канал - степень влияния первой текстуры, зеленый - второй, синий - третьей и альфа канал - степень влияния на итог текстуру четвертой текстуры. Для последующих текстур, создаются дополнительный управляющие текстуры (по одной на каждую дополнительную четверку). В самом шейдере просто перемножаются текстуры с их степенью влияния, складываются и выводится результат. Так как скрипт отслеживает редактирования всех контрольных текстур таким образом, чтоб сумма всех влияний в каждой точке не превышала 1, то у нас на экране не мешанина из пикселей, а вполне вменяемая картинка. Каждый проход шейдера накладывается на предыдущие аддитивным блендингом. За что отвечает вот эта строчка во втором шейдере:
#pragma surface surf Lambert decal:add //а точнее даже вот эта ее часть: decal:add
Назначьте вашему материалу шейдер MyShader -> Terrain, затем поменяйте у ландшафта стандартный материал, на ваш:

Начнем небольшие правки

Все правки вноситься будут в оба файла в аналогичные разделы, потому я не буду лишний раз писать, что изменения надо дублировать. Для начала развяжем себе руки и впишем после
#pragma surface surf Lambert
вот это
#pragma target 3.0
Этим самым мы указали, что наши шейдеры будут использовать третью модель шейдеров. Тем самым отсекли очень старые видеокарточки, но увеличили количество доступных в шейдере вычислений... так или иначе в конце нам это понадобится.
Затем в раздел Properties добавим следующее:
_Scale ("Texture Scale", Float) = 1
а после
sampler2D _Splat0,_Splat1,_Splat2,_Splat3;
добавим
float _Scale;
это будет наш масштаб. Его мы можем увидеть в свойствах нашего материала:
но его изменения нам пока ничего не дадут. Для того, чтоб он заработал, давайте изменим UV координаты наших текстур. Для этого заменим функцию
void surf (Input IN, inout SurfaceOutput o) {
	fixed4 splat_control = tex2D (_Control, IN.uv_Control);
	fixed3 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;
}
на
void surf (Input IN, inout SurfaceOutput o) {
	float2 realUV0, realUV1, realUV2, realUV3;
	
	realUV0 = IN.uv_Splat0 * _Scale;
	realUV1 = IN.uv_Splat1 * _Scale;
	realUV2 = IN.uv_Splat2 * _Scale;
	realUV3 = IN.uv_Splat3 * _Scale;
	
	fixed4 splat_control = tex2D (_Control, IN.uv_Control);
	fixed3 col;
	
	col  = splat_control.r * tex2D (_Splat0, realUV0).rgb;
	col += splat_control.g * tex2D (_Splat1, realUV1).rgb;
	col += splat_control.b * tex2D (_Splat2, realUV2).rgb;
	col += splat_control.a * tex2D (_Splat3, realUV3).rgb;
	o.Albedo = col;

	o.Alpha = 0.0;
}
Сохраним и вернемся в редактор. Если вы все сделали правильно, то при изменении добавленного нами параметра в материале, должны меняться и размеры текстур. Например вот так у меня теперь выглядит ландшафт для параметров 2 и 0.2 соответственно без изменений на самой сцене и неизменном положении камеры.
При большом удалении заметна "регулярность" текстур... что не так приятно... Избавимся от этого!

Следующий шаг

Я на самом деле изначально схитрил, объединив 4 тайла в одну текстуру... теперь моя идея заключается в следующем: порезать каждую текстуру на 4 куска, и вместо самой текстуры выводить рандомно взятый кусок.
Чтоб этого добиться введем параметр
_TileCount ("Texture grid size", Float) = 2.0
и переменную
float _TileCount;
так же как мы это делали со _Scale. Плюс... мы напишем еще две функции:
float2 rand2(float2 n)
{
  float2 result;
  result.x = frac(sin(fmod(dot(n.xy, float2(12.9898, 78.233)),3.14)) * 43758.5453);
  result.y = frac(sin(fmod(dot(n.yx, float2(12.9898, 78.233)),3.14)) * 43758.5453);   
  
  return result;
}

float2 TextOffset(float2 n)
{
	float2 result;
	result = rand2(floor(n));
	result = floor(result * _TileCount)/_TileCount;
	return result;
}
//вставить их надо перед "void surf (Input IN, inout SurfaceOutput o) {"
Введенный нами параметр - это количество тайлов в текстуре, по одной стороне. То есть в нашем случае это 2. Текстура 2 на 2 тайла.
Первая - функция это псевдорандом для шейдеров. Взят из интернета, для наших целей он вполне сгодится. Его минус в том, что при передаче в него одинаковых векторов, мы получаем одинаковые значения... но так как у нас для одних и тех же ячеек в течении всей игры тайл должен быть одним и тем же... нас это вполне устроит. Возвращает эта функция вектор со случайными координатами.
Вторая функция интереснее. В нее мы будем передавать UV координаты самой текстуры. Сам ландшафт отдает их не от 0 до 1, а от 0 до n*1, где n - это во сколько раз сам ландшафт больше нашей текстуры... ну или сколько раз она в него войдет. Переданные UV мы обрезаем до целой части и передаем рандому. Обрезаем мы его для того, чтоб для всего квадрата выдавалось одно и то же значение (например для 2.4 и 2.8 это всегда будет 2). Рандом возвращает значения от 0 до 1. Мы умножаем полученное на количество тайлов по одной стороне текстуры. Тем самым расширяя диапозон до "от 0 до _TileCount". Полученное число мы обрезаем до целой части, получая "случайно" выбранный номер тайла... а затем снова делим его на _TileCount, получая смещение UV координат тайла в текстуре.
Далее нам следует изменить функцию surf, и добавить после
        realUV0 = IN.uv_Splat0 * _Scale;
	realUV1 = IN.uv_Splat1 * _Scale;
	realUV2 = IN.uv_Splat2 * _Scale;
	realUV3 = IN.uv_Splat3 * _Scale;
эти строчки
	realUV0 = (frac(realUV0)/_TileCount + TextOffset(realUV0));
	realUV1 = (frac(realUV1)/_TileCount + TextOffset(realUV1));
	realUV2 = (frac(realUV2)/_TileCount + TextOffset(realUV2));
	realUV3 = (frac(realUV3)/_TileCount + TextOffset(realUV3));
Что мы делаем этим? Во первых мы переходим к честным UV координатам от 0 до 1, вычленяя дробную часть из них функцией frac. Затем мы делим полученное на _TileCount, чтоб получить координаты не от 0 до 1 (то есть всей текстуры), а только одного тайла. Затем делаем поправку на смещение... и получаем нужные нам координаты. Сохраняем шейдеры, идем в редактор и ставим значение нового параметра в 2.
Если все сделали правильно, то должно получится что-то вроде:
Далее можно сделать еще одно. Заменить текстуры на следующие (в них не 4 тайла, а 16):
И изменить параметр отвечающий за количество текстур с 2, до 4... у меня получилось что-то такое:

Вместо послесловия

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

 
unity3d, shader, gamedev

Просмотров: 7 711

» Лучшие комментарии


Mark Mocherad #1 - 6 лет назад 5
триггеры варкрафтовские на юнити создают, терраин тоже, пора уже делать модели и графику ) и будет Вар 4 )
GeneralElConsul #2 - 6 лет назад 4
триггеры варкрафтовские на юнити создают, терраин тоже, пора уже делать модели и графику ) и будет Вар 4 )
Есть такое выражение: можно вывезти бабу из деревни, но деревню из бабы вывезти нельзя. Вот по аналогии и конечно же в хорошем смысле)
MF #3 - 6 лет назад 3
Padalekki, ну текстурки я взял стеба ради, не более =)
Андреич #4 - 6 лет назад 4
Давненько тебя здесь не было видно, Андреич..)
prog #5 - 6 лет назад 2
У этого подхода есть крохотный недостаток - нет возможности принудительно сменить вариацию тайла в том или ином фрагменте карты, если случайная генерация выдала не очень приятный результат, плюс я не уверен что генератор случайных чисел выдаст одинаковые значения между запусками.
Как по мне, то наложение процедурных искажений на текстуру более перспективно в плане украшательства картинки, чем возня с вариациями тайлов.
MF #6 - 6 лет назад 3
prog, генератор выдаст одно и то же. Посмотри внимательно на функцию. Увидишь там хоть что-то вариативное от запуска к запуску... ну будешь большим молодцом. Что касается возможности принудительно сменить вариацию тайла... ну да, есть такое. Но как бы цель была сделать стандартный террайн чуть более красивым... если речь идет о такого рода модификациях, то скорее всего придется писать свой террайн энжин. Тут же есть ряд ограничений, хотя бы связанных с тем, что для каждого прохода шейдера все текстуры устанавливаются программно + шейдер как таковой не умеет сохранять данные на диск и брать их оттуда =)
prog #7 - 6 лет назад 2
MF, да, не очень внимательно посмотрел - перепутал с другой реализацией.
Mihahail #8 - 6 лет назад (отредактировано ) 3
Не, ну просто не вежливо такие hi-res картинки вылаживать. Точнее претензия не к разрешению, но к размеру. Сделайте в jpeg, это ведь не сложно. Или ужать, или под кат(я хз, подгружает ли он сейчас аяксом своё содержимое, но если да, то use it).
Я конечно понимаю, что тут все миллионеры и живут в столицах, где дешевый быстрый безлимитный интернет, но...
MF #9 - 6 лет назад 3
Mihahail, я извиняюсь за свою привычку работать с пнг файлами. В интернет что-то не выкладывал уже давно, а для моих нужд формат более чем удобный, вот и привык =). Теперь там jpg, но под каты прятать не стал.