В этой статье слишком много воды

    «Мы начинаем разработку новой игры, и нам нужна классная вода. Такую сможешь?»


    , — cпросили меня. «Да не вопрос! Конечно, смогу», — ответил я, но голос предательски задрожал. «А, еще и на Unity?», — и мне стало понятно, что впереди очень много работы.

    Итак, водичка. Unity до того момента я в глаза не видел, ровно как и C#, так что решил, что буду делать прототип на знакомых мне инструментах: С++ и DX9. Что я знал и умел на практике на тот момент так это скроллящиеся текстурки нормалей для формирования поверхности, и примитивный дисплейсмент маппинг на их основе. Тут же надо было менять абсолютно все. Реалистичная анимированная форма водной поверхности. Усложненый (сильно) шейдинг. Генерация пены. LOD система, привязанная к камере. Начал я выискивать информацию в интернете как же все это сделать то.

    Первым пунктом, естественно, было вникание в «Simulating Ocean Water» Джерри Тессендорфа.

    Академические пейперы с кучей заумных формул никогда мне особо не давались, так что после пары прочтений я мало что понял. Общие принципы были понятны: каждый кадр генерируется карта высот с помощью Fast Fourier Transform, которая, как функция от времени, плавно меняет свою форму формируя реалистичную водную поверхность. Но как и что считать я не знал. Я потихоньку вникал в премудрости просчета FFT на шейдерах в D3D9, и мне в этом очень помог исходник со статьей где-то в дебрях интернета, который я битый час пытался отыскать, но безуспешно (к сожалению). Первый результат был получен (страшный как ядерная война):


    Стартовые успехи порадовали, и начался перенос воды на Unity с его доработкой.

    К воде в игре про морские битвы выдвигались несколько требований:

    • Реалистичный внешний вид. Красивые как близкие так и дальние ракурсы, динамическая пена, скаттеринг и т. д.
    • Поддержка различных погодных условий: штиль, шторм и промежуточные состояния. Смена времени суток.
    • Физика плавучести кораблей по симулированной поверхности, плавучие объекты.
    • Так как игра мультиплеерная, вода должна быть у всех участников боя одинаковая.
    • Рисование по поверхности: нарисованные зоны полета ядер залпа, пена от попаданий ядер в воду.

    Геометрия


    Решено было построить quadtree-like структуру, с центром вокруг камеры, которая дискретно перестраивается при движении наблюдателя. Почему дискретно? Если двигать меш плавно вместе с камерой или использовать screen space reprojection как в статье Real-time water rendering — introducing the projected grid concept, то на дальних планах из-за недостаточного разрешения геометрической сетки при выборке карты высот волны будут «скакать» полигоны вверх и вниз. Это очень сильно бросается в глаза. Картинка «рябит». Чтобы это побороть, надо либо сильно увеличивать разрешение полигональной сетки water mesh'a, либо «уплощать» геометрию на дальних дистанциях, либо так строить и двигать полигоны, чтобы эти сдвиги не было видно. Вода у нас прогрессивная (хехе) и выбрал я третий путь. Как и в любой подобной технике (особенно знакомой всем, кто создавал terrain в играх), необходимо избавиться от T-junctions на границах переходов уровней детализации. Для решения этой задачи на старте предрасчитывается 3 вида квадов с заданными параметрами тесселяции:



    Первый тип для тех квадов, которые не являются переходными на более низкую детализацию. Ни одна из сторон не имеет уменьшенное в 2 раза количество вершин. Второй тип для граничных, но не угловых квадов. Третий тип — угловые граничные квады. Финальный меш для воды строится поворотом и масштабированием этих трех видов мешей.

    Вот так выглядит рендер с подсветкой разным цветом LOD уровней воды.


    На первых кадрах видно соединение двух различных уровней детализации.

    Видео как кадр заполняется водяными квадами:


    Напомню, это все было давно (и неправда). Сейчас более оптимально и гибко можно сделать сразу на GPU (GPU Pro 5. Quadtrees on the GPU). И рисовать будет в один draw call, и тесселяцией можно поднять детализацию.

    Позднее проект переехал на D3D11, но до апгрейда этой части рендера океана руки так и не дошли.

    Генерация формы волны


    Вот для этого нам понадобится Fast Fourier Transform. Для выбранного (нужного) разрешения текстуры волны (пока назовем ее так, далее я объясню, какие данные там хранятся) подготавливаем начальные данные, используя параметры, заданные художниками (сила, направление ветра, зависимость волны от направления ветра и другие). Все это необходимо скормить в формулы т.н. Phillips spectrum'а. Полученные начальные данные модифицируем каждый кадр с учетом времени и выполняем FFT над ними. На выходе получаем тайлящуюся по всем направлениям текстуру которая содержит смещение вершин плоского меша. Почему не просто heightmap? Если хранить только оффсет по высоте, то результатом будет нереалистичная «бурлящая» масса, лишь отдаленно напоминающая море:


    Если считать смещения для всех трех координат, то будут генерироваться красивые «острые» реалистичные волны:


    Одной анимированной текстурки мало. Виден тайлинг, недостаточно деталей на ближних планах. Берем описанный алгоритм и делаем не одну, а 3 fft-generated текстуры. Первая — крупные волны. Она задает основную форму волны и используется для физики. Вторая — средние волны. Ну и напоследок самые мелкие. 3 FFT генератора (4-й вариант это финальный микс):


    Параметры слоев задаются независимо друг от друга, и полученные текстуры смешиваются в шейдере воды в финальную форму волны. Параллельно со смещениями генерируются и карты нормалей каждого слоя.

    «Одинаковость» воды у всех участников боя обеспечивается синхронизацией параметров океана на старте боя. Эту информацию передает сервер каждому клиенту.

    Физическая модель плавучести


    Так как необходимо было сделать не только красивую картинку, но и реалистичное поведение кораблей. А также учитывая то, что в игре должно присутствовать штормовое море (крупные волны), то еще одной задачей, которую требовалось решить, являлось обеспечение плавучести объектов на поверхности сгенерированного моря. Сперва я попытался сделать GPU readback текстуры волны. Но, так как быстро выяснилось, что всю физику морского боя необходимо делать на сервере, то и море, а точнее первый его слой который задает форму волны, необходимо считать также и на сервере (а на нем, скорее всего, нет быстрого и/или совместимого GPU), то было принято решение написать полную функциональную копию GPU FFT генератора, на CPU в виде native C++ плагина к Unity. Сам FFT алгоритм я не реализовывал и использовал готовый в библиотеке Intel Performance Primitives (IPP). А вот всю обвязку и постпроцессинг результатов был выполнен мной, с последующей оптимизацией на SSE и распараллеливанием по потокам. Сюда входила и подготовка массива данных для FFT каждый кадр, и финальное преобразование посчитанных значений в wave offset map.

    Была еще одна интересная особенность алгоритма, которая исходила из требований к физике воды. Нужна была функция быстрого получения высоты волны в данной точке мира. Логично, ведь это и есть основа построения плавучести любого объекта. Но, так как на выходе FFT процессора у нас получалается offsetmap, а не heightmap, то обычная выборка из текстуры не давала нам высоту волны там где было необходимо. Для простоты рассмотрим 2D вариант:



    Для формирования волны, тексели (текстурные элементы, показанные вертикальными линиями) содержат вектор (стрелки) который задает смещение вертекса плоского меша (синие точки) в направлении его финальной позиции (острие стрелки). Предположим мы возьмем эти данные и попробуем извлечь из нее высоту воды в интересующей нас точке. Например, нам надо узнать высоту в точке hB. Если мы возьмем вектор в текселе tB, то мы получим смещение в точку около hC, что может сильно отличаться от того что нам нужно. Вариантов решения этой проблемы два: при каждом запросе высоты проверять множество соседних текселей, пока не найдем тот, который имеет смещение в интересующую нас позицию. В нашем примере мы найдем тексель tA как содержащий наиболее близкое смещение. Но такой подход не назовешь быстрым. Сканирование радиуса текселей непонятно какого размера (а от того, штормовое море или спокойное, смещения могут сильно варьироваться) может занять продолжительное время.

    Второй вариант — после просчета offset map конвертировать ее в height map, используя scattering подход. Это означает, что для каждого offset vector'а мы запишем высоту волны, которую он задает, в ту точку, куда он смещается. Это будет отдельный массив данных, который и будет использоваться для получения высоты в интересующей точке. Используя нашу иллюстрацию, ячейка tB будет содержать высоту hB полученную из вектора tA→hB. Есть еще одна особенность. Ячейка tA не будет содержать валидного значения, так как нет вектора, смещающегося в него. Для заполнения таких «дырок» выполняется проход заполнения их соседними значениями.

    Вот так это выглядит, если сделать визуализацию смещений с помощью векторов (красные — большое смещение, зеленый — малое):


    Далее все просто. Для корабля задается плоскость условной ватерлинии. На ней определяется прямоугольная сетка точек-проб, которая задает места приложения выталкивающих из воды сил для корабля. Затем для каждой точки проверяем, под водой она или нет, используя water heightmap, описанную выше. Если точка под водой, то прикладываем вертикальную силу вверх к physics hull корпуса в этой точке, масштабированной расстоянием от точки до водной поверхности. Если над водой, то ничего не делаем, гравитация сделает все для нас. На самом деле там формулы немного сложнее (вся для тонкого тюнига поведения корабля), но основной принцип такой. На видео визуализации плавучести ниже, синие кубы — это места расположения проб, а линии от них вниз — это величина выталкивающей из воды силы.


    В реализации сервера есть еще один интересный оптимизационный момент. Нет никакой надобности симулировать разную воду для разных боевых инстансов, если они проходят в одинаковых погодных условиях (одинаковые параметры FFT симулятора). Так что логичным решением было сделать пул симуляторов, к которым боевые инстансы выполняют запросы на получение симулированной воды с заданными параметрами. Если параметры одинаковые от нескольких инстансов, то им вернется одна и та же вода. Реализовано это с помощью Memorу Mapped File API. Когда FFT симулятор создается, он дает доступ к своим данным, экспортируя дескрипторы нужных блоков. Серверный инстанс вместо того, чтобы запускать реальный симулятор, запускает «пустышку» которая просто отдает данные, открытые по этим дескрипторам. Было несколько веселых багов, связанных с этим функционалом. Из-за ошибок подсчета ссылок симулятор уничтожался, но memory mapped file жив пока открыт хоть один дескриптор на него. Данные переставали обновляться (симулятора-то нет) и вода «останавливалась».

    На клиентской стороне нам необходима информация о форме волны для просчета попаданий ядер в волну и проигрывания систем частиц и пены. Просчет повреждений происходит на сервере и там также необходимо корректно определять, попало ли ядро в воду (волна может закрывать корабль, особенно в штормах). Тут уже необходимо делать heightmap tracing по аналогии как это делается в parallax mapping либо SSAO эффектах.

    Шейдинг


    В принципе как и везде. Отражения, преломления, subsurface scattering хитро замешиваем, учитывая глубину дна, учитываем fresnel effect, считаем спекуляр. Скаттеринг считаем для гребней в зависимости от позиции солнышка. Пена генерируется следующим образом: создаем «пятно пены» на гребнях волн (используем высоту как метрику), затем накладываем новосозданные пятна на пятна с предыдущих кадров одновременно уменьшая их интенсивность. Таким образом получаем размазывание пятен пены в виде хвоста от идущего гребня волны.


    Используем полученную текстуру «пятен» как маску к которой примешиваем текстуры пузырьков, разводов и т. д. Получаем довольно реалистичный динамический рисунок пены на поверхности волн. Данная маска создается для каждого FFT слоя (напомню, у нас их 3), и в финальном миксе они все смешиваются.

    На видео выше визуализация маски пены. Первый и второй слои. Я модифицирую параметры генератора и результат виден на текстуре.

    И видео немножко коряво настроенного штормового моря. Тут хорошо видна форма волны, возможности генератора и пена:


    Рисование по водной поверхности


    Картинка-пример использования:



    Используется для:

    • Маркеры, визуализации зоны разлета ядер.
    • Рисование пены в месте попадания ядер в воду.
    • Пенный след за кораблем
    • Выдавливание воды под кораблем, чтобы убрать эффект заливания волнами палубы и затопленного трюма.

    Очевидный базовый вариант — проективное текстурирование. Оно и было реализовано. Но тут появились дополнительные требования. Ближние виды — мыло из-за недостаточного разрешения (можно увеличивать, но не бесконечно), и хочется чтобы далеко было видно эти проективные рисунки на воде. Где решается такая же задача? Правильно, в тенях (shadow map). Как она там решается? Правильно, Cascaded (Parallel Split) Shadow Maps. Возьмем и мы эту технологию на вооружение и применим к нашей задаче. Разбиваем фрустум камеры на N (3-4 обычно) сабфрустумов. Для каждого строим описывающий прямоугольник в горизонтальной плоскости. Для каждого такого прямоугольника строим orthographic projection матрицу и рисуем все интересующие объекты для каждой из N таких ortho камер. Каждая такая камера рисует в отдельную текстуру, а затем, в шейдере океана, мы их комбинируем в одну цельную проективную картинку.


    Вот я положил на море большущую плоскость с текстурой флагов:



    Вот то что содержится в сплитах:



    Кроме обычных картинок надо абсолютно таким же образом нарисовать дополнительную маску пены (для следов кораблей и мест попаданий ядер), а также маску выдавливания воды под кораблями. Это много камер и много проходов. Поначалу оно так тормозно и работало, но затем, после перехода на D3D11, с помощью «размножения» геометрии в геометрическом шейдере и рисования каждой копии в отдельный render target через SV_RenderTergetArrayIndex, удалось сильно ускорить этот эффект.

    Улучшения и модернизации


    D3D11 очень сильно развязывает руки во многих моментах. После перехода на него и Unity 5 я сделал FFT генератор на сompute шейдерах. Визуально ничего не поменялось, но стало чуточку быстрее. Перевод просчет текстуры отражений с отдельного полноценного рендера камеры на технологию Screen Space Planar Reflections дал неплохой буст производительности. Про оптимизацию water surface objects я писал выше, а до перевода mesh'а на GPU Quadtree руки так и не дошли.

    Многое, возможно, можно было сделать оптимальнее и проще. Например, не городить огороды с CPU симулятором, а просто запустить GPU вариант на сервере с WARP (программным) d3d девайсом. Массивы данных там не очень большие.

    Ну, в общем как-то так. В то время, как разработка начиналось, все это было современно и круто. Сейчас уже местами подустарело. Появилось больше доступных материалов, даже есть похожий аналог на github: Crest. В большинстве игр, где есть моря, используется похожий подход.
    Share post

    Similar posts

    Comments 36

      +5

      Классно написано, читается легко. Хочется продолжения )

        0
        Спасибо! Сложу в избранное пригодится
          +6

          «Ничего не понятно, но очень интересно»


          Очень хорошо написано. Даже не понимая большей части происходящего все равно интересно читать

            0
            Очень интересно!
            К самой воде претензий нет.
            Вот видео вверху — которое NVIDIA wave works — когда вода с пеной — то граница вода-корабль выглядит хорошо. А когда вода спокойная — скажем на 0:20 то ощущение, что корабль просто воткнули в воду. С чем это связано?
              0

              А там у них привязка интенсивности пены к шкале Бофорта. Малое волнение — нет пены. Больше — пена появляется. Уверен, пенный след и пена вокруг корабля сделаны так же как и у меня: через маску. Так что на малом волнении этого следа и не видно (как и всей пены)

                0
                www.youtube.com/watch?v=P4ordwdSv6o
                Вот тут видно, что корабль влияет на воду, хотя движется медленно.
                  +6

                  Есть одно маленькое различие. То компьютерная симуляция, а это реальное видео :) Не все (пока) можно просимулировать в реалтайме, чтобы было как в жизни

                    0
                    Вот фиг знает. Я смотрел, смотрел… и думал — то ли реальное, то ли симуляция. На самом кораблике деталей как-то мало, но люди вроде иногда шевелятся. В принципе отличия-то понятные — образуется пена у носа и далее, в воде есть отражение, и за кораблем идут волны от него. Думается это можно сделать сейчас. Хотя… я знаю как это делать в плане CFD (правда без пены), но это явно не реал-тайм. Возможно есть какие-то принципиальные ограничения. Скажем — поверхность же постоянно деформируется, значит постоянно меняется освещение, и это гораздо хуже и чем «целый взвод лысых спецназовцев» и чем «одна маленькая, пушистая мышка».
                      0
                      чтобы было как в жизни
                      Позвольте согласиться с Daddy_Cool. Может и правильная вода на первом видео, но общее впечатление никакое. Корабль действительно «воткнули» в воду, кажется что он дрейфует. Но потом из трубы валит дым, он взбирается на волны не оставляя никаких следов на воде, даже от работающего винта. А позже выясняется что и винт не крутится, а дым для красоты. Вода не заливает палубу и т.д.

                      Вы же не математическую модель океана делаете, а красоту создаете. Мне, как дилетанту, кажется что картинка не должна быть «как в жизни». Небольшое утрирование и подчеркивание деталей (как в хороших мультфильмах), на которые все обращают внимание, сделает ее более «жизненной». Пена от винта, волны от форштевня. Все знают что «Форштевнем режет бриг игривую волну ...» и сразу обращают внимание — не режет. И хоть какая вода будет — зритель в нее уже не поверит.

                      Я не критикую Вашу воду, игру я ещё не успел посмотреть. Всё сказанное относится к ролику NVIDIA.
                0
                Тоже интересная тема
                www.youtube.com/watch?v=jGoSavwF9Pw
                  0
                  Вау! Даже маленькие вихри-водоворотики образуются
                  0
                  Отлично получилось! Мне еще очень нравится как сделана вода в Sea of Thieves, тоже, кстати, на юнити. Только за ради вида воды и моря возвращаюсь в игру снова и снова.
                    +1
                    Sea of Thieves на Unreal Engine
                      0
                      Точно, понял почему перепутал. В одном из разработческих видео они показывали прототипы на юнити (механику ломания мачт), и потом переносили на основной движок. Вот юнити и засел в памяти.
                      0
                      Да в Sea of Thieves классная вода. Мне нравится как там пена нарисована, и то что её сдувает с высоких волн. Нигде больше такого не видел.
                        0
                        меня люто разочаровала вода в Сабнавтике. Казалось бы, игра про бескрайний океан, стоило бы проработать физику воды. Ан нет. Вялые волны и все.
                          +2
                          Справедливости ради, в Сабнотике по поверхности очень мало ходишь, большая часть игры все таки под водой. А уж про подводный мир можно много говорить, что как бы и заложено в названии)
                          Учитывая как в целом отполированно ощущается игра во многих аспектах, я думаю, если б они взялись за волны, то вышло бы что-то на уровне Моря Воров.
                        0
                        А как игра-то называется? После того как узнал, что просчитывается попадание ядер в волну, я захотел пощупать.
                          0
                          Naval Action.
                            +1
                            Спасибо.
                            Пощупал, забавно. Хотя некоторые вещи вызывают недоумение. Вроде как симулятор, но корабль можно ремонтировать прямо в бою.
                            И может ещё подскажете. Вдруг слышали от коллег.
                            В моём понимании макс скорость корабля в зависимости от парусного вооружения будет при угле к ветру 90-180 градусов. Но в игре есть корабли, максимальная скорость которых достигается примерно под углом около 45 градусов к ветру. Т.е. против ветра.
                            И мне ни в тот вариант, что это правда (я не понимаю как это может работать, теоретический предел должен быть 90 градусов к ветру)
                            И я не верю что кто-то мог ТАК налажать. Это-же контринтуитивно-плавать быстрее против ветра.
                              0
                              плавать против ветра можно, за счет того, что сопротивление воде у корпуса корабля неодинаковое по продольному и поперечному направлениям. Вообще пишут, что у хороших парусных судов лучшая скорость достигается где-то при 30 градусах к направлению ветра
                              Тут подробнее
                              ru.wikipedia.org/wiki/%D0%9A%D1%83%D1%80%D1%81_%D0%BE%D1%82%D0%BD%D0%BE%D1%81%D0%B8%D1%82%D0%B5%D0%BB%D1%8C%D0%BD%D0%BE_%D0%B2%D0%B5%D1%82%D1%80%D0%B0
                                0
                                По вашей-же ссылке не 30 градусов.
                                Выделяют курс полный бакштаг, при котором угол превышает 135° градусов, то есть приближающийся к фордевинду, и крутой бакштаг (менее 135°). Парус устанавливается под углом к ветру. Обычно на этом курсе парусное судно развивает наивысшую скорость.
                                То, что ходить против ветра можно я знаю, более того сам ходил. Вопрос только по скорости.
                                  0
                                  Бейдеви́нд (нидерл. bij de wind), или на ветер — курс, при котором угол между направлением ветра и направлением движения судна составляет менее 90° (меньше 8 румбов)[4][1]. Выделяют бейдевинд полный и крутой. Границу между ними разные источники проводят по-разному (в диапазоне от 45 до 67,5°). Тяга паруса при бейдевинде целиком определяется его подъёмной силой, при увеличении давления ветра сила тяги уменьшается, зато возрастает сила дрейфа. Таким образом, на этом курсе парус, устанавливаемый с минимальным углом атаки к вымпельному ветру (5—10°), работает полностью как аэродинамическое крыло[5].

                                  Лучшие парусные суда ходят под углом 30—35° к направлению вымпельного ветра. Вследствие сложения векторов скорости ветра и встречного потока воздуха, скорость вымпельного ветра на курсе бейдевинд оказывается максимальной, также как и подъёмная сила на парусе, пропорциональная квадрату скорости ветра. Максимальной величины достигает и сила дрейфа. Если попытаться идти под более острым углом к ветру, то скорость судна будет снижаться, парус станет заполаскивать[6], подъёмная сила снизится и, наконец, наступит момент, когда дрейф и сопротивление воды движению намного превысят тягу. Судно потеряет ход.


                                  По ссылке именно это. Насчет достоверности информации по скорости стопроцентной гарантии дать не могу, из парусного опыта у меня только пара дней на виндсерфе
                                    0
                                    По вашей цитате лучшие СУДА ходят под 30-35 градусов к ВЫМПЕЛЬНОМУ ветру.
                                    А НЕ лучшая СКОРОСТЬ при 30-35 градусах. И ветер вымпельный, а не истинный.
                                    Кстати в цитате есть логическая ошибка. Вот эта часть не откуда не следует «также как и подъёмная сила на парусе» т.к. подъёмная сила паруса пропорциональна не только квадрату скорости ветра.
                                    Хотя цитата заставила задуматься. Наверно действительно теоретически возможно, что максимальная скорость будет при угле к ветру меньше 90 градусов.
                                    Т.к. чем меньше данный угол, тем больше будет скорость вымпельного ветра при равной скорости истинного. Правда ограничений видится куча, но теоретически, наверное, возможно.

                                      0
                                      да, согласен, ходят != достигается максимальная скорость
                          +5
                          … Когда понимаешь, что формула воды ни разу не H2O…
                            +2
                            Правильно. H2O это формула одной молекулы воды, а тут океан, минимум H2O^овердофига, а в нем же ещё соли всякие…
                            +1
                            К слову интересно физика и отрисовка воды в From Dust сделаны. Жаль не выходит ничего похожего уже много лет.
                              +1
                              Ну игру я не видел эту, но судя по видео там никакой физики нет. А есть там Flow Map (http://graphicsrunner.blogspot.com/2010/08/water-using-flow-maps.html). А геометрия статичная, с небольшим vertex displacement.
                                0
                                Ну зато там вода реально течёт, переливается из резервуара в резервуар, сохраняя общий объём и размывая при этом берега, перенося грунт в другое место.
                                0
                                да ну, там же вроде простецкое смещение текстуры по деформированным УВ координатам плюс фейд возле суши по карте глубин
                                0
                                Отличная статья. Сам для себя как-то делал волны, взяв за основу информацию из nVidia GPU Gems: developer.nvidia.com/gpugems/GPUGems/gpugems_ch01.html, на основе волн Герстнера, без особых проблем собирается в UE4 на нодах.

                                  0
                                  Как же это жутко интересно, сложно и непонятно без знания математики. Может быть есть какая-то отправная точка с чего можно начать изучение? Или если математический базис упущен, то можно забыть про программирование графики? Конечно можно начать с самого начала изучить линейную алгебру и тригонометрию, но тогда есть шанс утонить в математике так и не начав :)
                                    +1
                                    Без знания математики такие системы не создашь, это 100%. Но можно начать с простого, постепенно осваивая новые моменты, принципы, архитектуру 3D рендера и т.д. Я, например, с туториалов и всяких там обучалок начинал.
                                      0
                                      Спасибо, можете поделиться ссылками на туториалы? Может быть можете какие-то книги порекомендовать?
                                        +1
                                        Материалы для начинающих я рекомендовать не буду, так как я ими уже много лет не интересовался. Книги: Серии ShaderX, GPU Pro, GPU Zen, GPU Gems. Real-Time Rendering. Но это все для подготовленного читателя.

                                  Only users with full accounts can post comments. Log in, please.