Skip to content

Commit

Permalink
Added methods to get SDF as a 3D texture or raw byte array
Browse files Browse the repository at this point in the history
  • Loading branch information
Zylann committed Dec 29, 2024
1 parent 6d4e19d commit e319d0a
Show file tree
Hide file tree
Showing 12 changed files with 357 additions and 1 deletion.
1 change: 1 addition & 0 deletions common.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ def get_sources(env, is_editor_build, include_tests):
"util/godot/classes/geometry_2d.cpp",
"util/godot/classes/geometry_instance_3d.cpp",
"util/godot/classes/input_event_key.cpp",
"util/godot/classes/image_texture_3d.cpp",
"util/godot/classes/mesh.cpp",
"util/godot/classes/multimesh.cpp",
"util/godot/classes/node.cpp",
Expand Down
34 changes: 34 additions & 0 deletions doc/classes/VoxelBuffer.xml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,17 @@
Clears the buffer and gives it the specified size.
</description>
</method>
<method name="create_3d_texture_from_sdf_zxy" qualifiers="const">
<return type="ImageTexture3D" />
<param index="0" name="output_format" type="int" enum="Image.Format" />
<description>
Creates a 3D texture from the SDF channel.
If [code]output_format[/code] is a 8-bit pixel format, the texture will contain normalized signed distances, where 0.5 is the isolevel, 0 is the furthest away under surface, and 1 is the furthest away above surface.
Only 16-bit SDF channel is supported.
Only [constant Image.FORMAT_R8] and [constant Image.FORMAT_L8] output formats are suported.
Note: when sampling this texture in a shader, you need to swizzle 3D coordinates with [code].yxz[/code]. This is how voxels are internally stored, and this function does not change this convention.
</description>
</method>
<method name="debug_print_sdf_y_slices" qualifiers="const">
<return type="Image[]" />
<param index="0" name="scale" type="float" default="1.0" />
Expand Down Expand Up @@ -173,6 +184,14 @@
Gets metadata associated to this [VoxelBuffer].
</description>
</method>
<method name="get_channel_as_byte_array" qualifiers="const">
<return type="PackedByteArray" />
<param index="0" name="channel_index" type="int" enum="VoxelBuffer.ChannelId" />
<description>
Gets voxel data from a channel as uncompressed raw bytes. Check [enum VoxelBuffer.Depth] for information about the data format.
Note: if the channel is compressed, it will be decompressed on the fly into the returned array. If you want a different behavior in this case, check [method get_channel_compression] before calling this method.
</description>
</method>
<method name="get_channel_compression" qualifiers="const">
<return type="int" enum="VoxelBuffer.Compression" />
<param index="0" name="channel" type="int" />
Expand Down Expand Up @@ -316,6 +335,14 @@
Changes the bit depth of a given channel. This controls the range of values a channel can hold. See [enum VoxelBuffer.Depth] for more information.
</description>
</method>
<method name="set_channel_from_byte_array">
<return type="void" />
<param index="0" name="channel_index" type="int" enum="VoxelBuffer.ChannelId" />
<param index="1" name="data" type="PackedByteArray" />
<description>
Overwrites the contents of a channel from raw voxel data. Check [enum VoxelBuffer.Depth] for information about the expected data format.
</description>
</method>
<method name="set_voxel">
<return type="void" />
<param index="0" name="value" type="int" />
Expand Down Expand Up @@ -355,6 +382,13 @@
<description>
</description>
</method>
<method name="update_3d_texture_from_sdf_zxy" qualifiers="const">
<return type="void" />
<param index="0" name="existing_texture" type="ImageTexture3D" />
<description>
Updates an existing 3D texture from the SDF channel. See [method create_3d_texture_from_sdf_zxy] for more information.
</description>
</method>
</methods>
<constants>
<constant name="CHANNEL_TYPE" value="0" enum="ChannelId">
Expand Down
3 changes: 3 additions & 0 deletions doc/source/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ Primarily developped with Godot 4.3.

- `VoxelBlockyModel`: Added option to turn off "LOD skirts" when used with `VoxelLodTerrain`, which may be useful with transparent models
- `VoxelBlockyModelCube`: Added support for mesh rotation like `VoxelBlockyMesh` (prior to that, rotation buttons in the editor only swapped tiles around)
- `VoxelBuffer`:
- Added functions to create/update a `Texture3D` from the SDF channel
- Added functions to get/set a whole channel as a raw `PackedByteArray`
- `VoxelInstanceGenerator`: Added `OnePerTriangle` emission mode
- `VoxelToolLodTerrain`: Implemented raycast when the mesher is `VoxelMesherBlocky` or `VoxelMesherCubes`
- `VoxelInstanceGenerator`: Added ability to filter spawning by voxel texture indices, when using `VoxelMesherTransvoxel` with `texturing_mode` set to `4-blend over 16 textures`
Expand Down
1 change: 1 addition & 0 deletions edition/voxel_tool_lod_terrain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ void VoxelToolLodTerrain::do_sphere_async(Vector3 center, float radius) {
}

void VoxelToolLodTerrain::copy(Vector3i pos, VoxelBuffer &dst, uint8_t channels_mask) const {
ZN_PROFILE_SCOPE();
ERR_FAIL_COND(_terrain == nullptr);
if (channels_mask == 0) {
channels_mask = (1 << _channel);
Expand Down
14 changes: 14 additions & 0 deletions storage/voxel_buffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,20 @@ bool VoxelBuffer::get_channel_as_bytes_read_only(unsigned int channel_index, Spa
return false;
}

void VoxelBuffer::set_channel_from_bytes(const unsigned int channel_index, Span<const uint8_t> src) {
const Channel &channel = _channels[channel_index];
if (channel.compression == COMPRESSION_UNIFORM) {
#ifdef DEV_ENABLED
ZN_ASSERT(channel.data == nullptr);
#endif
ZN_ASSERT_RETURN(create_channel_noinit(channel_index, _size));
}
ZN_ASSERT_RETURN(channel.data != nullptr);
ZN_ASSERT_RETURN(src.size() == channel.size_in_bytes);
ZN_ASSERT(channel.compression == COMPRESSION_NONE);
src.copy_to(Span<uint8_t>(channel.data, channel.size_in_bytes));
}

bool VoxelBuffer::create_channel(int i, uint64_t defval) {
ZN_DSTACK();
if (!create_channel_noinit(i, _size)) {
Expand Down
9 changes: 9 additions & 0 deletions storage/voxel_buffer.h
Original file line number Diff line number Diff line change
Expand Up @@ -435,9 +435,13 @@ class VoxelBuffer {
return Vector3iUtil::get_volume_u64(_size);
}

// Gets a slice aliasing the channel's data
bool get_channel_as_bytes(unsigned int channel_index, Span<uint8_t> &slice);

// Gets a read-only slice aliasing the channel's data
bool get_channel_as_bytes_read_only(unsigned int channel_index, Span<const uint8_t> &slice) const;

// Gets a slice aliasing the channel's data, reinterpreted to a specific type
template <typename T>
bool get_channel_data(unsigned int channel_index, Span<T> &dst) {
Span<uint8_t> dst8;
Expand All @@ -446,6 +450,7 @@ class VoxelBuffer {
return true;
}

// Gets a read-only slice aliasing the channel's data, reinterpreted to a specific type
template <typename T>
bool get_channel_data_read_only(unsigned int channel_index, Span<const T> &dst) const {
Span<const uint8_t> dst8;
Expand All @@ -454,6 +459,10 @@ class VoxelBuffer {
return true;
}

// Overwrites contents of a channel with raw data. This skips default initialization of the channel, so it
// can be a little bit faster than using `decompress_channel`. The input data must have the right size.
void set_channel_from_bytes(const unsigned int channel_index, Span<const uint8_t> src);

void downscale_to(VoxelBuffer &dst, Vector3i src_min, Vector3i src_max, Vector3i dst_min) const;

bool equals(const VoxelBuffer &p_other) const;
Expand Down
203 changes: 203 additions & 0 deletions storage/voxel_buffer_gd.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
#include "../edition/voxel_tool_buffer.h"
#include "../util/dstack.h"
#include "../util/godot/classes/image.h"
#include "../util/godot/classes/image_texture_3d.h"
#include "../util/godot/core/packed_arrays.h"
#include "../util/math/color.h"
#include "../util/memory/memory.h"
#include "../util/string/format.h"
Expand Down Expand Up @@ -121,6 +123,175 @@ void op_buffer_buffer_f(
}
}

// Converts the SDF channel into a 3D texture. If the output format is R8 or L8, pixels will contain a normalized
// distance field (so in shader, 0.5 will be the isolevel instead of 0, and values will go from 0 to 1). Note: in
// shader, you should use the `yxz` swizzle to sample pixels of that texture, because this is how voxels are stored and
// this function does not convert the coordinate system.
TypedArray<Image> sdf_to_3d_texture_data_zxy(const VoxelBuffer &vb, const Image::Format output_format) {
ZN_PROFILE_SCOPE();

const VoxelBuffer::ChannelId channel = zylann::voxel::VoxelBuffer::CHANNEL_SDF;
const VoxelBuffer::Depth depth = vb.get_channel_depth(channel);
const uint64_t xy_area = vb.get_size().x * vb.get_size().y;
const VoxelBuffer::Compression channel_compression = vb.get_channel_compression(channel);

// TODO An array of images is going to waste resources... Godot should really have an Image3D class, or just allow
// to pass raw data... `Image` is more than 400 Kb, which is more than a single slice of 8-bit pixels in a 16x16x16
// chunk!
TypedArray<Image> images;
images.resize(vb.get_size().z);

// TODO Is it possible for a shader to specify that a 8-bit depth texture should be sampled as inorm8 and not
// unorm8? Because if not, we have to do extra work to convert every voxel here.
// That's why for now we have to do these conversions
struct L {
static inline uint8_t s8_to_u8(const int8_t i) {
return static_cast<uint8_t>(static_cast<int16_t>(i) + 128);
}

static inline uint8_t s16_to_u8(const int16_t i) {
return static_cast<uint16_t>((static_cast<int32_t>(i) + 32768) >> 8);
}
};

// Not all combinations are supported. For now we only implement those we need.
switch (depth) {
case VoxelBuffer::DEPTH_8_BIT:
ZN_PRINT_ERROR("Channel depth not supported.");
return TypedArray<Image>();

case VoxelBuffer::DEPTH_16_BIT: {
switch (output_format) {
case Image::FORMAT_R8:
case Image::FORMAT_L8: {
// Get fixed-point signed normalized 16-bit SDF to fixed-point unsigned normalized 8-bit SDF.
// Sampling this in shader may need something like `(texture(t, pos).r * 2.0 - 1.0) * scale`

if (channel_compression == VoxelBuffer::COMPRESSION_UNIFORM) {
const int16_t sd_s16 = vb.get_voxel(Vector3i(0, 0, 0), channel);
const uint8_t sd_u8 = L::s16_to_u8(sd_s16);

for (int z = 0; z < vb.get_size().z; ++z) {
PackedByteArray pba;
pba.resize(xy_area);
pba.fill(sd_u8);
Ref<Image> image = Image::create_from_data(
vb.get_size().y, vb.get_size().x, false, output_format, pba
);
images[z] = image;
}
} else {
Span<const int16_t> data;
ZN_ASSERT_RETURN_V(vb.get_channel_data_read_only(channel, data), TypedArray<Image>());

for (int z = 0; z < vb.get_size().z; ++z) {
PackedByteArray pba;
pba.resize(xy_area);
Span<uint8_t> pba_s(pba.ptrw(), pba.size());

Span<const int16_t> data_slice = data.sub(z * xy_area, xy_area);

for (unsigned int i = 0; i < xy_area; ++i) {
pba_s[i] = L::s16_to_u8(data_slice[i]);
}

Ref<Image> image = Image::create_from_data(
vb.get_size().y, vb.get_size().x, false, output_format, pba
);
images[z] = image;
}
}
} break;

default:
ZN_PRINT_ERROR("Image format not supported.");
return TypedArray<Image>();
}

} break;

case VoxelBuffer::DEPTH_32_BIT: {
ZN_PRINT_ERROR("Channel depth not supported.");
return TypedArray<Image>();
} break;

default:
ZN_PRINT_ERROR("Channel depth not supported.");
return TypedArray<Image>();
}

return images;
}

Ref<ImageTexture3D> create_3d_texture_from_sdf_zxy(const VoxelBuffer &vb, const Image::Format output_format) {
TypedArray<Image> images = sdf_to_3d_texture_data_zxy(vb, output_format);
Ref<ImageTexture3D> texture = zylann::godot::create_image_texture_3d(output_format, vb.get_size(), false, images);
return texture;
}

void update_3d_texture_from_sdf_zxy(const VoxelBuffer &vb, ImageTexture3D &texture) {
TypedArray<Image> images = sdf_to_3d_texture_data_zxy(vb, texture.get_format());
// Format and size must match
zylann::godot::update_image_texture_3d(texture, images);
}

PackedByteArray get_channel_as_byte_array(const VoxelBuffer &vb, const VoxelBuffer::ChannelId channel) {
ZN_ASSERT_RETURN_V(channel >= 0 && channel < VoxelBuffer::MAX_CHANNELS, PackedByteArray());

const Vector3i res = vb.get_size();
const uint64_t volume = Vector3iUtil::get_volume_u64(res);
const VoxelBuffer::Compression compression = vb.get_channel_compression(channel);
const VoxelBuffer::Depth depth = vb.get_channel_depth(channel);

PackedByteArray pba;

switch (compression) {
case VoxelBuffer::COMPRESSION_UNIFORM: {
// Decompress... can't just decompress the VoxelBuffer directly with existing methods, because it is const,
// and would waste intermediary memory.
// If this behavior is not desired, the caller must check compression first.
switch (depth) {
case VoxelBuffer::DEPTH_8_BIT: {
pba.resize(volume);
const uint8_t v = vb.get_voxel(Vector3i(0, 0, 0), channel);
pba.fill(v);
} break;

case VoxelBuffer::DEPTH_16_BIT: {
pba.resize(volume * sizeof(uint16_t));
const uint16_t v = vb.get_voxel(Vector3i(0, 0, 0), channel);
Span<uint16_t> pba_s = Span<uint8_t>(pba.ptrw(), pba.size()).reinterpret_cast_to<uint16_t>();
pba_s.fill(v);
} break;

case VoxelBuffer::DEPTH_32_BIT: {
pba.resize(volume * sizeof(uint32_t));
const uint32_t v = vb.get_voxel(Vector3i(0, 0, 0), channel);
Span<uint32_t> pba_s = Span<uint8_t>(pba.ptrw(), pba.size()).reinterpret_cast_to<uint32_t>();
pba_s.fill(v);
} break;

default:
ZN_PRINT_ERROR("Unhandled channel depth");
break;
}
} break;

case VoxelBuffer::COMPRESSION_NONE: {
Span<const uint8_t> src;
ZN_ASSERT_RETURN_V(vb.get_channel_as_bytes_read_only(channel, src), pba);
Span<uint8_t> pba_s(pba.ptrw(), volume);
src.copy_to(pba_s);
} break;

default:
ZN_PRINT_ERROR("Unhandled compression");
break;
}

return pba;
}

} // namespace zylann::voxel

namespace zylann::voxel::godot {
Expand Down Expand Up @@ -550,6 +721,26 @@ void VoxelBuffer::clear_voxel_metadata() {
_buffer->clear_voxel_metadata();
}

Ref<ImageTexture3D> VoxelBuffer::create_3d_texture_from_sdf_zxy(const Image::Format output_format) const {
return zylann::voxel::create_3d_texture_from_sdf_zxy(*_buffer, output_format);
}

void VoxelBuffer::update_3d_texture_from_sdf_zxy(Ref<ImageTexture3D> texture) const {
ZN_ASSERT_RETURN(texture.is_valid());
return zylann::voxel::update_3d_texture_from_sdf_zxy(*_buffer, **texture);
}

PackedByteArray VoxelBuffer::get_channel_as_byte_array(const ChannelId channel) const {
return zylann::voxel::get_channel_as_byte_array(
*_buffer, static_cast<zylann::voxel::VoxelBuffer::ChannelId>(channel)
);
}

void VoxelBuffer::set_channel_from_byte_array(const ChannelId channel, const PackedByteArray &pba) {
ZN_ASSERT_RETURN(channel >= 0 && channel < MAX_CHANNELS);
_buffer->set_channel_from_bytes(channel, to_span(pba));
}

Ref<Image> VoxelBuffer::debug_print_sdf_to_image_top_down() {
return debug_print_sdf_to_image_top_down(*_buffer);
}
Expand Down Expand Up @@ -715,6 +906,18 @@ void VoxelBuffer::_bind_methods() {
D_METHOD("copy_voxel_metadata_in_area", "src_buffer", "src_min_pos", "src_max_pos", "dst_min_pos"),
&VoxelBuffer::copy_voxel_metadata_in_area
);
ClassDB::bind_method(
D_METHOD("create_3d_texture_from_sdf_zxy", "output_format"), &VoxelBuffer::create_3d_texture_from_sdf_zxy
);
ClassDB::bind_method(
D_METHOD("update_3d_texture_from_sdf_zxy", "existing_texture"), &VoxelBuffer::update_3d_texture_from_sdf_zxy
);
ClassDB::bind_method(
D_METHOD("get_channel_as_byte_array", "channel_index"), &VoxelBuffer::get_channel_as_byte_array
);
ClassDB::bind_method(
D_METHOD("set_channel_from_byte_array", "channel_index", "data"), &VoxelBuffer::set_channel_from_byte_array
);
ClassDB::bind_method(
D_METHOD("debug_print_sdf_y_slices", "scale"), &VoxelBuffer::debug_print_sdf_y_slices, DEFVAL(1.0)
);
Expand Down
Loading

0 comments on commit e319d0a

Please sign in to comment.