MaterialX — что, куда и зачем

от автора

В этой небольшой заметке хотелось бы рассказать о технологии MaterialX. Что это такое, из чего состоит, как этим пользоваться, куда можно прикладывать применять. Да и вообще, как с этим жить. Сразу стандартные ссылки: официальный сайт, репозиторий на GitHub. И тоже сразу предупреждаю — никакого ИИ, генеративных моделей, агентов и прочей шалупени. Ну если только немного в целях насмешки. А так — только старая добрая компьютерная графика. И никакого телеграмм-канала тоже. Говорю же, олдскул.

Что это такое

Если бы можно было так просто двумя словами объяснить. Попробуем, но только используя три слова.

Во-первых, это стандарт. Стандарт для описания шэйдеров и материалов, заданных нодовыми графами. То есть цепочками элементарных нод, в которых входные порты соединяются с выходными, и всё это описывает как должны преобразовываться потоки данных при вычислении шэйдеров. Сейчас в компьютерной графике уже устоялось мнение, что подобные графы — это самый удобный способ создания материалов. Например, в Blender давным давно все материалы такие, в Unreal Engine тоже, даже в Unity (чего греха таить) тоже стараются не отставать и придумывают всё время Shader Graph.

Ну так вот, MaterialX сохраняет это описание в xml-файл. В принципе можно и самому сохранять xml-файлы с любым содержимым. Самому выбирать как задавать материал и что сохранять. Каждый из нас может придумать свой стандарт. Может быть кто-то придумает писать все ключевые слова не только на дореволюционном русском (естественно, с атями и ятями), но и задом-наперёд (тевирп!). Но такой стандарт никому не нужен. Даже для смеха. В спецификации MaterialX фиксирован формат сохранения, какие есть ключевые слова, как описываются взаимодействия между нодами, какие есть типы данных и так далее. Откомпилированная библиотека предоставляет API для создания документов, заполнения их данными, сохранения в xml-файл и чтение из уже готового xml-файла. Можно на c++, можно на Python.

Во-вторых, MaterialX содержит стандартную библиотеку с описанием исчерпывающего набора элементарных нод. Есть ноды, которые описывают математику преобразований, или же доступ к текстурам или атрибутам геометрии, а также есть ноды, описывающие элементарные модели освещения поверхностей (ну вроде модели Орена — Наяра диффузного освещения, и многие прочие подобные). Предполагается, что из таких элементарных моделей с использованием элементарной-же математики пользователи смогут собирать нужные шэйдеры.

В-третьих, и без этого все выше перечисленное было бы никому не нужно. Это то, про что иностранцы говорят «last but not least», или еще «most exciting», или вообще молчат-помалкивают, если английский не знают. Короче — это система генерации шэйдеров на других языках программирования шэйдеров из вот этих вот нодовых графов. Поддерживаются GLSL, MDL, MSL и OSL. Господи, кто все эти люди?! Это языки программирования шэйдеров. Говорю же. Уже упомянутая выше откомпилированная библиотека предоставляет API для такой генерации. Подаешь на вход xml-документ с описанием нодового графа, жмёшь кнопку — получаешь результат сгенерированный код шэйдера на нужном языке. Здорово, если не сказать больше.

Да, при этом есть проблемы. Например, кодогенерация доступна только для шэйдеров поверхностей (и не работает для волюметриков, то есть типа объемных субстанций вроде дыма, тумана и прочего такого). Или же какой-нибудь крохобор объявит сгенерированный код не оптимальным. Но по крайней мере это можно делать. И вот, к примеру, в системах рендера на основе трассировки лучей оптимизация шэйдеров не так критична. По сравнению с приложениями реального времени (сиречь играми). Время выполнения шэйдера для каждого сэмпла — это семечки по сравнению с непосредственно трассировкой. Так что то, что там osl-код получается громоздким, почти никогда не заметно. Это я тут так неявно делаю вид, что все понимают, дескать, osl-шэйдеры предназначены в основном для трассировщиков. Ну это же все знают, как иначе может быть?

Лирическое отступление

Всё, хватит болтовни. Скучно. Вот эти бесконечные портянки текста (пять абзацев — уже портянка) в нашу светлую эпоху генеративного ИИ опостылели. Ну сколько можно? Всё слова, да слова. Где дела-то?

Сейчас всё будет. Только сначала небольшое лирическое отступление. Не могу удержаться. Все примеры буду показывать, используя мою собственную интеграцию MaterialX в Softimage. Для тех, кто не знает, Softimage — это пакет трёхмерной графики (ныне по аглицки это называется DCC — Digital Content Creation). Ну как пакет… Я был там, я был там 12 лет назад. В те времена была тройка топ-программ: 3DMax, Maya и Softimage (все принадлежали Autodesk). Были и нишевые, вроде Houdini. И Blender был, но в категории 3d-андеграунда. И вот где-то в 2014 году Autodesk вероломно закрыл Softimage. Вот буквально вчера были какие-то анонсы, были разработки, были обновления, были интеграции современных технологий. Да это и были современные технологии. И раз — объявили, что ничего этого больше не будет, команда разработчиков распределяется по другим подразделениям, в том числе переключается на разработку Maya. Народ стоит в немом удивлении. Ведь столько топовых технологий компьютерной графики были разработаны под Softimage. Это и рендер Arnold, и Fabric Engine (если кто помнит, прорывная была технология, ныне закрыта и забыта). А мелкий бизнес, который жил за счет разработки дополнений для Softimage. Всё-ж пропало. Теперь этот бизнес даром никому не сдался. А ведь было ещё и комьюнити. Русское, между прочим, довольно многочисленное и компетентное.

Короче — оптимизировали, так оптимизировали. Ну народ с тех пор разошелся кто-куда. Кто на тот же Houdini, кто на поднимающий голову Blender. Но только не ваш покорный слуга. Очень уж мне нравился, да и до сих пор нравится, Softimage. Но на текущее время программа, конечно, подустарела. Поэтому я в меру своих скромных сил, делаю интеграцию некоторых современных технологий. Благо система плагинов в Softimage более или менее ничего, позволяет. Из наиболее существенных дополнений — это интеграция рендера Cycles (теперь хоть рендерить можно бесплатно, а то ведь остальные трассировщики, что ещё есть — платные), поддержка формата GLTF (а то проблемы были с тем, чтобы передать модель в тот же Blender), и теперь вот — интеграция MaterialX. Поддерживает экспорт материалов в нативный mtlx-формат, генерацию шэйдеров, а также есть окно для просмотра этих материалов. Окно на основе OpenGL. Использует как раз генерацию glsl-шэйдеров.

Структура материалов

Существует всего три ноды, которые могут быть корневыми нодами каждого материала: Surfacematerial, Volumematerial и Lama Surface. Исходя из названий понятно, что Surfacematerial — это для шэйдеров поверхностей (может отображаться в просмотрщике), Volumematerial — для шэйдеров волюметриков (объёмных сред), для них кодогенерация не работает, Lama Surface — тоже для поверхностей, но отдельного семейства шэйдеров Lama.

Формат MTLX для хранения материалов MaterialX можно использовать и как просто формат для переноса данных. В каком-нибудь движке или приложении написать свою интерпретацию и обработку этих нодовых графов. Поэтому не стоит уж совсем списывать со счетов шэйдеры для волюметриков. Да, внутри MaterialX с ними ничего особо не сделать, кроме как сохранить в файл или загрузить из файла, но всё равно, можно конструировать шэйдер. К Volumematerial можно подсоединять ноды Mix Volumeshader, Dot Volumeshader и Volume.

Первые две ноды — понятно, для комбинирования шэйдеров. Последняя (Volume) — для его задания через указание конкретных VDF и EDF. VDF расшифровывается как Volume Distibution Function, описывает поведение внутри объёмной среды, EDF — Emission Distribution Function, описывает свечение. Этот самый VDF формируется шестью нодами: Add VDF, Mix VDF, Multiply VDF to Color, Multiply VDF to Float, Anisotropic VDF и Absorption VDF.

Собственно две последних (Anisotropic VDF и Absorption VDF) генерируют плотность и модификацию цвета объёма, остальные нужны для их комбинирования.

Тут уместно сделать замечание о названиях нод. Дело в том, что Softimage щепетильный в вопросах типов данных, каждый входной и выходной порт должны иметь какой-то фиксированный и неизменный тип данных. MaterialX в этом вопросе имеет более гибкую мораль. Чтобы смешать два Volumshader-а, надо использовать ноду Mix, а чтобы смешать два VDF-а — тоже Mix. И в зависимости от типов их входных портов MaterialX выберет нужную ноду. Типа полиморфизм такой.

Продолжаем. Ещё в материалах MaterialX можно сохранять нодовые графы для источников света. Для этого в качестве корневых нод используются Point Light, Spot Light, Directional Light и Light.

Видно, что первые три (Point Light, Spot Light, Directional Light) описывают просто обычный источник света, а четвёртая (Light) задаёт источник света с помощью EDF.

Ну и раз уж пошло такое дело, то EDF формируется восемью нодами: Uniform EDF, Conical EDF, Measured EDF, Generalized Schlick EDF, Mix EDF, Add EDF, Multiply EDF to Color и Multiply EDF to Float.

То есть можно задавать EDF просто цветом, выбирая файл картинки, или же их комбинируя.

Теперь к поверхностям. К порту Surfaceshader корневой ноды Surfacematerial можно подсоединять одну из шести нод: Standard Surface, Open PBR Surface, GLTF PBR, USD Preview Surface, Disney Principled и Surface.

Первый пять из них (большие ноды на картинке) — это так называемые убер-шэйдеры. Они описывают комплексный шэйдер с большим числом слоёв. Каждый из них можно использовать как универсальный шэйдер для самых разных сценариев. Потому и «убер». Шестая нода (Surface) — самая маленькая и сиротливая по сравнению с этими лбами здоровенными. Она тут основная. Позволяет конструировать поверхностный шэйдер через указание BSDF (Bidirectional Surface Distribution Function), EDF и Opacity. Ну, EDF мы уже знаем, Opacity — это прозрачность, а BSDF — это основная функция для описания свойств поверхностей.

Теперь BSDF-ноды.Это как раз те самые элементарные ноды для описания одного конкретного свойства поверхности: Oren Nayar Diffuse BSDF, Burley Diffuse BSDF, Translucent BSDF, Dielectric BSDF, Conductor BSDF, Generalized Schlick BSDF, Subsurface BSDF, Sheen BSDF и Chiang Hair BSDF.

Что тут надо понимать. Для диффузной компоненты материала можно выбирать либо Oren Nayar, либо Burley. Для стекла и отражений — Dielectric. Для металлов (и зеркальных отражений) — Conductor. Ещё шесть нод для комбинирования свойств поверхностей.

Ещё Surfacematerial принимает на вход Displacementshader. Он, этот самый дисплэйс, формируется двумя нодами: Displacement Float и Displacement Vector3.

При кодогенерации эти ноды игнорируются. Ну по крайней мере при генерации glsl-шэйдеров. По идее они должны как-то реализовываться в вершинном шэйдере. Но вот поди ж ты, не реализуются. Может доработают ещё.

Ну и последнее — это ноды семейства Lama. По идее их можно цеплять к любому BSDF-порту. Тем более, что под капотом они используют все те же элементарные BSDF-ноды. Только с дополнительными преобразованиями входных параметров. Итого восемь BSDF-нод, три для их комбинирования, и ещё три — для задания EDF источников света.

В стандартной библиотеке есть ещё много других нод общего назначения. Для преобразований данных разного типа, их конвертации, вычислений, генерации процедурных текстур и-прочее-и-прочее. Нет смысла описывать, что каждая из них делает. Это надо непосредственно пробовать и смотреть на результат.

Сборка шэйдера

В этом разделе на конкретном примере разберём, как комбинировать ранее описанные ноды, чтобы собрать нужный нам шэйдер. «Так а какой нужен?» — спрашивает нетерпеливый читатель. Сейчас объясню какой. Готовые убер-шэйдеры слишком громоздкие. Да, в них много возможностей, но редко они нужны прямо все. А от параметров в глазах рябит. Поэтому соорудим PBR-шэйдер, слегка похожий на стандартный шэйдер в Unity. Он будет поддерживать диффузный цвет, глянцевитость, отражаемость, карты нормалей и эмиссию. И всё. Для простоты.

Начинаем собирать. Корневая нода Surfacematerial, к ней — Surface. BSDF отсутствует — материал тривиально чёрный. Вроде всё нормально.

Дальше добавляем Layer BSDF и к верхнему слою Dielectric BSDF, а к нижнему — Burley Diffuse BSDF. А результате получается глянцевый материал.

Dielectric BSDF принимает параметр шероховатости Roughness типа двумерный вектор, а Burley Diffuse BSDF — скаляр. Когда надо, это позволяет делать эффект анизотропии блика. Но нам не надо. Поэтому добавляем Constant Float, задаём значение 0.25. Преобразуем этот Float в Vector2 с помощью ноды Convert Float Vector2. И подсоединяем их к портам Roughness. Это будет не металлический слой.

Теперь металл. После Layer BSDF добавляем Mix BSDF, и к нему в верхний порт — Conductor BSDF, в нижний — результат того самого Layer BSDF. Установив значение параметра Mix = 1.0, увидим только металлический компонент.

Цвет металла задаётся значениями параметров IOR и Extinction. Чтобы преобразовать нормальный человеку понятный цвет в значения этих параметров используем ноду Artistic IOR. Подсоединяем постоянный цвет (то есть ноду Constant Color3 со значением Value = (0.157, 0.929, 0.133)) во входной порт Color диффузного компонента, а также в порт Reflectivity ноды Artistic IOR, а уж её выходы — в IOR и Extinction металлической компоненты. Значение параметра Edge Color ноды Artistic IOR делаем белым. Это чтобы под большим углом отражение не подкрашивалось ни в какой цвет. Ну и, конечно, подсоединяем двумерное значение Roughness к соответствующему порту Conductor BSDF.

Крутим параметр Mix и видим переход от диффузного компонента к металлическому.

Теперь добавим карту нормалей. Будем использовать вот такую текстуру. Можно прямо себе её сохранить.

Добавляем ноду Tiledimage Vector3, и в ней выбираем указанную выше текстуру. Надо именно Tiledimage, а не просто Image, так как у Tiledimage есть параметр, который позволяет расположить текстуру с повторением вдоль каждой из осей U и V. Указываем UVTiling = (5, 3). И надо именно ту версию Tiledimage, которая возвращает в качестве результата трёхмерный вектор Vector3. Ведь нормали — это векторы, а не пиксели. Наконец, подсоединяем всё это ко входному порту In ноды Normalmap Float, а её результат — к портам Normal всех трёх компонентов поверхности: Dielectric BSDF, Burley Diffuse BSDF и Conductor BSDF.

Последнее — слой свечения. Добавляем Constant Color3 (на картинке это нода MX Constant Color4, так как Softimage сильно умный, и так как уже есть нода с таким именем, то он посчитал, что 3 — это значение счётчика, и поэтому увеличил его до 4, нормальный-нет?), а также Constant Float. Помимо этого Multiply Color3FA и Uniform EDF. Соединяем всё это и подсоединяем к порту EDF ноды Surface. Смысл действий думаю понятен. Нода с цветом — это цвет свечения, а скаляр — это параметр для изменения его интенсивности.

Теперь проверим, как это работает на реальной модели. Я импортировал модель противника из демо-проекта Angry Bots 2 для Unity.

Назначаем всем его частям только сделанный материал. Единственное что осталось — это назначить правильные текстурные карты. EnemySpider_NRM.tga — для карты нормалей (используем Image Vector3 без всякого тайлинга), EnemySpider_D.tga — для диффузного и металлического цвета (не забываем указать, что цветовое пространство должно быть sRGB, по умолчанию используется линейное), EnemySpider_E.tga — для свечения (и множитель свечения задаём 1.0).

Последнее — это правильно извлечь текстуру для Roughness и степени металличности. Эти данные записаны в текстуру EnemySpider_M.tga: в красный канал R — степень металличности, а в альфа-канал A — гладкость поверхности (ну то есть величина, обратная шероховатости Roughness). Получается текстуру надо загрузить как четырёхканальную, делаем это с помощью ноды Image Color4. Потом используем ноду Separate4 Color4 чтобы разделить каналы. Канал R отправляем в коэффициент смешивания с металлической компонентой. С помощью ноды Subtract Float из 1-цы вычитаем канал A, и отправляем это в константу для Roughness.

Результат. Вроде нормально.

Создание ноды для расширения

В этом разделе обсудим следующее. Сейчас понятно как из уже готовых нод собирать шэйдер. Но естественно возникают ситуации, когда реализованных моделей освещения поверхностей не хватает, и требуется добавить свою модель. Вот сейчас это и сделаем. Придётся по-программировать, правда совсем немного. Добавим модель освещения Фонга. Сейчас же в основном используется PBR-подход, и модель Фонга считает позорной. В приличном обществе о ней даже вслух не говорят. Вот и хорошо, будем бунтарями против системы.

Надо сделать две вещи. Во-первых, добавить описание новой ноды — какие входные и выходные параметры она должна иметь. И, во-вторых, написать её реализацию на языках программирования шэйдеров. Мы это сделаем только для GLSL. Для простоты.

Поехали.

Описание ноды делается просто. В папку libraries с библиотекой MaterialX добавляем новую папку custom (там рядом должны ещё быть bxdf, cmlib, lights, nprlib, pbrlib, stdlib и targets). Внутри создаём файл custom_defs.mtlx вот с таким содержимым

<?xml version="1.0"?><materialx version="1.39">  <nodedef name="ND_phong_bsdf" node="phong_bsdf" nodegroup="custom" ><input name="color" type="color3" value="0.18, 0.18, 0.18" uiname="Color" /><input name="specular" type="color3" value="1.0, 1.0., 1.0" uiname="Specular Color" /><input name="shininess" type="float" value="196.0" uiname="Shininess" uimin="0.0" uimax="512.0" /><input name="ambient_amount" type="float" value="0.15" uiname="Ambient Amount" uimin="0.0" uimax="1.0" />    <output name="out" type="BSDF" />  </nodedef></materialx>

Нода готова. У неё четыре входных порта: color — цвет поверхности, specular — цвет блика, shiness — число для задания резкости блика, ambient_amount — степень освещённости теневой стороны объекта.

Теперь glsl-реализация. Внутри папки custom создаём папку genglsl, в неё помещаем два файла. Первый — custom_genglsl_impl.mtlx с содержимым

<?xml version="1.0"?><materialx version="1.39">  <implementation name="IM_phong_bsdf_genglsl" nodedef="ND_phong_bsdf" file="mx_phong_bsdf.glsl" function="mx_phong_bsdf" target="genglsl" /></materialx>

Смысл понятен. Тут фиксируется, что реализация ноды ND_phong_bsdf находится в файле mx_phong_bsdf.glsl и использует в качестве точки входа функцию mx_phong_bsdf. Вот это и надо сделать. Поэтому второй файл — mx_phong_bsdf.glsl с содержимым

#include "../../pbrlib/genglsl/lib/mx_closure_type.glsl"void mx_phong_bsdf(ClosureData closureData, vec3 color, vec3 specular, float shininess, float ambient_amount, inout BSDF bsdf){    bsdf.throughput = vec3(0.0);vec3 N = closureData.N;vec3 L = closureData.L;vec3 V = closureData.V;if (closureData.closureType == CLOSURE_TYPE_REFLECTION) {        vec3 R = reflect(-L, N);        float diff = max(dot(N, L), 0.0);        float spec = pow(max(dot(R, V), 0.0), shininess);        bsdf.response = color * diff + specular * spec;        bsdf.response *= closureData.occlusion * (1 - ambient_amount);    } else if (closureData.closureType == CLOSURE_TYPE_INDIRECT) {bsdf.response = ambient_amount * color;    }}

Тут надо прокомментировать, что есть что.

Первая строка #include “…/…/pbrlib/genglsl/lib/mx_closure_type.glsl” позволяет использовать структуру ClosureData.

Следующая строка void mx_phong_bsdf(ClosureData closureData, vec3 color, vec3 specular, float shininess, float ambient_amount, inout BSDF bsdf) задаёт сигнатуру функции. Она всегда должна быт такого типа. Сначала идёт входной параметр ClosureData closureData, эти данные кодогенератор готовит сам. Мы внутри функции можем использовать

  • closureData.L — нормализованный вектор направления от фрагмента к источнику света

  • closureData.V — нормализованный вектор направления от фрагмента к камере

  • closureData.N — нормализованный вектор нормали в точке фрагмента

  • closureData.P — мировые координаты местоположения точки фрагмента

  • closureData.occlusion — коэффициент влияния тени (это если система рендерит теневые карты)

  • closureData.closureType — проход, в который вызывается функция. Она вызывается в трёх проходах. Тип CLOSURE_TYPE_REFLECTION при расчёте освещения от каждого источника света, CLOSURE_TYPE_INDIRECT при расчёте освещения от глобальной карты окружения, CLOSURE_TYPE_TRANSMISSION при проходе для полупрозрачных объектов.

Продолжаем обсуждать сигнатуру функции. После параметра closureData идут, собственно, значения параметров для входных портов ноды. В конце — inout BSDF bsdf собирает информацию. Структура BSDF содержит два значения throughput — нам это не надо, и response — вклад в цвет фрагмента.

Сама функция работает так. При проходе освещения от источников света вычисляется диффузная компонента dot(N, L), блик pow(max(dot(R, V), 0.0), shininess). Эти значения умножаются на свои цвета, складываются, и результат умножается, во-первых, на теневой коэффициент, и, во-вторых, на величину, обратную степени внешнего освещения. При проходе освещения от карты окружения цвет поверхности умножается на эту самую степень внешнего освещения. Смысл один цвет умножать на 1 - ambient_amount, а второй — на ambient_amount состоит в том, чтобы в целом после обоих проходов на каждый фрагмент не оказывалось лишнего влияния освещения.

Смотрим что получилось. Вроде похоже на то, как должно быть.

Назначаем материал на какой-нибудь объект посложнее.

Чёрт возьми, я снова чувствую себя молодым!

Теперь код сгенерированного glsl-шэйдера.

Скрытый текст
#version 400struct BSDF { vec3 response; vec3 throughput; };#define EDF vec3struct VDF { vec3 response; vec3 throughput; };struct surfaceshader { vec3 color; vec3 transparency; };struct volumeshader { vec3 color; vec3 transparency; };struct displacementshader { vec3 offset; float scale; };struct lightshader { vec3 intensity; vec3 direction; };#define material surfaceshader// Uniform block: PrivateUniformsuniform sampler2D u_shadowMap;uniform mat4 u_shadowMatrix = mat4(1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000);uniform sampler2D u_ambOccMap;uniform float u_ambOccGain = 1.000000;uniform mat4 u_envMatrix = mat4(-1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, -1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000);uniform sampler2D u_envRadiance;uniform float u_envLightIntensity = 1.000000;uniform int u_envRadianceMips = 1;uniform int u_envRadianceSamples = 16;uniform sampler2D u_envIrradiance;uniform bool u_refractionTwoSided = false;uniform vec3 u_viewPosition = vec3(0.0);uniform int u_numActiveLightSources = 0;// Uniform block: PublicUniformsuniform surfaceshader backsurfaceshader;uniform displacementshader displacementshader1;uniform vec3 MX_Phong_Bsdf_color = vec3(0.176000, 0.176000, 0.176000);uniform vec3 MX_Phong_Bsdf_specular = vec3(1.000000, 1.000000, 1.000000);uniform float MX_Phong_Bsdf_shininess = 128.000000;uniform float MX_Phong_Bsdf_ambient_amount = 0.150000;uniform float MX_Surface_opacity = 1.000000;uniform bool MX_Surface_thin_walled = false;in VertexData{    vec2 texcoord_0;    vec3 positionWorld;    vec3 normalWorld;} vd;// Pixel shader outputsout vec4 out1;#define M_FLOAT_EPS 1e-8#define M_PI 3.1415926535897932#define mx_mod mod#define mx_inverse inverse#define mx_inversesqrt inversesqrt#define mx_sin sin#define mx_cos cos#define mx_tan tan#define mx_asin asin#define mx_acos acos#define mx_atan atan#define mx_radians radians#define mx_float_bits_to_int floatBitsToIntvec2 mx_matrix_mul(vec2 v, mat2 m) { return v * m; }vec3 mx_matrix_mul(vec3 v, mat3 m) { return v * m; }vec4 mx_matrix_mul(vec4 v, mat4 m) { return v * m; }vec2 mx_matrix_mul(mat2 m, vec2 v) { return m * v; }vec3 mx_matrix_mul(mat3 m, vec3 v) { return m * v; }vec4 mx_matrix_mul(mat4 m, vec4 v) { return m * v; }mat2 mx_matrix_mul(mat2 m1, mat2 m2) { return m1 * m2; }mat3 mx_matrix_mul(mat3 m1, mat3 m2) { return m1 * m2; }mat4 mx_matrix_mul(mat4 m1, mat4 m2) { return m1 * m2; }float mx_square(float x){    return x*x;}vec2 mx_square(vec2 x){    return x*x;}vec3 mx_square(vec3 x){    return x*x;}#define MAX_LIGHT_SOURCES 4struct LightData{    int type;    vec3 position;    vec3 color;    float intensity;    float decay_rate;    vec3 direction;    float inner_angle;    float outer_angle;    float exposure;};uniform LightData u_lightData[MAX_LIGHT_SOURCES];// https://developer.nvidia.com/gpugems/gpugems3/part-ii-light-and-shadows/chapter-8-summed-area-variance-shadow-mapsfloat mx_variance_shadow_occlusion(vec2 moments, float fragmentDepth){    const float MIN_VARIANCE = 0.00001;    // One-tailed inequality valid if fragmentDepth > moments.x.    float p = (fragmentDepth <= moments.x) ? 1.0 : 0.0;    // Compute variance.    float variance = moments.y - mx_square(moments.x);    variance = max(variance, MIN_VARIANCE);    // Compute probabilistic upper bound.    float d = fragmentDepth - moments.x;    float pMax = variance / (variance + mx_square(d));    return max(p, pMax);}float mx_shadow_occlusion(    sampler2D tex_sampler,    mat4 shadow_matrix,    vec3 world_position){    vec4 shadowCoord4 = mx_matrix_mul(shadow_matrix, vec4(world_position, 1.0));    vec3 shadowCoord = shadowCoord4.xyz / shadowCoord4.w;    shadowCoord = shadowCoord * 0.5 + 0.5;    vec2 shadowMoments = texture(tex_sampler, shadowCoord.xy).xy;    return  mx_variance_shadow_occlusion(shadowMoments, shadowCoord.z);}void mx_point_light(LightData light, vec3 position, out lightshader result){    result.direction = light.position - position;    float distance = length(result.direction) + M_FLOAT_EPS;    float attenuation = pow(distance + 1.0, light.decay_rate + M_FLOAT_EPS);    result.intensity = light.color * light.intensity / attenuation;    result.direction /= distance;}void mx_directional_light(LightData light, vec3 position, out lightshader result){    result.direction = -light.direction;    result.intensity = light.color * light.intensity;}void mx_spot_light(LightData light, vec3 position, out lightshader result){    result.direction = light.position - position;    float distance = length(result.direction) + M_FLOAT_EPS;    float attenuation = pow(distance + 1.0, light.decay_rate + M_FLOAT_EPS);    result.intensity = light.color * light.intensity / attenuation;    result.direction /= distance;    float low = min(light.inner_angle, light.outer_angle);    float high = light.inner_angle;    float cosDir = dot(result.direction, -light.direction);    float spotAttenuation = smoothstep(low, high, cosDir);    result.intensity *= spotAttenuation;}int numActiveLightSources(){    return min(u_numActiveLightSources, MAX_LIGHT_SOURCES) ;}void sampleLightSource(LightData light, vec3 position, out lightshader result){    result.intensity = vec3(0.000000, 0.000000, 0.000000);    result.direction = vec3(0.000000, 0.000000, 0.000000);    if (light.type == 1)    {        mx_point_light(light, position, result);    }    else if (light.type == 2)    {        mx_directional_light(light, position, result);    }    else if (light.type == 3)    {        mx_spot_light(light, position, result);    }    else if (light.type == 4)    {        vec3 L = light.position - position;        float distance = length(L);        L /= distance;        result.direction = L;        result.intensity = vec3(0.000000, 0.000000, 0.000000);    }}// These are defined based on the HwShaderGenerator::ClosureContextType enum// if that changes - these need to be updated accordingly.#define CLOSURE_TYPE_DEFAULT 0#define CLOSURE_TYPE_REFLECTION 1#define CLOSURE_TYPE_TRANSMISSION 2#define CLOSURE_TYPE_INDIRECT 3#define CLOSURE_TYPE_EMISSION 4struct ClosureData {    int closureType;    vec3 L;    vec3 V;    vec3 N;    vec3 P;    float occlusion;};ClosureData makeClosureData(int closureType, vec3 L, vec3 V, vec3 N, vec3 P, float occlusion){    return ClosureData(closureType, L, V, N, P, occlusion);}void mx_phong_bsdf(ClosureData closureData, vec3 color, vec3 specular, float shininess, float ambient_amount, inout BSDF bsdf){    bsdf.throughput = vec3(0.0);vec3 N = closureData.N;vec3 L = closureData.L;vec3 V = closureData.V;if (closureData.closureType == CLOSURE_TYPE_REFLECTION) {        vec3 R = reflect(-L, N);        float diff = max(dot(N, L), 0.0);        float spec = pow(max(dot(R, V), 0.0), shininess);        bsdf.response = color * diff + specular * spec;        bsdf.response *= closureData.occlusion * (1 - ambient_amount);    } else if (closureData.closureType == CLOSURE_TYPE_INDIRECT) {bsdf.response = ambient_amount * color;    }}void main(){    surfaceshader MX_Surface_out = surfaceshader(vec3(0.0),vec3(0.0));    {        vec3 N = normalize(vd.normalWorld);        vec3 V = normalize(u_viewPosition - vd.positionWorld);        vec3 P = vd.positionWorld;        vec3 L = vec3(0.000000, 0.000000, 0.000000);        float occlusion = 1.0;        float surfaceOpacity = MX_Surface_opacity;        // Shadow occlusion        occlusion = mx_shadow_occlusion(u_shadowMap, u_shadowMatrix, vd.positionWorld);        // Light loop        int numLights = numActiveLightSources();        lightshader lightShader;        for (int activeLightIndex = 0; activeLightIndex < numLights; ++activeLightIndex)        {            sampleLightSource(u_lightData[activeLightIndex], vd.positionWorld, lightShader);            L = lightShader.direction;            // Calculate the BSDF response for this light source            ClosureData closureData = makeClosureData(CLOSURE_TYPE_REFLECTION, L, V, N, P, occlusion);            BSDF MX_Phong_Bsdf_out = BSDF(vec3(0.0),vec3(1.0));            mx_phong_bsdf(closureData, MX_Phong_Bsdf_color, MX_Phong_Bsdf_specular, MX_Phong_Bsdf_shininess, MX_Phong_Bsdf_ambient_amount, MX_Phong_Bsdf_out);            // Accumulate the light's contribution            MX_Surface_out.color += lightShader.intensity * MX_Phong_Bsdf_out.response;            // Clear shadow factor for next light            occlusion = 1.0;        }        // Ambient occlusion        occlusion = 1.0;        // Add environment contribution        {            ClosureData closureData = makeClosureData(CLOSURE_TYPE_INDIRECT, L, V, N, P, occlusion);            BSDF MX_Phong_Bsdf_out = BSDF(vec3(0.0),vec3(1.0));            mx_phong_bsdf(closureData, MX_Phong_Bsdf_color, MX_Phong_Bsdf_specular, MX_Phong_Bsdf_shininess, MX_Phong_Bsdf_ambient_amount, MX_Phong_Bsdf_out);            MX_Surface_out.color += occlusion * MX_Phong_Bsdf_out.response;        }        // Calculate the BSDF transmission for viewing direction        ClosureData closureData = makeClosureData(CLOSURE_TYPE_TRANSMISSION, L, V, N, P, occlusion);        BSDF MX_Phong_Bsdf_out = BSDF(vec3(0.0),vec3(1.0));        mx_phong_bsdf(closureData, MX_Phong_Bsdf_color, MX_Phong_Bsdf_specular, MX_Phong_Bsdf_shininess, MX_Phong_Bsdf_ambient_amount, MX_Phong_Bsdf_out);        MX_Surface_out.color += MX_Phong_Bsdf_out.response;        // Compute and apply surface opacity        {            MX_Surface_out.color *= surfaceOpacity;            MX_Surface_out.transparency = mix(vec3(1.000000, 1.000000, 1.000000), MX_Surface_out.transparency, surfaceOpacity);        }    }    material MX_Surfacematerial_out = MX_Surface_out;    out1 = vec4(MX_Surfacematerial_out.color, 1.0);}

Скажем прямо — кода многовато. Это я ещё из него удалил кучу функций, которые тут просто не используются, но которые всегда включаются. Например для освещения HDR-картой окружения. Тут нам этого не надо. На мой дилетантский взгляд не очень понятно, можно ли тут что-то существенное с-оптимизировать. Если только руками убрать проход прозрачности (строки 276 — 289) и неиспользуемые юниформы для карты внешнего освещения (строки 15-22). Но в целом, наверное, нормально, если учесть что это результат генерации по шаблону, который должен поддерживать большую функциональность.

Послесловие

Я намеренно не писал никакого кода, связанного с непосредственным использованием API MaterialX. Будь то хоть Python, хоть c++. В нашу светлую эпоху генеративного ИИ (да-да, мы именно в такое время живём, не в эпоху же когнитивной деградации, в самом деле, понимать надо) эти самые помощники научат любому API, только попроси. С примерами и описанием функций. Даже в документацию не придётся лазить. Да этот ИИ — это и есть документация. А если что, так тот же ИИ и приложение сам напишет. Успевай только успешных успехов достигать. Такие времена!

За сим — откланиваюсь.

ссылка на оригинал статьи https://habr.com/ru/articles/1036750/