Не так давно мы опубликовали материал о тестировании Unity Cloth на мобильных устройствах. В другой статье на render.ru рассказали о том, как арт-отделу Plarium Krasnodar удалось сделать модель персонажа с высокой детализацией и при этом не перегрузить проект. Теперь давайте поговорим о работе над шейдером и анимацией для волос этой модели. Перед командой Graphic Development стояли следующие задачи: определить допустимый уровень детализации геометрии; устранить угловатость модели и текстуры; создать реалистичный блик; придать прическе подвижности. Сначала мы решили определить требования к модели.  Особых требований к детализации геометрии выявлено не было (как обычно — чем меньше вершин, тем лучше). Для замера производительности мы брали тестовый незаскиненный исходник и с помощью 3D-пакета с использованием модификаторов Smooth увеличивали детализацию. Детализация   PlayerLoop (ms) mediana   1396 вертексов   33.15   4423 вертексов    33.2   15537 вертексов    33.1   58003 вертексов    33.2                                      Показания взяты с устройства Android Samsung Galaxy Tab S2 С волосами персонажа была очевидная проблема: шейдер вместе с текстурой создавал жесткие концы, а анизотропный блик подчеркивал выступающие друг за другом полигоны волос.  Из этого стали понятны требования к текстуре и модели: полигоны прядей не должны пересекаться там, где виден блик, — так он будет красиво ходить по волосам, не подчеркивая угловатость; верхние слои прядей волос должны быть более детализированными, чем нижние, чтобы создавать общую форму и эффект подвижности. Затем мы принялись за текстуру. Чтобы кончики волос смотрелись лучше, требовалась коррекция альфа-канала.  Здесь выбор стоял между двумя вариантами:  первый — использовать жесткую обрезку cutout-шейдером (т. е. фильтровать альфа-канал по пороговому значению, получая полностью прозрачный или непрозрачный пиксель, без промежуточных областей); второй — использовать мягкий transparent-шейдер, который работает плавнее, но более требователен к железу.  Мягкий шейдер делал всего лишь одну дополнительную операцию — читку буфера кадра (для смешивания текущего пикселя волос с уже имеющейся на экране картинкой), за счет чего и достигался плавный уход в прозрачность. К тому же занимаемая экранная площадь прически нашего персонажа и количество наложений были невелики — так что особой разницы в ресурсозатратности между шейдерами в нашем случае не оказалось. Однако с мягким шейдером волосы смотрелись лучше.  Но тут была одна загвоздка — камера отрисовывала изображение в текстуру. Поэтому для того, чтобы оно корректно обрезалось по альфе и волосы отображались на текстуре, требовался дополнительный проход, который бы эту альфу создавал. В итоге был написан шейдер, содержащий в себе три прохода. Первый записывает правильный силуэт прически в альфу текстуры, вместе с объемом волос и их правильной обрезкой на кончиках. Второй рисует cutout-геометрию, а третий (transparent) — добавляет мягкие кончики.  Чтобы волосы выглядели живыми, они должны блестеть. Для достижения такого эффекта мы рассматривали два варианта: анизотропный динамический блик, реагирующий на освещение, и статичный фейковый, который мог бы облегчить обработку шейдера. Для создания статичного блика мы использовали технологию текстурирования Matcap — имитацию материала и окружения с помощью всего одного изображения.  Эта технология позволяет натягивать предзапеченное в текстуре освещение на 3D-модель для имитации освещения (теней, бликов, оттенков и т. д.). То есть мы могли наложить поверх текстуры волос нарисованный блик так, как нам удобно. Форму блика можно настраивать, но он никак не будет реагировать на смену освещения.  Визуальная разница между анизотропным и фейковым бликами: Разница в шейдерном коде при обработке анизотропного и фейкового бликов:   Anisotrophic   Matcap   fixed4 frag (v2f i) : SV_Target  {            float3 specular;          float3 binormal = normalize (cross( i.normal, i.tangent.xyz ));          float3 tangent = normalize(mul(unity_ObjectToWorld, i.tangent));          float HdotN = dot(halfDir, i.normal);          float VdotN = dot(i.viewDir, i.normal);          float anX = dot(halfDir, binormal) / _AnisotropicX;          float anY = dot(halfDir, tangent) / 4;                      float diffuseSpecular = lerp(1.0, specTex.g, _SpecularPower);          specular = diffuseSpecular *_LightColor * _Specular * _SpecularColor * exp (-2.0 * (anX * anX + anY + anY) / (1.0 + HdotN ));}  v2f vert (appdata v)  {            ...          float3 worldNorm = UnityObjectToWorldNormal(v.normal);            worldNorm = mul((float3x3)UNITY_MATRIX_V, worldNorm);          o.matcapUV = worldNorm.xy * 0.5 + 0.5;          ...  }  fixed4 frag (v2f i) : SV_Target  {            ...            fixed specularMask = tex2D(_MatcapTex, i.uv).g;            ...  }  Визуальная разница между анизотропным и фейковым бликами в рамках сцены, где появлялась девушка, была слабо различима, да и угол, под которым ее видел игрок, менялся не сильно. Но первый вариант генерировал больше математических операций, поэтому мы выбрали второй. Чтобы блик смотрелся органично и не создавал плоского эффекта, решили применить маску. Сначала мы пробовали вариант с картой нормали, но позже заменили ее менее тяжелой альфа-маской, которая давала похожий эффект. Применение альфа-маски в коде: За основу мы взяли обычную костную анимацию для прядей. А для создания дополнительной динамики решили использовать шевеление в вертексном шейдере. Для цветовой коррекции применили запеченное освещение в виде коэффициентов, которое называется SHA, или сферические гармоники. Такой метод рендеринга может создавать высокореалистичное затенение с относительно небольшими ресурсозатратами. Его основой служат предварительно рассчитанные коэффициенты, и он отлично подходит для рассеянного освещения от окружения (например, засветов от неба). Этот же принцип использовался не только для волос, но и для самой модели персонажа. Исходники:   Детализация геометрии   2500   Количество текстур   3   Разрешение текстуры 1   512x512   Разрешение текстуры 2   256x256   Разрешение текстуры 3   128x128   Шейдер:    Количество проходов   3   Количество текстурных читок   1 проход: 3 читки, 2 проход: 3 читки, 3 проход: 1 читка   Количество математических операций в вертексном шейдере   1 проход: 54 math, 2 проход: 51 math, 3 проход: 13 math   Количество математических операций во фрагментном шейдере   1 проход: 11 math, 2 проход: 17 math, 3 проход: 0 math   В получившемся шейдере нет ничего революционного — техники, которые применялись для его создания, используются во многих других проектах. Но именно благодаря совокупности этих техник мы добились нужного результата. Разработчик