From fcddf4da6130582b1736af7b5af1b921eed99ef9 Mon Sep 17 00:00:00 2001 From: Marc Gilleron Date: Sun, 31 Jul 2022 16:14:36 +0100 Subject: [PATCH] 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. --- constants/voxel_string_names.cpp | 3 + constants/voxel_string_names.h | 3 + edition/voxel_tool_lod_terrain.cpp | 4 +- generators/graph/voxel_generator_graph.cpp | 47 +++ generators/graph/voxel_generator_graph.h | 6 + generators/voxel_generator.cpp | 10 +- generators/voxel_generator.h | 13 +- meshers/transvoxel/transvoxel.cpp | 15 +- meshers/transvoxel/transvoxel.h | 7 +- .../transvoxel/voxel_mesher_transvoxel.cpp | 316 +++++++++++++++++- meshers/transvoxel/voxel_mesher_transvoxel.h | 11 + meshers/voxel_mesher.cpp | 34 +- meshers/voxel_mesher.h | 13 +- util/math/vector3t.h | 30 +- 14 files changed, 487 insertions(+), 25 deletions(-) diff --git a/constants/voxel_string_names.cpp b/constants/voxel_string_names.cpp index 08336363..02ae5a5f 100644 --- a/constants/voxel_string_names.cpp +++ b/constants/voxel_string_names.cpp @@ -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 diff --git a/constants/voxel_string_names.h b/constants/voxel_string_names.h index 11ba8025..d402e873 100644 --- a/constants/voxel_string_names.h +++ b/constants/voxel_string_names.h @@ -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 diff --git a/edition/voxel_tool_lod_terrain.cpp b/edition/voxel_tool_lod_terrain.cpp index 177fe257..5c162f00 100644 --- a/edition/voxel_tool_lod_terrain.cpp +++ b/edition/voxel_tool_lod_terrain.cpp @@ -621,7 +621,9 @@ Array separate_floating_chunks(VoxelTool &voxel_tool, Box3i world_box, Node *par } } - Ref 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 = 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()); diff --git a/generators/graph/voxel_generator_graph.cpp b/generators/graph/voxel_generator_graph.cpp index 9c8f9e69..a927f088 100644 --- a/generators/graph/voxel_generator_graph.cpp +++ b/generators/graph/voxel_generator_graph.cpp @@ -1081,6 +1081,53 @@ void VoxelGeneratorGraph::generate_set(Span in_x, Span in_y, Span< runtime.generate_set(cache.state, in_x, in_y, in_z, false, nullptr); } +void VoxelGeneratorGraph::generate_series(Span positions_x, Span positions_y, + Span positions_z, unsigned int channel, Span out_values, Vector3f min_pos, + Vector3f max_pos) { + std::shared_ptr 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(positions_x.data()); + float *ptr_y = const_cast(positions_y.data()); + float *ptr_z = const_cast(positions_z.data()); + generate_set(Span(ptr_x, positions_x.size()), Span(ptr_y, positions_y.size()), + Span(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; } diff --git a/generators/graph/voxel_generator_graph.h b/generators/graph/voxel_generator_graph.h index 56661cc7..fd280beb 100644 --- a/generators/graph/voxel_generator_graph.h +++ b/generators/graph/voxel_generator_graph.h @@ -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 positions_x, Span positions_y, Span positions_z, + unsigned int channel, Span out_values, Vector3f min_pos, Vector3f max_pos) override; + Ref duplicate(bool p_subresources) const override; // Utility diff --git a/generators/voxel_generator.cpp b/generators/voxel_generator.cpp index 223558bf..16f0bf62 100644 --- a/generators/voxel_generator.cpp +++ b/generators/voxel_generator.cpp @@ -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 positions_x, Span positions_y, + Span positions_z, unsigned int channel, Span out_values, Vector3f min_pos, + Vector3f max_pos) { + ZN_PRINT_ERROR("Not implemented"); +} + void VoxelGenerator::_b_generate_block(Ref out_buffer, Vector3 origin_in_voxels, int lod) { ERR_FAIL_COND(lod < 0); ERR_FAIL_COND(lod >= int(constants::MAX_LOD)); diff --git a/generators/voxel_generator.h b/generators/voxel_generator.h index 8bab8169..fbdd4b74 100644 --- a/generators/voxel_generator.h +++ b/generators/voxel_generator.h @@ -1,6 +1,8 @@ #ifndef VOXEL_GENERATOR_H #define VOXEL_GENERATOR_H +#include "../util/math/vector3f.h" +#include "../util/span.h" #include #include #include @@ -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 positions, - // Span channels, - // Span> out_values); + virtual void generate_series(Span positions_x, Span positions_y, + Span positions_z, unsigned int channel, Span out_values, Vector3f min_pos, + Vector3f max_pos); // Declares the channels this generator will use virtual int get_used_channels_mask() const; diff --git a/meshers/transvoxel/transvoxel.cpp b/meshers/transvoxel/transvoxel.cpp index aa49e186..85971c4b 100644 --- a/meshers/transvoxel/transvoxel.cpp +++ b/meshers/transvoxel/transvoxel.cpp @@ -305,7 +305,8 @@ Vector3f binary_search_interpolate(const IDeepSDFSampler &sampler, float s0, flo template void build_regular_mesh(Span 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 *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 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 apply_zero_sdf_fix(Span 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 *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 sdf_data = sdf_data_raw.reinterpret_cast_to(); build_regular_mesh(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 sdf_data = sdf_data_raw.reinterpret_cast_to(); build_regular_mesh(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 sdf_data = sdf_data_raw.reinterpret_cast_to(); build_regular_mesh(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: diff --git a/meshers/transvoxel/transvoxel.h b/meshers/transvoxel/transvoxel.h index f4573fdd..ef93322b 100644 --- a/meshers/transvoxel/transvoxel.h +++ b/meshers/transvoxel/transvoxel.h @@ -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 *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, diff --git a/meshers/transvoxel/voxel_mesher_transvoxel.cpp b/meshers/transvoxel/voxel_mesher_transvoxel.cpp index 0aa4d7f1..7ba05809 100644 --- a/meshers/transvoxel/voxel_mesher_transvoxel.cpp +++ b/meshers/transvoxel/voxel_mesher_transvoxel.cpp @@ -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 normals; + struct Tile { + uint8_t x; + uint8_t y; + uint8_t z; + uint8_t axis; + }; + std::vector 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 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 tls_sdf_buffer; + static thread_local std::vector tls_x_buffer; + static thread_local std::vector tls_y_buffer; + static thread_local std::vector 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 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 atlas; + Ref 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> images; + images.resize(data.tiles.size()); + + { + for (unsigned int tile_index = 0; tile_index < data.tiles.size(); ++tile_index) { + Ref 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 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.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 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 tls_cell_infos; + std::vector *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(_texture_mode), tls_cache, tls_mesh_arrays, &ds); + static_cast(_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(_texture_mode), tls_cache, tls_mesh_arrays, nullptr); + static_cast(_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 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); diff --git a/meshers/transvoxel/voxel_mesher_transvoxel.h b/meshers/transvoxel/voxel_mesher_transvoxel.h index 3ea28813..c255b9bf 100644 --- a/meshers/transvoxel/voxel_mesher_transvoxel.h +++ b/meshers/transvoxel/voxel_mesher_transvoxel.h @@ -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 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 diff --git a/meshers/voxel_mesher.cpp b/meshers/voxel_mesher.cpp index 958556bf..4fd063e4 100644 --- a/meshers/voxel_mesher.cpp +++ b/meshers/voxel_mesher.cpp @@ -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 VoxelMesher::build_mesh(Ref voxels, TypedArray materials) { +template +T try_get(const Dictionary &d, String key) { + const Variant *v = d.getptr(key); + if (v == nullptr) { + return T(); + } + return *v; +} + +Ref VoxelMesher::build_mesh( + Ref voxels, TypedArray materials, Dictionary additional_data) { ERR_FAIL_COND_V(voxels.is_null(), Ref()); Output output; - Input input = { voxels->get_buffer(), 0 }; + Input input = { voxels->get_buffer() }; + + if (additional_data.size() > 0) { + Ref generator = try_get>(additional_data, "generator"); + input.generator = generator.ptr(); + input.origin_in_voxels = try_get(additional_data, "origin_in_voxels"); + } + build(output, input); if (output.surfaces.size() == 0) { @@ -45,6 +64,13 @@ Ref VoxelMesher::build_mesh(Ref voxels, TypedArrayset_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 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); } diff --git a/meshers/voxel_mesher.h b/meshers/voxel_mesher.h index 2f62b5d3..ade8616c 100644 --- a/meshers/voxel_mesher.h +++ b/meshers/voxel_mesher.h @@ -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 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 normalmap_atlas; + Ref 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 build_mesh(Ref voxels, TypedArray materials); + Ref build_mesh(Ref voxels, TypedArray 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 diff --git a/util/math/vector3t.h b/util/math/vector3t.h index 69dded8a..c462f34a 100644 --- a/util/math/vector3t.h +++ b/util/math/vector3t.h @@ -11,10 +11,12 @@ namespace zylann { // shaders. template 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 &a, const Vector3T &b) { return a.x * b.x + a.y * b.y + a.z * b.z; } +template +inline Vector3T abs(const Vector3T v) { + return Vector3T(Math::abs(v.x), Math::abs(v.y), Math::abs(v.z)); +} + +template +inline typename Vector3T::Axis get_longest_axis(Vector3T v) { + v = abs(v); + if (v.x > v.y) { + if (v.x > v.z) { + return Vector3T::AXIS_X; + } else { + return Vector3T::AXIS_Z; + } + } else if (v.y > v.z) { + return Vector3T::AXIS_Y; + } + return Vector3T::AXIS_Z; +} + } // namespace math } // namespace zylann