Skip to content

SDF Objects in the Standard Viewer

The standard pytanga.viz viewer (the mesh-based Three.js viewer) can render some scene objects as smooth, ray-marched signed-distance-field (SDF) solids, mixed with the normal vertex/mesh pipeline in the same scene. The opt-in is a marker style class, SdfStyle, passed to Visualizer.add(...):

from pytanga.geometry import Point, Sphere
from pytanga.viz import SdfStyle, Visualizer

viz = Visualizer()
viz.add(Sphere(Point(0, 0, 0), 1.0), color="#4477cc")                 # normal mesh
viz.add(Sphere(Point(2.5, 0, 0), 1.1), style=SdfStyle(color="#ffaa00"))  # ray-marched
viz.show()
viz.wait()

SdfStyle

SdfStyle is a marker style: applying it opts that one entity into ray-marched SDF rendering (kind:"sdf" on the wire) instead of the normal mesh renderer. color/opacity still resolve through the normal priority chain (per-entity props > style > canonical > builtin); the remaining fields are SDF-specific knobs:

Field Type Default Meaning
color str None Optional override color (CSS hex).
opacity float None Optional override opacity (0..1).
soft_shadows bool True Enable soft self-shadowing in the ray-marcher.
max_steps int 256 Ray-march step budget.
bound_padding float 0.05 Inflate the proxy AABB (any over-estimate is safe).
antialias bool False Analytic ~1px silhouette edge fade (off by default; under investigation).

SDF object model (unified)

Beyond the SdfStyle marker, the standard viewer also ships a unified, composable object model: per-entity SDF styles, an SdfObject wrapper, Python operator CSG, and per-object materials.

Per-entity styles

Entity Style class Extra knob(s)
Sphere SdfSphereStyle —
Line SdfLineStyle thickness (default 1.0)
Circle SdfCircleStyle tube_radius (default 0.03)
Point SdfPointStyle size (default 0.08)
Cylinder SdfCylinderStyle —
Plane SdfPlaneStyle —
Disk SdfDiskStyle thickness (default 0.02)
PartialDisk SdfPartialDiskStyle thickness (default 0.02)
Box SdfBoxStyle —
Ellipsoid SdfEllipsoidStyle —
Ellipse SdfEllipseStyle thickness (default 0.02)
RegularPolygon SdfRegularPolygonStyle thickness (default 0.02)

Each inherits the SdfStyle knobs (color, opacity, soft_shadows, max_steps, bound_padding, antialias).

SdfObject + operators

from pytanga.geometry import Cylinder, Direction, Point, Sphere
from pytanga.viz import SdfCylinderStyle, SdfSphereStyle, Visualizer
from pytanga.viz.sdf import SdfObject

body = SdfObject(Sphere(Point(0, 0, 0), 1.2), id="body",
                 style=SdfSphereStyle(color="#ffaa00"))
drill = SdfObject(Cylinder(origin=Point(0, 0, 0), axis=Direction(0, 1, 0),
                           length=3.0, radius=0.35, align_center=0.5),
                  style=SdfCylinderStyle())

viz.add(body - drill)   # a drilled sphere (binary `Combine`)

SdfObject wraps a geometry entity plus an optional id and a per-entity style; it is converted to the low-level SDF tree at construction (never deep in the serializer). viz.add / viz.new accept SdfObject, Combine, Composed, and SdfGroup directly — no SdfStyle marker required.

Operators

Every SDF drawable (SdfObject / Combine / Composed / SdfGroup) supports Python CSG operators:

Operator Combine mode
a + b, a | b union (ECompose.UNION)
a - b subtract (ECompose.SUBTRACT)
a & b intersection (ECompose.INTERSECTION)
a ^ b xor (ECompose.XOR, binary-only)
-a tags a with SUBTRACT polarity (fold)
~a tags a with INTERSECTION polarity (fold)

Composed / SdfGroup members can be tagged with a fold mode via an SdfCompose(element, mode, smoothness=…) descriptor — the named replacement for the legacy (obj, "subtract") / (obj, "smooth_union", 0.15) tuple form (both remain supported).

Smooth blending

Smooth combine modes replace the hard min/max seam with a rounded, blended join of radius smoothness (frontend default 0.1):

Mode Combine mode
SdfCompose(obj, ECompose.SMOOTH_UNION) ECompose.SMOOTH_UNION
SdfCompose(obj, ECompose.SMOOTH_INTERSECTION) ECompose.SMOOTH_INTERSECTION
SdfCompose(obj, ECompose.SMOOTH_SUBTRACT) ECompose.SMOOTH_SUBTRACT

SdfCompose(obj, mode, smoothness=…) sets a per-member blend radius in a Composed / SdfGroup, Combine(ECompose.SMOOTH_UNION, a, b, smoothness=…) builds a smooth binary combine, and SdfStyle(smoothness=…) sets a per-object default:

from pytanga.viz.sdf import ECompose, SdfCompose, SdfGroup, capped_cylinder, sphere

group = SdfGroup(
    sphere(1.0, id="hub"),
    SdfCompose(capped_cylinder(1.5, 0.35, id="shaft"),
               ECompose.SMOOTH_UNION, smoothness=0.15),  # rounded join
)

Per-object materials

Composed / SdfGroup members keep their own color/opacity (a per-member materials array on the wire). Each member's style supplies its material; the proxy shader resolves the hit member's material at the surface.

Backward compatibility

viz.add(Sphere(...), style=SdfStyle(color=...)) (the marker path) keeps working and is now deprecated in favour of SdfObject(Sphere(...), style=SdfSphereStyle(...)).

Per-object CSG with Composed

A single SDF object can be internally Composed — its own combinator tree (e.g. a bead with a drilled hole).

from pytanga.viz import SdfStyle, Visualizer
from pytanga.viz.sdf import Composed, ECompose, SdfCompose, capped_cylinder, sphere

bead = Composed(
    sphere(0.7),
    SdfCompose(capped_cylinder(1.0, 0.45), ECompose.SUBTRACT),
)
viz.add(bead, style=SdfStyle(color="#44ff44"))

Groups with SdfGroup

SdfGroup bundles several members into one ray-marched solid, so cross-object CSG (union/intersection/subtract), smooth shading, and self-shadowing all work across the members — while each member keeps an independent runtime transform that can be animated separately (no shader recompile). The proxy bounding box is the union of the members' AABBs and resizes dynamically as they move.

Members may carry an optional id (every SDF constructor accepts an id=… keyword), so a member can be addressed by name or by 0-based index:

from pytanga.viz import SdfStyle, Visualizer
from pytanga.viz.sdf import ECompose, SdfCompose, SdfGroup, capped_cylinder, sphere

group = SdfGroup(
    sphere(1.0, position=(-1.0, 0.0, 0.0), id="left"),
    sphere(1.0, position=(1.0, 0.0, 0.0), id="orbit"),
    SdfCompose(capped_cylinder(1.5, 0.35), ECompose.SUBTRACT),  # cut through both spheres
)
sdf_grp = viz.new(group, style=SdfStyle(color="#ffaa00"))

# Animate a member independently — by id or by index, frame-by-frame.
sdf_grp.set_member_transform("orbit", position=(1.5, 0.4, 0.0))
viz.flush()

viz.new(…) returns a VizObjectRef; sdf_grp.entity is the SdfGroup itself, and sdf_grp.set_member_transform(…) is equivalent to viz.update_sdf_group_member(sdf_grp.id, …). Mutating directly through sdf_grp.entity.set_member_transform(…) also marks the node dirty, so either style works with a following flush().

Limitations

  • Member cap — an SdfGroup supports up to 16 members (a compile-time uniform-array bound).
  • Mutual shadows deferred — SDF objects get soft self-shadowing within an object/group, but do not cast or receive shadows onto other scene objects.
  • WebGL2 required — SDF objects need GLSL3 + gl_FragDepth. On WebGL1 they are skipped (hidden) and a single yellow warning banner is shown; the standard mesh pipeline keeps working.

Example

Script Topic
objects.py Mix standard meshes with SDF-styled objects (sphere + Composed bead + tween + interaction)
group.py SdfGroup with per-member CSG + independent member animation

Run with uv run python py/examples/viz/sdf/<script>.py.