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

View file

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

View file

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

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*
Определяет имя материала блока в формате `пак:имя_материала`, что влияет на выбор звуков взаимодействия с блоком.
Определения материалов расположены в /block_materials.
Определения материалов расположены в /block_materials (см. [материалы блоков](block-materials.md)).
## Варианты

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,9 +1,13 @@
in vec2 a_texCoord;
#include <world_fragment_header>
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;
}

View file

@ -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 <world_vertex_header>
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;
}

View file

@ -14,8 +14,12 @@ uniform bool u_debugNormals;
#include <shadows>
#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)

View file

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

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();
/// @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) {

View file

@ -1,5 +1,11 @@
#include "ContentBuilder.hpp"
#include <set>
#include <stdexcept>
#include <unordered_map>
#include "constants.hpp"
ContentBuilder::~ContentBuilder() = default;
void ContentBuilder::add(std::unique_ptr<ContentPackRuntime> 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<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() {
index_material_shaders(blockMaterials);
std::vector<Block*> blockDefsIndices;
auto groups = std::make_unique<DrawGroups>();
for (const std::string& name : blocks.names) {
@ -50,6 +97,7 @@ std::unique_ptr<Content> 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++) {

View file

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

View file

@ -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<int>(param.value));
shader.uniform1i(name, std::get<int>(value));
break;
case Type::FLOAT:
shader.uniform1f(name, std::get<float>(param.value));
shader.uniform1f(name, std::get<float>(value));
break;
case Type::VEC2:
shader.uniform2f(name, std::get<glm::vec2>(param.value));
shader.uniform2f(name, std::get<glm::vec2>(value));
break;
case Type::VEC3:
shader.uniform3f(name, std::get<glm::vec3>(param.value));
shader.uniform3f(name, std::get<glm::vec3>(value));
break;
case Type::VEC4:
shader.uniform4f(name, std::get<glm::vec4>(param.value));
shader.uniform4f(name, std::get<glm::vec4>(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<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) {
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<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;
found->second.set(value);
}
void PostEffect::setArray(const std::string& name, std::vector<ubyte>&& values) {

View file

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

View file

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

View file

@ -54,8 +54,11 @@ void BlocksRenderer::vertex(
static_cast<uint8_t>(normal.x * 127 + 128),
static_cast<uint8_t>(normal.y * 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) {
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),

View file

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

View file

@ -113,8 +113,8 @@ private:
static_cast<uint8_t>(normal.r * 127 + 128),
static_cast<uint8_t>(normal.g * 127 + 128),
static_cast<uint8_t>(normal.b * 127 + 128),
255
};
vert.flags = VERTEX_EMISSION_BIT;
vert.color = {
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/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);

View file

@ -5,18 +5,28 @@
#include "maths/aabb.hpp"
#include "util/Buffer.hpp"
#include <cstddef>
#include <vector>
#include <array>
#include <memory>
#include <glm/vec2.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
struct ChunkVertex {
glm::vec3 position;
glm::vec2 uv;
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[] = {
{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<typename VertexStructure>
class Mesh;

View file

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

View file

@ -166,6 +166,13 @@ struct BlockMaterial : Serializable {
std::string breakSound;
std::string hitSound;
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 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<int> tags;
BlockFuncNamesCache eventNames;