From 81489a13cfeb4db6b6b06ad06ef8f266d54cea23 Mon Sep 17 00:00:00 2001 From: lookibed <93531352+lookibed@users.noreply.github.com> Date: Fri, 2 Oct 2026 06:33:01 +0300 Subject: [PATCH] add block material shaders - 'shader' property of block materials: shaders/materials/.glslv, .glslf with material_vertex / material_fragment hooks and #param uniforms - material shaders code is included into main, translucent and shadows programs through generated headers, no extra draw calls - chunk vertex normal.w byte replaced with 'flags': bit 7 - emission flag, bits 0-6 - material shader index (vertex format size is not changed) - doc/*/block-materials.md --- doc/en/block-materials.md | 97 +++++++ doc/en/block-properties.md | 2 +- doc/en/content-packs.md | 1 + doc/en/main-page.md | 1 + doc/ru/block-materials.md | 97 +++++++ doc/ru/block-properties.md | 2 +- doc/ru/content-packs.md | 1 + doc/ru/main-page.md | 1 + res/shaders/clouds.glslv | 2 +- res/shaders/lib/commons.glsl | 9 + res/shaders/lib/world_fragment_header.glsl | 1 + res/shaders/lib/world_vertex_header.glsl | 1 + res/shaders/main.glslf | 6 +- res/shaders/main.glslv | 9 +- res/shaders/shadows.glslf | 8 +- res/shaders/shadows.glslv | 15 +- res/shaders/translucent.glslf | 6 +- res/shaders/translucent.glslv | 8 +- src/constants.hpp | 2 + src/content/ContentBuilder.cpp | 48 ++++ src/engine/AssetsManagement.cpp | 4 + src/graphics/core/PostEffect.cpp | 78 ++--- src/graphics/core/PostEffect.hpp | 9 + src/graphics/core/Shader.hpp | 4 + src/graphics/render/BlocksRenderer.cpp | 9 +- src/graphics/render/BlocksRenderer.hpp | 2 + src/graphics/render/CloudsRenderer.cpp | 2 +- src/graphics/render/MaterialShaders.cpp | 317 +++++++++++++++++++++ src/graphics/render/MaterialShaders.hpp | 39 +++ src/graphics/render/WorldRenderer.cpp | 2 + src/graphics/render/commons.hpp | 17 +- src/voxels/Block.cpp | 4 +- src/voxels/Block.hpp | 10 + 33 files changed, 761 insertions(+), 53 deletions(-) create mode 100644 doc/en/block-materials.md create mode 100644 doc/ru/block-materials.md create mode 100644 src/graphics/render/MaterialShaders.cpp create mode 100644 src/graphics/render/MaterialShaders.hpp diff --git a/doc/en/block-materials.md b/doc/en/block-materials.md new file mode 100644 index 000000000..a77c3910c --- /dev/null +++ b/doc/en/block-materials.md @@ -0,0 +1,97 @@ +# Block materials + +A material is a common kit of properties applied to groups of blocks. +Material definitions are located in the `block_materials/` folder of a content pack. +A block refers to a material with the `material` property in the `pack:material_name` format +(see [block properties](block-properties.md)). + +Example (`block_materials/glass.json`): + +```json +{ + "steps-sound": "steps/glass", + "place-sound": "blocks/glass_place", + "break-sound": "blocks/glass_break", + "sound-absorption": 0.3 +} +``` + +## Properties + +| Name | Type | Description | +|------------------|--------|----------------------------------------------------------| +| steps-sound | string | steps sound | +| place-sound | string | block placing sound | +| break-sound | string | block breaking sound | +| hit-sound | string | block hitting sound (`steps-sound` by default) | +| sound-absorption | number | ambient sounds absorption when the camera is in the block| +| shader | string | material shader name (see below) | + +## Material shaders + +A material shader allows to customize blocks and entities rendering without replacing the engine shaders. +Material shader code is included in the world shaders (opaque, translucent and shadows passes of blocks +and the entities program), so blocks with different materials are still rendered within a single draw call per chunk. + +An entity uses the shader of its `material` property (see [entity properties](entity-properties.md)). +The material may be overridden at runtime with `rig:set_material` (see [skeleton](scripting/ecs.md)). + +The `shader` property specifies the name of a shader located in the `shaders/materials/` folder of a pack. +Each of two files is optional, but an existing file must define its function: + +- `shaders/materials/name.glslv` - vertex stage, must define the function: + ```glsl + // position - vertex world position + vec3 material_vertex(vec3 position) + ``` + The function may also modify the vertex normal `a_realnormal` (world space), + for example when displacing a surface. +- `shaders/materials/name.glslf` - fragment stage, must define the function: + ```glsl + // color - block texture color before lighting applied + vec4 material_fragment(vec4 color) + ``` + +All the world shader uniforms (`u_timer`, `u_cameraPos`, `u_dayTime`, etc.), +vertex attributes (`v_position`, `v_texCoord`, `v_light`, `v_normal`) in the vertex stage and +varyings (`a_texCoord`, `a_modelpos`, `a_realnormal`, `a_skyLight`, etc.) in the fragment stage are available. +The same material shader code is compiled into all the programs (blocks, shadows and entities), so only +the common interface declared in `shaders/lib/world_vertex_header.glsl`, `world_fragment_header.glsl` +and `world_uniforms.glsl` may be used. +Shader parameters are declared with the `#param` directive, the same way as in [post-effects](scripting/builtins/libgfx-posteffects.md), +and controlled from scripts with the [gfx.materials](scripting/builtins/libgfx-materials.md) library. + +Example (`shaders/materials/leaves.glslv`): + +```glsl +#param float p_amplitude = 0.03 +#param float p_speed = 1.2 + +vec3 material_vertex(vec3 position) { + float phase = position.x * 0.7 + position.z * 1.1 + position.y * 0.4; + position.x += sin(u_timer * p_speed + phase) * p_amplitude; + return position; +} +``` + +Materials with the same `shader` share the shader code and its params. +Hook functions and params names are made unique for every shader automatically, +so different shaders may declare the same params. +Other functions and global variables of all the material shaders are compiled +into one program, so their names must be unique (use a prefix). + +> [!NOTE] +> Compilation errors refer to a material shader by its index: `3:12` means line 12 +> of the shader number 3. The indices are printed to the log on content loading, +> `128` is the generated dispatch code, `0` is the engine shader. + +Limitations: + +- up to 127 material shaders may be loaded at the same time +- array params are not supported, params names must not start with `_` +- the same param declared in both stages must have the same type +- vertex displacement must be continuous (depend on the vertex position only), + otherwise gaps between faces appear; it must not depend on `u_cameraPos`, + because the shadows pass is rendered from the light's point of view +- `texture()` calls inside `material_fragment` have undefined derivatives + at borders between materials, use `textureLod` there diff --git a/doc/en/block-properties.md b/doc/en/block-properties.md index 82ba10484..f4e0d95d0 100644 --- a/doc/en/block-properties.md +++ b/doc/en/block-properties.md @@ -81,7 +81,7 @@ until the block is destroyed or the camera moves away a certain distance. ### Material - *material* Defines the name of the block's material in the format `pack:material_name`, which affects the selection of block interaction sounds. -Material definitions are located in /block_materials. +Material definitions are located in /block_materials (see [block materials](block-materials.md)). ## Variants diff --git a/doc/en/content-packs.md b/doc/en/content-packs.md index 48ec57e6d..67f5c79a0 100644 --- a/doc/en/content-packs.md +++ b/doc/en/content-packs.md @@ -89,6 +89,7 @@ Don't be intimidated by the following text, as a minimal pack only requires `pac - `textures/` - Textures - `shaders/` - Shaders - `effects/` - Post-processing effects + - `materials/` - Block material shaders - `sounds/` - Sounds and Music - `texts/` - Localization files - GUI: diff --git a/doc/en/main-page.md b/doc/en/main-page.md index 65a22ee85..289110dd8 100644 --- a/doc/en/main-page.md +++ b/doc/en/main-page.md @@ -7,6 +7,7 @@ - [Assets preloading](assets-preload.md) - [Audio](audio.md) +- [Block materials](block-materials.md) - [Block models](block-models.md) - [Block properties](block-properties.md) - [Console](console.md) diff --git a/doc/ru/block-materials.md b/doc/ru/block-materials.md new file mode 100644 index 000000000..24e2472f3 --- /dev/null +++ b/doc/ru/block-materials.md @@ -0,0 +1,97 @@ +# Материалы блоков + +Материал - общий набор свойств, применяемых к группам блоков. +Определения материалов расположены в папке `block_materials/` контент-пака. +Блок ссылается на материал свойством `material` в формате `пак:имя_материала` +(см. [свойства блоков](block-properties.md)). + +Пример (`block_materials/glass.json`): + +```json +{ + "steps-sound": "steps/glass", + "place-sound": "blocks/glass_place", + "break-sound": "blocks/glass_break", + "sound-absorption": 0.3 +} +``` + +## Свойства + +| Имя | Тип | Описание | +|------------------|--------|------------------------------------------------------------| +| steps-sound | string | звук шагов | +| place-sound | string | звук установки блока | +| break-sound | string | звук разрушения блока | +| hit-sound | string | звук удара по блоку (по умолчанию - `steps-sound`) | +| sound-absorption | number | поглощение окружающих звуков, когда камера внутри блока | +| shader | string | имя шейдера материала (см. ниже) | + +## Шейдеры материалов + +Шейдер материала позволяет настроить отрисовку блоков и сущностей без замены шейдеров движка. +Код шейдера материала включается в мировые шейдеры (проходы непрозрачных, полупрозрачных блоков, теней +и программу сущностей), поэтому блоки с разными материалами по-прежнему отрисовываются одним вызовом на чанк. + +Сущность использует шейдер материала из своего свойства `material` (см. [свойства сущностей](entity-properties.md)). +Материал можно переопределить в рантайме через `rig:set_material` (см. [скелет](scripting/ecs.md)). + +Свойство `shader` указывает имя шейдера, расположенного в папке `shaders/materials/` пака. +Каждый из двух файлов опционален, но существующий файл обязан определять свою функцию: + +- `shaders/materials/имя.glslv` - вершинная стадия, должен определять функцию: + ```glsl + // position - мировая позиция вершины + vec3 material_vertex(vec3 position) + ``` + Функция также может изменять нормаль вершины `a_realnormal` (мировые координаты), + например, при смещении поверхности. +- `shaders/materials/имя.glslf` - фрагментная стадия, должен определять функцию: + ```glsl + // color - цвет текстуры блока до применения освещения + vec4 material_fragment(vec4 color) + ``` + +Доступны все uniform-переменные мировых шейдеров (`u_timer`, `u_cameraPos`, `u_dayTime` и т.д.), +атрибуты вершин (`v_position`, `v_texCoord`, `v_light`, `v_normal`) в вершинной стадии и +varying-переменные (`a_texCoord`, `a_modelpos`, `a_realnormal`, `a_skyLight` и т.д.) во фрагментной. +Один и тот же код материала компилируется во все программы (блоки, тени и сущности), поэтому можно +использовать только общий интерфейс, объявленный в `shaders/lib/world_vertex_header.glsl`, +`world_fragment_header.glsl` и `world_uniforms.glsl`. +Параметры шейдера объявляются директивой `#param`, так же, как в [пост-эффектах](scripting/builtins/libgfx-posteffects.md), +и управляются из скриптов библиотекой [gfx.materials](scripting/builtins/libgfx-materials.md). + +Пример (`shaders/materials/leaves.glslv`): + +```glsl +#param float p_amplitude = 0.03 +#param float p_speed = 1.2 + +vec3 material_vertex(vec3 position) { + float phase = position.x * 0.7 + position.z * 1.1 + position.y * 0.4; + position.x += sin(u_timer * p_speed + phase) * p_amplitude; + return position; +} +``` + +Материалы с одинаковым `shader` используют общий код шейдера и его параметры. +Имена функций-хуков и параметров автоматически делаются уникальными для каждого шейдера, +поэтому разные шейдеры могут объявлять одинаковые параметры. +Остальные функции и глобальные переменные всех шейдеров материалов компилируются +в одну программу, поэтому их имена должны быть уникальными (используйте префикс). + +> [!NOTE] +> Ошибки компиляции ссылаются на шейдер материала по его номеру: `3:12` означает строку 12 +> шейдера номер 3. Номера печатаются в лог при загрузке контента, +> `128` это сгенерированный код диспетчера, `0` шейдер движка. + +Ограничения: + +- одновременно может быть загружено до 127 шейдеров материалов +- параметры-массивы не поддерживаются, имена параметров не могут начинаться с `_` +- один и тот же параметр в обеих стадиях должен иметь одинаковый тип +- смещение вершин должно быть непрерывным (зависеть только от позиции вершины), + иначе между гранями появляются щели; оно не должно зависеть от `u_cameraPos`, + так как проход теней рисуется с точки зрения источника света +- у вызовов `texture()` внутри `material_fragment` не определены производные + на границах материалов, используйте там `textureLod` diff --git a/doc/ru/block-properties.md b/doc/ru/block-properties.md index 09db5c8ef..a6fce4d89 100644 --- a/doc/ru/block-properties.md +++ b/doc/ru/block-properties.md @@ -82,7 +82,7 @@ ### Материал - *material* Определяет имя материала блока в формате `пак:имя_материала`, что влияет на выбор звуков взаимодействия с блоком. -Определения материалов расположены в /block_materials. +Определения материалов расположены в /block_materials (см. [материалы блоков](block-materials.md)). ## Варианты diff --git a/doc/ru/content-packs.md b/doc/ru/content-packs.md index 4ab002104..db38c33a5 100644 --- a/doc/ru/content-packs.md +++ b/doc/ru/content-packs.md @@ -91,6 +91,7 @@ print(file.read("пак:package.json")) -- выведет содержимое p - `textures/` - текстуры - `shaders/` - шейдеры - `effects/` - эффекты пост-обработки + - `materials/` - шейдеры материалов блоков - `sounds/` - звуки и музыка - `texts/` - файлы локализации - GUI: diff --git a/doc/ru/main-page.md b/doc/ru/main-page.md index 916202151..fb70d7b7f 100644 --- a/doc/ru/main-page.md +++ b/doc/ru/main-page.md @@ -11,6 +11,7 @@ - [Движок генерации мира](world-generator.md) - [Консоль](console.md) - [Контент‐паки](content-packs.md) +- [Материалы блоков](block-materials.md) - [Модели блоков](block-models.md) - [Предзагрузка ассетов](assets-preload.md) - [Рекомендации по использованию движка](engine-use-recommendations.md) diff --git a/res/shaders/clouds.glslv b/res/shaders/clouds.glslv index 0b9c17774..b6fb159bb 100644 --- a/res/shaders/clouds.glslv +++ b/res/shaders/clouds.glslv @@ -41,7 +41,7 @@ void main() { a_fog = calc_fog(length(viewmodel * vec4(pos3d * FOG_POS_SCALE, 0.0)) / 256.0); #endif - a_emission = v_normal.w; + a_emission = unpack_emission(v_normal.w); vec4 viewmodelpos = u_view * a_modelpos; a_position = viewmodelpos.xyz; diff --git a/res/shaders/lib/commons.glsl b/res/shaders/lib/commons.glsl index e13b0ce15..6b88d86b0 100644 --- a/res/shaders/lib/commons.glsl +++ b/res/shaders/lib/commons.glsl @@ -8,4 +8,13 @@ vec3 apply_planet_curvature(vec3 modelPos, vec3 pos3d) { return modelPos; } +// chunk vertex flags (normal.w): bit 7 - emission flag, bits 0-6 - material +float unpack_emission(float w) { + return float(int(w * 255.0 + 0.5) >> 7); +} + +int unpack_material(float w) { + return int(w * 255.0 + 0.5) & 127; +} + #endif // COMMONS_GLSL_ diff --git a/res/shaders/lib/world_fragment_header.glsl b/res/shaders/lib/world_fragment_header.glsl index 8e6e88bdb..ad1fa051f 100644 --- a/res/shaders/lib/world_fragment_header.glsl +++ b/res/shaders/lib/world_fragment_header.glsl @@ -11,6 +11,7 @@ in vec3 a_realnormal; in vec3 a_skyLight; in vec4 a_modelpos; in float a_emission; +flat in int a_material; #include diff --git a/res/shaders/lib/world_vertex_header.glsl b/res/shaders/lib/world_vertex_header.glsl index 9e7550278..e92a0309f 100644 --- a/res/shaders/lib/world_vertex_header.glsl +++ b/res/shaders/lib/world_vertex_header.glsl @@ -11,6 +11,7 @@ out vec3 a_realnormal; out vec3 a_skyLight; out vec4 a_modelpos; out float a_emission; +flat out int a_material; #include diff --git a/res/shaders/main.glslf b/res/shaders/main.glslf index 3a12c406e..8d9186856 100644 --- a/res/shaders/main.glslf +++ b/res/shaders/main.glslf @@ -15,8 +15,12 @@ uniform bool u_alphaClip; uniform bool u_debugLights; uniform bool u_debugNormals; +#include <__materials_fragment__> + void main() { - vec4 texColor = texture(u_texture0, a_texCoord); + vec4 texColor = apply_material_fragment( + a_material, texture(u_texture0, a_texCoord) + ); float alpha = texColor.a; if (u_alphaClip) { if (alpha < 0.2f) diff --git a/res/shaders/main.glslv b/res/shaders/main.glslv index 08cc6469e..98db30854 100644 --- a/res/shaders/main.glslv +++ b/res/shaders/main.glslv @@ -14,11 +14,16 @@ uniform float u_dayTime; out vec4 a_torchLight; +#include <__materials_vertex__> + void main() { + a_material = unpack_material(v_normal.w); + a_realnormal = v_normal.xyz * 2.0 - 1.0; a_modelpos = u_model * vec4(v_position, 1.0f); + // material shader may also modify a_realnormal + a_modelpos.xyz = apply_material_vertex(a_material, a_modelpos.xyz); vec3 pos3d = a_modelpos.xyz - u_cameraPos; - a_realnormal = v_normal.xyz * 2.0 - 1.0; a_normal = calc_screen_normal(a_realnormal); a_torchLight = vec4(calc_torch_light( @@ -37,7 +42,7 @@ void main() { a_fog = calc_fog(length(viewmodel * vec4(pos3d * FOG_POS_SCALE, 0.0)) / 256.0); #endif - a_emission = v_normal.w; + a_emission = unpack_emission(v_normal.w); vec4 viewmodelpos = u_view * a_modelpos; a_position = viewmodelpos.xyz; diff --git a/res/shaders/shadows.glslf b/res/shaders/shadows.glslf index 53d6f5d2f..ac153a07f 100644 --- a/res/shaders/shadows.glslf +++ b/res/shaders/shadows.glslf @@ -1,9 +1,13 @@ -in vec2 a_texCoord; +#include uniform sampler2D u_texture0; +#include <__materials_fragment__> + void main() { - vec4 tex_color = texture(u_texture0, a_texCoord); + vec4 tex_color = apply_material_fragment( + a_material, texture(u_texture0, a_texCoord) + ); if (tex_color.a < 0.5) { discard; } diff --git a/res/shaders/shadows.glslv b/res/shaders/shadows.glslv index 4d4094acf..a7d6bce6f 100644 --- a/res/shaders/shadows.glslv +++ b/res/shaders/shadows.glslv @@ -5,13 +5,18 @@ layout (location = 1) in vec2 v_texCoord; layout (location = 2) in vec4 v_light; layout (location = 3) in vec4 v_normal; -out vec2 a_texCoord; +// same vertex stage interface as world shaders for material shaders +#include -uniform mat4 u_model; -uniform mat4 u_proj; -uniform mat4 u_view; +uniform float u_dayTime; + +#include <__materials_vertex__> void main() { a_texCoord = v_texCoord; - gl_Position = u_proj * u_view * u_model * vec4(v_position, 1.0f); + a_material = unpack_material(v_normal.w); + a_realnormal = v_normal.xyz * 2.0 - 1.0; + a_modelpos = u_model * vec4(v_position, 1.0f); + a_modelpos.xyz = apply_material_vertex(a_material, a_modelpos.xyz); + gl_Position = u_proj * u_view * a_modelpos; } diff --git a/res/shaders/translucent.glslf b/res/shaders/translucent.glslf index e5a852ac6..6d901addc 100644 --- a/res/shaders/translucent.glslf +++ b/res/shaders/translucent.glslf @@ -14,8 +14,12 @@ uniform bool u_debugNormals; #include +#include <__materials_fragment__> + void main() { - vec4 texColor = texture(u_texture0, a_texCoord); + vec4 texColor = apply_material_fragment( + a_material, texture(u_texture0, a_texCoord) + ); float alpha = texColor.a; if (u_alphaClip) { if (alpha < 0.2f) diff --git a/res/shaders/translucent.glslv b/res/shaders/translucent.glslv index d9d8031c3..b51de3b1f 100644 --- a/res/shaders/translucent.glslv +++ b/res/shaders/translucent.glslv @@ -14,11 +14,15 @@ out vec4 a_torchLight; uniform float u_dayTime; +#include <__materials_vertex__> + void main() { + a_material = unpack_material(v_normal.w); + a_realnormal = v_normal.xyz * 2.0 - 1.0; a_modelpos = u_model * vec4(v_position, 1.0f); + a_modelpos.xyz = apply_material_vertex(a_material, a_modelpos.xyz); vec3 pos3d = a_modelpos.xyz - u_cameraPos; - a_realnormal = v_normal.xyz * 2.0 - 1.0; a_normal = calc_screen_normal(a_realnormal); a_torchLight = vec4(calc_torch_light( @@ -33,7 +37,7 @@ void main() { mat4 viewmodel = u_view * u_model; a_distance = length(viewmodel * vec4(pos3d, 0.0)); a_fog = calc_fog(length(viewmodel * vec4(pos3d * FOG_POS_SCALE, 0.0)) / 256.0); - a_emission = v_normal.w; + a_emission = unpack_emission(v_normal.w); vec4 viewmodelpos = u_view * a_modelpos; a_position = viewmodelpos.xyz; diff --git a/src/constants.hpp b/src/constants.hpp index 03900b9e3..68682b7a8 100644 --- a/src/constants.hpp +++ b/src/constants.hpp @@ -58,6 +58,8 @@ inline constexpr blockid_t BLOCK_VOID = std::numeric_limits::max(); inline constexpr itemid_t ITEM_VOID = std::numeric_limits::max(); /// @brief max number of block definitions possible inline constexpr blockid_t MAX_BLOCKS = BLOCK_VOID; +/// @brief max number of material shaders (7 bits in chunk vertex) +inline constexpr int MAX_BLOCK_MATERIAL_SHADERS = 127; /// @brief calculates a 1D array index from 3D array indices inline constexpr uint vox_index(uint x, uint y, uint z, uint w=CHUNK_W, uint d=CHUNK_D) { diff --git a/src/content/ContentBuilder.cpp b/src/content/ContentBuilder.cpp index 83836dfe3..13e7bbf88 100644 --- a/src/content/ContentBuilder.cpp +++ b/src/content/ContentBuilder.cpp @@ -1,5 +1,11 @@ #include "ContentBuilder.hpp" +#include +#include +#include + +#include "constants.hpp" + ContentBuilder::~ContentBuilder() = default; void ContentBuilder::add(std::unique_ptr pack) { @@ -13,7 +19,48 @@ BlockMaterial& ContentBuilder::createBlockMaterial(const std::string& id) { return material; } +/// @brief Assign indices to the material shaders (materials using the same +/// shader share its index) +static void index_material_shaders( + const UptrsMap& blockMaterials +) { + std::set shaders; + for (const auto& [name, material] : blockMaterials) { + if (!material->shader.empty()) { + shaders.insert(material->shader); + } + } + if (shaders.size() > MAX_BLOCK_MATERIAL_SHADERS) { + throw std::runtime_error( + "too many material shaders (max " + + std::to_string(MAX_BLOCK_MATERIAL_SHADERS) + ")" + ); + } + std::unordered_map indices; + for (const auto& shader : shaders) { + indices[shader] = indices.size() + 1; + } + for (const auto& [name, material] : blockMaterials) { + if (!material->shader.empty()) { + material->rt.shaderId = indices.at(material->shader); + } + } +} + +static uint8_t material_shader_id( + const UptrsMap& blockMaterials, + const std::string& material +) { + const auto& found = blockMaterials.find(material); + if (found == blockMaterials.end()) { + return 0; + } + return found->second->rt.shaderId; +} + std::unique_ptr ContentBuilder::build() { + index_material_shaders(blockMaterials); + std::vector blockDefsIndices; auto groups = std::make_unique(); for (const std::string& name : blocks.names) { @@ -50,6 +97,7 @@ std::unique_ptr ContentBuilder::build() { if (def.material.empty()) { defaults.at("block-material").get(def.material); } + def.rt.materialShader = material_shader_id(blockMaterials, def.material); if (def.rotatable) { for (uint i = 0; i < BlockRotProfile::MAX_COUNT; i++) { diff --git a/src/engine/AssetsManagement.cpp b/src/engine/AssetsManagement.cpp index 2233b2d44..18999eb64 100644 --- a/src/engine/AssetsManagement.cpp +++ b/src/engine/AssetsManagement.cpp @@ -7,6 +7,7 @@ #include "engine/Engine.hpp" #include "EnginePaths.hpp" #include "graphics/core/Shader.hpp" +#include "graphics/render/MaterialShaders.hpp" #include "graphics/render/ModelsGenerator.hpp" #include "graphics/ui/GUI.hpp" #include "logic/scripting/scripting.hpp" @@ -63,6 +64,9 @@ void AssetsManagement::loadAssets(Content* content) { loader.addDefaults(content); try { + material_shaders::build_headers( + *Shader::preprocessor, paths.resPaths, content + ); // no need // correct log messages order is more useful // todo: before setting to true, check if GLSLExtension thread safe diff --git a/src/graphics/core/PostEffect.cpp b/src/graphics/core/PostEffect.cpp index b0c34571d..c53c7e523 100644 --- a/src/graphics/core/PostEffect.cpp +++ b/src/graphics/core/PostEffect.cpp @@ -20,27 +20,22 @@ PostEffect::PostEffect( : advanced(advanced), shader(std::move(shader)), params(std::move(params)) { } -static void apply_uniform_value( - const PostEffect::Param& param, - Shader& shader, - const std::string& name -) { - using Type = PostEffect::Param::Type; - switch (param.type) { +void PostEffect::Param::apply(Shader& shader, const std::string& name) const { + switch (type) { case Type::INT: - shader.uniform1i(name, std::get(param.value)); + shader.uniform1i(name, std::get(value)); break; case Type::FLOAT: - shader.uniform1f(name, std::get(param.value)); + shader.uniform1f(name, std::get(value)); break; case Type::VEC2: - shader.uniform2f(name, std::get(param.value)); + shader.uniform2f(name, std::get(value)); break; case Type::VEC3: - shader.uniform3f(name, std::get(param.value)); + shader.uniform3f(name, std::get(value)); break; case Type::VEC4: - shader.uniform4f(name, std::get(param.value)); + shader.uniform4f(name, std::get(value)); break; default: assert(false); @@ -93,7 +88,7 @@ Shader& PostEffect::use() { } apply_uniform_array(param, *shader, name, found->second); } else { - apply_uniform_value(param, *shader, name); + param.apply(*shader, name); } param.dirty = false; } @@ -119,30 +114,49 @@ static void set_value(PostEffect::Param::Value& dst, const dv::value& value) { dst = vec; } +void PostEffect::Param::set(const dv::value& src) { + switch (type) { + case Type::INT: + value = static_cast(src.asInteger()); + break; + case Type::FLOAT: + value = static_cast(src.asNumber()); + break; + case Type::VEC2: + set_value<2>(value, src); + break; + case Type::VEC3: + set_value<3>(value, src); + break; + case Type::VEC4: + set_value<4>(value, src); + break; + } + dirty = true; +} + +dv::value PostEffect::Param::get() const { + switch (type) { + case Type::INT: + return std::get(value); + case Type::FLOAT: + return std::get(value); + case Type::VEC2: + return dv::to_value(std::get(value)); + case Type::VEC3: + return dv::to_value(std::get(value)); + case Type::VEC4: + return dv::to_value(std::get(value)); + } + return nullptr; +} + void PostEffect::setParam(const std::string& name, const dv::value& value) { const auto& found = params.find(name); if (found == params.end()) { return; } - auto& param = found->second; - switch (param.type) { - case Param::Type::INT: - param.value = static_cast(value.asInteger()); - break; - case Param::Type::FLOAT: - param.value = static_cast(value.asNumber()); - break; - case Param::Type::VEC2: - set_value<2>(param.value, value); - break; - case Param::Type::VEC3: - set_value<3>(param.value, value); - break; - case Param::Type::VEC4: - set_value<4>(param.value, value); - break; - } - param.dirty = true; + found->second.set(value); } void PostEffect::setArray(const std::string& name, std::vector&& values) { diff --git a/src/graphics/core/PostEffect.hpp b/src/graphics/core/PostEffect.hpp index a29fd404d..5f0da57c8 100644 --- a/src/graphics/core/PostEffect.hpp +++ b/src/graphics/core/PostEffect.hpp @@ -36,6 +36,15 @@ public: Param(); Param(Type type, Value defValue, bool array); + + /// @brief Upload current value to the shader uniform (non-array only) + void apply(Shader& shader, const std::string& name) const; + + /// @brief Set value from a number or a vector (list) value + void set(const dv::value& value); + + /// @brief Current value as a number or a vector (list) value + dv::value get() const; }; PostEffect( diff --git a/src/graphics/core/Shader.hpp b/src/graphics/core/Shader.hpp index 62698063b..4f2c550ba 100644 --- a/src/graphics/core/Shader.hpp +++ b/src/graphics/core/Shader.hpp @@ -32,6 +32,10 @@ public: Shader(uint id, Source&& vertexSource, Source&& fragmentSource); ~Shader(); + uint getId() const { + return id; + } + void use(); void uniformMatrix(const std::string&, const glm::mat4& matrix); void uniformMatrix(const std::string&, const glm::mat3& matrix); diff --git a/src/graphics/render/BlocksRenderer.cpp b/src/graphics/render/BlocksRenderer.cpp index 8b8a667eb..6970046c3 100644 --- a/src/graphics/render/BlocksRenderer.cpp +++ b/src/graphics/render/BlocksRenderer.cpp @@ -54,8 +54,11 @@ void BlocksRenderer::vertex( static_cast(normal.x * 127 + 128), static_cast(normal.y * 127 + 128), static_cast(normal.z * 127 + 128), - static_cast(emission * 255) - } + }, + static_cast( + (emission > 0.5f ? VERTEX_EMISSION_BIT : 0) | + (materialShader & VERTEX_MATERIAL_MASK) + ) }; } @@ -513,6 +516,7 @@ void BlocksRenderer::render( if (def.translucent) { continue; } + materialShader = def.rt.materialShader; const UVRegion texfaces[6] { cache.getRegion(id, variantId, 0, densePass), cache.getRegion(id, variantId, 1, densePass), @@ -584,6 +588,7 @@ SortingMeshData BlocksRenderer::renderTranslucent( if (!def.translucent) { continue; } + materialShader = def.rt.materialShader; const UVRegion texfaces[6] { cache.getRegion(id, variantId, 0, densePass), cache.getRegion(id, variantId, 1, densePass), diff --git a/src/graphics/render/BlocksRenderer.hpp b/src/graphics/render/BlocksRenderer.hpp index 819e79bca..8b5adc8fd 100644 --- a/src/graphics/render/BlocksRenderer.hpp +++ b/src/graphics/render/BlocksRenderer.hpp @@ -56,6 +56,8 @@ private: bool cancelled = false; bool densePass = false; bool denseRender = false; + /// @brief material shader index of the block being rendered + uint8_t materialShader = 0; AABB meshAABB {}; const Chunk* chunk = nullptr; const VoxelsRenderVolume* voxelsBuffer = nullptr; diff --git a/src/graphics/render/CloudsRenderer.cpp b/src/graphics/render/CloudsRenderer.cpp index bfc53e4bb..9577b4c1d 100755 --- a/src/graphics/render/CloudsRenderer.cpp +++ b/src/graphics/render/CloudsRenderer.cpp @@ -113,8 +113,8 @@ private: static_cast(normal.r * 127 + 128), static_cast(normal.g * 127 + 128), static_cast(normal.b * 127 + 128), - 255 }; + vert.flags = VERTEX_EMISSION_BIT; vert.color = { 0, 0, 0, static_cast((coord.y / 8.0f * 0.25f + 0.75f) * 255) }; diff --git a/src/graphics/render/MaterialShaders.cpp b/src/graphics/render/MaterialShaders.cpp new file mode 100644 index 000000000..76ca10fea --- /dev/null +++ b/src/graphics/render/MaterialShaders.cpp @@ -0,0 +1,317 @@ +#include "MaterialShaders.hpp" + +#include +#include +#include +#include +#include +#include + +#include "coders/GLSLExtension.hpp" +#include "content/Content.hpp" +#include "data/dv.hpp" +#include "debug/Logger.hpp" +#include "engine/EnginePaths.hpp" +#include "graphics/core/Shader.hpp" +#include "io/io.hpp" +#include "util/stringutil.hpp" +#include "voxels/Block.hpp" + +static debug::Logger logger("material-shaders"); + +namespace { + struct MaterialShader { + std::string name; + uint8_t index; + GLSLExtension::ParamsMap params; + std::vector materials; + + std::string prefix() const { + return "material_" + std::to_string(index) + "_"; + } + }; + + struct Stage { + const char* header; + const char* extension; + const char* function; + /// @brief hook argument type and name + const char* type; + const char* arg; + }; + + const Stage STAGES[] { + {material_shaders::VERTEX_HEADER, + ".glslv", + "material_vertex", + "vec3", + "position"}, + {material_shaders::FRAGMENT_HEADER, + ".glslf", + "material_fragment", + "vec4", + "color"}, + }; + + /// @brief shader index -> shader + std::map shaders; + /// @brief material name -> shader index + std::unordered_map materials; + /// @brief source index of the generated dispatch code in GLSL errors + const int GENERATED_SOURCE = 128; + /// @brief incremented on any params change + uint64_t paramsVersion = 1; + + struct UploadState { + uint program; + uint64_t version; + }; + /// @brief shader -> uploaded params state + std::unordered_map uploaded; +} + +static bool is_identifier_char(char c) { + return std::isalnum(static_cast(c)) || c == '_'; +} + +/// @brief Remove comments from GLSL code +static std::string strip_comments(const std::string& code) { + std::string out; + out.reserve(code.size()); + for (size_t i = 0; i < code.size(); i++) { + if (code.compare(i, 2, "//") == 0) { + i = code.find('\n', i); + if (i == std::string::npos) { + break; + } + } else if (code.compare(i, 2, "/*") == 0) { + i = code.find("*/", i + 2); + if (i == std::string::npos) { + break; + } + i++; + continue; + } + out += code[i]; + } + return out; +} + +/// @brief Check if the function is defined in the code +static bool has_function(const std::string& source, const std::string& name) { + std::string code = strip_comments(source); + size_t pos = 0; + while ((pos = code.find(name, pos)) != std::string::npos) { + size_t end = pos + name.size(); + bool word = (pos == 0 || !is_identifier_char(code[pos - 1])) && + (end == code.size() || !is_identifier_char(code[end])); + pos = end; + if (!word) { + continue; + } + while (end < code.size() && + std::isspace(static_cast(code[end]))) { + end++; + } + if (end < code.size() && code[end] == '(') { + return true; + } + } + return false; +} + +/// @brief Include the material shader stage renaming the hook function and +/// the params to unique names (GLSL has no namespaces) +/// @return true if the stage file exists +static bool include_shader( + GLSLExtension& preprocessor, + const ResPaths& paths, + MaterialShader& shader, + const Stage& stage, + std::stringstream& ss +) { + io::path file = + paths.find("shaders/materials/" + shader.name + stage.extension); + if (!io::exists(file)) { + return false; + } + auto result = preprocessor.process(file, io::read_string(file), true, {}); + std::string prefix = shader.prefix(); + + ss << "// " << shader.name << " (" << file.string() << ")\n"; + // compilation errors refer to the shader by its index: : + ss << "#line 1 " << static_cast(shader.index) << "\n"; + ss << "#define " << stage.function << " " << prefix << stage.function + << "\n"; + for (const auto& [name, param] : result.params) { + if (param.array) { + throw std::runtime_error( + file.string() + ": array params are not supported" + ); + } + if (name[0] == '_') { + throw std::runtime_error( + file.string() + + ": param name must not start with '_': " + util::quote(name) + ); + } + const auto& found = shader.params.find(name); + if (found != shader.params.end() && found->second.type != param.type) { + throw std::runtime_error( + file.string() + ": param " + util::quote(name) + + " type differs from the other stage" + ); + } + ss << "#define " << name << " " << prefix << name << "\n"; + shader.params[name] = param; + } + ss << result.code << "\n"; + ss << "#undef " << stage.function << "\n"; + for (const auto& entry : result.params) { + ss << "#undef " << entry.first << "\n"; + } + if (!has_function(result.code, stage.function)) { + throw std::runtime_error( + file.string() + ": function " + stage.function + " is not defined" + ); + } + return true; +} + +static void build_stage_header( + GLSLExtension& preprocessor, const ResPaths& paths, const Stage& stage +) { + std::stringstream ss; + std::vector hooked; + + for (auto& [index, shader] : shaders) { + if (include_shader(preprocessor, paths, shader, stage, ss)) { + hooked.push_back(&shader); + } + } + // generated code source index, then back to the host shader source + ss << "#line 1 " << GENERATED_SOURCE << "\n"; + ss << stage.type << " apply_" << stage.function << "(int material, " + << stage.type << " " << stage.arg << ") {\n"; + if (!hooked.empty()) { + ss << " switch (material) {\n"; + for (const auto* shader : hooked) { + ss << " case " << static_cast(shader->index) + << ": return " << shader->prefix() << stage.function << "(" + << stage.arg << ");\n"; + } + ss << " }\n"; + } + ss << " return " << stage.arg << ";\n"; + ss << "}\n"; + ss << "#line 1 0\n"; + + preprocessor.addHeader(stage.header, {ss.str(), {}}); +} + +void material_shaders::build_headers( + GLSLExtension& preprocessor, const ResPaths& paths, const Content* content +) { + shaders.clear(); + materials.clear(); + uploaded.clear(); + paramsVersion++; + + if (content) { + for (const auto& [name, material] : content->getBlockMaterials()) { + uint8_t index = material->rt.shaderId; + if (index == 0) { + continue; + } + auto& shader = shaders[index]; + shader.name = material->shader; + shader.index = index; + shader.materials.push_back(name); + materials[name] = index; + } + } + for (const auto& stage : STAGES) { + build_stage_header(preprocessor, paths, stage); + } + for (const auto& [index, shader] : shaders) { + bool found = false; + for (const auto& stage : STAGES) { + found |= io::exists( + paths.find("shaders/materials/" + shader.name + stage.extension) + ); + } + if (!found) { + logger.warning() << "material shader " << util::quote(shader.name) + << " files not found"; + } + logger.info() << "material shader " << static_cast(index) << ": " + << shader.name << " (" + << util::join(shader.materials, ',') << ")"; + } +} + +void material_shaders::setup_shader(Shader& shader) { + auto& state = uploaded[&shader]; + if (state.program == shader.getId() && state.version == paramsVersion) { + return; + } + for (const auto& [index, materialShader] : shaders) { + for (const auto& [name, param] : materialShader.params) { + param.apply(shader, materialShader.prefix() + name); + } + } + state = {shader.getId(), paramsVersion}; +} + +static MaterialShader& require_shader(const std::string& material) { + const auto& found = materials.find(material); + if (found == materials.end()) { + throw std::runtime_error( + "material " + util::quote(material) + " has no shader" + ); + } + return shaders.at(found->second); +} + +void material_shaders::set_params( + const std::string& material, const dv::value& params +) { + auto& shader = require_shader(material); + // convert all values before updating + GLSLExtension::ParamsMap updated; + for (const auto& [name, value] : params.asObject()) { + const auto& found = shader.params.find(name); + if (found == shader.params.end()) { + throw std::runtime_error( + "material " + util::quote(material) + " has no param " + + util::quote(name) + ); + } + auto param = found->second; + try { + param.set(value); + } catch (const std::exception& err) { + throw std::runtime_error( + "material " + util::quote(material) + " param " + + util::quote(name) + ": " + err.what() + ); + } + updated[name] = std::move(param); + } + for (auto& [name, param] : updated) { + shader.params[name] = param; + } + paramsVersion++; +} + +dv::value material_shaders::get_params(const std::string& material) { + const auto& found = materials.find(material); + if (found == materials.end()) { + return nullptr; + } + auto table = dv::object(); + for (const auto& [name, param] : shaders.at(found->second).params) { + table[name] = param.get(); + } + return table; +} diff --git a/src/graphics/render/MaterialShaders.hpp b/src/graphics/render/MaterialShaders.hpp new file mode 100644 index 000000000..eebc244d2 --- /dev/null +++ b/src/graphics/render/MaterialShaders.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include + +#include "data/dv_fwd.hpp" + +class Content; +class Shader; +class ResPaths; +class GLSLExtension; + +/// @brief Material shaders (shaders/materials/.glslv, .glslf) are +/// included into the world shaders (main, translucent, shadows, entity) +namespace material_shaders { + inline constexpr const char* VERTEX_HEADER = "__materials_vertex__"; + inline constexpr const char* FRAGMENT_HEADER = "__materials_fragment__"; + + /// @brief Generate the world shaders headers from the material shaders. + /// Must be called before the world shaders (re)compilation. + /// @param content loaded content (nullable) + void build_headers( + GLSLExtension& preprocessor, + const ResPaths& paths, + const Content* content + ); + + /// @brief Upload the material shaders params to the shader + /// if changed since the previous call for this shader program + void setup_shader(Shader& shader); + + /// @brief Set the material shader params values ('param' directives) + /// @param material material name (pack:name) + /// @throws std::runtime_error if material or param does not exist + void set_params(const std::string& material, const dv::value& params); + + /// @brief Get the material shader params current values + /// @return table of param name -> value or null if material has no shader + dv::value get_params(const std::string& material); +} diff --git a/src/graphics/render/WorldRenderer.cpp b/src/graphics/render/WorldRenderer.cpp index 27dd4dcdd..efda7f7ae 100644 --- a/src/graphics/render/WorldRenderer.cpp +++ b/src/graphics/render/WorldRenderer.cpp @@ -35,6 +35,7 @@ #include "graphics/core/Shadows.hpp" #include "graphics/core/Cubemap.hpp" #include "graphics/core/Texture.hpp" +#include "graphics/render/MaterialShaders.hpp" #include "items/Inventory.hpp" #include "items/ItemDef.hpp" #include "items/ItemStack.hpp" @@ -200,6 +201,7 @@ void WorldRenderer::setupWorldShader( shader.uniform1f("u_timer", timer); shader.uniform1f("u_gamma", settings.graphics.gamma.get()); + material_shaders::setup_shader(shader); shader.uniform1f("u_fogFactor", fogFactor); shader.uniform1f("u_fogCurve", settings.graphics.fogCurve.get()); shader.uniform1i("u_debugLights", lightsDebug); diff --git a/src/graphics/render/commons.hpp b/src/graphics/render/commons.hpp index 52f7d5a95..864599052 100644 --- a/src/graphics/render/commons.hpp +++ b/src/graphics/render/commons.hpp @@ -5,18 +5,28 @@ #include "maths/aabb.hpp" #include "util/Buffer.hpp" +#include #include #include #include #include #include +/// @brief ChunkVertex::flags bit: vertex is not affected by lighting +inline constexpr uint8_t VERTEX_EMISSION_BIT = 0x80; +/// @brief ChunkVertex::flags mask: material shader index +inline constexpr uint8_t VERTEX_MATERIAL_MASK = 0x7F; +static_assert(VERTEX_MATERIAL_MASK == MAX_BLOCK_MATERIAL_SHADERS); + /// @brief Chunk mesh vertex format struct ChunkVertex { glm::vec3 position; glm::vec2 uv; std::array color; - std::array normal; + std::array normal; + /// @brief emission flag and material shader index, fourth component + /// of the normal attribute (see VERTEX_EMISSION_BIT, VERTEX_MATERIAL_MASK) + uint8_t flags; static constexpr VertexAttribute ATTRIBUTES[] = { {VertexAttribute::Type::FLOAT, false, 3}, @@ -25,6 +35,11 @@ struct ChunkVertex { {VertexAttribute::Type::UNSIGNED_BYTE, true, 4}, {{}, 0}}; }; +// normal and flags are passed as one 4-components vertex attribute +static_assert(sizeof(ChunkVertex) == 28); +static_assert( + offsetof(ChunkVertex, flags) == offsetof(ChunkVertex, normal) + 3 +); template class Mesh; diff --git a/src/voxels/Block.cpp b/src/voxels/Block.cpp index 8e28c54c4..6ae490d2e 100644 --- a/src/voxels/Block.cpp +++ b/src/voxels/Block.cpp @@ -25,7 +25,8 @@ dv::value BlockMaterial::serialize() const { {"place-sound", placeSound}, {"break-sound", breakSound}, {"hit-sound", hitSound}, - {"sound-absorption", soundAbsorption} + {"sound-absorption", soundAbsorption}, + {"shader", shader} }); } @@ -36,6 +37,7 @@ void BlockMaterial::deserialize(const dv::value& src) { src.at("break-sound").get(breakSound); src.at("hit-sound").get(hitSound); src.at("sound-absorption").get(soundAbsorption); + src.at("shader").get(shader); } CoordSystem::CoordSystem(glm::ivec3 axisX, glm::ivec3 axisY, glm::ivec3 axisZ) diff --git a/src/voxels/Block.hpp b/src/voxels/Block.hpp index a5185106b..757a7b7a8 100644 --- a/src/voxels/Block.hpp +++ b/src/voxels/Block.hpp @@ -166,6 +166,13 @@ struct BlockMaterial : Serializable { std::string breakSound; std::string hitSound; float soundAbsorption = 0.5f; + /// @brief material shader name (shaders/materials/), may be empty + std::string shader; + + struct { + /// @brief material shader index (0 - no shader) + uint8_t shaderId = 0; + } rt {}; dv::value toTable() const; // for compatibility dv::value serialize() const override; @@ -322,6 +329,9 @@ public: blockid_t surfaceReplacement = 0; + /// @brief block material shader index (see BlockMaterial::rt.shaderId) + uint8_t materialShader = 0; + std::set tags; BlockFuncNamesCache eventNames;