- Published: 2025-07-13
- Updated: 2025-07-16
- MeshLib Team
What is an SDF?
Signed Distance Fields (SDFs or distance volumes) are scalar fields most often stored as 3D voxel grids. Each voxel here records the shortest or unsigned or signed distance to a target surface. The value is negative inside the closed surface, zero on it, and positive outside. This makes SDFs ideal for various geometric queries.
Why Convert a Mesh to an SDF?
Converting a mesh to an SDF unlocks a set of operations and smooth shape edits
- Offsets. One is free to grow or shrink a whole object by an exact distance (e.g., adding a uniform 2 mm shell everywhere), without causing the mesh to fold over itself.
- Double‑offset blending, i.e., inflating each part, deflating their union, and obtaining seamless fillets or organic morphs.
- Booleans by simple min/max of two fields. You can add, cut, or overlap models reliably because the math happens on a tidy voxel grid rather than the raw triangles.
- Closed‑mesh re-meshing, i.e., rebuilding a watertight surface for already closed inputs at any chosen detail level (open meshes first need hole‑aware sign assignment).
Algorithms to Compute SDF
Before we dive into the nuts and bolts below, remember the end‑game: you compute a mesh signed distance grid, so that every point in space knows exactly how far it sits from your model. That single dataset then powers everything from offsets to rebuilds. And if you are calculating a mesh creating signed distance field for mesh boolean work, too, the same grid lets you union, intersect, or subtract shapes with one-line min/max math.
Now, let’s explore the options:
- Per‑voxel nearest‑surface query (voxel scan + BVH). We march through each candidate voxel and ask a BVH‑accelerated ‘closest‑triangle distance?’. You can compare it with looping over a 3D spreadsheet and writing down how far every little cube is from the model.
- Sign assignment. After unsigned distances are known, vote inside vs. outside with winding numbers or ray hits or flood‑fill the signs. Think of colouring voxels red for ‘inside’ and blue for “outside,” then spreading that colour through the grid.
- GPU‑parallel voxel scan: Run the same nearest‑surface query, but split the work across thousands of GPU threads for big speed‑ups. Effectively, the graphics card measures all cubes at once instead of the CPU doing them one‑by‑one.
- Sparse / adaptive grids. Store fine voxels only near the surface and keep coarse blocks elsewhere to cut memory and compute. In other words, save detail where the action is and skip the empty air.
How to Create SDF from 3D Mesh with MeshLib?
MeshLib gives you several flexible ways to get mesh signed distance data, depending on your geometry and downstream needs. There are several types of volumes:
- VDB grids. These are based on the OpenVDB library which tells MeshLib how to assign ‘inside’ vs ‘outside’ labels during volume creation. These store only useful voxels, as sparse grids, which is good for SDFs.
- Simple voxels (existing as raw data)
- Functional volume which stores nothing, while being able to process your distance calculation queries.
Sparse volume
Objective:
meshToDistanceVdbVolume(), converts a mesh (or a part of it) into a sparse distance field using OpenVDB. It builds a tree-structured signed or unsigned distance grid, storing only active voxels. i.e., those near the surface, which makes it memory-efficient. For signed distances, the input mesh must be closed. The function returns a VdbVolume.
Applications:
Great for large or complex models, especially when working with, say, ray-marched rendering or any workflow that benefits from fast queries but does not need every voxel stored. Think of this as your go-to option for scalable, production-grade SDF generation.
Code examples:
from meshlib import mrmeshpy as mm
mesh = mm.loadMesh("mesh.stl")
params = mm.MeshToVolumeParams()
params.surfaceOffset = 3 # 3 voxel layers around surface will be active
params.type = mm.MeshToVolumeParams.Type.Signed if mesh.topology.isClosed() else mm.MeshToVolumeParams.Type.Unsigned
params.voxelSize = mm.Vector3f.diagonal(mm.suggestVoxelSize(mesh,1e7)) # voxel size to have approximately 10mln voxels
voxelsShift = mm.AffineXf3f()
params.outXf = voxelsShift # shift of voxels to original mesh
vdbVolume = mm.meshToDistanceVdbVolume(mesh,params)
gSettings = mm.GridToMeshSettings()
gSettings.voxelSize = params.voxelSize
gSettings.isoValue = 0.5 # in voxels measure
offMesh = mm.gridToMesh(vdbVolume.data,gSettings)
offMesh.transform(voxelsShift) # shift out mesh to initial mesh position
mm.saveMesh(offMesh,"out_mesh.stl")
#include "MRMesh/MRMeshLoad.h"
#include "MRMesh/MRMeshSave.h"
#include "MRMesh/MRMesh.h"
#include "MRVoxels/MRVDBConversions.h"
#include "MRVoxels/MROffset.h"
#include "MRVoxels/MRVoxelsVolume.h"
#include
int main()
{
auto meshLoadRes = MR::MeshLoad::fromAnySupportedFormat( "mesh.stl" );
if ( !meshLoadRes.has_value() )
{
std::cerr << meshLoadRes.error();
return 1;
}
auto params = MR::MeshToVolumeParams();
params.surfaceOffset = 3; // voxel layers around surface will be active
params.type = meshLoadRes->topology.isClosed() ? MR::MeshToVolumeParams::Type::Signed : MR::MeshToVolumeParams::Type::Unsigned;
params.voxelSize = MR::Vector3f::diagonal( MR::suggestVoxelSize( *meshLoadRes, 1e7f ) );
MR::AffineXf3f voxelShift;
params.outXf = &voxelShift; // shift of voxels to original mesh
auto vdbVolumeRes = MR::meshToDistanceVdbVolume( *meshLoadRes, params );
if ( !vdbVolumeRes.has_value() )
{
std::cerr << vdbVolumeRes.error();
return 1;
}
auto gSettings = MR::GridToMeshSettings();
gSettings.voxelSize = params.voxelSize;
gSettings.isoValue = 0.5f; // in voxels measure
auto offMeshRes = MR::gridToMesh( vdbVolumeRes->data, gSettings );
if ( !offMeshRes.has_value() )
{
std::cerr << offMeshRes.error();
return 1;
}
auto saveRes = MR::MeshSave::toAnySupportedFormat( *offMeshRes, "out_mesh.stl" );
if ( !saveRes.has_value() )
{
std::cerr << saveRes.error();
return 1;
}
return 0;
}
Dense volume
Objective:
meshToDistanceVolume() builds a dense 3D grid where every single voxel contains a signed distance value (or unsigned, you parameterize this via MeshToDistanceVolumeParams::SignedDistanceToMeshOptions::signMode). Namely, it returns a simple volume, with each voxel recorded. It is ideal when you need full data access, say, for simulation, remeshing, or exporting the volume to machine learning models or image slices.
Applications:
This one is a match for small to medium-sized meshes, i.e., where performance and memory use are not bottlenecks. If you seek exact voxel-by-voxel control or intend to apply marching cubes directly, this is the cleanest path.
Code examples:
from meshlib import mrmeshpy as mm
from meshlib import mrcudapy as mc
mesh = mm.loadMesh("mesh.stl")
oneDimVoxelSize = mm.suggestVoxelSize(mesh,1e7)
originAndDims = mm.calcOriginAndDimensions(mesh.computeBoundingBox(),oneDimVoxelSize)
params = mm.MeshToDistanceVolumeParams()
params.vol.voxelSize = mm.Vector3f.diagonal(oneDimVoxelSize) # voxel size to have approximately 10mln voxels
params.vol.origin = originAndDims.origin
params.vol.dimensions = originAndDims.dimensions
params.dist.maxDistSq = (3*oneDimVoxelSize)**2 # 3 voxel layers with valid distance
params.dist.nullOutsideMinMax = True # do not calculate values too far
params.dist.signMode = mm.SignDetectionMode.HoleWindingRule # not the fastest but most robust in case of holes and self-intersections
if mc.isCudaAvailable():
params.fwn = mc.FastWindingNumber(mesh)
simpleVolume = mm.meshToDistanceVolume(mesh,params)
mcParams = mm.MarchingCubesParams()
mcParams.lessInside = True # as far as we use SDF (this value should be false for Density volumes)
mcParams.origin = params.vol.origin
mcParams.iso = oneDimVoxelSize * 0.5 # half of voxel offset
offMesh = mm.marchingCubes(simpleVolume,mcParams)
mm.saveMesh(offMesh,"out_mesh.stl")
#include "MRMesh/MRMeshLoad.h"
#include "MRMesh/MRMeshSave.h"
#include "MRMesh/MRMesh.h"
#include "MRMesh/MRMeshFwd.h"
#include "MRVoxels/MRMeshToDistanceVolume.h"
#include "MRVoxels/MROffset.h"
#include "MRVoxels/MRCalcDims.h"
#include "MRCuda/MRCudaBasic.h"
#include "MRCuda/MRCudaFastWindingNumber.h"
#include "MRVoxels/MRMarchingCubes.h"
#include
int main()
{
auto meshLoadRes = MR::MeshLoad::fromAnySupportedFormat( "mesh.stl" );
if ( !meshLoadRes.has_value() )
{
std::cerr << meshLoadRes.error();
return 1;
}
float oneDimVoxelSize = MR::suggestVoxelSize( *meshLoadRes, 1e7f );
auto originAndDims = MR::calcOriginAndDimensions( meshLoadRes->computeBoundingBox(), oneDimVoxelSize );
auto params = MR::MeshToDistanceVolumeParams();
params.vol.voxelSize = MR::Vector3f::diagonal( oneDimVoxelSize ); // voxel size to have approximately 10mln voxels
params.vol.origin = originAndDims.origin;
params.vol.dimensions = originAndDims.dimensions;
params.dist.maxDistSq = MR::sqr( 3 * oneDimVoxelSize ); // 3 voxel layers with valid distance
params.dist.nullOutsideMinMax = true; // do not calculate values too far
params.dist.signMode = MR::SignDetectionMode::HoleWindingRule; // not the fastest but most robust in case of holes and self - intersections
if ( MR::Cuda::isCudaAvailable() )
params.fwn = std::make_shared( *meshLoadRes );
auto simpleVolumeRes = MR::meshToDistanceVolume( *meshLoadRes, params );
if ( !simpleVolumeRes.has_value() )
{
std::cerr << simpleVolumeRes.error();
return 1;
}
auto mcParams = MR::MarchingCubesParams();
mcParams.lessInside = true; // as far as we use SDF( this value should be false for Density volumes )
mcParams.origin = params.vol.origin;
mcParams.iso = oneDimVoxelSize * 0.5f; // half of voxel offset
auto offMeshRes = MR::marchingCubes( *simpleVolumeRes, mcParams );
if ( !offMeshRes.has_value() )
{
std::cerr << offMeshRes.error();
return 1;
}
auto saveRes = MR::MeshSave::toAnySupportedFormat( *offMeshRes, "out_mesh.stl" );
if ( !saveRes.has_value() )
{
std::cerr << saveRes.error();
return 1;
}
return 0;
}
Function volume
Objective:
meshToDistanceFunctionVolume() stores nothing. It returns a function, which, in turn, will return a distance. The convenience is that no memory is needed. Concurrently, you will spend computational power on each query.
Applications:
One can opt for meshToDistanceFunctionVolume() when memory is tight, however, powerful CPU/GPU cycles are available. Also, this might be an option when you require a handful of distance queries rather than millions.
Code examples:
from meshlib import mrmeshpy as mm
from meshlib import mrcudapy as mc
mesh = mm.loadMesh("mesh.stl")
oneDimVoxelSize = mm.suggestVoxelSize(mesh,1e7)
originAndDims = mm.calcOriginAndDimensions(mesh.computeBoundingBox(),oneDimVoxelSize)
params = mm.MeshToDistanceVolumeParams()
params.vol.voxelSize = mm.Vector3f.diagonal(oneDimVoxelSize) # voxel size to have approximately 10mln voxels
params.vol.origin = originAndDims.origin
params.vol.dimensions = originAndDims.dimensions
params.dist.maxDistSq = (3*oneDimVoxelSize)**2 # 3 voxel layers with valid distance
params.dist.nullOutsideMinMax = True # do not calculate values too far
params.dist.signMode = mm.SignDetectionMode.HoleWindingRule # not the fastest but most robust in case of holes and self-intersections
if mc.isCudaAvailable():
params.fwn = mc.FastWindingNumber(mesh)
functionVolume = mm.meshToDistanceFunctionVolume(mesh,params)
mcParams = mm.MarchingCubesParams()
mcParams.lessInside = True # as far as we use SDF (this value should be false for Density volumes)
mcParams.origin = params.vol.origin
mcParams.iso = oneDimVoxelSize * 0.5 # half of voxel offset
offMesh = mm.marchingCubes(functionVolume,mcParams)
mm.saveMesh(offMesh,"out_mesh.stl")
#include "MRMesh/MRMeshLoad.h"
#include "MRMesh/MRMeshSave.h"
#include "MRMesh/MRMesh.h"
#include "MRMesh/MRMeshFwd.h"
#include "MRVoxels/MRMeshToDistanceVolume.h"
#include "MRVoxels/MROffset.h"
#include "MRVoxels/MRCalcDims.h"
#include "MRCuda/MRCudaBasic.h"
#include "MRCuda/MRCudaFastWindingNumber.h"
#include "MRVoxels/MRMarchingCubes.h"
#include
int main()
{
auto meshLoadRes = MR::MeshLoad::fromAnySupportedFormat( "mesh.stl" );
if ( !meshLoadRes.has_value() )
{
std::cerr << meshLoadRes.error();
return 1;
}
float oneDimVoxelSize = MR::suggestVoxelSize( *meshLoadRes, 1e7f );
auto originAndDims = MR::calcOriginAndDimensions( meshLoadRes->computeBoundingBox(), oneDimVoxelSize );
auto params = MR::MeshToDistanceVolumeParams();
params.vol.voxelSize = MR::Vector3f::diagonal( oneDimVoxelSize ); // voxel size to have approximately 10mln voxels
params.vol.origin = originAndDims.origin;
params.vol.dimensions = originAndDims.dimensions;
params.dist.maxDistSq = MR::sqr( 3 * oneDimVoxelSize ); // 3 voxel layers with valid distance
params.dist.nullOutsideMinMax = true; // do not calculate values too far
params.dist.signMode = MR::SignDetectionMode::HoleWindingRule; // not the fastest but most robust in case of holes and self - intersections
if ( MR::Cuda::isCudaAvailable() )
params.fwn = std::make_shared( *meshLoadRes );
auto functionVolume = MR::meshToDistanceFunctionVolume( *meshLoadRes, params );
auto mcParams = MR::MarchingCubesParams();
mcParams.lessInside = true; // as far as we use SDF( this value should be false for Density volumes )
mcParams.origin = params.vol.origin;
mcParams.iso = oneDimVoxelSize * 0.5f; // half of voxel offset
auto offMeshRes = MR::marchingCubes( functionVolume, mcParams );
if ( !offMeshRes.has_value() )
{
std::cerr << offMeshRes.error();
return 1;
}
auto saveRes = MR::MeshSave::toAnySupportedFormat( *offMeshRes, "out_mesh.stl" );
if ( !saveRes.has_value() )
{
std::cerr << saveRes.error();
return 1;
}
return 0;
}


Step-by-Step Mesh to SDF conversion flow
A. Load the mesh
C. Make your choice, what volume you need
D. Adjust meshToVolumeParams or meshToDistanceVolumeParams
E. Run the conversion and use the field, calling the function
MeshLib: Library for Mesh to SDF
MeshLib will help you as a highly functional modern C++ library, featuring official API bindings for Python, C, and C#. This versatility makes it easy to integrate into any geometry pipeline, whether you are building tools, simulations, or custom viewers. All in all, MeshLib library allows you to rely on the Laplacian approach with an ideal balance.
If you are looking for a mesh signed distance field generator that works, MeshLib gives you native tools to create robust SDFs from triangle meshes.
Voxel Guide: Size, Distance, Types
Get started with setup and bindings:
Language Support and Compatibility
- C++: fully supported on all systems with no limitations.
- Python: compatible with Python 3.8 to 3.13 across platforms:
- Windows: Python 3.8–3.13
- macOS: Python 3.8–3.13 (except 3.8 on macOS x64)
- Linux: Python 3.8–3.13 on any manylinux_2_31+ system
You can employ MeshLib’s Python bindings to quickly implement a signed distance function mesh workflow in just a few lines of code.
Explore the full source code, prebuilt modules, and examples in the GitHub repository for converting a mesh to a signed distance field.
Whether you are prototyping in Python or embedding SDF tools into a C++ engine, MeshLib offers a flexible and high-performance foundation for any signed distance function mesh project.
The Full List of Mesh File Formats We Support
Format | Import | Texture Support | Color Support | Export |
---|---|---|---|---|
STL | Yes | No | No | Yes |
OBJ | Yes | Yes | Yes | Yes |
OFF | Yes | No | No | Yes |
DXF | Yes | No | No | Yes |
STEP | Yes | No | No | No |
STP | Yes | No | Yes | No |
CTM | Yes | No | Yes | Yes |
3MF | Yes | Yes | Yes | No |
MODEL | Yes | No | No | No |
PLY | Yes | No | Yes | Yes |
GLTF | Yes | Yes | Yes | Yes |
What our customers say
Thomas Tong
Founder, Polyga

Gal Cohen
CTO, customed.ai

Mariusz Hermansdorfer
Head of Computational Design at Henning Larsen Architechts

HeonJae Cho, DDS, MSD, PhD
Chief Executive Officer, 3DONS INC

Ruedger Rubbert
Chief Technology Officer, Brius Technologies Inc








Start Your Journey with MeshLib
MeshLib SDK offers multiple ways to dive in — from live technical demos to full application trials and hands-on SDK access. No complicated setups or hidden steps. Just the tools you need to start building smarter, faster, and better.
