Files
test/intern/cycles/scene/image_vdb.cpp
Lukas Tönne 12f0bc7736 Fix #138388: Use grid voxel corners as value locations like OpenVDB
Blender grid rendering interprets voxel transforms in such a way that the voxel
values are located at the center of a voxel. This is inconsistent with OpenVDB
where the values are located at the lower corners for the purpose or sampling
and related algorithms.

While it is possible to offset grids when communicating with the OpenVDB
library, this is also error-prone and does not add any major advantage.
Every time a grid is passed to OpenVDB we currently have to take care to
transform by half a voxel to ensure correct sampling weights are used that match
the density displayed by the viewport rendering.

This patch changes volume grid generation, conversion, and rendering code so
that grid transforms match the corner-located values in OpenVDB.

- The volume primitive cube node aligns the grid transform with the location of
  the first value, which is now also the same as min/max bounds input of the
  node.
- Mesh<->Grid conversion does no longer require offsetting grid transform and
  mesh vertices respectively by 0.5 voxels.
- Texture space for viewport rendering is offset by half a voxel, so that it
  covers the same area as before and voxel centers remain at the same texture
  space locations.

Co-authored-by: Brecht Van Lommel <brecht@blender.org>
Pull Request: https://projects.blender.org/blender/blender/pulls/138449
2025-08-26 12:27:20 +02:00

263 lines
7.0 KiB
C++

/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#include "scene/image_vdb.h"
#include "util/log.h"
#include "util/nanovdb.h"
#include "util/openvdb.h"
#include "util/texture.h"
#ifdef WITH_OPENVDB
# include <openvdb/tools/Dense.h>
#endif
CCL_NAMESPACE_BEGIN
#ifdef WITH_OPENVDB
VDBImageLoader::VDBImageLoader(openvdb::GridBase::ConstPtr grid_,
const string &grid_name,
const float clipping)
: grid_name(grid_name), clipping(clipping), grid(grid_)
{
}
#endif
VDBImageLoader::VDBImageLoader(const string &grid_name, const float clipping)
: grid_name(grid_name), clipping(clipping)
{
}
VDBImageLoader::~VDBImageLoader() = default;
bool VDBImageLoader::load_metadata(const ImageDeviceFeatures &features, ImageMetaData &metadata)
{
if (!features.has_nanovdb) {
return false;
}
#ifdef WITH_NANOVDB
load_grid();
if (!grid) {
return false;
}
/* Convert to the few float types that we know. */
grid = openvdb_convert_to_known_type(grid);
if (!grid) {
return false;
}
/* Get number of channels from type. */
metadata.channels = openvdb_num_channels(grid);
/* Convert OpenVDB to NanoVDB grid. */
nanogrid = openvdb_to_nanovdb(grid, precision, clipping);
if (!nanogrid) {
grid.reset();
return false;
}
/* Set dimensions. */
bbox = grid->evalActiveVoxelBoundingBox();
if (bbox.empty()) {
metadata.type = IMAGE_DATA_TYPE_NANOVDB_EMPTY;
metadata.byte_size = 1;
grid.reset();
return true;
}
if (metadata.channels == 1) {
if (precision == 0) {
metadata.type = IMAGE_DATA_TYPE_NANOVDB_FPN;
}
else if (precision == 16) {
metadata.type = IMAGE_DATA_TYPE_NANOVDB_FP16;
}
else {
metadata.type = IMAGE_DATA_TYPE_NANOVDB_FLOAT;
}
}
else if (metadata.channels == 3) {
metadata.type = IMAGE_DATA_TYPE_NANOVDB_FLOAT3;
}
else if (metadata.channels == 4) {
metadata.type = IMAGE_DATA_TYPE_NANOVDB_FLOAT4;
}
else {
grid.reset();
return false;
}
metadata.byte_size = nanogrid.size();
/* Set transform from object space to voxel index. */
openvdb::math::Mat4f grid_matrix = grid->transform().baseMap()->getAffineMap()->getMat4();
Transform index_to_object;
for (int col = 0; col < 4; col++) {
for (int row = 0; row < 3; row++) {
index_to_object[row][col] = (float)grid_matrix[col][row];
}
}
metadata.transform_3d = transform_inverse(index_to_object);
metadata.use_transform_3d = true;
/* Only NanoGrid needed now, free OpenVDB grid. */
grid.reset();
return true;
#else
(void)metadata;
return false;
#endif
}
bool VDBImageLoader::load_pixels(const ImageMetaData &metadata,
void *pixels,
const size_t /*pixels_size*/,
const bool /*associate_alpha*/)
{
#ifdef WITH_NANOVDB
if (metadata.type == IMAGE_DATA_TYPE_NANOVDB_EMPTY) {
memset(pixels, 0, metadata.byte_size);
return true;
}
if (nanogrid) {
memcpy(pixels, nanogrid.data(), nanogrid.size());
return true;
}
#else
(void)pixels;
#endif
return false;
}
string VDBImageLoader::name() const
{
return grid_name;
}
bool VDBImageLoader::equals(const ImageLoader &other) const
{
#ifdef WITH_OPENVDB
const VDBImageLoader &other_loader = (const VDBImageLoader &)other;
return grid && grid == other_loader.grid;
#else
(void)other;
return true;
#endif
}
void VDBImageLoader::cleanup()
{
#ifdef WITH_OPENVDB
/* Free OpenVDB grid memory as soon as we can. */
grid.reset();
#endif
#ifdef WITH_NANOVDB
nanogrid.reset();
#endif
}
bool VDBImageLoader::is_vdb_loader() const
{
return true;
}
#ifdef WITH_OPENVDB
openvdb::GridBase::ConstPtr VDBImageLoader::get_grid()
{
return grid;
}
template<typename GridType>
openvdb::GridBase::ConstPtr create_grid(const float *voxels,
const size_t width,
const size_t height,
const size_t depth,
Transform transform_3d,
const float clipping)
{
using ValueType = typename GridType::ValueType;
openvdb::GridBase::ConstPtr grid;
const openvdb::CoordBBox dense_bbox(0, 0, 0, width - 1, height - 1, depth - 1);
typename GridType::Ptr sparse = GridType::create(ValueType(0.0f));
if (dense_bbox.empty()) {
return sparse;
}
const openvdb::tools::Dense<const ValueType, openvdb::tools::MemoryLayout::LayoutXYZ> dense(
dense_bbox, reinterpret_cast<const ValueType *>(voxels));
openvdb::tools::copyFromDense(dense, *sparse, ValueType(clipping));
/* Compute index to world matrix. */
const float3 voxel_size = make_float3(1.0f / width, 1.0f / height, 1.0f / depth);
transform_3d = transform_inverse(transform_3d);
const openvdb::Mat4R index_to_world_mat((double)(voxel_size.x * transform_3d[0][0]),
0.0,
0.0,
0.0,
0.0,
(double)(voxel_size.y * transform_3d[1][1]),
0.0,
0.0,
0.0,
0.0,
(double)(voxel_size.z * transform_3d[2][2]),
0.0,
(double)transform_3d[0][3] + voxel_size.x,
(double)transform_3d[1][3] + voxel_size.y,
(double)transform_3d[2][3] + voxel_size.z,
1.0);
const openvdb::math::Transform::Ptr index_to_world_tfm =
openvdb::math::Transform::createLinearTransform(index_to_world_mat);
sparse->setTransform(index_to_world_tfm);
return sparse;
}
#endif
void VDBImageLoader::grid_from_dense_voxels(const size_t width,
const size_t height,
const size_t depth,
const int channels,
const float *voxels,
Transform transform_3d)
{
#ifdef WITH_OPENVDB
/* TODO: Create NanoVDB grid directly? */
if (channels == 1) {
grid = create_grid<openvdb::FloatGrid>(voxels, width, height, depth, transform_3d, clipping);
}
else if (channels == 3) {
grid = create_grid<openvdb::Vec3fGrid>(voxels, width, height, depth, transform_3d, clipping);
}
else if (channels == 4) {
grid = create_grid<openvdb::Vec4fGrid>(voxels, width, height, depth, transform_3d, clipping);
}
/* Clipping already applied, no need to do it again. */
clipping = 0.0f;
#else
(void)width;
(void)height;
(void)depth;
(void)channels;
(void)voxels;
(void)transform_3d;
#endif
}
CCL_NAMESPACE_END