Added option for octahedral encoding of normals to save memory

This commit is contained in:
Marc Gilleron 2022-08-04 21:19:02 +01:00
parent 3606ab9b93
commit dcc5e3c174
6 changed files with 92 additions and 28 deletions

View File

@ -139,22 +139,38 @@ unsigned int prepare_triangles(unsigned int first_index, const transvoxel::CellI
return triangle_count;
}
inline uint8_t snorm_to_u8(float x) {
return math::clamp(127.f + 128.f * x, 0.f, 255.f);
inline uint8_t unorm_to_u8(float x) {
return math::clamp(255.f * x, 0.f, 255.f);
}
inline NormalMapData::EncodedNormal encode_normal_xyz(const Vector3f n) {
return { snorm_to_u8(n.x), snorm_to_u8(n.y), snorm_to_u8(n.z) };
// https://knarkowicz.wordpress.com/2014/04/16/octahedron-normal-vector-encoding/
Vector2f octahedron_wrap(Vector2f v) {
return (Vector2f(1.f) - math::abs(Vector2f(v.y, v.x))) * math::sign_nonzero(v);
}
// https://knarkowicz.wordpress.com/2014/04/16/octahedron-normal-vector-encoding/
inline Vector2f encode_normal_octahedron(Vector3f n) {
const float sum = Math::abs(n.x) + Math::abs(n.y) + Math::abs(n.z);
n.x /= sum;
n.y /= sum;
Vector2f r = n.z >= 0.f ? Vector2f(n.x, n.y) : octahedron_wrap(Vector2f(n.x, n.y));
r = r * 0.5f + Vector2f(0.5f);
return r;
}
inline Vector3f encode_normal_xyz(const Vector3f n) {
return Vector3f(0.5f) + 0.5f * n;
}
// 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.
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) {
Vector3i origin_in_voxels, unsigned int lod_index, bool octahedral_encoding) {
ZN_PROFILE_SCOPE();
normal_map_data.normals.resize(math::squared(tile_resolution) * cell_infos.size());
const unsigned int encoded_normal_size = octahedral_encoding ? 2 : 3;
normal_map_data.normals.resize(math::squared(tile_resolution) * cell_infos.size() * encoded_normal_size);
const unsigned int cell_size = 1 << lod_index;
const float step = float(cell_size) / tile_resolution;
@ -317,11 +333,24 @@ void compute_normalmap(Span<const transvoxel::CellInfo> cell_infos, const transv
}
// Encode normals
// TODO Optimize: use octahedral encoding to use one less byte
unsigned int tile_begin = cell_index * math::squared(tile_resolution);
for (unsigned int i = 0; i < tls_tile_normals.size(); ++i) {
ZN_ASSERT(tile_begin + i < normal_map_data.normals.size());
normal_map_data.normals[tile_begin + i] = encode_normal_xyz(tls_tile_normals[i]);
const unsigned int tile_begin = cell_index * math::squared(tile_resolution) * encoded_normal_size;
if (octahedral_encoding) {
for (unsigned int i = 0; i < tls_tile_normals.size(); ++i) {
const unsigned int offset = tile_begin + i * encoded_normal_size;
ZN_ASSERT(offset + encoded_normal_size <= normal_map_data.normals.size());
const Vector2f n = encode_normal_octahedron(tls_tile_normals[i]);
normal_map_data.normals[offset + 0] = unorm_to_u8(n.x);
normal_map_data.normals[offset + 1] = unorm_to_u8(n.y);
}
} else {
for (unsigned int i = 0; i < tls_tile_normals.size(); ++i) {
const unsigned int offset = tile_begin + i * encoded_normal_size; //
ZN_ASSERT(offset + encoded_normal_size <= normal_map_data.normals.size());
const Vector3f n = encode_normal_xyz(tls_tile_normals[i]);
normal_map_data.normals[offset + 0] = unorm_to_u8(n.x);
normal_map_data.normals[offset + 1] = unorm_to_u8(n.y);
normal_map_data.normals[offset + 2] = unorm_to_u8(n.z);
}
}
first_index += 3 * cell_info.triangle_count;
@ -329,7 +358,7 @@ void compute_normalmap(Span<const transvoxel::CellInfo> cell_infos, const transv
}
NormalMapImages store_normalmap_data_to_images(
const NormalMapData &data, unsigned int tile_resolution, Vector3i block_size) {
const NormalMapData &data, unsigned int tile_resolution, Vector3i block_size, bool octahedral_encoding) {
ZN_PROFILE_SCOPE();
NormalMapImages images;
@ -340,18 +369,21 @@ NormalMapImages store_normalmap_data_to_images(
tile_images.resize(data.tiles.size());
{
const unsigned int pixel_size = octahedral_encoding ? 2 : 3;
const Image::Format format = octahedral_encoding ? Image::FORMAT_RG8 : Image::FORMAT_RGB8;
for (unsigned int tile_index = 0; tile_index < data.tiles.size(); ++tile_index) {
PackedByteArray bytes;
{
const unsigned int tile_size_in_pixels = math::squared(tile_resolution);
const unsigned int tile_size_in_bytes = tile_size_in_pixels * sizeof(NormalMapData::EncodedNormal);
const unsigned int tile_size_in_bytes = tile_size_in_pixels * pixel_size;
bytes.resize(tile_size_in_bytes);
memcpy(bytes.ptrw(), data.normals.data() + tile_index * tile_size_in_pixels, tile_size_in_bytes);
memcpy(bytes.ptrw(), data.normals.data() + tile_index * tile_size_in_bytes, tile_size_in_bytes);
}
Ref<Image> image;
image.instantiate();
image->create_from_data(tile_resolution, tile_resolution, false, Image::FORMAT_RGB8, bytes);
image->create_from_data(tile_resolution, tile_resolution, false, format, bytes);
tile_images.write[tile_index] = image;
//image->save_png(String("debug_atlas_{0}.png").format(varray(tile_index)));

View File

@ -21,13 +21,8 @@ class VoxelGenerator;
// triangles, and be stored in an atlas. A shader can then read the atlas using a lookup texture to find the tile.
struct NormalMapData {
struct EncodedNormal {
uint8_t x;
uint8_t y;
uint8_t z;
};
// Encoded normals
std::vector<EncodedNormal> normals;
std::vector<uint8_t> normals;
struct Tile {
uint8_t x;
uint8_t y;
@ -46,7 +41,7 @@ struct NormalMapData {
// Sample voxels inside the cell to compute a tile of world space normals from the SDF.
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);
Vector3i origin_in_voxels, unsigned int lod_index, bool octahedral_encoding);
struct NormalMapImages {
Vector<Ref<Image>> atlas_images;
@ -59,7 +54,7 @@ struct NormalMapTextures {
};
NormalMapImages store_normalmap_data_to_images(
const NormalMapData &data, unsigned int tile_resolution, Vector3i block_size);
const NormalMapData &data, unsigned int tile_resolution, Vector3i block_size, bool octahedral_encoding);
// Converts normalmap data into textures. They can be used in a shader to apply normals and obtain extra visual details.
// This may not be allowed to run in a different thread than the main thread if the renderer is not using Vulkan.

View File

@ -244,13 +244,13 @@ void VoxelMesherTransvoxel::build(VoxelMesher::Output &output, const VoxelMesher
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);
*input.generator, input.origin_in_voxels, input.lod, _octahedral_normal_encoding_enabled);
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
// TODO Link tile resolution to LOD level, with a cap
NormalMapImages images =
store_normalmap_data_to_images(tls_normalmap_data, _normalmap_tile_resolution, block_size);
NormalMapImages images = store_normalmap_data_to_images(
tls_normalmap_data, _normalmap_tile_resolution, block_size, _octahedral_normal_encoding_enabled);
NormalMapTextures textures = store_normalmap_data_to_textures(images);
output.normalmap_atlas = textures.atlas;
output.normalmap_lookup = textures.lookup;
@ -403,6 +403,14 @@ unsigned int VoxelMesherTransvoxel::get_normalmap_tile_resolution() const {
return _normalmap_tile_resolution;
}
void VoxelMesherTransvoxel::set_octahedral_normal_encoding(bool enable) {
_octahedral_normal_encoding_enabled = enable;
}
bool VoxelMesherTransvoxel::get_octahedral_normal_encoding() const {
return _octahedral_normal_encoding_enabled;
}
void VoxelMesherTransvoxel::_bind_methods() {
ClassDB::bind_method(D_METHOD("build_transition_mesh", "voxel_buffer", "direction"),
&VoxelMesherTransvoxel::build_transition_mesh);
@ -441,6 +449,11 @@ void VoxelMesherTransvoxel::_bind_methods() {
ClassDB::bind_method(
D_METHOD("get_normalmap_tile_resolution"), &VoxelMesherTransvoxel::get_normalmap_tile_resolution);
ClassDB::bind_method(D_METHOD("set_octahedral_normal_encoding", "enabled"),
&VoxelMesherTransvoxel::set_octahedral_normal_encoding);
ClassDB::bind_method(
D_METHOD("get_octahedral_normal_encoding"), &VoxelMesherTransvoxel::get_octahedral_normal_encoding);
ADD_PROPERTY(
PropertyInfo(Variant::INT, "texturing_mode", PROPERTY_HINT_ENUM, "None,4-blend over 16 textures (4 bits)"),
"set_texturing_mode", "get_texturing_mode");
@ -463,6 +476,8 @@ void VoxelMesherTransvoxel::_bind_methods() {
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");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "normalmap_octahedral_encoding_enabled"), "set_octahedral_normal_encoding",
"get_octahedral_normal_encoding");
BIND_ENUM_CONSTANT(TEXTURES_NONE);
// TODO Rename MIXEL

View File

@ -53,6 +53,9 @@ public:
void set_normalmap_enabled(bool enable);
bool is_normalmap_enabled() const;
void set_octahedral_normal_encoding(bool enable);
bool get_octahedral_normal_encoding() const;
void set_normalmap_tile_resolution(unsigned int resolution);
unsigned int get_normalmap_tile_resolution() const;
@ -96,6 +99,7 @@ private:
// visual details using a shader.
bool _normalmap_enabled = false;
uint8_t _normalmap_tile_resolution = 8;
bool _octahedral_normal_encoding_enabled = true;
};
} // namespace zylann::voxel

View File

@ -239,8 +239,9 @@ inline void sort(T &a, T &b, T &c, T &d) {
// Returns -1 if `x` is negative, and 1 otherwise.
// Contrary to a usual version like GLSL, this one returns 1 when `x` is 0, instead of 0.
inline float sign_nonzero(float x) {
return x < 0.f ? -1.f : 1.f;
template <typename T>
inline T sign_nonzero(T x) {
return x < 0 ? -1 : 1;
}
// Trilinear interpolation between corner values of a unit-sized cube.

View File

@ -1,6 +1,8 @@
#ifndef ZN_VECTOR2T_H
#define ZN_VECTOR2T_H
#include "funcs.h"
namespace zylann {
template <typename T>
@ -18,6 +20,11 @@ struct Vector2T {
};
Vector2T() : x(0), y(0) {}
// It is recommended to use `explicit` because otherwise it would open the door to plenty of implicit conversions
// which would make many cases ambiguous.
explicit Vector2T(T p_v) : x(p_v), y(p_v) {}
Vector2T(T p_x, T p_y) : x(p_x), y(p_y) {}
inline const T &operator[](const unsigned int p_axis) const {
@ -75,6 +82,16 @@ T cross(const Vector2T<T> &a, const Vector2T<T> &b) {
return a.x * b.y - a.y * b.x;
}
template <typename T>
inline Vector2T<T> abs(const Vector2T<T> v) {
return Vector2T<T>(Math::abs(v.x), Math::abs(v.y));
}
template <typename T>
inline Vector2T<T> sign_nonzero(Vector2T<T> v) {
return Vector2T<T>(sign_nonzero(v.x), sign_nonzero(v.y));
}
} // namespace math
} // namespace zylann