<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://romanmarchenko.me/ru/blog</id>
    <title>Roman Marchenko Blog</title>
    <updated>2020-01-15T00:00:00.000Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="https://romanmarchenko.me/ru/blog"/>
    <subtitle>Roman Marchenko Blog</subtitle>
    <icon>https://romanmarchenko.me/img/favicon.ico</icon>
    <entry>
        <title type="html"><![CDATA[GPU Bound. Часть вторая. Бескрайний лес.]]></title>
        <id>https://romanmarchenko.me/ru/blog/gpubound-foliage</id>
        <link href="https://romanmarchenko.me/ru/blog/gpubound-foliage"/>
        <updated>2020-01-15T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Img]]></summary>
        <content type="html"><![CDATA[<p><img decoding="async" loading="lazy" alt="Img" src="https://romanmarchenko.me/assets/images/Jungle-e98345bdc9932f20bab29087ad56fb48.jpg" width="1200" height="684" class="img_ev3q"></p>
<p>Практически в каждой игре необходимо наполнять игровые уровни объектами, которые создают визуальное богатство, красоту и вариативность виртуального мира. Возьмите любую игру с открытым миром. Там деревья, трава, земля и вода основные «заполнители» картинки. Сегодня GPGPU будет совсем немного, но я попробую рассказать, как нарисовать в кадре много деревьев и камней, когда нельзя, но очень хочется.</p>
<p>Сразу следует оговориться, что у нас маленькая инди девстудия, и ресурсов вырисовывать и вымоделивать каждую мелочь часто нету. Отсюда и требование к различным подсистемам, быть «надстройкой» над уже готовым функционалом движка. Так было и в первой статье цикла про анимации (там мы использовали и ускоряли уже готовую систему анимации Unity), так будет и тут. Это сильно упрощает внедрение нового функционала в игру (меньше учиться, меньше багов, etc).</p>
<p>Итак, задача: надо нарисовать много леса. Игра у нас стратегия реального времени (RTS) с большими уровнями (30х30 км), и это задает основные требования к системе отрисовки:</p>
<ul>
<li class="">С помощью миникарты мы можем мгновенно перенестись в любую точку уровня. И данные про объекты для новой позиции должны быть готовы. Мы не можем как в FPS или TPS играх полагаться на подгрузку ресурсов через какое-то время.</li>
<li class="">Объектов на таких больших уровнях необходимо действительно большое количество. Сотни тысяч, если не миллионы.</li>
<li class="">Опять же, большие уровни делают очень долгой и сложной процедуру расстановки «лесов» в ручном режиме. Необходима процедурная генерация леса, камней и кустов, но с возможностью ручной корректировки и расстановки в ключевых местах игрового уровня.</li>
</ul>
<p>Как можно решить такую задачу? Такое количество обычных расставленных объектов юнити все равно не потянет. Умрем в куллинге и батчинге. Рендер возможен с помощью инстансинга. Надо писать систему управления. Деревья надо моделлить. Систему анимаций деревьев надо делать. Ох. Хочется красиво и сразу. Есть SpeedTree, но нет апи для анимаций, вид сверху на билборды ужасен, так как нет «horizontal billboard» и документация скудновата. Но когда нас такое останавливало? Будем оптимизировать SpeedTree рендер.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="рендеринг">Рендеринг<a href="https://romanmarchenko.me/ru/blog/gpubound-foliage#%D1%80%D0%B5%D0%BD%D0%B4%D0%B5%D1%80%D0%B8%D0%BD%D0%B3" class="hash-link" aria-label="Direct link to Рендеринг" title="Direct link to Рендеринг" translate="no">​</a></h3>
<p>Посмотрим для начала, так ли все плохо с обычными speedtree объектами:</p>
<p><img decoding="async" loading="lazy" alt="Speedtree" src="https://romanmarchenko.me/assets/images/SpeedTree_Orig-81023a473dfb8024cc0397516f2191cd.jpg" width="1920" height="995" class="img_ev3q"></p>
<p>Вот около 2000 деревьев на сцене. С рендером все в порядке, там instancing объединил деревья в батчи, а вот с CPU все плохо. Половина времени рендера камеры — это куллинг. А нам надо сотни тысяч. Однозначно отказываемся от GameObject'ов, но теперь нам надо расковырять структуру модельки SpeedTree, механизм переключения LOD'ов и сделать все ручками.</p>
<p>Дерево SpeedTree состоит из нескольких LODов (обычно 4), последний из которых billboard, а все остальные — геометрия различной степени детализации. Каждый из них состоит из нескольких сабмешей, со своим материалом:</p>
<iframe width="660" height="415" src="https://www.youtube.com/embed/FvjIYnvQARE?si=uaYz0xsFkb9jMZAk" title="YouTube video player" frameborder="0" allow="fullscreen; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin"></iframe>
<p>Это не специфика именно SpeedTree. Такую структуру может иметь любой объект. Переключение LODов реализовано двумя доступными режимами:</p>
<ul>
<li class="">Cross Fade:</li>
</ul>
<iframe width="660" height="415" src="https://www.youtube.com/embed/S4r4R2d0cBo?si=ORLZU9wuiLQMvbc8" title="YouTube video player" frameborder="0" allow="fullscreen; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin"></iframe>
<ul>
<li class="">Speed Tree:</li>
</ul>
<iframe width="660" height="415" src="https://www.youtube.com/embed/UBspjqdvizc?si=MyQkhSU83f4rcKEu" title="YouTube video player" frameborder="0" allow="fullscreen; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin"></iframe>
<p><strong>CrossFade</strong> (в терминах шейдеров Unity он определяется define'ом препроцессора LOD_FADE_CROSSFADE) — основной метод переключения LOD для любых объектов сцены с несколькими уровнями детализации. Он заключается в том, что когда происходит смена LOD, меш, который должен исчезнуть, не просто исчезает (так будет отчетливо виден скачок качества модели), а «растворяется» на экране с помощью dithering'a. Простой эффект, а позволяет избежать использования настоящей прозрачности (alpha blending). Модель, которая должна появиться, абсолютно таким же образом «проявляется» на экране.</p>
<p><strong>SpeedTree</strong> (LOD_FADE_PERCENTAGE) же сделан специально для деревьев. В геометрию листьев, веток и ствола, помимо основных координат, записаны еще и дополнительные координаты позиции вершин младшего по отношению к текущему LOD уровня. Степень перехода из одного уровня в другой и есть значение веса для линейной интерполяции этих двух позиций. Переход в/из billboard происходит с помощью CrossFade метода.</p>
<p>В принципе, это все, что надо знать для реализации собственной системы переключения LOD'ов. Сам рендеринг прост. Проходимся в цикле по всем типам деревьев, по всем LOD'ам и по всем сабмешам каждого LOD'a. Устанавливаем соответствующий материал, и отрисовываем все экземпляры данного объекта одним махом с помощью инстансинга. Таким образом, количество DrawCall равно количеству уникальных объектов на сцене. Как мы знаем, что рисовать? В этом нам поможет</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="генератор-леса">Генератор Леса<a href="https://romanmarchenko.me/ru/blog/gpubound-foliage#%D0%B3%D0%B5%D0%BD%D0%B5%D1%80%D0%B0%D1%82%D0%BE%D1%80-%D0%BB%D0%B5%D1%81%D0%B0" class="hash-link" aria-label="Direct link to Генератор Леса" title="Direct link to Генератор Леса" translate="no">​</a></h3>
<p>Сама высадка проста и неприхотлива. Для каждого типа дерева разбиваем мир на квады так, чтобы в каждый помещалось одно дерево. Пробегаемся по всем квадам и проверяем маску вида:</p>
<p><img decoding="async" loading="lazy" alt="TreeMask" src="https://romanmarchenko.me/assets/images/TreeMask-4fd15a3290ac9908e2bd6258f501cf16.jpg" width="1000" height="1000" class="img_ev3q"></p>
<p>в данной точке уровня, а можно ли садить тут дерево? Маску, c «лесистыми» местами, рисует level designer. Сначала все это дело было на CPU и C#. Генератор работал неспешно, а размеры уровней росли так, что ждать перегенерации по несколько десятков минут стало напряжно. Решено было перенести генератор на GPU и Compute шейдера. Тут тоже все просто. Нам нужна heightmap земли, маска посадки деревьев и AppendStructuredBuffer, куда мы добавляем сгенерированные деревья (позиция и ID, вот и все данные).</p>
<p>Расставленные руками деревья в ключевых точках специальный скрипт подтягивает в общие массивы и удаляет оригинальный объект со сцены.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="culling--lod-switсhing">Culling &amp; LOD switсhing<a href="https://romanmarchenko.me/ru/blog/gpubound-foliage#culling--lod-swit%D1%81hing" class="hash-link" aria-label="Direct link to Culling &amp; LOD switсhing" title="Direct link to Culling &amp; LOD switсhing" translate="no">​</a></h3>
<p>Знать позицию и тип дерева недостаточно, чтобы сделать эффективный рендеринг. Надо каждый кадр определять, какие объекты видны и какой LOD (учитывая логику переходов) отправлять на рендер.</p>
<p>Этим также будет заниматься специальный сompute shader. Для каждого объекта сначала выполняется Frustum Culling:</p>
<iframe width="660" height="415" src="https://www.youtube.com/embed/_yJMYxnUTwI?si=AkSHG3NhBG4wsmog" title="YouTube video player" frameborder="0" allow="fullscreen; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin"></iframe>
<p>Если объект видим, то далее выполняется логика переключения LOD. По размеру на экране определяем нужный LOD level. Если для LOD группы задан режим CrossFade, то инкрементируем время перехода для dithering'a. Если SpeedTree Percentage, то считаем нормализированное значение перехода между LOD'ами.</p>
<p>В современных графических АПИ есть замечательные функции, позволяющие draw submission информацию передавать в вызов отрисовки в compute buffer (например ID3D11DeviceContext::DrawIndexedInstancedIndirect для D3D11). Это значит, что заполнять этот compute buffer можно и на GPU. Таким образом получается сделать полностью CPU независимую систему (ну только Graphics.DrawMeshInstancedIndirect вызвать). В нашем случае необходимо записать только количество инстансов каждого сабмеша. Остальная информация (количество индексов в меше и смещения) статична.</p>
<p>Compute buffer, с аргументами для draw call разбиваем на секции, каждая из которых отвечает за вызов отрисовки своего submesh. В сompute shader'e для меша, который должен быть отрисован в текущем кадре, инкрементируем соответствующее InstanceCount значение.</p>
<p>Вот как это выглядит в рендере:</p>
<iframe width="660" height="415" src="https://www.youtube.com/embed/1L0ejS9yloU?si=veLgmegEoOFXLD7W" title="YouTube video player" frameborder="0" allow="fullscreen; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin"></iframe>
<p>GPU occlusion culling — очевидный следующий шаг, но для RTS с такой камерой, и не очень большой холмистостью выигрыши не так очевидны (а для заинтересованных вот и вот). Я пока не сделал.</p>
<p>Чтобы все корректно рисовалось, надо немного подправить SpeedTree'шные шейдера, чтобы брать позицию и значения для переходов между LOD из соответствующих compute буферов.</p>
<p>Теперь у нас рисуются красивые, но статичные деревья. А на SpeedTree деревья реалистично влияет ветер, анимируя их. Вся логика таких анимаций находится в файле SpeedTreeWind.cginc, но никакой документации, ни доступа к внутренним параметрам из Unity нет.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">CBUFFER_START(SpeedTreeWind)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float4 _ST_WindVector;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float4 _ST_WindGlobal;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float4 _ST_WindBranch;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float4 _ST_WindBranchTwitch;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float4 _ST_WindBranchWhip;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float4 _ST_WindBranchAnchor;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float4 _ST_WindBranchAdherences;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float4 _ST_WindTurbulences;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float4 _ST_WindLeaf1Ripple;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float4 _ST_WindLeaf1Tumble;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float4 _ST_WindLeaf1Twitch;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float4 _ST_WindLeaf2Ripple;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float4 _ST_WindLeaf2Tumble;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float4 _ST_WindLeaf2Twitch;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float4 _ST_WindFrondRipple;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float4 _ST_WindAnimation;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">CBUFFER_END</span><br></div></code></pre></div></div>
<p>Как бы нам выковырять их? Для этого, для каждого типа дерева отрендерим оригинальный SpeedTree объект где-нибудь в невидном месте (вернее в видном для Unity, но не видном в камере, а то параметры не будут обновляться). Этого можно добиться сильно увеличив bounding box, и поместив объект за камеру). Каждый кадр снимаем нужный набор значений с помощью material.GetVector(...).</p>
<p>Так, деревья колыхаются на ветру, но вид сверху на билборды удручающий:</p>
<iframe width="660" height="415" src="https://www.youtube.com/embed/bH88-YYaHZI?si=xaNnrA-DAZtQS4zO" title="YouTube video player" frameborder="0" allow="fullscreen; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin"></iframe>
<p>С шейдер вариантом BILLBOARD_FACE_CAMERA_POS еще хуже:</p>
<iframe width="660" height="415" src="https://www.youtube.com/embed/Rl_nGbAK-Dc?si=v-5QQGWf6IzyEIxV" title="YouTube video player" frameborder="0" allow="fullscreen; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin"></iframe>
<p>Нам нужны horizontal (top-down) billboards. Это стандартная фича SpeedTree со времен царя Гороха, но, судя по форумам, до сих пор не реализованная в Unity. Пост с официального форума SpeedTree: «The Unity integration never used the horizontal billboard». Будем прикручивать руками. Саму геометрию сделать несложно. Как узнать UV координаты спрайта в атласе для нее?</p>
<p><img decoding="async" loading="lazy" alt="Speedtree atlas" src="https://romanmarchenko.me/assets/images/speedtree_BB_Atlas-a74ba6b42d9130cd8def53f040c08e03.jpg" width="512" height="512" class="img_ev3q"></p>
<p>Достаем старенький SpeedTreeRT SDK, и находим в документации структурку:</p>
<div class="language-c codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-c codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">struct</span><span class="token plain"> </span><span class="token class-name">SBillboard</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    bool            m_bIsActive</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">float</span><span class="token operator" style="color:#393A34">*</span><span class="token plain">    m_pTexCoords</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">float</span><span class="token operator" style="color:#393A34">*</span><span class="token plain">    m_pCoords</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">float</span><span class="token plain">           m_fAlphaTestValue</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div>
<p>«m_pTexCoords points to a set of 4 (s,t) texture coordinates that define the images used on the billboard. m_pTexCoords contains 8 entries.», говорится в ней на иностранном языке. Что ж, будем в бинарном spm файле искать последовательность из 4-х floating point значений, каждое из которых лежит в диапазоне [0..1]. Методом научного тыка выясняем, что нужная последовательность находится перед блоком из 12 float cо знаками соответствующими паттерну:</p>
<div class="language-cs codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-cs codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">float signs[] = { -1, 1, -1, 1, 1, -1, 1, 1, 1, -1, 1, 1 };</span><br></div></code></pre></div></div>
<p>Пишем небольшую консольную утилиту на плюсах, которая перебирает все spm файлы, и в них ищет uv координаты для горизонтальных билбордов. На выходе получается такая себе CSV табличка:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">Azalea_Desktop.spm: 0, 1, 0.333333, 1, 0.333333, 0.666667, 0, 0.666667, </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Azalea_Desktop_Flowers_1.spm: 0, 1, 0.333333, 1, 0.333333, 0.666667, 0, 0.666667, </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Azalea_Desktop_Flowers_2.spm: 0, 1, 0.333333, 1, 0.333333, 0.666667, 0, 0.666667, </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Leaf_Map_Maker_Desktop_1_Modeler_Use_Only.spm:  Pattern not found!</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Leaf_Map_Maker_Desktop_2_Modeler_Use_Only.spm:  Pattern not found!</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">BarrelCactus_Cluster_Desktop_1.spm: 0, 0.592376, 0.407624, 0.592376, 0.407624, 0.184752, 0, 0.184752, </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">BarrelCactus_Cluster_Desktop_2.spm: 0, 1, 0.499988, 1, 0.499988, 0.500012, 0, 0.500012, </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">BarrelCactus_Desktop_1.spm: 0, 0.2208, 0.220748, 0.2208, 0.220748, 5.29885e-05, 0, 5.29885e-05, </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">BarrelCactus_Desktop_2.spm: 0, 1, 0.301392, 1, 0.301392, 0.698608, 0, 0.698608,</span><br></div></code></pre></div></div>
<p>Для назначения текстурных координат геометрии horizontal billboard'у, находим нужную запись и парсим ее.</p>
<p>Теперь стало вот так:</p>
<iframe width="660" height="415" src="https://www.youtube.com/embed/JICYI1PAV2E?si=2ja4_hZMtefOzuYv" title="YouTube video player" frameborder="0" allow="fullscreen; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin"></iframe>
<p>Все еще не очень. Будем фейдить с помощью alpha test threshold вертикальный билборд, в записимости от угла к камере:</p>
<iframe width="660" height="415" src="https://www.youtube.com/embed/wxiwziYSnaA?si=BdMNBcXMinFWjRmf" title="YouTube video player" frameborder="0" allow="fullscreen; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin"></iframe>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="итоги">Итоги<a href="https://romanmarchenko.me/ru/blog/gpubound-foliage#%D0%B8%D1%82%D0%BE%D0%B3%D0%B8" class="hash-link" aria-label="Direct link to Итоги" title="Direct link to Итоги" translate="no">​</a></h3>
<p>Профайлер, показывающий динамическую (сколько всего чего рендерится) и статическую (сколько всего на сцене объектов и их параметры) статистику:</p>
<iframe width="660" height="415" src="https://www.youtube.com/embed/sOe8zQDbKC4?si=5a5AtiY3O8x6AbmI" title="YouTube video player" frameborder="0" allow="fullscreen; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin"></iframe>
<p>Ну и финальное красивое видео (во второй половине показаны переключения quality levels):</p>
<iframe width="660" height="415" src="https://www.youtube.com/embed/VJMHpkiEGtY?si=1pR1wNHdQKeI5QOs" title="YouTube video player" frameborder="0" allow="fullscreen; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin"></iframe>
<p>Что имеем в итоге:</p>
<ul>
<li class="">Система полностью CPU независима.</li>
<li class="">Работает быстро.</li>
<li class="">Использует готовые SpeedTree ассеты, коих можно купить в интернете.</li>
<li class="">Конечно, я ее подружил с любыми LODGroup, а не только SpeedTree. Так что много камушков теперь тоже можно.</li>
</ul>
<p>Из недостатков можно отметить отсутствие occlusion culling и таки не очень выразительные билборды.</p>]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[GPU Bound. Как перенести на видеокарту все и немножко больше. Анимации.]]></title>
        <id>https://romanmarchenko.me/ru/blog/gpubound-animations</id>
        <link href="https://romanmarchenko.me/ru/blog/gpubound-animations"/>
        <updated>2019-09-26T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Когда-то давно, было огромным событием появления на GPU блока мультитекстурирования или hardware transformation & lighting (T&L). Настройка Fixed Function Pipeline была магическим шаманством. А те кто умел включать и использовать расширенные возможности конкретных чипов через D3D9 API hacks, считали себя познавшими дзен. Но время шло, появились шейдеры. Сначала, сильно лимитированные как по функционалу, так и по длине. Далее все больше возможностей, больше инструкций, больше скорость выполнения. Появился compute (CUDA, OpenCL, DirectCompute), и область применения мощностей видеокарт стала стремительно расширяться.]]></summary>
        <content type="html"><![CDATA[<p>Когда-то давно, было огромным событием появления на GPU блока мультитекстурирования или hardware transformation &amp; lighting (T&amp;L). Настройка Fixed Function Pipeline была магическим шаманством. А те кто умел включать и использовать расширенные возможности конкретных чипов через D3D9 API hacks, считали себя познавшими дзен. Но время шло, появились шейдеры. Сначала, сильно лимитированные как по функционалу, так и по длине. Далее все больше возможностей, больше инструкций, больше скорость выполнения. Появился compute (CUDA, OpenCL, DirectCompute), и область применения мощностей видеокарт стала стремительно расширяться.</p>
<p>В этом цикле (надеюсь) статей я постараюсь расказать и показать, как «необычно» можно применить возможности современного GPU, при разработке игр, помимо графических эффектов. Первая часть будет посвящена анимационной системе. Все что описано, основано на практическом опыте, реализовано и работает в реальных игровых проектах.</p>
<p>Ууууу, опять анимации. Про это уже сто раз написано и описано. Что там сложного? Пакуем матрицы костей в буфера/текстурки, и используем для скиннига в вертекс шейдере. Это было описано еще в <a href="https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch02.html" target="_blank" rel="noopener noreferrer" class="">GPU Gems 3 (Chapter 2. Animated Crowd Rendering)</a>. И реализовано в недавней <a href="https://github.com/Unity-Technologies/UniteAustinTechnicalPresentation" target="_blank" rel="noopener noreferrer" class="">Unite Tech Presentation</a>. А можно ли по-другому?</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="технодемка-от-unity">Технодемка от Unity<a href="https://romanmarchenko.me/ru/blog/gpubound-animations#%D1%82%D0%B5%D1%85%D0%BD%D0%BE%D0%B4%D0%B5%D0%BC%D0%BA%D0%B0-%D0%BE%D1%82-unity" class="hash-link" aria-label="Direct link to Технодемка от Unity" title="Direct link to Технодемка от Unity" translate="no">​</a></h3>
<p>Много хайпа, но так ли круто на самом деле? На хабре есть статья, где подробно описано, как сделана и работает скелетная анимация в данной технодемке. Параллельные джобы это все хорошо, мы их не рассматриваем. Но надо выяснить, что и как там, с точки зрения рендера.</p>
<p>В масштабной битве сражаются две армии, каждая состоящая… из одного типа юнита. Слева скелеты, справа рыцари. Разнообразие так себе. Каждый юнит состоит из 3-х LOD (~300, ~1000, ~4000 вершин на каждый), и на вертекс влияют всего 2 кости. Система анимаций состоит из всего 7 анимаций на каждый тип юнита (напомню, что их аж 2). Анимации не блендятся, а переключаются дискретно из простого кода, который исполняется в job'ax, на которые делается акцент в презентации. Никакой стейт машины нет. Когда у нас два типа меша, то и рисовать всю эту толпу можно за два instanced draw call. Скелетная анимация, как я уже писал, основана на технологии описанной в далеком 2009 году.
Новаторски? Хм… Прорыв? Эм… Подходит для современных игр? Ну, разве что, соотношением фпс к количеству юнитов хвастаться.</p>
<p>Основные недостатки данного подхода (пребейк матриц в текстуры):</p>
<ul>
<li class="">Зависимость от фреймрейта. Захотели в два раза больше кадров анимации — пожалуйте в два раза больше памяти.</li>
<li class="">Отсутствие блендинга анимаций. Их конечно можно сделать, но в шейдере скиннига будет образовываться сложная каша из логики блендинга.</li>
<li class="">Отсутствие привязки к Unity Animator стейт машине. Удобный инструмент для настройки поведения персонажа, который можно подключить к любой системе скинннига, но в нашем случае, по причине пункта 2, все становится очень сложно (представьте как блендить вложенные BlendTree).</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="gpas">GPAS<a href="https://romanmarchenko.me/ru/blog/gpubound-animations#gpas" class="hash-link" aria-label="Direct link to GPAS" title="Direct link to GPAS" translate="no">​</a></h3>
<p>GPU Powered Animation System. Название только что придумал.
К новой системе анимаций предъявлялось несколько требований:</p>
<ul>
<li class="">Быстро работать (ну понятно). Нужно анимировать десятки тысяч различных юнитов.</li>
<li class="">Быть полным (или почти) аналогом системы анимцаии Unity. Если там анимация выглядит так, то и в новой системе должно выглядеть точно также. Возможность переключения между built-in CPU и GPU системами. Это необходимо часто для отладки. Когда анимации «глючат», то переключением в классический аниматор можно понять: это глюки новой системы, или стейтмашины/самой анимации.</li>
<li class="">Все анимации настраиваются в Unity Animator. Удобный, проверенный, а главное готовый к использованию, инструмент. Мы будем строить велосипеды в другом месте.</li>
</ul>
<p>Переосмыслим подготовку и запекание анимаций. Матрицы мы использовать не будем. Современные видеокарты хорошо работают с циклами, нативно поддерживают int помимо float, так что мы будем работать с кейфреймами как на CPU.</p>
<p>Рассмотрим пример анимации в юнити Animation Viewer:</p>
<p><img decoding="async" loading="lazy" alt="Anim" src="https://romanmarchenko.me/assets/images/anim_attack2-a9bff41dc7e1bef15b862eacb22bb71c.jpg" width="1555" height="717" class="img_ev3q"></p>
<p>Видно, что кейфреймы заданы отдельно для позиции, масштаба и вращения. Для каких-то костей надо их много, для каких-то всего несколько, а для тех костей, что не анимируются отдельно, просто задан начальный и конечный кейфрейм.</p>
<p>Позиция — Vector3, кватернион — Vector4, масштаб — Vector3. Структура кейфрейма у нас будет одна общая (для упрощения), так что нам нужно 4 float, чтобы уместить любой из вышеперечисленных типов. Еще нам надо InTangent и OutTangent для правильной интерполяции между кейфреймами согласно кривизны. Ах да, и нормализированное время не забыть:</p>
<div class="language-cs codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-cs codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">struct KeyFrame</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">{</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float4 v;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float4 inTan, outTan;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float time;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">};</span><br></div></code></pre></div></div>
<p>Для получения всех кейфреймов используем AnimationUtility.GetEditorCurve().
Также, мы должны запомнить имена костей, так как надо будет сделать ремап костей анимации в кости скелета (а они могут не совпадать) на этапе подготовки гпу данных.</p>
<p>Заполнив линейные буфера с массивами кейферймов, мы запомним смещения в них, для того чтобы найти те, которые относятся к нужной нам анимации.</p>
<p>Теперь интересное. Анимация скелета на GPU.</p>
<p>Подготавливаем большой («количество анимированных скелетов» Х «количество костей в склете» Х «эмпирический коэффициент максимального количества блендов анимаций») буффер. В нем будем хранить позицию, вращение и масштаб кости в данный момент анимации. И для всех запланированных анимированных костей в этом кадре запускаем compute shader. Каждый thread выполняет анимацию своей кости.</p>
<p>Каждый кейфрейм, вне зависимости к какой величине он пренадлежит (Translate, Rotation, Scale), интерполируется абсолютно одинаково (поиск линейным перебором, да простит меня Кнут):</p>
<div class="language-cs codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-cs codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">void InterpolateKeyFrame(inout float4 rv, int startIdx, int endIdx, float t)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">{</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    for (int i = startIdx; i &lt; endIdx; ++i)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        KeyFrame k0 = keyFrames[i + 0];</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        KeyFrame k1 = keyFrames[i + 1];</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        float lerpFactor = (t - k0.time) / (k1.time - k0.time);</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        if (lerpFactor &lt; 0 || lerpFactor &gt; 1)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            continue;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        rv = CurveInterpoate(k0, k1, lerpFactor);</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        break;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">}</span><br></div></code></pre></div></div>
<p>Кривая представляет собой cubic Bezier curve, так что функция интерполяции будет следующая:</p>
<div class="language-cs codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-cs codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">float4 CurveInterpoate(KeyFrame v0, KeyFrame v1, float t)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">{</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float dt = v1.time - v0.time;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float4 m0 = v0.outTan * dt;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float4 m1 = v1.inTan * dt;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float t2 = t * t;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float t3 = t2 * t;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float a = 2 * t3 - 3 * t2 + 1;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float b = t3 - 2 * t2 + t;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float c = t3 - t2;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float d = -2 * t3 + 3 * t2;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float4 rv = a * v0.v + b * m0 + c * m1 + d * v1.v;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    return rv;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">}</span><br></div></code></pre></div></div>
<p>Посчитали локальную позу (TRS) кости. Далее, отдельным compute shader'ом, сблендим все нужные анимации для данной кости. Для этого у нас есть буффер с индексами анимаций и весами каждой анимации в финальном бленде. Эту информацию мы получаем из стейт машины. Ситуация BlendTree внутри BlendTree решается так. Например есть такое дерево:</p>
<p><img decoding="async" loading="lazy" alt="BlendTree" src="https://romanmarchenko.me/assets/images/blendtree-9e4fea809a7cb744efd50dae904728bf.jpg" width="1116" height="636" class="img_ev3q"></p>
<p>BlendTree Walk будет иметь вес 0.35, Run — 0.65. Соответственно, финальная позиция костей должна определяться 4-мя анимациями: Walk1, Walk2, Run1 и Run2. Их веса будут иметь значения (0.35 * 0.92, 0.35 * 0.08, 0.65 * 0.92, 0.65 * 0.08) = (0.322, 0.028, 0.598, 0.052) соответственно. Следует отметить, что сумма весов всегда должна быть равна еденице, не то волшебные баги обеспечены.</p>
<p>«Сердце» функции блендинга:</p>
<div class="language-cs codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-cs codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">float bw = animDef.blendWeight;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">			</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">BoneXForm boneToBlend = animatedBones[srcBoneIndex];</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">float4 q = boneToBlend.quat;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">float3 t = boneToBlend.translate;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">float3 s = boneToBlend.scale;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">if (dot(resultBone.quat, q) &lt; 0)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    q = -q;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">resultBone.translate += t * bw;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">resultBone.quat += q * bw;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">resultBone.scale += s * bw;</span><br></div></code></pre></div></div>
<p>Теперь можно перевести в матрицу трансформации. Стоп. Про иерархию костей забыли совсем.
По данным из скелета строим массив индексов, где ячейка с индексом кости содержит индекс своего parent'a. В root записываем -1.</p>
<p>Пример:</p>
<p><img decoding="async" loading="lazy" alt="Skeleton" src="https://romanmarchenko.me/assets/images/skeleton-0652b318329c79c8ebecd4be485b1568.jpg" width="521" height="379" class="img_ev3q"></p>
<div class="language-cs codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-cs codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">float4x4 animMat = IdentityMatrix();</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">float4x4 mat = initialPoses[boneId];</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">while (boneId &gt;= 0)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">{</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    BoneXForm b = blendedBones[boneId];</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float4x4 xform = MakeTransformMatrix(b.translate, b.quat, b.scale);</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    animMat = mul(animMat, xform);</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    boneId = bonesHierarchyIndices[boneId];</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">}</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mat = mul(mat, animMat);</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">resultSkeletons[id] = mat;</span><br></div></code></pre></div></div>
<p>Вот, в принципе и все основные моменты просчета и блендинга анимаций.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="gpsm">GPSM<a href="https://romanmarchenko.me/ru/blog/gpubound-animations#gpsm" class="hash-link" aria-label="Direct link to GPSM" title="Direct link to GPSM" translate="no">​</a></h3>
<p>GPU Powered State Machine (вы правильно угадали). Система анимаций, описанная выше, прекрасно работала бы с Unity Animation State Machine, но в таком случае все усилия станут бесполезны. При возможностях просчета десятков (если не сотен) тысяч анимаций в кадр, UnityAnimator не вытянет и тысячи одновременно работающих стейт машин. Хм…
Что представляет собой стейт машина в Unity? Это замкнутая система состояний и переходов, которая управляется простыми численными свойствами. Каждая стейт машина работает независимо друг от друга, и по одному и тому же набору входных данных. Постойте-ка. Это же идеальная задача для GPU и compute shader'ов!</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="baking-phase">Baking phase<a href="https://romanmarchenko.me/ru/blog/gpubound-animations#baking-phase" class="hash-link" aria-label="Direct link to Baking phase" title="Direct link to Baking phase" translate="no">​</a></h4>
<p>Сначала нам надо собрать и разместить в GPU friendly структуры все данные стейт машины. А это: состояния (states), переходы (transitions) и параметры (parameters).
Все эти данные размещаются в линейные буфера, и адресуются по индексам.
Каждый compute thread считает свою стейт машину. AnimatorController предоставляет интерфейс ко всем нужным внутренним структурам стейт машины.</p>
<p>Основные структуры стейт машины:</p>
<div class="language-cs codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-cs codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">struct State</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">{</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float speed;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    int firstTransition;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    int numTransitions;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    int animDefId;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">};</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">struct Transition</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">{</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float exitTime;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float duration;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    int sourceStateId;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    int targetStateId;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    int firstCondition;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    int endCondition;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    uint properties;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">};</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">struct StateData</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">{</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    int id;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float timeInState;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float animationLoop;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">};</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">struct TransitionData</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">{</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    int id;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float timeInTransition;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">};</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">struct CurrentState</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">{</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    StateData srcState, dstState;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    TransitionData transition;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">};</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">struct AnimationDef</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">{</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    uint animId;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    int nextAnimInTree;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    int parameterIdx;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float lengthInSec;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    uint numBones;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    uint loop;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">};</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">struct ParameterDef</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">{</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float2 line0ab, line1ab;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    int runtimeParamId;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    int nextParameterId;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">};</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">struct Condition</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">{</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    int checkMode;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    int runtimeParamIndex;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float referenceValue;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">};</span><br></div></code></pre></div></div>
<ul>
<li class="">State содержит скорость с которой проигрывается состояние, и индексы условий прехода в другие согласно стейт машине.</li>
<li class="">Transition содержит индексы состояния «из» и «в». Время перехода, время выхода и ссылку на массив условий входа в это состояние.</li>
<li class="">CurrentState — это runtime data block с данными о текущем состояние стейт машины.</li>
<li class="">AnimationDef содержит описание анимации со ссылками на другие, связанные с ней, по BlendTree.</li>
<li class="">ParameterDef — это описание параметра управляющего поведением стейт машины. Line0ab и Line1ab — это коэффициенты уравнения прямых, для определения веса анимации по значению параметра. Отсюда:</li>
</ul>
<p><img decoding="async" loading="lazy" alt="LineAB" src="https://romanmarchenko.me/assets/images/line01ab-85addec7cf14c8f7fb35641e399710ba.jpg" width="579" height="363" class="img_ev3q"></p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="runtime-phase">Runtime Phase<a href="https://romanmarchenko.me/ru/blog/gpubound-animations#runtime-phase" class="hash-link" aria-label="Direct link to Runtime Phase" title="Direct link to Runtime Phase" translate="no">​</a></h4>
<p>Главный цикл каждой стейт машины можно отобразить с помощью следующего алгоритма:</p>
<p><img decoding="async" loading="lazy" alt="GPSM" src="https://romanmarchenko.me/assets/images/gpsm-63427ecd3554228231a9bcb290c26c0e.jpg" width="441" height="791" class="img_ev3q"></p>
<p>В Unity animator 4 типа параметров: float, int, bool и trigger (которая bool). Всех их мы представим как float. При настройке условий есть возможность выбрать один из шести типов сравнения. If == Equals. IfNot == NotEqual. Так что мы будем использовать только 4. Индекс оператора передаем в поле checkMode структуры Condition.</p>
<div class="language-cs codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-cs codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">for (int i = t.firstCondition; i &lt; t.endCondition; ++i)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">{</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    Condition c = allConditions[i];</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float paramValue = runtimeParameters[c.runtimeParamIndex];</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    switch (c.checkMode)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    case 3:	if (paramValue &lt; c.referenceValue) return false;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    case 4:	if (paramValue &gt; c.referenceValue) return false;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    case 6:	if (abs(paramValue - c.referenceValue) &gt; 0.001f) return false;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    case 7:	if (abs(paramValue - c.referenceValue) &lt; 0.001f) return false;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">}</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">return true;</span><br></div></code></pre></div></div>
<p>Чтобы начать переход, необходимо, чтобы все условия были true. Странные case labels — это просто (int)AnimatorConditionMode. Interruption logic это хитрая логика прерывания и откатывания перехода.</p>
<p>После того как мы обновили состояние стейт машины и прокрутили time stamp'ы на delta t кадра, пора подготовить данные про то, какие анимации следует считать в этом кадре, и соответствующие веса. Этот этап пропускается, если моделька юнита не в кадре (Frustum culled). Зачем нам считать анимации того, чего не видно? Пробегаемся по blend tree source state, по blend tree destination state, добавляем все анимации из них, а веса считаем по нормализированному времени перехода из source в destination (времени пребывания в transition). C подготовленными данными в дело вступает GPAS, и считает анимации для каждой анимированной сущности в игре.</p>
<p>Из логики управления юнитами поступают параметры управления стейт машиной. Например надо включить бег, устанавливаем параметр CharSpeed, а правильно настроенная стейт машина плавно сблендит анимации перехода из «ходьбы» в «бег».</p>
<p>Естественно, что полной аналогии c Unity Animator не получилось. Внутренние принципы работы, если они не описаны в документации, приходилось реверсить и делать аналог. Некоторый функционал еще не доделан (может быть и не будет). Например BlendType в BlendTree поддерживается только 1D. Сделать другие типы, в принципе, не сложно, просто это сейчас не нужно. Нет animation event'ов, так как надо делать readback c GPU, а «правильный» readback будет отставать на несколько кадров, что не всегда приемлемо. Но тоже можно.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="рендер">Рендер<a href="https://romanmarchenko.me/ru/blog/gpubound-animations#%D1%80%D0%B5%D0%BD%D0%B4%D0%B5%D1%80" class="hash-link" aria-label="Direct link to Рендер" title="Direct link to Рендер" translate="no">​</a></h3>
<p>Рендер юнитов осуществляется с помощью инстансинга. По SV_InstanceID, в вертексном шейдере, получаем матрицы всех костей которые влияют на вертекс, и трансформируем его. Абсолютно ничего необычного:</p>
<div class="language-cs codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-cs codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">float4 ApplySkin(float3 v, uint vertexID, uint instanceID)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">{</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    BoneInfoPacked bip = boneInfos[vertexID];</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    BoneInfo bi = UnpackBoneInfo(bip);</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    SkeletonInstance skelInst = skeletonInstances[instanceID];</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    int bonesOffset = skelInst.boneOffset;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float4x4 animMat = 0;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    for (int i = 0; i &lt; 4; ++i)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        float bw = bi.boneWeights[i];</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        if (bw &gt; 0)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        {</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            uint boneId = bi.boneIDs[i];</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            float4x4 boneMat = boneMatrices[boneId + bonesOffset];</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            animMat += boneMat * bw;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        }</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    float4 rv = float4(v, 1);</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    rv = mul(rv, animMat);</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    return rv;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">}</span><br></div></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="итоги">Итоги<a href="https://romanmarchenko.me/ru/blog/gpubound-animations#%D0%B8%D1%82%D0%BE%D0%B3%D0%B8" class="hash-link" aria-label="Direct link to Итоги" title="Direct link to Итоги" translate="no">​</a></h3>
<p>Быстро ли все это хозяйство работает? Явно медленнее, чем семплинг текстурки с матрицами, но, все же, кое-какие цифры я могу показать (GTX 970).</p>
<p>Вот 50000 стейтмашин:</p>
<p><img decoding="async" loading="lazy" alt="CSStats" src="https://romanmarchenko.me/assets/images/statemachine_cs_stats-be9d4e7f62f1f5e8606db83f478d5e33.jpg" width="795" height="206" class="img_ev3q"></p>
<p>Вот 280000 анимированных костей:</p>
<p><img decoding="async" loading="lazy" alt="CSStats2" src="https://romanmarchenko.me/assets/images/animcompute_cs_stats-33b33e203e225d55f163ac27a86eab48.jpg" width="915" height="461" class="img_ev3q"></p>
<p>Разработка и отладка всего этого настоящая боль. Куча буферов и смещений. Куча компонентов и их взаимодействия. Бывало руки опускались, когда бъешься несколько дней о проблему головой, а найти, в чем неполадка, не можешь. Особенно «приятно», когда на тестовых данных все работает как надо, а в реальной «боевой» ситуации нет, да проскочит какой-то глюк анимации. Несоответствия работы стейтмашин Unity и своей, тоже не сразу видны. В общем, если решите сделать у себя аналог, то я вам не завидую. Собственно, вся разработка под GPU она такая, чего жаловаться.</p>
<p><strong>P.S.</strong> Хочется кинуть камень в огород разработчиков Unite TechDemo. У них на сцене большое количество одинаковых моделей руин и мостов, а они никак не оптимизировали их отрисовку. Вернее они попытались, поставив галочку «static». Только вот, в 16 битные индексы не запихнешь много геометрии (три раза хаха, 2017 год) и ничего не объеденилось, так как модели высокополигональные. Я проставил всем шейдерам «Enable Instancing», и снял галку «Static». Ощутимого буста не произошло, но, блин, вы же технодемку делаете, боретесь за каждый фпс. Нельзя так лажать.</p>
<details class="details_lb9f alert alert--info details_b_Ee" data-collapsed="true"><summary>Было</summary><div><div class="collapsibleContent_i85q"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">*** Summary ***</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Draw calls: 2553</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Dispatch calls: 0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">API calls: 8378</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  Index/vertex bind calls: 2992</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  Constant bind calls: 648</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  Sampler bind calls: 395</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  Resource bind calls: 805</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  Shader set calls: 682</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  Blend set calls: 230</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  Depth/stencil set calls: 92</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  Rasterization set calls: 238</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  Resource update calls: 1017</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  Output set calls: 74</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">API:Draw/Dispatch call ratio: 3.28163</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">298 Textures - 1041.01 MB (1039.95 MB over 32x32), 42 RTs - 306.94 MB.</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Avg. tex dimension: 1811.77x1810.21 (2016.63x2038.98 over 32x32)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">216 Buffers - 180.11 MB total 17.54 MB IBs 159.81 MB VBs.</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">1528.06 MB - Grand total GPU buffer + texture load.</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">*** Draw Statistics ***</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Total calls: 2553, instanced: 2, indirect: 2</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Instance counts:</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  1:  </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  2:  </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  3:  </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  4:  </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  5:  </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  6:  </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  7:  </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  8:  </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  9:  </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  10:  </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  11:  </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  12:  </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  13:  </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  14:  </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">&gt;=15: ******************************************************************************************************************************** (2)</span><br></div></code></pre></div></div></div></div></details>
<details class="details_lb9f alert alert--info details_b_Ee" data-collapsed="true"><summary>Стало</summary><div><div class="collapsibleContent_i85q"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">*** Summary ***</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Draw calls: 1474</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Dispatch calls: 0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">API calls: 11106</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  Index/vertex bind calls: 3647</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  Constant bind calls: 1039</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  Sampler bind calls: 348</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  Resource bind calls: 718</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  Shader set calls: 686</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  Blend set calls: 230</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  Depth/stencil set calls: 110</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  Rasterization set calls: 258</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  Resource update calls: 1904</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  Output set calls: 74</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">API:Draw/Dispatch call ratio: 7.5346</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">298 Textures - 1041.01 MB (1039.95 MB over 32x32), 42 RTs - 306.94 MB.</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Avg. tex dimension: 1811.77x1810.21 (2016.63x2038.98 over 32x32)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">427 Buffers - 93.30 MB total 9.81 MB IBs 80.51 MB VBs.</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">1441.25 MB - Grand total GPU buffer + texture load.</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">*** Draw Statistics ***</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Total calls: 1474, instanced: 391, indirect: 2</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Instance counts:</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  1:  </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  2: ******************************************************************************************************************************** (104)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  3: ************************************************* (40)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  4: ********************** (18)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  5: ****************************** (25)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  6: ********************************************************************************************* (76)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  7: *********************************** (29)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  8: ************************************************** (41)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  9: ********* (8)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  10: ************** (12)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  11:  </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  12: ****** (5)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  13: ******* (6)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  14: ** (2)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">&gt;=15: ****************************** (25)</span><br></div></code></pre></div></div></div></div></details>
<p><strong>P.P.S.</strong> Во все времена игры были, в основном, CPU bound, т.е. CPU не успевал за GPU. Слишком много логики и физики. Пренося часть игровой логики с CPU на GPU, мы разгружаем первый и нагружаем второй, т.е. делаем ситуацию GPU bound более вероятной. Отсюда и название статьи.</p>]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[В этой статье слишком много воды]]></title>
        <id>https://romanmarchenko.me/ru/blog/water</id>
        <link href="https://romanmarchenko.me/ru/blog/water"/>
        <updated>2019-05-14T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[«Мы начинаем разработку новой игры, и нам нужна классная вода. Такую сможешь?»]]></summary>
        <content type="html"><![CDATA[<p>«Мы начинаем разработку новой игры, и нам нужна классная вода. Такую сможешь?»</p>
<iframe width="660" height="415" src="https://www.youtube.com/embed/iilqtDkeIBE?si=abNdHxrdrQ5MooOL" title="YouTube video player" frameborder="0" allow="fullscreen; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin"></iframe>
<p>— cпросили меня. «Да не вопрос! Конечно, смогу», — ответил я, но голос предательски задрожал. «А, еще и на Unity?», — и мне стало понятно, что впереди очень много работы.</p>
<p>Итак, водичка. Unity до того момента я в глаза не видел, ровно как и C#, так что решил, что буду делать прототип на знакомых мне инструментах: С++ и DX9. Что я знал и умел на практике на тот момент так это скроллящиеся текстурки нормалей для формирования поверхности, и примитивный дисплейсмент маппинг на их основе. Тут же надо было менять абсолютно все. Реалистичная анимированная форма водной поверхности. Усложненый (сильно) шейдинг. Генерация пены. LOD система, привязанная к камере. Начал я выискивать информацию в интернете как же все это сделать то.</p>
<p>Первым пунктом, естественно, было вникание в <a href="http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.161.9102&amp;rep=rep1&amp;type=pdf" target="_blank" rel="noopener noreferrer" class="">«Simulating Ocean Water»</a> <a href="http://evasion.imag.fr/~Fabrice.Neyret/images/fluids-nuages/waves/Jonathan/articlesCG/waterslides2001.pdf" target="_blank" rel="noopener noreferrer" class="">Джерри Тессендорфа</a>.</p>
<p>Академические пейперы с кучей заумных формул никогда мне особо не давались, так что после пары прочтений я мало что понял. Общие принципы были понятны: каждый кадр генерируется карта высот с помощью Fast Fourier Transform, которая, как функция от времени, плавно меняет свою форму формируя реалистичную водную поверхность. Но как и что считать я не знал. Я потихоньку вникал в премудрости просчета FFT на шейдерах в D3D9, и мне в этом очень помог исходник со статьей где-то в дебрях интернета, который я битый час пытался отыскать, но безуспешно (к сожалению). Первый результат был получен (страшный как ядерная война):</p>
<iframe width="660" height="415" src="https://www.youtube.com/embed/tdfwF1R81O4?si=6i51ujD-8Gf-W0Xx" title="YouTube video player" frameborder="0" allow="fullscreen; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin"></iframe>
<p>Стартовые успехи порадовали, и начался перенос воды на Unity с его доработкой.</p>
<p>К воде в игре про морские битвы выдвигались несколько требований:</p>
<p>Реалистичный внешний вид. Красивые как близкие так и дальние ракурсы, динамическая пена, скаттеринг и т. д.
Поддержка различных погодных условий: штиль, шторм и промежуточные состояния. Смена времени суток.
Физика плавучести кораблей по симулированной поверхности, плавучие объекты.
Так как игра мультиплеерная, вода должна быть у всех участников боя одинаковая.
Рисование по поверхности: нарисованные зоны полета ядер залпа, пена от попаданий ядер в воду.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="геометрия">Геометрия<a href="https://romanmarchenko.me/ru/blog/water#%D0%B3%D0%B5%D0%BE%D0%BC%D0%B5%D1%82%D1%80%D0%B8%D1%8F" class="hash-link" aria-label="Direct link to Геометрия" title="Direct link to Геометрия" translate="no">​</a></h3>
<p>Решено было построить quadtree-like структуру, с центром вокруг камеры, которая дискретно перестраивается при движении наблюдателя. Почему дискретно? Если двигать меш плавно вместе с камерой или использовать screen space reprojection как в статье Real-time water rendering — introducing the projected grid concept, то на дальних планах из-за недостаточного разрешения геометрической сетки при выборке карты высот волны будут «скакать» полигоны вверх и вниз. Это очень сильно бросается в глаза. Картинка «рябит». Чтобы это побороть, надо либо сильно увеличивать разрешение полигональной сетки water mesh'a, либо «уплощать» геометрию на дальних дистанциях, либо так строить и двигать полигоны, чтобы эти сдвиги не было видно. Вода у нас прогрессивная (хехе) и выбрал я третий путь. Как и в любой подобной технике (особенно знакомой всем, кто создавал terrain в играх), необходимо избавиться от T-junctions на границах переходов уровней детализации. Для решения этой задачи на старте предрасчитывается 3 вида квадов с заданными параметрами тесселяции:</p>
<p><img decoding="async" loading="lazy" alt="Water Grid" src="https://romanmarchenko.me/assets/images/watergrid-ca3aef7f3d95eec16903748b32ea472e.jpg" width="2748" height="843" class="img_ev3q"></p>
<p>Первый тип для тех квадов, которые не являются переходными на более низкую детализацию. Ни одна из сторон не имеет уменьшенное в 2 раза количество вершин. Второй тип для граничных, но не угловых квадов. Третий тип — угловые граничные квады. Финальный меш для воды строится поворотом и масштабированием этих трех видов мешей.</p>
<p>Вот так выглядит рендер с подсветкой разным цветом LOD уровней воды:</p>
<iframe width="660" height="415" src="https://www.youtube.com/embed/JycUZlod3xs?si=C45xX2NXehjFAkMl" title="YouTube video player" frameborder="0" allow="fullscreen; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin"></iframe>
<p>На первых кадрах видно соединение двух различных уровней детализации.</p>
<p>Видео как кадр заполняется водяными квадами:</p>
<iframe width="660" height="415" src="https://www.youtube.com/embed/VldNrYKLb0w?si=hbZnyKDUKV5XHocJ" title="YouTube video player" frameborder="0" allow="fullscreen; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin"></iframe>
<p>Напомню, это все было давно (и неправда). Сейчас более оптимально и гибко можно сделать сразу на GPU (GPU Pro 5. Quadtrees on the GPU). И рисовать будет в один draw call, и тесселяцией можно поднять детализацию.</p>
<p>Позднее проект переехал на D3D11, но до апгрейда этой части рендера океана руки так и не дошли.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="генерация-формы-волны">Генерация формы волны<a href="https://romanmarchenko.me/ru/blog/water#%D0%B3%D0%B5%D0%BD%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D1%8F-%D1%84%D0%BE%D1%80%D0%BC%D1%8B-%D0%B2%D0%BE%D0%BB%D0%BD%D1%8B" class="hash-link" aria-label="Direct link to Генерация формы волны" title="Direct link to Генерация формы волны" translate="no">​</a></h3>
<p>Вот для этого нам понадобится Fast Fourier Transform. Для выбранного (нужного) разрешения текстуры волны (пока назовем ее так, далее я объясню, какие данные там хранятся) подготавливаем начальные данные, используя параметры, заданные художниками (сила, направление ветра, зависимость волны от направления ветра и другие). Все это необходимо скормить в формулы т.н. Phillips spectrum'а. Полученные начальные данные модифицируем каждый кадр с учетом времени и выполняем FFT над ними. На выходе получаем тайлящуюся по всем направлениям текстуру которая содержит смещение вершин плоского меша. Почему не просто heightmap? Если хранить только оффсет по высоте, то результатом будет нереалистичная «бурлящая» масса, лишь отдаленно напоминающая море:</p>
<iframe width="660" height="415" src="https://www.youtube.com/embed/2234r1xpuI8?si=-Z4R46TU-TzMnlq-" title="YouTube video player" frameborder="0" allow="fullscreen; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin"></iframe>
<p>Если считать смещения для всех трех координат, то будут генерироваться красивые «острые» реалистичные волны:</p>
<iframe width="660" height="415" src="https://www.youtube.com/embed/tFhKaV1u-AY?si=dBnpxhYolDIGdUnj" title="YouTube video player" frameborder="0" allow="fullscreen; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin"></iframe>
<p>Одной анимированной текстурки мало. Виден тайлинг, недостаточно деталей на ближних планах. Берем описанный алгоритм и делаем не одну, а 3 fft-generated текстуры. Первая — крупные волны. Она задает основную форму волны и используется для физики. Вторая — средние волны. Ну и напоследок самые мелкие. 3 FFT генератора (4-й вариант это финальный микс):</p>
<iframe width="660" height="415" src="https://www.youtube.com/embed/gqxbAOS4sGM?si=cheJnOLG2LWBFe5x" title="YouTube video player" frameborder="0" allow="fullscreen; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin"></iframe>
<p>Параметры слоев задаются независимо друг от друга, и полученные текстуры смешиваются в шейдере воды в финальную форму волны. Параллельно со смещениями генерируются и карты нормалей каждого слоя.</p>
<p>«Одинаковость» воды у всех участников боя обеспечивается синхронизацией параметров океана на старте боя. Эту информацию передает сервер каждому клиенту.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="физическая-модель-плавучести">Физическая модель плавучести<a href="https://romanmarchenko.me/ru/blog/water#%D1%84%D0%B8%D0%B7%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B0%D1%8F-%D0%BC%D0%BE%D0%B4%D0%B5%D0%BB%D1%8C-%D0%BF%D0%BB%D0%B0%D0%B2%D1%83%D1%87%D0%B5%D1%81%D1%82%D0%B8" class="hash-link" aria-label="Direct link to Физическая модель плавучести" title="Direct link to Физическая модель плавучести" translate="no">​</a></h3>
<p>Так как необходимо было сделать не только красивую картинку, но и реалистичное поведение кораблей. А также учитывая то, что в игре должно присутствовать штормовое море (крупные волны), то еще одной задачей, которую требовалось решить, являлось обеспечение плавучести объектов на поверхности сгенерированного моря. Сперва я попытался сделать GPU readback текстуры волны. Но, так как быстро выяснилось, что всю физику морского боя необходимо делать на сервере, то и море, а точнее первый его слой который задает форму волны, необходимо считать также и на сервере (а на нем, скорее всего, нет быстрого и/или совместимого GPU), то было принято решение написать полную функциональную копию GPU FFT генератора, на CPU в виде native C++ плагина к Unity. Сам FFT алгоритм я не реализовывал и использовал готовый в библиотеке Intel Performance Primitives (IPP). А вот всю обвязку и постпроцессинг результатов был выполнен мной, с последующей оптимизацией на SSE и распараллеливанием по потокам. Сюда входила и подготовка массива данных для FFT каждый кадр, и финальное преобразование посчитанных значений в wave offset map.</p>
<p>Была еще одна интересная особенность алгоритма, которая исходила из требований к физике воды. Нужна была функция быстрого получения высоты волны в данной точке мира. Логично, ведь это и есть основа построения плавучести любого объекта. Но, так как на выходе FFT процессора у нас получалается offsetmap, а не heightmap, то обычная выборка из текстуры не давала нам высоту волны там где было необходимо. Для простоты рассмотрим 2D вариант:</p>
<p><img decoding="async" loading="lazy" alt="Surface Wave" src="https://romanmarchenko.me/assets/images/surface_wave-1a91c6d13ccce5b71fdef6b2f264475a.jpg" width="1600" height="1296" class="img_ev3q"></p>
<p>Для формирования волны, тексели (текстурные элементы, показанные вертикальными линиями) содержат вектор (стрелки) который задает смещение вертекса плоского меша (синие точки) в направлении его финальной позиции (острие стрелки). Предположим мы возьмем эти данные и попробуем извлечь из нее высоту воды в интересующей нас точке. Например, нам надо узнать высоту в точке hB. Если мы возьмем вектор в текселе tB, то мы получим смещение в точку около hC, что может сильно отличаться от того что нам нужно. Вариантов решения этой проблемы два: при каждом запросе высоты проверять множество соседних текселей, пока не найдем тот, который имеет смещение в интересующую нас позицию. В нашем примере мы найдем тексель tA как содержащий наиболее близкое смещение. Но такой подход не назовешь быстрым. Сканирование радиуса текселей непонятно какого размера (а от того, штормовое море или спокойное, смещения могут сильно варьироваться) может занять продолжительное время.</p>
<p>Второй вариант — после просчета offset map конвертировать ее в height map, используя scattering подход. Это означает, что для каждого offset vector'а мы запишем высоту волны, которую он задает, в ту точку, куда он смещается. Это будет отдельный массив данных, который и будет использоваться для получения высоты в интересующей точке. Используя нашу иллюстрацию, ячейка tB будет содержать высоту hB полученную из вектора tA→hB. Есть еще одна особенность. Ячейка tA не будет содержать валидного значения, так как нет вектора, смещающегося в него. Для заполнения таких «дырок» выполняется проход заполнения их соседними значениями.</p>
<p>Вот так это выглядит, если сделать визуализацию смещений с помощью векторов (красные — большое смещение, зеленый — малое):</p>
<iframe width="660" height="415" src="https://www.youtube.com/embed/uBk8F55lR74?si=whSCwaA7GDfOOy9T" title="YouTube video player" frameborder="0" allow="fullscreen; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin"></iframe>
<p>Далее все просто. Для корабля задается плоскость условной ватерлинии. На ней определяется прямоугольная сетка точек-проб, которая задает места приложения выталкивающих из воды сил для корабля. Затем для каждой точки проверяем, под водой она или нет, используя water heightmap, описанную выше. Если точка под водой, то прикладываем вертикальную силу вверх к physics hull корпуса в этой точке, масштабированной расстоянием от точки до водной поверхности. Если над водой, то ничего не делаем, гравитация сделает все для нас. На самом деле там формулы немного сложнее (вся для тонкого тюнига поведения корабля), но основной принцип такой. На видео визуализации плавучести ниже, синие кубы — это места расположения проб, а линии от них вниз — это величина выталкивающей из воды силы.</p>
<iframe width="660" height="415" src="https://www.youtube.com/embed/mwNRIKbRdpQ?si=yNDXCoh9gwa8yirl" title="YouTube video player" frameborder="0" allow="fullscreen; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin"></iframe>
<p>В реализации сервера есть еще один интересный оптимизационный момент. Нет никакой надобности симулировать разную воду для разных боевых инстансов, если они проходят в одинаковых погодных условиях (одинаковые параметры FFT симулятора). Так что логичным решением было сделать пул симуляторов, к которым боевые инстансы выполняют запросы на получение симулированной воды с заданными параметрами. Если параметры одинаковые от нескольких инстансов, то им вернется одна и та же вода. Реализовано это с помощью Memorу Mapped File API. Когда FFT симулятор создается, он дает доступ к своим данным, экспортируя дескрипторы нужных блоков. Серверный инстанс вместо того, чтобы запускать реальный симулятор, запускает «пустышку» которая просто отдает данные, открытые по этим дескрипторам. Было несколько веселых багов, связанных с этим функционалом. Из-за ошибок подсчета ссылок симулятор уничтожался, но memory mapped file жив пока открыт хоть один дескриптор на него. Данные переставали обновляться (симулятора-то нет) и вода «останавливалась».</p>
<p>На клиентской стороне нам необходима информация о форме волны для просчета попаданий ядер в волну и проигрывания систем частиц и пены. Просчет повреждений происходит на сервере и там также необходимо корректно определять, попало ли ядро в воду (волна может закрывать корабль, особенно в штормах). Тут уже необходимо делать heightmap tracing по аналогии как это делается в parallax mapping либо SSAO эффектах.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="шейдинг">Шейдинг<a href="https://romanmarchenko.me/ru/blog/water#%D1%88%D0%B5%D0%B9%D0%B4%D0%B8%D0%BD%D0%B3" class="hash-link" aria-label="Direct link to Шейдинг" title="Direct link to Шейдинг" translate="no">​</a></h3>
<p>В принципе как и везде. Отражения, преломления, subsurface scattering хитро замешиваем, учитывая глубину дна, учитываем fresnel effect, считаем спекуляр. Скаттеринг считаем для гребней в зависимости от позиции солнышка. Пена генерируется следующим образом: создаем «пятно пены» на гребнях волн (используем высоту как метрику), затем накладываем новосозданные пятна на пятна с предыдущих кадров одновременно уменьшая их интенсивность. Таким образом получаем размазывание пятен пены в виде хвоста от идущего гребня волны.</p>
<iframe width="660" height="415" src="https://www.youtube.com/embed/MyzFcwr641g?si=F-9dsyxQCNHkYf4z" title="YouTube video player" frameborder="0" allow="fullscreen; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin"></iframe>
<p>Используем полученную текстуру «пятен» как маску к которой примешиваем текстуры пузырьков, разводов и т. д. Получаем довольно реалистичный динамический рисунок пены на поверхности волн. Данная маска создается для каждого FFT слоя (напомню, у нас их 3), и в финальном миксе они все смешиваются.</p>
<p>На видео выше визуализация маски пены. Первый и второй слои. Я модифицирую параметры генератора и результат виден на текстуре.</p>
<p>И видео немножко коряво настроенного штормового моря. Тут хорошо видна форма волны, возможности генератора и пена:</p>
<iframe width="660" height="415" src="https://www.youtube.com/embed/MAPUbmV4BAA?si=VKAHXUhTc8i2XRL7" title="YouTube video player" frameborder="0" allow="fullscreen; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin"></iframe>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="рисование-по-водной-поверхности">Рисование по водной поверхности<a href="https://romanmarchenko.me/ru/blog/water#%D1%80%D0%B8%D1%81%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5-%D0%BF%D0%BE-%D0%B2%D0%BE%D0%B4%D0%BD%D0%BE%D0%B9-%D0%BF%D0%BE%D0%B2%D0%B5%D1%80%D1%85%D0%BD%D0%BE%D1%81%D1%82%D0%B8" class="hash-link" aria-label="Direct link to Рисование по водной поверхности" title="Direct link to Рисование по водной поверхности" translate="no">​</a></h3>
<p>Картинка-пример использования:</p>
<p><img decoding="async" loading="lazy" alt="WSO" src="https://romanmarchenko.me/assets/images/wso-379da6fdf2a04db6edee90d966e1efe4.jpg" width="1266" height="720" class="img_ev3q"></p>
<p>Используется для:</p>
<ul>
<li class="">Маркеры, визуализации зоны разлета ядер.</li>
<li class="">Рисование пены в месте попадания ядер в воду.</li>
<li class="">Пенный след за кораблем</li>
<li class="">Выдавливание воды под кораблем, чтобы убрать эффект заливания волнами палубы и затопленного трюма.</li>
</ul>
<p>Очевидный базовый вариант — проективное текстурирование. Оно и было реализовано. Но тут появились дополнительные требования. Ближние виды — мыло из-за недостаточного разрешения (можно увеличивать, но не бесконечно), и хочется чтобы далеко было видно эти проективные рисунки на воде. Где решается такая же задача? Правильно, в тенях (shadow map). Как она там решается? Правильно, Cascaded (Parallel Split) Shadow Maps. Возьмем и мы эту технологию на вооружение и применим к нашей задаче. Разбиваем фрустум камеры на N (3-4 обычно) сабфрустумов. Для каждого строим описывающий прямоугольник в горизонтальной плоскости. Для каждого такого прямоугольника строим orthographic projection матрицу и рисуем все интересующие объекты для каждой из N таких ortho камер. Каждая такая камера рисует в отдельную текстуру, а затем, в шейдере океана, мы их комбинируем в одну цельную проективную картинку.</p>
<iframe width="660" height="415" src="https://www.youtube.com/embed/etG8tddmbmk?si=04ZYEiAQ0rpwiYS-" title="YouTube video player" frameborder="0" allow="fullscreen; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin"></iframe>
<p>Вот я положил на море большущую плоскость с текстурой флагов:</p>
<p><img decoding="async" loading="lazy" alt="WSO Flags" src="https://romanmarchenko.me/assets/images/wso_flags-e5b5e522248984b59bca0177d0796362.jpg" width="1556" height="871" class="img_ev3q"></p>
<p>Вот то что содержится в сплитах:</p>
<p><img decoding="async" loading="lazy" alt="WSO Splits" src="https://romanmarchenko.me/assets/images/flag_splits-26ac61c4768e94986e842a36cd390542.jpg" width="2000" height="667" class="img_ev3q"></p>
<p>Кроме обычных картинок надо абсолютно таким же образом нарисовать дополнительную маску пены (для следов кораблей и мест попаданий ядер), а также маску выдавливания воды под кораблями. Это много камер и много проходов. Поначалу оно так тормозно и работало, но затем, после перехода на D3D11, с помощью «размножения» геометрии в геометрическом шейдере и рисования каждой копии в отдельный render target через SV_RenderTergetArrayIndex, удалось сильно ускорить этот эффект.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="улучшения-и-модернизации">Улучшения и модернизации<a href="https://romanmarchenko.me/ru/blog/water#%D1%83%D0%BB%D1%83%D1%87%D1%88%D0%B5%D0%BD%D0%B8%D1%8F-%D0%B8-%D0%BC%D0%BE%D0%B4%D0%B5%D1%80%D0%BD%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D0%B8" class="hash-link" aria-label="Direct link to Улучшения и модернизации" title="Direct link to Улучшения и модернизации" translate="no">​</a></h3>
<p>D3D11 очень сильно развязывает руки во многих моментах. После перехода на него и Unity 5 я сделал FFT генератор на сompute шейдерах. Визуально ничего не поменялось, но стало чуточку быстрее. Перевод просчет текстуры отражений с отдельного полноценного рендера камеры на технологию Screen Space Planar Reflections дал неплохой буст производительности. Про оптимизацию water surface objects я писал выше, а до перевода mesh'а на GPU Quadtree руки так и не дошли.</p>
<p>Многое, возможно, можно было сделать оптимальнее и проще. Например, не городить огороды с CPU симулятором, а просто запустить GPU вариант на сервере с WARP (программным) d3d девайсом. Массивы данных там не очень большие.</p>
<p>Ну, в общем как-то так. В то время, как разработка начиналось, все это было современно и круто. Сейчас уже местами подустарело. Появилось больше доступных материалов, даже есть похожий аналог на github: Crest. В большинстве игр, где есть моря, используется похожий подход.</p>]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[Рендер Diablo III.]]></title>
        <id>https://romanmarchenko.me/ru/blog/d3</id>
        <link href="https://romanmarchenko.me/ru/blog/d3"/>
        <updated>2012-10-05T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Как устроены графические движки популярных игр с мировым именем? Какие технологии применяют разработчики в самых крупных игровых компаниях? Действительно ли, чтобы сделать красивую игровую графику необходимо применять самые передовые технологии современной 3D графики? На эти вопросы мы попробуем ответить на примере рендер части игры Diablo3, от компании Blizzard Entertainment.]]></summary>
        <content type="html"><![CDATA[<p>Как устроены графические движки популярных игр с мировым именем? Какие технологии применяют разработчики в самых крупных игровых компаниях? Действительно ли, чтобы сделать красивую игровую графику необходимо применять самые передовые технологии современной 3D графики? На эти вопросы мы попробуем ответить на примере рендер части игры Diablo3, от компании Blizzard Entertainment.</p>
<p>Я давно занимаюсь в сфере игровой разработки, и мое хобби реверс-инжиниринг графических движков популярных игровых продуктов. Когда вышел долгожданный сиквел серии Diablo, я сразу захотел узнать, какие технологии использовали разработчики в своем детище.</p>
<p>Рендер игры построен на базе технологии Direct3D 9. Это позволяет покрыть более широкую аппаратную базу видеокарт, а те расширенные возможности, которые предлагает D3D 10 и 11 зачастую либо вовсе не нужны, либо реализуемы теми или иными способами в девятой версии.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="тени">Тени<a href="https://romanmarchenko.me/ru/blog/d3#%D1%82%D0%B5%D0%BD%D0%B8" class="hash-link" aria-label="Direct link to Тени" title="Direct link to Тени" translate="no">​</a></h3>
<p>Для всей статической геометрии уровня используются предрасчитанные лайтмапы. Да-да, старый добрый способ, который применяется со времен, когда 3D-акселераторы стали поддерживать мультитекстурирование.</p>
<p><img decoding="async" loading="lazy" alt="LM" src="https://romanmarchenko.me/assets/images/lightmap-ef2f884b01aec34799709f01c961980d.jpg" width="1024" height="1024" class="img_ev3q"></p>
<p>Лайтмапа просчитывается заранее в пакете 3Д моделирования (3ds max, Maya), либо собственным рейтрейсером в редакторе уровней. Одна такая текстура используется для нескольких игровых объектов, либо их частей, если объект крупный (например, террейн).</p>
<p>Для динамических объектов (монстры, фигурки персонажа) используются динамические тени выполненные по технологии “shadow map” (stencil shadows в наше время уже практически не используется). Разработчики решили отступить от классических канонов в этой сфере, и не использовали hardware shadows (текстуры которые могут быть использованы как буфера глубины, и поддерживающие аппаратный Percentage Closer Filtering – PCF), которые предлагают все популярные производителями видеокарт. Вместо этого, была применена технология Variance Shadow Maps. Она позволяет получить мягкие края путем стандартного размытия текстуры тени (для классических карт теней этот метод неприменим, так как усреднение значений глубины пикселя не имеет смысла). Подробности VSM я расписывать не буду (см. полезные ссылки в конце статьи), скажу лишь только, что для ее реализации необходимо хранить 2 значения: глубину пикселя и глубину пикселя в квадрате. Именно второе значение диктует довольно жесткие условия к точности хранения этой информации, поэтому была выбрана текстура формата A32B32G32R32 float. Размер ее при максимальных настройках качества теней 2048х2048.</p>
<p><img decoding="async" loading="lazy" alt="Dynamic SM" src="https://romanmarchenko.me/assets/images/dynamic_sm-8394ef1b550cf27f141f5626d8757384.jpg" width="1024" height="1024" class="img_ev3q"></p>
<p>Процесс создания карты теней стандартный. Рисуем все объекты отбрасывающие тень (окклюдеры) в карту тени с позиции источника освещения. Размываем карту тени сначала по горизонтали, а затем по вертикали. При рендере объектов, которые должны получать тень (ресиверы), семплим шадоу мапу, определяем степень освещенности пикселя и соответственным образом затемняем финальный цвет. Семплинг карты теней должен происходить с билинейной фильтрацией. Аппаратная фильтрация формата A32B32G32R32F поддерживается далеко не всей линейкой shader model 3.0 capable видеокарт, поэтому она реализуется программно, в шейдере (хотя на моей видеокарте она поддерживается, но это не учли).</p>
<p>Рендеринг теней происходит с ортографической проекцией, если тень от направленного (directional) источника (солнце), либо с перспективной для конусовидных (omni). Техники перспективного искажения карты теней (например, Perspective Shadow Maps,Trapezoidal Shadow Maps и др.) для положения камеры используемой в игре (направление взгляда сверху вниз, под небольшим углом к направлению главного источнику освещения) не нужны и не используются. Каскадное разделение теней на сектора (Cascaded Shadow Maps, Parallel Split Shadow Maps) не реализованы по тем же причинам.
Карта тени в разрешении 512х512 со сглаживанием и без:</p>
<p><img decoding="async" loading="lazy" alt="SM" src="https://romanmarchenko.me/assets/images/sm-a1cb5519d34373513fe6c7d37bc68b31.jpg" width="570" height="396" class="img_ev3q"></p>
<p>Шейдер для патча террейна в варианте со сглаживанием состоит из 12 текстурных и 59 арифметических инструкций. Без сглаживания 10 и 29 соответственно. Разница в арифметических инструкциях есть реализация билинейной фильтрации и VSM.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="динамическое-освещение">Динамическое освещение<a href="https://romanmarchenko.me/ru/blog/d3#%D0%B4%D0%B8%D0%BD%D0%B0%D0%BC%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B5-%D0%BE%D1%81%D0%B2%D0%B5%D1%89%D0%B5%D0%BD%D0%B8%D0%B5" class="hash-link" aria-label="Direct link to Динамическое освещение" title="Direct link to Динамическое освещение" translate="no">​</a></h3>
<p>Удивительно, но все динамическое освещение повертексное. Как в старые добрые времена. Никаких карт нормалей в игре нет. Смелое решение, но учитывая финальный результат, очевидно, что он себя оправдал на 100%. Недостатка в детализации геометрии совершенно не ощущается. В вертексном шейдере реализованы один точечный источник света с квадратичной аттенюацией (как в классических FFP формулах), один цилиндрический источник (он используется как подсветка персонажа, для освещения близкого окружения героя) и до 16 точечных источников с простейшей линейной аттенюацией по расстоянию.</p>
<p>В игре реализованы также объемные источники освещения. Сделаны они следующим образом. Рисуем сферу, или другую выпуклую фигуру, в месте источника освещения. В вершинном шейдере высчитываем альфу вертексов на основе нормали и вектора направления камеры. Чем больше угол между ними, тем большая прозрачность должна быть. Получаем полупрозрачную сферу с увеличением прозрачности от центра к краям. Так как эта сфера будет пересекать геометрию уровня, мы получим визуальный артефакт изображения в месте пересечения объектов уровня и самой сферы. Данный недостаток исправляется абсолютно таким же методом, как и делаются так называемые soft particles. Берется выборка из буфера глубины и сравнивается с глубиной отрисовывамого пикселя. Если значения близки, то модифицируя альфу (уменьшая ее до нуля), мы делаем место пересечения геометрий невидимым.</p>
<p><img decoding="async" loading="lazy" alt="Volume Light" src="https://romanmarchenko.me/assets/images/volumelight-3c4dd2f5ed1c617899fe525a25a6074a.jpg" width="760" height="303" class="img_ev3q"></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="специальные-эффекты">Специальные эффекты<a href="https://romanmarchenko.me/ru/blog/d3#%D1%81%D0%BF%D0%B5%D1%86%D0%B8%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B5-%D1%8D%D1%84%D1%84%D0%B5%D0%BA%D1%82%D1%8B" class="hash-link" aria-label="Direct link to Специальные эффекты" title="Direct link to Специальные эффекты" translate="no">​</a></h3>
<p>Из интересных эффектов можно выделить проективное текстурирование. Для наложения на поверхность земли текстур различных игровых заклинаний (например, крики варвара, лужи яда, огненные дорожки монстров и т.д.) все эти эффекты рендерятся в отдельную текстуру:</p>
<p><img decoding="async" loading="lazy" alt="ProjTex" src="https://romanmarchenko.me/assets/images/projection_tex-bedf209aea00723eaa37eb315e8c4c8f.jpg" width="1024" height="1024" class="img_ev3q"></p>
<p>Затем выполняется повторный рендеринг всей геометрии, на которую должно быть выполнено проективное текстурирование с использованием построенного кумулятивного изображения с проецируемыми графическими эффектами. Смешивание изображений выполняется по альфа каналу.</p>
<p>Для некоторых эффектов (пост-процесс, в частности) необходима информация о глубине сцены в данной точке. Стандартные средства Direct3D 9 не позволяют получить буфер глубины как текстуру для последующего чтения. Очевидным вариантом будет рендеринг всей сцены еще раз, с выводом глубины пикселей в текстуру формата R32F. Этот метод в большинстве случаев неприемлем, так как удвоение отрисовываемой геометрии сильно скажется на общей производительности игры. Производители графических адаптеров давно знают об этой проблеме, и предлагают специальные форматы текстур, которые могут использоваться и как текстура в шейдере, и как буфер глубины при рендеринге. Одним из таких форматов является так называемый INTZ формат. Он то и используется в Diablo III. Текстура такого типа используется при рендеринге сцены как depth buffer, а затем значения из нее можно получить в шейдерах, где необходима информация о глубине. Я не знаю, как выполняется рендеринг на аппаратуре, которая не поддерживает INTZ текстуры (не все shader model 3 видеокарты поддерживают данный «хак»), у меня нет видеокарты без такой поддержки. Возможно, выполняется дополнительный проход, либо эффекты, зависящие от глубины, реализуются по-другому, либо совсем выключаются.</p>
<p>Подсветка объектов под курсором реализуется путем рендеринга выделяемого объекта в отдельную текстуру. Шейдер при этом используется простейший – вывод единицы в альфа канал рендер таргета, и цвета выделения в rgb каналы. Затем полученная текстура размывается по горизонтали и вертикали. Для корректного наложения эффекта и получения финального изображения необходимо оставить только ореол объекта, но не его основной силуэт. Имея оригинальное (не размытое) изображение, финальный шейдер наложения проверяет значение альфа канала в этой текстуре. Если он равен 1 (объект в этом пикселе есть), то выводимый альфа канал устанавливается в ноль. Если значение 0 (объекта в этом пикселе нет), то используется альфа канал размытой текстуры.</p>
<p><img decoding="async" loading="lazy" alt="Highlight" src="https://romanmarchenko.me/assets/images/highlight-8a0be5ca8e4f742b295789e2b797d472.jpg" width="769" height="255" class="img_ev3q"></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="пост-процесс-эффекты">Пост-процесс эффекты<a href="https://romanmarchenko.me/ru/blog/d3#%D0%BF%D0%BE%D1%81%D1%82-%D0%BF%D1%80%D0%BE%D1%86%D0%B5%D1%81%D1%81-%D1%8D%D1%84%D1%84%D0%B5%D0%BA%D1%82%D1%8B" class="hash-link" aria-label="Direct link to Пост-процесс эффекты" title="Direct link to Пост-процесс эффекты" translate="no">​</a></h3>
<p>Количеством пост-процесс эффектов игра похвастаться не может. Среди всего арсенала были замечены bloom, full screen distortion и полноэкранное сглаживание, выполненное по технологии FXAA. Дисторшн реализуется по классической схеме. Отрисовываем партиклы, которые должны привносить искажения в финальную картинку (горячий воздух, например) в специальную текстуру. Записываемые данные являются u и v смещениями текстурных координат. В следующем полноэкранном проходе используем эту текстуру и смещаем текстурные координаты для семплинга основного изображения сцены.</p>
<p><img decoding="async" loading="lazy" alt="Distortion" src="https://romanmarchenko.me/assets/images/distortion-584a903a4fe8aa0cebcb5d53827d30ae.jpg" width="855" height="283" class="img_ev3q"></p>
<p>Полноэкранное сглаживание выполнено также как пост-процесс эффект. Причин этому несколько. Использование INTZ буфера глубины становится невозможным (нельзя создать чистый multisampled INTZ depth buffer для последующего копирования его в non-multisampled INTZ текстуру), и shadow map будет занимать очень много памяти (напомню, что ее формат A32R32G32B32F, т.е. 16 байт на пиксель). Полноэкранное сглаживание в игре выполнено по технологии Fast Approximate Anti-Aliasing (FXAA).</p>
<p><img decoding="async" loading="lazy" alt="FXAA" src="https://romanmarchenko.me/assets/images/fxaa-75a913bf2652973c19a7f34e10c8fdd9.jpg" width="1112" height="515" class="img_ev3q"></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="геометрия-и-материалы">Геометрия и материалы<a href="https://romanmarchenko.me/ru/blog/d3#%D0%B3%D0%B5%D0%BE%D0%BC%D0%B5%D1%82%D1%80%D0%B8%D1%8F-%D0%B8-%D0%BC%D0%B0%D1%82%D0%B5%D1%80%D0%B8%D0%B0%D0%BB%D1%8B" class="hash-link" aria-label="Direct link to Геометрия и материалы" title="Direct link to Геометрия и материалы" translate="no">​</a></h3>
<p>Все данные вершин игровых моделей упакованы в cache-friendly 32 байт формат. Исключение составляют анимированные модели, там 48. Дополнительные данные это веса костей их индексы. В игре используется скелетная анимация, которая выполняется в шейдере. По этой причине для анимированных моделей количество точечных источников ограничено семью, из-за нехватки константных регистров для хранения параметров источников освещения и матриц костей.</p>
<p>Общее количество draw call’s невелико. Значение колеблется в пределах 300-800 DIP, что является хорошим показателем.</p>
<p>Шейдера выполнены по технологии uber-shader, т.е. компиляция множества вариантов одного эффекта с помощью перебора набора дефайнов препроцессора. Например эффект может быть с туманом и без, с тенью и без, с лайтмапой и без. За туман, тень и лайтмапу отвечает определенный define вида: #define USE_FOG 1. В теле шейдера блок кода, отвечающий за наложение тумана выполнен внутри блока #if USE_FOG … #endif. Таким образом переключая значение USE_FOG 1/0, мы получаем шейдер с туманом и без. Схожим образом делаются и все эффекты. Система сборки всех вариантов шейдеров, автоматически перебирает весь набор значений дефайнов, и для каждого набора компилирует шейдер.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="пользовательский-интерфейс">Пользовательский интерфейс<a href="https://romanmarchenko.me/ru/blog/d3#%D0%BF%D0%BE%D0%BB%D1%8C%D0%B7%D0%BE%D0%B2%D0%B0%D1%82%D0%B5%D0%BB%D1%8C%D1%81%D0%BA%D0%B8%D0%B9-%D0%B8%D0%BD%D1%82%D0%B5%D1%80%D1%84%D0%B5%D0%B9%D1%81" class="hash-link" aria-label="Direct link to Пользовательский интерфейс" title="Direct link to Пользовательский интерфейс" translate="no">​</a></h3>
<p>Внутриигровой интерфейс отрисовывается на экран довольно стандартно. Особой группировки элементов с целью уменьшения вызовов на отрисовку (DIP calls) не наблюдается. Хочется отметить рендеринг текста. Подготовка символов для рендера очень похожа на метод используемый в Scaleform GFX. Все уникальные символы отрисовываются в отдельную текстуру, и уже эта текстура используется для рендеринга текста. Не смотря на схожесть текстового рендера, сам Scaleform не используется.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="послесловие">Послесловие<a href="https://romanmarchenko.me/ru/blog/d3#%D0%BF%D0%BE%D1%81%D0%BB%D0%B5%D1%81%D0%BB%D0%BE%D0%B2%D0%B8%D0%B5" class="hash-link" aria-label="Direct link to Послесловие" title="Direct link to Послесловие" translate="no">​</a></h3>
<p>Сам рендер оставляет приятные впечатления. Такой себе микс олдскула и некоторых современных веяний. Производительность на высоте при красивой картинке (как всегда у игр от Blizzard, собственно). Большую роль в этой всей красоте отыгрывает работа художников и дизайнеров. Diablo III еще раз доказывает, что очень красивую графику можно сделать и на не самом технологичном рендере.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="полезные-ссылки">Полезные ссылки<a href="https://romanmarchenko.me/ru/blog/d3#%D0%BF%D0%BE%D0%BB%D0%B5%D0%B7%D0%BD%D1%8B%D0%B5-%D1%81%D1%81%D1%8B%D0%BB%D0%BA%D0%B8" class="hash-link" aria-label="Direct link to Полезные ссылки" title="Direct link to Полезные ссылки" translate="no">​</a></h3>
<ul>
<li class="">Variance Shadow Maps. <a href="http://www.punkuser.net/vsm" target="_blank" rel="noopener noreferrer" class="">www.punkuser.net/vsm</a></li>
<li class="">FXAA. <a href="https://developer.download.nvidia.com/assets/gamedev/files/sdk/11/FXAA_WhitePaper.pdf" target="_blank" rel="noopener noreferrer" class="">https://developer.download.nvidia.com/assets/gamedev/files/sdk/11/FXAA_WhitePaper.pdf</a></li>
<li class="">Список известных GPU «хаков». <a href="https://aras-p.info/texts/D3D9GPUHacks.html" target="_blank" rel="noopener noreferrer" class="">https://aras-p.info/texts/D3D9GPUHacks.html</a></li>
</ul>]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[Старкрафт 2. Рендер роликов.]]></title>
        <id>https://romanmarchenko.me/ru/blog/sc2p2</id>
        <link href="https://romanmarchenko.me/ru/blog/sc2p2"/>
        <updated>2010-10-10T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Продолжаем ковыряться в рендере старкрафта.]]></summary>
        <content type="html"><![CDATA[<p>Продолжаем ковыряться в рендере старкрафта.</p>
<p>Рендер роликов концептуально очень похож на рендер игры, но есть и отличия.
Как и в игровом рендере, если есть глобальные источники освещения, то создается шадоумапа от них. Затем идет отрисовка всех объектов в 4 рендер таргета формата argb16f:</p>
<ol>
<li class="">Основной буфер, куда ведется рендер цвета с освещением от глобальных источников. Альфа канал не используется.</li>
</ol>
<p><img decoding="async" loading="lazy" alt="RT1" src="https://romanmarchenko.me/assets/images/2_nolights_color_rt1-7db6ebcf95550292dddd6fa6360bc65c.jpg" width="1024" height="768" class="img_ev3q"></p>
<ol start="2">
<li class="">Нормали в пространстве камеры. В альфа канале глубина сцены.</li>
</ol>
<p><img decoding="async" loading="lazy" alt="RT2" src="https://romanmarchenko.me/assets/images/2_notmals_rt2-f015b9687ac09067d5cb9db3457e054f.jpg" width="1024" height="768" class="img_ev3q"></p>
<ol start="3">
<li class="">Диффузный цвет, который используется для некоторых пост процесс эффектов в дальнейшем. В альфа канал идет предрасчитанный статический (из текстур) АО фактор.</li>
</ol>
<p><img decoding="async" loading="lazy" alt="RT3" src="https://romanmarchenko.me/assets/images/2_diffuse_rt3-3acc330e5d3dde126494db4eed07fd01.jpg" width="1024" height="768" class="img_ev3q">
<img decoding="async" loading="lazy" alt="RT3A" src="https://romanmarchenko.me/assets/images/2_static_ao_rt3a-a4fe054517688a48fc992167d43a57cd.jpg" width="1024" height="768" class="img_ev3q"></p>
<ol start="4">
<li class="">Спекуляр фактор в RGB, альфа канал не используется.</li>
</ol>
<p><img decoding="async" loading="lazy" alt="RT4" src="https://romanmarchenko.me/assets/images/2_specular_term_rt4-d1292bdfbde73b88b03a02fb7acb1e20.jpg" width="1024" height="768" class="img_ev3q"></p>
<p>После того как заполнили буфера нужными данными идет рендер локальных источников освещения. Алгоритм абсолютно аналогичен описанному в первой части. Помечаем область влияния источника с помощью стенсил буфера и геометрического футпринта, а затем рендерится опять этот же футпринт с шейдером освещения. Deferred shading в чистом виде. Для источников, которые должны отбрасывать тень сначала рендерится шадоумапа с его позиции (для спот и направленных источников; omnidirectional shadowmaps замечены небыли) затем рендерится деферред источник с этой картой тени. Пару примерчиков шадоумап:</p>
<p><img decoding="async" loading="lazy" alt="SM1" src="https://romanmarchenko.me/assets/images/2_sm1-f6c9addbe791bd3c408e06e010512add.jpg" width="2048" height="2048" class="img_ev3q">
<img decoding="async" loading="lazy" alt="SM2" src="https://romanmarchenko.me/assets/images/2_sm2-722759ed5d885c775f58b56fe6d5cb0f.jpg" width="2048" height="2048" class="img_ev3q">
<img decoding="async" loading="lazy" alt="SM3" src="https://romanmarchenko.me/assets/images/2_sm3-0ac16f5bbe2d7ff87487437f7bb68663.jpg" width="2048" height="2048" class="img_ev3q"></p>
<p>Для примера, в этой сцене что на всех скриншотах, 24 deferred источника освещения, которые не отбрасывают тень, и еще 7, которые отбрасывают.
Вот как получилось после применения источников освещения:</p>
<p><img decoding="async" loading="lazy" alt="Scene and Lights" src="https://romanmarchenko.me/assets/images/2_scene_and_lights-7f9044a6be5ec13007b63a94e1b62f21.jpg" width="1024" height="768" class="img_ev3q"></p>
<p>После идут все полупрозрачные объекты. Для крупных объектов прямо на месте применяется пост процесс эффект для искажения изображения находящегося за полупрозрачным объектом. Для этого используется текстура с диффузным цветом в рендер таргете 3.
В рендере старкрафта 2 присутствуют также волуметрик эффекты, как то volumetric light и volumetric fog.
Реализуется данный эффект в 5 проходов:</p>
<ol>
<li class="">Рендерим бэкфейсы объема очерчивающего границы влияния volumetric эффекта. Тест глубины включен. Записываем глубину в рендер таргет.</li>
</ol>
<p><img decoding="async" loading="lazy" alt="Volumetric Effect 1" src="https://romanmarchenko.me/assets/images/2_volume_back1-c9413399248924d7d9cb3e5e28685c40.jpg" width="1024" height="768" class="img_ev3q"></p>
<ol start="2">
<li class="">Включаем альфаблендинг в режим сложения цветов (Blend source: D3DBLEND_ONE, Blend destination: D3DBLEND_ONE, Blend operation: D3DBLENDOP_ADD) и рисуем фронтфейсы объема, выводя отрицательную глубину пикселя. При блендинге пикселей происходит операция: глубина дальней стенки – глубина ближней стенки, что есть толщина объема в пространстве камеры.</li>
</ol>
<p><img decoding="async" loading="lazy" alt="Volumetric Effect 2" src="https://romanmarchenko.me/assets/images/2_volume_front1-15225aaca3f6107f372c46ebd07e7e36.jpg" width="1024" height="768" class="img_ev3q"></p>
<ol start="3">
<li class="">Толщину объема мы получили, и можем варьировать прозрачность в зависимости от этого числа, но нам надо еще учитывать объекты внутри объема. Для этого мы опять рендерим бекфейсы объема, но с инвертированным тестом глубины (D3DCMP_GREATER). Режим блендинга остался таким же как и в предыдущем шаге.</li>
</ol>
<p><img decoding="async" loading="lazy" alt="Volumetric Effect 3" src="https://romanmarchenko.me/assets/images/2_volume_back2-70af2d186aadabd2ab3c012cc5b6e8a5.jpg" width="1024" height="768" class="img_ev3q"></p>
<ol start="4">
<li class="">Так как на предыдущем шаге мы учитывали все объекты перед задней стенкой объема, даже те, что перед передней стенкой, мы должны исправить этот момент. Рисуем фронтфейсы с режимами как в шаге 3, только выводим минус глубину. Т.о. мы отнимаем глубину всех неверно учтенных пикселей, что находятся перед объемом volumetric эффекта.</li>
</ol>
<p><img decoding="async" loading="lazy" alt="Volumetric Effect 4" src="https://romanmarchenko.me/assets/images/2_volume_front2-ef25e579186aa7072406759a3f955930.jpg" width="1024" height="768" class="img_ev3q"></p>
<ol start="5">
<li class="">Используя текстуру глубины полученную на шагах 1-4, рисуем объем еще раз, но уже в главный рендер таргет цвета изменяя прозрачность в зависимости от глубин пикселей.
Скриншот только эффекта:</li>
</ol>
<p><img decoding="async" loading="lazy" alt="Volumetric Effect 5" src="https://romanmarchenko.me/assets/images/2_volumetric_effect-4545a5846e319d959deae4271491021e.jpg" width="1024" height="768" class="img_ev3q"></p>
<p>Результат с остальной картинкой:</p>
<p><img decoding="async" loading="lazy" alt="Volumetric Effect 6" src="https://romanmarchenko.me/assets/images/2_volumetric_effect_full-f846c88820c95040270baa1e94f7d5e4.jpg" width="1024" height="768" class="img_ev3q"></p>
<p>Далее идет Depth-of-Field. Эффект реализован достаточно интересно и в точности повторяет описание в презентации от разработчиков которую я упомянул в первой части. Я не буду повторяться тут, скажу лишь, что основная борьба велась за корректность отображения эффекта (правильность блендинга объектов перед, в и позади фокуса).
После DoF идет Bloom. И на этом рендеринг заканчивается.</p>
<p>Отдельно хочется отметить 2 момента:
Очень качественный рендеринг кожи человека, даже без применения хитрых техник, таких как texture space diffusion и subsurface scattering. Качественная настройка материалов и используемых текстур опять зарулило.</p>
<p>Отсутствие SSAO. Да-да. То о чем писали разработчики в своем докладе, почему-то не используется. Хотя шейдеры присутствуют. Я перепроверил настройки рендера когда обнаружил, что АО канал заполняется во время основного прохода рендера в 4 РТ. Видимо предрасчитанное АО устроило арт отдел. Вот красивая текстура лица Джима Рейнора с АО:</p>
<p><img decoding="async" loading="lazy" alt="Raynor Face AO" src="https://romanmarchenko.me/assets/images/2_raynor_face_AO-4d646b266be184f42e93cfa75a1974df.jpg" width="1024" height="1024" class="img_ev3q"></p>
<p>Вот собственно и все. Пишите комментарии, задавайте вопросы.</p>
<p><img decoding="async" loading="lazy" alt="Final" src="https://romanmarchenko.me/assets/images/2_final-45d635755eb87671c58816e20e21c666.jpg" width="1024" height="768" class="img_ev3q"></p>]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[Старкрафт 2: Секреты технологий.]]></title>
        <id>https://romanmarchenko.me/ru/blog/sc2p1</id>
        <link href="https://romanmarchenko.me/ru/blog/sc2p1"/>
        <updated>2010-10-02T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Мой последний реверс датирован 4 июля 2008 года. Оправдываться не буду, что обещал чаще, а получилось вот так. Наконец кривые желания и возможности пересеклись, и вашему вниманию предлагается реверс инжиниринг рендера всем известной игры Старкрафт 2, от не менее известной компании Близзард.]]></summary>
        <content type="html"><![CDATA[<p>Мой последний реверс датирован 4 июля 2008 года. Оправдываться не буду, что обещал чаще, а получилось вот так. Наконец кривые желания и возможности пересеклись, и вашему вниманию предлагается реверс инжиниринг рендера всем известной игры Старкрафт 2, от не менее известной компании Близзард.</p>
<p>Технологии шагнули далеко вперед. Уже можно играть в игры с поддержкой DX11. Есть игры которые изначально разрабатывались на DX10 и не поддерживают DX9 железо. Но, игры компании Близзард всегда славилась поддержкой широкого спектра аппаратуры (взять тот же ВоВ, который и на ФФП нормально бегает). Не исключением стала и SC2. Рендер стратегии разработан с использованием D3D версии 9 (для windows). DX10 не поддерживается. Я думаю после нескольких лет разработки было накладно переводить рендер на новый АПИ, поэтому имеем только DX9 (и Open GL для Mac).</p>
<p>Первое с чем я столкнулся после того как я сел за дело были проблемы с тулзами. СК2 использует хитрую систему запуска, через всякие лоадеры, так что под PerfHUD’ом запустить не удалось. Можно было бы поковыряться в исходниках лоадера (который форсит PerfHUD девайс), чтобы скипал несколько процессов, но из-за того, что стар молча выходил, при попытках тестовых запусков, я отложил вариант с PerfHUD’ом. Плюс некоторый функционал этого профайлера перестал работать на новой Win7 OS (не показывает перфоманс графики). Новый профайлер Nvidia Parallel Insight показал себя с лучшей стороны. Он быстрый, удобный и практически не глючный. Один недостаток – DX9 не поддерживается. Так что у меня оставался всего один вариант – MS PIX for Windows. За два года данную тулзу немного улучшили (особенно в плане производительности), так что пользовался я исключительно ей.</p>
<p>Вторая неприятность возникла, когда я взглянул на шейдеры. Обычно, когда шейдер собирается из эффект (*.fx) файла, то в бинарном байткоде остается немного дебаговой информации. Это информация включает комментарии в начале шейдера с блоком прешейдера (если используется) и, что самое главное, названия переменных и их соответствия константным регистрам.</p>
<p>Вот как выглядит шейдер который я ожидал: <a href="http://www.everfall.com/paste/id.php?fgnlr6ffcx0y" target="_blank" rel="noopener noreferrer" class="">http://www.everfall.com/paste/id.php?fgnlr6ffcx0y</a>
Вот так выглядят шейдера от Близзард: <a href="http://www.everfall.com/paste/id.php?njt5do17c4bi" target="_blank" rel="noopener noreferrer" class="">http://www.everfall.com/paste/id.php?njt5do17c4bi</a>
Что в первом, что во втором случае разобрать по асму что там происходит сложно, но в первом примере есть имена переменных, по которым можно сориентироваться. В общем, я понял, что так я далеко не уеду, и полез ковырять данные игры. Распаковал пару MPQшек, и нашел исходники шейдеров. Зачастую мне было сходу понятно, что делается в данном draw call. Но иногда я заходил в тупик. Помогало медитирование над входными текстурами, вертекс форматом и константами объявленными в шейдере. Если вижу, например, операцию вычитания двух регистров, один из которых константный со значением 1.5, я ищу по всем hlsl шейдерам текст -1.5 (или просто 1.5) и часто с первого раза попадаю в нужный. Если шейдер небольшой, то разбирал прямо по ассемблеру.</p>
<p>По рендеру СК2 есть замечательная дока: <a href="http://developer.amd.com/documentation/presentations/legacy/Chapter05-Filion-StarCraftII.pdf" target="_blank" rel="noopener noreferrer" class="">http://developer.amd.com/documentation/presentations/legacy/Chapter05-Filion-StarCraftII.pdf</a>
В ней описаны многие аспекты рендера, и я ей активно пользовался. Рекомендую к ознакомлению.</p>
<p>Данный разбор касается только игровой части рендера. Рендер роликов будет отдельным постингом.</p>
<p>Хочется остановиться на системе материалов используемой в СК2. Применяется техника убершейдера, с плотной интеграцией в С++ код. Вот пример главной функции пиксельного шейдера и вычисления цвета материала: <a href="http://www.everfall.com/paste/id.php?bjpc19uks7he" target="_blank" rel="noopener noreferrer" class="">http://www.everfall.com/paste/id.php?bjpc19uks7he</a>. Как видно, в исходном коде имеются множественные проверки булевых констант, которые определяют разветвления комбинаций опций шейдинга. Система пиксельной обработки построена на т.н. слоях (layers) каждый из которых состоит из одной текстуры (вернее самплера) и параметров-свойств этого слоя. Вот как оно реализовано: <a href="http://www.everfall.com/paste/id.php?5ekjmezzcllx" target="_blank" rel="noopener noreferrer" class="">http://www.everfall.com/paste/id.php?5ekjmezzcllx</a>. В шейдере, перед телом, идет декларация нескольких слоев:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">DECLARE_LAYER(Diffuse);</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">DECLARE_LAYER(Decal);</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">DECLARE_LAYER(Specular);</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">DECLARE_LAYER(Emissive);</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">DECLARE_LAYER(Emissive2);</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">DECLARE_LAYER(Envio);</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">DECLARE_LAYER(EnvioMask);</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">DECLARE_LAYER(AlphaMask);</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">DECLARE_LAYER(AlphaMask2);</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">DECLARE_LAYER(Normal);</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">DECLARE_LAYER(Heightmap);</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">DECLARE_LAYER(Lightmap);</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">DECLARE_LAYER(AmbientOcclusion);</span><br></div></code></pre></div></div>
<p>Настройка слоя происходит внутри тела шейдера с помощью макроса SETUP_LAYER (пример можно посмотреть в предыдущей ссылке с кодом главной функции пиксельного шейдера). DX Effect System не используется. В доке указанной выше, сказано, что система материалов разрабатывалась для максимального удобства программистов. Плюсовая часть ее мне не видна, к сожалению, но шейдерная сделана довольно интересно.</p>
<p>Рендер сцены начинается с рендера карты тени (shadowmap) размером 2048х2048 пикселов. Трик с нулевым рендер таргетом не используется. Соответствующий буфер цвета используется для рендера полупрозрачных объектов с позиции источника света для создания цветных теней. Также для полупрозрачный объектов используется отдельная карта теней с тем же размером. Искажающие перспективу и каскадные техники не используются по понятным причинам. Камера никогда не смотрит вдаль, и в нормальном игровом процессе расположена под углом 60-70 градусов к поверхности земли. В данной ситуации ни каскадные тени ни перспективное искажение возле положения камеры не дадут сколь видимого эффекта.</p>
<p>Пример шадоумапы:</p>
<p><img decoding="async" loading="lazy" alt="SM" src="https://romanmarchenko.me/assets/images/sm-bf5cdf8e43de60878eed9dc0de5e788d.jpg" width="2048" height="2048" class="img_ev3q"></p>
<p>Пример буфера цвета для цветных теней:</p>
<p><img decoding="async" loading="lazy" alt="Color SM" src="https://romanmarchenko.me/assets/images/color_sm-132cb01cee4929da94e576c252517166.jpg" width="2048" height="2048" class="img_ev3q"></p>
<p>Далее если в игре присутствует зерг, то создается текстура крипа (такая коричневая субстанция, которая покрывает землю вокруг базы зерга). Если вы играли в СК2, то возможно заметили, что на крипе присутствует эффект движения. Делается он просто. Из чёрно-белой текстуры шума с помощью нескольких коэффициентов выделяются несколько пятен. Затем полученная карта высот трансформируется в карту нормалей. Затем данная карта нормалей используется в дальнейшем для рендеринга самого крипа на земле.</p>
<p>Оригинальная текстура шума:</p>
<p><img decoding="async" loading="lazy" alt="Creep Noise" src="https://romanmarchenko.me/assets/images/creep_hm_original-02a9739f2145b6ca7503031c6d7089bc.jpg" width="256" height="256" class="img_ev3q"></p>
<p>Полученная карта высот:</p>
<p><img decoding="async" loading="lazy" alt="Creep HM" src="https://romanmarchenko.me/assets/images/creep_hm-dc60be26636d6f521662c2a5780066fa.jpg" width="512" height="512" class="img_ev3q"></p>
<p>Карта нормалей полученная из карты высот:</p>
<p><img decoding="async" loading="lazy" alt="Creep NM" src="https://romanmarchenko.me/assets/images/creep_nm-d491ac37f4cdff6a7c0acff789da4664.jpg" width="512" height="512" class="img_ev3q"></p>
<p>Заметьте, что оригинальная карта высот сделана тайлящейся по всем направлениям. Т.о. мы имеем тайлящуюся карту нормалей.</p>
<p>Дальше начинается основной рендеринг. Устанавливается 3 рендертаргета формата ARGB16F в первые 3 MRT слота. Первый RT содержит освещенную глобальными источниками света картинку. Второй нормали в пространстве камеры. Третий только диффузный цвет.</p>
<p><img decoding="async" loading="lazy" alt="MRT0" src="https://romanmarchenko.me/assets/images/rt1_main-6c06a7be4164c034db37f694e437abe1.jpg" width="1024" height="768" class="img_ev3q">
<img decoding="async" loading="lazy" alt="MRT1" src="https://romanmarchenko.me/assets/images/rt2_nomals-800a5ee766aad4e5b0db25f65e536960.jpg" width="1024" height="768" class="img_ev3q">
<img decoding="async" loading="lazy" alt="MRT2" src="https://romanmarchenko.me/assets/images/rt3_diffuse-b512f5cb891980a9df9a48eeac1b8331.jpg" width="1024" height="768" class="img_ev3q"></p>
<p>Система освещения в СК2 комбинированная. Глобальные источники, как то солнце и те что занимают много места на экране, применяются сразу в первом проходе (т.е. forward shading). Множество мелких источников применяются после, с помощью аддитивного блендинга (т.е. deferred shading). Deferred источники используют стенсил для минимизации обрабатываемых пикселей. Стенсил буфер очищается, и перед отрисовкой deferred источника, рендерится его футпринт (т.е. шар для точечных, конус для спот источников). Сначала помечаем всю поверхность источника света, а затем исключаем из стенсил маски те участки, где прошел тест глубины для бэк-фейс полигонов. Т.о. получается маска пикселов которые буду затронуты данным источником освещения. Forward источников может быть не много, упираемся в шейдерные константы:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">#define MAX_DIRECTIONAL_LIGHTS 3</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">#define MAX_POINT_LIGHTS 5</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">#define MAX_SPOTLIGHTS 5</span><br></div></code></pre></div></div>
<p>Deferred источников может быть бесконечное множество.</p>
<p>Перед рендером геометрии с шейдингом идет depth prepass, в котором заполняется буфер глубины патчами земли. Почему только земля? Потому что, неровности земли часто скрывают элементы ландшафта, а остальные объекты небольшие. Таким образом, экономим еще немного производительности на пиксельной обработке.</p>
<p>Шейдеры главного прохода очень большие. Наложение теней, полупрозрачных теней, параллакс маппинга (в коде шейдера реализованы методы relief mapping и parallax occlusion mapping. Хорошо видно после взрыва протосовского здания – котлован взрыва), тумана войны, наложение крипа, того же направленного освещения. Блендинг кучи текстур. Сплаттинг для террейна часто не помещается по инструкциям в один большой шейдер. Слоя земли смешиваются в отдельном RT, а затем этот RT используется как диффузная текстура в рендере патча земли. Все движения выполняются посредством применения скелетной анимации.</p>
<p>Обычные тени размываются посредством percentage closer filtering’а. Для рандомизации семплов используется текстура с 2D rotation векторами:
L канал:</p>
<p><img decoding="async" loading="lazy" alt="RotL" src="data:image/jpeg;base64,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" width="64" height="64" class="img_ev3q"></p>
<p>А канал:</p>
<p><img decoding="async" loading="lazy" alt="RotA" src="data:image/jpeg;base64,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" width="64" height="64" class="img_ev3q"></p>
<p>Туман войны считается на CPU и хрантися в небольшой текстурке:</p>
<p><img decoding="async" loading="lazy" alt="FoW" src="https://romanmarchenko.me/assets/images/fow-4a37af017eeaecaa057facd0fdfb1240.jpg" width="256" height="256" class="img_ev3q"></p>
<p>Эта текстура используется для всех эффектов в игре.</p>
<p>Полупрозрачные тени детально описаны в документе от разработчиков. Опишу вкратце. У нас есть карта тени основная, карта тени в которой только глубины полупрозрачных объектов, и цветная текстура отрисованная с позиции источника света с полупрозрачными объектами. Когда мы делаем тест на то в тени ли пиксель или нет, при отрицательном результате теста (не в тени) мы тестируем его еще раз, но уже используя shadowmap с глубинами полупрозрачных объектов. Если тест положительный (в тени от полупрозрачного объекта, но не в тени от непрозрачного), то берем цвет из цветной текстуры и окрашиваем тень в этот цвет. Эффект хорошо рядом с кристаллами и под прозрачными юнитами.</p>
<p><img decoding="async" loading="lazy" alt="Color Shadow" src="https://romanmarchenko.me/assets/images/color_shadow-0182b76d13cab2b83fcd06e757480a44.jpg" width="1024" height="768" class="img_ev3q"></p>
<p>Если на уровне есть вода, то дополнительно рендерится refraction текстура, а затем используется при рендеринге поверхности воды. Полупрозрачные объекты и системы частиц рисуются после всех непрозрачный, что логично.</p>
<p>После рендера всей геометрии идет проход deferred источников света, о которых я писал выше.</p>
<p>Далее идет отрисовка рамок UI, маркеров юнитов и портрета текущего выбранного юнита. К портрету юнита применятся depth of field.</p>
<p><img decoding="async" loading="lazy" alt="Before Bright Pass" src="https://romanmarchenko.me/assets/images/before_bright_pass-f3955cf2b4f46930315373479fad7620.jpg" width="1024" height="768" class="img_ev3q"></p>
<p>Затем идет брайтпасс, и блур. Получаем блум эффект. После этого комбинируем все с тонемаппингом.
Далее отрисовка миникарты со всем маркерами юнитов (одним вызовом), туманом войны и крипом.</p>
<p><img decoding="async" loading="lazy" alt="Minimap" src="https://romanmarchenko.me/assets/images/minimap-d1094313a9b1536e6f4b093ed51f695c.jpg" width="1024" height="768" class="img_ev3q"></p>
<p>Поэлементно дорисовывается весь интерфейс. Текст рендерится пачками – применяется рендер скейлформа, который пакует все нужные буквы в кадре в текстуру. Получается эффективно.</p>
<p><img decoding="async" loading="lazy" alt="Text" src="https://romanmarchenko.me/assets/images/text-646b014852773e52e5bb79754341310d.jpg" width="512" height="512" class="img_ev3q"></p>
<p>Результат:</p>
<p><img decoding="async" loading="lazy" alt="Final" src="https://romanmarchenko.me/assets/images/fin-616d971dffa1483291fa52d6731e59bb.jpg" width="1024" height="768" class="img_ev3q"></p>
<p>Формат вершин моделей сделан хорошо, зачастую 32 байта. Данные упакованы хорошо. Количество DIPов тоже неплохое. В средней сцене – 800-1200. Лимит зерглингов на экране с основной базой показал 1800-1900 DIPов. Все карты нормалей хранятся в DXT5 формате со сжатием и распределением по каналам для большего качества. Реверс инжиниринг проходил на компьютере Core 2 Duo, GF460GTX, все настройки ультра. Качество текстур – высокое (при попытка захвата кадра пиксом, на настройках текстур ультра, старкрафт падал с out of memory).</p>]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[Crysis]]></title>
        <id>https://romanmarchenko.me/ru/blog/crysis</id>
        <link href="https://romanmarchenko.me/ru/blog/crysis"/>
        <updated>2008-06-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Crysis... Как много в этом слове!)]]></summary>
        <content type="html"><![CDATA[<p>Crysis... Как много в этом слове! :) Это было сложно и невероятно увлекательно. Каждый эффект - новое открытие. Одно дело прочитать 1-2 абзаца описания реализации в документации, и совсем другое самостоятельно пощупать и поиграться с эффектом. Сначала, зачастую, ничего не понятно. Дальше по частицам начинает вырисовываться технология и эффект приобретает новую красоту. Обычно заканчивается тем, что находишь самое яркое его проявление и любуешься со всех сторон. :)</p>
<p>Да, это реверс рендера Кризиса. С самого начала хочу сделать 2 ремарки:</p>
<ol>
<li class="">Существует документация по технологиям Кризиса, где в сжатом виде рассказаны те или иные подходы к реализации. Мне известно 3 документа.
Первый это доклад Мартина Миттринга (Martin Mittring) на Siggraph 2007 (p97-mittring.pdf).
Второй - доклад Карстена Вензеля (Carsten Wenzel) на GDC 2007 (D3DTutorial_Crytek.pdf)
И последний - статья Тиаго Сусы (Tiago Sousa) в GPU Gems 3.
Все эти источники информации мне сильно помогли, так что советую их просмотреть. Я не буду акцентировать большое внимание на момментах, которые неплохо описаны в этих документах.</li>
<li class="">Я очень уважаю команду разработчиков. Это команда экстра класса. Естественно, что вряд ли я один смогу вот так, наскоком, постичь все премудрости движка. Поэтому вполне вероятно, что в процессе анализа я что-то упустил (возможно даже существенное), либо не так понял.</li>
</ol>
<p>Итак, Crysis, Windows Vista, DX10. Компьютер Core2 Duo E6550, 3GB RAM, GF 8600GTS. Все настройки Ultra High.
Наверное, у всех на слуху знаменитый Screen Space Ambient Occlusion (SSAO). С него и начну.
Основной ингредиент для приготовления данного эффекта является camera space normalized scene depth, или текстура с глубиной сцены в пространстве камеры, если по-русски. Рисуется этот эффект в отдельный буфер, и результат выглядит вот так:</p>
<p><img decoding="async" loading="lazy" alt="SSAO" src="https://romanmarchenko.me/assets/images/ssao_filtered_1-a18f354aa7f31b73fa99075b8339b207.jpg" width="1024" height="768" class="img_ev3q"></p>
<p>Это полностью реалтаймовый эффект, который считается на видеокарте. Как он реализован?
Для каждого пикселя изображения берутся 8 векторов, которые можно представить внутри кубика. Эти вектора идут от центра кубика до его углов. Как и в классическом алгоритме просчета AO, это и есть лучи с помощью которых мы проверяем заслонённость данной точки каким-то объектом (правда тут проверка производится не по полусфере, а по полной сфере, так как нормаль в точке недоступна). Затем, чтобы избавиться от регулярности, нужно как-то сэмулировать произвольное распределение лучей. Это сделано с помощью операции вычисления отраженного вектора из данного (полный аналог интринсика reflect в DX HLSL). Нормаль, относительно которой выполняется отражение берется из вот такой текстурки (увеличено в 50 раз):</p>
<p><img decoding="async" loading="lazy" alt="Rot Tex" src="https://romanmarchenko.me/assets/images/rot_texture-30cf7408919bbaff8ff899ba26ba4507.jpg" width="200" height="200" class="img_ev3q"></p>
<p>Это текстура размером 4х4 текселя, она проецируется так, каждый тексель этой текстурки соответсвует каждому пикселю выходного изображения. Достигается это тайлингом (текстурным повторением) размер_экрана/4 раз. Т.о., для каждого пикселя экрана мы получили псевдопроизвольную нормаль. Повернув все 8 исходных векторов относительно полученной псевдопроизвольной нормали, мы получим 8 псевдопроизвольных лучей для тестов. Лучи у нас единичной длины, так что их длина скейлится в зависимости от отдаленности обрабатываемой точки от near plane (ближние пиксели получают меньшую длину лучей, дальние большую). Затем полученные текстурные координаты и Z смещаются по каждому лучу и производится проверка на Z на конце вектора-луча. Если луч приникнет в какую-либо геометрию, то Z в буфере глубины камеры будет меньше Z на конце луча. Данная ситуация означает, что процент загороженных лучей будет увеличен, что в конце повлияет на общую освещенность точки. К сожалению, малое количество лучей-тестов и недостаточная произвольность выборок приводит к ярко выраженной, зернистой, структуре изображения:</p>
<p><img decoding="async" loading="lazy" alt="SSAO Filtered" src="https://romanmarchenko.me/assets/images/ssao-dfc6d0413d49d9891b9b4d2705aafe35.jpg" width="1024" height="768" class="img_ev3q"></p>
<p>Для нивелирования этого побочного эффекта, применятся проход полноэкранного сглаживания c color leak correction и используется scene camera z. В результате чего, получается картинка, которая приведена в начале.
Кому интересно посмотреть этот эффект вживую, вот вам ссылочка на RenderMonkey проект, сделанный мной. В нём содержится практически полная реплика эффекта из Кризиса.
Crysis.zip на upload.com.ua</p>
<p>Далее эфффект, который, на сколько мне помнится, не описан нигде. Dynamic Terrain AO, или динамический амбиент окклюжен для террейна (земли). Рендеринг производится в G канал текстуры с SSAO (в то время как сам SSAO хранится в R). Данный эффект предназначен для аттенюации освещения объектов под влиянием террейна:</p>
<p><img decoding="async" loading="lazy" alt="Terrain AO" src="https://romanmarchenko.me/assets/images/terrain_ao-486cd64a178fc20e0af0a6619d573dc0.jpg" width="1024" height="768" class="img_ev3q"></p>
<p>Создается он хитро. Террейн разбит на прямоугольные патчи различного размера. Для каждого такого патча имеется прямоугольный параллелепипед выровненный по самой нижней точке (т.е. самая нижня точка патча земли совпадает с нижней плоскостью параллелепипеда). По сути это ААBB, только вытянутый немного по вертикали. Данный бокс, рендерится 2 раза используя предрассчитанный ранее depth buffer. z-test on, z-write off и color write off. Первый раз с front face culling, второй с back face culling. Эти 2 прохода являют собой stencil fill pass. В обоих случаях StencilFunc = Always, ZFail = Replace, остальные операции Keep. Отличия между этими двумя проходами в StencilRefl. Первым идет front face culling, и тут StencilRef = 6 (не знаю чего именно 6. Cчитайте что просто больше нуля). После отрисовки бокса, стенсил как бы "подсвечивает" не нулями то место в кадре, которое соответствует данному патчу земли. Но эта информация для шейдинга пока неверна, так как все объекты в кадре, которые ближе к камере, чем задняя стенка бокса, оказываются помеченными. Для исправления ситуации используется второй рендер бокса, но с back face cull, и StencilRef = 0. Данным проходом мы снимаем пометку с пикселей, которые не попадают в пространство внутри куба. Т.е. если игрок, например, вне этого патча земли, то оружие, которое он держит в руках, будет исключено из сгенерированной маски. Чтобы проще было понять вот картиночка с кубиком. Фиолетовый цвет это Terrain AO в G канале текстуры. Вернее это уменьшение яркости канала, так как Terrain AO расчитвает только затенение.</p>
<p><img decoding="async" loading="lazy" alt="Terrain cube" src="https://romanmarchenko.me/assets/images/terrain_patch_cube-2e5fb2dc78753761e4e8268fd7264627.jpg" width="1024" height="768" class="img_ev3q"></p>
<p>После этого, в пространстве экрана рендерится прямоугольник, который описан над ААBB упомянутым ранее. В параметрах рендеринга устанавливается SetncilFunc = Equal, StencilRef = 6. Таким образом, обработке подвергнутся только пиксели объектов, которые лежат внутри баундинг бокса патча террейна.
Для картинки приведенной выше он выглядит так:</p>
<p><img decoding="async" loading="lazy" alt="Terrain bb" src="https://romanmarchenko.me/assets/images/terrain_patch_bb-6cc967e24ca3ced8f8ee99b758d44bb2.jpg" width="1024" height="768" class="img_ev3q"></p>
<p>Дальше - интереснее. Рисуем то мы всё в пространстве экрана. Как вообще можно что-то сделать полезное? Оказывается можно.
Для каждого патча террейна, есть пара текстурок, которые используются при его рендеринге. Часть информации из них нужна при генерации Terrain AO.
Вот они:</p>
<p><img decoding="async" loading="lazy" alt="Terrain info0" src="https://romanmarchenko.me/assets/images/terrainInfo0-eafd81ee9d3f40695675b6690c8970de.jpg" width="256" height="256" class="img_ev3q">
<img decoding="async" loading="lazy" alt="Terrain info1" src="https://romanmarchenko.me/assets/images/terrainInfo1-db26df98bd34ee3fee536a028be42bed.jpg" width="256" height="256" class="img_ev3q"></p>
<p>В терминах Crytek, они называются незамысловато: TerrainInfo0, TerrainInfo1. :)
Из нулевой текстуры нам нужна предрасчитанная грубая степень освещенности террейна от солнца (я так понимаю это что-то сродни статическому амбиент окклюжену). Она в R канале:</p>
<p><img decoding="async" loading="lazy" alt="Terrain info0 R" src="https://romanmarchenko.me/assets/images/terrainInfo0_r-dcceefa893862fb9441e7ebd0bf574b5.jpg" width="256" height="256" class="img_ev3q"></p>
<p>В текстуре с индексом 1, нам потребуется хейтмапа и карта высоты растительности на данном клочке земли (А и G каналы соответсвенно).</p>
<p><img decoding="async" loading="lazy" alt="Terrain info1 A" src="https://romanmarchenko.me/assets/images/terrainInfo1_a-f0652e97e2875f3825a80a057fa17af8.jpg" width="256" height="256" class="img_ev3q">
<img decoding="async" loading="lazy" alt="Terrain info1 G" src="https://romanmarchenko.me/assets/images/terrainInfo1_g-20db8b85e5fffdd6a05711465be230a6.jpg" width="256" height="256" class="img_ev3q"></p>
<p>Как же использовать эти текстуры? Естественно необходимо посчитать текстурные координаты для проецирования этих текстур на поверхность земли. Сделано просто шикарно. Первым шагом вычисляется мировые координаты обрабатываемого пикселя. У нас есть уже буфер глубины сцены. Формула проста: мировая позиция камеры + вектор направления взгляда на пиксель * Z. 1 mad инструкция! Как узнать вектор направления взгляда, спросите вы? Очень просто. Можно посчитать его для каждого вертекса в вершинном шейдере для уголов Screen Aligned Quad. Интерполированные значения будут соответствовать искомым. Но команда Crytek пошла ещё дальше по пути оптимизации. Соответствующий вектор передаётся как per-vertex info отдельным стримом. Вот что значит оптимизация! :)
Вторым шагом нам нужно перевести world space координаты в локальные координаты патча земли. Это делается простым вычитанием координат ближайшего к мировому 0 угла ААBB патча из мировых координат пикселя. Перевод в стандартный [0..1] uv тривиален. Полученные локальные, относительно патча, координаты делим на его ширину и длину. Текстурные координаты для проективного текстурирования получены.
Далее чистый арт. Зная высоту обрабатываемого пикселя, высоту террейна под этой точкой, высоту растительности и предрасчитанный статический АО, всё это смешивается в хитрых формулах, с кучей коэффициентов. В результате получается амбиент тень от деревьев снизу, затенения в арках и гротах и т.д. Fade out эффекта также реализован по расстоянию. Результат на изображении в начале описания эффекта.</p>
<p>Каустика от воды. Красивый эффект. Делается достаточно просто. На основе вектора направления солнечного света строим проективную матрицу. Эта матрица используется для вычисления текстурных координат для карты нормалей. Правдами и неправдами, мешая высокочастотные семплы и низкочастотные, добиваемся отсутствия регулярной структуры нормалей. Нормаль после всех пертрубаций преломляем с помощью refrаct относительно направления света от солнца. При этом немножко меняем угол преломления для каждого компонента цвета (R, G и B). Это даст в результате сдвиг спектра в результирующем изображении. Затем с помощью полученных нормалей семплим текстуру вида:</p>
<p><img decoding="async" loading="lazy" alt="Caustic Diamond" src="data:image/jpeg;base64,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" width="128" height="128" class="img_ev3q"></p>
<p>Реузльтат сеплирования возносится в степень, чтобы увеличить четкость изображения каустики. Степень видимости каустики определяется скалярным произведением направления света солнца и пертрубированной нормали. Есть также операции по предотвращению появления каустики на поверхностях под углом &gt; 90 градусов к солнышку.</p>
<p>Облака. Бывают разные. Например, самые верхние состоят из 1 полигона параллельного земле. Шейдинг их состоит на определении влияния солнца и неба на цвет. Для расчета плотности применяется texture space tracing. Вектор взгляда на солнышко переводится в текстурное пространство облака и делается 8 семплов и текстуры плотности. Шаг семплирования фиксирован.
Низкие же облака щейдятся с учетом atmospheric light scattering. Рендеринг большого облака, состоящего из множества маленьких, выполняется в отдельную тестуру. Затем полученное изображение уже переносится на основную картинку.
Пример:</p>
<p><img decoding="async" loading="lazy" alt="Cloud Cluster" src="https://romanmarchenko.me/assets/images/cloud_cluster-c1a93925e8ecc0d4009b07b99af0091d.jpg" width="1024" height="1024" class="img_ev3q">
<img decoding="async" loading="lazy" alt="Cloud Plane" src="https://romanmarchenko.me/assets/images/cloud_plane-a988ff273fe718472fe3c3beda66ff2e.jpg" width="1024" height="768" class="img_ev3q"></p>
<p>Целью данного подхода, думаю, было создание уникальной облачности, без сильной вычислительной нагрузки, ведь отрендериный кластер облака можно использовать несколько кадров подряд. А так как облако это медленно изменяющийся объект, то такой метод себя оправдывает.</p>
<p>Океан. Поверхность океана представляется высокотесселированным параллелепипедом нижняя плоскость которого лежит на уровне поверхности воды, а верхняя на уровне камеры (глаз) игрока. Всё как писал Мартин Миттринг.</p>
<p><img decoding="async" loading="lazy" alt="Ocaan Mesh" src="https://romanmarchenko.me/assets/images/water_mesh-3fc35d876f4e87796cec2379065e1fc4.jpg" width="1024" height="768" class="img_ev3q"></p>
<p>Я не совсем понимаю как они манипулируют координатами вертексов так, чтобы при растеризации экранная Z координата совпадала с water plane. Несколько часов ломания головы и пересмотра шейдеров мне не помогло. :) Ну да ладно. При помощи vertex texture fetch генерируются волны. В пиксельном шейдере всё как обычно: reflection, refraction, отражение солнышка. Также для рефракшена реализована хроматическая аберация (подробно тут), т.е. различные углы преломления световых волн разной длины. Это достигается путем небольшого смещения текстурных координат для каждого из цветовых каналов. Прямо как при генерации каустики.</p>
<p>Теперь рассмотрим рендер типичного кадра по порядку.
Отражения для воды. Упрощенные шейдеры.
Далее подготавливаются shadow maps для теней от нужных источников. Я не буду останавливаться на тенях подробно, так как они описаны в первом документе, который я упомянул первым в начале. Скажу лишь, что всё что там написано - правда. :) Для directional light sources - cascaded shadow maps. Для точечных кубемапа в обычной 2d текстуре (хотя по шейдерам похоже, что нативные кубемапы тоже поддерживаются). Для террейна variance shadow maps. Для большего заблуривания применяется не summed area tables, a простой проход блура VSM карты.
Вы уже, наверное, заметили что мало какой эффект обходится без camera space depth buffer. Он используется практически повсеместно. На этом шаге подготавливаем его. Запись ведется в текстуру формата R32F, параллельно с заполнением depth buffer, чтобы далее снизить depth complexity до 0.
Следующим эффектом идет SSAO + Terrain AO описанные выше. После подготовки этой текстуры она блурится.
Теперь настал черёд создания shadow mask для т.н. deffered shadowing. Тени мы рисуем в отдельный буфер, чтобы сэкономить инструкции в шедерах в основном проходе.
Основной проход выполняется в текстуру формата RGBA16F, так как HDR - наше всё. Буфер глубины настроен только на тест, так что перетирания пикселей нет. Это очень важно с учётом применения огромных шейдеров. Дополнительным бонусом является отсутствие необходимости сортировки объектов по дальности. В основном проходе также применены интересные решения. Например для металлов применяются модели освещения Schlick и Сook-Torrance (для люознательных: демка RenderMonkey и небольшая доканебольшая дока. Подробное описание можно найти в книге Kelly Dempski, Emmanuel Viale. 2004. "Advanced Lighting andMaterials with Shaders". Wordware Publishing, Inc.). Для облегчения вычислительной нагрузки, части функций предрассчитаны и вынесены в текстуры. Все системы частиц, и облака, используют depth buffer для удаления артефактов пересечения геометрии. Отличный батчинг растительности. Ближние деревья и кусты рисуются маленькими батчами и детализированно, дальние же вырождены в screen aligned quads и выводятся большущими батчами:</p>
<p><img decoding="async" loading="lazy" alt="Trees Batching" src="https://romanmarchenko.me/assets/images/trees_batching-46c19b7d87963830bafe2e30d710a3d0.jpg" width="1024" height="768" class="img_ev3q">
<img decoding="async" loading="lazy" alt="Flat Trees" src="https://romanmarchenko.me/assets/images/flat_trees-861d1deb629909f8306ae253bb6f3396.jpg" width="1024" height="1024" class="img_ev3q"></p>
<p>Рендерингу растительности посвящена статья в GPU Gems 3. Советую обратить на неё внимание. Если вкратце, то анимация реализуется на вертексном шейдере с применением различных карт жесткости. Освещение листвы двусторонне, с применением subsurface scattering.
Небо с абсолютно честным atmospheric light scattering. Есть вот такая симпатичная текстурка луны:</p>
<p><img decoding="async" loading="lazy" alt="Moon" src="https://romanmarchenko.me/assets/images/moon-90fb2c5b55da734bb4690a538d955736.jpg" width="128" height="128" class="img_ev3q"></p>
<p>Террейн и некоторые элементы сцены рисуются в 2 прохода. Первым проходом идет базовый цвет, вторым же накладываются детальный рельеф, каустика, etc.
В конце volumetric fog, опять же с использованием scene camera z текстурки. Получилось так:</p>
<p><img decoding="async" loading="lazy" alt="Main Pass" src="https://romanmarchenko.me/assets/images/main_pass-4b1c3bd386b14eda1a3f833111155aab.jpg" width="1024" height="768" class="img_ev3q"></p>
<p>После основного прохода из текстуры со сценой мы выделяем яркие фрагметны, которые мы блурим, для получения блум текстуры.
Считаем среднюю яркость сцены для перевода HDR в LDR в 1x1 текстурку.
Теперь пора применить всякие пост-процесс эффекты.
Motion blur: Блурим картинку по направлению движения камеры с учетом дальности пикселя (опять используем scene z) Ближние будут блурится сильнее. Применяется коррекция ликов цвета с оружия.</p>
<p><img decoding="async" loading="lazy" alt="Motion Blur" src="https://romanmarchenko.me/assets/images/motion_blur-4790f286cf7cb51e0b3e50a8974345ff.jpg" width="1030" height="793" class="img_ev3q"></p>
<p>Depth of field: Даунсемпл и блур изображения сцены. Затем простое смешивание с учётом глубины.
И ещё всяких много: chroma shift, и radial blur когда колбасят; эффект мокрого экрана, когда из воды вылазишь, и т.д.
Далее идёт tonemap resolve, т.е. перевод из HDR в LDR.</p>
<p>Но это ещё не всё! :) Есть ещё 1 красивый эффект - геренация light shafts. Метод похож на описанный в статье "Volumetric Light Scattering as a Post-Process" в GPU Gems 3. В текстурном пространстве пускаем луч в направлении солнышка. Двигаясь по лучу проверяем загораживет ли слонце какоё-то предмет, сравнивая Z с 1 (0 - near plane, 1 - far). Количество загороженых семплов и есть степень загороженности солнца предметами. Аттенюация по дальности также пристутсвует.</p>
<p><img decoding="async" loading="lazy" alt="Light Shafts" src="https://romanmarchenko.me/assets/images/sun_shafts1-2de6beff8119dd8d5b466341ac50b03a.jpg" width="512" height="384" class="img_ev3q"></p>
<p>После 3-х таких проходов с различным шагом (чтобы убрать артефакты дискретного семплинга), получаем таку. картиночку:</p>
<p><img decoding="async" loading="lazy" alt="Light Shafts 3" src="https://romanmarchenko.me/assets/images/sun_shafts3-03256fb99d1b92a7378939415cb8976f.jpg" width="512" height="384" class="img_ev3q"></p>
<p>Финальная композиция выполняется с применением т.н. Soft Light Blend Mode, аналогичного фотошоповскому. Данный метод позволяет осветлить одни участки изображения и затемнить другие. Почитать про различные режимы блендинга и их формулы можно тут: <a href="http://www.pegtop.net/delphi/articles/blendmodes/softlight.htm" target="_blank" rel="noopener noreferrer" class="">http://www.pegtop.net/delphi/articles/blendmodes/softlight.htm</a>
Финальное изображение:</p>
<p><img decoding="async" loading="lazy" alt="Light Shafts Final" src="https://romanmarchenko.me/assets/images/sun_shafts_final-2b2a98d685c9a0daf50311b37d096dad.jpg" width="1024" height="768" class="img_ev3q"></p>
<p>Гуи. По элементику, по буковке ~100 draw calls.
Рендер завершен.</p>
<p>А, там же ещё снег есть. :) Сильно его не ковырял. Он адванснутый. Основная идея: отталкиваемся от отношения вертикальной оси и нормали к поверхности для создания слоя. Остальное это создание нерегулярной его структуры и вида.</p>
<p>В кадре 2М триугольников, размер вертексов выравнивать не сильно старались. Draw calls доходит до 2000. Почти все текстуры в DXT сжатии. Нормали в BC5 (ATI2N).
Собственно всё. Что хочется отметить, так это повсеместное использование camera z буфера. Почти во всех эффектах.</p>
<p>Хочется поблагадорить команду Кризиса за замечательный экспириенс, и Серегу kss за апдейт мегатулзы, без которой данное увлекательное приключение не состоялось бы. :)</p>]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[Clive Barker's Jericho]]></title>
        <id>https://romanmarchenko.me/ru/blog/jericho</id>
        <link href="https://romanmarchenko.me/ru/blog/jericho"/>
        <updated>2007-10-12T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[После того, как демка игры Clive Barker’s Jericho появилась для свободного скачивания, многие люди оценили графику в ней как «лучшую которую они видели на сегодняшний момент». Как вы понимаете, я не мог пропустить такую игру.]]></summary>
        <content type="html"><![CDATA[<p>После того, как демка игры Clive Barker’s Jericho появилась для свободного скачивания, многие люди оценили графику в ней как «лучшую которую они видели на сегодняшний момент». Как вы понимаете, я не мог пропустить такую игру.</p>
<p>Конфигурация компьютера: A64 X2 4200+, GeForce 8800 GTX, 2GB Ram. Все настройки игры на максимум.</p>
<p>После запуска, невооруженным глазом сразу видно, что игра сильно нагружена спецеффектами, поэтому, как я и ожидал, первым проходом идёт заполнение scene depth буфера. Это не чисто only-z проход. На нем в R32F рендер таргет выводится camera space z. Полученная текстура далее используется во множестве случаев. Все последующие проходы геометрии используют полученный z-буфер устанавливая DepthWrite в false.</p>
<p>Движок игры многопроходный. Для каждого источника света существует отдельный проход рендера геометрии. Если источник должен отбрасывать тень, то первым идет рендер в R32F текстуру тени (omni shadows замечены небыли). Затем отрисовывается вся геометрия, на которую влияет данный источник. Во время рендера используются:
Диффузная текстура (сделайте монитор поярче, картиночки темноваты):</p>
<p><img decoding="async" loading="lazy" alt="Diffuse" src="https://romanmarchenko.me/assets/images/diffuse-d4609a94fda26dd74d018e22b7bad346.jpg" width="512" height="512" class="img_ev3q"></p>
<p>Нормал мапа:</p>
<p><img decoding="async" loading="lazy" alt="Normal" src="https://romanmarchenko.me/assets/images/normal_map-012d73c1144e89928e899316001ce5e7.jpg" width="512" height="512" class="img_ev3q"></p>
<p>Примняется DXT5 сжатие. X и Y компоненты вектора лежат в G и A каналах. Данный способ сжатия нормал мап широко известен. Z вычисляется как sqrt(1 – x^2 – y^2), так как нормали у нас единичной длины и мы знаем, что Z координата в tangent space normal maps всегда положительное число. Более подробно про сжатие карт номалей можно почитать тут и тут
Так как R компонента пустует, то её можно использовать для хранения чего-то полезного (но не критичного к качеству). В Jericho данный канал текстуры хранит низкочастотное значение высоты, или, если говорить иностранным языком - low frequency height map. Используется данный канал для самого простого параллакс маппинга. Ни Parallax Occlusion Mapping (URL), ни Relief Mapping не реализованы. Канал B текстуры не используется.
Height map:</p>
<p><img decoding="async" loading="lazy" alt="HM" src="https://romanmarchenko.me/assets/images/height_map-38e0201e1478bf0c3e581578b81dbec9.jpg" width="512" height="512" class="img_ev3q"></p>
<p>Также присутствует specular map:</p>
<p><img decoding="async" loading="lazy" alt="SpecMap" src="https://romanmarchenko.me/assets/images/specular-5a848eca7656160b4e48f64d9dabb91a.jpg" width="256" height="256" class="img_ev3q"></p>
<p>Кубемапа с бликами приассоциированная к объекту (для оружия например):</p>
<p><img decoding="async" loading="lazy" alt="Cubemap" src="https://romanmarchenko.me/assets/images/weapon_cubemap-2ddc4681ad98867a4f2d4eb1646ddcd5.jpg" width="384" height="512" class="img_ev3q"></p>
<p>Также используется кубемапа привязанная к висящим на потолке фонарикам.
Для правильной интерпретации цвета, которая должна давать последняя кубемапа, в шейдер передается также матрица положения соответствующего физического объекта. Таким образом трансформируя вектор направления «рисуемый объект» -&gt; «фонарь» с помощью этой матрицы мы получаем красивенькие динамические блики на объектах вокруг данного светоизлучателя:</p>
<div style="width:640px;height:360px"></div>
<p>Сцена с одним источником:</p>
<p><img decoding="async" loading="lazy" alt="Light1" src="https://romanmarchenko.me/assets/images/light1-c1a3f9781dffc0db504751961871d152.jpg" width="1280" height="1024" class="img_ev3q"></p>
<p>с двумя:</p>
<p><img decoding="async" loading="lazy" alt="Light2" src="https://romanmarchenko.me/assets/images/light2-311245708ad12cef03895f72281a3a21.jpg" width="1280" height="1024" class="img_ev3q"></p>
<p>с тремя:</p>
<p><img decoding="async" loading="lazy" alt="Light3" src="https://romanmarchenko.me/assets/images/light3-a1781c2ea189a8bfefecd5e9f47bf2a4.jpg" width="1280" height="1024" class="img_ev3q"></p>
<p>После всех проходов геометрии, рисуется вода. Вернее, это та субстанция похожая на кровь, которая в изобилии встречается на демоуровне. Тут применяются 3 скроллящиеся с разными скоростями и в разные стороны карты нормалей. В общем ничего примечательного.</p>
<p>Далее идут частицы, которые забатчены довольно большими группами. К слову много частиц рисуется с аддитивным блендингом, создавая таким образом эффект volumetric light and fog.
До партиклов:</p>
<p><img decoding="async" loading="lazy" alt="Without Particles" src="https://romanmarchenko.me/assets/images/without_particles-adb12fa910b11262828ddd43228e8f11.jpg" width="640" height="512" class="img_ev3q"></p>
<p>После партиклов:</p>
<p><img decoding="async" loading="lazy" alt="With Particles" src="https://romanmarchenko.me/assets/images/with_aprticles-a0e2972201ef885f7f6fe2dc06ee957e.jpg" width="640" height="512" class="img_ev3q"></p>
<p>Depth of Field. Для достижения данного эффекта сцена даунсемплится в РТ размером ¼ от оригинального, а затем 2 раза размывается. Полученная текстура комбинируется с исходным изображением, используя метод описанный ребятами из АТИ в ShaderX3. Ближняя плоскость, дальняя плоскость, фокусное расстояние, leak reduction используя scene depth texture – всё присутствует. Хочу отметить, что фокусное расстояние определяется динамически, в зависимости от объекта на который мы сейчас смотрим (простое пересечения луча с ближайшим объектом). Этим достигается плавное изменение степени заблуривания объектов.
Как я и предполагал, качество видео в Youtube не позволит проиллюстрировать DoF с переменным фокусным расстоянием, поэтому будет 2 картиночки (обратите внимание на дальнюю освещенную колонну):</p>
<p><img decoding="async" loading="lazy" alt="Dof1" src="https://romanmarchenko.me/assets/images/dof1-c0c63889a3eced5ec363851f6274310f.jpg" width="1280" height="1024" class="img_ev3q">
<img decoding="async" loading="lazy" alt="Dof2" src="https://romanmarchenko.me/assets/images/dof2-a641f4868ca28e80c90c30143c3a8294.jpg" width="1280" height="1024" class="img_ev3q"></p>
<p>После DoF идут т.н. “image distortion” пост-процесс эффекты. Подход достаточно оригинален и необычен. В отдельную ARGB8 текстуру рисуются все смещения текстурных координат (используются нормал мапы), которые должны искажать картинку. Это и эффект мокрого экрана (капли на стекле), и heat-haze от огня и эффект телекинеза. Затем используя полученную динамическую карту смещений и изображение сцены полученной на предыдущих шагах делаем distortion эффекты.
Динамическая карта смещений:</p>
<p><img decoding="async" loading="lazy" alt="Offset Map" src="https://romanmarchenko.me/assets/images/pp_offsets-f6c0fae60f9a45cc9228856805c06d34.jpg" width="1280" height="1024" class="img_ev3q"></p>
<p>Полученная картинка:</p>
<p><img decoding="async" loading="lazy" alt="PP" src="https://romanmarchenko.me/assets/images/pp-1b37b804ff7066fe07b586a09c1152da.jpg" width="1280" height="1024" class="img_ev3q"></p>
<p>Следующим идёт генерация velocity буфера для motion blur эффекта.
Подход такой же как и во многих других играх. В шейдер передаются предыдущая и текущая матрицы трансформации. После их применения вычисляется вектор смещения в XY плоскости, который записывается в ARGB8 текстуру. Дабы увеличить точность значений и зря не расходовать память а R канале содержится +Х, в G –X. В каналах B и A содержится +Y и –Y соответственно.
Визуализация velocity буфера в момент включения зума на снайперке у Black (она один из 3-х играбельных персонажей в демке).</p>
<p><img decoding="async" loading="lazy" alt="Velocity Buffer" src="https://romanmarchenko.me/assets/images/velocity_buffer-b996b24ed8299f8cdbc6326d4f4d38af.jpg" width="1280" height="1024" class="img_ev3q"></p>
<p>На следующем этапе используя полученный буфер размываем изображение:</p>
<p><img decoding="async" loading="lazy" alt="Motion Blur" src="https://romanmarchenko.me/assets/images/motion_blur-b6dc51d45e30cab3e809d037451d8214.jpg" width="1280" height="1024" class="img_ev3q"></p>
<p>и видео:</p>
<div style="width:640px;height:360px"></div>
<p>На следующем шаге идет очередной пост-процесс эффект. Сцена даунсемплится и размывается в несколько шагов до 1х1 изображения в котором хранится средняя яркость. Используя эту мини-текстуру выполняется bright pass, или говоря нашим, родным языком, выделяются яркие области. Текстура с яркими областями подвергается «диагональному» размыванию в виде буквы Х. Вверх-вправо, вверх-влево, вниз-вправо, вниз-влево. Полученные лучи крестообразных бликов собираются в одно изображение и блендится с оригинальной картинкой.</p>
<p>Обратите внимание на glow в районе руки:</p>
<p><img decoding="async" loading="lazy" alt="Star Glow" src="https://romanmarchenko.me/assets/images/star_glow-d9898d5a0ffe4a530b46dbc48436a8a7.jpg" width="1280" height="1024" class="img_ev3q"></p>
<p>Последни пунктом идет UI, которого совсем немного.</p>
<p>С 5-ю активными источниками получается где-то 1800 dip’ов и до 400К полигонов в кадре. Средние значения: 1200 draw calls и 200K полигонов. Вся геометрия в невыровненных вертекс-форматах, хотя данные немного сжаты: позиция во float3 и нормали в short4n. В движке хорошая сортировка как по шейдерам, так и по текстурам. ЦПУ в драйвере не сидит, что тоже хорошо. Смущает только объем загруженных в видеопамять текстур. Аж 550 мегабайт. Вот вам и хай-энд :)</p>
<p>В послесловии я хочу подлиться своими соображениями по поводу многопроходности. Раньше я думал, что с длинными шейдерами данный подход себя изжил. Но данный реверс отчетливо показал, что мои выводы были преждевременными. Главное это бороться за минимизацию pixel-processing overhead и количество батчей. Рендер Clive Barker’s Jericho демонстрирует отличные показатели по обоим пунктам.</p>]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[Что такое PCF, и с чем его едят.]]></title>
        <id>https://romanmarchenko.me/ru/blog/pcf</id>
        <link href="https://romanmarchenko.me/ru/blog/pcf"/>
        <updated>2007-10-03T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[В комментариях к предыдущему реверсу, товарищ uncle_lag попросил меня поподробнее рассказать, что такое PCF. Я так увлёкся, что решил, что данная тема достойна отдельного постинга.]]></summary>
        <content type="html"><![CDATA[<p>В комментариях к предыдущему реверсу, товарищ <code>uncle_lag</code> попросил меня поподробнее рассказать, что такое PCF. Я так увлёкся, что решил, что данная тема достойна отдельного постинга.</p>
<p>Для смягчения тени, мы не можем блурить shadow map (в привычном понимании этого слова), потому что в ней содержится глубина пикселя с позиции источника света. Операция разблуривания (усреднения соседних значений) глубины не имеет смысла. PCF алгоритм заключается в том, чтобы вместо того, чтобы проверить 1 пиксель на затенённость, проверяется несколько пикселей по заранее заданному filter kernel. Затем полученные значения (1 - в тени, 0 - не в тени) усредняются. Т.о. если в тени оказались всего 3 пикселя из 4-х, то степень затенённости можно установить в 0.75 (либо 75%), что создаёт полутень (переход от затенённой области в светлую). Данный алгоритм, в варианте квадратной области размером 2х2, реализован аппаратно в NVidia hardware, как составная часть т.н. NV hardware shadowmap hack, начиная с GeForce 3. Можно отметить, что для 2х2 алгоритма мы сможем иметь на выходе, только 5 градаций интенсивности затенения (0, 0.25, 0.5, 0.75 и 1), что приведет к довольно заметной "блочной" структуре полутени.</p>
<p>Тень без PCF</p>
<p><img decoding="async" loading="lazy" alt="No PCF" src="https://romanmarchenko.me/assets/images/blocky_shadow-f5da7a55bde15b84539e082885d5b246.jpg" width="897" height="641" class="img_ev3q"></p>
<p>Тень с 2x2 PCF без линейной интерполяции</p>
<p><img decoding="async" loading="lazy" alt="PCF22" src="https://romanmarchenko.me/assets/images/without_lerp-896ddbd0a70643336c2cbabb2d845cc4.jpg" width="906" height="644" class="img_ev3q"></p>
<p>Для этого на NV железе выполняется ещё и линейная интерполяция полученных значений по точному положению экранного пикселя, в пространстве карты тени. Алгоритм достаточно прост. Post-perspective XY координаты текущего пикселя в пространстве SM имеют координаты [0..1]. Домножая их на размер карты тени, в целой части результата мы получим координаты текселя карты тени соответсвующего данному экранному пикселю. В дробной же части у нас будет субтексельное положение пикселя внутри текселя sm.</p>
<p>Пояснительная картиночка:</p>
<p><img decoding="async" loading="lazy" alt="I1" src="https://romanmarchenko.me/assets/images/sm1-3dfeb4bcfc9f75b0ff0acfd561b48760.jpg" width="512" height="452" class="img_ev3q"></p>
<p>На ней изображён маппинг одного shadow texel в 3 screen pixels. Этот случай является как раз тем пресловутым perspective aliasing'ом с которым более-менее успешно борются все перспективные техники (PSM, TSM, LisPSM и др). Но речь сейчас не об этом. В нашем примере, координаты каждого из 3-х пикселей, будут иметь одно и то же целочисленное значение в пространстве карты теней. Но дробная часть координаты будет отличаться. Она то и является той субпискельной точностью, которая поможет нам ещё более сгладить края затенённых областей. Для первого сверху экранного пикселя она будет примерно 0.8, для среднего 0.5, а для нижнего 0.2.</p>
<p>Теперь рассмотрим вид с позиции карты тени (увеличенная 2х2 область):</p>
<p><img decoding="async" loading="lazy" alt="I2" src="https://romanmarchenko.me/assets/images/sm2-df1d9d32f23036b8a7534a8cf4744e15.jpg" width="512" height="512" class="img_ev3q"></p>
<p>Синим кружочком показано субтексельное положений экранного пикселя на карте тени. Цифрами - результат теста глубины из sm, c глубиной тестируемого экранного пикселя (1 - в тени, 0 - нет). Возьмём верхних 2 текселя shadow map'ы. При подходе влоб (без учёта субтексельного положения), степень затенённости в результате будет 0.5 ((1 + 0) / 2). Но мы знаем, что субтексельная позиция пикселя по оси u в нашем случае равна 0.2 (примерно). Следовательно, вместо простого усреднения соседних значений, мы можем сделать их линейную интерполяцию по субтексельному положению. Т.о. степень затенённости в нашем случае будет x + s(y - x) = 1 + 0.2(0 - 1) = 0.8, двигаясь дальше, вдоль оси u мы будем получать всё меньшее значение затенённости пикселя, т.о получая плавный градиент затенения. Тоже самое проделывается и по оси v.</p>
<p>Именно таким образом инженеры NV реализовали этот алгоритм в железе.
Результат налицо:</p>
<p><img decoding="async" loading="lazy" alt="PCF Lerp" src="https://romanmarchenko.me/assets/images/with_lerp-d09815413bfbcabeb0648267717db199.jpg" width="907" height="644" class="img_ev3q"></p>
<p>Фрагмент кода шейдера для эмуляции поведения NV видеокарт на ATI:</p>
<div class="language-nasm codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-nasm codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> Perform Percentage Closer Filtering</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> First determine lerp amounts</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">float4 la</span><span class="token comment" style="color:#999988;font-style:italic">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">float2 texLA </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> frac(fragPos.xy </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> shadowMapSize.xy)</span><span class="token comment" style="color:#999988;font-style:italic">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> Gather samples</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> Fetch </span><span class="token number" style="color:#36acaa">4</span><span class="token plain"> neighbour points</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">depth.x </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> tex2D(shadow_s, fragPos).r</span><span class="token comment" style="color:#999988;font-style:italic">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">depth.y </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> tex2D(shadow_s, fragPos </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> float4(shadowMapSize.z, </span><span class="token number" style="color:#36acaa">0.0</span><span class="token plain">, </span><span class="token number" style="color:#36acaa">0.0</span><span class="token plain">, </span><span class="token number" style="color:#36acaa">0.0</span><span class="token plain">)).r</span><span class="token comment" style="color:#999988;font-style:italic">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">depth.z </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> tex2D(shadow_s, fragPos </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> float4(</span><span class="token number" style="color:#36acaa">0.0</span><span class="token plain">, shadowMapSize.w, </span><span class="token number" style="color:#36acaa">0.0</span><span class="token plain">, </span><span class="token number" style="color:#36acaa">0.0</span><span class="token plain">)).r</span><span class="token comment" style="color:#999988;font-style:italic">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">depth.w </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> tex2D(shadow_s, fragPos </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> float4(shadowMapSize.z, shadowMapSize.w, </span><span class="token number" style="color:#36acaa">0.0</span><span class="token plain">, </span><span class="token number" style="color:#36acaa">0.0</span><span class="token plain">)).r</span><span class="token comment" style="color:#999988;font-style:italic">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> Check visibility for all points</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">depth </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> depth </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain"> fragPos.zzzz ? </span><span class="token number" style="color:#36acaa">0.0</span><span class="token plain"> : </span><span class="token number" style="color:#36acaa">1.0</span><span class="token comment" style="color:#999988;font-style:italic">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> Final shadow factor</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">return lerp</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">(</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    lerp(depth.x, depth.y, texLA.x),</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    lerp(depth.z, depth.w, texLA.x),</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    texLA.y</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">)</span><span class="token comment" style="color:#999988;font-style:italic">;</span><br></div></code></pre></div></div>
<p>Вопросы?</p>]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[Dirt - технологически всё чистенько.]]></title>
        <id>https://romanmarchenko.me/ru/blog/dirt</id>
        <link href="https://romanmarchenko.me/ru/blog/dirt"/>
        <updated>2007-10-02T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Этот пост посвящается памяти Колина Макрея.]]></summary>
        <content type="html"><![CDATA[<p>Этот пост посвящается памяти Колина Макрея.</p>
<p>Привет, друзья. Материалы по Dirt'у у меня были уже в основном готовы с середины июня, но множество всяких обстоятельств (хороших в основном :)) заставили отложить финальное оформление готового репорта до осени. И вот, набравшись сил, я решил завершить начатое.</p>
<p>Стандартная ремарка: компьютер A64 X2 4200+, GF8800GTX, WinXP SP2, 2GB RAM. Игра реверсилась в максимальных настройках в разрешении 1024х768.</p>
<p>Рендер игры оказался технологичным, и достаточно простым для реверсинга. Назначение практически всех шагов отрисовки, используемых текстур и констант были понятны сходу. Не требовалось долгих и утомительных анализов асмового кода шейдеров.</p>
<p>Первым этапом рисуется динамическая миникарта. Это фрагмент карты плюс индикаторы машинок соперников и индикатор нашего транспортного средства. Рисуется квадратами по 2 треугольника на каждый draw call.</p>
<p>Следующим идет генерация динамической кубемапы для отражений. Фотографируем сцену с позиции камеры. Автомобили, кусты, людей и всякую мелочевку не рисуем.</p>
<p><img decoding="async" loading="lazy" alt="Dyn Cubemap" src="https://romanmarchenko.me/assets/images/dyn_cubemap-0f63a1839a1822a802f1b66611cf74ad.jpg" width="1536" height="2048" class="img_ev3q"></p>
<p>Далее велосити буфер + scene depth. Используется текстура формата A16R16G16B16F. В rg каналах 2D вектор смещения точки с предыдущего кадра. Всё так же как и в моем предыдущем обзоре Lost Planet. В b глубина сцены во view space (т.е. не нормализированная). Этот RT используется в практически всех остальных этапах отрисовки.</p>
<p><img decoding="async" loading="lazy" alt="Velocity" src="https://romanmarchenko.me/assets/images/vel_buffer-20490b1dba7f8deee4f964041cca8141.jpg" width="1030" height="756" class="img_ev3q"></p>
<p>Dynamic ambient occlusion pass. Нет, это не тот AO, который описан в GPU Gems 2. :) Это такая терминология разработчиков. В этом проходе создается RGBA8 текстура размеров в 8 раз меньше размеров вьюпорта (т.е. 128х96 в нашем случае).Используется только R канал, в который отрисовываются автомобили в определенном (небольшом) радиусе от камеры. Далее эта текстура размывается по горизонтали и вертикали, и используется как мягкая тень от машинок при рендеринге земли и объектов на ней.</p>
<p><img decoding="async" loading="lazy" alt="Dyn AO" src="https://romanmarchenko.me/assets/images/dyn_ao-38b59215287d6d62e079779565b4ccb7.jpg" width="128" height="96" class="img_ev3q"></p>
<p>Следующий пункт – тени. Использются 3 R32F 2Kx2K текстуры. Все они центрированы по камере и покрывают разные площади.</p>
<p><img decoding="async" loading="lazy" alt="SM1" src="https://romanmarchenko.me/assets/images/sm1-88a4c2f572e725602da337e170f4960e.jpg" width="916" height="974" class="img_ev3q">
<img decoding="async" loading="lazy" alt="SM2" src="https://romanmarchenko.me/assets/images/sm2-5d995f4918342632a69f65cf4ed88a6c.jpg" width="916" height="974" class="img_ev3q">
<img decoding="async" loading="lazy" alt="SM3" src="https://romanmarchenko.me/assets/images/sm3-bfa582341d8acd1b997a0b3f447e1efa.jpg" width="916" height="974" class="img_ev3q"></p>
<p>Затем, используя scene depth текстуру и все 3 шадовмапы разработчки создают т.н. маску тени.</p>
<p><img decoding="async" loading="lazy" alt="ShadowMask" src="https://romanmarchenko.me/assets/images/shadow_mask-e0807a247cee1be45559c003672065bd.jpg" width="1024" height="768" class="img_ev3q"></p>
<p>Это достигается путем исполнения огромного шейдера в 250 инструкций. Зная scene depth и позицию пикселя в пространстве экрана можно вычислить его позицию в пространстве камеры. Затем стандартный SM. Из-за того, что приходится блендить 3 карты и тут же применять PCF и получаем столь длинный код.
На маске с тенями всё ещё достаточно хорошо видны артефакты теней и PCF алгоритма, поэтому полученная текстура ещё разок легко размывается, благо это уже обычное изображение, а не глубина сцены. Можно отметить, что при таком подходе размытые участки с тенью могут вылезти за пределы мешей на которых они лежат, образуя артефакты. Именно поэтому, используется лёгкое размытие. На финальной картинке артефактов невидно.</p>
<p><img decoding="async" loading="lazy" alt="Blurred SM" src="https://romanmarchenko.me/assets/images/blurred_shadow_mask-33e89fd35fb982b41be34d5e43ef6d7a.jpg" width="1024" height="768" class="img_ev3q"></p>
<p>Когда все подготовительные работы закончены, начинается основной проход. Front to back всё как надо. Сначала машинки. Шейдеры тут используются достаточно тяжёлые с точки зрения текстурной нагрузки. Для рендеринга авто используются диффузная текстура, карта нормалей, маска грязи, амбиент кубемапа, кубемапа с реалтайм отражениями, текстура с посчитанными тенями, текстура с повреждениями и спекулярная текстура с повреждениями. Все текстуры большие (1Kx1K)</p>
<p><img decoding="async" loading="lazy" alt="Damage Map" src="https://romanmarchenko.me/assets/images/damage_map-789e0223818526c4fb39e54179fd7fd3.jpg" width="1024" height="1024" class="img_ev3q"></p>
<p>Dirt map:</p>
<p><img decoding="async" loading="lazy" alt="Dirt Map" src="https://romanmarchenko.me/assets/images/dirt_map-2180512540aa237723fe994d6694d27d.jpg" width="1024" height="1024" class="img_ev3q"></p>
<p>Карта грязи имеет формат DXT5, и в своих каналах содержит:
Маску для грязи одного цвета (red):</p>
<p><img decoding="async" loading="lazy" alt="Dirt Map R" src="https://romanmarchenko.me/assets/images/dirt_r-fe6a2187e5ba98835c5a9489dc5ffe95.jpg" width="256" height="256" class="img_ev3q"></p>
<p>Маску для грязи второго цвета (blue):</p>
<p><img decoding="async" loading="lazy" alt="Dirt Map B" src="https://romanmarchenko.me/assets/images/dirt_b-ac6e46d2bd5f9192aacf41f806949478.jpg" width="256" height="256" class="img_ev3q"></p>
<p>Коэффициент затемнения грязевого слоя. Он являет собой результат формулы вертикального вектора (перпендикулярного земли) и нормали модели авто в этой точке. Используется вариация формулы wrap-around освещения (или напрямую, не суть важно)</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">сolour * clamp(N.L+factor)/(1+factor)</span><br></div></code></pre></div></div>
<p>Это сделано для того, чтобы угол в 90 градусов и больше не превращал всё в черноту.</p>
<p><img decoding="async" loading="lazy" alt="Dirt Map G" src="https://romanmarchenko.me/assets/images/dirt_g-af4b4cd918ce526c3f82b563853cdda0.jpg" width="256" height="256" class="img_ev3q"></p>
<p>В альфе содержится маска областей, которые не должны быть загрязнены (либо загрязнены частично). Эти области обычно включают надписи на авто.</p>
<p>Вот картиночки чистых и грязных машинок:</p>
<p><img decoding="async" loading="lazy" alt="Clean Car" src="https://romanmarchenko.me/assets/images/clean_car-7ed3d7fb71830136441dc8309e932c3b.jpg" width="1024" height="768" class="img_ev3q"></p>
<p><img decoding="async" loading="lazy" alt="Dirt Car" src="https://romanmarchenko.me/assets/images/dirty_car-24b169a0b76017fa3d1ad3f1cf81f4d3.jpg" width="1024" height="768" class="img_ev3q"></p>
<p>Освещаются машинки (как и всё остальное в игре) одним солнышком. Авто, при максимальное детализации помещается в 100 DIPов. По мере отдалённости мелкие детали в авто не рисуются. LOD'а шейдеров нет. Водители и зрители скинятся софтварно.
Для отрисовки земли/дорог используются несколько фотографий местности c воздуха (размером 512x512) и несколько текстур деталей. Для бленда слоев предназначена следующая текстура (оригинальный размер 1024^2, слои отдельно рядом):</p>
<p><img decoding="async" loading="lazy" alt="Ground Blend Map" src="https://romanmarchenko.me/assets/images/ground_blend_map-8bb57a61aa88d3e835228316bc6d9aa4.jpg" width="1024" height="512" class="img_ev3q"></p>
<p>Для всех диффузных текстур есть сопутствующая нормалмапа. Всё освещается попиксельно, от солнца.</p>
<p><img decoding="async" loading="lazy" alt="Satellite" src="https://romanmarchenko.me/assets/images/satellite_photo-369c8b1ac7d1223e227994d5aa78ab3e.jpg" width="512" height="512" class="img_ev3q"></p>
<p><img decoding="async" loading="lazy" alt="Satellite NM" src="https://romanmarchenko.me/assets/images/satellite_photo_nm-8e22fdb01546cce7b48008051db7c6b9.jpg" width="512" height="512" class="img_ev3q"></p>
<p>Деревья SpeedTree’шные. В конце полусфера неба, на текстуре которого нарисованы самый дальний слой леса.
Все модели хранятся в своих VB/IB. Выравнивания по cache-friendly sizes отсутвует. Используемые вертексформаты декларируют вертексы с размерами 28, 36, 40, 48 и 60 байт (некоторые меши освещаются повертексно). Частицы мягкие. Для сглаживания пересечений геометрии используется текстура со scene depth информацией подготовленная ранее.
Получилась вот такая картинка:</p>
<p><img decoding="async" loading="lazy" alt="Final Scene" src="https://romanmarchenko.me/assets/images/scene_completed-e28125825a32b3c1323b873e91c4d3ad.jpg" width="1024" height="768" class="img_ev3q"></p>
<p>Затем в 5 этапов даунсемплим картинку до 1х1 с посчитанной scene luminance. Используя velocity буфер и яркость сцены блурим и модифицируем освещение финальной картинки соответсвенно. К слову motion blur используется даже в главном меню игры.</p>
<p><img decoding="async" loading="lazy" alt="Final Scene HDR" src="https://romanmarchenko.me/assets/images/scene_after_hdr-f2eccc84c3b0abb9e7a4d852e40c65f9.jpg" width="1024" height="768" class="img_ev3q"></p>
<p>Затем блурим ещё 2 раза для создания glow областей. Финальное изображение:</p>
<p><img decoding="async" loading="lazy" alt="Final Image" src="https://romanmarchenko.me/assets/images/final_image-3cfbaead1cefcd6b9136b35629f42da4.jpg" width="1024" height="768" class="img_ev3q"></p>
<p>UI поэлементно, можно было батчить лучше. Буквы – модельки. Текст батчится побуквенно, т.е. все одинаковые буквы, если встречается несколько раз, отрисовываются за 1 draw call.</p>
<p>Вот и всё, собственно. Хотелось бы отметить, что для для этого исследования не потребовался PIXWin вообще. 4-м перфхудом экспортил текстуры и рендертаргеты. 5-м же модифицировал шейдеры, чтобы понять как работают неочевидные вещи.</p>]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[Lost Planet: Extreme Condition - консольный пришелец.]]></title>
        <id>https://romanmarchenko.me/ru/blog/lostplanet</id>
        <link href="https://romanmarchenko.me/ru/blog/lostplanet"/>
        <updated>2007-06-06T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Что-то я обленился в последнее время. :) Весна наверное. Сорри, за долгий перерыв.]]></summary>
        <content type="html"><![CDATA[<p>Что-то я обленился в последнее время. :) Весна наверное. Сорри, за долгий перерыв.</p>
<p>Итак, сегодня мы будем оперировать продукт японско-консольного мира, который намеревается вот-вот появиться на РС. Это игра Lost Planet: Extreme Condition. Полной версии игры у меня, естественно, нет, но есть публично доступная демка. Саму игру я впервые увидел на Xbox360, и она меня технологически впечатлила. Когда я решил побороть свою лень, и пореверсить что-нибудь, демо-версия была уже довольно долгое время доступна, и я выбрал её.</p>
<p>Конфигурация моей машины со времени последних обзоров не изменилась: A64 X2 4200+, GF8800GTX, 2GB RAM. Все настройки на максимум.</p>
<p>Сразу оговорюсь, что ревресилась DX9 версия. О DX10 я напишу ниже.</p>
<p>Общие впечатления по движку очень хорошие. Куча cutting-edge технологий, плюс виден очень грамотный подход к реализации. Весь SM3.0 only. На двух уровнях ~500DIPs на пустой сцене (персонаж + террейн), и до 1500 в бою с несколькими противниками. Вся геометрия, кроме совсем уж отдельных случаев, таких как интерфейс, имеет один и тот же вертекс формат:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">0 SHORT4N POSITION 0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">8 UBYTE4 BLENDINDICES 0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">12 UBYTE4N BLENDWEIGHT 0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">16 UBYTE4N NORMAL 0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">20 UBYTE4N TANGENT 0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">24 FLOAT16_2 TEXCOORD 0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">28 FLOAT16_2 TEXCOORD 1</span><br></div></code></pre></div></div>
<p>Итого 32 байта. Что бросается в глаза:</p>
<ol>
<li class="">Очень vertex cache friendly.</li>
<li class="">Много входных данных, которые хорошо соптимизированны.
• Позиция вместо 12 байт (float3) – 8 байт.
• Веса костей вместо 16 байт (float4) – 4 байта.
• Нормаль и тангент вместо 12 байт каждый (float3) - по 4 байта.
• Текстурные координаты вместо 8 байт каждый (float2) – по 2 байта.
В этом нет ничего экстраординарного, я для сравнения привёл то, как делают обычно. Просто видна аккуратность подхода. Все данные должны быть аккуратно созданы, чтобы не вылезти за допустимые границы.</li>
</ol>
<p>Благодаря красивому вертексу, а ещё и высокому быстродействию vertex processor’a современных видечипов, практически вся игровая геометрия (кроме террейна) рисуется шейдером со скиннингом. У статичных моделек значащая матрица всего одна – она является world матрицей объекта.</p>
<p>Множество мешей содержатся в одном VB и IB, которые рисуются с помощью оффсетов.</p>
<p>В игре используются динамически генерируемые кубемапы. Схема работы алгоритма хитрая. Изображение шотится в cubemap в позиции главного персонажа, раз в несколько кадров (возможно генерация фейсов размазана по кадрам, я, к сожалению, этот момент не поймал). Вместе с текущим отражением хранится ещё и предыдущее в отдельной текстуре. В начале каждого кадра в третью кубемапу блендятся предыдущее и текущее изображение, с учётом весов. Таким образом, производится плавный переход одной environment map’ы к другой. Чтобы лучше проиллюстрировать процесс, приведу несколько картинок:</p>
<p>Фейс кубемапы с текущим отражением:</p>
<p><img decoding="async" loading="lazy" alt="Cube Face Cur" src="https://romanmarchenko.me/assets/images/cur_refl-d040e3382fb86ca8b815141bec7a7c86.jpg" width="700" height="700" class="img_ev3q"></p>
<p>Фейс кубемапы с предыдущим отражением:</p>
<p><img decoding="async" loading="lazy" alt="Cube Face Prev" src="https://romanmarchenko.me/assets/images/prev_refl-96aa694637570daf8f15658104049792.jpg" width="700" height="700" class="img_ev3q"></p>
<p>Скомбинированное отражение (обратите внимание на видимый ghost-image здания):</p>
<p><img decoding="async" loading="lazy" alt="Cube Face Combined" src="https://romanmarchenko.me/assets/images/combined_refl-d5b15513c17647588702e16c21205a78.jpg" width="700" height="700" class="img_ev3q"></p>
<p>Кубемапы размером 128х128 A8R8G8B8. Бледнятся все мип-уровни. Визуально полезность данного технического решения определить я не смог, так как на уровнях, где можно побегать, хороших отражающих объектов я не заметил. На воде (о ней ниже), где сгенерированная envmap’a тоже используется, ничего толком не видно.</p>
<p>Далее генерируются шадовмапы. Используется технология т.н. Cascaded Shadow Maps, когда пространство, покрываемое view frustum-ом, разбивается на несколько регионов, по мере удалённости от позиции камеры, и каждой области назначается своя shadow-текстура. В LT<!-- -->:EC<!-- --> используются 3 текстуры формата R32F размером 2Кх2К. Соответственно hardware shadow map хаки от NV и ATI не используются. Кажется, даже применяются перспективные искажения кусков (визуально понять сложно). В текстуру идет z/w, ничего необычного.</p>
<p>Следующим пунктом идёт сохранение post-perspective z, в текстуру. Формат A8R8G8B8, размер экрана. Floating point упаковывается по в 3 канала по формуле.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">R = frc(z * 65535)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">G = frc(z * 255)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">A = z – R – G</span><br></div></code></pre></div></div>
<p>Всё хорошо, только непонятно зачем было геморроится с упаковкой (9 инструкций на упаковку, 2 на распаковку), если можно было воспользоваться R32F.</p>
<p>Симпатичная картиночка с упакованными данными:</p>
<p><img decoding="async" loading="lazy" alt="Scene Depth" src="https://romanmarchenko.me/assets/images/scene_depth-c0930ee00e60034a1f8c48d3c6d1367c.jpg" width="1024" height="768" class="img_ev3q"></p>
<p>Далее идёт рендер основного изображения. Всё в нём рисуется в RT формата A16R16G16F.</p>
<p>Первый проход для моделек сцены. Огромные и ужасные шейдеры. vs - 60 инструкций, ps 260 инструкций. Все в принципе одинаковые, с небольшими вариациями. Про тотальный хардварный скиннинг (4 кости) я уже говорил. Используется исключительно попиксельное освещение. 8 динамических точечных источников (другие, возможно, поддерживаются, но в коде я их не увидел). 4 важных, 4 не очень. Первая четвёрка отличается от второй исключительно наличием спекуляра. Источники включаются/выключаются посредством булевых регистров. Расчет стандартный (как в FFP), за исключением формулы аттенюации. Как по мне, в FFP она слишком хитромудрая. Здесь проще (нечто подобное я сам использовал):</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">lightIntensity = 1 - saturate((distance - startFadeDist)/fadeDist)</span><br></div></code></pre></div></div>
<p>Пояснительная картиночка:</p>
<p><img decoding="async" loading="lazy" alt="Scene Depth" src="https://romanmarchenko.me/assets/images/light_attenuation-e7b5e6a218d7a7de21c457ec7c6f397a.jpg" width="300" height="300" class="img_ev3q"></p>
<p>Если освещаемая точка будет внутри внутреннего круга, то интенсивность источника будет максимальная. В кольце от внутреннего круга до внешнего интенсивность падает линейно от 1 до 0. Ближние объекты освещаются с использованием текстур нормалей, для дальних берутся нормали из вертексов. Нормал-мапы в DXT5, в котором используются только green и alpha каналы для xy компонент вектора соответсвенно. Z = 1 – x^2 – y^2. Особенности запаковки карт нормалей в сжатые форматы можно почитать тут: <a href="http://developer.nvidia.com/object/bump_map_compression.html" target="_blank" rel="noopener noreferrer" class="">http://developer.nvidia.com/object/bump_map_compression.html</a>
Статические источники запакованы в 7 коэффициентов, которые используются в Spherical Harmonics Lighting (подробности про этот метод освещения в предыдущем реверсе).
Учитывается также туманчик.</p>
<p>Есть интересный момент в учёте ambient lighting. Он не константный для всей сцены, а запакован в текстурку. Таким образом нужные части моделек подсвечиваются ярче, чем другие.</p>
<p>Все шейдеры имеют несколько вариаций. Например, крупные объекты используют ambient occlusion map, мелкие нет. Совсем статические объекты (terrain, здания) используют лайтмапы вместо SH (правильно, 1 текстурная выборка быстрее десятка арифметических инструкций). Также применяется динамически сгенерированная environment cube map, о которой я писал в самом начале.</p>
<p>Диффузная текстурка персонажа</p>
<p><img decoding="async" loading="lazy" alt="Diffuse" src="https://romanmarchenko.me/assets/images/char_albedo-2b0cf3bd85812df3fc30ea8d97cc7464.jpg" width="1024" height="512" class="img_ev3q"></p>
<p>Амбиент текстура</p>
<p><img decoding="async" loading="lazy" alt="Ambient" src="https://romanmarchenko.me/assets/images/char_mask-0455f692d2df9a97f1c456f627d59be0.jpg" width="700" height="350" class="img_ev3q"></p>
<p>Ambient occlusion для террейна (оригинал был 2Kx2K)</p>
<p><img decoding="async" loading="lazy" alt="Terrain AO" src="https://romanmarchenko.me/assets/images/terrain_ao2-9e64fe1a9ad11ad6a6785c6393672841.jpg" width="1024" height="1024" class="img_ev3q"></p>
<p>Лайтмапа для террейна (оригинал был 2Kx2K). Она такая цветастая, т.к. каналы используются отдельно</p>
<p><img decoding="async" loading="lazy" alt="Terrain LM" src="https://romanmarchenko.me/assets/images/terrain_lm2-b405cc603022a5ebd5fdf6243024cee0.jpg" width="1024" height="1024" class="img_ev3q"></p>
<p>Альфа-канал лайтмапы (оригинал был 2Kx2K)</p>
<p><img decoding="async" loading="lazy" alt="Terrain LM Alpha" src="https://romanmarchenko.me/assets/images/terrain_lm2_alpha-ecd791b8e3a163d61509532e03bb075c.jpg" width="1024" height="1024" class="img_ev3q"></p>
<p>Второй проход для моделек сцены, в котором рисуются тени. Используются 3 R32F текстуры, о которых было выше. PCF в шейдере.</p>
<p>Скрин до расчёта теней:</p>
<p><img decoding="async" loading="lazy" alt="Only Lighting" src="https://romanmarchenko.me/assets/images/only_lighting-42a8fd21ae9e71edf98a04f43c6d286c.jpg" width="1024" height="768" class="img_ev3q"></p>
<p>Скрин после наложения теней:</p>
<p><img decoding="async" loading="lazy" alt="With Shadows" src="https://romanmarchenko.me/assets/images/lighting+shadow-a045ad6b36d458103c501d2b6ada2776.jpg" width="1024" height="768" class="img_ev3q"></p>
<p>В игре есть технология визуализации меха. Как оказалось она очень простая. Рисуем несколько раз меш, на котором должен быть мех, увеличивая его каждый раз в размерах. С помощью альфа-канала на текстуре модели получается, что во все стороны торчат как бы ворсинки и волокна. Если сильно присмотреться, то артефакты видны, и то, в близи. А так в глаза не бросается и выглядит симпатично.</p>
<p>На первом уровне есть вода. Она простая. 1 скроллящаяся в разные стороны нормал-мапа. По нормали выбираем тексели из динамической envmap’ы. Может быть полупрозрачная.
Далее soft партиклы. Они не освещаются, а для фейда по альфе на границах с геометрией, используется текстурка с post-prerspective z, подготовленная ранее.</p>
<p>После того как основной рендеринг сцены закончен, начинается перевод HDR image в LDR. Технология стандартная:</p>
<ol>
<li class="">Пинг-понгом посчитали усредненную scene luminance.</li>
<li class="">Bright-pass – выделили яркие области.</li>
<li class="">Поблурили картинку после bright-pass’a несколько раз.</li>
<li class="">Скомбинировали все ингредиенты с применением tone-mapping’а.</li>
</ol>
<p>HDR image (на ней нет очень ярких частей, к сожалению)</p>
<p><img decoding="async" loading="lazy" alt="HDR" src="https://romanmarchenko.me/assets/images/hdr_image-6e90d2c3a9702d5b52ca8948cff078aa.jpg" width="1024" height="768" class="img_ev3q"></p>
<p>LDR image</p>
<p><img decoding="async" loading="lazy" alt="LDR" src="https://romanmarchenko.me/assets/images/ldr_image-ada9b89caed81dfd4c143758e01bd2f7.jpg" width="1024" height="768" class="img_ev3q"></p>
<p>Следующий эффект – depth of field. Да, на статике его практически не видно, но он есть. :) Возможно в движении эффект более заметен (без него игру не запускал, если его вообще можно отключить). Реализован просто: берем downsampled (даже не заблуренную) картинку, оригинал и текстуру с z. Используя фокусное расстояние, рассчитываем степень заблуривания пикселя по формулам из статьи «Improved Depth of Field Rendering» из ShaderX3. По этому коэффициенту берем либо оригинал, либо уменьшенную картинку. Учитывается также дальность удаления пикселя от камеры.</p>
<p>Получается вот так:</p>
<p><img decoding="async" loading="lazy" alt="DoF" src="https://romanmarchenko.me/assets/images/after_dof-50d5392c4fc3cef96e310838309254e4.jpg" width="1024" height="768" class="img_ev3q"></p>
<p>Предпоследним идет motion-blur. Алгоритм эффекта таков:
Сначала копируется текущее изображения сцены, в дополнительный RT, в альфа-канал которого записывается дальность удаления пикселя от камеры.</p>
<p><img decoding="async" loading="lazy" alt="MB Scale" src="https://romanmarchenko.me/assets/images/mb_scale-fc4948a99fa56642d6bc1dff40a126f1.jpg" width="1024" height="768" class="img_ev3q"></p>
<p>Далее берется A8R8G8B8 текстурка, и рисуются все анимированные объекты (персонажи, крутящееся оружие на земле, и т.д.) со специальным шейдером. В него передаются предыдущие и текущие матрицы костей для скелетной анимации, предыдущая и текущая матрица view и projection. Вычисляются 2 позиции объекта (предыдущая и текущая), координаты переводятся в screen-space, а затем вычитаются (естественно с учетом множества коэффициентов и т.п.). Т.о. мы получили двумерный вектор смещения текущего пикселя со времени предыдущего кадра. В RT рисуются объекты в текущей и предыдущей позиции, чтобы захватить все пространство занимаемое моделью за эти 2 кадра.</p>
<p>Вот такая картиночка:</p>
<p><img decoding="async" loading="lazy" alt="MB Offset Vec" src="https://romanmarchenko.me/assets/images/mb_offset_vectors_dynamic-5665a61e82159dde04cfbd5bea221bdf.jpg" width="1024" height="768" class="img_ev3q"></p>
<p>Для статики всё проще. Т.к. её позиция определяется исключительно view-projection матрицей, и у нас есть координаты каждого пикселя (z - в текстуре, а xy – текстурные координаты), то можно получить позицию пикселя в предыдущем кадре. Для этого достаточно умножить текущее post-perspective положение на инверсную матрицу проекции, затем перевести координаты в пространство предыдущего кадра, используя cохранённые view и projection матрицы. Так получается вектор смещения для статики. Для того чтобы не перетирались данные рассчитанные для динамических объектов, используется stencil буфер.</p>
<p>Render Target с векторами смещения</p>
<p><img decoding="async" loading="lazy" alt="MB Offset Map" src="https://romanmarchenko.me/assets/images/mb_offset_map-fe33b3da2297a0ae9e9d39c3a1765873.jpg" width="1024" height="768" class="img_ev3q"></p>
<p>Финальная композиция использует полученные выше данные, чтобы заблурить пиксели учитывая вектор смещения пикселя.</p>
<p>Получили (эффект хорошо наблюдать, если взять картинку после depth of field и эту, и быстро переключать их в просмотрщике):</p>
<p><img decoding="async" loading="lazy" alt="After MB" src="https://romanmarchenko.me/assets/images/after_mb-6a6ba0d10668c84288f9317ed7509367.jpg" width="1024" height="768" class="img_ev3q"></p>
<p>На статической картинке артефакты довольно заметны, особенно при резком движении камеры, но в динамике их практически незаметно.</p>
<p>И, напоследок, делается что-то типа гамма- коррекции с каким-то хитрым color remap’ом, чтобы «подсинить» картинку.</p>
<p>Финальное изображение:</p>
<p><img decoding="async" loading="lazy" alt="After Gamma" src="https://romanmarchenko.me/assets/images/after_gamma-62f6c359bfc8e6402db8cf69c86abe99.jpg" width="1024" height="768" class="img_ev3q"></p>
<p>Ну и UI (блоками, не поэлементно).</p>
<p>Что можно сказать напоследок? Присутствует хорошая сортировка по материалам/текстурам/вертекс буферам. А вообще очень наворочено и технологично. Естественно всё это хозяйство жрёт немеряно памяти и вычислительных ресурсов. Вот такой он, консольный пришелец. :)
З.Ы. Обещанные 2 слова о DX10 версии. Я было хотел реверсить её, но по каким-то причинам под PixWin на Висте игра ужаснейшим образом тормозит. Кадр в секунд 30 где-то. Причём, даже в заставочном видео (тут соврал, в видео 1 кадр - секунды 3). Я кое-как добрался до загрузки уровня, терпеливо прождал около часа, вырубил игру нафиг, и пошёл реверсить DX9 версию. :)</p>]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[Секреты Relic Entertainment. Часть 2.]]></title>
        <id>https://romanmarchenko.me/ru/blog/coh</id>
        <link href="https://romanmarchenko.me/ru/blog/coh"/>
        <updated>2007-01-25T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Сорри за долгий перерыв, продолжаем копание в движках и играх.//www.companyofheroesgame.com/). Продукт, несомненно, отличный, но меня в первую очередь интересовало, на сколько его рендер отличался от DoW 40K. Оказалось кардинально.]]></summary>
        <content type="html"><![CDATA[<p>Сорри за долгий перерыв, продолжаем копание в движках и играх. :) Как я и обещал, вторая подопытная игра от Relic - RTS Company Of Heroes (<a href="http://www.companyofheroesgame.com/" target="_blank" rel="noopener noreferrer" class="">http://www.companyofheroesgame.com/</a>). Продукт, несомненно, отличный, но меня в первую очередь интересовало, на сколько его рендер отличался от DoW 40K. Оказалось кардинально.</p>
<p>Из первой части мы хорошо помним, что в DoW всё сделано на FFP. Видно было, что СоН выглядит технологичнее, но вероятность того, что движки схожи, существовала очень и очень большая.</p>
<p>Вторая страшная тайна Relic заключается в том, что в CoH используется совершенно отличный от DoW рендер. Представьте себе автомобиль 1990 и 2006 годов выпуска, какой-нибудь известной марки (нет, ВАЗ не надо, они не развивались :)). Ауди, или БМВ подойдет. Старые машины тоже были качественно сделаны, и сейчас многие неплохо ездят, но то количество нововведений, технологий и опций, которые доступны сейчас, просто умопомрачительно. Так вот, качественный прыжок в движках от Relic примерно такой же.</p>
<p>Реверсил я на машине A64 X2 4200+, 2 гига памяти, GF 8800GTX. Все настройки в игре на максимуме.</p>
<p>FFP нету совсем. В основном используется shader model 3.0. Сразу хочется отметить очень навороченную систему освещения. Сделано очень грамотно и красиво. Но обо всем по порядку.</p>
<p>С самого начала создается, так называемая Point Light Map (в терминах самого Relic). Это обычная, изначально темная, текстура, в которую рисуются все яркие места на карте. На ночных уровнях это фонари прожекторов, например. Процесс простой: если смотреть на карту сверху, то в местах, где ярко, рисуем квадратики с текстурой блика. В результате получаем картинку, которая отдалённо напоминает спутниковую фотографию ночного города.
Пример:</p>
<p><img decoding="async" loading="lazy" alt="Point Light Map" src="https://romanmarchenko.me/assets/images/point_light_map-ebe953be62dff334e5ec002530994012.jpg" width="700" height="624" class="img_ev3q"></p>
<p>Затем идет 2 прохода в 2 различные карты теней. Первый для близких от камеры объектов (больше деталей), вторая для более дальних. Все видимые модели не захватываются. Если присмотреться, то видно как появляется тени на удалённой геометрии. Никаких хитромудрых техник (PSM, TSM) не реализовано. Естественно, используется NV shadow map hack. На ATI, думаю, будет использоваться ихние depth textures. Шейдеры простые, но есть один нюанс: всегда используется texkill для эмуляции альфатеста. Почему не использовали обычный альфатест?. Оказалось всё просто: в буфер цвета выводится глубина пикселя. По-видимому, программисты решили сэкономить на вариациях, чтобы сделать единственный шейдер для обычной R32F, а также NV и ATI depth textures. Это решение мне кажется спорным, так как, сделав ещё одно разбиение (без texkill’a и alphatest’a), получаем double speed z-only проход. Обе текстуры 2Кх2К.</p>
<p><img decoding="async" loading="lazy" alt="Shadow comparison" src="https://romanmarchenko.me/assets/images/shadow_comparison-7d91ad7490651996faa28cf81f083e71.jpg" width="705" height="442" class="img_ev3q"></p>
<p>Water reflection pass (если в кадре есть вода, естественно). Туда попадают все объекты, рисуемые шейдерами, которые используются в основном проходе (о нём далее). Единственная оптимизация, которая применяется, это куллинг дальних объектов. Если двигаться вдоль водной поверхности, то видно как появляются объекты в отражении.</p>
<p><img decoding="async" loading="lazy" alt="Reflections comparison" src="https://romanmarchenko.me/assets/images/reflections_comparison-d865cdf0f0a7578032b367a1e1db71fd.jpg" width="700" height="850" class="img_ev3q"></p>
<p>Небо. Простая полусфера, рисуемая несложными шейдерами.
Интересный момент №1: в игре используется Atmospheric Light Scattering. Я в этой сфере не силён, но после небольшого изучения появились подозрения, что используется упрощенная модель, которая была описана Naty Hoffman’ом и Arcot J Preetham’ом, на GDC 2002 (дополнительное описание есть в ATI SDK). А упрощенная потому, что в игре нет динамической смены дня и ночи, поэтому большинство коэффициентов можно предрасcчитать на CPU.</p>
<p>Пример текстуры неба:</p>
<p><img decoding="async" loading="lazy" alt="Sky" src="https://romanmarchenko.me/assets/images/sky_img-f0d29514b465dcf8decf276851d82904.jpg" width="1024" height="1024" class="img_ev3q"></p>
<p>Объекты на карте. Дома, юниты: всё тут. Шейдеры сложные. Вертексный шейдер для статичных моделек – 126 инструкций. Если скиненая – 150 (используется аппаратный скиннинг). Пиксельный общий - 65 инструкций.
Что же такого там делается?</p>
<ol>
<li class="">
<p>Интересный момент №2: в игре используется Spherical Harmonics Lighting. Подход очень сильно напоминает метод, который описал Tom Forsyth в заметках к своей презентации на GDC 2003 (<a href="http://home.comcast.net/~tom_forsyth/papers/SH_GDCE_TomF.zip" target="_blank" rel="noopener noreferrer" class="">http://home.comcast.net/~tom_forsyth/papers/SH_GDCE_TomF.zip</a>. Блог тут: <a href="http://home.comcast.net/~tom_forsyth/blog.wiki.html" target="_blank" rel="noopener noreferrer" class="">http://home.comcast.net/~tom_forsyth/blog.wiki.html</a>. Смотреть пост за 17 января).
В двух словах суть метода такова: 3-4 самых ярких источника мы обсчитываем как обычно (бамп, спекуляр, и т.д.). Все остальные переводим на CPU в SH коэффициенты и считаем освещение в вертексном шейдере. Грубо говоря, освещение с помощью SH можно считать диффузным освещением с использованием кубемапы. Только в реальном времени считать кубемапу не в пример накладнее 7 коэффициентов (именно столько используется в CoH). Использование всего семи первых членов полинома, вносит достаточно большую ошибку (обычно берут 9), но, по-видимому, разработчики были удовлетворены результатами экспериментов.</p>
</li>
<li class="">
<p>Используется описанный выше Atmospheric Light Scattering. Визуально это выглядит как туманчик, которым покрываются дальние объекты.</p>
</li>
<li class="">
<p>Используются 2 карты теней. Искусственно мягкие не делали, ограничились бесплатным 2x2 PCF на NV железе. Как там дело на ATI, не знаю. Реализовать не сложно, но уже и так довольно тяжело тянуть эту шейдерную нагрузку.</p>
</li>
<li class="">
<p>Источники «средней важности» считаются в VS. Самые важные в PS c нормал-маппингом.</p>
</li>
<li class="">
<p>В пиксельном шейдере использутся:</p>
<p>Specular map</p>
<p><img decoding="async" loading="lazy" alt="Specular map" src="https://romanmarchenko.me/assets/images/specular_map-669caa95c95df266e2f703c6d8320aaf.jpg" width="700" height="350" class="img_ev3q"></p>
<p>Gloss map</p>
<p><img decoding="async" loading="lazy" alt="Gloss map" src="https://romanmarchenko.me/assets/images/gloss_map-f49ec9d3075dfe9d5327a3df1138451d.jpg" width="700" height="350" class="img_ev3q"></p>
<p>Ambient occlusion map</p>
<p><img decoding="async" loading="lazy" alt="AO map" src="https://romanmarchenko.me/assets/images/ao_map-c058ee438bb69a5c35bc82663fadc597.jpg" width="700" height="700" class="img_ev3q"></p>
</li>
</ol>
<p>AO Detail map (в терминах Relic: Dirt map)
Local ambient occlusion map.
LAO
Совершенно чудесная текстура. :) Мои долгие медитации над кодом шейдера не дали сколь-либо чётких результатов. Видно, что в разных каналах содержится разная информации о освещенности уровня с видом сверху. Происходит какая-то аттенюация базового цвета, с использованием этой текстуры.
Каналы отдельно:</p>
<p>R:</p>
<p><img decoding="async" loading="lazy" alt="LAO R" src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAJQAhADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD5/ooqWGEzEgUARUVb+wP70fYH96AKlFW/sD+9H2B/egCpRVv7A/vR9gf3oAqUVb+wP70fYH96AKlFW/sD+9H2B/egCpRVv7A/vR9gf3oAqUVb+wP70fYH96AKlFW/sD+9H2B/egCpRVv7A/vR9gf3oAqUVb+wP70fYH96AKlFW/sD+9H2B/egCpRVv7A/vR9gf3oAqUVb+wP70fYH96AKlFW/sD+9H2B/egCpRVv7A/vR9gf3oAqUVb+wP70fYH96AKlFW/sD+9H2B/egCpRVv7A/vR9gf3oAqUVb+wP70fYH96AKlFW/sD+9H2B/egCpRVv7A/vR9gf3oAqUVb+wP70fYH96AKlFXRpzkUf2dJQBSoq7/Z0lH9nSUAUqKu/2dJR/Z0lAFKirv9nSUf2dJQBSoq7/AGdJR/Z0lAFKirv9nSUf2dJQBSoq7/Z0lH9nSUAUqKu/2dJR/Z0lAFKirv8AZ0lH9nSUAUqKsT2rQAE1XoAKu6d981Sq7p3+sNAGwEGKXYKVfuiloAbsFGwU6igBuwUbBTqKAG7BRsFOooAbsFGwU6igBuwUbBTqKAG7BRsFOooAbsFGwU6igBuwUbBTqKAG7BRsFOooAbsFGwU6igBuwUbBTqKAG7BRsFOooAbsFGwU6igBuwUbBTqKAG7BRsFOooAbsFGwU6igBuwUbBTqKAG7BRsFOooAbsFGwU6igBNgpdi0UtABsFGxfSlpaAECCl2LS0ooATYKXYtOFFADdi0bFp1FADdi0bFp1FADdi0bFp1FADdi0bFp1FADdi0bFp1FADdi0bFp1FADdi0hQYp9B6UAYuqfdFZVauq/dFZVABV3Tv8AWGqVXdO/1hoA2l+6KWkX7opaACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAWlptKKAHCnCminCgBwooFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABSHpS01uhoAx9U+6Kyq1NU6CsugAq7p3+sNUqu6d/rDQBtL90UtIv3RS0AFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUopKUUAOFOFNFOFADhRQKKACiiigAooooAKKKKAENJmnUmKAFpM0tIaAClptKKAA009KfSEcUAYuqdBWXWrqv3RWVQAVd07/AFhqlV3Tv9YaANpfuilpF+6KWgAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAClFJSigBwpwpopwoAcKKBTgKAExxSVJijbigCOilbrSUAFFFFABRRRQAUhpaQ0AIKdTaUGgBaD0ooPSgDF1X7orKrV1X7orKoAKu6d/rDVKrunf6w0AbS/dFLSL90UtABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFKKSlFADhThTRThQA4U4U0U4UAPFBopCaAGHrSUppKACiiigAooooAKQ0tBoAaOtOptLQAtB6UUHpQBi6r90VlVq6r90VlUAFXdO/wBYapVd07/WGgDaX7opaRfuiloAKKKKACiiigAooooAKKKKACiiigAooooAKB1oooAfjimmjNIaACiiigAooooAKKKKACiiigAooooAKKKKACiilxQAlFOxQRQA2iiigAooooAKKKKACiiigApRSUooAcKcKaKcKAHCnCminCgB3akNL2pDQAw0UGigAoopaACkpaQ0AFBoooATFLRRQAUHpRQelAGLqv3RWVWrqv3RWVQAVd07/WGqVXdO/wBYaANpfuilpF+6KWgAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACnqKbUsQzQA8R0148VbVKZIooApGm1LIKioAKKKKACiiigAooooAKUUlKKAHCnCminCgBwpQaQUUAOzxSE0lFABRRRQAUtJRQAtJRmigAooooAKKKKACg9KKD0oAxdV+6Kyq1dV+6KyqACrunf6w1Sq7p3+sNAG0v3RS0i/dFLQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFGMUUAFFFFABRRRQAUUUUAFFFFABRRRQAUtFFABU8NQ1IjYFAF5W4qOUimKxpJCaAIJDUVPYUygAooooAKKKKACiiigApRSUooAcKcKaKcKAHCiiigAooooAKKKKACiiigAooooAKKKKACiiigAoPSig9KAMXVfuisqtXVfuisqgAq7p3+sNUqu6d/rDQBtL90UtIv3RS0AFFFFABRRRQAUUUUAFFFFABRRRQAUUUUALR0NJRQAE5ooooAKKKKACiiigAooooAKKKKACiiigApaSigBaelR1KimgCZKc1IimhqAIXFREVM1RmgBlFBooAKKKKACiiigApwptOFADhThSClFAC0UUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFB6UUHpQBi6r90VlVq6r90VlUAFXdO/wBYapVd07/WGgDaX7opaRfuiloAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiinKKAFVasIlNQCplIoAeF4qOQVJuFRswoAgYGoiKsEioHoAZRRRQAUUUUAFFFFABThTacDQBIKWkFLQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFB6UUHpQBi6r90VlVq6r90VlUAFXdO/1hqlV3Tv9YaANpfuilpF+6KWgAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKctNozQBMHxTg9QZNGTQBY8ymM9RZNJmgB+400mkooAKKKKACiiigAooooAKKKKAJUOadUaNin5oAWijNFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABQelFB6UAYuq/dFZVauq/dFZVABV3Tv9YapVe0775oA2V+6KWkX7opaACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACijNGaACijNGaACijNGaACijNFABRRRQAUUUUAKKcKYKcKAHinU0UtAC0UUUAFFFFABRRRQAUUUUAFFFFABRRRQAUHpRQelAGLqv3RWVWrqv3RWVQAVd07/AFhqlV3Tv9YaANpfuilpF+6KWgAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACkNLSGgBKM0UlAC5opKKAFzRmkzSZoAdmnrzUWanjFACYpCKlZQBmoicmgBKKKKAAdak24qMdalXpQAoopaKACiiigAooooAKKKKACiiigAooooAKKKKACg9KKD0oAxdV+6Kyq1dV+6KyqACrunf6w1Sq7p3+sNAG0v3RS0i/dFLQAUUUUAFPApoNPBoAaRTaeTTCaACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKQ0tIaAEpDS0hoASkzQaSgBc0lGaTNAC1NG+KhpQaALDvxUKnLGm5Jp60AOooooAB1qVTUVPFAElFIKWgAooooAKKKKACiiigAooooAKKKKACiiigAoPSig9KAMXVfuisqtXVfuisqgAq7p3+sNUqu6d/rDQBtL90UtIv3RS0AFFFFACUoNJSUAKTRSUtABRRRQAUtJS0AFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFIaWkNACUhpaQ0ANNIaU0hoASkoooAUUtIKWgAp6mmU4UASUUgpaACnimU8UAPFLSCloAKKKKACiiigAooooAKKKKACiiigAooooAKD0ooPSgDF1X7orKrV1X7orKoAKu6d/rDVKrunf6w0AbS/dFLSL90UtABRRRQAlFFFACUUUUAFLSUtABS0lLQAUUUUAFFO20hoASiiigAooooAKKKKACiiigAooooAKKKKACiiigApDS0UANpDTsU0igBppDTiKSgBuKMU6jFACUUYpaAACnAUgpwoAUUtIKWgAp4pg61IooAcKWiigAooooAKKKKACiiigAooooAKKKKACiiigAoPSig9KAMXVfuisqtXVfuisqgAq7p3+sNUqu6d/rDQBtL90UtIv3RS0AFFFFACUlLSUAFFFFABS0lLQAUtJS0AFFFFAEg6Uw9aep4prDmgBtFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUmKWigBhFJin4pMUANopcUUANopaKAAU4UlLQAopaQUtAAOtSrUQ61KtAD8UY4pVI70ucUANxSU/gikwKAG0UppKACiiigAooooAKKKKACiiigAoPSig9KAMXVfuisqtXVfuisqgAq7p3+sNUqu6d/rDQBtL90UtIv3RS0AFFFFACUlOpDQAlFFFABS0lLQAUtJS0AFFFFADgaQnNJRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUlLRQA2ilpKAEopaKACilooABS0UUAA61IpqOnigCSlApopwoAXtSGnUhoAbRRRQAUUUUAFFFFABRRRQAUUUUAFB6UUHpQBi6r90VlVq6r90VlUAFXdO/1hqlV3Tv8AWGgDaX7opaRfuiloAKKUUoWgBtNqVgMUzbQA2ipNtNIxQAlFFLQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFJS0UANpaKKACloooAKKKKACnimDrUqigBRThTacDQA6kNGaKAG0UUUAFFFFABRRRQAUUUUAFFFFABQelFB6UAYuq/dFZVauq/dFZVABV3Tv9YapVd07/WGgDaX7opaRfuiloAUU4U0U7NADuDSHGOKbmkzmgB4prUmcUhOaACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKUUAAp4NMpRQA/NLmmCnUALmlzSCigBaKKKACiiigAooooAKKKKACiiigAoPSig9KAMXVfuisqtXVfuisqgAq7p3+sNUqu6d/rDQBtL90UtIv3RS0AFGaKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigApRSUooAKUUlLQAop1NpwoAWlFJSigAooooAKKKKACiiigAooooAKKKKACg9KKD0oAxdV+6Kyq1dV+6KyqACr2nf6w1Rq9p33zQBsr90UtIv3RS0AFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUmaAFopM0ZoAWikzQDQAtFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFAoooAWlpKWgBacKaKcKAFpRSCloAKKKKACiiigAooooAKKKKACiiigAoPSig9KAMXVfuisqtXVfuisqgAq9p33zVGr2nf6w0AbK/dFLSL90UtABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFOpp60AFFFFABRRRQAhpuaHNMzQA/NGaZmjNAD80A0zNAPNAE1FFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRmiigBytipBzUNSA0APpKB0ooAKKSloAWikpaACiiigAooooAKKKKACg9KKD0oAxdV+6Kyq1dV+6KyqACrunf6w1Sq7p3+sNAG0v3RS00fdozQA6im5ozQA6ikBpaACiiigAooooAKKKKACiiigAoHFFFACk5pKKKAH9qYetO7U09aACiiigAooooAjl6io6mdN3emeUfWgBlGaf5R9aPKPrQAyin+UfWjyj60ASjpRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFPFMpQcUASjpSGmg8UZoAWlptLQA6ikozQA6ikzS0AFFFFABRRRQAUHpRQelAGLqv3RWVWrqv3RWVQAVd07/AFhqlV3Tv9YaANkfdpDSj7tIaAEzRmiigBwNKDTRS5oAdRSA0tABRRRQAUUUUAFFFFABRRRQAUUUUALmkoooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAzS0lFADqWm0uaAFzS5puaM0AOzTgajzThQA+iiigAooooAKD0ooPSgDF1X7orKrV1X7orKoAKu6d/rDVKrunf6w0AbI+7SGjsKMUAJRTsUlACUUtLigBBTxSAUtABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUALmng1HSg4oAlyKMiotxpQ3PNAEtFN3ClJoAWg9KaDTj0oAxdV+6Kyq1dV+6KyqACrunf6w1Sq7p3+sNAGyBwKXFC/dFLQAlJTqSgBKWiloAKKKKACiiigAooooAKKKKACiiloASin0nWgBtFOIxSUAJRS0lABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFLQAlFLRQAlGKWigAxSUtFACUUuKSgAooooAKKKKACnZptFAD1NKXAFR0dqAMvVDlRWXWnqX3RWZQAVd07/WGqVXdO/1hoA2l+6KWkX7opaACiiigAooooAKKKKACiiigAooooAKKKKAClpKUUAFHNLg0hGKADmlpKKACiikoAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigApaKKACiiloASilooASiiigAopM0UAFFFFAC0lFFABRS0lABSnpQKD0oAydS+6KzK09S+6KzKACrunf6w1Sq7p3+sNAG0v3RS0i/dFLQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUALmkJzRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUCgBaKKWgAoopaAEopTTTQAUlFFABRRRQAUUUUAFLRRQAUUuKSgAoPSig9KAMnUvuisytPUvuisygAq7p3+sNUqu6d/rDQBtL90UtIv3RS0AFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUtJS0AKKXFIKlQZFAEZ4pM09lxUZ4NAC5pDRmkoAKKKKACiiigAoFFAoAWlFJSigAooooAKQ9KWkPSgDJ1L7orMrT1L7orMoAKu6d/rDVKrunf6w0AbS/dFLSL90UtABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFKKSloABUqNUVOBxQBKRxULdalD/LUR5NADaKdim0AFFFFABRRRQAUCilFABS0lLQAUUUUAFIelLSHpQBk6l90VmVp6l90VmUAFXdO/wBYapVd07/WGgDaX7opaRfuiloAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigApaSloAKWkpaAAUtAooAKaTTqYaACiiigAooooAKUUlFADuKKTNLQAUUUlABmg9KKD0oAyNS+6Kza0tS6Cs2gAq7p3+sNUqu6d/rDQBtL90UtIvIFOIwaAEooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAClpKWgApaSloAUUCkFOHWgBdvFRng1OCCMUxo+aAIqXFLtwaWgBlFLjmjFACUUYoxQAopcUAUtACUlLSUAFIelLSHoaAMnUugrNrS1LoKzaACrunf6w1Sq7p3+sNAG2nQUp5pF+6KXNACYopSaSgAooooAKKKKACiiigBcUlOFNNABQOtFFADz0plOOcU2gAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAFpc02loAWlptLmgBwODT9/FRZooAcetJSZozQAop2zNNzSh8CgB3l8UEACk8w4ppNABSUUUAFJRRQAUh6UppD0oAydS6Cs2tLUugrNoAKvad/rDVGr2nffNAGyv3RS0gIwOaXcKACijcKMj1oAKKNwpNw9aAFopNw9aNw9aAFopNw9aNw9aAJF5FBWo94Hel3j1oAUilC0zePWl3j1oAc3AxTaQuD3o3D1oAWik3D1o3D1oAWik3D1o3D1oAWik3D1o3D1oAWik3D1o3D1oAWik3D1o3D1oAWik3D1o3D1oAWik3D1o3D1oAWik3D1o3D1oAWik3D1o3D1oAWik3D1o3D1oAWik3D1o3D1oAWik3D1o3D1oAWik3D1o3D1oAWik3D1o3D1oAWik3D1o3D1oAWlpu4etG4etADqKbuHrRuHrQA6jNN3D1o3CgB2aM03cPWjcPWgB1FN3D1o3D1oAdRTdw9aNw9aAHUmaTcPWjcPWgBaD0pNw9aCwx1oAytS6Cs2tLUvuis2gAqe2n8hiagooA0P7R9qP7R9qz6KAND+0faj+0T6Vn0UAaH9omj+0T6Vn0UAaH9o+1H9o+1Z9FAF/+0TR/aJqhRQBf/tE0f2iaoUUAX/7RNL/AGiaz6KAL/8AaJpf7RNZ9FAGh/aNH9o1n0UAaH9o0f2j7Vn0UAaH9o+1H9o+1Z9FAGh/aPtR/aJrPooAv/2iaP7RNUKKAL/9omj+0TVCigC//aJo/tE1QooAv/2iaP7RNUKKAL/9omj+0TVCigC//aJo/tE1QooAv/2iaP7RNUKKAL/9omj+0TVCigC//aJo/tE1QooAv/2iaP7RNUKKAL/9omj+0TVCigC//aJo/tE1QooAv/2iaP7RNUKKAL/9omj+0TVCigC//aJo/tE1QooAv/2iaP7RNUKKAL/9omj+0TVCigDQ/tE0f2jWfRQBof2j7Uf2j7Vn0UAWbi588AYqtRRQB//Z" width="528" height="592" class="img_ev3q"></p>
<p>G:</p>
<p><img decoding="async" loading="lazy" alt="LAO G" src="https://romanmarchenko.me/assets/images/lao_g-a586f6a2059c26da7f0b45fc581df901.jpg" width="528" height="592" class="img_ev3q"></p>
<p>B:</p>
<p><img decoding="async" loading="lazy" alt="LAO B" src="https://romanmarchenko.me/assets/images/lao_b-2003cb5656323aed5934bb5482c77c1d.jpg" width="528" height="592" class="img_ev3q"></p>
<p>Туман войны (считается на CPU):</p>
<p><img decoding="async" loading="lazy" alt="FOW" src="https://romanmarchenko.me/assets/images/fow-fa0d152a806390cd789499c2c3288fa4.jpg" width="500" height="561" class="img_ev3q"></p>
<p>Для разрушаемых объектов есть ещё и damage map. Накладывается по маске.</p>
<p>Вот так всё непросто. :)</p>
<p>Водичка. В принципе незамысловатая. Только отражения. Есть текстура с маской прибрежных зон, для имитации пены на волнах. Тот же light scattering. Направление ветра регулируется из приложения.</p>
<p>Земля. Уже 10 текстур занято, места для тайлов и маски совсем не остается. Выход простой – отмоделить и оттекстурить землю, как обычные объекты. Шейдеры используются абсолютно такие же, как для объектов, только вдобавок применяется еще и Point Light Map описанная в самом начале.</p>
<p>Партиклы. Сложный вертексный шейдер хитро считает освещение, зависящее от кучи входных параметров. Также он считает текстурные координаты, так как в Relic использовали хитрую систему запаковки текстур в атласы. Когда одной большой (2Кх2К) стало мало, а рисовать всё скопом очень хотелось, программисты вспомнили про кубемапы. Вот так на свет появилась огромная кубемапа (6х2Кх2К), в которой содержатся все изображения, необходимые для рендеринга эффектов.
Вот примеры 2-х сторон:</p>
<p><img decoding="async" loading="lazy" alt="Particles1" src="https://romanmarchenko.me/assets/images/effects_1-cc9c800fb7ba642d92e2a5902c7bac01.jpg" width="1024" height="1024" class="img_ev3q"></p>
<p><img decoding="async" loading="lazy" alt="Particles2" src="https://romanmarchenko.me/assets/images/effects_2-bcfcdbacec847648d80ee7f7aa9ab674.jpg" width="1024" height="1024" class="img_ev3q"></p>
<p>На самом деле, очень необычное решение. Я такого ещё нигде не видел.
В пиксельном шейдере вездесущий light scattering + странное смешивание семплов из кубемапы с nearest и point фильтрациями (2 семплера). Для чего они это делают? Я не разобрался. :)</p>
<p>В конце идут границы игровых зон и выделения юнитов. Тут тоже таится необычность. Это высокотесселированые модельки, которые считаются на CPU. Тяжело описать, лучше 1 раз увидеть:</p>
<p><img decoding="async" loading="lazy" alt="Edges" src="https://romanmarchenko.me/assets/images/selection_outline-bf545c4c86c39c7a45cda15faaade720.jpg" width="764" height="465" class="img_ev3q"></p>
<p>Блум. Даунсемпл + color remap + фильтр Гаусса по горизонтали и вертикали.</p>
<p>UI. Вообще неплохой, но после всего остального сильно проигрывает в эффектности. Батчить можно было лучше. Текст по словам, а вот иконки на миникарте по одной.</p>
<p>Общие впечатления от рендера очень положительные. Технологично и чётко. При стандартном наклоне камеры (если Backspace нажать), на максимальной высоте 600-800 DIP’ов. Из них 100 DIP’ов UI. Если смотреть горизонтально, то 2000, но это не критично, так как так никто не играет. Как и в DoW, применена scissor rectangle оптимизация: область, закрываемая UI, отсекается. Это было не столь важно в DoW, но в CoH, с его супер шейдерами, важно. Естественно, всё это безобразие не без офигенного арта.</p>
<p>Мини выводы:</p>
<ol>
<li class="">В этот раз решил хороший баланс арта и технологии. :)</li>
<li class="">Небольшие замечания по движку всё равно есть. Например, вместо Atmospheric Light Scattering’а, влияние которого видно только при горизонтальном положении камеры, можно было сделать обычный туман. Картинка пострадала бы не сильно, но мы бы получили прирост в скорости, за счет более лёгких шейдеров. Хотя в игре много cut-scenes… В общем это так, ни на что не претендующие мысли вслух. Отмечу также отсутствие double speed z, и UI.</li>
<li class="">Движки DoW и СoH совсем непохожи друг на друга. Я не буду утверждать, что они были сделаны разными командами, но подозрение есть. Ещё одно доказательство «разности»: в CoH остались специальные маркеры, которые видны в PixWin. Это мне немного облегчило «разделку». А вообще, по таким деталям отмечаешь высокую культуру программирования.</li>
</ol>]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[Секреты Relic Entertainment. Часть 1.]]></title>
        <id>https://romanmarchenko.me/ru/blog/warhammer40k</id>
        <link href="https://romanmarchenko.me/ru/blog/warhammer40k"/>
        <updated>2006-12-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Со мной вряд-ли будет кто-то будет спорить, о том, что Relic отличная компания. Homeworld, Warhammer 40K DoW – Dark Crusade.]]></summary>
        <content type="html"><![CDATA[<p>Со мной вряд-ли будет кто-то будет спорить, о том, что Relic отличная компания. Homeworld, Warhammer 40K: Dawn of War и Company of Heroes – это игры которые мне очень нравятся. Я решил зареверсить последние новинки этой компании, и начал с W40K: DoW – Dark Crusade.</p>
<p>Готовы узнать страшную тайну? :) В игре полностью FFP’шный рендер! Fixed Function Pipeline, то есть совсем без шейдеров! Честно признаться, это было для меня полной неожиданностью.</p>
<p>Исследование, как обычно, проходило на максимальных настройках графики.</p>
<p>Движок отличается хорошей сортировкой объектов по рендерстейтам и текстурам. DIP’ов не много.</p>
<p>Отрисовка сцены начинается с земли. Простые квадратные патчи. На экран помещается 5-6, при стандартном положении камеры. Используются 2 текстурки:</p>
<p><img decoding="async" loading="lazy" alt="Ground Specular" src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAAgACADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwCjFucBJCF52sAev17VJbJtdWkwY85C7x/OllAPzb8Rk5znv0B/lz9PrToAI3O1g24nHfBwDwT9Ov0pDOo06BGi4GPcDmt+z0lp1xt4z155/GsXRlEr7AMu3X6c4r0bSrfy7cEj86YjyHU7VbFwoTbGR95zk/menQ/5FUIrZndTIr7AuACMFhxzj9K6LxRq5iuUhttirjLuFDHk++eOv5Guee6l3sJZGKFcDJ4APbHYUhnVaXvglSQK8ZI6FeT9a77TL5XQIXDEDBI9a8mtb+UAbpH4PygDIwf0962o9Za327CV28jmmI//2Q==" width="32" height="32" class="img_ev3q"></p>
<p><img decoding="async" loading="lazy" alt="Ground Tex" src="https://romanmarchenko.me/assets/images/ground_tex-5186fd793384738a86ded6ed83e63b33.jpg" width="512" height="512" class="img_ev3q"></p>
<p>Первая делает имитацию бликов на земле. Блендятся они линейной интерполяцией, используя альфа канал диффузной текстуры, который выглядит вот так:</p>
<p><img decoding="async" loading="lazy" alt="Ground Tex Alpha" src="https://romanmarchenko.me/assets/images/ground_tex_alpha-98c4cbb3487b861072b964a6c14781cb.jpg" width="512" height="512" class="img_ev3q"></p>
<p>Результат после первого прохода:</p>
<p><img decoding="async" loading="lazy" alt="Ground Pass1" src="https://romanmarchenko.me/assets/images/ground_pass1-5f20924eb4ba81ac8f9d7246453cfa22.jpg" width="1024" height="768" class="img_ev3q"></p>
<p>Далее второй проход для земли. Накладываются детали на землю, используя стандартный альфаблендинг.
Примеры текстурок с деталями (зелено-красные клетки показывают альфу):</p>
<p><img decoding="async" loading="lazy" alt="Ground Details Pass2" src="https://romanmarchenko.me/assets/images/ground_details_pass2-7c2d4c92c92e7a68022fa62d969f207c.jpg" width="512" height="512" class="img_ev3q"></p>
<p><img decoding="async" loading="lazy" alt="Ground Details2 Pass2" src="https://romanmarchenko.me/assets/images/ground_details2_pass2-1c85febb30827ba6d0d6eea7d164be0e.jpg" width="512" height="512" class="img_ev3q"></p>
<p>Затем поверх рисуются тени от зданий и юнитов, рытвины от взрывов, и т.д.
Используются примерно такие текстуры:</p>
<p><img decoding="async" loading="lazy" alt="Object Shadow" src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAEAAQADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDi6KKK+ZP3E9Kooor5o/i881ooor6U/tA9Kooor5o/i881ooor6U/tA9Kooor5o/i881ooor6U/tA9Kooor5o/i881ooor6U/tA9Kooor5o/i881ooor6U/tA9Kooor5o/i881ooor6U/tA9Kooor5o/i881ooor6U/tA9Kooor5o/i8KKKKAPNaKKK+lP7QPSqKKK+aP4vPNaKKK+lP7QPSqKKTNfNH8Xnm1FFFfSn9oHpWaM03NGa+bsfxhY83oopM19If2dc9LzRmm7qM182fxjY83opKM19If2dc9LzRTc0ua+bP4xsebUUUV9If2eelUUUV80fxeea0UUV9Kf2gelUUUV80fxeea0UUV9Kf2gFFFFAHpVFFFfNH8XnmtFFJX0p/Z9z0rNFNzSZr5ux/GVjzmkzSZ5oNfS2P7L5j0kmmk4pjPioWnAJ5r51RufxxGDZwOaC2KhEgIprSfLkc19LyH9fOukem+ZS7s1nxz5PJqUzAd+9fOum0fyK6LRwmaNwqEyDFIHySPSvoeU/rx1kj0syUgkzWc12pPBxT0uRnHrXgeyZ/InsGcTmlqINzxTwfWveaP69jUTPS80uaiDCnA181Y/jpxPOqKTNGa+jP7LuelZpaZmlBr5s/jKx5vRRRX0h/Z56VRRRXzR/F4UUUUAea0UUlfSn9ns9IJppNIzYqF5Md6+eUbn8axjc8+3UZ+XNQ7hnGRUTzhTjrX0ygf15PEJHpfm5pPOHrWX9sCA5b6037USM5GDXhKgz+UFhmciz84FQSMe1IpDE8/lUFxciNcBhjgmvpIUrn9M4nHKKO2e6AU8Z4/OqFxqioGCyKePxrIuNVLR4j3Zz1z0H+TVG0iMsoOGLZ+VTWEMJFLU/DKeAglqLFegoE2DbnluetL9pEhwOAOKzoiYxJtuFjJ4YE5zTbOVi+1SOf71d0qSP1/D5hK9melzXQhXnkHpisq41MBu4H1qpezk5POAOMetZ043QM7OMjowPBPpXlUcKnufi2HwSerM+K4DfKGBJ5+lTC5BJBIAH61kpHIjptcHIyMHINSGZ1l8vecMCTgdfavXdJH7DDMKiep6TBfq5wG/H1rQjuAUBzn0rhFvWVgqnHOdvetu1u8r1wBXg1sJY/FcRgXExg2Dz1p2+qqynywpGTnineZjrx9D1r0HA/paOKVj0wSinB/eshb0E/exjrUy3O48YA9M1886LR/KcsO0cdmlqENuI7CpM4xXuNH9dQnc9LBpaYDxTxXzLP43aPNqKKK+kP7PCiiigD0g0wmnGo3OK+cSP4yijzcnmopGH4U5ueB1qtdFkQYPXivqoRuf1xiq3Ijpr2+8tRz0PFZSal8xZm6Zzjv7VUupJJmILgBaptKqbki4Hdic1NLDxUdT+fMPhIqOqK3mMXy2dxHC9hUiz4BGOo24HY1RiBDbj0zwfUVaWFSjuueDnk9vSvScEfqUMXNbnUicG35bkdRWJcl3mfDHYBuODTkuwkiqA3J5qPzCkhLryw+7ntXJThyn5tSpqGhlMpwGJDI/QL7etXyywRJOpCh8heO3/wBaqYdFhdtuNx+QZPA71WCFyGYs/J6Gu8+8N0RQLcLGq70KE7jxuOM4FKmY5VdAu0Dsabb5+1fMowq/KPU9/wAamk2xyBlLMg5QdMZ9f89q45LQ+HqK6MS5eURZ2lVY4zjr7VHBbb8F1Y9wO2Knlk89fLOcZ6n19qf5sMTEv8zLj5BzkV1xPvasrvU0ZYo4Iw4IjLHA96SKW1V9snzgcfIM8+3aoTNHKzyOoQKMKpPPTjAqK28vI8xHYZ/hYCuc+GM4AhwZQS7cAY/CrCSCN3QEEnHfocVMPKuJlcwiLZ05xkdqWKSIRfeB3H5lIwcVvJXPuqU3F3OogvdkZJIYBgMnNQXeoLCCqhg2eHHT8KyhdpuHPI+YY6ewptzeG5jU+Xjb0+n/ANc150aCufnEcLFalOO6WQeWoYlh+RqwJPIUkSbjVGFQ3COsTNzuY4wMZNLLJIkTIMsCc7tmOP8ADpXc6aP0+njJp7nptpdeZADnmtOJ8gYNcjpV1ugVR97ua6K1nz0WvmMRR5Wfh2Lw/IzjhThTBnPNPBr0mf1jB3PShS0gpa+ZP40CkpaSgDzWmt0p1Mavp0f2XPY7W6bYM965rUboEHJwO/vXQ3mSpGa5m8TD/dzj19a4sHFX1P5Yy6KvqcfJJ+5K4GQM81HBEsoDbcDqTU0y+ZuLZCjqccmqyytHuWNsAnANfTI/W5bmwHRSUUkAdOMkmhZmVGHIB4ApqgwjeQuccAmmBfMuMkZDcj2Fcx8SUpECuoXDDIIOfvCpXALNtkJLc9BxT7gAWm/AB3BR0qjLIW4AUFiM4/Suk+0ubgEe8ys3y5wNwquo3nqEUnPBxkU5shAJNoXsDyagjKliSvA5xXMfFFWPJcswD/3Swp1ym+6KRgYA5IHGcc0xJWDMGUlR2Hp6UsJClw3C44z+hrpPtUzTEckkbHGFT7w71HKpiMeCpbPAHB/GiOYBDmSQ5ByB2NR7CFLMM9Pmz0rlSPiYqxTmjf5S6AbweQc/WpoPLYmNsRgLxvIFQeZJJJG6x4ReBk8etOVTuYzbRgEk55b2rrPtjVmVP3aoVaQZLHqPYChsxWzAQ/vGHJI6D2p9viQPIiAA4AU84AqVyJS7uuzC4znJ6cVyHxdjAVEMYOMDBJyetJBATkk7cKc+1PjjGwRkMGbrkd80XEzRqYUXC45O3lq6j7QvoA4JkbBPbsPrVkW8cbkGVmUDgZxu9qzwWc4yTn9a0rcShfLO11wDk/wn0rlZ8SZunzxx5EjY4xjFaEEoZs84zVRrXcgYnLZ+bHFWLcKgAOSfSiskfqmWSknY9SibcKsp0qpCcgYGKtLXx09z+a6i1PO6KBRX0B/ZCCiiigZ6Mahl5FTmoJOlfPRP40hueZS+mDVCYfvBySRzV+YnnB/H0qjIuGDZAHPOa+tpH9S5hsyzdOuH3uxcnCoB2qvG3lc4AKjORznmprmQM7yBQSOOfT1qF7jco2hg5XA7DFaR2PxyOxlySrIpRU5P3eOlOj2xr1I9GNRqrQuA20MD37Cgq7tnbu6nPTArrPtTanlR2jjVgR3IFV9212do8sDxu9KQ4YoduFAJGec/Wkw8mRksB69q5z4wpkGQmSbK7Rwvr7CoWYlyxTjgDNSFzHMoDZJGeBnHtQB5bs8hBKnhcZzXSfZmuA42nZkrzwe1JJFuVZXJweFDcnFME+CgBVQSeCv86WRw7bgTgdcDgCuU+MMvydroVXIznBHFDr8yeZlPmOcjilZ2eTIcBMY3bcY9uKeAGjLSTttJONx5HofrXSfZmrLJEmcDLFeo55qM75nAVUGwZJJ/rVeFirHCK5xgZ7e9Sec0blvLweCe4+tcx8YRDfCvmHnaM9MD2qCS4adApQIuckZ5apH82cKWZyq8qpXjNNmQRTMbhWznoK6T7Q0WlPmRKCQSOT3x6e1R3GSC24nGO3U//qpwdS26NW6YJIGPwpry+adkascnLcVzHxZmRQb5FdRgf3fetCI+WsxTCugYNz147VVc/ZYhiRckcA9TUQYRwxF/mHJK+o7V0n2iOw0dyNvmMrDAxkc59K2rZdszD/arnrFAWVlIweQqtwPrXRWfUE968DGH5djzmU6dKlFRoQQOKkA4reR/TdLY9KFOFNFOFfMH8csKSlpKQjzQ01sin0w4zX06P7KnsdjesCoU9OtctqbhQQMECuquGUMd4+XuBXManLFsk3I2OuRx9K48Hufyzl61RxR8xIsh8buPr7UiEqCfvADGMc/hU212YOhxkDgjP61G6NEx5J7AjvX0p+tPcuqu1g2cMOT0PFSbRLGMZCg4J6k09YC0W0scr6Dpk0qqXVY4xkE4YevNcx8UZsUYCgFDtbPTr+NQzllQFPlQkjaO1SiQxOVXOO56ipLiF7cBpMjzU+XdjI966T7NlnZLDErElR14pSQU3cluhz3FCoRGfNLcZ+Unv9KmmiWO1VgjZZ+AB2/p9K5j4w58bec1Yt1Uod+emc9qQBTC77Qi9Mbuo9vWpYSWIWTG09jgACuo+zL+DJxk7C3GPy5p04dovLJ+VPujv6c0SlZm2xr8icAjj/PNHlrJIUM23ceuM4rlPjDJxHEVZQWBOMinPJIZgRkADC8dqsJCq4SRv90evP8An86rMpMmGBwpOfSuk+zN1J28qQvuyV+TnkEd+OtRxWquEkeTdn7y7Tx+NNjnBJTy2zyMg09Jdx2JGV2Y4znn0rmPjCmzLHI6koSOApPOQPTtUSpILYRRFeerA4OKbI0fmKx+V8nLHv8AhUizp5TRxYc9eO9dJ9oWmjVHy+4kEEjrx9aluEbZuHryPallbYm5yCXUqdh4xTWncxBR24G3+Vcp8UylBAFAcZG4YweMVowAAcfwjFZ8bg+XGC4Ibcc9BWrbopBcnknninWP1LLHqejQfrVtOlU4SOMcirqnivjqm5/NtXc86opKWvfP7HQUUUUDPRmqFyMGpjUElfPRP40hueZS8Djk/wAqzbsBs7j16ACtOTJPHFUHQk7uB6k9h619dRP6izJXTLazIGdWGOMZPY1E7zONxRVVRnpyff60j/elZ23DHTGM9qawllQM2xccgjr7CtEfj6ehmoGkuXCNsCjJI74pcbG2pncTnPr60BHSRcSNgnqO5qfgNuILADAyK6j7Y0IEiXezsRuXIyDjNRSqWIQMWYnHrgCpmLJbZZBk8DPbv0qupxHsVwmRkkjk1zHxZmbdxCFiRncSO3an+bcLIV/1gQYUEZAqBW2k4bjGBjtVgSqY+RjacDA6H1NdJ9maBI3q0zKW6bT0H0AqE5nlKKfkU8Z7CpYEWS6O87UKnB64FIu+J+rqMkAAc4zXMfGGZljIUDbVXsvellR0wS2wFflGOtEbFG42szsRkjPHekmybjdknC5AA6AV0n2Zqo5TJDFnK/ebt9PSppMJxh23Eg8Y5xTYvnYI+0HjIJxkdaajPdXQ2MRs+ZiCRn1rmPjDIG6DI3ccgD0p8bxKqbGywHQA9cUMiyOWAKoQRxjkev6UkMCrCZHY88gDuK6j7MvSxyFRLwVOcAdhVy3g8qwM+Nu3Bds8nPQVVQlZWBYBR0yc1JcI7bFXByu7IPWuU+MMjykFwI0YkEDk1PvSC3YK2ZWJXGPuiqaoG6Z5YDd2ArTngW2tLeWKRc7jhe44610n2iNq06JjbhsZA6iumsWLKQxIHWuXsImN0x6sDnNdTaYIAGfevCxjPzHMWcsg4qcVHGpxUgrWR/TFHY9KFOFNFOFfMH8csKSlpKQjzWmMOKfTT0r6dH9lz2OxuuEP0rmtQBMZyM4PAHeulu+OfQ1zeoZBPAJ7Y9a4sJufytgVqca0YdvLRgp7571NDBJ5OwgFkzkZqFT5q7gMHOM5p6S/K6K3PI4FfTI/XZ7mn5jbN2QW+6Pb6UsjNgABiQMEnsKi2kBgWxjoPWnJMhiZZRz1znk+1cx8SZZKgK27IDAk4JpFeOaSVSoVn/jc9BUw2qTGVPyDt0FQF0LB1iwSO/PHrXSfaF0sCVKrubv2FIjO/wC7jwo28j1q5C4eJ0GEzzk9qreW2Aqbsnp7DvXMfFmUwUPyd2FyoHAFSS8t5pZTkYxnmkis8uGf7oPzAjHFMljEMS5RdzEHrXSfZms7EbV5Cjpx1+lJkABXBCDPQc4z0p0AzlsZOcls8f59qc7rJl8Ar2B7+ua5j4wyCrMxAcAL6dM0qTYUo2AjnccDrU4eExAco+ei9CMVRI3SAjJB54FdR9mbKRqeWDEN90+3pT4kRQR98ZI9qsXLboYyCNxPJxnaMdBVPzfs8hRQdnsec1ynxhnwldoULvkwdo25xSp+9OHYZZiSvTn1NFrO0KyFQfmHPPQdarxnfNvb5mLZ9BXSfZm5byJaxux+bfnHy5xjoasQZuWLzyAKpGAO5/wFVVXyjIfvDHA5wPanwTF870GB/HnFcrPimMjizJ8gJLjoOn1zVmLaq5AOT7VSjlj3gZZdqbeeAcVdRcNgjHt6UVj9Xy1o9LgIAq0tVIatr0r4+e5/NNXc85FLSClr3z+x0FFFFAz0Y1DJU7VEw4r52J/GkNzy+bmsy5/d7iwO08AVrSgg/WqF0iFWDHkjnNfX0Xqf1JmdNuLsWkjeSDOGjUnnn+VPt7aKBpGZl4X5c8jPpUk87bBGi9WG5jyRVdT5gWPq4TkAc59K0TufjsWrGVLIfN3SNgMNoz6dqjjePzQX2bAcD1zT5CsiBUDAKOQRnFSxiKGVmkIJIAU8HBrqPtzTuhG4iMTL86YZSeSfWhoE8sJI6Rsx6k9BjrSny2/0gMWKrkBh3/z3qk0wLPkGRn7muXc+KuUoI8SKrc9zzTp0jLMQGCtwNx5qAxNGjTM2z0UHmojcTSdTnJyTjmus+0NTDHhwenCk04LvjRQgAbOWpit5bB9oLnoCOBVkQ7MFjhid4Oa5W7HxLdjOZAsJVRkkYGTjp/WoVZlD8ZAGcAdqlaUKsjNzK3Q+gppCvCwclc4APb8a6Uz7iUbGgJDKw2KSe27t+FSkIHwvC7cjimW5jWCTJG5VODnrTPtBYAYAVQBgHkj0rmPiCosDSMI1fDfTr9aglUF9qoRyQe4z/kU6O+8pP3YO8euKiRppO25VOeeADXSfamtbxmWRs4VAOSePanGH7MySoAynIAbv702SKZSWO33C1CdxkByWI6E1zHxVw+VlJlZAuMgbvbpVQbmt2G0AEAgngEZpiRq0ojDl16kY6GrAZvOkXIZc4UNj/PFdLPtkbNq7vMvIByCfX6V1emqCOh9zXOabb7Z0JABHGOtddaREAEf/AF68HGzWx+XZjNbHIJz9KkHSmIOPanitZH9N0loelCnCkHSlr5k/jhhSGlopCPNDSEU6kNfTH9myR3U6ZxWTdQglmZcjHpW66jBqnNEPTPrXi0anKz+QqFXlZ5fcQtgkAgMM8VSMjLIqxEgFs++cVsTo5QKCAp9Kqi18sbwAT9K+shVVj+hsTl8ubQnMbLgMG+d8soqUxo3yKMDrwO9baaa0h3YH3SOR0zUy2ATICjkelcUsTHofkUsZHocRsKAfLlB0BGc061jjS6AAw+ctz932/pU08WDkkrxgc1WY/LuBYAn/ACa9GMrn6fXouBoXIXzF6NkYJP6c1GpSAbZMnHI44NE7LkqecZC7hyKkkEZtwAw3M2PUnA5rFHwBR2lwQiorrggkiorkmWdg2PMyBhelNaUjKIGVA3yZ7/5FFuhaTe0mGHOe5rpZ9xTV3ZGxOu7ag6DvjrUcrokBjy2/HzEitE25Me9icnjrWRcM4dx0welcVN3PgKUroyYofMIHOWOOBS3CJFIUVeMDBJ5qzDJGkKkHDDpx1NV03G4Znj39dwzXefdWNiC38tVkbaSwyO+KsjMjqvO8c5GOKgQlnCbCuCTjrxWnZW6+U7cktxkiuCpNJXPhK1RQRiRyl8HaOuCDyD+FII7f98CXwuWjAHX2PoKkih+Qtg5HOaDG7qwztUnn3roVQ/UJYGVrolCqfkjLBW657+laEFgZSCCSR1AFXhpBLB1BCDuR0q/BYAFdu7aOCSTzXnTxUUtD8kqY6KRykcAQgnAz/SrkZHYUwRgYUduvvUyLitKk7n9AYTDciPR0GKmApiipBXysmfybJnm9FFFfRH9mBRRRQB6TSEU6kr5s/jBHmnWmlc0/FJX0qZ/Zco3O6khGenSoWt93atHbTTHXgqo0fx7Gs0eandwF6Cmldpzip9tIVzX0XOf1s8Ojprm1Ejbto+hrIvNNMxwSAAeMdq614BzkVVktwRjbxXlUsS4n8t0MY4s8t/s8spdnJbPI70rWQjjO0HHX8K1NuMgDJPX2pkoc8HqOAB2FfRqv3P3uplbvoIkJmONhx9M1qW1ieoXGOtbFtpqxncV4HSryWoH8OP615FXGJ6I/E6+PUlZHnpgwmMGs+aEsx+9u963TFUL243cjJJr04VbH7tiMucthqQHaBhhgdSeKngtvm6ZLYJ4rpTpoyUCgA9wOalj0sL1Bye5FcUsbG2h+JTzGNtDhmhM7IP4V7mrEcJBIXoOlWY4gB0/OpQo/u4rqnVufvGGwCidlFacYPTFTfYwOg4q8kQAqTYK+ZdZn8xSrs82K5QjrQEI4qXFKRmve5j+tI0Ejv449pNThaULTwK+acrn8gync82xS0UV9Cf2UkelUUUV80fxgFFFFAHmtFFFfSn9oHpOKTFOor5o/i+55piilor6U/s+x6NikxUlJivm7n8ZXPNCKaVBqSkxX0qZ/ZLgmd4Ycc0wwAnJq7tpClfPqoz+PVVZ5mqY7U/bzyKkAxRX0XMf18qSR35izTGt+c4q3t5oxXznOz+P1UaPMBFk08pipMUuK+j5j+v8A2KO/EfGMYFSBMVJilAr5tyufx+5tnm4FLRS19Fc/shI9HxRinUYr5q5/GVzzXFLRRX0h/Zx6VRRRXzR/GB5rRRRX0p/aAUUUUAelUUUV80fxeea0UUV9Kf2gelUUUV80fxeea0UUV9Kf2gek0YpaK+aP4vPNaKKK+lP7PsekYoxTsUV82fxjc81oxRRX0h/Z1j0jFGKdijFfNn8Y3PNaKKK+kP7PPSsUUUV80fxeea0UUV9Kf2gelUUUV80fxeea0UUV9Kf2gelUUUV80fxeFFFFAHmtFFFfSn9oHpVFFFfNH8XnmtFFFfSn9oHpVFFFfNH8XnmtFFFfSn9oHpVFFFfNH8XnmtFFFfSn9oHpVFFFfNH8XnmtFFFfSn9oHpVFFFfNH8XnmtFFFfSn9oHpVFFFfNH8XnmtFFFfSn9oHpVFFFfNH8XnmtFFFfSn9oH/2Q==" width="256" height="256" class="img_ev3q"></p>
<p>После всего этого следует ещё 1 проход, в котором накладывается лайтмапа, туман войны и вот такая странная 2-х тексельная текстурка (я её увеличил и добавил рамочку, так как белое на белом смотрится не очень:) ):</p>
<p><img decoding="async" loading="lazy" alt="2tex" src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAgEASABIAAD/4QVgRXhpZgAATU0AKgAAAAgABwESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAABAAAAagEoAAMAAAABAAIAAAExAAIAAAAUAAAAcgEyAAIAAAAUAAAAhodpAAQAAAABAAAAnAAAAMgAAABIAAAAAQAAAEgAAAABQWRvYmUgUGhvdG9zaG9wIDcuMAAyMDA2OjEyOjA3IDEyOjExOjA1AAAAAAOgAQADAAAAAf//AACgAgAEAAAAAQAAAICgAwAEAAAAAQAAAEAAAAAAAAAABgEDAAMAAAABAAYAAAEaAAUAAAABAAABFgEbAAUAAAABAAABHgEoAAMAAAABAAIAAAIBAAQAAAABAAABJgICAAQAAAABAAAEMgAAAAAAAABIAAAAAQAAAEgAAAAB/9j/4AAQSkZJRgABAgEASABIAAD/7QAMQWRvYmVfQ00AAv/uAA5BZG9iZQBkgAAAAAH/2wCEAAwICAgJCAwJCQwRCwoLERUPDAwPFRgTExUTExgRDAwMDAwMEQwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwBDQsLDQ4NEA4OEBQODg4UFA4ODg4UEQwMDAwMEREMDAwMDAwRDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDP/AABEIAEAAgAMBIgACEQEDEQH/3QAEAAj/xAE/AAABBQEBAQEBAQAAAAAAAAADAAECBAUGBwgJCgsBAAEFAQEBAQEBAAAAAAAAAAEAAgMEBQYHCAkKCxAAAQQBAwIEAgUHBggFAwwzAQACEQMEIRIxBUFRYRMicYEyBhSRobFCIyQVUsFiMzRygtFDByWSU/Dh8WNzNRaisoMmRJNUZEXCo3Q2F9JV4mXys4TD03Xj80YnlKSFtJXE1OT0pbXF1eX1VmZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3EQACAgECBAQDBAUGBwcGBTUBAAIRAyExEgRBUWFxIhMFMoGRFKGxQiPBUtHwMyRi4XKCkkNTFWNzNPElBhaisoMHJjXC0kSTVKMXZEVVNnRl4vKzhMPTdePzRpSkhbSVxNTk9KW1xdXl9VZmdoaWprbG1ub2JzdHV2d3h5ent8f/2gAMAwEAAhEDEQA/AOt+rH1Y+rd/1b6Tdd0nCtttwsd9lj8epznOdVW573vdXuc9zlpf80/qr/5TYH/sLT/6TS+qf/iV6N/4Qxf/ADzWtZJTk/8ANP6q/wDlNgf+wtP/AKTS/wCaf1V/8psD/wBhaf8A0mtZJJTk/wDNP6q/+U2B/wCwtP8A6TS/5p/VX/ymwP8A2Fp/9JrWSSU5P/NP6q/+U2B/7C0/+k0v+af1V/8AKbA/9haf/Sa1kklPyqkkkkpSSSSSlJJJJKUkkkkp/9Dv/qn/AOJXo3/hDF/881rWWT9U/wDxK9G/8IYv/nmtaySlJJJJKUkkkkpSSSSSn5VSSSSUpJJJJSkkkklKSSSSU//R7/6p/wDiV6N/4Qxf/PNa1lk/VP8A8SvRv/CGL/55rWskpSSSSSlJJJJKUkkkkp+VUkkklKSSSSUpJJJJSkkkklP/0u/+qf8A4lejf+EMX/zzWtZcv9WPrP8AVuj6t9Jpu6thVW1YWOyyt+RU1zXNqra9j2Os3Ne1y0v+dn1V/wDLnA/9iqf/AEokp1klk/8AOz6q/wDlzgf+xVP/AKUS/wCdn1V/8ucD/wBiqf8A0okp1klk/wDOz6q/+XOB/wCxVP8A6US/52fVX/y5wP8A2Kp/9KJKdZJZP/Oz6q/+XOB/7FU/+lEv+dn1V/8ALnA/9iqf/SiSn5rSSSSUpJJJJSkkkklKSSSSU//Z/+0J/FBob3Rvc2hvcCAzLjAAOEJJTQQlAAAAAAAQAAAAAAAAAAAAAAAAAAAAADhCSU0D7QAAAAAAEABIAAAAAQACAEgAAAABAAI4QklNBCYAAAAAAA4AAAAAAAAAAAAAP4AAADhCSU0EDQAAAAAABAAAAHg4QklNBBkAAAAAAAQAAAAeOEJJTQPzAAAAAAAJAAAAAAAAAAABADhCSU0ECgAAAAAAAQAAOEJJTScQAAAAAAAKAAEAAAAAAAAAAjhCSU0D9QAAAAAASAAvZmYAAQBsZmYABgAAAAAAAQAvZmYAAQChmZoABgAAAAAAAQAyAAAAAQBaAAAABgAAAAAAAQA1AAAAAQAtAAAABgAAAAAAAThCSU0D+AAAAAAAcAAA/////////////////////////////wPoAAAAAP////////////////////////////8D6AAAAAD/////////////////////////////A+gAAAAA/////////////////////////////wPoAAA4QklN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L3JkZjpSREY+CjwveDp4YXBtZXRhPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKPD94cGFja2V0IGVuZD0ndyc/Pv/uAA5BZG9iZQBkgAAAAAH/2wCEAAgGBgYGBggGBggMCAcIDA4KCAgKDhANDQ4NDRARDAwMDAwMEQwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwBCQgICQoJCwkJCw4LDQsOEQ4ODg4REQwMDAwMEREMDAwMDAwRDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDP/AABEIAEAAgAMBIgACEQEDEQH/3QAEAAj/xAGiAAAABwEBAQEBAAAAAAAAAAAEBQMCBgEABwgJCgsBAAICAwEBAQEBAAAAAAAAAAEAAgMEBQYHCAkKCxAAAgEDAwIEAgYHAwQCBgJzAQIDEQQABSESMUFRBhNhInGBFDKRoQcVsUIjwVLR4TMWYvAkcoLxJUM0U5KismNzwjVEJ5OjszYXVGR0w9LiCCaDCQoYGYSURUaktFbTVSga8uPzxNTk9GV1hZWltcXV5fVmdoaWprbG1ub2N0dXZ3eHl6e3x9fn9zhIWGh4iJiouMjY6PgpOUlZaXmJmam5ydnp+So6SlpqeoqaqrrK2ur6EQACAgECAwUFBAUGBAgDA20BAAIRAwQhEjFBBVETYSIGcYGRMqGx8BTB0eEjQhVSYnLxMyQ0Q4IWklMlomOywgdz0jXiRIMXVJMICQoYGSY2RRonZHRVN/Kjs8MoKdPj84SUpLTE1OT0ZXWFlaW1xdXl9UZWZnaGlqa2xtbm9kdXZ3eHl6e3x9fn9zhIWGh4iJiouMjY6Pg5SVlpeYmZqbnJ2en5KjpKWmp6ipqqusra6vr/2gAMAwEAAhEDEQA/AOh+S/Jfk668neXrq68vaZNcTaZZSTTSWVu7u728bO7u0ZZnZjyZmw8/wJ5H/wCpa0r/AKQbb/qnm8if8oP5a/7ZVj/1DR5IMVY//gTyP/1LWlf9INt/1Tzf4E8j/wDUtaV/0g23/VPJBmxVj/8AgTyP/wBS1pX/AEg23/VPN/gTyP8A9S1pX/SDbf8AVPJBmxVj/wDgTyP/ANS1pX/SDbf9U83+BPI//UtaV/0g23/VPJBmxV8AZs2bFXZs2bFXZs2bFXZs2bFX/9Dr/kT/AJQfy1/2yrH/AKho8kGR/wAif8oP5a/7ZVj/ANQ0eSDFXZs2bFXZs2bFXZs2bFXwBmzZsVdmzZsVdmzZsVdmzZsVf//R6/5E/wCUH8tf9sqx/wCoaPJBkf8AIn/KD+Wv+2VY/wDUNHkgxV2bNmxV2bNmxV2bNmxV8AZs2bFXZs2bFXZs2bFXZs2bFX//0uv+RP8AlB/LX/bKsf8AqGjyQZA/JfnTyda+TvL1rdeYdMhuIdMso5oZL23R0dLeNXR0aQMrqw4srYef478j/wDUy6V/0nW3/VTFWQZsj/8AjvyP/wBTLpX/AEnW3/VTN/jvyP8A9TLpX/Sdbf8AVTFWQZsj/wDjvyP/ANTLpX/Sdbf9VM3+O/I//Uy6V/0nW3/VTFWQZsj/APjvyP8A9TLpX/Sdbf8AVTN/jvyP/wBTLpX/AEnW3/VTFXxBmzZsVdmzZsVdmzZsVdmzZsVf/9k=" width="128" height="64" class="img_ev3q"></p>
<p>Это альфаканал. Сама она белая. Текстурными координатами для неё является позиция вертекса в пространстве камеры (D3DTSS_TCI_CAMERASPACEPOSITION), умноженная на матрицу странного вида. После долгих (и тщетных) попыток понять всю эту хитрую математику логически, я запустил игру и стал приглядываться, пытаясь обнаружить что-то визуально. Единственное, что я заметил, так это темная полоса посреди экрана. Уверен, что это и есть эффект, который даёт эта текстура, хотя для чего это было сделано остается загадкой. Кто знает, может, без этой хитрости, картинка резко отличается в худшую сторону? :)</p>
<p>Лайтмапа земли:</p>
<p><img decoding="async" loading="lazy" alt="Ground LM" src="https://romanmarchenko.me/assets/images/ground_lm-6517abc8d12d70212515714aa56ddeb2.jpg" width="512" height="512" class="img_ev3q"></p>
<p>Туман войны в формате LUMINANСE8. Считается на CPU:</p>
<p><img decoding="async" loading="lazy" alt="FOW" src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAIAAgADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDLhtZ7RDHcSB3ZmkBBJ+VyWUc+isB+HHFSU54/LYL5McOQG2p0ORnd0HJzk8dSeT1LaACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigBWj8o7fJSHIDbU6HIzu6Dk5yeOpPJ6lKc8flMF8lIsgNtTocjO7oOTnJ46k8nqW0AFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFADnj8pgvkxxZAbbH0ORnd0HJzk8dSeT1Lac8fltt8qOLIDbY+hyM56Dk5yeOpPJ6ltABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQA6SPymC+THFlQ22PocjOeg5OcnjqTyepbT5I/KYL5McWVDbY+hyAd3QcnOTx1J5PUsoAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAHyReUwXyI4cqrbYzkHIB3dBy2dx9yeT1LKkmi8lwvkRQ5VW2xHIOQDu6Dls7j7k8nqY6ACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigCWeLyZAvkRQZRG2RHIOVB3dBy2dx9yeT1MVSzxeTIF+zxQZRG2RHIOVB3dBy2dx46k8nqYqACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigCWeLyZAv2eKDKI2yI5ByoO7oOWzuPHUnk9TFUk8XlSBfIihyittiOQcqDu6Dls7j7k8nqY6ACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigCSaPynC+RFDlFbbEcg5UHd0HJzuPuTyepjqSaLypAvkRQ5RW2xHIOVB3dByc7j7k8nqY6ACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigCSePypAvkQw5RW2xHg5UHd0HJzuPuTyepjqSaLypAvkRQ5RW2xHIOVB3dByc7j7k8nqY6ACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigCSePypAvkQw5RW2xHg5UHd0HJzuPuTyepjqSaPynC+RFDlFbbEcg5UHd0HJzuPuTyepjoAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACux8O+AbrVokurt/s9u3IGPmYVS8FaKdY1yMuuYIfnf0PoK9qVQqhVAAHAAoA5qPwFoCRqrWpcgYLFjk0y4+H+hTQMkcDRMejq3IrqaKAPF/Efgu90JTOp8+1z99Ryv1rmK+i54Y7iB4ZVDI4wQRXhPiLSX0bWp7Vgdm7ch9VNAGVRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFAEk0XlSBfIihyittiOQcqDu6Dls7j7k8nqY6kni8qQL5EUOUVtsRyDlQd3Qctncfcnk9THQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAekfCwHGoHHHy816PXKfD20jt/DEcq53TMWbNdXQAUUUUAFeZ/E7TZBcW+og5jK+WfY16ZXAfE6+hGn21lnMzPvx6CgDy+iiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAJJ4vKkC+RFDlFbbEcg5UHd0HLZ3H3J5PUx1JNF5UgXyIocorbYjkHKg7ug5bO4+5PJ6mOgAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA9U+G2rJNpkmnySfvImyin+6fSu7r54sr2fT7pLm2kKSIcgivTNJ+JNlNGkeoRtDJ0LjlfrQB3dQXl5b2Fs9xcyLHEgyWNc3d/ELQ7eLdFK87f3VWvO/Eviq68QzAEGK2X7sQP6mgDvdT+IulWsGbPdcynoAMAfWvMNW1W51i/e7uny7dB2UelUaKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAJJo/KcL5EUOUVtsRyDlQd3Qctncfcnk9THUk0XlOF8iKHKK22I5ByoO7oOTncfcnk9THQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUASTReVIF8iKHKK22I5ByoO7oOTncfcnk9THUk8XkyBfIigyittiOQcqDu6Dls7j7k8nqY6ACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigCWeLyZAvkRQZRG2RHIOVB3dBy2dx9yeT1MVSzxeTIF+zxQZRG2RHIOVB3dBy2dx46k8nqYqACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigCWeLyZAvkRQZRG2RHIOVB3dBy2dx9yeT1MVSTx+VIF8iKHKK22I5ByoO7oOWzuPuTyepjoAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAJJo/KcL5EUOUVtsRyDlQd3QcnO4+5PJ6mOpJo/KcL5EUOUVtsRyDlQd3QcnO4+5PJ6mOgAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAkmj8pwvkRQ5RW2xHIOVB3dByc7j7k8nqY6lni8mQL5EMOURtsJypyoO48D5jnJ9yeT1MVABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQBLcReVKF8iGHKI22E5U5UHceB8xzuPuTyepiqW5i8qVV8iGHMaNthOVOVB3HgfM2dx9yeT1MVABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQBLPH5UgXyIYcojbYjwcqDuPA+Zs7j7k8nqYqkni8mQL5EUOUVtsRyDlQd3Qctncfcnk9THQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUASTR+VIF8iKHKK22I5ByoO7oOWzuPuTyepjqSaPynC+RFDlFbbEcg5UHd0HJzuPuTyepjoAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAJJo/KcL5EUOUVtsRyDlQd3QcnO4+5PJ6mOpJo/KcL5EUOUVtsRyDlQd3QcnO4+5PJ6mOgAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAkmj8pwvkRQ5RW2xHIOVB3dByc7j7k8nqY6kmj8pwvkRQ5RW2xHIOVB3dByc7j7k8nqY6ACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigCSaPynC+RFDlFbbEcg5UHd0HJzuPuTyepjqSaPynC+RFDlFbbEcg5UHd0HJzuPuTyepjoAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAJJo/KcL5EUOUVtsRyDlQd3QcnO4+5PJ6mOnyx+U4XyYocqrbYuhyAd3QcnOTx1J5PUsoAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAJJovKcL5EUOUVtsRyDlQd3QcnOT7k8nqY6kmi8qQL5EUOUVtsRyDlQd3Qctncfcnk9THQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUASTReVIF8iKHKK22I5ByoO7oOWzuPuTyepjqSaPynC+RFDlFbbEcg5UHd0HJzuPuTyepjoAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAHyx+U4XyYocqrbYuhyAd3QcnOTx1J5PUsp8kflMF8mOHKq22PocgHd0HJzk8dSeT1LKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigB8kflMF8iOHKhtsZyDkA7ug5Odx9yeT1LKfJF5LBfIjgyobZGcg5AO7oOWzuPuTyetMoAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAHSReSwX7PHBlQ2yNsg5Gd3QctncfQk9etNp0kXksF+zxwZUNsjbIORnd0HLZ3EdiT1602gAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAdJF5LBfs8cGVDbI2yDkZ3dBy2dx9CT1602nyReSwXyI4MqrbI2yDkA7ug5bO4+5PJ60ygAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAkmi8lwvkRQ5VW2xHIOQDu6Dls7j7k8nqY6fIqowCxQRDap2wnKkkAkngfMep9yeT1LKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/2Q==" width="512" height="512" class="img_ev3q"></p>
<p>Результат таков:</p>
<p><img decoding="async" loading="lazy" alt="Ground Complete" src="https://romanmarchenko.me/assets/images/ground_complete-82d8872d614d433f5a6686cfa9f8256f.jpg" width="1024" height="768" class="img_ev3q"></p>
<p>Далее следует небо. На меш в форме купола накладывается текстура вот такого вида:</p>
<p><img decoding="async" loading="lazy" alt="Sky" src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAEABAADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDtqKKK5DcKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACilooASiloxQAlFLiigBKKWigBKKWigBKKWigBKKWigBKKWjFACUUuKMUAJRS4ooASilxRigBKKXFFACUUtFACUUtGKAEopaKAEopaKAEopaKAEopaMUAJRS4oxQAlFLiigBKKXFGKAEopcUYoASilxRigBKKWjFACUUuKKAEopaKAEopaKAEopaKAEopcUUAJRS0UAJRS0UAJRS0YoASilxRQAlFLRQAlFLRQAlFLiigBKKWjFACUUuKMUAJRS0YoASilooASiloxQAlFLijFACUUuKMUAJRS4oxQAlFLijFACUUuKKAEopaKAEopaKAEopaKAEopaKAEopaKAEopaMUAJRS0YoASilooASilooASiloxQAlFLiigBKKWigBKKXFFACUUtFACUUtGKAEopaKAEopaMUAJRS4oxQAtGKdijFMQ3FFOxRigLjaKdRigBtFOoxQFxtGKdijFFguNxRinYoxQFxuKKdijFAXG0Yp2KMUBcbiinYoxQFxtFOxRigBtFOxRigLjcUYp2KMUBcbijFOoxQFxuKMU7FGKAuNxRTsUYoAbRTsUYoC43FGKdijFAXG4op2KMUANop2KMUANxRinYoxQFxuKMU7FGKAG4oxTsUYoAbiinYoxQA3FGKdijFAXG0U7FGKAuNop2KMUBcbijFOxRQA3FFOxRigBuKKdijFADaMU7FGKAG4oxTsUUBcbiinYoxQFxtFOxRigLjaMU7FGKAuNxRTsUYoC42inYoxQFxtFOxRigBtFOxRigBtFOxRQFxuKMU7FGKAuNxRTsUYosFxtFOxRigBtGKdijFADcUYp2KMUBcbijFOxRRYLjcUYp2KKLBcbijFOxRigLjcUU7FGKAuNxRTsUYoC42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" width="1024" height="256" class="img_ev3q"></p>
<p>которая блендится с константным цветом, по-видимому, для аттенюации (я тут не до конца разобрался). Текстура неба сделана так, чтобы нижняя её часть совпадала с цветом тумана, который используется на уровне. Таким образом достигается бесшовный переход цветов.</p>
<p>Когда отрисовка неба закончена, начинается рендеринг объектов.
Формула простая: диффузная текстура + туман войны + та самая, хитрая 2-х тексельная текстурка . Постоянно включено солнышко – первый directional источник.
Пример текстуры объектов:</p>
<p><img decoding="async" loading="lazy" alt="Object Diffuse" src="https://romanmarchenko.me/assets/images/object_diffuse-1d2290a97dbb8a25336661278153dbf6.jpg" width="512" height="512" class="img_ev3q"></p>
<p>Юниты отображаются абсолютно аналогично. Скининг на CPU.
Диффузная текстура:</p>
<p><img decoding="async" loading="lazy" alt="Monster Diffuse" src="https://romanmarchenko.me/assets/images/monster_diffuse-2de8524d8fe297348414246c5a4b9848.jpg" width="512" height="512" class="img_ev3q"></p>
<p>Системы частиц. Тут всё стандартно.
Далее идут маркеры под юнитами, рамка под выделенным зданием (рисуется каждая сторона прямоугольника отдельно, что очень странно), и т.д.</p>
<p><img decoding="async" loading="lazy" alt="Before UI" src="https://romanmarchenko.me/assets/images/before_ui-de72c6caf0e87e2ea1f31448b1da92a0.jpg" width="1024" height="768" class="img_ev3q"></p>
<p>Последним идет UI. DPUP’ов нет (слава богу), но всё поэлементно. Текстуры для UI не в атласах (что тоже странно). Каждая кнопочка – отдельная картинка. Текст блоками.</p>
<p>Вот и всё. W40K: DoW – это ярчайший пример заруливания арта и дизайнерской работы. Сказать больше нечего. После таких вот откровений, мне стало очень интересно, а как дело обстоит в Company of Heroes? Помня о том, что там я видел шадоумапы (да, их можно сделать на FFP, но только на NVidia аппаратуре), я понимал, что вряд-ли встречу чистый FFP во второй раз. Единственное, что могу сказать сейчас, так это то, что CoH таит в себе не меньшее количество сюрпризов. :) Часть 2 не за горами. Stay tuned!</p>]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[Gothic 3. Двойственное впечатление.]]></title>
        <id>https://romanmarchenko.me/ru/blog/gothic3</id>
        <link href="https://romanmarchenko.me/ru/blog/gothic3"/>
        <updated>2006-11-09T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Сегодня наш пациент рендер готики 3.]]></summary>
        <content type="html"><![CDATA[<p>Сегодня наш пациент рендер готики 3.
Проблемы начались с самого начала. Готика никак не хочет запускаться из-под pixwin. Какая-то статически прилинкованая dll'ка инициализируется с ошибкой. Пробовал подебагать, но, к сожалению, чтобы попробовать решить проблему скила не хватает :) Итого остаётся NVPerfHUD. Разработчики забыли выбросить его поддержку из кода, так что никаких форсилок не понадобилось. Но перфхуд экспортит не все ресурсы (готика часто крашится) и выловить нужный draw call не так уж и просто, так что много картинок сегодня не будет.</p>
<p>Недавно, я себе открыл простой и быстрый способ определения качества рендера. Надо всего-лишь запустить игру с дебаговым директом, а потом смотреть где и как падает. :) Готика падает очень быстро. Первый кадр рендера ГУИ. Как контр-пример: вархамерровский движок Relic'а (Warhammer 40K: Down of War, W40K: Winter Assault, W40K: Dark Crusade, Company of Heroes). Прошёл в Dark Crusade пару миссий, и только потом заметил, что сижу на дебаговом директе. Хороший рендер. :) Извините, отвлёкся.</p>
<p>Машина на которой тестировалась игра: A64X2 4200+, 2GB RAM, GF7800GT. Настройки игры - high.</p>
<p>Фпс 15-20. Чему удивляешься сразу, так это маленькому количеству DIP calls. 400-700. Всегда. Без динамических тенюшек так вообще 300. Все здорово до того момента, когда мы достаём факел. DIPы подскакивают до 1200. Вы тоже подумали сразу про omnidirectional shadow maps? Правильно, они там есть. :) Кстати, фпс падает всего на 2-3.</p>
<p>Что, как и в каком порядке рисуется в готике:</p>
<ol>
<li class="">
<p>Shadow map от солнышка (directional light). Тут все просто. Берём квадрат вокруг персонажа со стороной 20 метров, и все объекты что туда попали рисуем. PSM, TSM, LiSPSM и т.д. не используется. Используем NV depth texture hack.</p>
</li>
<li class="">
<p>Если включен факел, то рисуем R32F cubemap.</p>
</li>
<li class="">
<p>Z-only pass. Рисуется не все, а только крупные объекты. Очень правильно.</p>
</li>
<li class="">
<p>Баундинг боксы для использования HW Occlusion Query. Опять же, объекты не все.</p>
</li>
<li class="">
<p>Если включен Depth of Field эффект, используем MRT. 2 рендер таргета: цвет (R8G8B8A8), и глубина (тоже R8G8B8A8). Глубина записывается как есть, никакой запаковки флоата в восьмибитные каналы не происходит.</p>
</li>
<li class="">
<p>Рисуем персонажей. Скиннинг на GPU, нормал мапы, попиксельные точечные (я видел 2) и направленные источники. 2 шадовмапы (от точечного источника и солнышка). Предрасчитанная текстура с тенями от террейна. Используются даже Spherical Harmonics (не спрашивайте меня, что это. Я сам с трудом представляю:) ). Шейдеры показывать не буду, так как они длинные и страшные.</p>
</li>
<li class="">
<p>Ближняя земелька рисуется очень похоже. Нет скининга, есть несколько слоёв смешанных по маске. Я бы текстурки показал, но по причинам описанным выше (pix не работает), я их не добыл. Сорри.</p>
</li>
<li class="">
<p>Ближние объекты (деревья, камни, травка).</p>
</li>
<li class="">
<p>Всякие дальние объекты. Огромные куски земли. Спидтришные деревья, крупные объекты. Шейдеры тут простые. Зачастую туманчик + 1 текстура.
Получается примерно следующие картиночки:</p>
</li>
<li class="">
<p>Простое Небо.</p>
</li>
</ol>
<p>Далее идет пост-процесс.</p>
<ol>
<li class="">Даунсемплим картинку. Используем color remap, чтобы выделить яркие области.</li>
<li class="">Блурим её разочек.</li>
<li class="">Собираем финальное изображение. Блум текстура + сцена + глубина сцены. Пуассоновским фильтром блурим основную картинку, радиус распределения получая из текстуры с глубиной. Не Depth of Field, конечно, но нечто похожее.</li>
</ol>
<p>Про воду забыл. Я по игре много не бегал, но морская вода совсем не такая, которая была описана Куртом Пельзером (Kurt Pelser) в ShaderX2 - Tips and Tricks. Простая какая-то. Я думал уже накручивать сложность эффектов так накручивать.
Хотя ещё проходы они бы, наверное, уже не потянули. :)</p>
<p>ГУИ - хороший. Не по буковке. :) Отвлекусь ещё раз. Вот в Supreme Commander Multiplayer Beta ГУИ! ГУИще самый настоящий. :) Кто не знает - это стратегия такая. Так вот, там весь ингейм ГУИ рисуется за 1 (один) draw call! :) Сильно.</p>
<p>Короткие выводы.
Что понравилось:
Впечатлён технологичностью рендера. Отличная draw call reduction оптимизация, что позволило сделать даже такую экзотическую вещь, как omni shadows.</p>
<p>Что не понравилось:
Чувствуется халатный подход (дебаговый D3D. VB, IB постоянно создаются в рантайме).</p>
<p>Мне рендер скорее понравился, чем нет. Это даже не смотря на тяжёлые шейдеры, и как следствие низкий фпс.</p>
<p>Update
Нашёл параллакс. Есть на каменной площади в стартовом городишке.
По просьбам больше картинок, которые прояснят ситуацию с батчингом геометрии.
Ближняя травка:
Ближняя земелька:
Дальние деревья:
Камешки:
Дальняя земля:
В городах все стандартно. Домики по кускам. Отдельные объекты (лавочки, столбики, заборчик).</p>]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[Ода о кривых руках, или почему так тормозит NFS Carbon.]]></title>
        <id>https://romanmarchenko.me/ru/blog/nfscarbon</id>
        <link href="https://romanmarchenko.me/ru/blog/nfscarbon"/>
        <updated>2006-11-01T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Ода о кривых руках, или почему так тормозит NFS Carbon.]]></summary>
        <content type="html"><![CDATA[<p>Ода о кривых руках, или почему так тормозит NFS Carbon.</p>
<p>Покупка новой видеокарты всегда несёт с собой новые ожидания о красивых эффектах в играх, возросшему ФПСу, и всеобщую радость хозяина. К сожалению, в последнее время, новой видеокартой (процессором, памятью) мы выравниваем руки программистов игр. Это очень печально, особенно на фоне того, что разработчики и издатели с мировыми именами позволяют себе такую техническую некомпетентность, за счёт нашего кошелька.</p>
<p>Итак NFS Carbon. Супер бренд, ведущий издатель. У вас высокие ожидание к этому продукту? У меня тоже. Были. Если, вы читали мои предыдущие постинги, то знаете, что я увлекаюсь реверс ижинерингом рендеров популярных игр. Обычно я брал хорошую игру, и смотрел почему же так все хорошо. Когда я запустил демку нового NFS, меня стали сильно одолевать вопросы "почему все так плохо?" и "откуда такие тормоза?".</p>
<p>Итак, "чудо" програмистского исскуства NFS Carbon.
Ингейм рендер (первая гонка в демке): в кадре от 1000 до 3000 (!) DIPов. Это ужас. Среднее значение 2200-2300. Рисуется все мелкими объектами. Окна на домах отдельно каждое, и т.д. Огромные шейдеры. Огромноая универсальность. В шейдерах для машинок учитывается всё: какие-то туманы, microflakes для металлических поверхностей (я понимаю использовать их в "гараже", там машинку рассматриваешь. Зачем они в гонке, объясните?). Слава богу для машинок есть LOD шейдеров. Для статики нету. Всего переключений шейдеров на кадр 100-200, а вот текстур 1500-2000, а рендер стейтов (о ужас) 15000 и больше!</p>
<p>Ужасно рисуется небо. Каким-то ужасно сложным шейдером (инструкций в 70, кажется) смешивается 2 текстурки первыми DIP вызовами. Естественно занимает это все дело половину экрана. А потом, с чистой совестью, затёрли процентов 80 неба домиками. Какие молодцы. У вас крутая видяха, у которой пиксельные конвеера исчисляются десятками? Я вас поздравляю. Теперь есть чем её занять. :)</p>
<p>На нашем болиде realtime environment mapping, создание которого размазано по 6 кадрам (т.е. в 1 кадр игры рисуется 1 сторона кубемапы). Flatout 2 ясно показал, что честного и не надо вовсе. Я долго ездил в NFS'е на Gallardo туда сюда, пытаясь более точно определить схему обновления кубемапы. Вычислял эти честные отражения. Так и не вычислил. Единственное, где их более менее видно,то в кинематографических пролётах камеры на старте гонки. Хотя, так как машинки не отражаются (они в кубическую текстуру не рисуются), то и понту не много. А машинки не отражаются потому, что так как текстура всего одна (только для нашей машинки), то было бы заметно, что отражения в машинах компьютерных гонщиков (для них используется та же текстура) стало неправильное. В общем, моё мнение по этому вопросу - выкинуть реалтайм нафиг, и практически без потери качества поднять ФПС. Плюс ко всему, есть A16R16G16B16F текстура, в красный канал которой, на каждом кадре, записывается нормализированная глубина всей сцены. Используется она только время генераци кубической текстуры (как я так и не понял). Я было подумал про soft particles. Ан нет, фигушки. ABG каналы не используются. У вас на борту видеокарты много памяти? Так вот знайте, что играя в NFS Carbon почти 5 мегабайт (это в разрешении 1024х768) заняты пустотой.</p>
<p>GUI. Попробуете отгадать? Да, да, DrawPrimitiveUP. Весь GUI рисутеся DPUP'ами! Поэлементно. По буковке. 200-300 вызывов. У вас быстрый процессор? (даже с двумя ядрами?) Вот знайте, что играя в NFS Carbon он занимается простаиванием и пустой пересылкой вертексов в видеокарту. Очень полезная работа.</p>
<p>Motion blur. На самом деле это просто весовой Radial Blur. В основном проходе рендера пишем в альфа канал степень разблуривания конкретного пикселя. Ближние сильнее дальние слабее. На первый взгляд все просто. Смущает всего две вещи:</p>
<ol>
<li class="">Почему шейдер блура такой огромный (100 инструкций)?</li>
<li class="">Почему в картинке, которая блурится нет машинок? Хотя может потому, что машинки обычно двигаются с сопоставимыми скоростями. Не предусмотрено одно: траффик часто двигается навстречу. Чёткий автомобиль, что едет по соседней полосе, на фоне размытой дороги смотрится аляповато.</li>
</ol>
<p>Гараж и главное меню. Всегда очень "уважал" игры, которые начинают тормозить прямо с меню. Я "уважаю" NFS Carbon :). Она начинает тормозить с экрана, где пишут "Loading..." Но это не удивительно, ведь вы не знали, что на этом экранчике 1000 DPUP'ов :)
В общем гараж это обычный уровень, на котором рисуется даже небо (кто его там увидит, тому пирожок). Все те же, никому не нужные, сложные шейдеры. Отдельный перл гаража - отражения на полу. Не видно? Присмотритесь внимательнее. Так вот, в это "очень заметное отражение" рисуется абсолютно вся геометрия гаража. С теми же ужасными огромными шейдерами. Все супер мелкие детальки автомобиля. Все полочки, детальки и рюшечки гаража. Я не знаю чем думают разработчики, когда тратят столько ресурсов на фишку, которой практически не видно (я нашел отражения, только после того как увидел, что туда что-то рисуется).</p>
<p>Краткий вывод почему все так плохо:</p>
<ul>
<li class="">Огромное количество вызовов рендера (драйвер тайм половина времени кадра)</li>
<li class="">Использвангие DPUP.</li>
<li class="">Сомнительные графические решения.</li>
<li class="">Большущие убер-шейдеры (я в них не разбирался, но у меня стойкое чувство, что можно проще).</li>
</ul>
<p>З.Ы. Тестировалась демо версия. Сильно сомневаюсь, что в релизе будет лучше.
З.З.Ы. Предыдущие версии NFS'а не исследовал. Я не знаю как там, но, по крайней мере, не тормозят.</p>]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[PainKiller: Технологии рендера.]]></title>
        <id>https://romanmarchenko.me/ru/blog/painkiller</id>
        <link href="https://romanmarchenko.me/ru/blog/painkiller"/>
        <updated>2006-09-06T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Как-то давно, я уже реверсил рендер пейнкиллера, но только очень поверхностно. А так как о моем мини-исследовании рендера FlatOut’а я неожиданно получил очень положительные отзывы, то решил серьезно заняться этой, несоменно, великолепной игрой.]]></summary>
        <content type="html"><![CDATA[<p>Как-то давно, я уже реверсил рендер пейнкиллера, но только очень поверхностно. А так как о моем мини-исследовании рендера FlatOut’а я неожиданно получил очень положительные отзывы, то решил серьезно заняться этой, несоменно, великолепной игрой.
Итак, почему же PainKiller. Причины схожи с причинами, которые меня побудили взглянуть на FlatOut. Хоть и игра уже немного старая, картиночка, выдаваемая игрой, мне очень нравится. Учитывая великолепную скорость отрисовки, рендер заслуживает всяческих похвал.</p>
<p>Итак, реверсил я на максимальных настройках графики ("insane" в параметрах конфиграции) , на видеокарте GF6600.</p>
<p>Рендер игрового кадра состоит из следующих этапов:</p>
<ul>
<li class="">
<p>Если на уровне есть вода, то рисуется рефлекшин мапа. Никаких упрощений геометрии и шейдеров, по сравнению с основным проходом (их детальное описание см. дальше), я не заметил. Рисуется все, что видно в главную камеру. Т.е. теоретически можем поиметь эффект внезапного исчезновения/появления отражений невидимых в камеру объектов. Практически же, я пытался поймать эту ситуацию (не долго правдаJ). Не поймал.</p>
</li>
<li class="">
<p>Тени для монстров. Для каждого используется RTT (render target texture) A8R8G8B8 128х128.</p>
</li>
<li class="">
<p>Блурим эти текстуры каждую по 2 раза простым 4 tap фильтром, используя смещения координат.
Получаем</p>
</li>
</ul>
<p><img decoding="async" loading="lazy" alt="Monster Shadow" src="data:image/jpeg;base64,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" width="128" height="128" class="img_ev3q"></p>
<ul>
<li class="">Монстрики. Используется хардварный скининг. Точно количество костей не знаю, но учитывая что vs.1.1, смею предположить что около 20. Модель содержит, естественно, больше, поэтому она разбивается на несколько сабмешей, по костям. Количество влияний на вершину – 4. Скиненные персонажи рисуются в 1 проход, поэтому освещаются сразу.
Поддерживаются, похоже, только directional источники, коих может быть до 5 штук. Пиксельного шейдера нет. Освещение посчитанное в vs, умножается на диффузную текстуру (которая, кстати, очень качественная).
Текстура монстрика (оригинальный размер 1024x1024):</li>
</ul>
<p><img decoding="async" loading="lazy" alt="Shader Base" src="https://romanmarchenko.me/assets/images/monster_base_tex-6671d51b2d1aa7e21c36d400c49a1712.jpg" width="512" height="512" class="img_ev3q"></p>
<details class="details_lb9f alert alert--info details_b_Ee" data-collapsed="true"><summary>Шейдер для монстров</summary><div><div class="collapsibleContent_i85q"><div class="language-nasm codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-nasm codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">vs_1_1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_position v0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_normal v1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_texcoord v2</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_blendweight v3</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_blendindices v4</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp4 oT0.x, v2, c24</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp4 oT0.y, v2, c25</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp4 oT1.x, v2, c24</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp4 oT1.y, v2, c25</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp3 </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.w, v3, c8.z</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">add </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.w, c8.z, </span><span class="token operator" style="color:#393A34">-</span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.w</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">, v4.zyxw, c8.y, c8.x</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mov a0.x, </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">.x</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">m3x3 </span><span class="token register variable" style="color:#36acaa">r5</span><span class="token plain">.xyz, v1, c0</span><span class="token operator" style="color:#393A34">[</span><span class="token plain">a0.x</span><span class="token operator" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> Скининг позиции по первой кости</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">m4x3 </span><span class="token register variable" style="color:#36acaa">r6</span><span class="token plain">.xyz, v0, c0</span><span class="token operator" style="color:#393A34">[</span><span class="token plain">a0.x</span><span class="token operator" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> Скининг нормали</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mul </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r5</span><span class="token plain">.xyzz, v3.x </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> Блендим по весу кости</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mul </span><span class="token register variable" style="color:#36acaa">r4</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r6</span><span class="token plain">.xyzz, v3.x</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mov a0.x, </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">.y</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">m3x3 </span><span class="token register variable" style="color:#36acaa">r5</span><span class="token plain">.xyz, v1, c0</span><span class="token operator" style="color:#393A34">[</span><span class="token plain">a0.x</span><span class="token operator" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> тут и дальше аналогично по </span><span class="token number" style="color:#36acaa">2</span><span class="token operator" style="color:#393A34">-</span><span class="token number" style="color:#36acaa">4</span><span class="token plain"> костям.</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">m4x3 </span><span class="token register variable" style="color:#36acaa">r6</span><span class="token plain">.xyz, v0, c0</span><span class="token operator" style="color:#393A34">[</span><span class="token plain">a0.x</span><span class="token operator" style="color:#393A34">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r5</span><span class="token plain">.xyzz, v3.y, </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad </span><span class="token register variable" style="color:#36acaa">r4</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r6</span><span class="token plain">.xyzz, v3.y, </span><span class="token register variable" style="color:#36acaa">r4</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mov a0.x, </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">.z</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">m3x3 </span><span class="token register variable" style="color:#36acaa">r5</span><span class="token plain">.xyz, v1, c0</span><span class="token operator" style="color:#393A34">[</span><span class="token plain">a0.x</span><span class="token operator" style="color:#393A34">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">m4x3 </span><span class="token register variable" style="color:#36acaa">r6</span><span class="token plain">.xyz, v0, c0</span><span class="token operator" style="color:#393A34">[</span><span class="token plain">a0.x</span><span class="token operator" style="color:#393A34">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r5</span><span class="token plain">.xyzz, v3.z, </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad </span><span class="token register variable" style="color:#36acaa">r4</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r6</span><span class="token plain">.xyzz, v3.z, </span><span class="token register variable" style="color:#36acaa">r4</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mov </span><span class="token register variable" style="color:#36acaa">r4</span><span class="token plain">.w, c8.z</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">m4x4 </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r4</span><span class="token plain">, c0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mov oPos, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad oFog, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.z, c9.y, c9.x</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mov </span><span class="token register variable" style="color:#36acaa">r4</span><span class="token plain">, c11.xyzz</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mov </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.w, c10.w</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp3 </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.x, </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain">, c13 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> n dot l</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp3 </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.y, </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain">, c14 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> n dot h</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">lit </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> посчитали освещение от directional источника. Тут всего </span><span class="token number" style="color:#36acaa">1</span><span class="token plain"> источник, но может быть больше. Считается аналогично.</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad oD0, </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">.y, c12, </span><span class="token register variable" style="color:#36acaa">r4</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mul </span><span class="token register variable" style="color:#36acaa">r5</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">.z, c12</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mul oD1, </span><span class="token register variable" style="color:#36acaa">r5</span><span class="token plain">, c10</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">-</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">ps – ffp</span><br></div></code></pre></div></div></div></div></details>
<ul>
<li class="">Первый проход на объекты. FFP + простые шейдеры (1.1).
При отрисовке земли используется 2 диффузные текстуры с рисунком камешков и травички, которые блендятся по заданной маске. Также используется лайтмапа.</li>
</ul>
<details class="details_lb9f alert alert--info details_b_Ee" data-collapsed="true"><summary>Шейдер для земли</summary><div><div class="collapsibleContent_i85q"><div class="language-nasm codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-nasm codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">vs_1_1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_position v0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_texcoord v1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_texcoord1 v2</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">m4x4 </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">, v0, c0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mov oPos, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad oFog, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.w, c9.y, c9.x</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp4 oT0.x, v1, c24</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp4 oT0.y, v1, c25</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp4 oT1.x, v1, c27</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp4 oT1.y, v1, c28</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mov oT2.xy, v2.xyyy</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mov oT3.xy, v2.xyyy</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> approximately </span><span class="token number" style="color:#36acaa">12</span><span class="token plain"> instruction slots used</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">ps_1_1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">tex t0 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> ground texture </span><span class="token number" style="color:#36acaa">1</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">tex t1 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> ground texture </span><span class="token number" style="color:#36acaa">2</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">tex t2 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> blend map</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">tex t3 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> light map</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mul </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.xyz, t0, </span><span class="token number" style="color:#36acaa">1</span><span class="token operator" style="color:#393A34">-</span><span class="token plain">t2 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> Темные регионы на маске блендинга соответсвуют t0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> mul </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.w, t0.w, </span><span class="token number" style="color:#36acaa">1</span><span class="token operator" style="color:#393A34">-</span><span class="token plain">t2.w </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> Зачем тут параллелить альфа и вектор пайпы? Не пойму.</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.xyz, t1, t2, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> Светлые регионы </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> t1. Результат сложили</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> mad </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.w, t1.w, t2.w, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.w </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> Опять…</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mul_x2 </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.xyz, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">, t3 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> на лайтмапу и </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">2</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> mul </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.w, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.w, t3.w </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> альфу на </span><span class="token number" style="color:#36acaa">2</span><span class="token plain"> не умножаем.</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> approximately </span><span class="token number" style="color:#36acaa">7</span><span class="token plain"> instruction slots used (</span><span class="token number" style="color:#36acaa">4</span><span class="token plain"> texture, </span><span class="token number" style="color:#36acaa">3</span><span class="token plain"> arithmetic)</span><br></div></code></pre></div></div></div></div></details>
<p>Пример маски блендинга для земли:</p>
<p><img decoding="async" loading="lazy" alt="Ground Blend Map" src="https://romanmarchenko.me/assets/images/ground_blend_map-9180fa56f80eb7b05fa3e752139ed2f2.jpg" width="512" height="512" class="img_ev3q"></p>
<p>Пример лайтмапы для земли:</p>
<p><img decoding="async" loading="lazy" alt="Ground LM" src="data:image/jpeg;base64,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IDmlopMUALRSYoxQAtFIBiloATNGaWigBOtAGKCcUZpALTSc0UUxhRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUxBRRRQAUhFLRQA2inUYoGNopcUYoASilxRigBKKXFGKAEopcUYoASilxRigBaKKKQBRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFAC5ozSUUALmkoooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiimIKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKQwooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiimIKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiikMKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiimIKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooopDCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooopiCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKQwooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKYgooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiikMKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooopiCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooopDCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiimIKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKQwooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA/9k=" width="512" height="512" class="img_ev3q"></p>
<p>При отрисовке всех остальных объектов используется лайтмапа и 1 диффузная текстура.
Для ближней геометрии используется также текстура с деталями.</p>
<details class="details_lb9f alert alert--info details_b_Ee" data-collapsed="true"><summary>Шейдер для объектов</summary><div><div class="collapsibleContent_i85q"><div class="language-nasm codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-nasm codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">vs_1_1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_position v0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_texcoord v1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_texcoord1 v2</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">m4x4 </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">, v0, c0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mov oPos, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad oFog, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.z, c9.y, c9.x </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> считаем туманчик</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp4 oT0.x, v1, c24 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> тут какой</span><span class="token operator" style="color:#393A34">-</span><span class="token plain">то рескейлинг. Присутствует во всех шейдерах. Глубоко не вникал.</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp4 oT0.y, v1, c25</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp4 oT1.x, v1, c27</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp4 oT1.y, v1, c28</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mov oT2, v2</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> approximately </span><span class="token number" style="color:#36acaa">11</span><span class="token plain"> instruction slots used</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">На момент теста в константах было вот что.</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> c24 – </span><span class="token number" style="color:#36acaa">20</span><span class="token plain">, </span><span class="token number" style="color:#36acaa">0</span><span class="token plain">, </span><span class="token number" style="color:#36acaa">0</span><span class="token plain">, </span><span class="token number" style="color:#36acaa">0</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> c25 – </span><span class="token number" style="color:#36acaa">0</span><span class="token plain">, </span><span class="token number" style="color:#36acaa">20</span><span class="token plain">, </span><span class="token number" style="color:#36acaa">0</span><span class="token plain">, </span><span class="token number" style="color:#36acaa">0</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> c27 – </span><span class="token number" style="color:#36acaa">20</span><span class="token plain">, </span><span class="token number" style="color:#36acaa">0</span><span class="token plain">, </span><span class="token number" style="color:#36acaa">0</span><span class="token plain">, </span><span class="token number" style="color:#36acaa">0</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> c28 – </span><span class="token number" style="color:#36acaa">0</span><span class="token plain">, </span><span class="token number" style="color:#36acaa">20</span><span class="token plain">, </span><span class="token number" style="color:#36acaa">0</span><span class="token plain">, </span><span class="token number" style="color:#36acaa">0</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">ps – FFP</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">t0 – base</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">t1 – detail – для ближних объектов этого нет.</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">t2 – lightmap</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">формула блендинга текстур: (base </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> detail – </span><span class="token number" style="color:#36acaa">0.5</span><span class="token plain">) </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> lightmap </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">2</span><br></div></code></pre></div></div></div></div></details>
<p>Лайтмапа для объектов (оригинальный размер 1024x1024):</p>
<p><img decoding="async" loading="lazy" alt="Object LM" src="https://romanmarchenko.me/assets/images/object_lm-cec4f6edc2ece61aed0ab6a61e327f3d.jpg" width="512" height="512" class="img_ev3q"></p>
<ul>
<li class="">
<p>Вода. Первый проход.
Сложные шейдеры, до конца не осилил :) Приводить их не буду.
Большое количество входных параметров. Vs – 56 инструкций, ps – 31 инструкция. В этом проходе накладываем reflections, анимируя её используя 2 dudv карты. Волнение геометрии создаем анимацией вершин в vs. Я вообще обнаружил повертексную анимацию, только после того, как шейдер проанализировал, и увидел, что там меняется позиция вершины. Так думал, что вода плоская.</p>
</li>
<li class="">
<p>Второй проход для воды.
Освещаем её 4-мя направленными (directional) источниками повертексно.</p>
</li>
</ul>
<details class="details_lb9f alert alert--info details_b_Ee" data-collapsed="true"><summary>Шейдер для второго прохода для воды</summary><div><div class="collapsibleContent_i85q"><div class="language-nasm codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-nasm codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">vs_1_1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_position v0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_normal v1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_texcoord v2</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">m4x4 </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">, v0, c0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mov oPos, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad oFog, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.z, c9.y, c9.x</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp4 oT0.x, v2, c24</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp4 oT0.y, v2, c25</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp4 oT1.x, v2, c27</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp4 oT1.y, v2, c28</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mov </span><span class="token register variable" style="color:#36acaa">r4</span><span class="token plain">, c11.xyzz </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> </span><span class="token register variable" style="color:#36acaa">r4</span><span class="token plain">.xyzw </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.125</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mov </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.w, c10.w</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">m3x3 </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain">.xyz, v1, c4 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> во view space</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp3 </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.x, </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain">, c13 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> n dot l</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp3 </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.y, </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain">, c14 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> n dot h</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">lit </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> осветили. После операции </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">.y </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> diffuse, </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">.z </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> specular</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad </span><span class="token register variable" style="color:#36acaa">r4</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">.y, c12, </span><span class="token register variable" style="color:#36acaa">r4</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> в c12 хранится цвет источника света</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mul </span><span class="token register variable" style="color:#36acaa">r5</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">.z, c12 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> для спекуляра тоже самое.</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp3 </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.x, </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain">, c16 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> дальше аналогично накапливаем diffuse и specular contribution для еще </span><span class="token number" style="color:#36acaa">3</span><span class="token operator" style="color:#393A34">-</span><span class="token plain">х источников.</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp3 </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.y, </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain">, c17</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">lit </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad </span><span class="token register variable" style="color:#36acaa">r4</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">.y, c15, </span><span class="token register variable" style="color:#36acaa">r4</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad </span><span class="token register variable" style="color:#36acaa">r5</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">.z, c15, </span><span class="token register variable" style="color:#36acaa">r5</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp3 </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.x, </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain">, c19</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp3 </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.y, </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain">, c20</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">lit </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad </span><span class="token register variable" style="color:#36acaa">r4</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">.y, c18, </span><span class="token register variable" style="color:#36acaa">r4</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad </span><span class="token register variable" style="color:#36acaa">r5</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">.z, c18, </span><span class="token register variable" style="color:#36acaa">r5</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp3 </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.x, </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain">, c22</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp3 </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.y, </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain">, c23</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">lit </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad oD0, </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">.y, c21, </span><span class="token register variable" style="color:#36acaa">r4</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad </span><span class="token register variable" style="color:#36acaa">r5</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">.z, c21, </span><span class="token register variable" style="color:#36acaa">r5</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mul oD1, </span><span class="token register variable" style="color:#36acaa">r5</span><span class="token plain">, c10</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> approximately </span><span class="token number" style="color:#36acaa">36</span><span class="token plain"> instruction slots used</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">ps – ffp</span><br></div></code></pre></div></div></div></div></details>
<p>В арсенале PainKiller’а есть еще один тип воды. Без честных отражений, с использованием лайтмап. Такой тип используется на уровнях, где воды много.
Например в локации C5L1_City_On_Water. Первый же тип используется на первом уровне аддона Battle Out Of Hell: C6L1_Orphanage. Думаю, что мощный редактор позволяет по-разному варьировать параметры воды, чтобы получить нужный визуальный стиль.</p>
<ul>
<li class="">Небо.
До 4-х слоев. Компоненты трехслойного неба:
полусфера, внутри чашечка в зените, потом опять полусфера. Эти слои блендятся стандартным альфаблендом (srcalpha, invsrcalpha), и используют шейдеры точно такие же как при рендеринге земли. Неиспользованные слои (вспомним, что при рендеринге земли используется 2 текстуры для комбинирования) заменяются dummy текстурой.</li>
</ul>
<p>Пример текстур неба:
Оригинальный размер 2048х512</p>
<p><img decoding="async" loading="lazy" alt="Sky1" src="https://romanmarchenko.me/assets/images/sky_1-87ce80e17f0c77e6ad2b6ea84349f277.jpg" width="1024" height="256" class="img_ev3q"></p>
<p>Оригинальный размер 1024x1024</p>
<p><img decoding="async" loading="lazy" alt="Sky2" src="https://romanmarchenko.me/assets/images/sky_2-ffba210f6abfb55f081cd8bbf0be2225.jpg" width="512" height="512" class="img_ev3q"></p>
<p>Оригинальный размер 1024x1024</p>
<p><img decoding="async" loading="lazy" alt="Sky3" src="https://romanmarchenko.me/assets/images/sky_3-d696dbbf8bfbf9048b712d6fed3a9d9a.jpg" width="512" height="512" class="img_ev3q"></p>
<ul>
<li class="">Второй проход на сцену – освещение. Тут все что земля, что остальные статические объекты рисуются одинаково. Освещение осуществляется от одного точечного источника, попиксельно. Весь процесс достаточно хитрый. Для учета расстояния используется текстура аттенюации такого содержания:</li>
</ul>
<p><img decoding="async" loading="lazy" alt="Mistic Circle" src="data:image/jpeg;base64,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" width="128" height="128" class="img_ev3q"></p>
<p>В вертексном шейдере мы вычисляем расстояние от источника света до вершины. Поделив полученное расстояние на дистанцию влияния источника, мы пакуем вектор так, что все расстояния на которые влияет источник попадают в диапазон [-1,1]. Скейлим дальше в [0,1]. Получили текстурные координаты для лукапа в текстуру аттенюации (так как address mode установлен в clamp, то все лукапы за пределами текстуры будут происходить по ее границе). В нашем примере используется простой кружочек, что означает простое постепенное ослабление освещения с расстоянием. Но задав, например тот же кружочек, но инвертировав его цвет, мы получим освещёнными дальние объекты, но неосвещённые ближние. В общем вариантов масса.
Шейдеры для освещения статики:</p>
<details class="details_lb9f alert alert--info details_b_Ee" data-collapsed="true"><summary>Шейдер для второго прохода для воды</summary><div><div class="collapsibleContent_i85q"><div class="language-nasm codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-nasm codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">vs_1_1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_position v0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_texcoord v1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_normal v3</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">m4x4 </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">, v0, c0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mov oPos, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad oFog, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.w, c9.y, c9.x</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">add </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">, v0, </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">c13 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> world pos – light pos</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mul </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">, c12.w </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> В с</span><span class="token number" style="color:#36acaa">12</span><span class="token plain">.w коэффициент дальности освещения источником. После умножения все расстояния на которые влияет источник, попадут в диапазон </span><span class="token operator" style="color:#393A34">[</span><span class="token operator" style="color:#393A34">-</span><span class="token number" style="color:#36acaa">1</span><span class="token plain">, </span><span class="token number" style="color:#36acaa">1</span><span class="token operator" style="color:#393A34">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">, c8.w, c8.w </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> Пакуем вектор </span><span class="token operator" style="color:#393A34">[</span><span class="token operator" style="color:#393A34">-</span><span class="token number" style="color:#36acaa">1</span><span class="token plain">, </span><span class="token number" style="color:#36acaa">1</span><span class="token operator" style="color:#393A34">]</span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token operator" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">0</span><span class="token plain">, </span><span class="token number" style="color:#36acaa">1</span><span class="token operator" style="color:#393A34">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mov oT0.xy, </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">.xyyy </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> xy записали в tc0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mov oT1.x, </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">.z </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> z – в tc1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mov oT1.y, c8.w </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> c8.w </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.5</span><span class="token plain">, получаем эмуляцию </span><span class="token number" style="color:#36acaa">1D</span><span class="token plain"> лукапа по горизонтальной линии в центре текстуры.</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mov oT2, </span><span class="token operator" style="color:#393A34">-</span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> записали vertex to light vector</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp4 oT3.x, v1, c33 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> учитывая, что c33 </span><span class="token number" style="color:#36acaa">1000</span><span class="token plain">, а с</span><span class="token number" style="color:#36acaa">34</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0100</span><span class="token plain">, эти </span><span class="token number" style="color:#36acaa">2</span><span class="token plain"> строки очень хитрый метод записать mov oT3.xy, v1. Возможно тут сделано с прицелом на анимацию текстурных координат.</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp4 oT3.y, v1, c34</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mov oD0, c12</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad oD1, v3, c8.w, c8.w </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> запаковали нормаль в цвет</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> approximately </span><span class="token number" style="color:#36acaa">17</span><span class="token plain"> instruction slots used</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">ps_1_1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">tex t0 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> Кружочек, снаружи непрозрачный, внутри прозрачный. А так белый.</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">tex t1 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> Тот же кружочек</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">tex t2 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> normalized vertex to light vector from normalization cubemap</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">tex t3 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> diffuse texture</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mul </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.xyz, t3.w, v0 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> diffuse color </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> base texture alpha</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp3_sat </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">, v1_bx2, t2_bx2 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> n dot l</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mul </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.xyz, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> add </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.w, </span><span class="token number" style="color:#36acaa">1</span><span class="token operator" style="color:#393A34">-</span><span class="token plain">t0.w, </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">t1.w </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> Тут получаем аттенюацию по расстоянию, используя наш кружочек. t0.w – аттенюация по xy координатам, t1.w по z.</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mul </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.xyz, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.w </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> Применили вычисленную аттенюацию к цвету.</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> mul </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.w, </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">.w, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.w</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mul_x2 </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.xyz, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">, t3 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> добавили diffuse texture</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> approximately </span><span class="token number" style="color:#36acaa">9</span><span class="token plain"> instruction slots used (</span><span class="token number" style="color:#36acaa">4</span><span class="token plain"> texture, </span><span class="token number" style="color:#36acaa">5</span><span class="token plain"> arithmetic)</span><br></div></code></pre></div></div></div></div></details>
<p>Основные стетйты тендера установлены в:
Depth write = false
Srcblend = one
Dstblend = one</p>
<p>Т.е. получаем блендинг pixel color + incoming color</p>
<ul>
<li class="">
<p>Третий проход на статику (только если включены тени). Рисуем геометрию, проективно накладывая текстуры с тенюшками от монстриков. Ничего особо интересного, разве что, тени еще больше разблуриваются (2 лукапа на текстуру).</p>
</li>
<li class="">
<p>Партиклы. Используются динамические VB/IB. FFP.</p>
</li>
<li class="">
<p>Оружие. Sm30. Normal mapping от нескольких (я видел 2) directional источников освещения. Тут все стандартно, за исключением того, что normal map’ы у них не в tangent, a в model space. Просто источники они тоже поворачивают в model space. Анимации оружия скинтся хардварно (на вертекс влияет 2 кости)
Normal map оружия (оригинальный размер 1024х1024):</p>
</li>
</ul>
<p><img decoding="async" loading="lazy" alt="Weapon Normal Map" src="https://romanmarchenko.me/assets/images/weapon_normal_map-e44737d2e0e87d47e7cc81016b404aec.jpg" width="512" height="512" class="img_ev3q"></p>
<p>Specular mask хранится в альфа канале normal map’ы (оригинальный размер 1024х1024):</p>
<p><img decoding="async" loading="lazy" alt="Weapon Specular Mask" src="https://romanmarchenko.me/assets/images/weapon_specular_mask-9db8db44c5bd5a1fbb56bb10055eb658.jpg" width="512" height="512" class="img_ev3q"></p>
<p>Диффузная карта (оригинальный размер 1024х1024):</p>
<p><img decoding="async" loading="lazy" alt="Weapon Diffuse Map" src="https://romanmarchenko.me/assets/images/weapon_diffuse_map-c17444caa580125d453dc375e95d74e5.jpg" width="512" height="512" class="img_ev3q"></p>
<details class="details_lb9f alert alert--info details_b_Ee" data-collapsed="true"><summary>Шейдер для оружия</summary><div><div class="collapsibleContent_i85q"><div class="language-nasm codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-nasm codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> Registers:</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> Name Reg Size</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">-</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> GClipMat c0 </span><span class="token number" style="color:#36acaa">4</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> GFogParams c9 </span><span class="token number" style="color:#36acaa">1</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> GLightDir0 c13 </span><span class="token number" style="color:#36acaa">1</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> GHalfDir0 c14 </span><span class="token number" style="color:#36acaa">1</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> GLightDir1 c16 </span><span class="token number" style="color:#36acaa">1</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> GHalfDir1 c17 </span><span class="token number" style="color:#36acaa">1</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> GSkinBones c27 </span><span class="token number" style="color:#36acaa">69</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">vs_3_0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">def c4, </span><span class="token number" style="color:#36acaa">765.005859</span><span class="token plain">, </span><span class="token number" style="color:#36acaa">1</span><span class="token plain">, </span><span class="token number" style="color:#36acaa">0</span><span class="token plain">, </span><span class="token number" style="color:#36acaa">0</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_position v0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_texcoord v1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_blendweight v2</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_blendindices v3</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_position o0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_fog o1.x</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_texcoord o2.xy</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_texcoord1 o3.xy</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_texcoord2 o4.xyz</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_texcoord3 o5.xyz</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_texcoord4 o6.xyz</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_texcoord5 o7.xyz</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mul </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.xy, c4.x, v3.zyzw</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mova a0.xy, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp4 </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">.x, v0, c27</span><span class="token operator" style="color:#393A34">[</span><span class="token plain">a0.x</span><span class="token operator" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> Скининг</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp4 </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">.y, v0, c28</span><span class="token operator" style="color:#393A34">[</span><span class="token plain">a0.x</span><span class="token operator" style="color:#393A34">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp4 </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">.z, v0, c29</span><span class="token operator" style="color:#393A34">[</span><span class="token plain">a0.x</span><span class="token operator" style="color:#393A34">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp4 </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain">.x, v0, c27</span><span class="token operator" style="color:#393A34">[</span><span class="token plain">a0.y</span><span class="token operator" style="color:#393A34">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp4 </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain">.y, v0, c28</span><span class="token operator" style="color:#393A34">[</span><span class="token plain">a0.y</span><span class="token operator" style="color:#393A34">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp4 </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain">.z, v0, c29</span><span class="token operator" style="color:#393A34">[</span><span class="token plain">a0.y</span><span class="token operator" style="color:#393A34">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">lrp </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.xyz, v2.x, </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">2</span><span class="token plain"> кости, блендим по весам.</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mov </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.w, c4.y</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp4 o0.x, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">, c0 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> x </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> wvp</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp4 o0.y, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">, c1 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> y </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> wvp</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mov </span><span class="token register variable" style="color:#36acaa">r4</span><span class="token plain">.xyz, c28</span><span class="token operator" style="color:#393A34">[</span><span class="token plain">a0.x</span><span class="token operator" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> Тут немножко хитро. Так как используется не tangent space normal map, а model space, то параметры источников света (dir, half) перед установкой в константы переводится в это пространство. Затем поворачиваем вектора в соответствии с матрицами костей.</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mul </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain">.xyz, </span><span class="token register variable" style="color:#36acaa">r4</span><span class="token plain">, c13.y </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> y в animated model space</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mul </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">.xyz, </span><span class="token register variable" style="color:#36acaa">r4</span><span class="token plain">, c14.y</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mov </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain">.xyz, c27</span><span class="token operator" style="color:#393A34">[</span><span class="token plain">a0.x</span><span class="token operator" style="color:#393A34">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad </span><span class="token register variable" style="color:#36acaa">r5</span><span class="token plain">.xyz, </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain">, c13.x, </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> x</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain">.xyz, </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain">, c14.x, </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mov </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">.xyz, c29</span><span class="token operator" style="color:#393A34">[</span><span class="token plain">a0.x</span><span class="token operator" style="color:#393A34">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad o4.xyz, </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">, c13.z, </span><span class="token register variable" style="color:#36acaa">r5</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> o4 </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> light vector </span><span class="token number" style="color:#36acaa">0</span><span class="token plain"> в animated model space</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad o5.xyz, </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">, c14.z, </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> o5 – half vector </span><span class="token number" style="color:#36acaa">0</span><span class="token plain"> в animated model space</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mul </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain">.xyz, </span><span class="token register variable" style="color:#36acaa">r4</span><span class="token plain">, c16.y</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mul </span><span class="token register variable" style="color:#36acaa">r4</span><span class="token plain">.xyz, </span><span class="token register variable" style="color:#36acaa">r4</span><span class="token plain">, c17.y</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain">.xyz, </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain">, c16.x, </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain">.xyz, </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain">, c17.x, </span><span class="token register variable" style="color:#36acaa">r4</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad o6.xyz, </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">, c16.z, </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> o6 – light vector </span><span class="token number" style="color:#36acaa">1</span><span class="token plain"> в animated model space</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad o7.xyz, </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">, c17.z, </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> o7 – half vector </span><span class="token number" style="color:#36acaa">1</span><span class="token plain"> в animated model space</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp4 </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">.z, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">, c2 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> z </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> wvp</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp4 </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">.w, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">, c3 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> w </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> wvp</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad o1.x, </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">.z, c9.y, c9.x </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> fog</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mov o0.zw, </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mov o2.xy, v1 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> tc0 pass through</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mov o3.xy, v1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> approximately </span><span class="token number" style="color:#36acaa">33</span><span class="token plain"> instruction slots used</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> Name Reg Size</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">-</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> GLightEnable0 b0 </span><span class="token number" style="color:#36acaa">1</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> GLightEnable1 b1 </span><span class="token number" style="color:#36acaa">1</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> GAmbientColor c0 </span><span class="token number" style="color:#36acaa">1</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> GLightColor0 c1 </span><span class="token number" style="color:#36acaa">1</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> GLightColor1 c2 </span><span class="token number" style="color:#36acaa">1</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> ColorSampler s0 </span><span class="token number" style="color:#36acaa">1</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> NormalSampler s1 </span><span class="token number" style="color:#36acaa">1</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">ps_3_0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">def c3, </span><span class="token number" style="color:#36acaa">2</span><span class="token plain">, </span><span class="token operator" style="color:#393A34">-</span><span class="token number" style="color:#36acaa">1</span><span class="token plain">, </span><span class="token number" style="color:#36acaa">10</span><span class="token plain">, </span><span class="token number" style="color:#36acaa">0</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_texcoord v2.xy</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_texcoord1 v3.xy</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_texcoord2 v4.xyz</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_texcoord3 v5.xyz</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_texcoord4 v6.xyz</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_texcoord5 v7.xyz</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_2d s0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_2d s1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">texld_pp </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">, v2, s0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">texld </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">, v3, s1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">if b0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad_pp </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain">.xyz, c3.x, </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">, c3.y </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> распаковали нормаль из NM</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">nrm_pp </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain">.xyz, v4 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> нормализировали light vector </span><span class="token number" style="color:#36acaa">0</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">nrm_pp </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">.xyz, v5 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> нормализировали half vector </span><span class="token number" style="color:#36acaa">0</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp3_sat_pp </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain">.w, </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> n dot l</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp3_sat_pp </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain">.z, </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> n dot h</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">pow_pp </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">.z, </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain">.z, c3.z </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> посчитали спекуляр</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mul_pp </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain">.xyz, </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain">.w, c1 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> учли цвет источника</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mul_pp </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">.xyz, </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">.z, </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> учли спекуляр</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">if b1 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> для второго тоже самое</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">nrm_pp </span><span class="token register variable" style="color:#36acaa">r4</span><span class="token plain">.xyz, v7</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">nrm_pp </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain">.xyz, v6</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp3_sat_pp </span><span class="token register variable" style="color:#36acaa">r4</span><span class="token plain">.w, </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r4</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dp3_sat_pp </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain">.z, </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">pow_pp </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain">.w, </span><span class="token register variable" style="color:#36acaa">r4</span><span class="token plain">.w, c3.z</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mul_pp </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain">.xyz, </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain">.z, c2</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad_pp </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">.xyz, </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain">.w, </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad_pp </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain">.xyz, </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain">.w, c1, </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">endif</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">add </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain">.xyz, </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain">, c0 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> добавили амбиент</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mul </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.xyz, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> добавили диффузную текстуру</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad_pp oC0.xyz, </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">.w, </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> в альфа канале normal map’ы</span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> specular mask</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mov_pp oC0.w, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.w</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">else</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mul_pp oC0.xyz, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">, c0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mov_pp oC0.w, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.w</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">endif</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> approximately </span><span class="token number" style="color:#36acaa">45</span><span class="token plain"> instruction slots used (</span><span class="token number" style="color:#36acaa">2</span><span class="token plain"> texture, </span><span class="token number" style="color:#36acaa">43</span><span class="token plain"> arithmetic)</span><br></div></code></pre></div></div></div></div></details>
<ul>
<li class="">
<p>Downsample картики, с небольшим color remap’ом, для выделения ярких областей.</p>
</li>
<li class="">
<p>Separable Gaussian blur. Горизонтальный, вертикальный, 13х13. Получили bloom текстуру.</p>
</li>
</ul>
<details class="details_lb9f alert alert--info details_b_Ee" data-collapsed="true"><summary>Шейдер horizontal pass’a</summary><div><div class="collapsibleContent_i85q"><div class="language-nasm codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-nasm codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">vs_1_1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_position v0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_texcoord v1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">add oT1.xy, v1, c20 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> Посмещали в стороны текстурные координаты</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">add oT2.xy, v1, c21</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">add oT3.xy, v1, c22</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">add oT4.xy, v1, </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">c20</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">add oT5.xy, v1, </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">c21</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">add oT6.xy, v1, </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">c22</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mov oPos, v0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mov oT0.xy, v1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">ps_2_0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl t0.xy</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl t1.xy</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl t2.xy</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl t3.xy</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl t4.xy</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl t5.xy</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl t6.xy</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_2d s0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">texld_pp </span><span class="token register variable" style="color:#36acaa">r6</span><span class="token plain">, t1, s0 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">7</span><span class="token plain"> семплов используя TC посчитанные в VS</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">texld_pp </span><span class="token register variable" style="color:#36acaa">r5</span><span class="token plain">, t0, s0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">texld_pp </span><span class="token register variable" style="color:#36acaa">r4</span><span class="token plain">, t2, s0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">texld_pp </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain">, t3, s0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">texld_pp </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain">, t4, s0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">texld_pp </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">, t5, s0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">texld_pp </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">, t6, s0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mul </span><span class="token register variable" style="color:#36acaa">r6</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r6</span><span class="token plain">, c1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad_pp </span><span class="token register variable" style="color:#36acaa">r5</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r5</span><span class="token plain">, c0, </span><span class="token register variable" style="color:#36acaa">r6</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> Аккумулируем с учетом весов.</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad_pp </span><span class="token register variable" style="color:#36acaa">r4</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r4</span><span class="token plain">, c2, </span><span class="token register variable" style="color:#36acaa">r5</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad_pp </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain">, c3, </span><span class="token register variable" style="color:#36acaa">r4</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad_pp </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain">, c1, </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad_pp </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">, c2, </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad_pp </span><span class="token register variable" style="color:#36acaa">r6</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">, c3, </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">add </span><span class="token register variable" style="color:#36acaa">r5</span><span class="token plain">.xy, t0, c7 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> Посчитали координаты еще </span><span class="token number" style="color:#36acaa">6</span><span class="token plain"> семплов.</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">add </span><span class="token register variable" style="color:#36acaa">r4</span><span class="token plain">.xy, t0, c8 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> Можно было заюзать все </span><span class="token number" style="color:#36acaa">4</span><span class="token plain"> компонента у oTx регистров в VS.</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">add </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain">.xy, t0, c9</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">add </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain">.xy, t0, </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">c7</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">add </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">.xy, t0, </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">c8</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">add </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">.xy, t0, </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">c9</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">texld_pp </span><span class="token register variable" style="color:#36acaa">r5</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r5</span><span class="token plain">, s0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">texld_pp </span><span class="token register variable" style="color:#36acaa">r4</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r4</span><span class="token plain">, s0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">texld_pp </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain">, s0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">texld_pp </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain">, s0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">texld_pp </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">, s0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">texld_pp </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">, s0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad_pp </span><span class="token register variable" style="color:#36acaa">r5</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r5</span><span class="token plain">, c4, </span><span class="token register variable" style="color:#36acaa">r6</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad_pp </span><span class="token register variable" style="color:#36acaa">r4</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r4</span><span class="token plain">, c5, </span><span class="token register variable" style="color:#36acaa">r5</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad_pp </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain">, c6, </span><span class="token register variable" style="color:#36acaa">r4</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad_pp </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain">, c4, </span><span class="token register variable" style="color:#36acaa">r3</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad_pp </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain">, c5, </span><span class="token register variable" style="color:#36acaa">r2</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad_pp </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">, c6, </span><span class="token register variable" style="color:#36acaa">r1</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mov_pp oC0, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> approximately </span><span class="token number" style="color:#36acaa">33</span><span class="token plain"> instruction slots used (</span><span class="token number" style="color:#36acaa">13</span><span class="token plain"> texture, </span><span class="token number" style="color:#36acaa">20</span><span class="token plain"> arithmetic)</span><br></div></code></pre></div></div></div></div></details>
<p>Блум текстура:</p>
<p><img decoding="async" loading="lazy" alt="Bloom" src="https://romanmarchenko.me/assets/images/pp_full_blur-a42f19136e3e80a07b10f48088f224ff.jpg" width="512" height="512" class="img_ev3q"></p>
<ul>
<li class="">
<p>Блендинг с блум тектурой. FFP. Блендим по формуле: blurred текстура + текстура сцены.
Полученный результат отлично выглядит на динамическом небе.</p>
</li>
<li class="">
<p>UI. Ингейм интерфейса в painkiller почти нет. Думаю именно по этому он сделан так ужасно J Рисуется поэлементно, группировок никаких нет. Зато в startup menu разработчики постарались. Особенно убило окно конфигурации кнопок. Несколько скринов из под NVPerfHUD’а:</p>
</li>
</ul>
<p><img decoding="async" loading="lazy" alt="UI 1" src="https://romanmarchenko.me/assets/images/ui_controls_1-45e1331fc6eea9c6c5d65d41ef1bfca1.jpg" width="512" height="384" class="img_ev3q">
<img decoding="async" loading="lazy" alt="UI 2" src="https://romanmarchenko.me/assets/images/ui_controls_2-cce8981b056eb1c3852c0f9ea7ba9322.jpg" width="512" height="384" class="img_ev3q">
<img decoding="async" loading="lazy" alt="UI 3" src="https://romanmarchenko.me/assets/images/ui_controls_3-55c53971e72bbebd72f3331d3d11b576.jpg" width="512" height="384" class="img_ev3q">
<img decoding="async" loading="lazy" alt="UI 4" src="https://romanmarchenko.me/assets/images/ui_controls_4-4c3c63cecf8a6b47c168b2eeb13f7a4c.jpg" width="512" height="384" class="img_ev3q"></p>
<p>3 раза перетиреть почти весь экран, причем такими маленькими квадратиками, это надо уметь :) 2000 дипов.</p>
<ul>
<li class="">Demon mode. Кто не играл, это такой режим, когда собираешь 66 душ, и превращаешься в бессмертного психа-убийцу. :)
Процесс рендеринга таков:
Рисуем сцену нормально в RTT. Затем из нее делаем ч.б. картинку (способ стандартный: dp3 pixel, float3(0.3, 0.59, 0.11). Потом рисуем монстров специальным шейдером, который используя нормали и градиентную текстуру выделяет границу объекта красным цветом. И, напоследок, используя dudv bump map, вносим в картинку искажения. Для получения motion blur эффекта блендим предыдущий и текущий кадр используя весовые коэффициенты.</li>
</ul>
<details class="details_lb9f alert alert--info details_b_Ee" data-collapsed="true"><summary>Demon mode shader</summary><div><div class="collapsibleContent_i85q"><div class="language-nasm codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-nasm codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">ps_1_1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">tex t0 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> Семпл из оригинальной текстуры</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">texbem t1, t0 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> Эффект мембраны, который возникает когда стреляешь в demon mode, реализуется твиками bumpenvmat текстурного стейджа.</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">tex t2 </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> Используем текстуру с предыдущего кадра, чтобы получить плавающий эффект.</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mul </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">, t1, c0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain">, t2, c1, </span><span class="token register variable" style="color:#36acaa">r0</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> Блендим текущий и предыдущий кадр по весам. Получаем motion blur like эффект.</span><br></div></code></pre></div></div></div></div></details>
<p>Градиентная текстура:</p>
<p><img decoding="async" loading="lazy" alt="Deamon Mode Gradient" src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAgADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD5/ooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigApRRRQNDhThRRUmsRwp4ooqWbxHinCiioZ0RHiniiioZ0QHiniiioZ0wJBTxRRUM6oEgp60UVmzqgPFSCiioZ1QHiniiioZ1QHiniiis2dMB4p4ooqWdERwpwooqGdERwp1FFSbIdT1ooqWWSLUq0UVnIykTrUyUUVjIwkTpViOiisJHNMsx9qspRRXNI45lhamWiiueRzSP/9k=" width="512" height="1" class="img_ev3q"></p>
<p>Получаем вот такую картинку:</p>
<p><img decoding="async" loading="lazy" alt="Deamon Mode PP" src="https://romanmarchenko.me/assets/images/daemon_mode_after_pp-6b200059d71aba6eb38941c4782120ea.jpg" width="512" height="512" class="img_ev3q"></p>
<p>Вот собственно и всё.</p>
<p>Теперь общие сведения по рендеру.</p>
<ol>
<li class="">Дипов мало. В аутдорах до 1000 (редко больше, но бывает). В индорах обычно меньше. Иногда сильно. При таком количестве проходов, это очень хороший показатель.</li>
<li class="">Сортировка по материалам и текстурам хорошая. D3DXEffect не используется.</li>
<li class="">Арт просто супер. Эффекты там где надо. Все хорошо настроены и сбалансированы. Дизайн уровней великолепен. Текстуры сделаны очень качественно. Полетайте в редакторе по C6L2_LoonyPark, например. Просто загляденье.</li>
<li class="">Загрузка видеопамяти 100 – 120 MB (у видеокарты на борту 128). AGP – 6 MB. Чувствуется умелая рука настройщика.</li>
<li class="">LOD’ов геометрии нет. Есть удаление glow партиклов по расстоянию.</li>
</ol>
<p>Мои мини выводы (большинство перекликается с выводами по FlatOut2):</p>
<ol>
<li class="">Первое, что надо продумать в рендере, это минимизация DP calls. PainKiller наглядно показал, что с малым количеством DP больше простора для применения интересных и разнообразных алгоритмов (которые могут потребовать несколько проходов рендера). Если для одного прохода требуется больше 1000 DP, то многопроходным алгоритмам, скорее всего, дорога закрыта.
. Не надо брезговать предрасчетом освещения. Не подходят лайтмапы? Рассмотрите использование ambient occlusion. Никакие реалтайм алгоритмы не дадут вам того-же качества при таком малом потреблении ресурсов.</li>
<li class="">Арт и хорошая дизайнерская работа решает с огромной силой. Эти компоненты не просто делают игру красивее, но и позволяют применить более простые алгоритмы визуализации без ущерба для картинки, что разгружает рендер. Получается, что хороший арт ускоряет игру! Во как! J</li>
<li class="">Чтобы был хороший дизайн, надо давать хорошие рычаги управления этим дизайном (это не из исследования рендера, это после ознакомления с редактором и структурирования своих мыслей, после этого исследования). Методы управления по типу: «сказать программисту, чтобы исправил параметры материала там и там», не работают. Знаю по собственному опыту. Признаю, важность этого момента я недооценивал.</li>
</ol>]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[Flatout2 render: как это работает?]]></title>
        <id>https://romanmarchenko.me/ru/blog/flatout2</id>
        <link href="https://romanmarchenko.me/ru/blog/flatout2"/>
        <updated>2006-08-15T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Решил пореверсить рендер FlatOut2. Причина банальна: не так часто сейчас выходят игры,]]></summary>
        <content type="html"><![CDATA[<p>Решил пореверсить рендер FlatOut2. Причина банальна: не так часто сейчас выходят игры,
которые отлично выглядят, и не тормозят при этом. Результаты ниже, может кому-нибудь
пригодятся эти сведения, для написания своего автосима :)</p>
<p>Сначала общие сведения.
В кадре 70 - 120 тыс. полигонов. Все рисуется исключительно шейдерами 1.1 (что, в общем
то, и не удивительно, учитывая то, что игра, также, вышла и на XBox). Дипов от 700 до 1100, что
на удивление много.</p>
<p>Теперь процесс рендера поэтапно.</p>
<ol>
<li class="">Тени для машин. Каждый автомобильчик имеет свою текстуру с тенью (256х256, R5G6B5).
Туда рисуются только кузов и колеса. Очень похоже, что это самый малоденальный LOD. Шейдер
элементарный. Получается такая картинка:</li>
</ol>
<p><img decoding="async" loading="lazy" alt="Car Shadow" src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAEAAQADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD3+iiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAoopquj7tjK207Tg5wfSgB1FFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFZF7rsUIKWuJZM/eI+Uf40Aa9VptRs4M+ZcICDggHJB+grkp7y4uTmaZnGc4zx+XSoaYHSS+IbZQwjjkdgcDOAD/X9Kz5tevJG/dlIlycALnI981l0UDJJLmeZQss0jrnOGcnmkjlkhbdFIyMRjKtimUUAbFt4gnQqtwiyL3YcN/hW5a31veLmGQFsZKnhh+H41xdOR3jcPGzKw6MpwaAO6orCstfyQl4AOP9Yo/mP8PyrcVgyhlIKkZBB4IpCFooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAqveXsNjEHmJ5OFUdTViuU1x2fVZFY5CBQvsMZ/mTQAy91W4vQUYhIifuL+mT3qjRRTGFFFFABRRRQAUUUUAFFFFABV2x1S4sThTvi/uMeOvb0qlRQB2lpeQXsZeF84xuU8FfrViuHhnlt5BJDIyMO4PX/Gup07VIr2MKxVJxwUz19xSEX6KKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigArkNWjeLU5t5Y7juBPcH/AA6fhXX1i+IrfdBFcKOUO1sL2PTJ+v8AOgDnaKKKYwooooAKKKKACiiigAooooAKKKKAClVijBlJDA5BBxikooAla5nkZWeaRmQ5Us5JU+1bWma2NqwXbHdnCynp/wAC/wAf/wBdYFFAHeUVy+l6u9q4inZngPAJ5KfT29v8np1YMoZSCpGQQeCKQhaKKKACiiigAooooAKKKKACiiigAooooAKgvbf7XZywZwWHB9xyP1qeigDhpYnhlaKVSrqcEHtTK1/ETH7dGvGBGD056n/CsimMKKKKACiiigAooooAKKKKACiiigAooooAKKKKACr2m6k9hLg5aFj8y/1HvVGigDuYpUniWSNgyMMgin1xtlfzWMoZGJTPzRk8H/6/vXWW1zFdwLLE2VP5g+hpCJqKKKACiiigAooooAKKKKACiiigAooooA57xHGomgkycspU+nB/+vWJXWa1AZ9NcjJaMhwAfTr+ma5OmMKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAqzY30tjP5kfKnhlPRhVaigDtra5iu4FlibKnqO4PoamrjLG+lsZ/Mj5U8Mh6MP8966+CZLiBJozlXGRSESUUUUAFFFFABRRRQAUUUUAFFFFABXK6rphs5i8SuYGGc9Qpz0JrqqKAOFRHlcIis7HoqjJrTi0C8eMsxSNs4Csc5Hrxn/P69KqIm7YqruO44GMn1p1AHE3NtLaTmGZcMOh7EeoqGu1urOC8jCTJnH3SDgj6Vyd9Yy2M/lycqeVcDhh/ntTGVqKKKACiiigAooooAKKKKACiiigAooooAKKKKACtDStRNlcBXY/Z3PzDGcH1FZ9FAHdqwZQykFSMgg8EUtcvpWqtZsIZiWtyfqU9x7e3+T06sGUMpBUjIIPBFIQtFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFMliSaJo5FDIwwQafRQBymo6TLZZkU74M4Dd19M1nV3bKGUqwBUjBB6Guc1TRvs6Ge23NGOXQ8lfce1MZj0UUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFamlaqbNhDMSbcn/vg+o9vb/Jy6KAO8orF0LUPMQWcn3kHyMT1Hp/nt9K2qQgooooAKKKKACiiigAooooAKKKKACiiigAooooAxtV0czsZ7VR5hPzp03e49/8/XnWUoxVgVYHBB4wa7us3UtJS+PmIRHMB1xw3pn/ABpgcrRT5YpIJWjkUq6nBBplAwooooAKKKKACiiigAooooAKKKKACiiigB0btHIrocMpBB9DXZWVx9rs4p8YLDke44P61xdb/h+8dt1owBVVLqfTnkfrQBu0UUUhBRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQBVvbCG+iKuoD4+WQDkf/W9q5CWMwzPExG5GKkjpkGu5ribtFjvJ40GFWRgB7A00MhooooAKKKKACiiigAooooAKKKKACiiigArW8Pf8hCT/AK5H+YqOy0a4u1WRiIoiMhm5JHsP89a6KzsobGIpCDycszdTQBYooopCCiiigAooooAKKKKACiiigAooooAKKKKACiiigArkdXgNvqcvXa53gk9c9f1zXXVka9Z+dai4RcvF1wOq/wD1v8aAOaooopjCipYLea5lEcKF3xnHtV260S7twWVRMmf4Ov5f4ZoAzaKVlZGKsCGBwQeopKACilVWZgqglicADkmrsWj30oUiAqrHq5Ax9R1oAo1JDBLcSBIUZ2PYCuittAto0UzlpXx8w3YXPt3rUSNI0CRoqKOgUYAoA5+08PyPhrp/LH9xeW/Pp/Otq1sbezXEMYDYwXPLH8asUUhBRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQBhXfh8vK8lvKgDNnYwwF+hH+FLbeHVUhrmXd6onA6+tblFAEcMEVvGI4kVFHYD/OakoooAr3FlbXePPiVyOh6H8xS/YbT/n1g/79ip6KAI44IYc+VEiZ67VAzUlFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFAH/2Q==" width="256" height="256" class="img_ev3q"></p>
<ol start="2">
<li class="">
<p>Небо. Сделано просто и оригинально: само небо - полукуб. Легко представить себе, как кубик
разрезанный горизонтально пополам. Затем на уровне горизонта кольцо, с панорамной текстурой
гор, домов, леса и т.д. Удобненько, потому что, тип неба (вечернее, дневное) можно менять
отдельно. Хотя, можно было все тоже сделать одним кубиком и одной кубемапой. Вышло
бы меньше draw calls.</p>
</li>
<li class="">
<p>Земля. Вот тут просто супер. Для каждой трассы есть одна (2048х2048) текстура земли, в которой
рисунок уровня с видом сверху. Все статические тени от холмов, мостов, и других объектов уже на
ней. При рендеринге используется эта текстура, и различные текстуры деталей (листики там, камешки).
При их сложении получается очень симпатично. Модель освещения тоже очень простая и очень
эффективная. Для всех статических объектов (не только земли) освещение предрасчитано, и хранится
как диффузный цвет в вертексах меша. В реалтайме ничего не считается. В результате получаем
очень короткие шейдеры, которые очень быстро исполняются. На этом этапе есть хорошая сортировка
как по материалам, так и по текстурам.</p>
</li>
</ol>
<p>Пример текстуры всей локации:</p>
<p><img decoding="async" loading="lazy" alt="Map" src="https://romanmarchenko.me/assets/images/level_tex-ab2325844537569dc1d67ff3e4bfe8bc.jpg" width="1024" height="1024" class="img_ev3q"></p>
<p>Пример текстуры деталей:</p>
<p><img decoding="async" loading="lazy" alt="Map details" src="https://romanmarchenko.me/assets/images/level_details-be2f2b2885a649823e860897a3b4ea98.jpg" width="512" height="512" class="img_ev3q"></p>
<ol start="4">
<li class="">
<p>Кактусы, деревья. Есть статик батчингом. Я так и не понял, почему он то используется, то нет.
Есть подозрение, что нефизические объекты за пределами трека рисуются кучами, а все остальные нет.
На счет сортировки по материалам, то тоже не понятно. Хоть все рисуется одними и теми же шейдерами,
но все равно каждые несколько draw calls обрамлены ID3DXEffect::Begin, BeginPass, EndPass, End.
Освещение предрасчитано и тут. Шейдер такой же как и для земли. ps: 1+1 инструкций, vs: 6.</p>
</li>
<li class="">
<p>Машинки, часть 1. Без колес и стекол. Используются самые сложные шейдеры в игре.
Есть диффузное освещение с использованием кубемапы (A8, вверху светло, книзу темнеет), спекуляр
от солнца. В ps, также, передаётся степень повреждения рисуемой детали, на основе которой
выбирается текстура новой, либо повреждённой машины. В алфа канале текстуры авто содержится
отражающая степень поверхности. Для отражений используют левую кубемапу. Она одинаковая
для всех трасс. На ней деревца и горы. Никакой динамики.</p>
</li>
</ol>
<p>Новая машинка:</p>
<p><img decoding="async" loading="lazy" alt="Car new" src="https://romanmarchenko.me/assets/images/car_new-cbb08762dbc1827ae9796f91df6c673a.jpg" width="512" height="512" class="img_ev3q"></p>
<p>Поврежденная машинка:</p>
<p><img decoding="async" loading="lazy" alt="Car damaged" src="https://romanmarchenko.me/assets/images/car_damaged-fd8bf5186491d6e8424b61237a565c00.jpg" width="512" height="512" class="img_ev3q"></p>
<p>before_p6. Физические объекты. Используют диффузную кубемапу. Опять странная сортировка по материалам.
Частичная какая-то.
7. Машинки, часть 2. Элементы двигателя, интеръер, колесные диски. Шейдер как для части 1.
8. Водитель. Хардварный скининг.
9. Тени под машинами. Строится меш, который стелится по земле.
Сначала рисуется простое тёмное пятно. Затем используется динамическая текстура тени сделанная в п.1.
Интересно то как они заблуривают тень. Шейдеры с комментариями:</p>
<div class="language-asm codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-asm codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">vs_1_1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_position v0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_color v2</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dcl_texcoord v3</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">m4x4 oPos, v0, c0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">add oT0.xy, v3, c32 // c32 0.000 -0.004 0.000 0.000</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">add oT1.xy, v3, c33 // c33 -0.004 0.000 0.000 0.000</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">add oT2.xy, v3, c34 // c34 0.004 0.000 0.000 0.000</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">add oT3.xy, v3, c35 // c35 0.000 0.004 0.000 0.000</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">ps_1_1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">tex t0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">tex t1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">tex t2</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">tex t3</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mul r0, c7, t0 // В c7, как можно догадаться, 1/4. В каждом текстурном стейдже одна</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad r0, c7, t1, r0 // и та же текстура с тенью от машины</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad r0, c7, t2, r0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">mad_sat r0, c7, t3, r0</span><br></div></code></pre></div></div>
<p>Т.е. блурят семпля 4 раза одну и ту же текстуру, немного смещая её в стороны. Учитывая их малый размер (256), и то что используется хардварная фильтрация текстур, и , результат выходит просто отличный.</p>
<ol start="10">
<li class="">Дальние + ближние кустики. То большие батчи (100-200 штук за раз), то по-одному. Простой шейдер.</li>
<li class="">Машинки, часть 3. Шины, стекла, фары. Для последних двух, простые отражения, используя
ту же кубемапу, что и для кузова автомобиля.</li>
<li class="">Системы частиц. Дым, пыль, искры. Простейшие шейдеры.</li>
</ol>
<p>Теперь всё нарисовано. Получилось вот так:</p>
<p><img decoding="async" loading="lazy" alt="Before Post-Process" src="https://romanmarchenko.me/assets/images/before_pp-05603699b797046751a5a9f247498c5d.jpg" width="1024" height="768" class="img_ev3q"></p>
<p>Далее пост-процесс.</p>
<ol start="13">
<li class="">Считаем яркость картинки. Проще говоря, из цветной делаем черно-белую.</li>
<li class="">Используя яркость пикселя (из ч.б. изображения) и специальной градиентной 1D текстуры
делается color remap. Яркие пиксели получают один оттенок, темные другой. Вечером, например,
освещённые участки немного красноватые. После ремапа, полученная картинка комбинируется с цветной,
и получается вот такое:</li>
</ol>
<p><img decoding="async" loading="lazy" alt="Color remap" src="https://romanmarchenko.me/assets/images/color_remap-2087d1cd2807e1048f033db8056b1dce.jpg" width="1024" height="768" class="img_ev3q"></p>
<ol start="15">
<li class="">Дайнсемплим изображение из п.14 в 256x256. Затем выделяем яркие места простым шейдером.</li>
<li class="">Блурим пинг-понгом 4 раза, методом, аналогичным с
методом блура тенюшек от машин. 4 tap, используются смещения в текстурных координатах.
Получаем вот такую картинку:</li>
</ol>
<p><img decoding="async" loading="lazy" alt="Blur grayscale" src="data:image/jpeg;base64,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" width="256" height="256" class="img_ev3q"></p>
<ol start="17">
<li class="">Объеденяем заблюреную текстуру, с основной картинкой. Получили glow еффект:</li>
</ol>
<p><img decoding="async" loading="lazy" alt="Blur grayscale" src="https://romanmarchenko.me/assets/images/glow-b14ce59264855dae8cf1852cfc7b3cae.jpg" width="1024" height="768" class="img_ev3q"></p>
<ol start="18">
<li class="">Делаем 2 radial blur тем-же методом, только теперь текстурные координаты не спещаются,
а с каждым разом увеличиваются. Получили изображение, которое используется при нитро-ускорении:</li>
</ol>
<p><img decoding="async" loading="lazy" alt="Blur grayscale" src="data:image/jpeg;base64,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" width="256" height="256" class="img_ev3q"></p>
<ol start="19">
<li class="">UI. Тут все стандартно. Рисуем поэлементно. Сортировок никаких нет.</li>
</ol>
<p>Это всё. Выводы, которые я сделал из этого маленького исследования:</p>
<ul>
<li class="">Арт решает (я это и раньше знал, но убеждаюсь все больше). Во Flatout2 нет ни одной
cutting-edge графической технологии. Новомодных технологий можно наклепать три корзины.
Но если у вас нет хорошего арта, и грамотной (!) настройки этих всех эффектов - у вас все будет
тормозить и при этом выглядеть отстойно.</li>
<li class="">Еще раз отметил для себя хороший принцип, который должен знать каждый разработчик игр:
не надо считать каждый раз то, что можно посчитать один раз.</li>
<li class="">Сортировки по материалам и текстурам в движке скорее нет, чем есть.</li>
<li class="">Был немножко удивлен простоте, но не в ущерб качеству (хотя dip'ов можно было сделать и
поменьше). Рендер прост и быстр.</li>
</ul>]]></content>
    </entry>
</feed>