Абстрактные материалы
Чтобы упростить программирование шейдеров и скрыть сложность реализации различных фич, были добавлены Абстрактные материалы. Подобно абстрактным классам в объектно-ориентированном программировании, они служат шаблоном определения определенных параметров и функций.
Сам абстрактный материал не может быть назначен объектам, он используется для наследования от него других абстрактных и базовых материалов, расширяя его функциональность, что дает вам большую гибкость.
Вы можете создать свой собственный абстрактный материал или использовать готовые Mesh / Mesh Transparent / Mesh Unlit / Decal с тесселяцией или без нее для создания собственного базового материала.
Наследование и переопределение#
Наследуя от абстрактного материала, вы можете переопределить любые значения и добавить некоторые другие компоненты к дочернему материалу:
AbstractMaterial A
{
Slider t = 0.5
}
BaseMaterial B <parent = A>
{
Slider t = 1.0
Slider t2 = 2.0
}
Когда дочерний материал реализует одноименный шейдер, происходит слияние исходного кода родительского и дочернего шейдера (код родительского шейдера предшествует дочернему):
AbstractMaterial A
{
Shader example =
#{
// A_fragment
}#
}
AbstractMaterial B <parent = A>
{
Shader example =
#{
// B_fragment
}#
}
// the result example shader for the abstract material B will look like this:
Shader example =
#{
// A_fragment
// B_fragment
#}
Вы можете расширить и включить код другого шейдера, используя наследование и маркер #shader shader_name.
AbstractMaterial A
{
// including the different shader code
Shader a =
#{
// shader code A
#shader b
}#
Shader b =
#{
// shader code B1
}#
}
AbstractMaterial B <parent = A>
{
// expanding the parent shader code
Shader b =
#{
// shader code B2
}#
}
// the resulting example shader for the abstract material B will look like this:
Shader a =
#{
// shader code A
// shader code B1
// shader code B2
#}
Те же правила применяются к Скрипт код (используйте #script script_name в качестве маркера).
Элементы в родительских и дочерних материалах Группы с тем же именем могут быть объединены. Обе группы должны иметь аргумент merge_group, установленный на true. Первая запись группы определяет место в пользовательском интерфейсе редактора, где расположены все элементы этой группы (родительский и дочерний):
AbstractMaterial A
{
Group A1
{
Slider T1
Group example <merge_group=true>
{
Slider T2
}
}
}
BaseMaterial B <parent = A>
{
Group B1
{
Slider T3
Group example <merge_group=true>
{
Slider T4
}
}
}
// the result grouping for the BaseMaterial B will look like this ("example" in the BaseMaterial B is merged with the parent example group):
Group A1
{
Slider T1
Group example
{
Slider T2
Slider T4
}
}
Group B1
{
Slider T3
}
Сравнение с традиционной логикой базового материала#
Абстрактные материалы обобщают внутреннюю логику и обеспечивают упрощенный интерфейс ввода и вывода для производных базовых материалов. Традиционный подход включает в себя много повторяющейся работы по созданию материалов с разными оттенками для одних и тех же типов объектов. Поэтому рекомендуется использовать смесь абстрактного и базового материала. См. следующие примеры, которые помогают добиться такого же модифицированного эффекта SSAO.
Сочетание абстрактного и базового материала#
Этот пример демонстрирует, как объединяются абстрактный и базовый материалы.
my_abstract.abstmat
AbstractMaterial <preview_hidden=true var_prefix=var texture_prefix=tex>
{
Texture2D color <source=procedural>
Shader common=
#{
#}
Shader base_shader=
#{
#include <core/materials/shaders/render/common.h>
STRUCT_FRAG_BEGIN
INIT_COLOR(float4)
STRUCT_FRAG_END
#shader common
MAIN_FRAG_BEGIN(FRAGMENT_IN)
#shader fragment
MAIN_FRAG_END
#}
Pass post
{
Fragment=base_shader
}
Expression RENDER_CALLBACK_END_POST_MATERIALS=
#{
Texture source = engine.render.getTemporaryTexture(engine.render_state.getScreenColorTexture());
source.copy(engine.render_state.getScreenColorTexture());
setTexture("color", source);
renderPassToTexture("post", engine.render_state.getScreenColorTexture());
engine.render.releaseTemporaryTexture(source);
#}
}
my_base.basemat
BaseMaterial <parent=my_abstract>
{
Texture2D normal <source=gbuffer_normal>
Texture2D scene_depth <source=current_depth>
Texture2D modulation = "core/textures/common/checker_d.dds"
Texture2D <source=ssao>
Slider global_scale = 2
Shader common=
#{
float3 triplanar_sample(float2 uv, float depth_in)
{
float3 normal_ws = mul3(s_imodelview, unpackGBufferNormal(TEXTURE(tex_normal, uv).rgb));
float3 position_ws = s_camera_position + mul3(s_imodelview, nativeDepthToPositionVS(depth_in, uv));
float4 texcoord = position_ws.xyyz * var_global_scale;
float3 weight = triplanarWeightFast(normal_ws, 0.5f);
return TEXTURE_TRIPLANAR(tex_modulation, texcoord, weight).rgb;
}
float4 final(float2 uv, float depth_in)
{
float4 base = TEXTURE(tex_color, uv);
float ssao = TEXTURE(tex_ssao, uv).r;
float3 effect = triplanar_sample(uv, depth_in);
return float4(lerp(base.rgb * effect, base.rgb, ssao), base.a);
}
#}
Shader fragment=
#{
float depth = TEXTURE(tex_scene_depth, IN_UV).r;
if (depth == 0.0f || depth == 1.0f)
discard;
OUT_COLOR = final(IN_UV, depth);
#}
}
Только основной материал#
Этот пример демонстрирует тот же вариант использования, что и над но с использованием только базового материала.
my_single_base.basemat
BaseMaterial <preview_hidden=true var_prefix=var texture_prefix=tex>
{
Texture2D color <source=procedural>
Texture2D normal <source=gbuffer_normal>
Texture2D scene_depth <source=current_depth>
Texture2D modulation = "core/textures/common/checker_d.dds"
Texture2D <source=ssao>
Slider global_scale = 2
Shader base_shader=
#{
#include <core/materials/shaders/render/common.h>
STRUCT_FRAG_BEGIN
INIT_COLOR(float4)
STRUCT_FRAG_END
float3 triplanar_sample(float2 uv, float depth_in)
{
float3 normal_ws = mul3(s_imodelview, unpackGBufferNormal(TEXTURE(tex_normal, uv).rgb));
float3 position_ws = s_camera_position + mul3(s_imodelview, nativeDepthToPositionVS(depth_in, uv));
float4 texcoord = position_ws.xyyz * var_global_scale;
float3 weight = triplanarWeightFast(normal_ws, 0.5f);
return TEXTURE_TRIPLANAR(tex_modulation, texcoord, weight).rgb;
}
float4 final(float2 uv, float depth_in)
{
float4 base = TEXTURE(tex_color, uv);
float ssao = TEXTURE(tex_ssao, uv).r;
float3 effect = triplanar_sample(uv, depth_in);
return float4(lerp(effect, base.rgb, ssao), base.a);
}
MAIN_FRAG_BEGIN(FRAGMENT_IN)
float depth = TEXTURE(tex_scene_depth, IN_UV).r;
if (depth == 0.0f || depth == 1.0f)
discard;
OUT_COLOR = final(IN_UV, depth);
MAIN_FRAG_END
#}
Pass post
{
Fragment=base_shader
}
Expression RENDER_CALLBACK_END_POST_MATERIALS=
#{
Texture source = engine.render.getTemporaryTexture(engine.render_state.getScreenColorTexture());
source.copy(engine.render_state.getScreenColorTexture());
setTexture("color", source);
renderPassToTexture("post", engine.render_state.getScreenColorTexture());
engine.render.releaseTemporaryTexture(source);
#}
}
Создание и использование собственного материала#
Давайте создадим материал с нуля. В этом примере мы возьмем базовый абстрактный материал для эффектов постобработки.
1. Создание родительского абстрактного материала#
Создайте новый текстовый файл с расширением *.abstmat. В качестве имени материала используется имя файла, назовем его my_abstract.
Абстрактный материал содержит все основные параметры, которые наследуются всеми дочерними материалами, а также базовый код шейдера. Эти основные вещи объявляются следующим образом:
AbstractMaterial <preview_hidden=true var_prefix=var texture_prefix=tex>
{
// Let's add a screen texture, we will use it later in BaseMaterial
Texture2D color <source=procedural>
Shader common=
#{
#}
// Here comes the core of our fragment shader:
Shader base_shader= // Declare the shader node for further use
#{
#include <core/materials/shaders/render/common.h> //Base UUSL include
// We render to a single texture with 4 channels
STRUCT_FRAG_BEGIN
INIT_COLOR(float4)
STRUCT_FRAG_END
// Here we'll add code from inherited materials
#shader common
// Declare the main fragment function
MAIN_FRAG_BEGIN(FRAGMENT_IN)
// Here we'll add code from inherited materials
#shader fragment
MAIN_FRAG_END
#}
}
Теперь нам нужно добавить проход рендеринга, потому что один шейдер сам по себе ничего не рендерит. Поскольку мы реализуем материал пост-эффекта, мы должны добавить проход post и указать шейдеры, которые будут использоваться для него (мы будем использовать наш base_shader как фрагментный шейдер).
Pass post
{
Fragment=base_shader
}
UNIGINE поддерживает Скриптовые материалы функциональность, позволяющая выполнять выражения (фрагменты кода) на определенных этапах последовательность рендеринга , и мы собираемся использовать его.
// Let's subscribe our USC expression to render callback after all Engine's post-effects
Expression RENDER_CALLBACK_END_POST_MATERIALS=
#{
// Make a copy of the screen texture
Texture source = engine.render.getTemporaryTexture(engine.render_state.getScreenColorTexture());
source.copy(engine.render_state.getScreenColorTexture());
// Set it as the source texture for the post-effect
setTexture("color", source);
// Render the pass "post" directly to the screen
renderPassToTexture("post", engine.render_state.getScreenColorTexture());
// Since we won't use a newly requested texture, we can tell the engine that anyone else can use it.
engine.render.releaseTemporaryTexture(source);
#}
Собрав все вместе, мы получаем полный код нашего абстрактного материала, который вы можете скопировать и вставить в свой текстовый редактор и сохранить как файл my_abstract.abstmat в папке data вашего проекта:
my_abstract_base1.basemat
AbstractMaterial <preview_hidden=true var_prefix=var texture_prefix=tex>
{
// Let's add a screen texture, we will use it later in BaseMaterial
Texture2D color <source=procedural>
Shader common=
#{
#}
// Here comes the core of our fragment shader:
Shader base_shader= // Declare the shader node for further use
#{
#include <core/materials/shaders/render/common.h> //Base UUSL include
// We render to a single texture with 4 channels
STRUCT_FRAG_BEGIN
INIT_COLOR(float4)
STRUCT_FRAG_END
// Here we'll add code from inherited materials
#shader common
// Declare the main fragment function
MAIN_FRAG_BEGIN(FRAGMENT_IN)
// Here we'll add code from inherited materials
#shader fragment
MAIN_FRAG_END
#}
// Adding a post pass to render our effect
Pass post
{
Fragment=base_shader
}
// Let's subscribe our USC expression to render callback after all Engine's post-effects
Expression RENDER_CALLBACK_END_POST_MATERIALS=
#{
// Make a copy of the screen texture
Texture source = engine.render.getTemporaryTexture(engine.render_state.getScreenColorTexture());
source.copy(engine.render_state.getScreenColorTexture());
// Set it as the source texture for the post-effect
setTexture("color", source);
// Render the pass "post" directly to the screen
renderPassToTexture("post", engine.render_state.getScreenColorTexture());
// Since we won't use a newly requested texture, we can tell the engine that anyone else can use it.
engine.render.releaseTemporaryTexture(source);
#}
}
2. Наследование базового материала от абстрактного#
Теперь мы собираемся наследовать базовый материал от созданного абстрактного (my_abstract.abstmat), чтобы расширить его функциональность.
Для начала мы реализуем простой пост-эффект цветового фильтра.
Создайте новый файл с расширением *.basemat. Опять же, имя файла используется в качестве имени основного материала.
Опишите базовый материал, указав, что он имеет абстрактный материал my_abstract в качестве родителя:
BaseMaterial <parent=my_abstract>
{
// Add the color parameter that will define the optical filter color
Color my_color = [0.5 0.5 0.5 1]
// All shader variables that are defined in the material should have a prefix to be referred to in shaders
// For sliders, colors, and other basic parameters, it is defined with var_prefix currently set as 'var'
// For textures, it is texture_prefix currently set as 'tex'
Shader fragment=
#{
// Sample screen texture
float4 sample = TEXTURE(tex_color, IN_UV);
// Multiply the screen texture by specified color
OUT_COLOR = sample * var_my_color;
#}
}
Все переменные шейдера, определенные в материале, должны иметь префикс, чтобы на них можно было ссылаться в шейдерах:
- Префикс для slider, color и других основных параметров задаются с помощью аргумента var_prefix.
- Префикс для текстур задается с помощью аргумента texture_prefix.
Например:
// Setting prefixes
AbstractMaterial <preview_hidden=true var_prefix=var texture_prefix=tex>
/*...*/
// Adding the parameter
Color my_color = [0.5 0.5 0.5 1]
/*...*/
// Refering to a variable
OUT_COLOR = sample * var_my_color;
Сохраните базовый материал как my_abstract_base1.basemat в папку data вашего проекта.
Вот и все. То, что мы написали, достаточно для создания простого эффекта постобработки.
3. Добавление другого производного материала#
Теперь мы собираемся вывести другой, более сложный материал из нашего абстрактного.
Этот материал должен использовать трехплоскостное отображение для применения указанной текстуры modulation с указанным intensity в закрытых областях (согласно карте SSAO).
Давайте создадим новый файл базового материала и установим тот же абстрактный материал (my_abstract.abstmat) в качестве его родителя.
Вот полный код нашего второго базового материала (сохраните его как my_abstract_base2.basemat в папке data вашего проекта):
my_abstract_base2.basemat
BaseMaterial <parent=my_abstract>
{
// For this example more scene textures are required
Texture2D normal <source=gbuffer_normal>
Texture2D scene_depth <source=current_depth>
Texture2D <source=ssao>
// Modulation texture, UV scale, and effect intensity
Texture2D modulation = "core/textures/common/checker_d.dds"
Slider global_scale = 2
Slider power = 10 <min=0 max=3>
Shader common=
#{
float3 triplanar_sample(float2 uv, float depth_in)
{
float3 normal_ws = mul3(s_imodelview, unpackGBufferNormal(TEXTURE(tex_normal, uv).rgb));
float3 position_ws = s_camera_position + mul3(s_imodelview, nativeDepthToPositionVS(depth_in, uv));
float4 texcoord = position_ws.xyyz * var_global_scale;
float3 weight = triplanarWeightFast(normal_ws, 0.5f);
return TEXTURE_TRIPLANAR(tex_modulation, texcoord, weight).rgb;
}
float4 final(float2 uv, float depth_in)
{
float4 base = TEXTURE(tex_color, uv);
float ssao = TEXTURE(tex_ssao, uv).r;
float3 effect = triplanar_sample(uv, depth_in);
return float4(lerp(base.rgb * effect * var_power, base.rgb, ssao), base.a);
}
#}
Shader fragment=
#{
// Sample depth texture
float depth = TEXTURE(tex_scene_depth, IN_UV).r;
if (depth == 0.0f || depth == 1.0f)
discard;
OUT_COLOR = final(IN_UV, depth);
#}
}
4. Наследование пользовательских материалов от базовых и их использование#
Значения параметров, заявленные в базовых материалах, могут быть изменены в режиме реального времени только в унаследованных пользовательских материалах. Следовательно, вам необходимо наследовать пользовательские материалы от базовых материалов для ваших проектов и корректировать их по мере необходимости (назначать и изменять значения параметров и т. д.).
Чтобы протестировать наш первый пост-эффект, сделайте следующее:
- Найдите my_abstract_base1.basemat вMaterialsщелкните его правой кнопкой мыши, выберите Create Child и переименуйте новый материал в my_color_filter.
- Поскольку это пост-эффект, для глобального применения материала мы должны открыть файл Settings -> Render -> Scriptable Materials.
- Нажмите Add New Scriptable Material и укажите материал my_color_filter. После этого вы можете изменить цвет фильтра.
Вот финальное изображение с нашим первым пост-материалом.
Повторите описанные выше шаги для базового материала my_abstract_base2.basemat, чтобы протестировать второй пост-эффект. Вот как это может выглядеть: