На данный момент Unity3D не поддерживает наложение на встроенный ландшафт карты нормалей и отражения(specular). Гугление по этому поводу принесло не очень впечатляющие результаты в виде вот этого шейдера и некоторых его модификаций. Воодушевившись картинкой и скачав архив меня постигло разочарование. Во-первых для работы шейдера на ландшафт необходимо вешать скрипт которым управляется шейдер (что очень неудобно), а во-вторых в данной реализации больше 4х карт нормалей нельзя назначить.
В этой статье я опишу процесс создания собственного шейдера для ландшафта, параллельно рассказав как работает стандартный шейдер.
Для отрисовки ландшафта в юнити используется два шейдера:
Hidden/TerrainEngine/Splatmap/Lightmap-FirstPass и Hidden/TerrainEngine/Splatmap/Lightmap-AddPass, скачать их можно здесь
Первый шейдер рисует первые 4 текстуры ландшафта. Второй шейдер рисует последовально остальные текстуры по 4 за раз, пока не закончатся текстуры.
Итак что передается в шейдер из движка:
_SplatX — текстура с материалом
_Control — управляющая карта. Это текстура в которой каждый из каналов задает яркость одного из материалов в определенной точке. Управляющая карта создается на основе карты материалов ландшафта (Alphamaps) для каждой четверки материалов в недрах движка. Именно потому что у управляющей текстуры 4 канала, шейдеры рендерят не больше чем по 4 материала за раз.
Разберем что дальше происходит в шейдере:
Он имеет единственную процедуру, в которой считается цвет текущей точки (o.Albedo), и он равен сумме произведений яркости точки из управляющей карты RGBA и ее цвета из текстуры материала.
Результат его работы можно увидеть ниже:
Засветка в данном случае произошла из-за наложения нескольких материалов(каналов) на управляющей карте, в реальных условия такого не должно быть, т.к. обычно один материал на ландшафте плавно переходит в другой, и довольно в редких случаях нужно накладывать один материал на другой.
Второй шейдер я рассматривать не буду, т.к. он практически идентичен, только используется для текстур с индексом выше 3.
Так как в одном проходе может быть только 4 материала и не хочется прибегать к помощи скриптов для назначения нормалей шейдеру, будем засовывать нормали через ландшафт, как показано на картинке ниже.
Теперь каждая вторая текстура на ландшафте является нормалью к предыдущему материалу. Важно чтобы в инспекторе у этой текстуры был выставлен тип — нормаль. Кроме того у нас есть неиспользуемый канал A в текстуре материала, в который отлично помещается карта Specular.
Новая процедура surf:
В процедуре все должно быть понятно, так как кроме арифметических операций там больше почти ничего нет. Единственное, что хотелось бы разобрать — вот эту строчку:
Нормаль — это единичный вектор перпендикулярный к поверхности. И так как нам нужно ее плавно уменьшать, мы не можем просто умножать ее на какой-то коэффициент. Для решения этой задачи я интерполирую текущую нормаль в т.н. «нулевую нормаль», при которой на текстуре не будет никакого рельефа.
Чтобы карта нормалей могла примениться, на мехе должны быть посчитанные тангенсы (вектор перпендикулярный нормали и параллельный поверхности, направленный в сторону увеличения координаты U на развертке). Обычно их считает ПО, в котором разрабатывается модель, но так как ландшафт строится в юнити «на лету», то тангенсов там нет.
Придется посчитать тангенсы внутри шейдера самим:
Шейдер для последующих проходов практически идентичен.
Для тех кто создает ландшафт динамически, не забудьте исправить индексы материалов в коде. Они должны быть умножены на два, т.к. по нечётным индексам лежат нормали.
Минусы данного метода:
Кратко, для тех кому лень читать:
Результат со стандартной текстурой, и картами снятыми с нее (стало / было):
Скачать шейдер
В этой статье я опишу процесс создания собственного шейдера для ландшафта, параллельно рассказав как работает стандартный шейдер.
Механизм отрисовки ландшафта в стандартных шейдерах юнити
Для отрисовки ландшафта в юнити используется два шейдера:
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+ операций)
Кратко, для тех кому лень читать:
- Шейдер рисует ландшафт с картами нормалей и спекулара
- Текстуры на ландшафте должны чередоваться через одну. Диффузка / Нормаль / Диффузка / Нормаль и т. д.
- Спекулар карта хранится в альфа канале диффузки
Результат со стандартной текстурой, и картами снятыми с нее (стало / было):
Скачать шейдер