Changes to blocky materials

- They are now defined in each model
- VoxelTerrain no longer has material slots, except for a global override
- Models can have up to 2 materials instead of only one
This commit is contained in:
Marc Gilleron 2022-05-16 22:55:06 +01:00
parent d5aaef772d
commit 12862a46d0
19 changed files with 731 additions and 435 deletions

View File

@ -13,14 +13,14 @@ Voxel data used by this mesher may be stored in the following channel: `VoxelBuf
### Creating voxel types
The mesher has a `library` property of type `VoxelLibrary`. This is a resource containing a list of all the models you want to use in order to build a voxel mesh: grass, dirt, wood, leaves, water, shrubs, stairs, door parts etc. You can create a new library in place, or make one saved to a file if you want to re-use it in several places. You can also create it from code.
The mesher has a `library` property of type `VoxelBlockyLibrary`. This is a resource containing a list of all the models you want to use in order to build a voxel mesh: grass, dirt, wood, leaves, water, shrubs, stairs, door parts etc. You can create a new library in place, or make one saved to a file if you want to re-use it in several places. You can also create it from code.
First, set how many voxel types you need by setting the `voxel_count` property. If you need more later, you can increase it.
A list of voxel types is shown below:
![Screenshot of the list of voxels in VoxelLibrary](images/voxel_library_voxels_list.png)
![Screenshot of the list of voxels in VoxelBlockyLibrary](images/voxel_library_voxels_list.png)
Each slot can contain a `Voxel` resource. The index shown on their left will be the ID they use in voxel data. Voxel `0` is a special case: by convention, it may be used as the default "air" voxel. You may assign a new `Voxel` resource to each slot, and fill in their properties.
Each slot can contain a `VoxelBlockyModel` resource. The index shown on their left will be the ID they use in voxel data. Voxel `0` is a special case: by convention, it may be used as the default "air" voxel. You may assign a new `VoxelBlockyModel` resource to each slot, and fill in their properties.
With default 16-bit voxel data, you can create up to 65,536 voxel types. But in the future, some bits of the `TYPE` channel might be used to store orientation, so a more realistic limit could be around 4,096.
@ -28,13 +28,13 @@ With default 16-bit voxel data, you can create up to 65,536 voxel types. But in
A simple start is to set the `geometry_type` to `Cube`, so the voxel will be a cube.
Currently, texturing on this mesher is meant to use atlases. You may create a texture containing all the tiles your voxels can use. For example, here is one from the [blocky game](https://github.com/Zylann/voxelgame/tree/master/project/blocky_game) demo:
With this mesher, using atlases is recommended to allow re-using materials and reduce the number of draw calls. You can create a texture containing all the tiles your voxels can use. For example, here is one from the [blocky game](https://github.com/Zylann/voxelgame/tree/master/project/blocky_game) demo:
![Atlas used in the blocky game demo](images/blocky_game_atlas.png)
This atlas is a square texture and can contain up to 16x16 tiles. This number is important and needs to be set on the `VoxelLibrary` `atlas_size` property, so texture coordinates can be generated properly:
This atlas is a square texture and can contain up to 16x16 tiles. This number is important and needs to be set on the `VoxelBlockyLibrary` `atlas_size` property, so texture coordinates can be generated properly when you use the `Cube` geometry type:
![VoxelLibrary atlas size property screenshot](images/voxel_library_atlas_size_property.png)
![VoxelBlockyLibrary atlas size property screenshot](images/voxel_library_atlas_size_property.png)
Voxel types using the `Cube` geometry can have different tiles on each of their faces. You can decide which one to use by assigning properties of `Voxel`, under the `Cube tiles` category. Coordinates here are in tiles, not pixels.
@ -44,22 +44,17 @@ For example, if you want to use the "planks" tile, you may use x=3 and y=1:
![Tile coordinates](images/cube_tile_coordinates.png)
So far we defined a cubic voxel with specific texture coordinates on its faces, but we still have to actually assign the texture and material used to render it. This is configured on the `VoxelTerrain` node directly. `VoxelTerrain` has a list of materials in its properties:
So far we defined a cubic voxel with specific texture coordinates on its faces, but we still have to actually assign the texture and material used to render it. This can be done in the `Material overrides` section, in which you can assign a material with the texture in it.
![VoxelTerrain material properties screenshot](images/voxel_terrain_material_properties.png)
Make sure to assign its `albedo_texture` to your texture. You may also check the `Vertex Color/Use as albedo` property, because this will allow the mesher to bake ambient occlusion on the edge of cubes.
You may assign a new `SpatialMaterial` in the first slot, and assign its `albedo_texture` to your texture. You may also check the `Vertex Color/Use as albedo` property, because this will allow the mesher to bake ambient occlusion on the edge of cubes.
![Material for blocky terrain](images/material_blocky_game.png)
![SpatialMaterial for blocky terrain](images/material_blocky_game.png)
You can use more atlases or more materials by adding them to the `VoxelTerrain` node. Voxel types can only use one of them at a time. In order to choose which material a voxel will use, you may assign its `material ID` property. This property corresponds to the ID of the material assigned on the `VoxelTerrain`.
!!! note
The reason texture atlas is preferred with a minimal amount of materials is because it allows to reduce the number of draw calls tremendously. The more materials you use, the more draw calls voxels will require, and the slower rendering will be.
Each model can use different materials with different textures, but keep in mind the more you re-use materials, the better. It reduces the number of draw calls and makes rendering faster.
### Meshes
Creating voxel types with the `Cube` geometry is a shortcut that can be used for simple voxels, but the most versatile workflow is to use actual meshes. If you change `geometry_type` to `CustomMesh`, you may be allowed to assign a mesh resource. In this mode, the `Cube tiles` properties are not available, because you will have to assign texture coordinates of the mesh within a 3D modeler like Blender.
Creating voxel types with the `Cube` geometry is a shortcut that can be used for simple voxels, but the most versatile workflow is to use actual meshes. If you change `geometry_type` to `CustomMesh`, you are allowed to assign a mesh resource. In this mode, the `Cube tiles` properties are not available, because you will have to assign texture coordinates of the mesh within a 3D modeler like Blender.
![Blender screenshot for UV editing a block](images/blender_block_uv_mapping.png)
@ -68,17 +63,20 @@ Meshes can have any shape you want, however there are a few constraints to respe
- The origin of the mesh should be its lower corner.
- Blender's coordinate system is Z-up, but Godot is Y-up. Make sure the meshes you export don't go into negative coordinates once imported in Godot.
- Vertices should preferably be located within the 0..1 range, in all directions
- Keep it low-poly. The mesher can deal with large models, but performance can decrease very quickly if a numerous voxel type has a complex mesh.
- Keep it low-poly. The mesher can deal with large models, but performance can decrease very quickly if a complex model appears a lot of times.
- Faces lying on the sides of the 1x1x1 unit cube will be the only faces that can be culled by the mesher. Make sure they are perfectly lining up. If they don't, it can cause dramatic slowdowns due to the amount of generated geometry not getting culled.
![Blender screenshot for face lining up with cube side](images/blender_face_cube_side.png)
The best format to use to export your meshes is OBJ. Godot imports this format by default as a mesh resource. Other formats are not suitable because Godot imports them as scenes, and `Voxel` resources require meshes, not scenes.
Materials are not necessary to export, they are still setup in Godot on the terrain node. If you export/import your meshes with a material on them, it will be ignored.
The best format to use to export your meshes is OBJ. Godot imports this format by default as a mesh resource. Other formats are not suitable because Godot imports them as scenes, and `VoxelBlockyModel` resources require meshes, not scenes.
You can choose to export materials from here too, but it is recommended to do it in Godot because it allows you to re-use them.
!!! note
A second material can be used in each model. This is useful if a given mesh needs both transparent and opaque parts. This works as usual, by having a mesh with two surfaces. However, face culling will still use properties of the model regardless. For example, if a model has opaque sides and is transparent in the middle, it may be defined as a non-transparent block, so when placed next to other opaque blocks, geometry of its sides will be culled. See (Transparency)[#transparency] section for more info.
### Usage of voxel type IDs
Voxel IDs defined in a `VoxelLibrary` are like tiles in a tilemap: for simple games, they can directly correspond to a type of block. However, you may want to avoid treating them directly this way over time. Instead, you may define your own list of block types, and each type can correspond to one, or multiple `Voxel` IDs.
Voxel IDs defined in a `VoxelBlockyLibrary` are like tiles in a tilemap: for simple games, they can directly correspond to a type of block. However, you may want to avoid treating them directly this way over time. Instead, you may define your own list of block types, and each type can correspond to one, or multiple `VoxelBlockyModel` IDs.
Examples from Minecraft:
@ -99,9 +97,9 @@ You may want some of your voxel types to be transparent. There is in fact two ma
- Using alpha clip: transparent pixels are discarded, allowing rendering through the opaque pass, which avoids some typical issues with transparent surfaces.
- Alpha blend: actual transparency, which has a few limitations when multiple transparent surfaces are rendered behind each other
Both require to use a different material from the default one you may have used. You will need to add more materials on the `VoxelTerrain` node. Thanks to using a texture atlas, a typical setup only needs 3 materials using the same atlas: opaque, alpha clip and transparent Remember to assign the `material ID` of your voxel types so they use the right material.
Both require to use a different material from the default one you may have used. Note if you use a texture atlas, a typical setup only needs 3 materials using the same atlas: opaque, alpha clip and transparent.
`Voxel` resources also have a `transparency_index` property. This property allows to tune how two voxels occlude their faces. For example, let's say you have two transparent voxels, glass and leaves. By default, if you put them next to each other, the face they share will be culled, allowing you to see through the leaves from the glass block:
`VoxelBlockyModel` resources also have a `transparency_index` property. This property allows to tune how two voxels occlude their faces. For example, let's say you have two transparent voxels, glass and leaves. By default, if you put them next to each other, the face they share will be culled, allowing you to see through the leaves from the glass block:
![Screenshot of transparency index not being exploited](images/transparency_index_example1.png)
@ -114,7 +112,7 @@ Here, glass has `transparency_index=2`, and leaves have `transparency_index=1`:
### Random tick
`Voxel` has a property named `random_tickable`. This is for use with a very specific function of `VoxelToolTerrain`: [run_blocky_random_tick](api/VoxelToolTerrain.md)
`VoxelBlockyModel` has a property named `random_tickable`. This is for use with a very specific function of `VoxelToolTerrain`: [run_blocky_random_tick](api/VoxelToolTerrain.md)
`VoxelMesherCubes`
@ -153,9 +151,9 @@ func _physics_process(delta):
velocity = motion / delta
```
This technique mainly works if you use `VoxelMesherBlocky`, because it gets information about which block is collidable from the `VoxelLibrary` used with it. It might have some limited support in other meshers though.
This technique mainly works if you use `VoxelMesherBlocky`, because it gets information about which block is collidable from the `VoxelBlockyLibrary` used with it. It might have some limited support in other meshers though.
If you use `VoxelMesherBlocky`, it will use the list of AABBs specified in `Voxel` resources. If the list is empty, the voxel won't have collisions. You can also filter out some collisions by assigning the `collision mask` property of `VoxelBoxMover`. This will be matched against the `collision mask` property found on `Voxel` resources.
If you use `VoxelMesherBlocky`, it will use the list of AABBs specified in `VoxelBlockyModel` resources. If the list is empty, the voxel won't have collisions. You can also filter out some collisions by assigning the `collision mask` property of `VoxelBoxMover`. This will be matched against the `collision mask` property found on `VoxelBlockyModel` resources.
### Raycast
@ -173,4 +171,4 @@ if hit != null:
print("Hit voxel ", hit.position)
```
If you use `VoxelMesherBlocky`, it is possible to filter out some voxel types by specifying the `collision mask` argument. This will be matched against the `collision mask` property found on `Voxel` resources.
If you use `VoxelMesherBlocky`, it is possible to filter out some voxel types by specifying the `collision mask` argument. This will be matched against the `collision mask` property found on `VoxelBlockyModel` resources.

View File

@ -43,6 +43,10 @@ Godot 4 is required from this version.
- `VoxelInstanceLibraryMultiMeshItem`: Support setting up mesh LODs from a scene with name `LODx` suffixes
- `VoxelMesherTransvoxel`: initial support for deep SDF sampling, to affine vertex positions at low levels of details (slow and limited for now).
- Blocky voxels
- `VoxelMesherBlocky`: materials are now unlimited and specified in each model, either as overrides or directly from mesh (You still need to consider draw calls when using many materials)
- `VoxelMesherBlocky`: each model can have up to 2 materials
- Fixes
- `VoxelBuffer`: frequently creating buffers with always different sizes no longer wastes memory
- `Voxel`: properties of the inspector were not refreshed when changing `geometry_type`
@ -65,6 +69,7 @@ Godot 4 is required from this version.
- Breaking changes
- Some functions now take `Vector3i` instead of `Vector3`. If you used to send `Vector3` without `floor()` or `round()`, it can have side-effects in negative coordinates.
- `VoxelTerrain`: the main way to specify materials is no longer here, but in meshers instead.
- `VoxelLodTerrain`: `set_process_mode` and `get_process_mode` were renamed `set_process_callback` and `get_process_callback` (due to a name conflict)
- `VoxelLodTerrain`: `has_block` was renamed `has_data_block`
- `VoxelMesherTransvoxel`: Shader API: The data in `COLOR` and `UV` was moved respectively to `CUSTOM0` and `CUSTOM1` (old attributes no longer work for this use case)
@ -106,6 +111,7 @@ This branch is the last supporting Godot 3
- `VoxelTool`: `raycast` locking up if you send a Vector3 containing NaN
- `VoxelInstancer`: fix instances not refreshing when an item is modified and the mesh block size is 32
- `VoxelInstancer`: fix crash when removing an item from the library while an instancer node is using it
- `VoxelInstancer`: fix errors when removing scene instances
- `VoxelStreamScript`: fix voxel data not getting retrieved when `BLOCK_FOUND` is returned
- Terrain was not visible if a room/portals system was used. For now it is not culled by rooms.

View File

@ -41,26 +41,27 @@ Ref<Mesh> build_mesh(const VoxelBufferInternal &voxels, VoxelMesher &mesher,
mesh.instantiate();
for (unsigned int i = 0; i < output.surfaces.size(); ++i) {
Array surface = output.surfaces[i];
VoxelMesher::Output::Surface &surface = output.surfaces[i];
Array arrays = surface.arrays;
if (surface.is_empty()) {
if (arrays.is_empty()) {
continue;
}
CRASH_COND(surface.size() != Mesh::ARRAY_MAX);
if (!is_surface_triangulated(surface)) {
CRASH_COND(arrays.size() != Mesh::ARRAY_MAX);
if (!is_surface_triangulated(arrays)) {
continue;
}
if (p_scale != 1.f) {
scale_surface(surface, p_scale);
scale_surface(arrays, p_scale);
}
if (p_offset != Vector3()) {
offset_surface(surface, p_offset);
offset_surface(arrays, p_offset);
}
mesh->add_surface_from_arrays(output.primitive_type, surface, Array(), Dictionary(), output.mesh_flags);
mesh->add_surface_from_arrays(output.primitive_type, arrays, Array(), Dictionary(), output.mesh_flags);
surface_index_to_material.push_back(i);
}

View File

@ -170,20 +170,26 @@ void VoxelBlockyLibrary::bake() {
// This is the only place we modify the data.
_indexed_materials.clear();
VoxelBlockyModel::MaterialIndexer materials{ _indexed_materials };
_baked_data.models.resize(_voxel_types.size());
for (size_t i = 0; i < _voxel_types.size(); ++i) {
Ref<VoxelBlockyModel> config = _voxel_types[i];
if (config.is_valid()) {
_voxel_types[i]->bake(_baked_data.models[i], _atlas_size, _bake_tangents);
_voxel_types[i]->bake(_baked_data.models[i], _atlas_size, _bake_tangents, materials);
} else {
_baked_data.models[i].clear();
}
}
_baked_data.indexed_materials_count = _indexed_materials.size();
generate_side_culling_matrix();
uint64_t time_spent = Time::get_singleton()->get_ticks_usec() - time_before;
ZN_PRINT_VERBOSE(format("Took {} us to bake VoxelLibrary", time_spent));
ZN_PRINT_VERBOSE(
format("Took {} us to bake VoxelLibrary, indexed {} materials", time_spent, _indexed_materials.size()));
}
void VoxelBlockyLibrary::generate_side_culling_matrix() {
@ -212,7 +218,7 @@ void VoxelBlockyLibrary::generate_side_culling_matrix() {
CRASH_COND(_voxel_types.size() != _baked_data.models.size());
// Gather patterns
// Gather patterns for each model
for (uint16_t type_id = 0; type_id < _voxel_types.size(); ++type_id) {
if (_voxel_types[type_id].is_null()) {
continue;
@ -221,58 +227,65 @@ void VoxelBlockyLibrary::generate_side_culling_matrix() {
VoxelBlockyModel::BakedData &model_data = _baked_data.models[type_id];
model_data.contributes_to_ao = true;
// For each side
for (uint16_t side = 0; side < Cube::SIDE_COUNT; ++side) {
const std::vector<Vector3f> &positions = model_data.model.side_positions[side];
const std::vector<int> &indices = model_data.model.side_indices[side];
ERR_FAIL_COND(indices.size() % 3 != 0);
std::bitset<RASTER_SIZE * RASTER_SIZE> bitmap;
for (unsigned int j = 0; j < indices.size(); j += 3) {
const Vector3f va = positions[indices[j]];
const Vector3f vb = positions[indices[j + 1]];
const Vector3f vc = positions[indices[j + 2]];
// For each surface (they are all combined for simplicity, though it is also a limitation)
for (unsigned int surface_index = 0; surface_index < model_data.model.surface_count; ++surface_index) {
const VoxelBlockyModel::BakedData::Surface &surface = model_data.model.surfaces[surface_index];
// Convert 3D vertices into 2D
Vector2f a, b, c;
switch (side) {
case Cube::SIDE_NEGATIVE_X:
case Cube::SIDE_POSITIVE_X:
a = Vector2f(va.y, va.z);
b = Vector2f(vb.y, vb.z);
c = Vector2f(vc.y, vc.z);
break;
const std::vector<Vector3f> &positions = surface.side_positions[side];
const std::vector<int> &indices = surface.side_indices[side];
ERR_FAIL_COND(indices.size() % 3 != 0);
case Cube::SIDE_NEGATIVE_Y:
case Cube::SIDE_POSITIVE_Y:
a = Vector2f(va.x, va.z);
b = Vector2f(vb.x, vb.z);
c = Vector2f(vc.x, vc.z);
break;
// For each triangle
for (unsigned int j = 0; j < indices.size(); j += 3) {
const Vector3f va = positions[indices[j]];
const Vector3f vb = positions[indices[j + 1]];
const Vector3f vc = positions[indices[j + 2]];
case Cube::SIDE_NEGATIVE_Z:
case Cube::SIDE_POSITIVE_Z:
a = Vector2f(va.x, va.y);
b = Vector2f(vb.x, vb.y);
c = Vector2f(vc.x, vc.y);
break;
// Convert 3D vertices into 2D
Vector2f a, b, c;
switch (side) {
case Cube::SIDE_NEGATIVE_X:
case Cube::SIDE_POSITIVE_X:
a = Vector2f(va.y, va.z);
b = Vector2f(vb.y, vb.z);
c = Vector2f(vc.y, vc.z);
break;
default:
CRASH_NOW();
}
case Cube::SIDE_NEGATIVE_Y:
case Cube::SIDE_POSITIVE_Y:
a = Vector2f(va.x, va.z);
b = Vector2f(vb.x, vb.z);
c = Vector2f(vc.x, vc.z);
break;
a *= RASTER_SIZE;
b *= RASTER_SIZE;
c *= RASTER_SIZE;
case Cube::SIDE_NEGATIVE_Z:
case Cube::SIDE_POSITIVE_Z:
a = Vector2f(va.x, va.y);
b = Vector2f(vb.x, vb.y);
c = Vector2f(vc.x, vc.y);
break;
// Rasterize pattern
rasterize_triangle_barycentric(a, b, c, [&bitmap](unsigned int x, unsigned int y) {
if (x >= RASTER_SIZE || y >= RASTER_SIZE) {
return;
default:
CRASH_NOW();
}
const unsigned int i = x + y * RASTER_SIZE;
bitmap.set(i);
});
a *= RASTER_SIZE;
b *= RASTER_SIZE;
c *= RASTER_SIZE;
// Rasterize pattern
rasterize_triangle_barycentric(a, b, c, [&bitmap](unsigned int x, unsigned int y) {
if (x >= RASTER_SIZE || y >= RASTER_SIZE) {
return;
}
const unsigned int i = x + y * RASTER_SIZE;
bitmap.set(i);
});
}
}
// Find if the same pattern already exists
@ -396,6 +409,11 @@ void VoxelBlockyLibrary::generate_side_culling_matrix() {
print_line("");*/
}
Ref<Material> VoxelBlockyLibrary::get_material_by_index(unsigned int index) const {
ERR_FAIL_INDEX_V(index, _indexed_materials.size(), Ref<Material>());
return _indexed_materials[index];
}
Ref<VoxelBlockyModel> VoxelBlockyLibrary::_b_get_voxel(unsigned int id) {
ERR_FAIL_COND_V(id >= _voxel_types.size(), Ref<VoxelBlockyModel>());
return _voxel_types[id];
@ -407,6 +425,15 @@ Ref<VoxelBlockyModel> VoxelBlockyLibrary::_b_get_voxel_by_name(StringName name)
return _voxel_types[id];
}
TypedArray<Material> VoxelBlockyLibrary::_b_get_materials() const {
TypedArray<Material> materials;
materials.resize(_indexed_materials.size());
for (size_t i = 0; i < _indexed_materials.size(); ++i) {
materials[i] = _indexed_materials[i];
}
return materials;
}
void VoxelBlockyLibrary::_bind_methods() {
ClassDB::bind_method(D_METHOD("create_voxel", "id", "name"), &VoxelBlockyLibrary::create_voxel);
ClassDB::bind_method(D_METHOD("get_voxel", "id"), &VoxelBlockyLibrary::_b_get_voxel);
@ -425,6 +452,8 @@ void VoxelBlockyLibrary::_bind_methods() {
ClassDB::bind_method(D_METHOD("bake"), &VoxelBlockyLibrary::bake);
ClassDB::bind_method(D_METHOD("get_materials"), &VoxelBlockyLibrary::_b_get_materials);
ADD_PROPERTY(PropertyInfo(Variant::INT, "atlas_size"), "set_atlas_size", "get_atlas_size");
ADD_PROPERTY(PropertyInfo(Variant::INT, "voxel_count", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR),
"set_voxel_count", "get_voxel_count");

View File

@ -24,6 +24,8 @@ public:
// Lots of data can get moved but it's only on load.
std::vector<VoxelBlockyModel::BakedData> models;
unsigned int indexed_materials_count = 0;
inline bool has_model(uint32_t i) const {
return i < models.size();
}
@ -80,6 +82,8 @@ public:
return _baked_data_rw_lock;
}
Ref<Material> get_material_by_index(unsigned int index) const;
private:
void set_voxel(unsigned int id, Ref<VoxelBlockyModel> voxel);
@ -91,6 +95,9 @@ private:
Ref<VoxelBlockyModel> _b_get_voxel(unsigned int id);
Ref<VoxelBlockyModel> _b_get_voxel_by_name(StringName name);
// Convenience method to get all indexed materials after baking,
// which can be passed to VoxelMesher::build for testing
TypedArray<Material> _b_get_materials() const;
static void _bind_methods();
@ -105,6 +112,7 @@ private:
// Used in multithread context by the mesher. Don't modify that outside of bake().
RWLock _baked_data_rw_lock;
BakedData _baked_data;
std::vector<Ref<Material>> _indexed_materials;
};
} // namespace zylann::voxel

View File

@ -2,6 +2,7 @@
#include "../../util/godot/funcs.h"
#include "../../util/macros.h"
#include "../../util/math/conv.h"
#include "../../util/string_funcs.h"
#include "voxel_blocky_library.h"
#include "voxel_mesher_blocky.h" // TODO Only required because of MAX_MATERIALS... could be enough inverting that dependency
@ -9,8 +10,7 @@
namespace zylann::voxel {
VoxelBlockyModel::VoxelBlockyModel() :
_id(-1), _material_id(0), _transparency_index(0), _color(1.f, 1.f, 1.f), _geometry_type(GEOMETRY_NONE) {}
VoxelBlockyModel::VoxelBlockyModel() : _color(1.f, 1.f, 1.f) {}
static Cube::Side name_to_side(const String &s) {
if (s == "left") {
@ -38,14 +38,19 @@ bool VoxelBlockyModel::_set(const StringName &p_name, const Variant &p_value) {
String name = p_name;
// TODO Eventualy these could be Rect2 for maximum flexibility?
if (name.begins_with("cube_tiles/")) {
String s = name.substr(ZN_ARRAY_LENGTH("cube_tiles/") - 1, name.length());
if (name.begins_with("cube_tiles_")) {
String s = name.substr(ZN_ARRAY_LENGTH("cube_tiles_") - 1, name.length());
Cube::Side side = name_to_side(s);
if (side != Cube::SIDE_COUNT) {
Vector2 v = p_value;
set_cube_uv_side(side, Vector2f(v.x, v.y));
return true;
}
} else if (name.begins_with("material_override_")) {
const int index = name.substr(ZN_ARRAY_LENGTH("material_override_")).to_int();
set_material_override(index, p_value);
return true;
}
return false;
@ -54,14 +59,19 @@ bool VoxelBlockyModel::_set(const StringName &p_name, const Variant &p_value) {
bool VoxelBlockyModel::_get(const StringName &p_name, Variant &r_ret) const {
String name = p_name;
if (name.begins_with("cube_tiles/")) {
String s = name.substr(ZN_ARRAY_LENGTH("cube_tiles/") - 1, name.length());
if (name.begins_with("cube_tiles_")) {
String s = name.substr(ZN_ARRAY_LENGTH("cube_tiles_") - 1, name.length());
Cube::Side side = name_to_side(s);
if (side != Cube::SIDE_COUNT) {
const Vector2f f = _cube_tiles[side];
r_ret = Vector2(f.x, f.y);
return true;
}
} else if (name.begins_with("material_override_")) {
const int index = name.substr(ZN_ARRAY_LENGTH("material_override_")).to_int();
r_ret = get_material_override(index);
return true;
}
return false;
@ -69,13 +79,37 @@ bool VoxelBlockyModel::_get(const StringName &p_name, Variant &r_ret) const {
void VoxelBlockyModel::_get_property_list(List<PropertyInfo> *p_list) const {
if (_geometry_type == GEOMETRY_CUBE) {
p_list->push_back(PropertyInfo(Variant::FLOAT, "cube_geometry/padding_y"));
p_list->push_back(PropertyInfo(Variant::VECTOR2, "cube_tiles/left"));
p_list->push_back(PropertyInfo(Variant::VECTOR2, "cube_tiles/right"));
p_list->push_back(PropertyInfo(Variant::VECTOR2, "cube_tiles/bottom"));
p_list->push_back(PropertyInfo(Variant::VECTOR2, "cube_tiles/top"));
p_list->push_back(PropertyInfo(Variant::VECTOR2, "cube_tiles/back"));
p_list->push_back(PropertyInfo(Variant::VECTOR2, "cube_tiles/front"));
p_list->push_back(PropertyInfo(Variant::NIL, "Cube geometry", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_GROUP));
p_list->push_back(PropertyInfo(Variant::FLOAT, "cube_geometry_padding_y"));
p_list->push_back(
PropertyInfo(Variant::NIL, "Cube tiles", PROPERTY_HINT_NONE, "cube_tiles_", PROPERTY_USAGE_GROUP));
p_list->push_back(PropertyInfo(Variant::VECTOR2, "cube_tiles_left"));
p_list->push_back(PropertyInfo(Variant::VECTOR2, "cube_tiles_right"));
p_list->push_back(PropertyInfo(Variant::VECTOR2, "cube_tiles_bottom"));
p_list->push_back(PropertyInfo(Variant::VECTOR2, "cube_tiles_top"));
p_list->push_back(PropertyInfo(Variant::VECTOR2, "cube_tiles_back"));
p_list->push_back(PropertyInfo(Variant::VECTOR2, "cube_tiles_front"));
}
if (_surface_count > 0) {
p_list->push_back(PropertyInfo(
Variant::NIL, "Material overrides", PROPERTY_HINT_NONE, "material_override_", PROPERTY_USAGE_GROUP));
for (unsigned int i = 0; i < _surface_count; ++i) {
p_list->push_back(PropertyInfo(Variant::OBJECT, String("material_override_{0}").format(varray(i)),
PROPERTY_HINT_RESOURCE_TYPE, Material::get_class_static()));
}
// p_list->push_back(
// PropertyInfo(Variant::NIL, "Surface collision", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_GROUP));
// for (unsigned int i = 0; i < _surface_count; ++i) {
// p_list->push_back(PropertyInfo(Variant::OBJECT, String("collision_enabled_{0}").format(varray(i)),
// PROPERTY_HINT_RESOURCE_TYPE, Material::get_class_static()));
// }
}
}
@ -94,9 +128,14 @@ void VoxelBlockyModel::set_color(Color color) {
_color = color;
}
void VoxelBlockyModel::set_material_id(unsigned int id) {
ERR_FAIL_COND(id >= VoxelMesherBlocky::MAX_MATERIALS);
_material_id = id;
void VoxelBlockyModel::set_material_override(int index, Ref<Material> material) {
ERR_FAIL_INDEX(index, _surface_count);
_surface_params[index].material_override = material;
}
Ref<Material> VoxelBlockyModel::get_material_override(int index) const {
ERR_FAIL_INDEX_V(index, _surface_count, Ref<Material>());
return _surface_params[index].material_override;
}
void VoxelBlockyModel::set_transparent(bool t) {
@ -122,16 +161,23 @@ void VoxelBlockyModel::set_geometry_type(GeometryType type) {
switch (_geometry_type) {
case GEOMETRY_NONE:
_collision_aabbs.clear();
_surface_count = 0;
break;
case GEOMETRY_CUBE:
_collision_aabbs.clear();
_collision_aabbs.push_back(AABB(Vector3(0, 0, 0), Vector3(1, 1, 1)));
_empty = false;
_surface_count = 1;
break;
case GEOMETRY_CUSTOM_MESH:
// Gotta be user-defined
if (_custom_mesh.is_valid()) {
_surface_count = _custom_mesh->get_surface_count();
} else {
_surface_count = 0;
}
break;
default:
@ -149,9 +195,21 @@ VoxelBlockyModel::GeometryType VoxelBlockyModel::get_geometry_type() const {
void VoxelBlockyModel::set_custom_mesh(Ref<Mesh> mesh) {
_custom_mesh = mesh;
if (_custom_mesh.is_valid()) {
set_surface_count(_custom_mesh->get_surface_count());
} else {
set_surface_count(0);
}
}
void VoxelBlockyModel::set_cube_geometry() {}
void VoxelBlockyModel::set_surface_count(unsigned int new_count) {
if (new_count != _surface_count) {
_surface_count = new_count;
#ifdef TOOLS_ENABLED
notify_property_list_changed();
#endif
}
}
void VoxelBlockyModel::set_random_tickable(bool rt) {
_random_tickable = rt;
@ -169,7 +227,8 @@ Ref<Resource> VoxelBlockyModel::duplicate(bool p_subresources) const {
d._id = -1;
d._name = _name;
d._material_id = _material_id;
d._surface_params = _surface_params;
d._surface_count = _surface_count;
d._transparency_index = _transparency_index;
d._color = _color;
d._geometry_type = _geometry_type;
@ -196,8 +255,11 @@ static void bake_cube_geometry(
VoxelBlockyModel &config, VoxelBlockyModel::BakedData &baked_data, int p_atlas_size, bool bake_tangents) {
const float sy = 1.0;
baked_data.model.surface_count = 1;
VoxelBlockyModel::BakedData::Surface &surface = baked_data.model.surfaces[0];
for (unsigned int side = 0; side < Cube::SIDE_COUNT; ++side) {
std::vector<Vector3f> &positions = baked_data.model.side_positions[side];
std::vector<Vector3f> &positions = surface.side_positions[side];
positions.resize(4);
for (unsigned int i = 0; i < 4; ++i) {
int corner = Cube::g_side_corners[side][i];
@ -208,7 +270,7 @@ static void bake_cube_geometry(
positions[i] = p;
}
std::vector<int> &indices = baked_data.model.side_indices[side];
std::vector<int> &indices = surface.side_indices[side];
indices.resize(6);
for (unsigned int i = 0; i < 6; ++i) {
indices[i] = Cube::g_side_quad_triangles[side][i];
@ -231,14 +293,14 @@ static void bake_cube_geometry(
const float s = 1.0 / atlas_size;
for (unsigned int side = 0; side < Cube::SIDE_COUNT; ++side) {
baked_data.model.side_uvs[side].resize(4);
std::vector<Vector2f> &uvs = baked_data.model.side_uvs[side];
surface.side_uvs[side].resize(4);
std::vector<Vector2f> &uvs = surface.side_uvs[side];
for (unsigned int i = 0; i < 4; ++i) {
uvs[i] = (config.get_cube_tile(side) + uv[i]) * s;
}
if (bake_tangents) {
std::vector<float> &tangents = baked_data.model.side_tangents[side];
std::vector<float> &tangents = surface.side_tangents[side];
for (unsigned int i = 0; i < 4; ++i) {
for (unsigned int j = 0; j < 4; ++j) {
tangents.push_back(Cube::g_side_tangents[side][j]);
@ -250,7 +312,8 @@ static void bake_cube_geometry(
baked_data.empty = false;
}
static void bake_mesh_geometry(VoxelBlockyModel &config, VoxelBlockyModel::BakedData &baked_data, bool bake_tangents) {
static void bake_mesh_geometry(VoxelBlockyModel &config, VoxelBlockyModel::BakedData &baked_data, bool bake_tangents,
VoxelBlockyModel::MaterialIndexer &materials) {
Ref<Mesh> mesh = config.get_custom_mesh();
if (mesh.is_null()) {
@ -258,73 +321,94 @@ static void bake_mesh_geometry(VoxelBlockyModel &config, VoxelBlockyModel::Baked
return;
}
Array arrays = mesh->surface_get_arrays(0);
VoxelBlockyModel::BakedData::Model &model = baked_data.model;
ERR_FAIL_COND(arrays.size() == 0);
if (mesh->get_surface_count() > VoxelBlockyModel::BakedData::Model::MAX_SURFACES) {
ZN_PRINT_WARNING(format("Mesh has more than {} surfaces, extra surfaces will not be baked.",
VoxelBlockyModel::BakedData::Model::MAX_SURFACES));
}
PackedInt32Array indices = arrays[Mesh::ARRAY_INDEX];
ERR_FAIL_COND_MSG(indices.size() % 3 != 0, "Mesh is empty or does not contain triangles");
const unsigned int surface_count =
math::min(uint32_t(mesh->get_surface_count()), VoxelBlockyModel::BakedData::Model::MAX_SURFACES);
PackedVector3Array positions = arrays[Mesh::ARRAY_VERTEX];
PackedVector3Array normals = arrays[Mesh::ARRAY_NORMAL];
PackedVector2Array uvs = arrays[Mesh::ARRAY_TEX_UV];
PackedFloat32Array tangents = arrays[Mesh::ARRAY_TANGENT];
baked_data.model.surface_count = surface_count;
baked_data.empty = positions.size() == 0;
for (unsigned int surface_index = 0; surface_index < surface_count; ++surface_index) {
Array arrays = mesh->surface_get_arrays(surface_index);
ERR_FAIL_COND(normals.size() == 0);
ERR_CONTINUE(arrays.size() == 0);
struct L {
static uint8_t get_sides(Vector3f pos) {
uint8_t mask = 0;
const float tolerance = 0.001;
mask |= Math::is_equal_approx(pos.x, 0.f, tolerance) << Cube::SIDE_NEGATIVE_X;
mask |= Math::is_equal_approx(pos.x, 1.f, tolerance) << Cube::SIDE_POSITIVE_X;
mask |= Math::is_equal_approx(pos.y, 0.f, tolerance) << Cube::SIDE_NEGATIVE_Y;
mask |= Math::is_equal_approx(pos.y, 1.f, tolerance) << Cube::SIDE_POSITIVE_Y;
mask |= Math::is_equal_approx(pos.z, 0.f, tolerance) << Cube::SIDE_NEGATIVE_Z;
mask |= Math::is_equal_approx(pos.z, 1.f, tolerance) << Cube::SIDE_POSITIVE_Z;
return mask;
}
PackedInt32Array indices = arrays[Mesh::ARRAY_INDEX];
ERR_CONTINUE_MSG(indices.size() % 3 != 0, "Mesh surface is empty or does not contain triangles");
static bool get_triangle_side(
const Vector3f &a, const Vector3f &b, const Vector3f &c, Cube::SideAxis &out_side) {
const uint8_t m = get_sides(a) & get_sides(b) & get_sides(c);
if (m == 0) {
// At least one of the points doesn't belong to a face
PackedVector3Array positions = arrays[Mesh::ARRAY_VERTEX];
PackedVector3Array normals = arrays[Mesh::ARRAY_NORMAL];
PackedVector2Array uvs = arrays[Mesh::ARRAY_TEX_UV];
PackedFloat32Array tangents = arrays[Mesh::ARRAY_TANGENT];
baked_data.empty = positions.size() == 0;
ERR_CONTINUE(normals.size() == 0);
struct L {
static uint8_t get_sides(Vector3f pos) {
uint8_t mask = 0;
const float tolerance = 0.001;
mask |= Math::is_equal_approx(pos.x, 0.f, tolerance) << Cube::SIDE_NEGATIVE_X;
mask |= Math::is_equal_approx(pos.x, 1.f, tolerance) << Cube::SIDE_POSITIVE_X;
mask |= Math::is_equal_approx(pos.y, 0.f, tolerance) << Cube::SIDE_NEGATIVE_Y;
mask |= Math::is_equal_approx(pos.y, 1.f, tolerance) << Cube::SIDE_POSITIVE_Y;
mask |= Math::is_equal_approx(pos.z, 0.f, tolerance) << Cube::SIDE_NEGATIVE_Z;
mask |= Math::is_equal_approx(pos.z, 1.f, tolerance) << Cube::SIDE_POSITIVE_Z;
return mask;
}
static bool get_triangle_side(
const Vector3f &a, const Vector3f &b, const Vector3f &c, Cube::SideAxis &out_side) {
const uint8_t m = get_sides(a) & get_sides(b) & get_sides(c);
if (m == 0) {
// At least one of the points doesn't belong to a face
return false;
}
for (unsigned int side = 0; side < Cube::SIDE_COUNT; ++side) {
if (m == (1 << side)) {
// All points belong to the same face
out_side = (Cube::SideAxis)side;
return true;
}
}
// The triangle isn't in one face
return false;
}
for (unsigned int side = 0; side < Cube::SIDE_COUNT; ++side) {
if (m == (1 << side)) {
// All points belong to the same face
out_side = (Cube::SideAxis)side;
return true;
}
}
// The triangle isn't in one face
return false;
};
if (uvs.size() == 0) {
// TODO Properly generate UVs if there arent any
uvs = PackedVector2Array();
uvs.resize(positions.size());
}
};
if (uvs.size() == 0) {
// TODO Properly generate UVs if there arent any
uvs = PackedVector2Array();
uvs.resize(positions.size());
}
const bool tangents_empty = (tangents.size() == 0);
const bool tangents_empty = (tangents.size() == 0);
#ifdef TOOLS_ENABLED
if (tangents_empty && bake_tangents) {
WARN_PRINT(String("Voxel model '{0}' with ID {1} does not have tangents. They will be generated."
"You should consider providing a mesh with tangents, or at least UVs and normals, "
"or turn off tangents baking in VoxelLibrary.")
.format(varray(config.get_voxel_name(), config.get_id())));
}
if (tangents_empty && bake_tangents) {
WARN_PRINT(String("Voxel model '{0}' with ID {1} does not have tangents. They will be generated."
"You should consider providing a mesh with tangents, or at least UVs and normals, "
"or turn off tangents baking in VoxelLibrary.")
.format(varray(config.get_voxel_name(), config.get_id())));
}
#endif
// Separate triangles belonging to faces of the cube
{
// Separate triangles belonging to faces of the cube
VoxelBlockyModel::BakedData::Surface &surface = model.surfaces[surface_index];
Ref<Material> material = mesh->surface_get_material(surface_index);
if (material.is_valid()) {
surface.material_id = materials.get_or_create_index(material);
} else {
surface.material_id = -1;
}
// PackedInt32Array::Read indices_read = indices.read();
// PackedVector3Array::Read positions_read = positions.read();
// PackedVector3Array::Read normals_read = normals.read();
@ -335,8 +419,6 @@ static void bake_mesh_geometry(VoxelBlockyModel &config, VoxelBlockyModel::Baked
std::unordered_map<int, int> added_regular_indices;
FixedArray<Vector3f, 3> tri_positions;
VoxelBlockyModel::BakedData::Model &model = baked_data.model;
for (int i = 0; i < indices.size(); i += 3) {
Cube::SideAxis side;
@ -364,7 +446,7 @@ static void bake_mesh_geometry(VoxelBlockyModel &config, VoxelBlockyModel::Baked
if (L::get_triangle_side(tri_positions[0], tri_positions[1], tri_positions[2], side)) {
// That triangle is on the face
int next_side_index = model.side_positions[side].size();
int next_side_index = surface.side_positions[side].size();
for (int j = 0; j < 3; ++j) {
int src_index = indices[i + j];
@ -374,25 +456,25 @@ static void bake_mesh_geometry(VoxelBlockyModel &config, VoxelBlockyModel::Baked
if (existing_dst_index_it == added_indices.end()) {
// Add new vertex
model.side_indices[side].push_back(next_side_index);
model.side_positions[side].push_back(tri_positions[j]);
model.side_uvs[side].push_back(to_vec2f(uvs[indices[i + j]]));
surface.side_indices[side].push_back(next_side_index);
surface.side_positions[side].push_back(tri_positions[j]);
surface.side_uvs[side].push_back(to_vec2f(uvs[indices[i + j]]));
if (bake_tangents) {
if (tangents_empty) {
model.side_tangents[side].push_back(tangent[0]);
model.side_tangents[side].push_back(tangent[1]);
model.side_tangents[side].push_back(tangent[2]);
model.side_tangents[side].push_back(tangent[3]);
surface.side_tangents[side].push_back(tangent[0]);
surface.side_tangents[side].push_back(tangent[1]);
surface.side_tangents[side].push_back(tangent[2]);
surface.side_tangents[side].push_back(tangent[3]);
} else {
// i is the first vertex of each triangle which increments by steps of 3.
// There are 4 floats per tangent.
int ti = (i / 3) * 4;
model.side_tangents[side].push_back(tangents[ti]);
model.side_tangents[side].push_back(tangents[ti + 1]);
model.side_tangents[side].push_back(tangents[ti + 2]);
model.side_tangents[side].push_back(tangents[ti + 3]);
surface.side_tangents[side].push_back(tangents[ti]);
surface.side_tangents[side].push_back(tangents[ti + 1]);
surface.side_tangents[side].push_back(tangents[ti + 2]);
surface.side_tangents[side].push_back(tangents[ti + 3]);
}
}
@ -401,40 +483,40 @@ static void bake_mesh_geometry(VoxelBlockyModel &config, VoxelBlockyModel::Baked
} else {
// Vertex was already added, just add index referencing it
model.side_indices[side].push_back(existing_dst_index_it->second);
surface.side_indices[side].push_back(existing_dst_index_it->second);
}
}
} else {
// That triangle is not on the face
int next_regular_index = model.positions.size();
int next_regular_index = surface.positions.size();
for (int j = 0; j < 3; ++j) {
int src_index = indices[i + j];
auto existing_dst_index_it = added_regular_indices.find(src_index);
if (existing_dst_index_it == added_regular_indices.end()) {
model.indices.push_back(next_regular_index);
model.positions.push_back(tri_positions[j]);
model.normals.push_back(to_vec3f(normals[indices[i + j]]));
model.uvs.push_back(to_vec2f(uvs[indices[i + j]]));
surface.indices.push_back(next_regular_index);
surface.positions.push_back(tri_positions[j]);
surface.normals.push_back(to_vec3f(normals[indices[i + j]]));
surface.uvs.push_back(to_vec2f(uvs[indices[i + j]]));
if (bake_tangents) {
if (tangents_empty) {
model.tangents.push_back(tangent[0]);
model.tangents.push_back(tangent[1]);
model.tangents.push_back(tangent[2]);
model.tangents.push_back(tangent[3]);
surface.tangents.push_back(tangent[0]);
surface.tangents.push_back(tangent[1]);
surface.tangents.push_back(tangent[2]);
surface.tangents.push_back(tangent[3]);
} else {
// i is the first vertex of each triangle which increments by steps of 3.
// There are 4 floats per tangent.
int ti = (i / 3) * 4;
model.tangents.push_back(tangents[ti]);
model.tangents.push_back(tangents[ti + 1]);
model.tangents.push_back(tangents[ti + 2]);
model.tangents.push_back(tangents[ti + 3]);
surface.tangents.push_back(tangents[ti]);
surface.tangents.push_back(tangents[ti + 1]);
surface.tangents.push_back(tangents[ti + 2]);
surface.tangents.push_back(tangents[ti + 3]);
}
}
@ -442,7 +524,7 @@ static void bake_mesh_geometry(VoxelBlockyModel &config, VoxelBlockyModel::Baked
++next_regular_index;
} else {
model.indices.push_back(existing_dst_index_it->second);
surface.indices.push_back(existing_dst_index_it->second);
}
}
}
@ -450,12 +532,11 @@ static void bake_mesh_geometry(VoxelBlockyModel &config, VoxelBlockyModel::Baked
}
}
void VoxelBlockyModel::bake(BakedData &baked_data, int p_atlas_size, bool bake_tangents) {
void VoxelBlockyModel::bake(BakedData &baked_data, int p_atlas_size, bool bake_tangents, MaterialIndexer &materials) {
baked_data.clear();
// baked_data.contributes_to_ao is set by the side culling phase
baked_data.transparency_index = _transparency_index;
baked_data.material_id = _material_id;
baked_data.color = _color;
switch (_geometry_type) {
@ -468,7 +549,7 @@ void VoxelBlockyModel::bake(BakedData &baked_data, int p_atlas_size, bool bake_t
break;
case GEOMETRY_CUSTOM_MESH:
bake_mesh_geometry(*this, baked_data, bake_tangents);
bake_mesh_geometry(*this, baked_data, bake_tangents, materials);
break;
default:
@ -476,6 +557,41 @@ void VoxelBlockyModel::bake(BakedData &baked_data, int p_atlas_size, bool bake_t
break;
}
BakedData::Model &model = baked_data.model;
// Set empty sides mask
model.empty_sides_mask = 0;
for (unsigned int side = 0; side < Cube::SIDE_COUNT; ++side) {
bool empty = true;
for (unsigned int surface_index = 0; surface_index < model.surface_count; ++surface_index) {
const BakedData::Surface &surface = model.surfaces[surface_index];
if (surface.side_indices[side].size() > 0) {
empty = false;
break;
}
}
if (empty) {
model.empty_sides_mask |= (1 << side);
}
}
// Assign material overrides if any
for (unsigned int surface_index = 0; surface_index < model.surface_count; ++surface_index) {
if (surface_index < _surface_count) {
const SurfaceParams &surface_params = _surface_params[surface_index];
const Ref<Material> material = surface_params.material_override;
BakedData::Surface &surface = model.surfaces[surface_index];
if (material.is_valid()) {
const unsigned int material_index = materials.get_or_create_index(material);
surface.material_id = material_index;
}
//surface.collision_enabled = surface_params.collision_enabled;
}
}
_empty = baked_data.empty;
}
@ -510,6 +626,10 @@ void VoxelBlockyModel::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_color", "color"), &VoxelBlockyModel::set_color);
ClassDB::bind_method(D_METHOD("get_color"), &VoxelBlockyModel::get_color);
ClassDB::bind_method(
D_METHOD("set_material_override", "index", "material"), &VoxelBlockyModel::set_material_override);
ClassDB::bind_method(D_METHOD("get_material_override", "index"), &VoxelBlockyModel::get_material_override);
ClassDB::bind_method(D_METHOD("set_transparent", "transparent"), &VoxelBlockyModel::set_transparent);
ClassDB::bind_method(D_METHOD("is_transparent"), &VoxelBlockyModel::is_transparent);
@ -520,9 +640,6 @@ void VoxelBlockyModel::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_random_tickable", "rt"), &VoxelBlockyModel::set_random_tickable);
ClassDB::bind_method(D_METHOD("is_random_tickable"), &VoxelBlockyModel::is_random_tickable);
ClassDB::bind_method(D_METHOD("set_material_id", "id"), &VoxelBlockyModel::set_material_id);
ClassDB::bind_method(D_METHOD("get_material_id"), &VoxelBlockyModel::get_material_id);
ClassDB::bind_method(D_METHOD("set_geometry_type", "type"), &VoxelBlockyModel::set_geometry_type);
ClassDB::bind_method(D_METHOD("get_geometry_type"), &VoxelBlockyModel::get_geometry_type);
@ -545,11 +662,13 @@ void VoxelBlockyModel::_bind_methods() {
"set_transparent", "is_transparent");
ADD_PROPERTY(PropertyInfo(Variant::INT, "transparency_index"), "set_transparency_index", "get_transparency_index");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "random_tickable"), "set_random_tickable", "is_random_tickable");
ADD_PROPERTY(PropertyInfo(Variant::INT, "material_id"), "set_material_id", "get_material_id");
ADD_PROPERTY(PropertyInfo(Variant::INT, "geometry_type", PROPERTY_HINT_ENUM, "None,Cube,CustomMesh"),
"set_geometry_type", "get_geometry_type");
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "custom_mesh", PROPERTY_HINT_RESOURCE_TYPE, Mesh::get_class_static()),
"set_custom_mesh", "get_custom_mesh");
ADD_GROUP("Box collision", "");
ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "collision_aabbs", PROPERTY_HINT_TYPE_STRING, itos(Variant::AABB) + ":"),
"set_collision_aabbs", "get_collision_aabbs");
ADD_PROPERTY(PropertyInfo(Variant::INT, "collision_mask", PROPERTY_HINT_LAYERS_3D_PHYSICS), "set_collision_mask",

View File

@ -28,7 +28,8 @@ public:
// It becomes distinct because it's going to be used in a multithread environment,
// while the configuration that produced the data can be changed by the user at any time.
struct BakedData {
struct Model {
struct Surface {
// Inside part of the model.
std::vector<Vector3f> positions;
std::vector<Vector3f> normals;
std::vector<Vector2f> uvs;
@ -42,7 +43,8 @@ public:
FixedArray<std::vector<int>, Cube::SIDE_COUNT> side_indices;
FixedArray<std::vector<float>, Cube::SIDE_COUNT> side_tangents;
FixedArray<uint32_t, Cube::SIDE_COUNT> side_pattern_indices;
int material_id = -1;
//bool collision_enabled = true;
void clear() {
positions.clear();
@ -60,8 +62,28 @@ public:
}
};
struct Model {
static const uint32_t MAX_SURFACES = 2;
// A model can have up to 2 materials.
// If more is needed or profiling tells better, we could change it to a vector?
FixedArray<Surface, MAX_SURFACES> surfaces;
unsigned int surface_count = 0;
// Cached information to check this case early
uint8_t empty_sides_mask = 0;
// Tells what is the "shape" of each side in order to cull them quickly when in contact with neighbors.
// Side patterns are still determined based on a combination of all surfaces.
FixedArray<uint32_t, Cube::SIDE_COUNT> side_pattern_indices;
void clear() {
for (unsigned int i = 0; i < surfaces.size(); ++i) {
surfaces[i].clear();
}
}
};
Model model;
int material_id;
Color color;
uint8_t transparency_index;
bool contributes_to_ao;
@ -102,12 +124,10 @@ public:
return _color;
}
void set_material_id(unsigned int id);
_FORCE_INLINE_ unsigned int get_material_id() const {
return _material_id;
}
void set_material_override(int index, Ref<Material> material);
Ref<Material> get_material_override(int index) const;
// TODO Might become obsolete
// TODO Might become obsoleted by transparency index
void set_transparent(bool t = true);
_FORCE_INLINE_ bool is_transparent() const {
return _transparency_index != 0;
@ -159,7 +179,23 @@ public:
//------------------------------------------
// Properties for internal usage only
void bake(BakedData &baked_data, int p_atlas_size, bool bake_tangents);
struct MaterialIndexer {
std::vector<Ref<Material>> &materials;
unsigned int get_or_create_index(const Ref<Material> &p_material) {
for (size_t i = 0; i < materials.size(); ++i) {
const Ref<Material> &material = materials[i];
if (material == p_material) {
return i;
}
}
const unsigned int ret = materials.size();
materials.push_back(p_material);
return ret;
}
};
void bake(BakedData &baked_data, int p_atlas_size, bool bake_tangents, MaterialIndexer &materials);
const std::vector<AABB> &get_collision_aabbs() const {
return _collision_aabbs;
@ -174,32 +210,41 @@ private:
static void _bind_methods();
void set_cube_geometry();
Array _b_get_collision_aabbs() const;
void _b_set_collision_aabbs(Array array);
private:
void set_surface_count(unsigned int new_count);
// Identifiers
int _id;
int _id = -1;
StringName _name;
// Properties
int _material_id;
struct SurfaceParams {
// If assigned, these materials override those present on the mesh itself.
Ref<Material> material_override;
// If true and classic mesh physics are enabled, the surface will be present in the collider.
//bool collision_enabled = true;
};
FixedArray<SurfaceParams, BakedData::Model::MAX_SURFACES> _surface_params;
unsigned int _surface_count = 0;
// If two neighboring voxels are supposed to occlude their shared face,
// this index decides wether or not it should happen. Equal indexes culls the face, different indexes doesn't.
uint8_t _transparency_index = 0;
Color _color;
GeometryType _geometry_type;
GeometryType _geometry_type = GEOMETRY_NONE;
FixedArray<Vector2f, Cube::SIDE_COUNT> _cube_tiles;
Ref<Mesh> _custom_mesh;
std::vector<AABB> _collision_aabbs;
bool _random_tickable = false;
bool _empty = true;
// Used for AABB physics only, not classic physics
std::vector<AABB> _collision_aabbs;
uint32_t _collision_mask = 1;
};

View File

@ -43,11 +43,12 @@ inline bool contributes_to_ao(const VoxelBlockyLibrary::BakedData &lib, uint32_t
return true;
}
static thread_local std::vector<int> tls_index_offsets;
} // namespace
template <typename Type_T>
void generate_blocky_mesh(
FixedArray<VoxelMesherBlocky::Arrays, VoxelMesherBlocky::MAX_MATERIALS> &out_arrays_per_material,
void generate_blocky_mesh(std::vector<VoxelMesherBlocky::Arrays> &out_arrays_per_material,
const Span<Type_T> type_buffer, const Vector3i block_size, const VoxelBlockyLibrary::BakedData &library,
bool bake_occlusion, float baked_occlusion_darkness) {
ERR_FAIL_COND(block_size.x < static_cast<int>(2 * VoxelMesherBlocky::PADDING) ||
@ -64,7 +65,9 @@ void generate_blocky_mesh(
const Vector3i min = Vector3iUtil::create(VoxelMesherBlocky::PADDING);
const Vector3i max = block_size - Vector3iUtil::create(VoxelMesherBlocky::PADDING);
int index_offsets[VoxelMesherBlocky::MAX_MATERIALS] = { 0 };
std::vector<int> &index_offsets = tls_index_offsets;
index_offsets.clear();
index_offsets.resize(out_arrays_per_material.size(), 0);
FixedArray<int, Cube::SIDE_COUNT> side_neighbor_lut;
side_neighbor_lut[Cube::SIDE_LEFT] = row_size;
@ -131,71 +134,80 @@ void generate_blocky_mesh(
const int voxel_index = y + x * row_size + z * deck_size;
const int voxel_id = type_buffer[voxel_index];
if (voxel_id != 0 && library.has_model(voxel_id)) {
const VoxelBlockyModel::BakedData &voxel = library.models[voxel_id];
if (voxel_id == VoxelBlockyModel::AIR_ID || !library.has_model(voxel_id)) {
continue;
}
VoxelMesherBlocky::Arrays &arrays = out_arrays_per_material[voxel.material_id];
int &index_offset = index_offsets[voxel.material_id];
const VoxelBlockyModel::BakedData &voxel = library.models[voxel_id];
const VoxelBlockyModel::BakedData::Model &model = voxel.model;
// Hybrid approach: extract cube faces and decimate those that aren't visible,
// and still allow voxels to have geometry that is not a cube
// Hybrid approach: extract cube faces and decimate those that aren't visible,
// and still allow voxels to have geometry that is not a cube
// Sides
for (unsigned int side = 0; side < Cube::SIDE_COUNT; ++side) {
const std::vector<Vector3f> &side_positions = voxel.model.side_positions[side];
// Sides
for (unsigned int side = 0; side < Cube::SIDE_COUNT; ++side) {
if ((model.empty_sides_mask & (1 << side)) != 0) {
// This side is empty
continue;
}
const uint32_t neighbor_voxel_id = type_buffer[voxel_index + side_neighbor_lut[side]];
if (!is_face_visible(library, voxel, neighbor_voxel_id, side)) {
continue;
}
// The face is visible
int shaded_corner[8] = { 0 };
if (bake_occlusion) {
// Combinatory solution for
// https://0fps.net/2013/07/03/ambient-occlusion-for-minecraft-like-worlds/ (inverted)
// function vertexAO(side1, side2, corner) {
// if(side1 && side2) {
// return 0
// }
// return 3 - (side1 + side2 + corner)
// }
for (unsigned int j = 0; j < 4; ++j) {
const unsigned int edge = Cube::g_side_edges[side][j];
const int edge_neighbor_id = type_buffer[voxel_index + edge_neighbor_lut[edge]];
if (contributes_to_ao(library, edge_neighbor_id)) {
++shaded_corner[Cube::g_edge_corners[edge][0]];
++shaded_corner[Cube::g_edge_corners[edge][1]];
}
}
for (unsigned int j = 0; j < 4; ++j) {
const unsigned int corner = Cube::g_side_corners[side][j];
if (shaded_corner[corner] == 2) {
shaded_corner[corner] = 3;
} else {
const int corner_neigbor_id = type_buffer[voxel_index + corner_neighbor_lut[corner]];
if (contributes_to_ao(library, corner_neigbor_id)) {
++shaded_corner[corner];
}
}
}
}
// Subtracting 1 because the data is padded
Vector3f pos(x - 1, y - 1, z - 1);
for (unsigned int surface_index = 0; surface_index < model.surface_count; ++surface_index) {
const VoxelBlockyModel::BakedData::Surface &surface = model.surfaces[surface_index];
VoxelMesherBlocky::Arrays &arrays = out_arrays_per_material[surface.material_id];
ZN_ASSERT(surface.material_id >= 0 && surface.material_id < index_offsets.size());
int &index_offset = index_offsets[surface.material_id];
const std::vector<Vector3f> &side_positions = surface.side_positions[side];
const unsigned int vertex_count = side_positions.size();
if (vertex_count == 0) {
continue;
}
const uint32_t neighbor_voxel_id = type_buffer[voxel_index + side_neighbor_lut[side]];
if (!is_face_visible(library, voxel, neighbor_voxel_id, side)) {
continue;
}
// The face is visible
int shaded_corner[8] = { 0 };
if (bake_occlusion) {
// Combinatory solution for
// https://0fps.net/2013/07/03/ambient-occlusion-for-minecraft-like-worlds/ (inverted)
// function vertexAO(side1, side2, corner) {
// if(side1 && side2) {
// return 0
// }
// return 3 - (side1 + side2 + corner)
// }
for (unsigned int j = 0; j < 4; ++j) {
const unsigned int edge = Cube::g_side_edges[side][j];
const int edge_neighbor_id = type_buffer[voxel_index + edge_neighbor_lut[edge]];
if (contributes_to_ao(library, edge_neighbor_id)) {
++shaded_corner[Cube::g_edge_corners[edge][0]];
++shaded_corner[Cube::g_edge_corners[edge][1]];
}
}
for (unsigned int j = 0; j < 4; ++j) {
const unsigned int corner = Cube::g_side_corners[side][j];
if (shaded_corner[corner] == 2) {
shaded_corner[corner] = 3;
} else {
const int corner_neigbor_id =
type_buffer[voxel_index + corner_neighbor_lut[corner]];
if (contributes_to_ao(library, corner_neigbor_id)) {
++shaded_corner[corner];
}
}
}
}
const std::vector<Vector2f> &side_uvs = voxel.model.side_uvs[side];
const std::vector<float> &side_tangents = voxel.model.side_tangents[side];
// Subtracting 1 because the data is padded
Vector3f pos(x - 1, y - 1, z - 1);
const std::vector<Vector2f> &side_uvs = surface.side_uvs[side];
const std::vector<float> &side_tangents = surface.side_tangents[side];
// Append vertices of the faces in one go, don't use push_back
@ -238,7 +250,7 @@ void generate_blocky_mesh(
if (bake_occlusion) {
for (unsigned int i = 0; i < vertex_count; ++i) {
Vector3f v = side_positions[i];
Vector3f vertex_pos = side_positions[i];
// General purpose occlusion colouring.
// TODO Optimize for cubes
@ -251,7 +263,8 @@ void generate_blocky_mesh(
float s = baked_occlusion_darkness *
static_cast<float>(shaded_corner[corner]);
//float k = 1.f - Cube::g_corner_position[corner].distance_to(v);
float k = 1.f - Cube::g_corner_position[corner].distance_squared_to(v);
float k = 1.f -
Cube::g_corner_position[corner].distance_squared_to(vertex_pos);
if (k < 0.0) {
k = 0.0;
}
@ -272,7 +285,7 @@ void generate_blocky_mesh(
}
}
const std::vector<int> &side_indices = voxel.model.side_indices[side];
const std::vector<int> &side_indices = surface.side_indices[side];
const unsigned int index_count = side_indices.size();
{
@ -286,45 +299,54 @@ void generate_blocky_mesh(
index_offset += vertex_count;
}
}
// Inside
if (voxel.model.positions.size() != 0) {
// TODO Get rid of push_backs
const std::vector<Vector3f> &positions = voxel.model.positions;
const unsigned int vertex_count = positions.size();
const Color modulate_color = voxel.color;
const std::vector<Vector3f> &normals = voxel.model.normals;
const std::vector<Vector2f> &uvs = voxel.model.uvs;
const std::vector<float> &tangents = voxel.model.tangents;
const Vector3f pos(x - 1, y - 1, z - 1);
if (tangents.size() > 0) {
const int append_index = arrays.tangents.size();
arrays.tangents.resize(arrays.tangents.size() + vertex_count * 4);
memcpy(arrays.tangents.data() + append_index, tangents.data(),
(vertex_count * 4) * sizeof(float));
}
for (unsigned int i = 0; i < vertex_count; ++i) {
arrays.normals.push_back(normals[i]);
arrays.uvs.push_back(uvs[i]);
arrays.positions.push_back(positions[i] + pos);
// TODO handle ambient occlusion on inner parts
arrays.colors.push_back(modulate_color);
}
const std::vector<int> &indices = voxel.model.indices;
const unsigned int index_count = indices.size();
for (unsigned int i = 0; i < index_count; ++i) {
arrays.indices.push_back(index_offset + indices[i]);
}
index_offset += vertex_count;
// Inside
for (unsigned int surface_index = 0; surface_index < model.surface_count; ++surface_index) {
const VoxelBlockyModel::BakedData::Surface &surface = model.surfaces[surface_index];
if (surface.positions.size() == 0) {
continue;
}
// TODO Get rid of push_backs
VoxelMesherBlocky::Arrays &arrays = out_arrays_per_material[surface.material_id];
ZN_ASSERT(surface.material_id >= 0 && surface.material_id < index_offsets.size());
int &index_offset = index_offsets[surface.material_id];
const std::vector<Vector3f> &positions = surface.positions;
const unsigned int vertex_count = positions.size();
const Color modulate_color = voxel.color;
const std::vector<Vector3f> &normals = surface.normals;
const std::vector<Vector2f> &uvs = surface.uvs;
const std::vector<float> &tangents = surface.tangents;
const Vector3f pos(x - 1, y - 1, z - 1);
if (tangents.size() > 0) {
const int append_index = arrays.tangents.size();
arrays.tangents.resize(arrays.tangents.size() + vertex_count * 4);
memcpy(arrays.tangents.data() + append_index, tangents.data(),
(vertex_count * 4) * sizeof(float));
}
for (unsigned int i = 0; i < vertex_count; ++i) {
arrays.normals.push_back(normals[i]);
arrays.uvs.push_back(uvs[i]);
arrays.positions.push_back(positions[i] + pos);
// TODO handle ambient occlusion on inner parts
arrays.colors.push_back(modulate_color);
}
const std::vector<int> &indices = surface.indices;
const unsigned int index_count = indices.size();
for (unsigned int i = 0; i < index_count; ++i) {
arrays.indices.push_back(index_offset + indices[i]);
}
index_offset += vertex_count;
}
}
}
@ -383,8 +405,9 @@ void VoxelMesherBlocky::build(VoxelMesher::Output &output, const VoxelMesher::In
Cache &cache = _cache;
for (unsigned int i = 0; i < cache.arrays_per_material.size(); ++i) {
Arrays &a = cache.arrays_per_material[i];
std::vector<Arrays> &arrays_per_material = cache.arrays_per_material;
for (unsigned int i = 0; i < arrays_per_material.size(); ++i) {
Arrays &a = arrays_per_material[i];
a.clear();
}
@ -449,19 +472,26 @@ void VoxelMesherBlocky::build(VoxelMesher::Output &output, const VoxelMesher::In
const Vector3i block_size = voxels.get_size();
const VoxelBufferInternal::Depth channel_depth = voxels.get_channel_depth(channel);
unsigned int material_count = 0;
{
// We can only access baked data. Only this data is made for multithreaded access.
RWLockRead lock(params.library->get_baked_data_rw_lock());
const VoxelBlockyLibrary::BakedData &library_baked_data = params.library->get_baked_data();
material_count = library_baked_data.indexed_materials_count;
if (arrays_per_material.size() < material_count) {
arrays_per_material.resize(material_count);
}
switch (channel_depth) {
case VoxelBufferInternal::DEPTH_8_BIT:
generate_blocky_mesh(cache.arrays_per_material, raw_channel, block_size, library_baked_data,
generate_blocky_mesh(arrays_per_material, raw_channel, block_size, library_baked_data,
params.bake_occlusion, baked_occlusion_darkness);
break;
case VoxelBufferInternal::DEPTH_16_BIT:
generate_blocky_mesh(cache.arrays_per_material, raw_channel.reinterpret_cast_to<uint16_t>(), block_size,
generate_blocky_mesh(arrays_per_material, raw_channel.reinterpret_cast_to<uint16_t>(), block_size,
library_baked_data, params.bake_occlusion, baked_occlusion_darkness);
break;
@ -469,12 +499,16 @@ void VoxelMesherBlocky::build(VoxelMesher::Output &output, const VoxelMesher::In
ERR_PRINT("Unsupported voxel depth");
return;
}
output.surfaces.resize(material_count);
}
// TODO Optimization: we could return a single byte array and use Mesh::add_surface down the line?
// That API does not seem to exist yet though.
for (unsigned int i = 0; i < material_count; ++i) {
const Arrays &arrays = arrays_per_material[i];
for (unsigned int i = 0; i < MAX_MATERIALS; ++i) {
const Arrays &arrays = cache.arrays_per_material[i];
if (arrays.positions.size() != 0) {
Array mesh_arrays;
mesh_arrays.resize(Mesh::ARRAY_MAX);
@ -497,6 +531,7 @@ void VoxelMesherBlocky::build(VoxelMesher::Output &output, const VoxelMesher::In
mesh_arrays[Mesh::ARRAY_NORMAL] = normals;
mesh_arrays[Mesh::ARRAY_COLOR] = colors;
mesh_arrays[Mesh::ARRAY_INDEX] = indices;
if (arrays.tangents.size() > 0) {
PackedFloat32Array tangents;
raw_copy_to(tangents, arrays.tangents);
@ -504,12 +539,14 @@ void VoxelMesherBlocky::build(VoxelMesher::Output &output, const VoxelMesher::In
}
}
output.surfaces.push_back(mesh_arrays);
} else {
// Empty
output.surfaces.push_back(Array());
ZN_ASSERT(i < output.surfaces.size());
Output::Surface &surface = output.surfaces[i];
surface.arrays = mesh_arrays;
}
// else {
// // Empty
// output.surfaces.push_back(Output::Surface());
// }
}
output.primitive_type = Mesh::PRIMITIVE_TRIANGLES;
@ -535,6 +572,14 @@ int VoxelMesherBlocky::get_used_channels_mask() const {
return (1 << VoxelBufferInternal::CHANNEL_TYPE);
}
Ref<Material> VoxelMesherBlocky::get_material_by_index(unsigned int index) const {
Ref<VoxelBlockyLibrary> lib = get_library();
if (lib.is_null()) {
return Ref<Material>();
}
return lib->get_material_by_index(index);
}
#ifdef TOOLS_ENABLED
void VoxelMesherBlocky::get_configuration_warnings(TypedArray<String> &out_warnings) const {

View File

@ -17,7 +17,6 @@ class VoxelMesherBlocky : public VoxelMesher {
GDCLASS(VoxelMesherBlocky, VoxelMesher)
public:
static const unsigned int MAX_MATERIALS = 8; // Arbitrary. Tweak if needed.
static const int PADDING = 1;
VoxelMesherBlocky();
@ -41,6 +40,8 @@ public:
return false;
}
Ref<Material> get_material_by_index(unsigned int index) const;
// Using std::vector because they make this mesher twice as fast than Godot Vectors.
// See why: https://github.com/godotengine/godot/issues/24731
struct Arrays {
@ -76,7 +77,7 @@ private:
};
struct Cache {
FixedArray<Arrays, MAX_MATERIALS> arrays_per_material;
std::vector<Arrays> arrays_per_material;
};
// Parameters

View File

@ -897,8 +897,10 @@ void VoxelMesherCubes::build(VoxelMesher::Output &output, const VoxelMesher::Inp
for (unsigned int material_index = 0; material_index < MATERIAL_COUNT; ++material_index) {
const Arrays &arrays = cache.arrays_per_material[material_index];
Output::Surface surface;
if (arrays.positions.size() != 0) {
Array mesh_arrays;
Array &mesh_arrays = surface.arrays;
mesh_arrays.resize(Mesh::ARRAY_MAX);
{
@ -926,12 +928,13 @@ void VoxelMesherCubes::build(VoxelMesher::Output &output, const VoxelMesher::Inp
}
}
output.surfaces.push_back(mesh_arrays);
} else {
// Empty
output.surfaces.push_back(Array());
//surface.collision_enabled = (material_index == MATERIAL_OPAQUE);
}
// else {
// // Empty
// }
output.surfaces.push_back(surface);
}
output.primitive_type = Mesh::PRIMITIVE_TRIANGLES;
@ -1005,6 +1008,31 @@ int VoxelMesherCubes::get_used_channels_mask() const {
return (1 << VoxelBufferInternal::CHANNEL_COLOR);
}
void VoxelMesherCubes::set_material_by_index(Materials id, Ref<Material> material) {
_materials[id] = material;
}
Ref<Material> VoxelMesherCubes::get_material_by_index(unsigned int i) const {
ERR_FAIL_INDEX_V(i, _materials.size(), Ref<Material>());
return _materials[i];
}
void VoxelMesherCubes::_b_set_opaque_material(Ref<Material> material) {
set_material_by_index(MATERIAL_OPAQUE, material);
}
Ref<Material> VoxelMesherCubes::_b_get_opaque_material() const {
return get_material_by_index(MATERIAL_OPAQUE);
}
void VoxelMesherCubes::_b_set_transparent_material(Ref<Material> material) {
set_material_by_index(MATERIAL_TRANSPARENT, material);
}
Ref<Material> VoxelMesherCubes::_b_get_transparent_material() const {
return get_material_by_index(MATERIAL_TRANSPARENT);
}
void VoxelMesherCubes::_bind_methods() {
ClassDB::bind_method(
D_METHOD("set_greedy_meshing_enabled", "enable"), &VoxelMesherCubes::set_greedy_meshing_enabled);
@ -1016,6 +1044,15 @@ void VoxelMesherCubes::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_color_mode", "mode"), &VoxelMesherCubes::set_color_mode);
ClassDB::bind_method(D_METHOD("get_color_mode"), &VoxelMesherCubes::get_color_mode);
ClassDB::bind_method(D_METHOD("set_material_by_index", "id", "material"), &VoxelMesherCubes::set_material_by_index);
ClassDB::bind_method(D_METHOD("_get_opaque_material"), &VoxelMesherCubes::_b_get_opaque_material);
ClassDB::bind_method(D_METHOD("_set_opaque_material", "material"), &VoxelMesherCubes::_b_set_opaque_material);
ClassDB::bind_method(D_METHOD("_get_transparent_material"), &VoxelMesherCubes::_b_get_transparent_material);
ClassDB::bind_method(
D_METHOD("_set_transparent_material", "material"), &VoxelMesherCubes::_b_set_transparent_material);
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "greedy_meshing_enabled"), "set_greedy_meshing_enabled",
"is_greedy_meshing_enabled");
ADD_PROPERTY(PropertyInfo(Variant::INT, "color_mode", PROPERTY_HINT_ENUM, "Raw,MesherPalette,ShaderPalette"),
@ -1023,6 +1060,12 @@ void VoxelMesherCubes::_bind_methods() {
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "palette", PROPERTY_HINT_RESOURCE_TYPE,
VoxelColorPalette::get_class_static()),
"set_palette", "get_palette");
ADD_PROPERTY(
PropertyInfo(Variant::OBJECT, "opaque_material", PROPERTY_HINT_RESOURCE_TYPE, Material::get_class_static()),
"_set_opaque_material", "_get_opaque_material");
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "transparent_material", PROPERTY_HINT_RESOURCE_TYPE,
Material::get_class_static()),
"_set_transparent_material", "_get_transparent_material");
BIND_ENUM_CONSTANT(MATERIAL_OPAQUE);
BIND_ENUM_CONSTANT(MATERIAL_TRANSPARENT);

View File

@ -61,6 +61,9 @@ public:
return true;
}
void set_material_by_index(Materials id, Ref<Material> material);
Ref<Material> get_material_by_index(unsigned int i) const override;
// Structs
// Using std::vector because they make this mesher twice as fast than Godot Vectors.
@ -98,10 +101,15 @@ public:
}
};
protected:
private:
void _b_set_opaque_material(Ref<Material> material);
Ref<Material> _b_get_opaque_material() const;
void _b_set_transparent_material(Ref<Material> material);
Ref<Material> _b_get_transparent_material() const;
static void _bind_methods();
private:
struct Parameters {
ColorMode color_mode = COLOR_RAW;
Ref<VoxelColorPalette> palette;
@ -119,6 +127,8 @@ private:
Parameters _parameters;
RWLock _parameters_lock;
FixedArray<Ref<Material>, MATERIAL_COUNT> _materials;
// Work cache
static thread_local Cache _cache;
};

View File

@ -1520,7 +1520,9 @@ void VoxelMesherDMC::build(VoxelMesher::Output &output, const VoxelMesher::Input
}
// surfaces[material][array_type], for now single material
output.surfaces.push_back(surface);
Output::Surface output_surface;
output_surface.arrays = surface;
output.surfaces.push_back(output_surface);
if (params.mesh_mode == MESH_NORMAL) {
output.primitive_type = Mesh::PRIMITIVE_TRIANGLES;

View File

@ -217,7 +217,7 @@ void VoxelMesherTransvoxel::build(VoxelMesher::Output &output, const VoxelMesher
fill_surface_arrays(regular_arrays, s_mesh_arrays);
}
output.surfaces.push_back(regular_arrays);
output.surfaces.push_back({ regular_arrays });
for (int dir = 0; dir < Cube::SIDE_COUNT; ++dir) {
ZN_PROFILE_SCOPE();
@ -233,7 +233,7 @@ void VoxelMesherTransvoxel::build(VoxelMesher::Output &output, const VoxelMesher
Array transition_arrays;
fill_surface_arrays(transition_arrays, s_mesh_arrays);
output.transition_surfaces[dir].push_back(transition_arrays);
output.transition_surfaces[dir].push_back({ transition_arrays });
}
// const uint64_t time_spent = Time::get_singleton()->get_ticks_usec() - time_before;

View File

@ -20,20 +20,28 @@ Ref<Mesh> VoxelMesher::build_mesh(Ref<gd::VoxelBuffer> voxels, TypedArray<Materi
int surface_index = 0;
for (unsigned int i = 0; i < output.surfaces.size(); ++i) {
Array surface = output.surfaces[i];
if (surface.is_empty()) {
Output::Surface &surface = output.surfaces[i];
Array &arrays = surface.arrays;
if (arrays.is_empty()) {
continue;
}
CRASH_COND(surface.size() != Mesh::ARRAY_MAX);
if (!is_surface_triangulated(surface)) {
CRASH_COND(arrays.size() != Mesh::ARRAY_MAX);
if (!is_surface_triangulated(arrays)) {
continue;
}
mesh->add_surface_from_arrays(output.primitive_type, surface, Array(), Dictionary(), output.mesh_flags);
Ref<Material> material;
if (int(i) < materials.size()) {
mesh->surface_set_material(surface_index, materials[i]);
material = materials[i];
}
if (material.is_null()) {
material = get_material_by_index(i);
}
mesh->add_surface_from_arrays(output.primitive_type, arrays, Array(), Dictionary(), output.mesh_flags);
mesh->surface_set_material(surface_index, material);
++surface_index;
}
@ -59,6 +67,11 @@ void VoxelMesher::set_padding(int minimum, int maximum) {
_maximum_padding = maximum;
}
Ref<Material> VoxelMesher::get_material_by_index(unsigned int i) const {
// May be implemented in some meshers
return Ref<Material>();
}
void VoxelMesher::_bind_methods() {
// Shortcut if you want to generate a mesh directly from a fixed grid of voxels.
// Useful for testing the different meshers.

View File

@ -35,9 +35,12 @@ public:
};
struct Output {
// Each surface correspond to a different material
std::vector<Array> surfaces;
FixedArray<std::vector<Array>, Cube::SIDE_COUNT> transition_surfaces;
struct Surface {
Array arrays;
};
// Each surface correspond to a different material and can be empty.
std::vector<Surface> surfaces;
FixedArray<std::vector<Surface>, Cube::SIDE_COUNT> transition_surfaces;
Mesh::PrimitiveType primitive_type = Mesh::PRIMITIVE_TRIANGLES;
unsigned int mesh_flags = 0;
Ref<Image> atlas_image;
@ -72,6 +75,10 @@ public:
return true;
}
// Some meshers can provide materials themselves. These will be used for corresponding surfaces. Returns null if the
// index does not have a material assigned. If not provided here, a default material may be used.
virtual Ref<Material> get_material_by_index(unsigned int i) const;
#ifdef TOOLS_ENABLED
virtual void get_configuration_warnings(TypedArray<String> &out_warnings) const {}
#endif

View File

@ -33,6 +33,10 @@ public:
return _mesh_state;
}
void set_material_override(Ref<Material> material) {
_mesh_instance.set_material_override(material);
}
private:
MeshState _mesh_state = MESH_NEVER_UPDATED;
};

View File

@ -77,40 +77,18 @@ VoxelTerrain::~VoxelTerrain() {
VoxelServer::get_singleton().remove_volume(_volume_id);
}
// TODO See if there is a way to specify materials in voxels directly?
bool VoxelTerrain::_set(const StringName &p_name, const Variant &p_value) {
if (p_name.operator String().begins_with("material/")) {
unsigned int idx = p_name.operator String().get_slicec('/', 1).to_int();
ERR_FAIL_COND_V(idx >= VoxelMesherBlocky::MAX_MATERIALS || idx < 0, false);
set_material(idx, p_value);
return true;
void VoxelTerrain::set_material_override(Ref<Material> material) {
if (_material_override == material) {
return;
}
return false;
_material_override = material;
_mesh_map.for_each_block([material](VoxelMeshBlockVT &block) { //
block.set_material_override(material);
});
}
bool VoxelTerrain::_get(const StringName &p_name, Variant &r_ret) const {
if (p_name.operator String().begins_with("material/")) {
unsigned int idx = p_name.operator String().get_slicec('/', 1).to_int();
ERR_FAIL_COND_V(idx >= VoxelMesherBlocky::MAX_MATERIALS || idx < 0, false);
r_ret = get_material(idx);
return true;
}
return false;
}
void VoxelTerrain::_get_property_list(List<PropertyInfo> *p_list) const {
// Need to add a group here because otherwise it appears under the last group declared in `_bind_methods`
p_list->push_back(PropertyInfo(Variant::NIL, "Materials", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_GROUP));
const String material_type_hint = ShaderMaterial::get_class_static() + "," + BaseMaterial3D::get_class_static();
for (unsigned int i = 0; i < VoxelMesherBlocky::MAX_MATERIALS; ++i) {
p_list->push_back(
PropertyInfo(Variant::OBJECT, "material/" + itos(i), PROPERTY_HINT_RESOURCE_TYPE, material_type_hint));
}
Ref<Material> VoxelTerrain::get_material_override() const {
return _material_override;
}
void VoxelTerrain::set_stream(Ref<VoxelStream> p_stream) {
@ -403,36 +381,6 @@ bool VoxelTerrain::is_automatic_loading_enabled() const {
return _automatic_loading_enabled;
}
void VoxelTerrain::set_material(unsigned int id, Ref<Material> material) {
ERR_FAIL_INDEX(id, VoxelMesherBlocky::MAX_MATERIALS);
Ref<Material> old_material = _materials[id];
if (material != old_material) {
// Update existing meshes
_mesh_map.for_each_block([material, old_material](VoxelMeshBlockVT &block) {
Ref<Mesh> mesh = block.get_mesh();
if (mesh.is_valid()) {
// We can't just assign by material index because some meshes don't use all materials of the
// terrain, therefore they don't have as many surfaces. So we have to find which surfaces use the
// old material.
for (int surface_index = 0; surface_index < mesh->get_surface_count(); ++surface_index) {
if (mesh->surface_get_material(surface_index) == old_material) {
mesh->surface_set_material(surface_index, material);
}
}
}
});
_materials[id] = material;
}
}
Ref<Material> VoxelTerrain::get_material(unsigned int id) const {
ERR_FAIL_INDEX_V(id, VoxelMesherBlocky::MAX_MATERIALS, Ref<Material>());
return _materials[id];
}
void VoxelTerrain::try_schedule_mesh_update(VoxelMeshBlockVT &mesh_block) {
if (mesh_block.get_mesh_state() == VoxelMeshBlockVT::MESH_UPDATE_NOT_SENT) {
// Already in the list
@ -1491,31 +1439,33 @@ void VoxelTerrain::apply_mesh_update(const VoxelServer::BlockMeshOutput &ob) {
Ref<ArrayMesh> mesh;
//need to put both blocky and smooth surfaces into one list
std::vector<Array> collidable_surfaces;
int surface_index = 0;
for (unsigned int i = 0; i < ob.surfaces.surfaces.size(); ++i) {
Array surface = ob.surfaces.surfaces[i];
if (surface.is_empty()) {
int gd_surface_index = 0;
for (unsigned int surface_index = 0; surface_index < ob.surfaces.surfaces.size(); ++surface_index) {
const VoxelMesher::Output::Surface &surface = ob.surfaces.surfaces[surface_index];
Array arrays = surface.arrays;
if (arrays.is_empty()) {
continue;
}
CRASH_COND(surface.size() != Mesh::ARRAY_MAX);
if (!is_surface_triangulated(surface)) {
CRASH_COND(arrays.size() != Mesh::ARRAY_MAX);
if (!is_surface_triangulated(arrays)) {
continue;
}
collidable_surfaces.push_back(surface);
collidable_surfaces.push_back(arrays);
if (mesh.is_null()) {
mesh.instantiate();
}
mesh->add_surface_from_arrays(
ob.surfaces.primitive_type, surface, Array(), Dictionary(), ob.surfaces.mesh_flags);
mesh->surface_set_material(surface_index, _materials[i]);
++surface_index;
ob.surfaces.primitive_type, arrays, Array(), Dictionary(), ob.surfaces.mesh_flags);
Ref<Material> material = _mesher->get_material_by_index(surface_index);
mesh->surface_set_material(gd_surface_index, material);
++gd_surface_index;
}
if (mesh.is_valid() && is_mesh_empty(**mesh)) {
@ -1531,13 +1481,17 @@ void VoxelTerrain::apply_mesh_update(const VoxelServer::BlockMeshOutput &ob) {
if (ob.surfaces.surfaces.size() > 0 && mesh.is_valid() && !block->has_mesh()) {
// TODO The mesh could come from an edited region!
// We would have to know if specific voxels got edited, or different from the generator
_instancer->on_mesh_block_enter(ob.position, ob.lod, ob.surfaces.surfaces[0]);
// TODO Support multi-surfaces in VoxelInstancer
_instancer->on_mesh_block_enter(ob.position, ob.lod, ob.surfaces.surfaces[0].arrays);
}
}
const bool gen_collisions = _generate_collisions && block->collision_viewers.get() > 0;
block->set_mesh(mesh, DirectMeshInstance::GIMode(get_gi_mode()));
if (_material_override.is_valid()) {
block->set_material_override(_material_override);
}
if (gen_collisions) {
block->set_collision_mesh(to_span_const(collidable_surfaces), get_tree()->is_debugging_collisions_hint(), this,
_collision_margin);
@ -1677,8 +1631,8 @@ PackedInt32Array VoxelTerrain::_b_get_viewer_network_peer_ids_in_area(Vector3i a
}
void VoxelTerrain::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_material", "id", "material"), &VoxelTerrain::set_material);
ClassDB::bind_method(D_METHOD("get_material", "id"), &VoxelTerrain::get_material);
ClassDB::bind_method(D_METHOD("set_material_override", "material"), &VoxelTerrain::set_material_override);
ClassDB::bind_method(D_METHOD("get_material_override"), &VoxelTerrain::get_material_override);
ClassDB::bind_method(D_METHOD("set_max_view_distance", "distance_in_voxels"), &VoxelTerrain::set_max_view_distance);
ClassDB::bind_method(D_METHOD("get_max_view_distance"), &VoxelTerrain::get_max_view_distance);
@ -1755,6 +1709,10 @@ void VoxelTerrain::_bind_methods() {
"get_collision_mask");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "collision_margin"), "set_collision_margin", "get_collision_margin");
ADD_GROUP("Materials", "");
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "material_override"), "set_material_override", "get_material_override");
ADD_GROUP("Networking", "");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "block_enter_notification_enabled"),

View File

@ -77,8 +77,8 @@ public:
void set_automatic_loading_enabled(bool enable);
bool is_automatic_loading_enabled() const;
void set_material(unsigned int id, Ref<Material> material);
Ref<Material> get_material(unsigned int id) const;
void set_material_override(Ref<Material> material);
Ref<Material> get_material_override() const;
VoxelDataMap &get_storage() {
return _data_map;
@ -146,10 +146,6 @@ protected:
void _on_gi_mode_changed() override;
private:
bool _set(const StringName &p_name, const Variant &p_value);
bool _get(const StringName &p_name, Variant &r_ret) const;
void _get_property_list(List<PropertyInfo> *p_list) const;
void _process();
void process_viewers();
//void process_received_data_blocks();
@ -279,7 +275,7 @@ private:
// If enabled, VoxelViewers will cause blocks to automatically load around them.
bool _automatic_loading_enabled = true;
Ref<Material> _materials[VoxelMesherBlocky::MAX_MATERIALS];
Ref<Material> _material_override;
GodotObjectUniquePtr<VoxelDataBlockEnterInfo> _data_block_enter_info_obj;

View File

@ -27,21 +27,22 @@ namespace zylann::voxel {
namespace {
Ref<ArrayMesh> build_mesh(
Span<const Array> surfaces, Mesh::PrimitiveType primitive, int flags, Ref<Material> material) {
Ref<ArrayMesh> build_mesh(Span<const VoxelMesher::Output::Surface> surfaces, Mesh::PrimitiveType primitive, int flags,
Ref<Material> material) {
ZN_PROFILE_SCOPE();
Ref<ArrayMesh> mesh;
unsigned int surface_index = 0;
for (unsigned int i = 0; i < surfaces.size(); ++i) {
Array surface = surfaces[i];
const VoxelMesher::Output::Surface &surface = surfaces[i];
Array arrays = surface.arrays;
if (surface.is_empty()) {
if (arrays.is_empty()) {
continue;
}
CRASH_COND(surface.size() != Mesh::ARRAY_MAX);
if (!is_surface_triangulated(surface)) {
CRASH_COND(arrays.size() != Mesh::ARRAY_MAX);
if (!is_surface_triangulated(arrays)) {
continue;
}
@ -51,7 +52,7 @@ Ref<ArrayMesh> build_mesh(
// TODO Use `add_surface`, it's about 20% faster after measuring in Tracy (though we may see if Godot 4 expects
// the same)
mesh->add_surface_from_arrays(primitive, surface, Array(), Dictionary(), flags);
mesh->add_surface_from_arrays(primitive, arrays, Array(), Dictionary(), flags);
mesh->surface_set_material(surface_index, material);
// No multi-material supported yet
++surface_index;
@ -1434,7 +1435,7 @@ void VoxelLodTerrain::apply_mesh_update(const VoxelServer::BlockMeshOutput &ob)
if (_instancer != nullptr && ob.surfaces.surfaces.size() > 0) {
// TODO The mesh could come from an edited region!
// We would have to know if specific voxels got edited, or different from the generator
_instancer->on_mesh_block_enter(ob.position, ob.lod, ob.surfaces.surfaces[0]);
_instancer->on_mesh_block_enter(ob.position, ob.lod, ob.surfaces.surfaces[0].arrays);
}
// Lazy initialization
@ -1474,7 +1475,7 @@ void VoxelLodTerrain::apply_mesh_update(const VoxelServer::BlockMeshOutput &ob)
{
ZN_PROFILE_SCOPE_NAMED("Transition meshes");
for (unsigned int dir = 0; dir < mesh_data.transition_surfaces.size(); ++dir) {
Ref<ArrayMesh> transition_mesh = build_mesh(to_span_const(mesh_data.transition_surfaces[dir]),
Ref<ArrayMesh> transition_mesh = build_mesh(to_span(mesh_data.transition_surfaces[dir]),
mesh_data.primitive_type, mesh_data.mesh_flags, _material);
block->set_transition_mesh(transition_mesh, dir, DirectMeshInstance::GIMode(get_gi_mode()));
@ -1485,8 +1486,15 @@ void VoxelLodTerrain::apply_mesh_update(const VoxelServer::BlockMeshOutput &ob)
if (has_collision) {
if (_collision_update_delay == 0 ||
static_cast<int>(now - block->last_collider_update_time) > _collision_update_delay) {
block->set_collision_mesh(to_span_const(mesh_data.surfaces), get_tree()->is_debugging_collisions_hint(),
this, _collision_margin);
static thread_local std::vector<Array> tls_collidable_surfaces;
std::vector<Array> &collidable_surfaces = tls_collidable_surfaces;
collidable_surfaces.clear();
for (unsigned int i = 0; i < mesh_data.surfaces.size(); ++i) {
collidable_surfaces.push_back(mesh_data.surfaces[i].arrays);
}
block->set_collision_mesh(
to_span(collidable_surfaces), get_tree()->is_debugging_collisions_hint(), this, _collision_margin);
block->set_collision_layer(_collision_layer);
block->set_collision_mask(_collision_mask);
block->last_collider_update_time = now;
@ -1502,7 +1510,10 @@ void VoxelLodTerrain::apply_mesh_update(const VoxelServer::BlockMeshOutput &ob)
// The caller providing `mesh_data` doesnt use `mesh_data` later so we could have moved the vector,
// but at the moment it's passed with `const` so that isn't possible. Indeed we are only going to read the
// data, but `const` also means the structure holding it is read-only as well.
block->deferred_collider_data = mesh_data.surfaces;
block->deferred_collider_data.resize(mesh_data.surfaces.size());
for (size_t i = 0; i < mesh_data.surfaces.size(); ++i) {
block->deferred_collider_data[i] = mesh_data.surfaces[i].arrays;
}
}
}