Вместо предисловия
Мы с LLlypuK'ом в очередной раз затеяли захват мира долгостроящийся проект, но на этот раз мы не гарантируем его выполнение... и это скорее даже не сама цель... Целью можно назвать изучение такой среды как Unity3d, а профит в получении некоторых навыков. Если получится из этого еще и игру сделать, то мы не расстроимся, а даже наоборот. Если кому интересно, проект имеет рабочее название Voluntarium, но это все, что мы пока о нем можем сказать.
О чем же я буду говорить? О том, какие интересные (сугубо на мой личный взгляд) решения мы нашли, чем можем поделиться и так далее. Ну что-ж.... первый блин -поехали.
Медленно к сути
Задался я целью соорудить на базе стандартного редактора ландшафта некий шейдер, который изменил бы внешний вид игры в лучшую сторону, так как стандартный весьма уныл... все чего мы добились я показывать не стану, но об одном аспекте рассказать стоит. Те кто пользовался стандартным ландшафтом заметили, что при рисовании невозможно изменить размер рисуемых текстур... с этого и начнем.
Нам понадобятся
http://xgm.guru/files/192/123998/Ashen_Grass_small.tga
http://xgm.guru/files/192/123998/Ashen_Vines_small.tga
http://xgm.guru/files/192/123998/Ashen_Rock_small.tga
Можно скачать отсюда
Или же взять здесь... нам понадобятся эти шейдеры:
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" }
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 }
К делу
- Для начала создадим 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):
http://xgm.guru/files/192/123998/Ashen_Grass.tga
http://xgm.guru/files/192/123998/Ashen_Rock.tga
http://xgm.guru/files/192/123998/Ashen_Vines.tga
И изменить параметр отвечающий за количество текстур с 2, до 4... у меня получилось что-то такое:
Вместо послесловия
Собственно жду отзывов, как и от тех, кто крут в этом (а ведь я только учусь, не претендую на лавры), так и тех, кто только хочет заняться изучением unity3d и шейдеров в том числе. Будет интересны ваши отзывы, предложения и вопросы.
Смотрите также:
Комментарии
триггеры варкрафтовские на юнити создают, терраин тоже, пора уже делать модели и графику ) и будет Вар 4 )
триггеры варкрафтовские на юнити создают, терраин тоже, пора уже делать модели и графику ) и будет Вар 4 )
Есть такое выражение: можно вывезти бабу из деревни, но деревню из бабы вывезти нельзя. Вот по аналогии и конечно же в хорошем смысле)
Давненько тебя здесь не было видно, Андреич..)
У этого подхода есть крохотный недостаток - нет возможности принудительно сменить вариацию тайла в том или ином фрагменте карты, если случайная генерация выдала не очень приятный результат, плюс я не уверен что генератор случайных чисел выдаст одинаковые значения между запусками.
Как по мне, то наложение процедурных искажений на текстуру более перспективно в плане украшательства картинки, чем возня с вариациями тайлов.
prog, генератор выдаст одно и то же. Посмотри внимательно на функцию. Увидишь там хоть что-то вариативное от запуска к запуску... ну будешь большим молодцом. Что касается возможности принудительно сменить вариацию тайла... ну да, есть такое. Но как бы цель была сделать стандартный террайн чуть более красивым... если речь идет о такого рода модификациях, то скорее всего придется писать свой террайн энжин. Тут же есть ряд ограничений, хотя бы связанных с тем, что для каждого прохода шейдера все текстуры устанавливаются программно + шейдер как таковой не умеет сохранять данные на диск и брать их оттуда =)
Не, ну просто не вежливо такие hi-res картинки вылаживать. Точнее претензия не к разрешению, но к размеру. Сделайте в jpeg, это ведь не сложно. //Или ужать, или под кат(я хз, подгружает ли он сейчас аяксом своё содержимое, но если да, то use it).
Я конечно понимаю, что тут все миллионеры и живут в столицах, где дешевый быстрый безлимитный интернет, но...
Mihahail, я извиняюсь за свою привычку работать с пнг файлами. В интернет что-то не выкладывал уже давно, а для моих нужд формат более чем удобный, вот и привык =). Теперь там jpg, но под каты прятать не стал.
Перенес статью в Юнити.
CollectableItemData.cs
[CreateMenuItem(fileName = "newItem", menuName = "Data/Items/Collectable", order = 51]