Skip to main content

Robust iso-surface rendering via interval-arithmetic ray casting for arbitrary implicit geometries

Project description

isoray

Robust iso-surface rendering via interval-arithmetic ray casting for arbitrary implicit geometries.

isoray renders the zero-level-set of any implicit function f: R^3 -> R using interval-arithmetic bisection. The primary target is black-box functions where only point evaluations are available — no symbolic formula access required. When the analytic formula is known, Natural Interval Arithmetic (NIA) and Affine Arithmetic (AA) provide tighter bounds as an optimization path.

Key Features

  • Black-box compatible — works with any function f(point) -> scalar. No symbolic access needed.
  • Guaranteed robustness — interval arithmetic ensures no surface features are missed, even for thin shells, cusps, high-frequency surfaces, and near-tangent intersections.
  • Three inclusion strategiesSamplingInclusion (black-box), NaturalIntervalInclusion (analytic), AffineInclusion (analytic, tighter bounds).
  • Newton refinement — optional post-bisection refinement for sub-pixel hit-point accuracy.
  • Mesh SDF support — render triangle meshes via signed distance fields (MeshSDF).
  • CSG operations — Union, Intersection, Difference compositing of implicit functions.
  • Batch-vectorized — all evaluations operate on (N, 3) arrays, no per-point loops.

Installation

Requires Python >= 3.11.

# Using uv (recommended)
uv pip install -e .

# Or using pip
pip install -e .

Dependencies

  • numpy >= 1.24.0
  • pillow >= 11.0.0
  • point-cloud-utils >= 0.34.0 (for mesh SDF)

Quick Start

import numpy as np
from isoray import render

# Define any implicit function: f(pos) -> (values, gradients_or_None)
def my_sphere(pos):
    return np.sum(pos**2, axis=1) - 1.0, None

# Render with one call — just provide the function and bounding box
result = render(my_sphere, bounds=([-1.5]*3, [1.5]*3), save_to="sphere.png")

Camera direction and distance are auto-calculated. Override as needed:

# Custom camera direction, explicit distance, high resolution
result = render(my_sphere, bounds=([-1.5]*3, [1.5]*3),
                direction=(0, 0, 1), distance=5.0,
                width=1024, height=1024, save_to="sphere_hd.png")

# Depth map instead of shaded image
result = render(my_sphere, bounds=([-1.5]*3, [1.5]*3),
                mode="depth", save_to="sphere_depth.png")

# Orthographic (parallel) projection
result = render(my_sphere, bounds=([-1.5]*3, [1.5]*3),
                projection="parallel", save_to="sphere_ortho.png")

Using a built-in geometry:

from isoray import Sphere, render

sphere = Sphere(radius=1.0)
result = render(sphere, bounds=sphere.bounds(), save_to="sphere.png")

Run the built-in example:

uv run python examples/render_simple.py

Low-Level Pipeline

For full control, use the six-step pipeline directly:

from isoray import (
    SamplingInclusion, generate_rays, ray_aabb_intersect,
    bisect_trace, newton_refine, shade_lambertian, save_image,
)

origins, directions = generate_rays(256, 256, fov_deg=60.0, eye=[0, 0, 3])
t_min, t_max, hit = ray_aabb_intersect(origins, directions, [-1.5]*3, [1.5]*3)
inclusion = SamplingInclusion(my_sphere, lipschitz=2.5)
result = bisect_trace(origins, directions, t_min, t_max, hit,
                      inclusion, my_sphere, max_depth=40)
result.t = newton_refine(result.t, origins, directions, result.hit, my_sphere,
                         t_lo=result.t_lo, t_hi=result.t_hi)
result.position = origins + result.t[:, None] * directions
# ... compute normals, shade, save

Rendering Pipeline

generate_rays -> ray_aabb_intersect -> bisect_trace -> newton_refine -> shade_lambertian -> save_image
  1. Ray generation — Pinhole camera model emits rays through each pixel.
  2. AABB intersection — Slab test clips rays to the bounding box, discarding misses early.
  3. Bisection trace — Level-synchronized interval bisection walks along each ray. The inclusion function bounds f over axis-aligned boxes; intervals that cannot contain zero are pruned.
  4. Newton refinement — Optional iterative refinement of hit points using directional derivatives.
  5. Shading — Lambertian shading from surface normals (analytic gradient or centered finite difference).
  6. Output — PNG images for both shaded color and depth maps.

Architecture

Core Protocols

Protocol Module Signature
ImplicitFunction geometry/interface.py __call__(pos: (N,3)) -> (val: (N,), grad: (N,3)|None) + bounds()
InclusionFunction arithmetic/interface.py evaluate(lo: (N,3), hi: (N,3)) -> (f_lo: (N,), f_hi: (N,))

Inclusion Strategies

Strategy Module Use Case
SamplingInclusion arithmetic/interval.py Black-box: samples 8 corners + center, adds Lipschitz margin
NaturalIntervalInclusion arithmetic/interval.py Analytic: evaluates formula with interval arithmetic ops
AffineInclusion arithmetic/affine.py Analytic: tighter bounds via noise-symbol correlation tracking

Module Layout

isoray/
  geometry/       # ImplicitFunction implementations
    interface.py  #   Protocol definition
    primitives.py #   Sphere, Torus, ThinShell, HighFrequencySurface, WhitneyUmbrella, CuspSurface
    csg.py        #   Union, Intersection, Difference
    mesh_sdf.py   #   MeshSDF (triangle mesh via point_cloud_utils)
  arithmetic/     # Interval & affine arithmetic
    interface.py  #   InclusionFunction protocol
    interval.py   #   ia_* ops, NIA recipes, SamplingInclusion, NaturalIntervalInclusion
    affine.py     #   AffineForm dataclass, aa_* ops, AA recipes, AffineInclusion
  tracer/         # Ray casting core
    bisection.py  #   Level-synchronized bisection (the core algorithm)
    refine.py     #   Newton refinement
    ray.py        #   Ray-AABB slab intersection
  renderer/       # Image output
    render.py     #   High-level render() API
    camera.py     #   Pinhole + orthographic ray generation
    shader.py     #   Lambertian shading
    image.py      #   PNG output (color + depth map)
  utils.py        # centered_difference_normals (6N-point finite diff)

Built-in Geometries

Geometry Description
Sphere |p - c|^2 - r^2
Torus (sqrt(x^2+y^2) - R)^2 + z^2 - r^2
ThinShell |x^2+y^2+z^2 - r^2| - epsilon
HighFrequencySurface sin(wx)sin(wy)sin(wz) - c
WhitneyUmbrella x^2 - y^2*z (self-intersecting)
CuspSurface x^3 - y^2 (degenerate gradient)
MeshSDF Signed distance field from triangle mesh (STL/OBJ)
Union / Intersection / Difference CSG compositing

Adding Custom Geometry

Black-box function (no formula access)

import numpy as np
from isoray import SamplingInclusion, bisect_trace

class MyFunction:
    def __call__(self, pos: np.ndarray) -> tuple[np.ndarray, None]:
        # pos: (N, 3) -> val: (N,), grad: None (unknown)
        x, y, z = pos[:, 0], pos[:, 1], pos[:, 2]
        val = x**2 + y**2 + z**2 - 1.0  # your function here
        return val, None  # gradient not available

    def bounds(self) -> tuple[np.ndarray, np.ndarray]:
        return np.array([-2.0, -2.0, -2.0]), np.array([2.0, 2.0, 2.0])

f = MyFunction()
inclusion = SamplingInclusion(f, lipschitz=3.0)  # estimate Lipschitz constant

Analytic function (NIA/AA recipes)

For tighter bounds, provide interval arithmetic or affine arithmetic recipes:

from isoray import NaturalIntervalInclusion, AffineInclusion
from isoray.arithmetic.interval import ia_mul, ia_add, ia_sub

# NIA recipe: pure function using ia_* interval ops
def my_nia(x_lo, x_hi, y_lo, y_hi, z_lo, z_hi, params):
    xx = ia_mul(x_lo, x_hi, x_lo, x_hi)
    yy = ia_mul(y_lo, y_hi, y_lo, y_hi)
    zz = ia_mul(z_lo, z_hi, z_lo, z_hi)
    sum_lo, sum_hi = ia_add(*ia_add(xx, yy), zz)
    return ia_sub(sum_lo, sum_hi, np.float64(1.0), np.float64(1.0))

inclusion = NaturalIntervalInclusion(my_nia, params=None)

Development

# Run all tests (146 tests)
uv run pytest

# Run specific test module
uv run pytest tests/test_tracer/test_bisection.py

# Lint and format
uv run ruff check isoray/ tests/
uv run ruff format isoray/ tests/

# Type check
uv run ty check

# Render all robustness test cases
uv run python examples/render_robustness.py

Test Organization

Directory Coverage
tests/test_arithmetic/ Interval and affine arithmetic operations, inclusion properties
tests/test_geometry/ Primitive function correctness
tests/test_tracer/ Bisection convergence, Newton refinement accuracy
tests/test_robustness/ Challenging cases: thin shell, cusp, grazing ray, near-tangent, high-frequency, self-intersecting, mesh SDF
tests/test_renderer/ End-to-end rendering pipeline

How It Works

The core algorithm is level-synchronized interval bisection. For each ray:

  1. The ray's parameter range [t_min, t_max] (from the AABB intersection) is represented as an interval.
  2. At each bisection step, the inclusion function computes guaranteed bounds [f_lo, f_hi] on f over the axis-aligned box swept by the ray segment.
  3. If f_lo > 0 or f_hi < 0 (the interval doesn't contain zero), the segment is pruned — no surface can exist there.
  4. Otherwise, the segment is split and both halves are recursively examined.
  5. When the interval width falls below a threshold, a surface hit is recorded.
  6. Optional Newton refinement then converges the hit point to machine precision.

This approach guarantees no surface features are missed, unlike fixed-step ray marching which can skip thin features or high-frequency detail.

License

MIT License. See LICENSE for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

isoray-0.1.0.tar.gz (24.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

isoray-0.1.0-py3-none-any.whl (31.5 kB view details)

Uploaded Python 3

File details

Details for the file isoray-0.1.0.tar.gz.

File metadata

  • Download URL: isoray-0.1.0.tar.gz
  • Upload date:
  • Size: 24.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.12

File hashes

Hashes for isoray-0.1.0.tar.gz
Algorithm Hash digest
SHA256 c6d5a2a7438de417e22c3cc452a3c21ad9d48b3719d7f8e64b95508301ffd7d6
MD5 477882bcd2e56d583a7a4d75df6a6635
BLAKE2b-256 50f4689d9612f0b811121e7965d20ea51a9f1a1821cfe82d2dba30c29b4ebca7

See more details on using hashes here.

File details

Details for the file isoray-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: isoray-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 31.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.12

File hashes

Hashes for isoray-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 78ceb90b09e399fe3159fdc22ca0057e81d9e67ddd834832974daac365467597
MD5 abaec7001cbf7e43f4e2d7b46e3f666d
BLAKE2b-256 a99c85dea77f7c3d4d82927d5860bd8d426a436a1699c227b7a0b0f7d563840a

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page