WIP normalmaps for Transvoxel.

Adds enough stuff to test the approach. It is not correct at the
moment, probably needs to project to triangles to make cells line up,
currently it's using planes. There might be incorrect coordinates too,
did that with bit of guessing. A grid-like pattern is also showing, so
might have to make tiles a bit larger to account for filtering.
master
Marc Gilleron 2022-07-31 16:14:36 +01:00
parent b5257f0f48
commit fcddf4da61
14 changed files with 487 additions and 25 deletions

View File

@ -38,6 +38,9 @@ VoxelStringNames::VoxelStringNames() {
u_block_local_transform = StaticCString::create("u_block_local_transform");
u_lod_fade = StaticCString::create("u_lod_fade");
voxel_normalmap_atlas = StaticCString::create("voxel_normalmap_atlas");
voxel_normalmap_lookup = StaticCString::create("voxel_normalmap_lookup");
#ifdef DEBUG_ENABLED
_voxel_debug_vt_position = StaticCString::create("_voxel_debug_vt_position");
#endif

View File

@ -33,6 +33,9 @@ public:
StringName u_block_local_transform;
StringName u_lod_fade;
StringName voxel_normalmap_atlas;
StringName voxel_normalmap_lookup;
#ifdef DEBUG_ENABLED
StringName _voxel_debug_vt_position;
#endif

View File

@ -621,7 +621,9 @@ Array separate_floating_chunks(VoxelTool &voxel_tool, Box3i world_box, Node *par
}
}
Ref<Mesh> mesh = mesher->build_mesh(info.voxels, materials);
// TODO If normalmapping is used here with the Transvoxel mesher, we need to either turn it off just for
// this call, or to pass the right options
Ref<Mesh> mesh = mesher->build_mesh(info.voxels, materials, Dictionary());
// The mesh is not supposed to be null,
// because we build these buffers from connected groups that had negative SDF.
ERR_CONTINUE(mesh.is_null());

View File

@ -1081,6 +1081,53 @@ void VoxelGeneratorGraph::generate_set(Span<float> in_x, Span<float> in_y, Span<
runtime.generate_set(cache.state, in_x, in_y, in_z, false, nullptr);
}
void VoxelGeneratorGraph::generate_series(Span<const float> positions_x, Span<const float> positions_y,
Span<const float> positions_z, unsigned int channel, Span<float> out_values, Vector3f min_pos,
Vector3f max_pos) {
std::shared_ptr<Runtime> runtime_ptr;
{
RWLockRead rlock(_runtime_lock);
runtime_ptr = _runtime;
}
if (runtime_ptr == nullptr) {
return;
}
int buffer_index;
float defval = 0.f;
switch (channel) {
case VoxelBufferInternal::CHANNEL_SDF:
buffer_index = runtime_ptr->sdf_output_buffer_index;
defval = 1.f;
break;
case VoxelBufferInternal::CHANNEL_TYPE:
buffer_index = runtime_ptr->type_output_buffer_index;
break;
default:
ZN_PRINT_ERROR("Unexpected channel");
return;
}
if (buffer_index == -1) {
for (unsigned int i = 0; i < out_values.size(); ++i) {
out_values[i] = defval;
}
return;
}
{
// The implementation cannot guarantee constness at compile time, but it should not modifiy the data either way
float *ptr_x = const_cast<float *>(positions_x.data());
float *ptr_y = const_cast<float *>(positions_y.data());
float *ptr_z = const_cast<float *>(positions_z.data());
generate_set(Span<float>(ptr_x, positions_x.size()), Span<float>(ptr_y, positions_y.size()),
Span<float>(ptr_z, positions_z.size()));
}
const VoxelGraphRuntime::Buffer &buffer = _cache.state.get_buffer(buffer_index);
memcpy(out_values.data(), buffer.data, sizeof(float) * out_values.size());
}
const VoxelGraphRuntime::State &VoxelGeneratorGraph::get_last_state_from_current_thread() {
return _cache.state;
}

View File

@ -168,8 +168,14 @@ public:
bool supports_single_generation() const override {
return true;
}
bool supports_series_generation() const override {
return true;
}
VoxelSingleValue generate_single(Vector3i position, unsigned int channel) override;
void generate_series(Span<const float> positions_x, Span<const float> positions_y, Span<const float> positions_z,
unsigned int channel, Span<float> out_values, Vector3f min_pos, Vector3f max_pos) override;
Ref<Resource> duplicate(bool p_subresources) const override;
// Utility

View File

@ -15,6 +15,9 @@ int VoxelGenerator::get_used_channels_mask() const {
}
VoxelSingleValue VoxelGenerator::generate_single(Vector3i pos, unsigned int channel) {
VoxelSingleValue v;
v.i = 0;
ZN_ASSERT_RETURN_V(channel >= 0 && channel < VoxelBufferInternal::MAX_CHANNELS, v);
// Default slow implementation
// TODO Optimize: a small part of the slowness is caused by the allocator.
// It is not a good use of `VoxelMemoryPool` for such a small size called so often.
@ -23,7 +26,6 @@ VoxelSingleValue VoxelGenerator::generate_single(Vector3i pos, unsigned int chan
buffer.create(1, 1, 1);
VoxelQueryData q{ buffer, pos, 0 };
generate_block(q);
VoxelSingleValue v;
if (channel == VoxelBufferInternal::CHANNEL_SDF) {
v.f = buffer.get_voxel_f(0, 0, 0, channel);
} else {
@ -32,6 +34,12 @@ VoxelSingleValue VoxelGenerator::generate_single(Vector3i pos, unsigned int chan
return v;
}
void VoxelGenerator::generate_series(Span<const float> positions_x, Span<const float> positions_y,
Span<const float> positions_z, unsigned int channel, Span<float> out_values, Vector3f min_pos,
Vector3f max_pos) {
ZN_PRINT_ERROR("Not implemented");
}
void VoxelGenerator::_b_generate_block(Ref<gd::VoxelBuffer> out_buffer, Vector3 origin_in_voxels, int lod) {
ERR_FAIL_COND(lod < 0);
ERR_FAIL_COND(lod >= int(constants::MAX_LOD));

View File

@ -1,6 +1,8 @@
#ifndef VOXEL_GENERATOR_H
#define VOXEL_GENERATOR_H
#include "../util/math/vector3f.h"
#include "../util/span.h"
#include <core/io/resource.h>
#include <core/math/vector3i.h>
#include <core/variant/typed_array.h>
@ -47,13 +49,16 @@ public:
return false;
}
virtual bool supports_series_generation() const {
return false;
}
// TODO Not sure if it's a good API regarding performance
virtual VoxelSingleValue generate_single(Vector3i pos, unsigned int channel);
// virtual void generate_series(
// Span<const Vector3> positions,
// Span<const uint8_t> channels,
// Span<Span<VoxelSingleValue>> out_values);
virtual void generate_series(Span<const float> positions_x, Span<const float> positions_y,
Span<const float> positions_z, unsigned int channel, Span<float> out_values, Vector3f min_pos,
Vector3f max_pos);
// Declares the channels this generator will use
virtual int get_used_channels_mask() const;

View File

@ -305,7 +305,8 @@ Vector3f binary_search_interpolate(const IDeepSDFSampler &sampler, float s0, flo
template <typename Sdf_T, typename WeightSampler_T>
void build_regular_mesh(Span<const Sdf_T> sdf_data, TextureIndicesData texture_indices_data,
const WeightSampler_T &weights_sampler, const Vector3i block_size_with_padding, uint32_t lod_index,
TexturingMode texturing_mode, Cache &cache, MeshArrays &output, const IDeepSDFSampler *deep_sdf_sampler) {
TexturingMode texturing_mode, Cache &cache, MeshArrays &output, const IDeepSDFSampler *deep_sdf_sampler,
std::vector<CellInfo> *cell_info) {
ZN_PROFILE_SCOPE();
// This function has some comments as quotes from the Transvoxel paper.
@ -678,6 +679,10 @@ void build_regular_mesh(Span<const Sdf_T> sdf_data, TextureIndicesData texture_i
output.indices.push_back(i2);
}
if (cell_info != nullptr) {
cell_info->push_back(CellInfo{ pos - min_pos, triangle_count });
}
} // x
} // y
} // z
@ -1309,7 +1314,7 @@ Span<const Sdf_T> apply_zero_sdf_fix(Span<const Sdf_T> p_sdf_data) {
DefaultTextureIndicesData build_regular_mesh(const VoxelBufferInternal &voxels, unsigned int sdf_channel,
uint32_t lod_index, TexturingMode texturing_mode, Cache &cache, MeshArrays &output,
const IDeepSDFSampler *deep_sdf_sampler) {
const IDeepSDFSampler *deep_sdf_sampler, std::vector<CellInfo> *cell_infos) {
ZN_PROFILE_SCOPE();
// From this point, we expect the buffer to contain allocated data in the relevant channels.
@ -1357,13 +1362,13 @@ DefaultTextureIndicesData build_regular_mesh(const VoxelBufferInternal &voxels,
case VoxelBufferInternal::DEPTH_8_BIT: {
Span<const int8_t> sdf_data = sdf_data_raw.reinterpret_cast_to<const int8_t>();
build_regular_mesh<int8_t>(sdf_data, indices_data, weights_data, voxels.get_size(), lod_index,
texturing_mode, cache, output, deep_sdf_sampler);
texturing_mode, cache, output, deep_sdf_sampler, cell_infos);
} break;
case VoxelBufferInternal::DEPTH_16_BIT: {
Span<const int16_t> sdf_data = sdf_data_raw.reinterpret_cast_to<const int16_t>();
build_regular_mesh<int16_t>(sdf_data, indices_data, weights_data, voxels.get_size(), lod_index,
texturing_mode, cache, output, deep_sdf_sampler);
texturing_mode, cache, output, deep_sdf_sampler, cell_infos);
} break;
// TODO Remove support for 32-bit SDF in Transvoxel?
@ -1372,7 +1377,7 @@ DefaultTextureIndicesData build_regular_mesh(const VoxelBufferInternal &voxels,
case VoxelBufferInternal::DEPTH_32_BIT: {
Span<const float> sdf_data = sdf_data_raw.reinterpret_cast_to<const float>();
build_regular_mesh<float>(sdf_data, indices_data, weights_data, voxels.get_size(), lod_index,
texturing_mode, cache, output, deep_sdf_sampler);
texturing_mode, cache, output, deep_sdf_sampler, cell_infos);
} break;
case VoxelBufferInternal::DEPTH_64_BIT:

View File

@ -150,9 +150,14 @@ public:
virtual float get_single(const Vector3i position_in_voxels, uint32_t lod_index) const = 0;
};
struct CellInfo {
Vector3i position;
uint32_t triangle_count;
};
DefaultTextureIndicesData build_regular_mesh(const VoxelBufferInternal &voxels, unsigned int sdf_channel,
uint32_t lod_index, TexturingMode texturing_mode, Cache &cache, MeshArrays &output,
const IDeepSDFSampler *deep_sdf_sampler);
const IDeepSDFSampler *deep_sdf_sampler, std::vector<CellInfo> *cell_infos);
void build_transition_mesh(const VoxelBufferInternal &voxels, unsigned int sdf_channel, int direction,
uint32_t lod_index, TexturingMode texturing_mode, Cache &cache, MeshArrays &output,

View File

@ -4,6 +4,7 @@
#include "../../storage/voxel_data_map.h"
#include "../../thirdparty/meshoptimizer/meshoptimizer.h"
#include "../../util/godot/funcs.h"
#include "../../util/math/conv.h"
#include "../../util/profiling.h"
#include "transvoxel_shader_minimal.h"
#include "transvoxel_tables.cpp"
@ -186,6 +187,267 @@ struct DeepSampler : transvoxel::IDeepSDFSampler {
}
};
struct NormalMapData {
std::vector<Vector3f> normals;
struct Tile {
uint8_t x;
uint8_t y;
uint8_t z;
uint8_t axis;
};
std::vector<Tile> tiles;
inline void clear() {
normals.clear();
tiles.clear();
}
};
// inline uint8_t get_direction_id(Vector3f v) {
// const Vector3f::Axis longest_axis = math::get_longest_axis(v);
// return 2 * longest_axis + (v[longest_axis] >= 0.f);
// }
// For each non-empty cell of the mesh, choose an axis-aligned projection based on triangle normals in the cell.
// Sample voxels inside the cell to compute a tile of world space normals from the SDF.
static void compute_normalmap(Span<const transvoxel::CellInfo> cell_infos, const transvoxel::MeshArrays &mesh,
NormalMapData &normal_map_data, unsigned int tile_resolution, VoxelGenerator &generator,
Vector3i origin_in_voxels, unsigned int lod_index) {
ZN_PROFILE_SCOPE();
normal_map_data.normals.reserve(math::squared(tile_resolution) * cell_infos.size());
const unsigned int cell_size = 1 << lod_index;
const float step = float(cell_size) / tile_resolution;
// Each normal needs 4 samples:
// (x, y, z )
// (x+s, y, z )
// (x, y+s, z )
// (x, y, z+s)
// We can avoid sampling neighbors multiple times.
// So the SDF buffer will store each sample of the tile with 1 extra neighbor in each axis: (res+1)*(res+1)*2
// Then we'll derive normals from neighbor differences.
const unsigned int buffer_size = math::squared(tile_resolution + 1) * 2;
static thread_local std::vector<float> tls_sdf_buffer;
static thread_local std::vector<float> tls_x_buffer;
static thread_local std::vector<float> tls_y_buffer;
static thread_local std::vector<float> tls_z_buffer;
tls_sdf_buffer.resize(buffer_size);
tls_x_buffer.resize(buffer_size);
tls_y_buffer.resize(buffer_size);
tls_z_buffer.resize(buffer_size);
normal_map_data.tiles.reserve(cell_infos.size());
unsigned int ii = 0;
for (unsigned int cell_index = 0; cell_index < cell_infos.size(); ++cell_index) {
const transvoxel::CellInfo cell_info = cell_infos[cell_index];
Vector3f normal_sum;
for (unsigned int triangle_index = 0; triangle_index < cell_info.triangle_count; ++triangle_index) {
const unsigned vi0 = mesh.indices[ii];
const unsigned vi1 = mesh.indices[ii + 1];
const unsigned vi2 = mesh.indices[ii + 2];
ii += 3;
const Vector3f normal0 = mesh.normals[vi0];
const Vector3f normal1 = mesh.normals[vi1];
const Vector3f normal2 = mesh.normals[vi2];
normal_sum += normal0;
normal_sum += normal1;
normal_sum += normal2;
}
#ifdef DEBUG_ENABLED
ZN_ASSERT(cell_info.position.x >= 0);
ZN_ASSERT(cell_info.position.y >= 0);
ZN_ASSERT(cell_info.position.z >= 0);
ZN_ASSERT_CONTINUE(cell_info.position.x < 256);
ZN_ASSERT_CONTINUE(cell_info.position.y < 256);
ZN_ASSERT_CONTINUE(cell_info.position.z < 256);
#endif
const NormalMapData::Tile tile{ //
uint8_t(cell_info.position.x), //
uint8_t(cell_info.position.y), //
uint8_t(cell_info.position.z), //
uint8_t(math::get_longest_axis(normal_sum))
};
normal_map_data.tiles.push_back(tile);
const Vector3f origin = to_vec3f(origin_in_voxels + cell_info.position * cell_size);
unsigned int ax;
unsigned int ay;
unsigned int az;
// TODO I don't know yet if the chosen orientations will match triplanar conventions used in Godot
switch (tile.axis) {
case Vector3f::AXIS_X:
ax = Vector3f::AXIS_Z;
ay = Vector3f::AXIS_Y;
az = Vector3f::AXIS_X;
break;
case Vector3f::AXIS_Y:
ax = Vector3f::AXIS_X;
ay = Vector3f::AXIS_Z;
az = Vector3f::AXIS_Y;
break;
case Vector3f::AXIS_Z:
ax = Vector3f::AXIS_X;
ay = Vector3f::AXIS_Y;
az = Vector3f::AXIS_Z;
break;
default:
ZN_CRASH();
}
Vector3f offset_origin = origin;
offset_origin[az] += cell_size * 0.5f;
unsigned int bi = 0;
for (unsigned int zi = 0; zi < 2; ++zi) {
for (unsigned int yi = 0; yi < tile_resolution + 1; ++yi) {
for (unsigned int xi = 0; xi < tile_resolution + 1; ++xi) {
// TODO Add bias to center differences when calculating the normals?
// TODO Should we project to cell triangles instead of using a plane to get a more accurate result?
Vector3f pos = offset_origin;
// Casting to `int` here because even if the target is float, temporaries can be negative uints
pos[ax] += int(xi) * step;
pos[ay] += int(yi) * step;
pos[az] += int(zi) * step;
tls_x_buffer[bi] = pos.x;
tls_y_buffer[bi] = pos.y;
tls_z_buffer[bi] = pos.z;
++bi;
}
}
}
// TODO Support edited voxels
// TODO Support modifiers
// TODO `generate_block` could also work if `origin_in_voxels` was `Vector3` instead of `Vector3i`
generator.generate_series(to_span(tls_x_buffer), to_span(tls_y_buffer), to_span(tls_z_buffer),
VoxelBufferInternal::CHANNEL_SDF, to_span(tls_sdf_buffer), origin,
origin + float(tile_resolution + 1) * Vector3f(step, step, step));
for (unsigned int yi = 0; yi < tile_resolution; ++yi) {
for (unsigned int xi = 0; xi < tile_resolution; ++xi) {
const unsigned int bi000 = xi + yi * (tile_resolution + 1);
const unsigned int bi100 = bi000 + 1;
const unsigned int bi010 = bi000 + tile_resolution + 1;
const unsigned int bi001 = bi000 + math::squared(tile_resolution + 1);
#ifdef DEBUG_ENABLED
ZN_ASSERT(bi000 < tls_sdf_buffer.size());
ZN_ASSERT(bi100 < tls_sdf_buffer.size());
ZN_ASSERT(bi010 < tls_sdf_buffer.size());
ZN_ASSERT(bi001 < tls_sdf_buffer.size());
#endif
const float sd000 = tls_sdf_buffer[bi000];
const float sd100 = tls_sdf_buffer[bi100];
const float sd010 = tls_sdf_buffer[bi010];
const float sd001 = tls_sdf_buffer[bi001];
const Vector3f normal = math::normalized(Vector3f(sd100 - sd000, sd010 - sd000, sd001 - sd000));
Vector3f cnormal;
cnormal.x = normal[ax];
cnormal.y = normal[ay];
cnormal.z = normal[az];
normal_map_data.normals.push_back(cnormal);
}
}
}
// FixedArray<unsigned int, Vector3f::AXIS_COUNT> axis_count;
// fill(axis_count, 0u);
// for (unsigned int i = 0; i < normal_map_data.tiles.size(); ++i) {
// const NormalMapData::Tile tile = normal_map_data.tiles[i];
// ++axis_count[tile.axis];
// }
// println("Test");
}
struct NormalMapTextures {
Ref<Texture2DArray> atlas;
Ref<Texture2D> lookup;
};
// Converts normalmap data into textures. They can be used in a shader to apply normals and obtain extra visual details.
static NormalMapTextures store_normalmap_data_to_textures(
const NormalMapData &data, unsigned int tile_resolution, Vector3i block_size) {
ZN_PROFILE_SCOPE();
NormalMapTextures textures;
{
ZN_PROFILE_SCOPE_NAMED("Atlas images");
Vector<Ref<Image>> images;
images.resize(data.tiles.size());
{
for (unsigned int tile_index = 0; tile_index < data.tiles.size(); ++tile_index) {
Ref<Image> image;
image.instantiate();
// TODO Optimize: use octahedral encoding to use one less byte
image->create(tile_resolution, tile_resolution, false, Image::FORMAT_RGB8);
unsigned int normal_index = math::squared(tile_resolution) * tile_index;
for (unsigned int y = 0; y < tile_resolution; ++y) {
for (unsigned int x = 0; x < tile_resolution; ++x) {
ZN_ASSERT(normal_index < data.normals.size());
const Vector3f normal = data.normals[normal_index];
// TODO Optimize: create image from bytes directly, set_pixel is slower
image->set_pixel(
x, y, Color(0.5f + 0.5f * normal.x, 0.5f + 0.5f * normal.y, 0.5f + 0.5f * normal.z));
++normal_index;
}
}
images.write[tile_index] = image;
//image->save_png(String("debug_atlas_{0}.png").format(varray(tile_index)));
}
}
{
ZN_PROFILE_SCOPE_NAMED("Atlas texture");
Ref<Texture2DArray> atlas;
atlas.instantiate();
const Error err = atlas->create_from_images(images);
ZN_ASSERT_RETURN_V(err == OK, textures);
textures.atlas = atlas;
}
}
{
ZN_PROFILE_SCOPE_NAMED("Lookup image+texture");
Ref<Image> image;
image.instantiate();
const unsigned int sqri = Math::ceil(Math::sqrt(double(Vector3iUtil::get_volume(block_size))));
// RG: layer index
// B: projection direction
image->create(sqri, sqri, false, Image::FORMAT_RGB8);
const unsigned int deck_size = block_size.x * block_size.y;
for (unsigned int tile_index = 0; tile_index < data.tiles.size(); ++tile_index) {
const NormalMapData::Tile tile = data.tiles[tile_index];
const unsigned int pi = tile.x + tile.y * block_size.x + tile.z * deck_size;
const unsigned int px = pi % sqri;
const unsigned int py = pi / sqri;
const float r = (tile_index & 0xff) / 255.f;
const float g = (tile_index >> 8) / 255.f;
const float b = tile.axis / 255.f;
// TODO Optimize: create image from bytes directly, set_pixel is slower and uneasy with floats
image->set_pixel(px, py, Color(r, g, b));
}
Ref<ImageTexture> lookup = ImageTexture::create_from_image(image);
textures.lookup = lookup;
}
return textures;
}
void VoxelMesherTransvoxel::build(VoxelMesher::Output &output, const VoxelMesher::Input &input) {
ZN_PROFILE_SCOPE();
@ -211,6 +473,12 @@ void VoxelMesherTransvoxel::build(VoxelMesher::Output &output, const VoxelMesher
// const uint64_t time_before = Time::get_singleton()->get_ticks_usec();
transvoxel::DefaultTextureIndicesData default_texture_indices_data;
static thread_local std::vector<transvoxel::CellInfo> tls_cell_infos;
std::vector<transvoxel::CellInfo> *cell_infos = nullptr;
if (_normalmap_enabled && input.generator != nullptr) {
tls_cell_infos.clear();
cell_infos = &tls_cell_infos;
}
if (_deep_sampling_enabled && input.generator != nullptr && input.data != nullptr && input.lod > 0) {
const DeepSampler ds(*input.generator, *input.data, sdf_channel, input.origin_in_voxels);
@ -219,10 +487,10 @@ void VoxelMesherTransvoxel::build(VoxelMesher::Output &output, const VoxelMesher
// `generate_single` in between, knowing they will all be done within the specified area.
default_texture_indices_data = transvoxel::build_regular_mesh(voxels, sdf_channel, input.lod,
static_cast<transvoxel::TexturingMode>(_texture_mode), tls_cache, tls_mesh_arrays, &ds);
static_cast<transvoxel::TexturingMode>(_texture_mode), tls_cache, tls_mesh_arrays, &ds, cell_infos);
} else {
default_texture_indices_data = transvoxel::build_regular_mesh(voxels, sdf_channel, input.lod,
static_cast<transvoxel::TexturingMode>(_texture_mode), tls_cache, tls_mesh_arrays, nullptr);
static_cast<transvoxel::TexturingMode>(_texture_mode), tls_cache, tls_mesh_arrays, nullptr, cell_infos);
}
if (tls_mesh_arrays.vertices.size() == 0) {
@ -230,6 +498,22 @@ void VoxelMesherTransvoxel::build(VoxelMesher::Output &output, const VoxelMesher
return;
}
if (cell_infos != nullptr) {
ZN_PROFILE_SCOPE_NAMED("Normalmap");
static thread_local NormalMapData tls_normalmap_data;
tls_normalmap_data.clear();
ZN_ASSERT(input.generator != nullptr);
compute_normalmap(to_span(*cell_infos), tls_mesh_arrays, tls_normalmap_data, _normalmap_tile_resolution,
*input.generator, input.origin_in_voxels, input.lod);
const Vector3i block_size =
input.voxels.get_size() - Vector3iUtil::create(get_minimum_padding() + get_maximum_padding());
// TODO This may be deferred to main thread when using GLES3
NormalMapTextures textures =
store_normalmap_data_to_textures(tls_normalmap_data, _normalmap_tile_resolution, block_size);
output.normalmap_atlas = textures.atlas;
output.normalmap_lookup = textures.lookup;
}
transvoxel::MeshArrays *combined_mesh_arrays = &tls_mesh_arrays;
if (_mesh_optimization_params.enabled) {
// TODO When voxel texturing is enabled, this will decrease quality a lot.
@ -361,6 +645,22 @@ Ref<ShaderMaterial> VoxelMesherTransvoxel::get_default_lod_material() const {
return g_minimal_shader_material;
}
void VoxelMesherTransvoxel::set_normalmap_enabled(bool enable) {
_normalmap_enabled = enable;
}
bool VoxelMesherTransvoxel::is_normalmap_enabled() const {
return _normalmap_enabled;
}
void VoxelMesherTransvoxel::set_normalmap_tile_resolution(unsigned int resolution) {
_normalmap_tile_resolution = math::clamp(resolution, 1u, 128u);
}
unsigned int VoxelMesherTransvoxel::get_normalmap_tile_resolution() const {
return _normalmap_tile_resolution;
}
void VoxelMesherTransvoxel::_bind_methods() {
ClassDB::bind_method(D_METHOD("build_transition_mesh", "voxel_buffer", "direction"),
&VoxelMesherTransvoxel::build_transition_mesh);
@ -391,6 +691,14 @@ void VoxelMesherTransvoxel::_bind_methods() {
D_METHOD("set_transitions_enabled", "enabled"), &VoxelMesherTransvoxel::set_transitions_enabled);
ClassDB::bind_method(D_METHOD("get_transitions_enabled"), &VoxelMesherTransvoxel::get_transitions_enabled);
ClassDB::bind_method(D_METHOD("set_normalmap_enabled", "enabled"), &VoxelMesherTransvoxel::set_normalmap_enabled);
ClassDB::bind_method(D_METHOD("is_normalmap_enabled"), &VoxelMesherTransvoxel::is_normalmap_enabled);
ClassDB::bind_method(D_METHOD("set_normalmap_tile_resolution", "resolution"),
&VoxelMesherTransvoxel::set_normalmap_tile_resolution);
ClassDB::bind_method(
D_METHOD("get_normalmap_tile_resolution"), &VoxelMesherTransvoxel::get_normalmap_tile_resolution);
ADD_PROPERTY(
PropertyInfo(Variant::INT, "texturing_mode", PROPERTY_HINT_ENUM, "None,4-blend over 16 textures (4 bits)"),
"set_texturing_mode", "get_texturing_mode");
@ -410,6 +718,10 @@ void VoxelMesherTransvoxel::_bind_methods() {
ADD_PROPERTY(
PropertyInfo(Variant::BOOL, "transitions_enabled"), "set_transitions_enabled", "get_transitions_enabled");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "normalmap_enabled"), "set_normalmap_enabled", "is_normalmap_enabled");
ADD_PROPERTY(PropertyInfo(Variant::INT, "normalmap_tile_resolution"), "set_normalmap_tile_resolution",
"get_normalmap_tile_resolution");
BIND_ENUM_CONSTANT(TEXTURES_NONE);
// TODO Rename MIXEL
BIND_ENUM_CONSTANT(TEXTURES_BLEND_4_OVER_16);

View File

@ -50,6 +50,12 @@ public:
void set_transitions_enabled(bool enable);
bool get_transitions_enabled() const;
void set_normalmap_enabled(bool enable);
bool is_normalmap_enabled() const;
void set_normalmap_tile_resolution(unsigned int resolution);
unsigned int get_normalmap_tile_resolution() const;
Ref<ShaderMaterial> get_default_lod_material() const override;
static void load_static_resources();
@ -85,6 +91,11 @@ private:
bool _deep_sampling_enabled = false;
bool _transitions_enabled = true;
// If enabled, an atlas of normalmaps will be generated for each cell of the resulting mesh, in order to add more
// visual details using a shader.
bool _normalmap_enabled = false;
uint8_t _normalmap_tile_resolution = 8;
};
} // namespace zylann::voxel

View File

@ -1,14 +1,33 @@
#include "voxel_mesher.h"
#include "../constants/voxel_string_names.h"
#include "../generators/voxel_generator.h"
#include "../storage/voxel_buffer_gd.h"
#include "../util/godot/funcs.h"
namespace zylann::voxel {
Ref<Mesh> VoxelMesher::build_mesh(Ref<gd::VoxelBuffer> voxels, TypedArray<Material> materials) {
template <typename T>
T try_get(const Dictionary &d, String key) {
const Variant *v = d.getptr(key);
if (v == nullptr) {
return T();
}
return *v;
}
Ref<Mesh> VoxelMesher::build_mesh(
Ref<gd::VoxelBuffer> voxels, TypedArray<Material> materials, Dictionary additional_data) {
ERR_FAIL_COND_V(voxels.is_null(), Ref<ArrayMesh>());
Output output;
Input input = { voxels->get_buffer(), 0 };
Input input = { voxels->get_buffer() };
if (additional_data.size() > 0) {
Ref<VoxelGenerator> generator = try_get<Ref<VoxelGenerator>>(additional_data, "generator");
input.generator = generator.ptr();
input.origin_in_voxels = try_get<Vector3i>(additional_data, "origin_in_voxels");
}
build(output, input);
if (output.surfaces.size() == 0) {
@ -45,6 +64,13 @@ Ref<Mesh> VoxelMesher::build_mesh(Ref<gd::VoxelBuffer> voxels, TypedArray<Materi
++gd_surface_index;
}
if (output.normalmap_atlas.is_valid()) {
// That should be in return value, but for now I just want this for testing with GDScript, so it gotta go
// somewhere
mesh->set_meta(VoxelStringNames::get_singleton().voxel_normalmap_atlas, output.normalmap_atlas);
mesh->set_meta(VoxelStringNames::get_singleton().voxel_normalmap_lookup, output.normalmap_lookup);
}
return mesh;
}
@ -75,7 +101,9 @@ Ref<Material> VoxelMesher::get_material_by_index(unsigned int i) const {
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.
ClassDB::bind_method(D_METHOD("build_mesh", "voxel_buffer", "materials"), &VoxelMesher::build_mesh);
// TODO Have an object type to specify input
ClassDB::bind_method(D_METHOD("build_mesh", "voxel_buffer", "materials", "additional_data"),
&VoxelMesher::build_mesh, DEFVAL(Dictionary()));
ClassDB::bind_method(D_METHOD("get_minimum_padding"), &VoxelMesher::get_minimum_padding);
ClassDB::bind_method(D_METHOD("get_maximum_padding"), &VoxelMesher::get_maximum_padding);
}

View File

@ -26,13 +26,13 @@ public:
const VoxelBufferInternal &voxels;
// When using LOD, some meshers can use the generator and edited voxels to affine results.
// If not provided, the mesher will only use `voxels`.
VoxelGenerator *generator;
const VoxelDataLodMap *data;
VoxelGenerator *generator = nullptr;
const VoxelDataLodMap *data = nullptr;
// Origin of the block is required when doing deep sampling.
Vector3i origin_in_voxels;
// LOD index. 0 means highest detail. 1 means half detail etc.
// Not initialized because it confused GCC (???)
uint8_t lod; // = 0;
uint8_t lod = 0;
// If true, collision information is required.
// Sometimes it doesn't change anything as the rendering mesh can be used as collider,
// but in other setups it can be different and will be returned in `collision_surface`.
@ -67,13 +67,18 @@ public:
// May be used to store extra information needed in shader to render the mesh properly
// (currently used only by the cubes mesher when baking colors)
Ref<Image> atlas_image;
// Normalmap atlas used for smooth voxels
// TODO Find a better organization to pass this around, also it can't always be created from a thread
Ref<Texture2DArray> normalmap_atlas;
Ref<Texture2D> normalmap_lookup;
};
// This can be called from multiple threads at once. Make sure member vars are protected or thread-local.
virtual void build(Output &output, const Input &voxels);
// Builds a mesh from the given voxels. This function is simplified to be used by the script API.
Ref<Mesh> build_mesh(Ref<gd::VoxelBuffer> voxels, TypedArray<Material> materials);
Ref<Mesh> build_mesh(Ref<gd::VoxelBuffer> voxels, TypedArray<Material> materials, Dictionary additional_data);
// Gets how many neighbor voxels need to be accessed around the meshed area, toward negative axes.
// If this is not respected, the mesher might produce seams at the edges, or an error

View File

@ -11,10 +11,12 @@ namespace zylann {
// shaders.
template <typename T>
struct Vector3T {
static const unsigned int AXIS_X = 0;
static const unsigned int AXIS_Y = 1;
static const unsigned int AXIS_Z = 2;
static const unsigned int AXIS_COUNT = 3;
enum Axis { //
AXIS_X = 0,
AXIS_Y = 1,
AXIS_Z = 2,
AXIS_COUNT = 3
};
union {
struct {
@ -187,6 +189,26 @@ inline T dot(const Vector3T<T> &a, const Vector3T<T> &b) {
return a.x * b.x + a.y * b.y + a.z * b.z;
}
template <typename T>
inline Vector3T<T> abs(const Vector3T<T> v) {
return Vector3T<T>(Math::abs(v.x), Math::abs(v.y), Math::abs(v.z));
}
template <typename T>
inline typename Vector3T<T>::Axis get_longest_axis(Vector3T<T> v) {
v = abs(v);
if (v.x > v.y) {
if (v.x > v.z) {
return Vector3T<T>::AXIS_X;
} else {
return Vector3T<T>::AXIS_Z;
}
} else if (v.y > v.z) {
return Vector3T<T>::AXIS_Y;
}
return Vector3T<T>::AXIS_Z;
}
} // namespace math
} // namespace zylann