add block material shaders

- 'shader' property of block materials: shaders/materials/<name>.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
This commit is contained in:
lookibed 2026-10-02 06:33:01 +03:00
parent f90750c066
commit 81489a13cf
33 changed files with 761 additions and 53 deletions

97
doc/en/block-materials.md Normal file
View file

@ -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

View file

@ -81,7 +81,7 @@ until the block is destroyed or the camera moves away a certain distance.
### Material - *material* ### Material - *material*
Defines the name of the block's material in the format `pack:material_name`, which affects the selection of block interaction sounds. 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 ## Variants

View file

@ -89,6 +89,7 @@ Don't be intimidated by the following text, as a minimal pack only requires `pac
- `textures/` - Textures - `textures/` - Textures
- `shaders/` - Shaders - `shaders/` - Shaders
- `effects/` - Post-processing effects - `effects/` - Post-processing effects
- `materials/` - Block material shaders
- `sounds/` - Sounds and Music - `sounds/` - Sounds and Music
- `texts/` - Localization files - `texts/` - Localization files
- GUI: - GUI:

View file

@ -7,6 +7,7 @@
- [Assets preloading](assets-preload.md) - [Assets preloading](assets-preload.md)
- [Audio](audio.md) - [Audio](audio.md)
- [Block materials](block-materials.md)
- [Block models](block-models.md) - [Block models](block-models.md)
- [Block properties](block-properties.md) - [Block properties](block-properties.md)
- [Console](console.md) - [Console](console.md)

97
doc/ru/block-materials.md Normal file
View file

@ -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`

View file

@ -82,7 +82,7 @@
### Материал - *material* ### Материал - *material*
Определяет имя материала блока в формате `пак:имя_материала`, что влияет на выбор звуков взаимодействия с блоком. Определяет имя материала блока в формате `пак:имя_материала`, что влияет на выбор звуков взаимодействия с блоком.
Определения материалов расположены в /block_materials. Определения материалов расположены в /block_materials (см. [материалы блоков](block-materials.md)).
## Варианты ## Варианты

View file

@ -91,6 +91,7 @@ print(file.read("пак:package.json")) -- выведет содержимое p
- `textures/` - текстуры - `textures/` - текстуры
- `shaders/` - шейдеры - `shaders/` - шейдеры
- `effects/` - эффекты пост-обработки - `effects/` - эффекты пост-обработки
- `materials/` - шейдеры материалов блоков
- `sounds/` - звуки и музыка - `sounds/` - звуки и музыка
- `texts/` - файлы локализации - `texts/` - файлы локализации
- GUI: - GUI:

View file

@ -11,6 +11,7 @@
- [Движок генерации мира](world-generator.md) - [Движок генерации мира](world-generator.md)
- [Консоль](console.md) - [Консоль](console.md)
- [Контент‐паки](content-packs.md) - [Контент‐паки](content-packs.md)
- [Материалы блоков](block-materials.md)
- [Модели блоков](block-models.md) - [Модели блоков](block-models.md)
- [Предзагрузка ассетов](assets-preload.md) - [Предзагрузка ассетов](assets-preload.md)
- [Рекомендации по использованию движка](engine-use-recommendations.md) - [Рекомендации по использованию движка](engine-use-recommendations.md)

View file

@ -41,7 +41,7 @@ void main() {
a_fog = calc_fog(length(viewmodel * vec4(pos3d * FOG_POS_SCALE, 0.0)) / 256.0); a_fog = calc_fog(length(viewmodel * vec4(pos3d * FOG_POS_SCALE, 0.0)) / 256.0);
#endif #endif
a_emission = v_normal.w; a_emission = unpack_emission(v_normal.w);
vec4 viewmodelpos = u_view * a_modelpos; vec4 viewmodelpos = u_view * a_modelpos;
a_position = viewmodelpos.xyz; a_position = viewmodelpos.xyz;

View file

@ -8,4 +8,13 @@ vec3 apply_planet_curvature(vec3 modelPos, vec3 pos3d) {
return modelPos; 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_ #endif // COMMONS_GLSL_

View file

@ -11,6 +11,7 @@ in vec3 a_realnormal;
in vec3 a_skyLight; in vec3 a_skyLight;
in vec4 a_modelpos; in vec4 a_modelpos;
in float a_emission; in float a_emission;
flat in int a_material;
#include <world_uniforms> #include <world_uniforms>

View file

@ -11,6 +11,7 @@ out vec3 a_realnormal;
out vec3 a_skyLight; out vec3 a_skyLight;
out vec4 a_modelpos; out vec4 a_modelpos;
out float a_emission; out float a_emission;
flat out int a_material;
#include <world_uniforms> #include <world_uniforms>

View file

@ -15,8 +15,12 @@ uniform bool u_alphaClip;
uniform bool u_debugLights; uniform bool u_debugLights;
uniform bool u_debugNormals; uniform bool u_debugNormals;
#include <__materials_fragment__>
void main() { 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; float alpha = texColor.a;
if (u_alphaClip) { if (u_alphaClip) {
if (alpha < 0.2f) if (alpha < 0.2f)

View file

@ -14,11 +14,16 @@ uniform float u_dayTime;
out vec4 a_torchLight; out vec4 a_torchLight;
#include <__materials_vertex__>
void main() { 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 = 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; vec3 pos3d = a_modelpos.xyz - u_cameraPos;
a_realnormal = v_normal.xyz * 2.0 - 1.0;
a_normal = calc_screen_normal(a_realnormal); a_normal = calc_screen_normal(a_realnormal);
a_torchLight = vec4(calc_torch_light( 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); a_fog = calc_fog(length(viewmodel * vec4(pos3d * FOG_POS_SCALE, 0.0)) / 256.0);
#endif #endif
a_emission = v_normal.w; a_emission = unpack_emission(v_normal.w);
vec4 viewmodelpos = u_view * a_modelpos; vec4 viewmodelpos = u_view * a_modelpos;
a_position = viewmodelpos.xyz; a_position = viewmodelpos.xyz;

View file

@ -1,9 +1,13 @@
in vec2 a_texCoord; #include <world_fragment_header>
uniform sampler2D u_texture0; uniform sampler2D u_texture0;
#include <__materials_fragment__>
void main() { 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) { if (tex_color.a < 0.5) {
discard; discard;
} }

View file

@ -5,13 +5,18 @@ layout (location = 1) in vec2 v_texCoord;
layout (location = 2) in vec4 v_light; layout (location = 2) in vec4 v_light;
layout (location = 3) in vec4 v_normal; layout (location = 3) in vec4 v_normal;
out vec2 a_texCoord; // same vertex stage interface as world shaders for material shaders
#include <world_vertex_header>
uniform mat4 u_model; uniform float u_dayTime;
uniform mat4 u_proj;
uniform mat4 u_view; #include <__materials_vertex__>
void main() { void main() {
a_texCoord = v_texCoord; 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;
} }

View file

@ -14,8 +14,12 @@ uniform bool u_debugNormals;
#include <shadows> #include <shadows>
#include <__materials_fragment__>
void main() { 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; float alpha = texColor.a;
if (u_alphaClip) { if (u_alphaClip) {
if (alpha < 0.2f) if (alpha < 0.2f)

View file

@ -14,11 +14,15 @@ out vec4 a_torchLight;
uniform float u_dayTime; uniform float u_dayTime;
#include <__materials_vertex__>
void main() { 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 = 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; vec3 pos3d = a_modelpos.xyz - u_cameraPos;
a_realnormal = v_normal.xyz * 2.0 - 1.0;
a_normal = calc_screen_normal(a_realnormal); a_normal = calc_screen_normal(a_realnormal);
a_torchLight = vec4(calc_torch_light( a_torchLight = vec4(calc_torch_light(
@ -33,7 +37,7 @@ void main() {
mat4 viewmodel = u_view * u_model; mat4 viewmodel = u_view * u_model;
a_distance = length(viewmodel * vec4(pos3d, 0.0)); a_distance = length(viewmodel * vec4(pos3d, 0.0));
a_fog = calc_fog(length(viewmodel * vec4(pos3d * FOG_POS_SCALE, 0.0)) / 256.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; vec4 viewmodelpos = u_view * a_modelpos;
a_position = viewmodelpos.xyz; a_position = viewmodelpos.xyz;

View file

@ -58,6 +58,8 @@ inline constexpr blockid_t BLOCK_VOID = std::numeric_limits<blockid_t>::max();
inline constexpr itemid_t ITEM_VOID = std::numeric_limits<itemid_t>::max(); inline constexpr itemid_t ITEM_VOID = std::numeric_limits<itemid_t>::max();
/// @brief max number of block definitions possible /// @brief max number of block definitions possible
inline constexpr blockid_t MAX_BLOCKS = BLOCK_VOID; 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 /// @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) { inline constexpr uint vox_index(uint x, uint y, uint z, uint w=CHUNK_W, uint d=CHUNK_D) {

View file

@ -1,5 +1,11 @@
#include "ContentBuilder.hpp" #include "ContentBuilder.hpp"
#include <set>
#include <stdexcept>
#include <unordered_map>
#include "constants.hpp"
ContentBuilder::~ContentBuilder() = default; ContentBuilder::~ContentBuilder() = default;
void ContentBuilder::add(std::unique_ptr<ContentPackRuntime> pack) { void ContentBuilder::add(std::unique_ptr<ContentPackRuntime> pack) {
@ -13,7 +19,48 @@ BlockMaterial& ContentBuilder::createBlockMaterial(const std::string& id) {
return material; return material;
} }
/// @brief Assign indices to the material shaders (materials using the same
/// shader share its index)
static void index_material_shaders(
const UptrsMap<std::string, BlockMaterial>& blockMaterials
) {
std::set<std::string> 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<std::string, uint8_t> 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<std::string, BlockMaterial>& 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<Content> ContentBuilder::build() { std::unique_ptr<Content> ContentBuilder::build() {
index_material_shaders(blockMaterials);
std::vector<Block*> blockDefsIndices; std::vector<Block*> blockDefsIndices;
auto groups = std::make_unique<DrawGroups>(); auto groups = std::make_unique<DrawGroups>();
for (const std::string& name : blocks.names) { for (const std::string& name : blocks.names) {
@ -50,6 +97,7 @@ std::unique_ptr<Content> ContentBuilder::build() {
if (def.material.empty()) { if (def.material.empty()) {
defaults.at("block-material").get(def.material); defaults.at("block-material").get(def.material);
} }
def.rt.materialShader = material_shader_id(blockMaterials, def.material);
if (def.rotatable) { if (def.rotatable) {
for (uint i = 0; i < BlockRotProfile::MAX_COUNT; i++) { for (uint i = 0; i < BlockRotProfile::MAX_COUNT; i++) {

View file

@ -7,6 +7,7 @@
#include "engine/Engine.hpp" #include "engine/Engine.hpp"
#include "EnginePaths.hpp" #include "EnginePaths.hpp"
#include "graphics/core/Shader.hpp" #include "graphics/core/Shader.hpp"
#include "graphics/render/MaterialShaders.hpp"
#include "graphics/render/ModelsGenerator.hpp" #include "graphics/render/ModelsGenerator.hpp"
#include "graphics/ui/GUI.hpp" #include "graphics/ui/GUI.hpp"
#include "logic/scripting/scripting.hpp" #include "logic/scripting/scripting.hpp"
@ -63,6 +64,9 @@ void AssetsManagement::loadAssets(Content* content) {
loader.addDefaults(content); loader.addDefaults(content);
try { try {
material_shaders::build_headers(
*Shader::preprocessor, paths.resPaths, content
);
// no need // no need
// correct log messages order is more useful // correct log messages order is more useful
// todo: before setting to true, check if GLSLExtension thread safe // todo: before setting to true, check if GLSLExtension thread safe

View file

@ -20,27 +20,22 @@ PostEffect::PostEffect(
: advanced(advanced), shader(std::move(shader)), params(std::move(params)) { : advanced(advanced), shader(std::move(shader)), params(std::move(params)) {
} }
static void apply_uniform_value( void PostEffect::Param::apply(Shader& shader, const std::string& name) const {
const PostEffect::Param& param, switch (type) {
Shader& shader,
const std::string& name
) {
using Type = PostEffect::Param::Type;
switch (param.type) {
case Type::INT: case Type::INT:
shader.uniform1i(name, std::get<int>(param.value)); shader.uniform1i(name, std::get<int>(value));
break; break;
case Type::FLOAT: case Type::FLOAT:
shader.uniform1f(name, std::get<float>(param.value)); shader.uniform1f(name, std::get<float>(value));
break; break;
case Type::VEC2: case Type::VEC2:
shader.uniform2f(name, std::get<glm::vec2>(param.value)); shader.uniform2f(name, std::get<glm::vec2>(value));
break; break;
case Type::VEC3: case Type::VEC3:
shader.uniform3f(name, std::get<glm::vec3>(param.value)); shader.uniform3f(name, std::get<glm::vec3>(value));
break; break;
case Type::VEC4: case Type::VEC4:
shader.uniform4f(name, std::get<glm::vec4>(param.value)); shader.uniform4f(name, std::get<glm::vec4>(value));
break; break;
default: default:
assert(false); assert(false);
@ -93,7 +88,7 @@ Shader& PostEffect::use() {
} }
apply_uniform_array(param, *shader, name, found->second); apply_uniform_array(param, *shader, name, found->second);
} else { } else {
apply_uniform_value(param, *shader, name); param.apply(*shader, name);
} }
param.dirty = false; param.dirty = false;
} }
@ -119,30 +114,49 @@ static void set_value(PostEffect::Param::Value& dst, const dv::value& value) {
dst = vec; dst = vec;
} }
void PostEffect::Param::set(const dv::value& src) {
switch (type) {
case Type::INT:
value = static_cast<int>(src.asInteger());
break;
case Type::FLOAT:
value = static_cast<float>(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<int>(value);
case Type::FLOAT:
return std::get<float>(value);
case Type::VEC2:
return dv::to_value(std::get<glm::vec2>(value));
case Type::VEC3:
return dv::to_value(std::get<glm::vec3>(value));
case Type::VEC4:
return dv::to_value(std::get<glm::vec4>(value));
}
return nullptr;
}
void PostEffect::setParam(const std::string& name, const dv::value& value) { void PostEffect::setParam(const std::string& name, const dv::value& value) {
const auto& found = params.find(name); const auto& found = params.find(name);
if (found == params.end()) { if (found == params.end()) {
return; return;
} }
auto& param = found->second; found->second.set(value);
switch (param.type) {
case Param::Type::INT:
param.value = static_cast<int>(value.asInteger());
break;
case Param::Type::FLOAT:
param.value = static_cast<float>(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;
} }
void PostEffect::setArray(const std::string& name, std::vector<ubyte>&& values) { void PostEffect::setArray(const std::string& name, std::vector<ubyte>&& values) {

View file

@ -36,6 +36,15 @@ public:
Param(); Param();
Param(Type type, Value defValue, bool array); 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( PostEffect(

View file

@ -32,6 +32,10 @@ public:
Shader(uint id, Source&& vertexSource, Source&& fragmentSource); Shader(uint id, Source&& vertexSource, Source&& fragmentSource);
~Shader(); ~Shader();
uint getId() const {
return id;
}
void use(); void use();
void uniformMatrix(const std::string&, const glm::mat4& matrix); void uniformMatrix(const std::string&, const glm::mat4& matrix);
void uniformMatrix(const std::string&, const glm::mat3& matrix); void uniformMatrix(const std::string&, const glm::mat3& matrix);

View file

@ -54,8 +54,11 @@ void BlocksRenderer::vertex(
static_cast<uint8_t>(normal.x * 127 + 128), static_cast<uint8_t>(normal.x * 127 + 128),
static_cast<uint8_t>(normal.y * 127 + 128), static_cast<uint8_t>(normal.y * 127 + 128),
static_cast<uint8_t>(normal.z * 127 + 128), static_cast<uint8_t>(normal.z * 127 + 128),
static_cast<uint8_t>(emission * 255) },
} static_cast<uint8_t>(
(emission > 0.5f ? VERTEX_EMISSION_BIT : 0) |
(materialShader & VERTEX_MATERIAL_MASK)
)
}; };
} }
@ -513,6 +516,7 @@ void BlocksRenderer::render(
if (def.translucent) { if (def.translucent) {
continue; continue;
} }
materialShader = def.rt.materialShader;
const UVRegion texfaces[6] { const UVRegion texfaces[6] {
cache.getRegion(id, variantId, 0, densePass), cache.getRegion(id, variantId, 0, densePass),
cache.getRegion(id, variantId, 1, densePass), cache.getRegion(id, variantId, 1, densePass),
@ -584,6 +588,7 @@ SortingMeshData BlocksRenderer::renderTranslucent(
if (!def.translucent) { if (!def.translucent) {
continue; continue;
} }
materialShader = def.rt.materialShader;
const UVRegion texfaces[6] { const UVRegion texfaces[6] {
cache.getRegion(id, variantId, 0, densePass), cache.getRegion(id, variantId, 0, densePass),
cache.getRegion(id, variantId, 1, densePass), cache.getRegion(id, variantId, 1, densePass),

View file

@ -56,6 +56,8 @@ private:
bool cancelled = false; bool cancelled = false;
bool densePass = false; bool densePass = false;
bool denseRender = false; bool denseRender = false;
/// @brief material shader index of the block being rendered
uint8_t materialShader = 0;
AABB meshAABB {}; AABB meshAABB {};
const Chunk* chunk = nullptr; const Chunk* chunk = nullptr;
const VoxelsRenderVolume* voxelsBuffer = nullptr; const VoxelsRenderVolume* voxelsBuffer = nullptr;

View file

@ -113,8 +113,8 @@ private:
static_cast<uint8_t>(normal.r * 127 + 128), static_cast<uint8_t>(normal.r * 127 + 128),
static_cast<uint8_t>(normal.g * 127 + 128), static_cast<uint8_t>(normal.g * 127 + 128),
static_cast<uint8_t>(normal.b * 127 + 128), static_cast<uint8_t>(normal.b * 127 + 128),
255
}; };
vert.flags = VERTEX_EMISSION_BIT;
vert.color = { vert.color = {
0, 0, 0, static_cast<uint8_t>((coord.y / 8.0f * 0.25f + 0.75f) * 255) 0, 0, 0, static_cast<uint8_t>((coord.y / 8.0f * 0.25f + 0.75f) * 255)
}; };

View file

@ -0,0 +1,317 @@
#include "MaterialShaders.hpp"
#include <cctype>
#include <map>
#include <sstream>
#include <stdexcept>
#include <unordered_map>
#include <vector>
#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<std::string> 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<uint8_t, MaterialShader> shaders;
/// @brief material name -> shader index
std::unordered_map<std::string, uint8_t> 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<const Shader*, UploadState> uploaded;
}
static bool is_identifier_char(char c) {
return std::isalnum(static_cast<unsigned char>(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<unsigned char>(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: <index>:<line>
ss << "#line 1 " << static_cast<int>(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<const MaterialShader*> 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<int>(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<int>(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;
}

View file

@ -0,0 +1,39 @@
#pragma once
#include <string>
#include "data/dv_fwd.hpp"
class Content;
class Shader;
class ResPaths;
class GLSLExtension;
/// @brief Material shaders (shaders/materials/<name>.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);
}

View file

@ -35,6 +35,7 @@
#include "graphics/core/Shadows.hpp" #include "graphics/core/Shadows.hpp"
#include "graphics/core/Cubemap.hpp" #include "graphics/core/Cubemap.hpp"
#include "graphics/core/Texture.hpp" #include "graphics/core/Texture.hpp"
#include "graphics/render/MaterialShaders.hpp"
#include "items/Inventory.hpp" #include "items/Inventory.hpp"
#include "items/ItemDef.hpp" #include "items/ItemDef.hpp"
#include "items/ItemStack.hpp" #include "items/ItemStack.hpp"
@ -200,6 +201,7 @@ void WorldRenderer::setupWorldShader(
shader.uniform1f("u_timer", timer); shader.uniform1f("u_timer", timer);
shader.uniform1f("u_gamma", settings.graphics.gamma.get()); shader.uniform1f("u_gamma", settings.graphics.gamma.get());
material_shaders::setup_shader(shader);
shader.uniform1f("u_fogFactor", fogFactor); shader.uniform1f("u_fogFactor", fogFactor);
shader.uniform1f("u_fogCurve", settings.graphics.fogCurve.get()); shader.uniform1f("u_fogCurve", settings.graphics.fogCurve.get());
shader.uniform1i("u_debugLights", lightsDebug); shader.uniform1i("u_debugLights", lightsDebug);

View file

@ -5,18 +5,28 @@
#include "maths/aabb.hpp" #include "maths/aabb.hpp"
#include "util/Buffer.hpp" #include "util/Buffer.hpp"
#include <cstddef>
#include <vector> #include <vector>
#include <array> #include <array>
#include <memory> #include <memory>
#include <glm/vec2.hpp> #include <glm/vec2.hpp>
#include <glm/vec3.hpp> #include <glm/vec3.hpp>
/// @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 /// @brief Chunk mesh vertex format
struct ChunkVertex { struct ChunkVertex {
glm::vec3 position; glm::vec3 position;
glm::vec2 uv; glm::vec2 uv;
std::array<uint8_t, 4> color; std::array<uint8_t, 4> color;
std::array<uint8_t, 4> normal; std::array<uint8_t, 3> 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[] = { static constexpr VertexAttribute ATTRIBUTES[] = {
{VertexAttribute::Type::FLOAT, false, 3}, {VertexAttribute::Type::FLOAT, false, 3},
@ -25,6 +35,11 @@ struct ChunkVertex {
{VertexAttribute::Type::UNSIGNED_BYTE, true, 4}, {VertexAttribute::Type::UNSIGNED_BYTE, true, 4},
{{}, 0}}; {{}, 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<typename VertexStructure> template<typename VertexStructure>
class Mesh; class Mesh;

View file

@ -25,7 +25,8 @@ dv::value BlockMaterial::serialize() const {
{"place-sound", placeSound}, {"place-sound", placeSound},
{"break-sound", breakSound}, {"break-sound", breakSound},
{"hit-sound", hitSound}, {"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("break-sound").get(breakSound);
src.at("hit-sound").get(hitSound); src.at("hit-sound").get(hitSound);
src.at("sound-absorption").get(soundAbsorption); src.at("sound-absorption").get(soundAbsorption);
src.at("shader").get(shader);
} }
CoordSystem::CoordSystem(glm::ivec3 axisX, glm::ivec3 axisY, glm::ivec3 axisZ) CoordSystem::CoordSystem(glm::ivec3 axisX, glm::ivec3 axisY, glm::ivec3 axisZ)

View file

@ -166,6 +166,13 @@ struct BlockMaterial : Serializable {
std::string breakSound; std::string breakSound;
std::string hitSound; std::string hitSound;
float soundAbsorption = 0.5f; float soundAbsorption = 0.5f;
/// @brief material shader name (shaders/materials/<name>), 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 toTable() const; // for compatibility
dv::value serialize() const override; dv::value serialize() const override;
@ -322,6 +329,9 @@ public:
blockid_t surfaceReplacement = 0; blockid_t surfaceReplacement = 0;
/// @brief block material shader index (see BlockMaterial::rt.shaderId)
uint8_t materialShader = 0;
std::set<int> tags; std::set<int> tags;
BlockFuncNamesCache eventNames; BlockFuncNamesCache eventNames;