17 мая 2012 в 12:46

Unity3D 3.х Terrain Bump Specular Shader из песочницы

На данный момент 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+ операций)

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

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


Скачать шейдер
Alexandr @agasper
карма
26,0
рейтинг 0,0
Похожие публикации
Самое читаемое Разработка

Комментарии (24)

  • 0
    Когда у меня пару лет назад возникла похожая задача — изменить шейдер ландшафта, жутко мешала необходимость заменять стандартные шейдеры в Hidden/TerrainEngine/..., т.е. то, что шейдер нельзя заменить динамически (на одной сцене мой, на другой — стандартный). По сути, это какой-то недокументированный хак. Это по прежнему так и работает или в последних версиях Unity что-то изменили?
    • 0
      Шейдер можно заменить только полностью для всего проекта, динамически менять его для каждой сцены по прежнему нельзя.
      • 0
        А как насчёт пройтись в скрипте по нужным объектам и поправить материалы?
        • 0
          Ландшафту в юнити нельзя менять материал
  • 0
    А почему не сделать так?
    1) Сливаем смешанные диффузы в рендер таргет
    2) Сливаем смешанные нормалы в другой рендер таргет
    3) Рисуем ландшафт с экранным фетчем диффуз и нормал рендер таргета
    • 0
      Не совсем понимаю о чем вы
      • 0
        1) делаем 2 рендер таргета покрывающих экран
        2) 1 проход — мешаем только диффуз текстуры и записываем в рендер таргет 1
        3) 2 проход — мешаем только нормал текстуры и записываем в рендер таргет 2
        4) 3 проход — делаем финальный проход где диффуз и нормал цвета фетчим из этих текстур.

    • 0
      Вы не забывайте, где всё это происходит. В своём рендере такое сделать не проблема, а вот от Unity я бы такой свободы не ожидал.
  • 0
    А в чем преимущество?
    • 0
      промазал
      • 0
        Работает для SM 2.0 и с Flash тоже не будет проблем
        • 0
          Если будет время попробую сделать так.
  • 0
    Несколько вопросов/замечаний:
    o.Normal += lerp(fixed3(0.5,0.5,1), n2, clamp(splat_control.b + 0.3,0,1));

    А нормализовывать её мы не будем? Если UnpackNormal — функция Unity, то она почти наверняка возвращает единичную нормаль, а значит и «o.Normal» от нас ждут единичной.

    // A general tangent estimation
    fixed3 T1 = float3(1, 0, 1);
    fixed3 Bi = cross(T1, v.normal);
    fixed3 newTangent = normalize(cross(v.normal, Bi));

    В каком пространстве всё это считается: объектном или мировом?
    Почему в качестве опорного тангенса выбран (1,0,1)? По идее, раз Unity этой информации не предоставляет, то Вам нужна какая-то договорённость с дизайнером. Скажем, U направлена по мировой/локальной оси X, или что-то в этом духе. Эта договорённость и определяет опорный тангенс. Не верю, что вы договорились до (1,0,1) ну никак:)

    Альтернативно, Вы можете вывести тангенс и битангенс из pixel derivatives, если Unity позволит Вам:
    1. Подставить тангенс уже в пиксельном шейдере (чего уж там, можно и UnpackNormal самому переписать).
    2. Использовать инструкции dFdx, dFdy (в терминологии GL). Уверен, что не все устройста, на которых работает Unity, эти инструкции поддерживают.

    В общем, получается, что нормали произвольной длины и в неконтролируемо-повёрнутом пространстве. Разве что для шума годятся, не более…
    • 0
      Насколько я знаю далее оно в недрах ShaderLab нормализуется.
      На всякий случай сейчас добавлю нормализацию в коде.
      Функция расчета тангенса взята из другого шейдера ландшафта и прекрасно работает
      Договориться с дизайнером о чем? Мех в данном случае строит юнити.
      В общем, получается, что нормали произвольной длины и в неконтролируемо-повёрнутом пространстве. Разве что для шума годятся, не более…

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

        Ваша «нулевая нормаль» мало того что не «нулевая», так даже и не единичная :) Должно быть (0,0,1). И впредь, если копируете чужой код — ссылайтесь на него в комментариях к коду, а то все будут думать, что это вы свой код не понимаете.

        Насколько я знаю далее оно в недрах ShaderLab нормализуется.

        Это возможно, но не имело бы смысла, ибо вредит производительности. UnpackNormal точно возвращает единичную нормаль (в пространстве TBN), так что тратить целую инструкцию нормализации «на всякий случай» никто не будет.

        Функция расчета тангенса взята из другого шейдера ландшафта и прекрасно работает

        Ага, и следующий, кто скопирует Ваш код, тоже будет так писать. Тяп-ляп, copy-paste, и вуаля — работает! Это не подход в 3D-графике, и уж тем более не для тех, кто выставляет свой код на суд общественности.
        Я вижу, что тангенс неверный, и даже могу объяснить выбор (1,0,1) вместо, скажем (1,0,0). Просто при (1,0,0) высока вероятность совпасть с нормалью, на чём код успешно рухнет (или произведёт на свет NaN и Inf значения). Естественно, (1,0,1) от этого тоже ничего не защитит, просто вероятность меньше :) Это то же самое, что объяснять, почему самоубийцы прыгают с крыши высоких домов, а не низких: логика есть, но в целом подход неверный (лучше совсем не самоубиваться).

        Договориться с дизайнером о чем? Мех в данном случае строит юнити.

        Значит нужно понять, куда у меха Unity направлена U координата.

        Зачем писать про шум если вы не знакомы с юнити и тем более не видели шейдер в работе?

        То есть по Вашему, раз песочек сверкает при повороте камеры, то шейдер работает? Я делаю суждения из кода и скринов, которые Вы предоставили. Ваша карта нормалей — классический шум нормалей (песок), так что, конечно, на её ориентацию можно забить. Вот если бы показали что-либо вменяемое — там и посмотрели бы.
        • 0
          Я бы не стал выкладывать шейдер не протестировав его. Тестил на нормалях скал, досок и все было замечательно. К сожалению сейчас под рукой нет юнити, чтобы сделать скрины.
          Расчет тангенсов взят из другого проверенного шейдера с офф. форума, которым я долго пользовался до этого (ссылка на него в начале поста).
          Значит нужно понять, куда у меха Unity направлена U координата.

          По идее U всегда направлена по X.
          • 0
            Расчет тангенсов взят из другого проверенного шейдера с офф. форума, которым я долго пользовался до этого (ссылка на него в начале поста).

            Не вижу я там официального форума, а виже блог некого «sixtimesnothing».

            По идее U всегда направлена по X.

            А значит и тангенс будет ...? (1,0,1)?
            Садитесь, незачёт.

            Пожалуй, слишком я Вас сильно критикую… В интернете практически нет абсолютно верных реализации normal/parallax mapping с указанием ограничений и подробным описанием логики. А у Вас всё-таки первый пост… И главное — чтобы смотрелось красиво, так ведь? Понимаю, но Вы всё же задумайтесь над моими замечаниями, если хотите понять, как Ваш код (не-) работает.
            • 0
              Не вижу я там официального форума, а виже блог некого «sixtimesnothing».

              Вот его пост на офф. форуме

              Я выше написал что алгоритм расчета тангенсов не мой, более того как вы видите я не знаю как его реализовать по-другому в рамках данного шейдера без потери работоспособности.
              Примеров реализации расчета тангенсов в коде очень много, но в шейдере ни одного. Более того в паре статей мелькало утверждение, что в шейдере достоверно посчитать тангенс невозможно.
              С учетом вышесказанного и того что шейдер прекрасно работает, вы могли бы предложить лучшую реализацию функции расчета тангенса вместо критики.
              • 0
                Вот его пост на офф. форуме

                Да, форум Unity, но это не подтверждает правильность кода. Автор же не разработчик Unity, а такой же как мы с вами.

                Примеров реализации расчета тангенсов в коде очень много, но в шейдере ни одного.

                Обратите внимание на этот вопрос (и мой ответ к нему).

                Собственно, вычислить tangent из вершинного шейдера нельзя. Так что Ваша задача не вычислить, а воспроизвести его из той самой «договорённости», минуя вершинные атрибуты. Из пиксельного — можно, но с потерей некоторой доли гладкости вдоль рёбер (смотрите алгоритм по ссылке выше). Правильно, конечно, генерировать тангенсы во время экспорта.

                Что посоветовать? Давайте хотя бы использовать правильный тангенс:
                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);
                ...

                • 0
                  В ближайшее время разберусь с этим вопросом и поправлю процедуру. Спасибо за пример.
  • 0
    Давно я уже не копался в Unity3D, еще со второй версии, а все еще в ней сложно сделать вменяемые вещи. Ну вот кто в наше время делает ландшафт текстурным сплаттингом в несколько проходов? Бъем сетку чаще, коэффициенты смешивания в вершинных атрибутах — можем мешать за проход гораздо больше 4 текстур, да еще и в реальном времени можно этот сплаттинг перестраивать.

    И небольшое замечание по поводу названия темы: у вас написано Terrain Bump Specular Shader, но в статье вы используете normalmap, это все же несколько разные техники, хоть и похожие.
    • 0
      В Юнити все шейдера с нормал мапой называются с приставкой Bumped, поэтому чтобы он не выделялся среди остальных, я назвал его по той же схеме.
  • 0
    Да и честно говоря, terrain в юнити — еще та тормозячка, недавно под андроид пытались его оптимизировать — никак. Низкий фпс и все. Пришлось через модели делать.
    • 0
      Уменьшение разрешения тоже не помогло?

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.