689 lines
24 KiB
C++
689 lines
24 KiB
C++
#include "distance_normalmaps.h"
|
|
#include "../edition/funcs.h"
|
|
#include "../generators/voxel_generator.h"
|
|
#include "../storage/voxel_data.h"
|
|
#include "../storage/voxel_data_grid.h"
|
|
#include "../util/math/conv.h"
|
|
#include "../util/math/triangle.h"
|
|
#include "../util/profiling.h"
|
|
|
|
#include <core/io/image.h>
|
|
#include <scene/resources/texture.h>
|
|
|
|
namespace zylann::voxel {
|
|
|
|
static void dilate_normalmap(Span<Vector3f> normals, Vector2i size) {
|
|
ZN_PROFILE_SCOPE();
|
|
|
|
static const int s_dx[4] = { -1, 1, 0, 0 };
|
|
static const int s_dy[4] = { 0, 0, -1, 1 };
|
|
|
|
struct NewNormal {
|
|
unsigned int loc;
|
|
Vector3f normal;
|
|
};
|
|
static thread_local std::vector<NewNormal> tls_new_normals;
|
|
tls_new_normals.clear();
|
|
|
|
unsigned int loc = 0;
|
|
for (int y = 0; y < size.y; ++y) {
|
|
for (int x = 0; x < size.x; ++x) {
|
|
if (normals[loc] != Vector3f()) {
|
|
++loc;
|
|
continue;
|
|
}
|
|
Vector3f sum;
|
|
int count = 0;
|
|
for (unsigned int di = 0; di < 4; ++di) {
|
|
const int nx = x + s_dx[di];
|
|
const int ny = y + s_dy[di];
|
|
if (nx >= 0 && nx < size.x && ny >= 0 && ny < size.y) {
|
|
const Vector3f nn = normals[nx + ny * size.y];
|
|
if (nn != Vector3f()) {
|
|
sum += nn;
|
|
++count;
|
|
}
|
|
}
|
|
}
|
|
if (count > 0) {
|
|
sum /= float(count);
|
|
tls_new_normals.push_back(NewNormal{ loc, sum });
|
|
}
|
|
++loc;
|
|
}
|
|
}
|
|
|
|
for (const NewNormal nn : tls_new_normals) {
|
|
normals[nn.loc] = nn.normal;
|
|
}
|
|
}
|
|
|
|
NormalMapData::Tile compute_tile_info(const CurrentCellInfo &cell_info, Span<const Vector3f> mesh_normals,
|
|
Span<const int> mesh_indices, unsigned int first_index) {
|
|
Vector3f normal_sum;
|
|
unsigned int ii = first_index;
|
|
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(cell_info.position.x < 256);
|
|
ZN_ASSERT(cell_info.position.y < 256);
|
|
ZN_ASSERT(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))
|
|
};
|
|
return tile;
|
|
}
|
|
|
|
void get_axis_indices(Vector3f::Axis axis, unsigned int &ax, unsigned int &ay, unsigned int &az) {
|
|
switch (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();
|
|
}
|
|
}
|
|
|
|
typedef FixedArray<math::BakedIntersectionTriangleForFixedDirection, CurrentCellInfo::MAX_TRIANGLES> CellTriangles;
|
|
|
|
unsigned int prepare_triangles(unsigned int first_index, const CurrentCellInfo &cell_info, const Vector3f direction,
|
|
CellTriangles &baked_triangles, Span<const Vector3f> mesh_vertices, Span<const int> mesh_indices) {
|
|
unsigned int triangle_count = 0;
|
|
|
|
unsigned int ii = first_index;
|
|
for (unsigned int ti = 0; ti < cell_info.triangle_count; ++ti) {
|
|
#ifdef DEBUG_ENABLED
|
|
ZN_ASSERT(ii + 2 < mesh_indices.size());
|
|
#endif
|
|
const unsigned vi0 = mesh_indices[ii];
|
|
const unsigned vi1 = mesh_indices[ii + 1];
|
|
const unsigned vi2 = mesh_indices[ii + 2];
|
|
ii += 3;
|
|
const Vector3f a = mesh_vertices[vi0];
|
|
const Vector3f b = mesh_vertices[vi1];
|
|
const Vector3f c = mesh_vertices[vi2];
|
|
math::BakedIntersectionTriangleForFixedDirection baked_triangle;
|
|
// The triangle can be parallel to the direction
|
|
if (baked_triangle.bake(a, b, c, direction)) {
|
|
baked_triangles[triangle_count] = baked_triangle;
|
|
++triangle_count;
|
|
}
|
|
}
|
|
|
|
return triangle_count;
|
|
}
|
|
|
|
inline uint8_t unorm_to_u8(float x) {
|
|
return math::clamp(255.f * x, 0.f, 255.f);
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
void query_sdf_with_edits(VoxelGenerator &generator, const VoxelData &voxel_data, const VoxelDataGrid &grid,
|
|
Span<const float> query_x_buffer, Span<const float> query_y_buffer, Span<const float> query_z_buffer,
|
|
Span<float> query_sdf_buffer, Vector3f query_min_pos, Vector3f query_max_pos) {
|
|
ZN_PROFILE_SCOPE();
|
|
|
|
// TODO Get scale properly
|
|
const float sdf_scale =
|
|
VoxelBufferInternal::get_sdf_quantization_scale(VoxelBufferInternal::DEFAULT_SDF_CHANNEL_DEPTH);
|
|
const float sdf_scale_inv = 1.f / sdf_scale;
|
|
|
|
for (unsigned int query_index = 0; query_index < query_sdf_buffer.size(); ++query_index) {
|
|
const Vector3 posf(query_x_buffer[query_index], query_y_buffer[query_index], query_z_buffer[query_index]);
|
|
|
|
// TODO Optimize: broaden the amount of samples done at once to benefit more from bulk processing
|
|
// Attempting to evaluate multiple values at once when possible: 8 samples for cube of linear interpolation
|
|
FixedArray<float, 8> sd_samples;
|
|
FixedArray<float, 8> x_gen;
|
|
FixedArray<float, 8> y_gen;
|
|
FixedArray<float, 8> z_gen;
|
|
FixedArray<uint8_t, 8> i_gen;
|
|
unsigned int gen_count = 0;
|
|
|
|
const VoxelBufferInternal::ChannelId channel = VoxelBufferInternal::CHANNEL_SDF;
|
|
|
|
const Vector3i posi0 = math::floor_to_int(posf);
|
|
unsigned int i = 0;
|
|
|
|
// Gather 8 samples from edited voxels
|
|
{
|
|
ZN_PROFILE_SCOPE();
|
|
for (int z = 0; z < 2; ++z) {
|
|
for (int y = 0; y < 2; ++y) {
|
|
for (int x = 0; x < 2; ++x) {
|
|
const Vector3i posi = posi0 + Vector3i(x, y, z);
|
|
// TODO Optimize: instead of locking for individual samples, lock the area with a spatial lock
|
|
// (because we can't lock multiple blocks at once otherwise it causes deadlocks)
|
|
// TODO Optimize: the grid could be told to gather raw channels so we get direct access
|
|
// (requires the former optimization)
|
|
if (grid.try_get_voxel_f(posi, sd_samples[i], channel)) {
|
|
sd_samples[i] *= sdf_scale_inv;
|
|
} else {
|
|
// Not edited, add to the list of voxels to generate
|
|
x_gen[gen_count] = posi.x;
|
|
y_gen[gen_count] = posi.y;
|
|
z_gen[gen_count] = posi.z;
|
|
i_gen[gen_count] = i;
|
|
++gen_count;
|
|
}
|
|
++i;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Complete samples with generator. Note, these samples are not scaled since we are working with floats instead
|
|
// of encoded buffer values.
|
|
if (gen_count > 0) {
|
|
ZN_PROFILE_SCOPE();
|
|
FixedArray<float, 8> gen_samples;
|
|
|
|
generator.generate_series(to_span(x_gen, gen_count), to_span(y_gen, gen_count), to_span(z_gen, gen_count),
|
|
channel, to_span(gen_samples, gen_count), query_min_pos, query_max_pos);
|
|
|
|
voxel_data.get_modifiers().apply(to_span(x_gen, gen_count), to_span(y_gen, gen_count),
|
|
to_span(z_gen, gen_count), to_span(gen_samples, gen_count), query_min_pos, query_max_pos);
|
|
|
|
for (unsigned int j = 0; j < gen_count; ++j) {
|
|
sd_samples[i_gen[j]] = gen_samples[j];
|
|
}
|
|
}
|
|
|
|
// Interpolate
|
|
const float sd_interp = math::interpolate_trilinear(sd_samples[0], sd_samples[1], sd_samples[5], sd_samples[4],
|
|
sd_samples[2], sd_samples[3], sd_samples[7], sd_samples[6], math::fract(posf));
|
|
|
|
query_sdf_buffer[query_index] = sd_interp;
|
|
}
|
|
}
|
|
|
|
bool try_query_sdf_with_edits(VoxelGenerator &generator, const VoxelData &voxel_data, Span<const float> query_x_buffer,
|
|
Span<const float> query_y_buffer, Span<const float> query_z_buffer, Span<float> query_sdf_buffer,
|
|
Vector3f query_min_pos, Vector3f query_max_pos) {
|
|
ZN_PROFILE_SCOPE();
|
|
|
|
// Pad by 1 in case there are neighboring edited voxels. If not done, it creates a grid pattern following LOD0 block
|
|
// boundaries because samples near there assume there was no edited neighbors when interpolating
|
|
const Vector3i query_min_pos_i = math::floor_to_int(query_min_pos) - Vector3iUtil::create(1);
|
|
const Vector3i query_max_pos_i = math::ceil_to_int(query_max_pos) + Vector3iUtil::create(1);
|
|
|
|
struct ClearOnExit {
|
|
VoxelDataGrid &grid;
|
|
inline ~ClearOnExit() {
|
|
grid.clear();
|
|
}
|
|
};
|
|
|
|
// Re-use memory because it will be used a lot
|
|
static thread_local VoxelDataGrid tls_grid;
|
|
// Ensure cleanup references to voxel buffers
|
|
ClearOnExit grid_clear_on_exit{ tls_grid };
|
|
|
|
{
|
|
const Box3i voxel_box = Box3i::from_min_max(query_min_pos_i, query_max_pos_i);
|
|
// TODO Don't hardcode block size (even though for now I have no plan to make it configurable)
|
|
if (Vector3iUtil::get_volume(voxel_box.size >> constants::DEFAULT_BLOCK_SIZE_PO2) > math::cubed(8)) {
|
|
// Box too big for quick sparse readings, won't handle edits
|
|
ZN_PRINT_VERBOSE("Box too big to render virtual normalmaps");
|
|
return false;
|
|
}
|
|
|
|
voxel_data.get_blocks_grid(tls_grid, voxel_box, 0);
|
|
// const VoxelDataLodMap::Lod &lod0 = voxel_data.lods[0];
|
|
// RWLockRead rlock(lod0.map_lock);
|
|
// tls_grid.reference_area(lod0.map, voxel_box);
|
|
}
|
|
|
|
if (!tls_grid.has_any_block()) {
|
|
// No edited voxels in the area, can use a faster path
|
|
return false;
|
|
}
|
|
|
|
query_sdf_with_edits(generator, voxel_data, tls_grid, query_x_buffer, query_y_buffer, query_z_buffer,
|
|
query_sdf_buffer, query_min_pos, query_max_pos);
|
|
|
|
return true;
|
|
}
|
|
|
|
inline void query_sdf(VoxelGenerator &generator, const VoxelData *voxel_data, Span<const float> query_x_buffer,
|
|
Span<const float> query_y_buffer, Span<const float> query_z_buffer, Span<float> query_sdf_buffer,
|
|
Vector3f query_min_pos, Vector3f query_max_pos) {
|
|
ZN_PROFILE_SCOPE();
|
|
bool generator_only = true;
|
|
|
|
if (voxel_data != nullptr) {
|
|
// TODO Optimize: if there are no edited voxels in the entire mesh, completely skip this function.
|
|
// Doing this efficiently requires an acceleration structure to do fast "exists" queries.
|
|
generator_only = !try_query_sdf_with_edits(generator, *voxel_data, query_x_buffer, query_y_buffer,
|
|
query_z_buffer, query_sdf_buffer, query_min_pos, query_max_pos);
|
|
}
|
|
|
|
if (generator_only) {
|
|
// Note, these samples are not scaled since we are working with floats instead of encoded buffer values.
|
|
generator.generate_series(query_x_buffer, query_y_buffer, query_z_buffer, VoxelBufferInternal::CHANNEL_SDF,
|
|
query_sdf_buffer, query_min_pos, query_max_pos);
|
|
|
|
if (voxel_data != nullptr) {
|
|
voxel_data->get_modifiers().apply(
|
|
query_x_buffer, query_y_buffer, query_z_buffer, query_sdf_buffer, query_min_pos, query_max_pos);
|
|
}
|
|
}
|
|
|
|
#if DEBUG_ENABLED
|
|
for (const float sd : query_sdf_buffer) {
|
|
ZN_ASSERT(!(Math::is_nan(sd) || Math::is_inf(sd)));
|
|
}
|
|
#endif
|
|
}
|
|
|
|
// 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(ICellIterator &cell_iterator, Span<const Vector3f> mesh_vertices,
|
|
Span<const Vector3f> mesh_normals, Span<const int> mesh_indices, NormalMapData &normal_map_data,
|
|
unsigned int tile_resolution, VoxelGenerator &generator, const VoxelData *voxel_data, Vector3i origin_in_voxels,
|
|
unsigned int lod_index, bool octahedral_encoding) {
|
|
ZN_PROFILE_SCOPE();
|
|
|
|
ZN_ASSERT_RETURN(generator.supports_series_generation());
|
|
|
|
const unsigned int cell_count = cell_iterator.get_count();
|
|
const unsigned int encoded_normal_size = octahedral_encoding ? 2 : 3;
|
|
normal_map_data.normals.resize(math::squared(tile_resolution) * cell_count * encoded_normal_size);
|
|
|
|
const unsigned int cell_size = 1 << lod_index;
|
|
const float step = float(cell_size) / tile_resolution;
|
|
|
|
normal_map_data.tiles.reserve(cell_count);
|
|
|
|
unsigned int first_index = 0;
|
|
unsigned int cell_index = 0;
|
|
CurrentCellInfo cell_info;
|
|
|
|
while (cell_iterator.next(cell_info)) {
|
|
const NormalMapData::Tile tile = compute_tile_info(cell_info, mesh_normals, mesh_indices, first_index);
|
|
normal_map_data.tiles.push_back(tile);
|
|
|
|
const Vector3f cell_origin_world = to_vec3f(origin_in_voxels + cell_info.position * cell_size);
|
|
|
|
unsigned int ax;
|
|
unsigned int ay;
|
|
unsigned int az;
|
|
get_axis_indices(Vector3f::Axis(tile.axis), ax, ay, az);
|
|
|
|
Vector3f quad_origin_world = cell_origin_world;
|
|
quad_origin_world[az] += cell_size * 0.5f;
|
|
|
|
Vector3f direction;
|
|
direction[az] = 1.f;
|
|
|
|
static thread_local std::vector<Vector2i> tls_tile_sample_positions;
|
|
tls_tile_sample_positions.clear();
|
|
tls_tile_sample_positions.reserve(math::squared(tile_resolution));
|
|
|
|
// Each normal needs 4 samples:
|
|
// (x, y, z )
|
|
// (x+s, y, z )
|
|
// (x, y+s, z )
|
|
// (x, y, z+s)
|
|
const unsigned int max_buffer_size = math::squared(tile_resolution) * 4;
|
|
|
|
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.clear();
|
|
tls_x_buffer.clear();
|
|
tls_y_buffer.clear();
|
|
tls_z_buffer.clear();
|
|
tls_sdf_buffer.reserve(max_buffer_size);
|
|
tls_x_buffer.reserve(max_buffer_size);
|
|
tls_y_buffer.reserve(max_buffer_size);
|
|
tls_z_buffer.reserve(max_buffer_size);
|
|
|
|
// Optimize triangles
|
|
CellTriangles baked_triangles;
|
|
unsigned int triangle_count =
|
|
prepare_triangles(first_index, cell_info, direction, baked_triangles, mesh_vertices, mesh_indices);
|
|
|
|
// Fill query buffers
|
|
{
|
|
ZN_PROFILE_SCOPE_NAMED("Compute positions");
|
|
for (unsigned int yi = 0; yi < tile_resolution; ++yi) {
|
|
for (unsigned int xi = 0; xi < tile_resolution; ++xi) {
|
|
// TODO Add bias to center differences when calculating the normals?
|
|
Vector3f pos000 = quad_origin_world;
|
|
// Casting to `int` here because even if the target is float, temporaries can be negative uints
|
|
pos000[ax] += int(xi) * step;
|
|
pos000[ay] += int(yi) * step;
|
|
|
|
// Project to triangles
|
|
const Vector3f ray_origin_world = pos000 - direction * cell_size;
|
|
const Vector3f ray_origin_mesh = ray_origin_world - to_vec3f(origin_in_voxels);
|
|
const float NO_HIT = 999999.f;
|
|
float nearest_hit_distance = NO_HIT;
|
|
for (unsigned int ti = 0; ti < triangle_count; ++ti) {
|
|
const math::TriangleIntersectionResult result =
|
|
baked_triangles[ti].intersect(ray_origin_mesh, direction);
|
|
if (result.case_id == math::TriangleIntersectionResult::INTERSECTION &&
|
|
result.distance < nearest_hit_distance) {
|
|
nearest_hit_distance = result.distance;
|
|
}
|
|
}
|
|
|
|
if (nearest_hit_distance == NO_HIT) {
|
|
// Don't query if there is no triangle
|
|
continue;
|
|
}
|
|
|
|
pos000 = ray_origin_world + direction * nearest_hit_distance;
|
|
tls_tile_sample_positions.push_back(Vector2i(xi, yi));
|
|
|
|
tls_x_buffer.push_back(pos000.x);
|
|
tls_y_buffer.push_back(pos000.y);
|
|
tls_z_buffer.push_back(pos000.z);
|
|
|
|
Vector3f pos100 = pos000;
|
|
pos100.x += step;
|
|
tls_x_buffer.push_back(pos100.x);
|
|
tls_y_buffer.push_back(pos100.y);
|
|
tls_z_buffer.push_back(pos100.z);
|
|
|
|
Vector3f pos010 = pos000;
|
|
pos010.y += step;
|
|
tls_x_buffer.push_back(pos010.x);
|
|
tls_y_buffer.push_back(pos010.y);
|
|
tls_z_buffer.push_back(pos010.z);
|
|
|
|
Vector3f pos001 = pos000;
|
|
pos001.z += step;
|
|
tls_x_buffer.push_back(pos001.x);
|
|
tls_y_buffer.push_back(pos001.y);
|
|
tls_z_buffer.push_back(pos001.z);
|
|
}
|
|
}
|
|
}
|
|
|
|
tls_sdf_buffer.resize(tls_x_buffer.size());
|
|
|
|
// Query voxel data
|
|
query_sdf(generator, voxel_data, to_span(tls_x_buffer), to_span(tls_y_buffer), to_span(tls_z_buffer),
|
|
to_span(tls_sdf_buffer), cell_origin_world, cell_origin_world + Vector3f(cell_size));
|
|
|
|
static thread_local std::vector<Vector3f> tls_tile_normals;
|
|
tls_tile_normals.clear();
|
|
tls_tile_normals.resize(math::squared(tile_resolution));
|
|
|
|
// Compute normals from SDF results
|
|
{
|
|
ZN_PROFILE_SCOPE_NAMED("Compute normals");
|
|
unsigned int bi = 0;
|
|
for (const Vector2i sample_position : tls_tile_sample_positions) {
|
|
const unsigned int bi000 = bi;
|
|
const unsigned int bi100 = bi + 1;
|
|
const unsigned int bi010 = bi + 2;
|
|
const unsigned int bi001 = bi + 3;
|
|
bi += 4;
|
|
// TODO I wish this was solved https://github.com/godotengine/godot/issues/31608
|
|
#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));
|
|
const unsigned int normal_index = sample_position.x + sample_position.y * tile_resolution;
|
|
#ifdef DEBUG_ENABLED
|
|
ZN_ASSERT(normal_index < normal_map_data.normals.size());
|
|
#endif
|
|
tls_tile_normals[normal_index] = normal;
|
|
}
|
|
}
|
|
|
|
for (unsigned int dilation_steps = 0; dilation_steps < 2; ++dilation_steps) {
|
|
// Fill up some pixels around triangle borders, to give some margin when sampling near them in shader
|
|
dilate_normalmap(to_span(tls_tile_normals), Vector2i(tile_resolution, tile_resolution));
|
|
}
|
|
|
|
// Encode normals
|
|
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;
|
|
++cell_index;
|
|
}
|
|
}
|
|
|
|
inline void copy_2d_region(Span<uint8_t> dst, Vector2i dst_size, Span<const uint8_t> src, Vector2i src_size,
|
|
Vector2i dst_pos, unsigned int item_size_in_bytes) {
|
|
#ifdef DEBUG_ENABLED
|
|
ZN_ASSERT(src_size.x >= 0 && src_size.y >= 0);
|
|
ZN_ASSERT(dst_size.x >= 0 && dst_size.y >= 0);
|
|
ZN_ASSERT(dst_pos.x >= 0 && dst_pos.y >= 0 && dst_pos.x + src_size.x <= dst_size.x &&
|
|
dst_pos.y + src_size.y <= dst_size.y);
|
|
ZN_ASSERT(src.size() == src_size.x * src_size.y * item_size_in_bytes);
|
|
ZN_ASSERT(dst.size() == dst_size.x * dst_size.y * item_size_in_bytes);
|
|
ZN_ASSERT(!src.overlaps(dst));
|
|
#endif
|
|
const unsigned int dst_begin = (dst_pos.x + dst_pos.y * dst_size.x) * item_size_in_bytes;
|
|
const unsigned int src_row_size = src_size.x * item_size_in_bytes;
|
|
const unsigned int dst_row_size = dst_size.x * item_size_in_bytes;
|
|
uint8_t *dst_p = dst.data() + dst_begin;
|
|
const uint8_t *src_p = src.data();
|
|
for (unsigned int src_y = 0; src_y < (unsigned int)src_size.y; ++src_y) {
|
|
memcpy(dst_p, src_p, src_row_size);
|
|
dst_p += dst_row_size;
|
|
src_p += src_row_size;
|
|
}
|
|
}
|
|
|
|
NormalMapImages store_normalmap_data_to_images(
|
|
const NormalMapData &data, unsigned int tile_resolution, Vector3i block_size, bool octahedral_encoding) {
|
|
ZN_PROFILE_SCOPE();
|
|
|
|
NormalMapImages images;
|
|
|
|
{
|
|
ZN_PROFILE_SCOPE_NAMED("Atlas images");
|
|
|
|
const unsigned int pixel_size = octahedral_encoding ? 2 : 3;
|
|
const Image::Format format = octahedral_encoding ? Image::FORMAT_RG8 : Image::FORMAT_RGB8;
|
|
const unsigned int tile_size_in_pixels = math::squared(tile_resolution);
|
|
const unsigned int tile_size_in_bytes = tile_size_in_pixels * pixel_size;
|
|
|
|
#ifdef VOXEL_VIRTUAL_TEXTURE_USE_TEXTURE_ARRAY
|
|
|
|
Vector<Ref<Image>> tile_images;
|
|
tile_images.resize(data.tiles.size());
|
|
|
|
for (unsigned int tile_index = 0; tile_index < data.tiles.size(); ++tile_index) {
|
|
PackedByteArray bytes;
|
|
{
|
|
bytes.resize(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, format, bytes);
|
|
|
|
tile_images.write[tile_index] = image;
|
|
//image->save_png(String("debug_atlas_{0}.png").format(varray(tile_index)));
|
|
}
|
|
|
|
images.atlas = tile_images;
|
|
|
|
#else
|
|
const unsigned int tiles_across = int(Math::ceil(Math::sqrt(float(data.tiles.size()))));
|
|
const unsigned int pixels_across = tiles_across * tile_resolution;
|
|
|
|
PackedByteArray bytes;
|
|
bytes.resize(math::squared(tiles_across) * tile_size_in_bytes);
|
|
Span<uint8_t> bytes_span(bytes.ptrw(), bytes.size());
|
|
|
|
for (unsigned int tile_index = 0; tile_index < data.tiles.size(); ++tile_index) {
|
|
const Vector2i tile_pos_pixels =
|
|
int(tile_resolution) * Vector2i(tile_index % tiles_across, tile_index / tiles_across);
|
|
Span<const uint8_t> tile =
|
|
to_span_from_position_and_size(data.normals, tile_index * tile_size_in_bytes, tile_size_in_bytes);
|
|
copy_2d_region(bytes_span, Vector2i(pixels_across, pixels_across), tile,
|
|
Vector2i(tile_resolution, tile_resolution), tile_pos_pixels, pixel_size);
|
|
}
|
|
|
|
Ref<Image> atlas;
|
|
atlas.instantiate();
|
|
atlas->create_from_data(pixels_across, pixels_across, false, format, bytes);
|
|
images.atlas = atlas;
|
|
|
|
#endif // VOXEL_VIRTUAL_TEXTURE_USE_TEXTURE_ARRAY
|
|
}
|
|
|
|
{
|
|
ZN_PROFILE_SCOPE_NAMED("Lookup image");
|
|
|
|
const unsigned int sqri = Math::ceil(Math::sqrt(double(Vector3iUtil::get_volume(block_size))));
|
|
|
|
PackedByteArray bytes;
|
|
{
|
|
const unsigned int pixel_size = 2;
|
|
bytes.resize(math::squared(sqri) * pixel_size);
|
|
|
|
uint8_t *bytes_w = bytes.ptrw();
|
|
memset(bytes_w, 0, bytes.size());
|
|
|
|
const unsigned int deck_size = block_size.x * block_size.y;
|
|
#ifdef DEBUG_ENABLED
|
|
bool tile_index_overflow = false;
|
|
#endif
|
|
|
|
for (unsigned int tile_index = 0; tile_index < data.tiles.size(); ++tile_index) {
|
|
const NormalMapData::Tile tile = data.tiles[tile_index];
|
|
// RG: tttttttt aatttttt
|
|
const uint8_t r = tile_index & 0xff;
|
|
const uint8_t g = ((tile_index >> 8) & 0x3f) | (tile.axis << 6);
|
|
#ifdef DEBUG_ENABLED
|
|
if (tile_index > 0x3fff && !tile_index_overflow) {
|
|
tile_index_overflow = true;
|
|
ZN_PRINT_VERBOSE("Tile index overflow");
|
|
}
|
|
#endif
|
|
const unsigned int pi = pixel_size * (tile.x + tile.y * block_size.x + tile.z * deck_size);
|
|
ZN_ASSERT(int(pi) < bytes.size());
|
|
bytes_w[pi] = r;
|
|
bytes_w[pi + 1] = g;
|
|
}
|
|
}
|
|
|
|
Ref<Image> image;
|
|
image.instantiate();
|
|
image->create_from_data(sqri, sqri, false, Image::FORMAT_RG8, bytes);
|
|
images.lookup = image;
|
|
}
|
|
|
|
return images;
|
|
}
|
|
|
|
// Converts normalmap data into textures. They can be used in a shader to apply normals and obtain extra visual details.
|
|
NormalMapTextures store_normalmap_data_to_textures(const NormalMapImages &data) {
|
|
ZN_PROFILE_SCOPE();
|
|
|
|
NormalMapTextures textures;
|
|
|
|
{
|
|
ZN_PROFILE_SCOPE_NAMED("Atlas texture");
|
|
#ifdef VOXEL_VIRTUAL_TEXTURE_USE_TEXTURE_ARRAY
|
|
Ref<Texture2DArray> atlas;
|
|
atlas.instantiate();
|
|
const Error err = atlas->create_from_images(data.atlas);
|
|
ZN_ASSERT_RETURN_V(err == OK, textures);
|
|
textures.atlas = atlas;
|
|
#else
|
|
textures.atlas = ImageTexture::create_from_image(data.atlas);
|
|
#endif
|
|
}
|
|
|
|
{
|
|
ZN_PROFILE_SCOPE_NAMED("Lookup texture");
|
|
Ref<ImageTexture> lookup = ImageTexture::create_from_image(data.lookup);
|
|
textures.lookup = lookup;
|
|
}
|
|
|
|
return textures;
|
|
}
|
|
|
|
unsigned int get_virtual_texture_tile_resolution_for_lod(const NormalMapSettings &settings, unsigned int lod_index) {
|
|
const unsigned int relative_lod_index = lod_index - settings.begin_lod_index;
|
|
const unsigned int tile_resolution = math::clamp(int(settings.tile_resolution_min << relative_lod_index),
|
|
int(settings.tile_resolution_min), int(settings.tile_resolution_max));
|
|
return tile_resolution;
|
|
}
|
|
|
|
} // namespace zylann::voxel
|