Skip to content

creation

triwarp.creation

Parametric mesh generators: boxes, Platonic solids, surfaces of revolution and extrusions.

Every function returns a bare (vertices, faces) pair using triwarp's flat (3 * n_faces,) face layout, so results feed straight into the rest of the package or into Trimesh via tw.Trimesh(*tw.creation.box()).

revolve is the engine behind most of the module: uv_sphere, capsule, cylinder, cone, annulus and torus each build a small 2D profile and sweep it around the Z axis. box and the four Platonic solids (tetrahedron, octahedron, icosahedron, dodecahedron) are constant tables, and icosphere refines one of them in closed form. grid and sphere_cap are the two open primitives — a flat patch and a curved one, each with exactly one boundary loop.

parametric_surface and its three named siblings (super_ellipsoid, super_toroid, random_hills) sample an analytic map on an identified lattice, and are the only builders here that produce a non-orientable surface, an odd Euler characteristic, or a mesh whose scale is far from 1.

Each function carries the fixed host-side cost of a Warp wrapper call, so build a primitive once and reuse it rather than calling these repeatedly in a loop.

DEFAULT_SECTIONS module-attribute

DEFAULT_SECTIONS = 32

ParametricSurfaceKind module-attribute

ParametricSurfaceKind = Literal[
    "bohemian_dome",
    "bour",
    "boy",
    "catalan_minimal",
    "conic_spiral",
    "cross_cap",
    "dini",
    "enneper",
    "figure8_klein",
    "henneberg",
    "klein",
    "kuen",
    "mobius",
    "plucker_conoid",
    "pseudosphere",
    "roman",
]

Analytic surface selected by parametric_surface.

box

box(
    extents: tuple[float, float, float] | None = None,
    transform: mat44 | array[mat44] | None = None,
    bounds: Sequence[Sequence[float]] | None = None,
    device: DeviceLike = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]

Create a cuboid as 8 vertices and 12 triangles.

Parameters:

Name Type Description Default
extents tuple[float, float, float] | None

(3,) edge lengths. The box is centered on the origin. Defaults to a unit cube.

None
transform mat44 | array[mat44] | None

Transform applied after construction, as a (1,) wp.mat44 array or a scalar wp.mat44. Face winding is reversed when the transform has negative determinant, so normals keep pointing outward.

None
bounds Sequence[Sequence[float]] | None

(2, 3) axis-aligned corners. Overrides extents and transform, and yields a box spanning exactly those corners rather than one centered on the origin.

None
device DeviceLike

Warp device for the result. Defaults to the current device.

None

Returns:

Type Description
tuple[array[vec3], array[int32]]

(vertices, faces) with 8 vertices and 36 flat face indices.

Raises:

Type Description
ValueError

If bounds is combined with extents or transform, or if either has the wrong shape.

See Also

icosahedron cylinder trimesh.creation.box

Source code in triwarp/creation.py
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
def box(
    extents: tuple[float, float, float] | None = None,
    transform: wp.mat44 | wp.array[wp.mat44] | None = None,
    bounds: Sequence[Sequence[float]] | None = None,
    device: wp.DeviceLike = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]:
    """
    Create a cuboid as 8 vertices and 12 triangles.

    Parameters
    ----------
    extents
        ``(3,)`` edge lengths. The box is centered on the origin. Defaults to a unit cube.
    transform
        Transform applied after construction, as a ``(1,)`` ``wp.mat44`` array or a scalar
        ``wp.mat44``. Face winding is reversed when the transform has negative determinant, so
        normals keep pointing outward.
    bounds
        ``(2, 3)`` axis-aligned corners. Overrides ``extents`` and ``transform``, and yields a
        box spanning exactly those corners rather than one centered on the origin.
    device
        Warp device for the result. Defaults to the current device.

    Returns
    -------
    tuple[wp.array[wp.vec3], wp.array[wp.int32]]
        ``(vertices, faces)`` with 8 vertices and 36 flat face indices.

    Raises
    ------
    ValueError
        If ``bounds`` is combined with ``extents`` or ``transform``, or if either has the wrong
        shape.

    See Also
    --------
    [`icosahedron`][triwarp.creation.icosahedron]
    [`cylinder`][triwarp.creation.cylinder]
    [`trimesh.creation.box`][]
    """
    vertices_np = _BOX_VERTICES.copy()

    if bounds is not None:
        if transform is not None or extents is not None:
            raise ValueError("bounds overrides extents/transform: pass only one")
        bounds_np = np.asanyarray(bounds, dtype=np.float64)
        if bounds_np.shape != (2, 3):
            raise ValueError(f"bounds must be (2, 3) float, got {bounds_np.shape}")
        vertices_np *= np.ptp(bounds_np, axis=0)
        vertices_np += bounds_np[0]
    elif extents is not None:
        extents_np = np.asanyarray(extents, dtype=np.float64)
        if extents_np.shape != (3,):
            raise ValueError(f"extents must be (3,) float, got {extents_np.shape}")
        vertices_np -= 0.5
        vertices_np *= extents_np
    else:
        vertices_np -= 0.5

    vertices = _upload_points(vertices_np, wp.vec3, device)
    faces = wp.array(_BOX_FACES, dtype=wp.int32, device=device)
    return _apply_transform(vertices, faces, transform)

grid

grid(
    count: tuple[int, int] = (10, 10),
    extents: tuple[float, float] = (1.0, 1.0),
    center: bool = True,
    transform: mat44 | array[mat44] | None = None,
    device: DeviceLike = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]

Create a flat, regularly triangulated rectangle in the z = 0 plane.

The cheapest mesh with interior vertices and a boundary loop, which is what makes it the default fixture for anything that needs one: parametrization, boundary conditions, texture baking. Every quad cell is split by the same diagonal, so both triangles of a cell are right-angled — be aware that this makes every diagonal's cotangent weight exactly zero, which some solvers are sensitive to.

Parameters:

Name Type Description Default
count tuple[int, int]

(nx, ny) vertex counts along X and Y; each must be at least 2. The result has nx * ny vertices and 2 * (nx - 1) * (ny - 1) triangles.

(10, 10)
extents tuple[float, float]

(width, height) total span along X and Y.

(1.0, 1.0)
center bool

When True (the default) the patch spans [-extents / 2, extents / 2]; when False its lower corner sits at the origin, matching MeshLab's create_grid.

True
transform mat44 | array[mat44] | None

Transform applied after construction, as a (1,) wp.mat44 array or a scalar wp.mat44. Face winding is reversed when the transform has negative determinant.

None
device DeviceLike

Warp device for the result. Defaults to the current device.

None

Returns:

Type Description
tuple[array[vec3], array[int32]]

(vertices, faces) on device, wound so the normals point along +Z.

Raises:

Type Description
ValueError

If either entry of count is less than 2, or either entry of extents is negative.

See Also

box extrude_triangulation

Notes

Both buffers are written closed-form on the device, one thread per vertex and one per quad cell, rather than assembled on the host. Positions come out bit-identical to a NumPy build: the kernel does the same arithmetic in float64 before the float32 store, and phrases each sample as extent * (k / (n - 1)) so the far edge lands on the extent exactly, matching what numpy.linspace needs its endpoint special case for.

Source code in triwarp/creation.py
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
def grid(
    count: tuple[int, int] = (10, 10),
    extents: tuple[float, float] = (1.0, 1.0),
    center: bool = True,
    transform: wp.mat44 | wp.array[wp.mat44] | None = None,
    device: wp.DeviceLike = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]:
    """
    Create a flat, regularly triangulated rectangle in the ``z = 0`` plane.

    The cheapest mesh with interior vertices and a boundary loop, which is what makes it the
    default fixture for anything that needs one: parametrization, boundary conditions, texture
    baking. Every quad cell is split by the same diagonal, so both triangles of a cell are
    right-angled — be aware that this makes every diagonal's cotangent weight exactly zero, which
    some solvers are sensitive to.

    Parameters
    ----------
    count
        ``(nx, ny)`` **vertex** counts along X and Y; each must be at least 2. The result has
        ``nx * ny`` vertices and ``2 * (nx - 1) * (ny - 1)`` triangles.
    extents
        ``(width, height)`` total span along X and Y.
    center
        When ``True`` (the default) the patch spans ``[-extents / 2, extents / 2]``; when
        ``False`` its lower corner sits at the origin, matching MeshLab's ``create_grid``.
    transform
        Transform applied after construction, as a ``(1,)`` ``wp.mat44`` array or a scalar
        ``wp.mat44``. Face winding is reversed when the transform has negative determinant.
    device
        Warp device for the result. Defaults to the current device.

    Returns
    -------
    tuple[wp.array[wp.vec3], wp.array[wp.int32]]
        ``(vertices, faces)`` on ``device``, wound so the normals point along ``+Z``.

    Raises
    ------
    ValueError
        If either entry of ``count`` is less than 2, or either entry of ``extents`` is negative.

    See Also
    --------
    [`box`][triwarp.creation.box]
    [`extrude_triangulation`][triwarp.creation.extrude_triangulation]

    Notes
    -----
    Both buffers are written **closed-form on the device**, one thread per vertex and one per quad
    cell, rather than assembled on the host. Positions come out **bit-identical** to a NumPy
    build: the kernel does the same arithmetic in ``float64`` before the ``float32`` store, and
    phrases each sample as ``extent * (k / (n - 1))`` so the far edge lands on the extent exactly,
    matching what ``numpy.linspace`` needs its endpoint special case for.
    """
    nx, ny = int(count[0]), int(count[1])
    if nx < 2 or ny < 2:
        raise ValueError(f"count must be at least 2 along each axis, got {count}")
    width, height = float(extents[0]), float(extents[1])
    if width < 0.0 or height < 0.0:
        raise ValueError(f"extents must be non-negative, got {extents}")

    vertices = wp.empty(nx * ny, dtype=wp.vec3, device=device)
    wp.launch(
        kernel_creation.grid_vertices,
        dim=(nx, ny),
        inputs=[
            wp.int32(nx),
            wp.int32(ny),
            wp.float64(width),
            wp.float64(height),
            wp.float64(-0.5 * width if center else 0.0),
            wp.float64(-0.5 * height if center else 0.0),
        ],
        outputs=[vertices],
        device=device,
    )
    faces = wp.empty(6 * (nx - 1) * (ny - 1), dtype=wp.int32, device=device)
    wp.launch(
        kernel_creation.grid_faces,
        dim=(nx - 1, ny - 1),
        inputs=[wp.int32(ny)],
        outputs=[faces],
        device=device,
    )
    return _apply_transform(vertices, faces, transform)

icosahedron

icosahedron(
    device: DeviceLike = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]

Create a regular icosahedron on the unit sphere, centered on the origin.

Parameters:

Name Type Description Default
device DeviceLike

Warp device for the result. Defaults to the current device.

None

Returns:

Type Description
tuple[array[vec3], array[int32]]

(vertices, faces) with 12 vertices and 20 triangles.

See Also

icosphere trimesh.creation.icosahedron

Source code in triwarp/creation.py
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
def icosahedron(device: wp.DeviceLike = None) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]:
    """
    Create a regular icosahedron on the unit sphere, centered on the origin.

    Parameters
    ----------
    device
        Warp device for the result. Defaults to the current device.

    Returns
    -------
    tuple[wp.array[wp.vec3], wp.array[wp.int32]]
        ``(vertices, faces)`` with 12 vertices and 20 triangles.

    See Also
    --------
    [`icosphere`][triwarp.creation.icosphere]
    [`trimesh.creation.icosahedron`][]
    """
    vertices = _upload_points(_ICOSAHEDRON_VERTICES, wp.vec3, device)
    faces = wp.array(_ICOSAHEDRON_FACES, dtype=wp.int32, device=device)
    return vertices, faces

tetrahedron

tetrahedron(
    device: DeviceLike = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]

Create a regular tetrahedron on the unit sphere, centered on the origin.

The four vertices are the even-parity cube corners (+-1, +-1, +-1) / sqrt(3). This is the coarsest closed triangle mesh there is, which makes it the natural stress case for anything that assumes a vertex has a large ring: every vertex has valence 3 and every face borders every other.

Parameters:

Name Type Description Default
device DeviceLike

Warp device for the result. Defaults to the current device.

None

Returns:

Type Description
tuple[array[vec3], array[int32]]

(vertices, faces) with 4 vertices and 4 triangles.

See Also

octahedron icosahedron

Source code in triwarp/creation.py
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
def tetrahedron(device: wp.DeviceLike = None) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]:
    """
    Create a regular tetrahedron on the unit sphere, centered on the origin.

    The four vertices are the even-parity cube corners ``(+-1, +-1, +-1) / sqrt(3)``. This is the
    coarsest closed triangle mesh there is, which makes it the natural stress case for anything
    that assumes a vertex has a large ring: every vertex has valence 3 and every face borders every
    other.

    Parameters
    ----------
    device
        Warp device for the result. Defaults to the current device.

    Returns
    -------
    tuple[wp.array[wp.vec3], wp.array[wp.int32]]
        ``(vertices, faces)`` with 4 vertices and 4 triangles.

    See Also
    --------
    [`octahedron`][triwarp.creation.octahedron]
    [`icosahedron`][triwarp.creation.icosahedron]
    """
    return (
        _upload_points(_TETRAHEDRON_VERTICES, wp.vec3, device),
        wp.array(_TETRAHEDRON_FACES, dtype=wp.int32, device=device),
    )

octahedron

octahedron(
    device: DeviceLike = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]

Create a regular octahedron on the unit sphere, centered on the origin.

The six vertices are the +- unit axis directions, so every face is an octant of the coordinate frame — the shape to reach for when a test wants a closed mesh whose faces are exactly axis-aligned.

Parameters:

Name Type Description Default
device DeviceLike

Warp device for the result. Defaults to the current device.

None

Returns:

Type Description
tuple[array[vec3], array[int32]]

(vertices, faces) with 6 vertices and 8 triangles.

See Also

tetrahedron icosahedron

Source code in triwarp/creation.py
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
def octahedron(device: wp.DeviceLike = None) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]:
    """
    Create a regular octahedron on the unit sphere, centered on the origin.

    The six vertices are the ``+-`` unit axis directions, so every face is an octant of the
    coordinate frame — the shape to reach for when a test wants a closed mesh whose faces are
    exactly axis-aligned.

    Parameters
    ----------
    device
        Warp device for the result. Defaults to the current device.

    Returns
    -------
    tuple[wp.array[wp.vec3], wp.array[wp.int32]]
        ``(vertices, faces)`` with 6 vertices and 8 triangles.

    See Also
    --------
    [`tetrahedron`][triwarp.creation.tetrahedron]
    [`icosahedron`][triwarp.creation.icosahedron]
    """
    return (
        _upload_points(_OCTAHEDRON_VERTICES, wp.vec3, device),
        wp.array(_OCTAHEDRON_FACES, dtype=wp.int32, device=device),
    )

dodecahedron

dodecahedron(
    device: DeviceLike = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]

Create a regular dodecahedron on the unit sphere, centered on the origin.

triwarp is triangle-only, so the twelve pentagonal faces arrive triangulated: each pentagon is fanned from one of the eight cube-corner vertices, giving 36 triangles over 20 vertices with no added centroid. The fan is MeshLab's, so this is vertex- and face-for-face identical to create_dodecahedron up to the uniform scaling onto the unit sphere.

Because the triangulation is a fan rather than a symmetric split, the triangles are not congruent and the mesh is not a good uniform-sampling proxy — use icosphere for that.

Parameters:

Name Type Description Default
device DeviceLike

Warp device for the result. Defaults to the current device.

None

Returns:

Type Description
tuple[array[vec3], array[int32]]

(vertices, faces) with 20 vertices and 36 triangles.

See Also

icosahedron octahedron

Source code in triwarp/creation.py
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
def dodecahedron(device: wp.DeviceLike = None) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]:
    """
    Create a regular dodecahedron on the unit sphere, centered on the origin.

    triwarp is triangle-only, so the twelve pentagonal faces arrive triangulated: each pentagon is
    fanned from one of the eight cube-corner vertices, giving 36 triangles over 20 vertices with no
    added centroid. The fan is MeshLab's, so this is vertex- and face-for-face identical to
    ``create_dodecahedron`` up to the uniform scaling onto the unit sphere.

    Because the triangulation is a fan rather than a symmetric split, the triangles are *not*
    congruent and the mesh is not a good uniform-sampling proxy — use
    [`icosphere`][triwarp.creation.icosphere] for that.

    Parameters
    ----------
    device
        Warp device for the result. Defaults to the current device.

    Returns
    -------
    tuple[wp.array[wp.vec3], wp.array[wp.int32]]
        ``(vertices, faces)`` with 20 vertices and 36 triangles.

    See Also
    --------
    [`icosahedron`][triwarp.creation.icosahedron]
    [`octahedron`][triwarp.creation.octahedron]
    """
    return (
        _upload_points(_DODECAHEDRON_VERTICES, wp.vec3, device),
        wp.array(_DODECAHEDRON_FACES, dtype=wp.int32, device=device),
    )

icosphere

icosphere(
    subdivisions: int = 3,
    radius: float = 1.0,
    device: DeviceLike = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]

Create a geodesic sphere by recursively subdividing an icosahedron.

Each refinement level splits every triangle into four and projects the new vertices back onto the sphere, so the result has 20 * 4 ** subdivisions faces and 10 * 4 ** subdivisions + 2 vertices — the same mesh trimesh.creation.icosphere builds. Triangles stay far more uniform than uv_sphere's, at roughly an order of magnitude more work.

Parameters:

Name Type Description Default
subdivisions int

Number of refinement levels. Face count grows as 4 ** subdivisions, so values above ~7 get expensive. Values <= 0 return a plain icosahedron scaled to radius.

3
radius float

Sphere radius.

1.0
device DeviceLike

Warp device for the result. Defaults to the current device.

None

Returns:

Type Description
tuple[array[vec3], array[int32]]

(vertices, faces) on device.

Notes

The connectivity is generated in closed form rather than by iterating subdivide: every vertex of the refined mesh is addressed directly by its barycentric coordinates within one of the 20 base faces (see kernels.creation.icosphere_vertex_index), so the whole face buffer is written by one kernel and the vertices by one launch per refinement level, with no host synchronization.

The geometry is still the recursive one, level by level, because that is what the reference produces: a vertex is the projected midpoint of two vertices of the previous level, which is not the same point as the projection of the corresponding barycentric point of the base face (the two differ by a few percent of the edge length). Vertex order differs from trimesh — vertices come out as the 12 base corners, then the interior points of each base edge, then the interior points of each base face.

See Also

icosahedron uv_sphere subdivide trimesh.creation.icosphere

Source code in triwarp/creation.py
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
def icosphere(
    subdivisions: int = 3, radius: float = 1.0, device: wp.DeviceLike = None
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]:
    """
    Create a geodesic sphere by recursively subdividing an icosahedron.

    Each refinement level splits every triangle into four and projects the new vertices back onto
    the sphere, so the result has ``20 * 4 ** subdivisions`` faces and ``10 * 4 ** subdivisions +
    2`` vertices — the same mesh [`trimesh.creation.icosphere`][] builds. Triangles stay far more
    uniform than [`uv_sphere`][triwarp.creation.uv_sphere]'s, at roughly an order of magnitude
    more work.

    Parameters
    ----------
    subdivisions
        Number of refinement levels. Face count grows as ``4 ** subdivisions``, so values above
        ~7 get expensive. Values ``<= 0`` return a plain icosahedron scaled to ``radius``.
    radius
        Sphere radius.
    device
        Warp device for the result. Defaults to the current device.

    Returns
    -------
    tuple[wp.array[wp.vec3], wp.array[wp.int32]]
        ``(vertices, faces)`` on ``device``.

    Notes
    -----
    The connectivity is generated in closed form rather than by iterating
    [`subdivide`][triwarp.remesh.subdivide]: every vertex of the refined mesh is addressed directly
    by its barycentric coordinates within one of the 20 base faces (see
    ``kernels.creation.icosphere_vertex_index``), so the whole face buffer is written by **one**
    kernel and the vertices by one launch per refinement level, with no host synchronization.

    The *geometry* is still the recursive one, level by level, because that is what the reference
    produces: a vertex is the projected midpoint of two vertices of the previous level, which is
    not the same point as the projection of the corresponding barycentric point of the base face
    (the two differ by a few percent of the edge length). Vertex *order* differs from trimesh —
    vertices come out as the 12 base corners, then the interior points of each base edge, then the
    interior points of each base face.

    See Also
    --------
    [`icosahedron`][triwarp.creation.icosahedron]
    [`uv_sphere`][triwarp.creation.uv_sphere]
    [`subdivide`][triwarp.remesh.subdivide]
    [`trimesh.creation.icosphere`][]
    """
    levels = max(0, int(subdivisions))
    n = 1 << levels
    radius_f = wp.float32(float(radius))

    table = wp.array(_ICOSPHERE_FACE_TABLE, dtype=wp.int32, device=device)
    corners = _upload_points(_ICOSAHEDRON_VERTICES, wp.vec3, device)
    vertices = wp.empty(10 * 4**levels + 2, dtype=wp.vec3, device=corners.device)
    # The 12 base corners occupy the first block of the numbering, so scaling them to `radius` is
    # the whole of level 0.
    wp.map(kernel_creation.project_to_radius, corners, radius_f, out=vertices[:12])
    for level in range(1, levels + 1):
        wp.launch(
            kernel_creation.icosphere_generation,
            dim=(20, (1 << level) + 1, (1 << level) + 1),
            inputs=[table, wp.int32(n), wp.int32(1 << level), radius_f, vertices],
            device=vertices.device,
        )

    faces = wp.empty(20 * n * n * 3, dtype=wp.int32, device=vertices.device)
    wp.launch(
        kernel_creation.icosphere_faces,
        dim=(20, n, n),
        inputs=[table, wp.int32(n), faces],
        device=vertices.device,
    )
    return vertices, faces

uv_sphere

uv_sphere(
    radius: float = 1.0,
    count: tuple[int, int] | None = None,
    transform: mat44 | array[mat44] | None = None,
    device: DeviceLike = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]

Create a latitude/longitude sphere centered on the origin.

Much cheaper than icosphere, at the cost of triangles that shrink toward the poles.

Parameters:

Name Type Description Default
radius float

Sphere radius.

1.0
count tuple[int, int] | None

(2,) number of latitude and longitude lines. Defaults to (32, 64).

None
transform mat44 | array[mat44] | None

Transform applied after construction, as a (1,) wp.mat44 array or a scalar wp.mat44.

None
device DeviceLike

Warp device for the result. Defaults to the current device.

None

Returns:

Type Description
tuple[array[vec3], array[int32]]

(vertices, faces) on device.

Raises:

Type Description
ValueError

If count is given and is not a (2,) integer pair.

Notes

The longitude count is doubled when count is given explicitly but not when it is left at the default, so count=(32, 64) yields 128 longitude sections while count=None yields 64. This asymmetry is inherited from trimesh.creation.uv_sphere and kept for parity.

The two pole points are snapped to exactly (0, -radius) and (0, radius) in the 2D profile. sin(pi) is 1.2e-16 rather than zero, and without the snap the pole vertices of adjacent slices differ by 1.2e-16 * radius — enough that the weld and the degenerate-triangle filter in revolve both start to fail at large radii. The correction is far below float32 resolution.

See Also

icosphere capsule revolve trimesh.creation.uv_sphere

Source code in triwarp/creation.py
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
def uv_sphere(
    radius: float = 1.0,
    count: tuple[int, int] | None = None,
    transform: wp.mat44 | wp.array[wp.mat44] | None = None,
    device: wp.DeviceLike = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]:
    """
    Create a latitude/longitude sphere centered on the origin.

    Much cheaper than [`icosphere`][triwarp.creation.icosphere], at the cost of triangles that
    shrink toward the poles.

    Parameters
    ----------
    radius
        Sphere radius.
    count
        ``(2,)`` number of latitude and longitude lines. Defaults to ``(32, 64)``.
    transform
        Transform applied after construction, as a ``(1,)`` ``wp.mat44`` array or a scalar
        ``wp.mat44``.
    device
        Warp device for the result. Defaults to the current device.

    Returns
    -------
    tuple[wp.array[wp.vec3], wp.array[wp.int32]]
        ``(vertices, faces)`` on ``device``.

    Raises
    ------
    ValueError
        If ``count`` is given and is not a ``(2,)`` integer pair.

    Notes
    -----
    The longitude count is doubled when ``count`` is given explicitly but not when it is left at
    the default, so ``count=(32, 64)`` yields 128 longitude sections while ``count=None`` yields
    64. This asymmetry is inherited from [`trimesh.creation.uv_sphere`][] and kept for parity.

    The two pole points are snapped to exactly ``(0, -radius)`` and ``(0, radius)`` in the 2D
    profile. ``sin(pi)`` is ``1.2e-16`` rather than zero, and without the snap the pole vertices
    of adjacent slices differ by ``1.2e-16 * radius`` — enough that the weld and the
    degenerate-triangle filter in [`revolve`][triwarp.creation.revolve] both start to fail at
    large radii. The correction is far below ``float32`` resolution.

    See Also
    --------
    [`icosphere`][triwarp.creation.icosphere]
    [`capsule`][triwarp.creation.capsule]
    [`revolve`][triwarp.creation.revolve]
    [`trimesh.creation.uv_sphere`][]
    """
    if count is None:
        latitude, longitude = 32, 64
    else:
        counts = np.asanyarray(count, dtype=np.int64)
        if counts.shape != (2,):
            raise ValueError(f"count must be (2,) int, got {counts.shape}")
        counts = counts + counts % 2
        latitude, longitude = int(counts[0]), int(counts[1]) * 2

    radius_f = abs(float(radius))
    theta = np.linspace(0.0, math.pi, num=latitude)
    profile = np.column_stack((np.sin(theta), -np.cos(theta))) * radius_f
    # Snap the poles: sin(0) is exact but sin(pi) is not (see Notes).
    profile[0] = (0.0, -radius_f)
    profile[-1] = (0.0, radius_f)

    return revolve(
        _upload_points(profile, wp.vec2, device), sections=longitude, transform=transform
    )

sphere_cap

sphere_cap(
    angle: float = math.pi / 6.0,
    subdivisions: int = 3,
    radius: float = 1.0,
    device: DeviceLike = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]

Create a spherical cap around +Z: an open disc of a sphere, with one boundary loop.

A triangular lattice of 2 ** subdivisions concentric rings is projected onto the sphere, so ring r carries 6 * r vertices evenly spaced in azimuth at polar angle angle * r / n_rings. Unlike a sliced icosphere the rim is a clean circle of exactly 6 * n_rings vertices, which is what makes this the fixture of choice for boundary-condition work on a curved surface.

Parameters:

Name Type Description Default
angle float

Polar half-angle of the cap in radians, measured from +Z. Must be in (0, pi) exclusive: at pi the whole rim collapses onto the south pole and every rim triangle would be degenerate. MeshLab's create_sphere_cap takes the full aperture in degrees instead, so its angle=60 is math.radians(30) here.

pi / 6.0
subdivisions int

Number of refinement passes; the lattice has 2 ** subdivisions rings, giving 1 + 3 * n * (n + 1) vertices and 6 * n ** 2 triangles for n = 2 ** subdivisions. Must be >= 0.

3
radius float

Sphere radius.

1.0
device DeviceLike

Warp device for the result. Defaults to the current device.

None

Returns:

Type Description
tuple[array[vec3], array[int32]]

(vertices, faces) on device, wound so the normals point away from the sphere center. Vertex 0 is the apex at (0, 0, radius) and the last 6 * n vertices are the rim, in azimuthal order.

Raises:

Type Description
ValueError

If angle is outside (0, pi) or subdivisions is negative.

See Also

icosphere uv_sphere

Source code in triwarp/creation.py
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
def sphere_cap(
    angle: float = math.pi / 6.0,
    subdivisions: int = 3,
    radius: float = 1.0,
    device: wp.DeviceLike = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]:
    """
    Create a spherical cap around ``+Z``: an open disc of a sphere, with one boundary loop.

    A triangular lattice of ``2 ** subdivisions`` concentric rings is projected onto the sphere, so
    ring ``r`` carries ``6 * r`` vertices evenly spaced in azimuth at polar angle
    ``angle * r / n_rings``. Unlike a sliced [`icosphere`][triwarp.creation.icosphere] the rim is a
    clean circle of exactly ``6 * n_rings`` vertices, which is what makes this the fixture of choice
    for boundary-condition work on a curved surface.

    Parameters
    ----------
    angle
        Polar half-angle of the cap in **radians**, measured from ``+Z``. Must be in ``(0, pi)``
        exclusive: at ``pi`` the whole rim collapses onto the south pole and every rim triangle
        would be degenerate. MeshLab's ``create_sphere_cap`` takes the full aperture in degrees
        instead, so its ``angle=60`` is ``math.radians(30)`` here.
    subdivisions
        Number of refinement passes; the lattice has ``2 ** subdivisions`` rings, giving
        ``1 + 3 * n * (n + 1)`` vertices and ``6 * n ** 2`` triangles for ``n = 2 **
        subdivisions``. Must be ``>= 0``.
    radius
        Sphere radius.
    device
        Warp device for the result. Defaults to the current device.

    Returns
    -------
    tuple[wp.array[wp.vec3], wp.array[wp.int32]]
        ``(vertices, faces)`` on ``device``, wound so the normals point away from the sphere
        center. Vertex 0 is the apex at ``(0, 0, radius)`` and the last ``6 * n`` vertices are the
        rim, in azimuthal order.

    Raises
    ------
    ValueError
        If ``angle`` is outside ``(0, pi)`` or ``subdivisions`` is negative.

    See Also
    --------
    [`icosphere`][triwarp.creation.icosphere]
    [`uv_sphere`][triwarp.creation.uv_sphere]
    """
    if not 0.0 < angle < math.pi:
        raise ValueError(f"angle must be in (0, pi) radians, got {angle}")
    if subdivisions < 0:
        raise ValueError(f"subdivisions must be non-negative, got {subdivisions}")

    n_rings = 2 ** int(subdivisions)
    # ``ring_start[r]`` is where ring r's ``6 * r`` vertices begin; ring 0 is the single apex.
    ring_start = [0] + [1 + 3 * r * (r - 1) for r in range(1, n_rings + 1)]

    vertices_np = np.empty((1 + 3 * n_rings * (n_rings + 1), 3), dtype=np.float64)
    vertices_np[0] = (0.0, 0.0, radius)
    for r in range(1, n_rings + 1):
        theta = angle * r / n_rings
        phi = 2.0 * math.pi * np.arange(6 * r) / (6 * r)
        vertices_np[ring_start[r] : ring_start[r] + 6 * r] = np.column_stack(
            (
                radius * math.sin(theta) * np.cos(phi),
                radius * math.sin(theta) * np.sin(phi),
                np.full(6 * r, radius * math.cos(theta)),
            )
        )

    # Stitch ring r-1 to ring r: six sectors, and in each the outer ring carries one more vertex
    # than the inner one. That extra vertex is what turns the strip into ``2 * r - 1`` triangles --
    # ``6 * r`` outward-pointing (one per outer edge) and ``6 * (r - 1)`` inward-pointing (one per
    # inner edge) -- rather than an even fan, and it is why the total lands on exactly ``6 * n **
    # 2``.
    faces_np = np.empty((6 * n_rings * n_rings, 3), dtype=np.int32)
    written = 0
    for r in range(1, n_rings + 1):
        outer_base, inner_base = ring_start[r], ring_start[r - 1]
        inner_count = 6 * (r - 1) if r > 1 else 1
        outer_index = np.arange(6 * r)
        sector, step = np.divmod(outer_index, r)
        faces_np[written : written + 6 * r] = np.column_stack(
            (
                outer_base + outer_index,
                outer_base + (outer_index + 1) % (6 * r),
                inner_base + (sector * (r - 1) + step) % inner_count,
            )
        )
        written += 6 * r
        if r > 1:
            inner_index = np.arange(inner_count)
            sector, step = np.divmod(inner_index, r - 1)
            faces_np[written : written + inner_count] = np.column_stack(
                (
                    inner_base + inner_index,
                    outer_base + (sector * r + step + 1) % (6 * r),
                    inner_base + (inner_index + 1) % inner_count,
                )
            )
            written += inner_count

    return (
        _upload_points(vertices_np, wp.vec3, device),
        wp.array(np.ascontiguousarray(faces_np.reshape(-1)), dtype=wp.int32, device=device),
    )

capsule

capsule(
    height: float = 1.0,
    radius: float = 1.0,
    count: tuple[int, int] | None = None,
    transform: mat44 | array[mat44] | None = None,
    device: DeviceLike = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]

Create a capsule: a cylinder along Z closed by a hemisphere at each end.

The result is centered on the origin and spans z in [-height / 2 - radius, height / 2 + radius]; height is the center-to-center distance between the two hemispheres.

Parameters:

Name Type Description Default
height float

Center-to-center distance of the two hemispheres.

1.0
radius float

Radius of the cylinder and of both hemispheres.

1.0
count tuple[int, int] | None

(2,) number of sections along latitude and longitude. Defaults to (32, 64). Both entries are rounded up to even, which is what gives the two quarter-circle profiles below the equator the same point count.

None
transform mat44 | array[mat44] | None

Transform applied after construction, as a (1,) wp.mat44 array or a scalar wp.mat44.

None
device DeviceLike

Warp device for the result. Defaults to the current device.

None

Returns:

Type Description
tuple[array[vec3], array[int32]]

(vertices, faces) on device.

Raises:

Type Description
ValueError

If count is given and is not a (2,) integer pair.

Notes

Unlike uv_sphere, the longitude count is not doubled here. trimesh.creation.capsule's docstring describes the geometry as having one hemisphere at the origin and the other at height; the implementation (and this port) centers it instead.

The profile is two quarter-circles, each swept from the equator to a pole, rather than one continuous sweep from pole to pole: sharing the equator between two exactly cos(0) = 1-radius points (one shifted to each hemisphere) is what gives the cylindrical wall between them an exact, constant radius, matching trimesh.creation.capsule's own construction (which does this for the same reason, per its own comment, "two quarter circles sharing an equator vertex"). A single pole-to-pole sweep only approximates the equator at the two points nearest latitude / 2, which are a fraction of a section short of it, and reads as a nearly-vertical wall rather than an exactly vertical one; more concretely, it counts two fewer profile points than the exact construction, which shows up as a vertex-count mismatch against trimesh.creation.capsule at every longitude count.

See Also

uv_sphere cylinder trimesh.creation.capsule

Source code in triwarp/creation.py
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
def capsule(
    height: float = 1.0,
    radius: float = 1.0,
    count: tuple[int, int] | None = None,
    transform: wp.mat44 | wp.array[wp.mat44] | None = None,
    device: wp.DeviceLike = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]:
    """
    Create a capsule: a cylinder along Z closed by a hemisphere at each end.

    The result is centered on the origin and spans ``z`` in
    ``[-height / 2 - radius, height / 2 + radius]``; ``height`` is the center-to-center distance
    between the two hemispheres.

    Parameters
    ----------
    height
        Center-to-center distance of the two hemispheres.
    radius
        Radius of the cylinder and of both hemispheres.
    count
        ``(2,)`` number of sections along latitude and longitude. Defaults to ``(32, 64)``. Both
        entries are rounded up to even, which is what gives the two quarter-circle profiles below
        the equator the same point count.
    transform
        Transform applied after construction, as a ``(1,)`` ``wp.mat44`` array or a scalar
        ``wp.mat44``.
    device
        Warp device for the result. Defaults to the current device.

    Returns
    -------
    tuple[wp.array[wp.vec3], wp.array[wp.int32]]
        ``(vertices, faces)`` on ``device``.

    Raises
    ------
    ValueError
        If ``count`` is given and is not a ``(2,)`` integer pair.

    Notes
    -----
    Unlike [`uv_sphere`][triwarp.creation.uv_sphere], the longitude count is *not* doubled here.
    [`trimesh.creation.capsule`][]'s docstring describes the geometry as having one hemisphere at
    the origin and the other at ``height``; the implementation (and this port) centers it
    instead.

    The profile is two quarter-circles, each swept from the equator to a pole, rather than one
    continuous sweep from pole to pole: sharing the equator between two *exactly*
    ``cos(0) = 1``-radius points (one shifted to each hemisphere) is what gives the cylindrical
    wall between them an exact, constant radius, matching
    [`trimesh.creation.capsule`][]'s own construction (which does this for the same reason,
    per its own comment, "two quarter circles sharing an equator vertex"). A single pole-to-pole
    sweep only *approximates* the equator at the two points nearest ``latitude / 2``, which are a
    fraction of a section short of it, and reads as a nearly-vertical wall rather than an exactly
    vertical one; more concretely, it counts two fewer profile points than the exact
    construction, which shows up as a vertex-count mismatch against
    [`trimesh.creation.capsule`][] at every longitude count.

    See Also
    --------
    [`uv_sphere`][triwarp.creation.uv_sphere]
    [`cylinder`][triwarp.creation.cylinder]
    [`trimesh.creation.capsule`][]
    """
    counts = np.array([32, 64], dtype=np.int64) if count is None else np.asanyarray(count, np.int64)
    if counts.shape != (2,):
        raise ValueError(f"count must be (2,) int, got {counts.shape}")
    counts = counts + counts % 2
    latitude, longitude = int(counts[0]), int(counts[1])

    height_f = abs(float(height))
    radius_f = abs(float(radius))
    # Two quarter-circles sharing the equator, not one pole-to-pole sweep -- see the Notes above
    # for why the naive single sweep is both the wrong shape and the wrong vertex count.
    quarter_points = latitude // 2 + 1
    theta = np.concatenate(
        (
            np.linspace(-math.pi / 2.0, 0.0, quarter_points),
            np.linspace(0.0, math.pi / 2.0, quarter_points),
        )
    )
    profile = np.column_stack((np.cos(theta), np.sin(theta))) * radius_f
    half = len(profile) // 2
    profile[:half, 1] -= height_f / 2.0
    profile[half:, 1] += height_f / 2.0
    # Snap the poles the way uv_sphere does: cos(+-pi/2) is 6.1e-17, not zero.
    profile[0] = (0.0, -height_f / 2.0 - radius_f)
    profile[-1] = (0.0, height_f / 2.0 + radius_f)

    return revolve(
        _upload_points(profile, wp.vec2, device), sections=longitude, transform=transform
    )

cylinder

cylinder(
    radius: float,
    height: float | None = None,
    sections: int | None = None,
    segment: Sequence[Sequence[float]] | None = None,
    transform: mat44 | array[mat44] | None = None,
    device: DeviceLike = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]

Create a closed cylinder along Z, centered on the origin.

Parameters:

Name Type Description Default
radius float

Cylinder radius.

required
height float | None

Cylinder height. Required unless segment is passed.

None
sections int | None

Number of pie wedges around the revolution. Defaults to 32.

None
segment Sequence[Sequence[float]] | None

(2, 3) axis endpoints. Overrides both height and transform, placing the cylinder along the segment.

None
transform mat44 | array[mat44] | None

Transform applied after construction, as a (1,) wp.mat44 array or a scalar wp.mat44.

None
device DeviceLike

Warp device for the result. Defaults to the current device.

None

Returns:

Type Description
tuple[array[vec3], array[int32]]

(vertices, faces) on device.

Raises:

Type Description
ValueError

If neither height nor segment is given, or segment is not (2, 3).

See Also

annulus cone capsule trimesh.creation.cylinder

Source code in triwarp/creation.py
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
def cylinder(
    radius: float,
    height: float | None = None,
    sections: int | None = None,
    segment: Sequence[Sequence[float]] | None = None,
    transform: wp.mat44 | wp.array[wp.mat44] | None = None,
    device: wp.DeviceLike = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]:
    """
    Create a closed cylinder along Z, centered on the origin.

    Parameters
    ----------
    radius
        Cylinder radius.
    height
        Cylinder height. Required unless ``segment`` is passed.
    sections
        Number of pie wedges around the revolution. Defaults to 32.
    segment
        ``(2, 3)`` axis endpoints. Overrides both ``height`` and ``transform``, placing the
        cylinder along the segment.
    transform
        Transform applied after construction, as a ``(1,)`` ``wp.mat44`` array or a scalar
        ``wp.mat44``.
    device
        Warp device for the result. Defaults to the current device.

    Returns
    -------
    tuple[wp.array[wp.vec3], wp.array[wp.int32]]
        ``(vertices, faces)`` on ``device``.

    Raises
    ------
    ValueError
        If neither ``height`` nor ``segment`` is given, or ``segment`` is not ``(2, 3)``.

    See Also
    --------
    [`annulus`][triwarp.creation.annulus]
    [`cone`][triwarp.creation.cone]
    [`capsule`][triwarp.creation.capsule]
    [`trimesh.creation.cylinder`][]
    """
    transform, half = _resolve_cylinder_axis(height, segment, transform)
    radius_f = float(radius)
    profile = np.array(
        [[0.0, -half], [radius_f, -half], [radius_f, half], [0.0, half]], dtype=np.float64
    )
    return revolve(_upload_points(profile, wp.vec2, device), sections=sections, transform=transform)

cone

cone(
    radius: float,
    height: float,
    sections: int | None = None,
    transform: mat44 | array[mat44] | None = None,
    device: DeviceLike = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]

Create a closed cone along Z with its base on the z = 0 plane.

Parameters:

Name Type Description Default
radius float

Radius of the cone at its widest (the base).

required
height float

Height of the cone; the apex sits at (0, 0, height).

required
sections int | None

Number of pie wedges around the revolution. Defaults to 32.

None
transform mat44 | array[mat44] | None

Transform applied after construction, as a (1,) wp.mat44 array or a scalar wp.mat44.

None
device DeviceLike

Warp device for the result. Defaults to the current device.

None

Returns:

Type Description
tuple[array[vec3], array[int32]]

(vertices, faces) on device.

See Also

cylinder trimesh.creation.cone

Source code in triwarp/creation.py
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
def cone(
    radius: float,
    height: float,
    sections: int | None = None,
    transform: wp.mat44 | wp.array[wp.mat44] | None = None,
    device: wp.DeviceLike = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]:
    """
    Create a closed cone along Z with its base on the ``z = 0`` plane.

    Parameters
    ----------
    radius
        Radius of the cone at its widest (the base).
    height
        Height of the cone; the apex sits at ``(0, 0, height)``.
    sections
        Number of pie wedges around the revolution. Defaults to 32.
    transform
        Transform applied after construction, as a ``(1,)`` ``wp.mat44`` array or a scalar
        ``wp.mat44``.
    device
        Warp device for the result. Defaults to the current device.

    Returns
    -------
    tuple[wp.array[wp.vec3], wp.array[wp.int32]]
        ``(vertices, faces)`` on ``device``.

    See Also
    --------
    [`cylinder`][triwarp.creation.cylinder]
    [`trimesh.creation.cone`][]
    """
    profile = np.array([[0.0, 0.0], [float(radius), 0.0], [0.0, float(height)]], dtype=np.float64)
    return revolve(_upload_points(profile, wp.vec2, device), sections=sections, transform=transform)

annulus

annulus(
    r_min: float,
    r_max: float,
    height: float | None = None,
    sections: int | None = None,
    transform: mat44 | array[mat44] | None = None,
    segment: Sequence[Sequence[float]] | None = None,
    device: DeviceLike = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]

Create an annular cylinder (a tube with flat ends) along Z, centered on the origin.

Parameters:

Name Type Description Default
r_min float

Inner radius. When smaller than 1e-8 this delegates to cylinder, which has a different topology (no inner wall).

required
r_max float

Outer radius.

required
height float | None

Height of the annular cylinder. Required unless segment is passed.

None
sections int | None

Number of pie wedges around the revolution. Defaults to 32.

None
transform mat44 | array[mat44] | None

Transform applied after construction, as a (1,) wp.mat44 array or a scalar wp.mat44.

None
segment Sequence[Sequence[float]] | None

(2, 3) axis endpoints. Overrides both height and transform.

None
device DeviceLike

Warp device for the result. Defaults to the current device.

None

Returns:

Type Description
tuple[array[vec3], array[int32]]

(vertices, faces) on device.

Raises:

Type Description
ValueError

If neither height nor segment is given, or segment is not (2, 3).

See Also

cylinder torus trimesh.creation.annulus

Source code in triwarp/creation.py
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
def annulus(
    r_min: float,
    r_max: float,
    height: float | None = None,
    sections: int | None = None,
    transform: wp.mat44 | wp.array[wp.mat44] | None = None,
    segment: Sequence[Sequence[float]] | None = None,
    device: wp.DeviceLike = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]:
    """
    Create an annular cylinder (a tube with flat ends) along Z, centered on the origin.

    Parameters
    ----------
    r_min
        Inner radius. When smaller than ``1e-8`` this delegates to
        [`cylinder`][triwarp.creation.cylinder], which has a different topology (no inner wall).
    r_max
        Outer radius.
    height
        Height of the annular cylinder. Required unless ``segment`` is passed.
    sections
        Number of pie wedges around the revolution. Defaults to 32.
    transform
        Transform applied after construction, as a ``(1,)`` ``wp.mat44`` array or a scalar
        ``wp.mat44``.
    segment
        ``(2, 3)`` axis endpoints. Overrides both ``height`` and ``transform``.
    device
        Warp device for the result. Defaults to the current device.

    Returns
    -------
    tuple[wp.array[wp.vec3], wp.array[wp.int32]]
        ``(vertices, faces)`` on ``device``.

    Raises
    ------
    ValueError
        If neither ``height`` nor ``segment`` is given, or ``segment`` is not ``(2, 3)``.

    See Also
    --------
    [`cylinder`][triwarp.creation.cylinder]
    [`torus`][triwarp.creation.torus]
    [`trimesh.creation.annulus`][]
    """
    transform, half = _resolve_cylinder_axis(height, segment, transform)
    r_min_f = abs(float(r_min))
    if r_min_f < TOLERANCE_MERGE:
        return cylinder(
            radius=r_max, height=2.0 * half, sections=sections, transform=transform, device=device
        )

    r_max_f = abs(float(r_max))
    # Counter-clockwise rectangle with the first point repeated: the duplicate closes the profile
    # (and therefore the inner-wall/cap seam) once revolve's weld runs.
    profile = np.array(
        [[r_min_f, -half], [r_max_f, -half], [r_max_f, half], [r_min_f, half], [r_min_f, -half]],
        dtype=np.float64,
    )
    return revolve(_upload_points(profile, wp.vec2, device), sections=sections, transform=transform)

torus

torus(
    major_radius: float,
    minor_radius: float,
    major_sections: int = 32,
    minor_sections: int = 32,
    transform: mat44 | array[mat44] | None = None,
    device: DeviceLike = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]

Create a torus around Z, centered on the origin.

Parameters:

Name Type Description Default
major_radius float

Distance from the center of the torus to the center of the tube.

required
minor_radius float

Radius of the tube.

required
major_sections int

Number of sections around the major radius.

32
minor_sections int

Number of sections around the minor radius.

32
transform mat44 | array[mat44] | None

Transform applied after construction, as a (1,) wp.mat44 array or a scalar wp.mat44.

None
device DeviceLike

Warp device for the result. Defaults to the current device.

None

Returns:

Type Description
tuple[array[vec3], array[int32]]

(vertices, faces) with 2 * major_sections * minor_sections triangles.

Notes

The closing point of the tube profile is snapped to equal its first point exactly. sin(2 * pi) evaluates to -2.4e-16, so without the snap the minor seam would only close through revolve's absolute weld tolerance and would come apart at large radii.

See Also

annulus revolve trimesh.creation.torus

Source code in triwarp/creation.py
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
def torus(
    major_radius: float,
    minor_radius: float,
    major_sections: int = 32,
    minor_sections: int = 32,
    transform: wp.mat44 | wp.array[wp.mat44] | None = None,
    device: wp.DeviceLike = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]:
    """
    Create a torus around Z, centered on the origin.

    Parameters
    ----------
    major_radius
        Distance from the center of the torus to the center of the tube.
    minor_radius
        Radius of the tube.
    major_sections
        Number of sections around the major radius.
    minor_sections
        Number of sections around the minor radius.
    transform
        Transform applied after construction, as a ``(1,)`` ``wp.mat44`` array or a scalar
        ``wp.mat44``.
    device
        Warp device for the result. Defaults to the current device.

    Returns
    -------
    tuple[wp.array[wp.vec3], wp.array[wp.int32]]
        ``(vertices, faces)`` with ``2 * major_sections * minor_sections`` triangles.

    Notes
    -----
    The closing point of the tube profile is snapped to equal its first point exactly. ``sin(2 *
    pi)`` evaluates to ``-2.4e-16``, so without the snap the minor seam would only close through
    [`revolve`][triwarp.creation.revolve]'s absolute weld tolerance and would come apart at large
    radii.

    See Also
    --------
    [`annulus`][triwarp.creation.annulus]
    [`revolve`][triwarp.creation.revolve]
    [`trimesh.creation.torus`][]
    """
    minor_f = float(minor_radius)
    phi = np.linspace(0.0, 2.0 * math.pi, int(minor_sections) + 1, endpoint=True)
    profile = np.column_stack((minor_f * np.cos(phi), minor_f * np.sin(phi)))
    profile += (float(major_radius), 0.0)
    profile[-1] = profile[0]

    return revolve(
        _upload_points(profile, wp.vec2, device), sections=major_sections, transform=transform
    )

revolve

revolve(
    linestring: array[vec2],
    angle: float | None = None,
    cap: bool = False,
    sections: int | None = None,
    transform: mat44 | array[mat44] | None = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]

Revolve a 2D profile around the 2D Y axis, which becomes the 3D Z axis.

This is the shared engine behind every radially symmetric primitive in this module. The 2D X component of linestring is used as the revolution radius and the 2D Y component as the height along Z.

Parameters:

Name Type Description Default
linestring array[vec2]

(n,) ordered 2D profile points, n >= 2. A closed profile should be wound counter-clockwise for outward-facing normals.

required
angle float | None

Angle in radians to revolve through. None (the default) is a full revolution.

None
cap bool

For a partial revolution, triangulate the two end faces so the result is a closed volume. Ignored for a full revolution, which needs no caps. Requires a simple (non self-intersecting) profile — see triangulate_polygon.

False
sections int | None

Number of pie wedges around the revolution. Defaults to 32 per full revolution, scaled by angle.

None
transform mat44 | array[mat44] | None

Transform applied after construction, as a (1,) wp.mat44 array or a scalar wp.mat44. Face winding is reversed when its determinant is negative.

None

Returns:

Type Description
tuple[array[vec3], array[int32]]

(vertices, faces) on linestring.device.

Raises:

Type Description
ValueError

If linestring is not a rank-1 wp.vec2 array with at least 2 points, or sections resolves to less than 1.

RuntimeError

If linestring and transform are not all on one device.

Notes

Two clean-up steps are part of the algorithm, not optional polish:

  • Degenerate triangles are dropped. Any template triangle whose area is at most 1e-8 is discarded, which is how the polar triangles of a sphere, the apex and base fans of a cone, and the axis triangles of a cylinder disappear without being special-cased. The threshold is absolute, exactly as trimesh's tol.merge is, so a sphere's polar triangles stop registering as degenerate somewhere above radius = 1e4. Build near unit scale and pass a scaling transform if you need the result large.
  • Coincident vertices are collapsed, which is what makes the result watertight: a cone apex, a sphere pole, and the closing point of a closed profile each appear once per slice beforehand. trimesh discovers these by position hashing (the merge_vertices pass of Trimesh(process=True)); this port derives them from the profile instead — a profile point within 1e-8 of the axis becomes a single shared vertex, and a profile whose last point repeats its first shares that column. That is exact and scale independent, where a position hash both misses +0.0/-0.0 pole pairs and starts merging genuinely distinct vertices at high section counts.

A consequence of deriving the collapse rather than measuring it: a profile that repeats an interior point, or whose ends are close but not equal, keeps its duplicate vertices (the zero-area faces between them are still dropped, so they are simply unreferenced).

See Also

uv_sphere cylinder torus extrude_triangulation trimesh.creation.revolve

Source code in triwarp/creation.py
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
def revolve(
    linestring: wp.array[wp.vec2],
    angle: float | None = None,
    cap: bool = False,
    sections: int | None = None,
    transform: wp.mat44 | wp.array[wp.mat44] | None = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]:
    """
    Revolve a 2D profile around the 2D Y axis, which becomes the 3D Z axis.

    This is the shared engine behind every radially symmetric primitive in this module. The 2D X
    component of ``linestring`` is used as the revolution radius and the 2D Y component as the
    height along Z.

    Parameters
    ----------
    linestring
        ``(n,)`` ordered 2D profile points, ``n >= 2``. A closed profile should be wound
        counter-clockwise for outward-facing normals.
    angle
        Angle in radians to revolve through. ``None`` (the default) is a full revolution.
    cap
        For a partial revolution, triangulate the two end faces so the result is a closed volume.
        Ignored for a full revolution, which needs no caps. Requires a simple (non
        self-intersecting) profile — see
        [`triangulate_polygon`][triwarp.polyline.triangulate_polygon].
    sections
        Number of pie wedges around the revolution. Defaults to 32 per full revolution, scaled by
        ``angle``.
    transform
        Transform applied after construction, as a ``(1,)`` ``wp.mat44`` array or a scalar
        ``wp.mat44``. Face winding is reversed when its determinant is negative.

    Returns
    -------
    tuple[wp.array[wp.vec3], wp.array[wp.int32]]
        ``(vertices, faces)`` on ``linestring.device``.

    Raises
    ------
    ValueError
        If ``linestring`` is not a rank-1 ``wp.vec2`` array with at least 2 points, or
        ``sections`` resolves to less than 1.
    RuntimeError
        If ``linestring`` and ``transform`` are not all on one device.

    Notes
    -----
    Two clean-up steps are part of the algorithm, not optional polish:

    - **Degenerate triangles are dropped.** Any template triangle whose area is at most ``1e-8``
      is discarded, which is how the polar triangles of a sphere, the apex and base fans of a
      cone, and the axis triangles of a cylinder disappear without being special-cased. The
      threshold is absolute, exactly as trimesh's ``tol.merge`` is, so a sphere's polar triangles
      stop registering as degenerate somewhere above ``radius = 1e4``. Build near unit scale and
      pass a scaling ``transform`` if you need the result large.
    - **Coincident vertices are collapsed**, which is what makes the result watertight: a cone
      apex, a sphere pole, and the closing point of a closed profile each appear once *per slice*
      beforehand. `trimesh` discovers these by position hashing (the ``merge_vertices`` pass of
      ``Trimesh(process=True)``); this port derives them from the profile instead — a profile
      point within ``1e-8`` of the axis becomes a single shared vertex, and a profile whose last
      point repeats its first shares that column. That is exact and scale independent, where a
      position hash both misses ``+0.0``/``-0.0`` pole pairs and starts merging genuinely distinct
      vertices at high section counts.

    A consequence of deriving the collapse rather than measuring it: a profile that repeats an
    *interior* point, or whose ends are close but not equal, keeps its duplicate vertices (the
    zero-area faces between them are still dropped, so they are simply unreferenced).

    See Also
    --------
    [`uv_sphere`][triwarp.creation.uv_sphere]
    [`cylinder`][triwarp.creation.cylinder]
    [`torus`][triwarp.creation.torus]
    [`extrude_triangulation`][triwarp.creation.extrude_triangulation]
    [`trimesh.creation.revolve`][]
    """
    require_same_device(linestring=linestring, transform=transform)
    twt.ensure_ndim(linestring, 1, dtype=wp.vec2)
    device = linestring.device
    per = int(linestring.shape[0])
    if per < 2:
        raise ValueError(f"linestring must have at least 2 points, got {per}")

    full = 2.0 * math.pi
    closed = angle is None or abs(float(angle) - full) <= _CLOSED_ANGLE_ATOL
    span = full if angle is None else float(angle)
    if sections is None:
        sections = int(span / full * DEFAULT_SECTIONS)
    # trimesh converts the wedge count to a point count here.
    n_points = int(sections) + 1
    n_slices = n_points - 1
    if n_slices < 1:
        raise ValueError(f"sections must be at least 1, got {sections}")

    # Everything that depends only on the profile is decided on the host, from at most a few dozen
    # points: which template triangles survive, and where each (slice, profile point) pair lands in
    # the merged output. The two kernels below then write their final layout directly, with no
    # compaction pass and no device-to-host synchronization.
    profile_np = linestring.numpy().astype(np.float64)
    # A closed revolution folds its last slice back onto the first; an open one keeps them all.
    # The vertex layout and the launch must agree on this exactly -- the layout reserves the block
    # the kernel then writes into -- so it is computed once and passed to both.
    n_kept_slices = n_slices if closed else n_points
    keep_np = _revolve_kept_template(profile_np, span / float(n_slices))
    column_np, offsets_np, on_axis_np, n_vertices = _revolve_vertex_layout(
        profile_np, n_kept_slices
    )
    n_keep = int(keep_np.shape[0])
    layout = (
        wp.array(column_np, dtype=wp.int32, device=device),
        wp.array(offsets_np, dtype=wp.int32, device=device),
        wp.array(on_axis_np, dtype=wp.bool, device=device),
    )

    vertices = wp.empty(n_vertices, dtype=wp.vec3, device=device)
    wp.launch(
        kernel_creation.revolve_vertices,
        dim=(n_kept_slices, per),
        inputs=[
            linestring,
            wp.float32(span),
            wp.int32(n_points),
            wp.int32(n_kept_slices),
            *layout,
            vertices,
        ],
        device=device,
    )

    cap_faces = None
    n_cap = 0
    if cap and not closed:
        profile_3d = wp.empty(per, dtype=wp.vec3, device=device)
        wp.map(kernel_array.lift_vec2, linestring, wp.float32(0.0), out=profile_3d)
        # Ear clipping introduces no new vertices, so its indices address profile points directly --
        # the guarantee trimesh gets from ``triangulate_polygon(force_vertices=True)``.
        cap_faces = tw.polyline.polyline_triangulate(profile_3d).reshape((-1,))
        n_cap = int(cap_faces.shape[0]) // 3

    faces = wp.empty((n_slices * n_keep + 2 * n_cap) * 3, dtype=wp.int32, device=device)
    if n_keep > 0:
        wp.launch(
            kernel_creation.revolve_faces,
            dim=(n_slices, n_keep),
            inputs=[
                wp.array(keep_np, dtype=wp.int32, device=device),
                wp.int32(per),
                wp.int32(n_keep),
                wp.int32(n_kept_slices),
                *layout,
                faces[: n_slices * n_keep * 3],
            ],
            device=device,
        )
    if cap_faces is not None and n_cap > 0:
        base = n_slices * n_keep * 3
        for slice_index, reverse, offset in (
            (0, False, base),
            (n_kept_slices - 1, True, base + n_cap * 3),
        ):
            wp.launch(
                kernel_creation.revolve_cap_faces,
                dim=n_cap,
                inputs=[
                    cap_faces,
                    wp.int32(slice_index),
                    reverse,
                    wp.int32(n_kept_slices),
                    *layout,
                    faces[offset : offset + n_cap * 3],
                ],
                device=device,
            )

    return _apply_transform(vertices, faces, transform)

extrude_triangulation

extrude_triangulation(
    vertices: array[vec2],
    faces: array[int32],
    height: float,
    transform: mat44 | array[mat44] | None = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]

Extrude a 2D triangulation along Z into a watertight mesh.

The input triangulation becomes the two caps; the walls are raised from its boundary edges, which are recovered from the triangulation itself rather than assumed, so a subdivided outline works.

Parameters:

Name Type Description Default
vertices array[vec2]

(n,) 2D vertex positions.

required
faces array[int32]

Length-3 * n_faces flat triangle index buffer into vertices.

required
height float

Distance to extrude along Z. May be negative; the triangulation is re-wound to agree with its sign so the result always has positive volume.

required
transform mat44 | array[mat44] | None

Transform applied after construction, as a (1,) wp.mat44 array or a scalar wp.mat44. Face winding is reversed when its determinant is negative.

None

Returns:

Type Description
tuple[array[vec3], array[int32]]

(vertices, faces) with 2 * n vertices, on vertices.device.

Raises:

Type Description
ValueError

If vertices is not a rank-1 wp.vec2 array, faces is not a flat multiple of 3, or abs(height) is at most 1e-8.

RuntimeError

If vertices, faces and transform are not all on one device.

Notes

Unlike trimesh.creation.extrude_triangulation, which samples the first ten triangles to decide the input winding, this averages the signed area of every triangle. The answer is the same for any consistently wound triangulation and does not depend on which triangles happen to come first.

The walls index the cap vertices directly instead of carrying their own four-vertex quad per boundary edge, so the output is watertight with no vertex-merge pass (trimesh relies on Trimesh(process=True) to fuse the three blocks) and has exactly 2 * n vertices.

See Also

extrude_polygon sweep_polygon revolve trimesh.creation.extrude_triangulation

Source code in triwarp/creation.py
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
def extrude_triangulation(
    vertices: wp.array[wp.vec2],
    faces: wp.array[wp.int32],
    height: float,
    transform: wp.mat44 | wp.array[wp.mat44] | None = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]:
    """
    Extrude a 2D triangulation along Z into a watertight mesh.

    The input triangulation becomes the two caps; the walls are raised from its boundary edges,
    which are recovered from the triangulation itself rather than assumed, so a subdivided outline
    works.

    Parameters
    ----------
    vertices
        ``(n,)`` 2D vertex positions.
    faces
        Length-``3 * n_faces`` flat triangle index buffer into ``vertices``.
    height
        Distance to extrude along Z. May be negative; the triangulation is re-wound to agree with
        its sign so the result always has positive volume.
    transform
        Transform applied after construction, as a ``(1,)`` ``wp.mat44`` array or a scalar
        ``wp.mat44``. Face winding is reversed when its determinant is negative.

    Returns
    -------
    tuple[wp.array[wp.vec3], wp.array[wp.int32]]
        ``(vertices, faces)`` with ``2 * n`` vertices, on ``vertices.device``.

    Raises
    ------
    ValueError
        If ``vertices`` is not a rank-1 ``wp.vec2`` array, ``faces`` is not a flat multiple of 3,
        or ``abs(height)`` is at most ``1e-8``.
    RuntimeError
        If ``vertices``, ``faces`` and ``transform`` are not all on one device.

    Notes
    -----
    Unlike [`trimesh.creation.extrude_triangulation`][], which samples the first ten triangles to
    decide the input winding, this averages the signed area of *every* triangle. The answer is the
    same for any consistently wound triangulation and does not depend on which triangles happen to
    come first.

    The walls index the cap vertices directly instead of carrying their own four-vertex quad per
    boundary edge, so the output is watertight with no vertex-merge pass (trimesh relies on
    ``Trimesh(process=True)`` to fuse the three blocks) and has exactly ``2 * n`` vertices.

    See Also
    --------
    [`extrude_polygon`][triwarp.creation.extrude_polygon]
    [`sweep_polygon`][triwarp.creation.sweep_polygon]
    [`revolve`][triwarp.creation.revolve]
    [`trimesh.creation.extrude_triangulation`][]
    """
    require_same_device(vertices=vertices, faces=faces, transform=transform)
    twt.ensure_ndim(vertices, 1, dtype=wp.vec2)
    twt.ensure_ndim(faces, 1, dtype=wp.int32)
    device = vertices.device
    n = int(vertices.shape[0])
    n_faces = int(faces.shape[0]) // 3
    if int(faces.shape[0]) % 3 != 0:
        raise ValueError(f"faces size must be a multiple of 3, got {int(faces.shape[0])}")
    height_f = float(height)
    if abs(height_f) < TOLERANCE_MERGE:
        raise ValueError(f"height must be nonzero, got {height_f}")

    if n_faces == 0:
        return wp.empty(0, dtype=wp.vec3, device=device), wp.empty(0, dtype=wp.int32, device=device)

    # Re-wind the triangulation to match the sign of the extrusion, so both caps and the walls end
    # up facing outward.
    areas = wp.empty(n_faces, dtype=wp.float32, device=device)
    wp.launch(
        kernel_creation.triangulation_signed_areas,
        dim=n_faces,
        inputs=[vertices, faces, areas],
        device=device,
    )
    if math.copysign(1.0, tw.reduce.mean(areas)) != math.copysign(1.0, height_f):
        flipped = wp.empty_like(faces)
        wp.launch(
            kernel_repair.reverse_face_winding, dim=n_faces, inputs=[faces, flipped], device=device
        )
        faces = flipped

    bottom = wp.empty(2 * n, dtype=wp.vec3, device=device)
    wp.map(kernel_array.lift_vec2, vertices, wp.float32(0.0), out=bottom[:n])
    wp.map(kernel_array.lift_vec2, vertices, wp.float32(height_f), out=bottom[n:])

    boundary = tw.boundary.oriented_boundary_edges(bottom[:n].contiguous(), faces)
    n_boundary = int(boundary.shape[0])

    out_faces = wp.empty((2 * n_faces + 2 * n_boundary) * 3, dtype=wp.int32, device=device)
    # Bottom cap winding is reversed; the top cap keeps it and is offset by one vertex block.
    wp.launch(
        kernel_creation.offset_cap_faces_both,
        dim=(2, n_faces),
        inputs=[faces, wp.int32(n), out_faces[: 2 * n_faces * 3]],
        device=device,
    )
    if n_boundary > 0:
        wp.launch(
            kernel_creation.extrude_wall_faces,
            dim=n_boundary,
            inputs=[boundary, wp.int32(n), out_faces[2 * n_faces * 3 :]],
            device=device,
        )

    return _apply_transform(bottom, out_faces, transform)

extrude_polygon

extrude_polygon(
    polygon: array[vec2],
    height: float,
    transform: mat44 | array[mat44] | None = None,
    mid_plane: bool = False,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]

Extrude a simple 2D polygon along Z into a watertight prism.

Triangulates the ring with triangulate_polygon and hands the result to extrude_triangulation.

Parameters:

Name Type Description Default
polygon array[vec2]

(n,) 2D ring vertices, in order. A repeated closing point is dropped. Interior rings (holes) are not supported.

required
height float

Distance to extrude along Z.

required
transform mat44 | array[mat44] | None

Transform applied after construction, as a (1,) wp.mat44 array or a scalar wp.mat44.

None
mid_plane bool

Center the extrusion on z = 0 instead of starting it there.

False

Returns:

Type Description
tuple[array[vec3], array[int32]]

(vertices, faces) on polygon.device.

Raises:

Type Description
RuntimeError

If polygon and transform are not all on one device.

See Also

triangulate_polygon extrude_triangulation sweep_polygon trimesh.creation.extrude_polygon

Source code in triwarp/creation.py
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
def extrude_polygon(
    polygon: wp.array[wp.vec2],
    height: float,
    transform: wp.mat44 | wp.array[wp.mat44] | None = None,
    mid_plane: bool = False,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]:
    """
    Extrude a simple 2D polygon along Z into a watertight prism.

    Triangulates the ring with [`triangulate_polygon`][triwarp.polyline.triangulate_polygon] and
    hands the result to [`extrude_triangulation`][triwarp.creation.extrude_triangulation].

    Parameters
    ----------
    polygon
        ``(n,)`` 2D ring vertices, in order. A repeated closing point is dropped. Interior rings
        (holes) are not supported.
    height
        Distance to extrude along Z.
    transform
        Transform applied after construction, as a ``(1,)`` ``wp.mat44`` array or a scalar
        ``wp.mat44``.
    mid_plane
        Center the extrusion on ``z = 0`` instead of starting it there.

    Returns
    -------
    tuple[wp.array[wp.vec3], wp.array[wp.int32]]
        ``(vertices, faces)`` on ``polygon.device``.

    Raises
    ------
    RuntimeError
        If ``polygon`` and ``transform`` are not all on one device.

    See Also
    --------
    [`triangulate_polygon`][triwarp.polyline.triangulate_polygon]
    [`extrude_triangulation`][triwarp.creation.extrude_triangulation]
    [`sweep_polygon`][triwarp.creation.sweep_polygon]
    [`trimesh.creation.extrude_polygon`][]
    """
    require_same_device(polygon=polygon, transform=transform)
    ring, faces = tw.polyline.triangulate_polygon(polygon)
    if mid_plane:
        translation = np.eye(4)
        translation[2, 3] = abs(float(height)) / -2.0
        if transform is None:
            transform = wp.mat44(*translation.flatten())
        else:
            composed = tw.transform.matrix_to_numpy(transform).dot(translation)
            transform = wp.mat44(*composed.flatten())
    return extrude_triangulation(ring, faces, height, transform=transform)

sweep_polygon

sweep_polygon(
    polygon: array[vec2],
    path: array[vec3],
    angles: array[float32] | None = None,
    cap: bool = True,
    connect: bool = True,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]

Sweep a simple 2D polygon along a 3D path.

One copy of the polygon is placed at every path vertex, on the plane bisecting the two adjacent path segments, and consecutive copies are bridged into walls.

Parameters:

Name Type Description Default
polygon array[vec2]

(n,) 2D ring vertices, in order. A repeated closing point is dropped. Interior rings (holes) are not supported.

required
path array[vec3]

(m,) path vertices, m >= 2. The path counts as closed when its first and last vertex coincide.

required
angles array[float32] | None

(m,) roll of the polygon about the path tangent at each path vertex, in radians. Defaults to no roll.

None
cap bool

Triangulate the two ends. Ignored when the path is closed and connect is set, which needs no caps.

True
connect bool

For a closed path, fuse the last slice back onto the first so the sweep is a single watertight body.

True

Returns:

Type Description
tuple[array[vec3], array[int32]]

(vertices, faces) on polygon.device.

Raises:

Type Description
ValueError

If path has fewer than 2 vertices, angles does not match path in length, or the polygon's triangulation is not bounded by exactly one edge per ring vertex (which means the ring is not a simple polygon).

RuntimeError

If polygon, path and angles are not all on one device.

See Also

extrude_polygon revolve trimesh.creation.sweep_polygon

Source code in triwarp/creation.py
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
def sweep_polygon(
    polygon: wp.array[wp.vec2],
    path: wp.array[wp.vec3],
    angles: wp.array[wp.float32] | None = None,
    cap: bool = True,
    connect: bool = True,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]:
    """
    Sweep a simple 2D polygon along a 3D path.

    One copy of the polygon is placed at every path vertex, on the plane bisecting the two
    adjacent path segments, and consecutive copies are bridged into walls.

    Parameters
    ----------
    polygon
        ``(n,)`` 2D ring vertices, in order. A repeated closing point is dropped. Interior rings
        (holes) are not supported.
    path
        ``(m,)`` path vertices, ``m >= 2``. The path counts as closed when its first and last
        vertex coincide.
    angles
        ``(m,)`` roll of the polygon about the path tangent at each path vertex, in radians.
        Defaults to no roll.
    cap
        Triangulate the two ends. Ignored when the path is closed and ``connect`` is set, which
        needs no caps.
    connect
        For a closed path, fuse the last slice back onto the first so the sweep is a single
        watertight body.

    Returns
    -------
    tuple[wp.array[wp.vec3], wp.array[wp.int32]]
        ``(vertices, faces)`` on ``polygon.device``.

    Raises
    ------
    ValueError
        If ``path`` has fewer than 2 vertices, ``angles`` does not match ``path`` in length, or the
        polygon's triangulation is not bounded by exactly one edge per ring vertex (which means the
        ring is not a simple polygon).
    RuntimeError
        If ``polygon``, ``path`` and ``angles`` are not all on one device.

    See Also
    --------
    [`extrude_polygon`][triwarp.creation.extrude_polygon]
    [`revolve`][triwarp.creation.revolve]
    [`trimesh.creation.sweep_polygon`][]
    """
    require_same_device(polygon=polygon, path=path, angles=angles)
    twt.ensure_ndim(path, 1, dtype=wp.vec3)
    device = polygon.device
    n_path = int(path.shape[0])
    if n_path < 2:
        raise ValueError(f"path must have at least 2 points, got {n_path}")
    if angles is None:
        angles = wp.zeros(n_path, dtype=wp.float32, device=device)
    elif int(angles.shape[0]) != n_path:
        raise ValueError(
            f"angles must have one entry per path point ({n_path}), got {angles.shape}"
        )

    ring, cap_faces = tw.polyline.triangulate_polygon(polygon)
    stride = int(ring.shape[0])
    # oriented_boundary_edges only uses the vertex count (as its row-hash base), and the ring's own
    # 3D positions are never needed here, so a zero buffer of the right length is enough.
    boundary = tw.boundary.oriented_boundary_edges(
        wp.zeros(stride, dtype=wp.vec3, device=device), cap_faces
    )
    n_boundary = int(boundary.shape[0])
    if n_boundary != stride:
        raise ValueError(
            f"polygon must be a simple ring: its triangulation has {n_boundary} boundary edges "
            f"for {stride} vertices"
        )

    # Two 12-byte endpoint reads decide whether the path closes; the rest of it never leaves the
    # device. ``read_scalar`` avoids allocating a fresh host array per call the way
    # ``path[k : k + 1].numpy()[0]`` would.
    first = read_scalar(path, 0)
    last = read_scalar(path, n_path - 1)
    closed = math.dist(first, last) < TOLERANCE_MERGE
    connect_closed = closed and connect

    transforms = wp.empty(n_path, dtype=wp.mat44, device=device)
    wp.launch(
        kernel_creation.sweep_transforms,
        dim=n_path,
        inputs=[path, angles, connect_closed, transforms],
        device=device,
    )

    # A connected closed path drops its duplicate final slice and wraps onto the first instead.
    n_slices = n_path - 1
    n_vertices = (n_slices if connect_closed else n_path) * stride
    vertices = wp.empty(n_vertices, dtype=wp.vec3, device=device)
    wp.launch(
        kernel_creation.sweep_slice_vertices,
        dim=(n_vertices // stride, stride),
        inputs=[ring, transforms, wp.int32(stride), vertices],
        device=device,
    )

    n_cap = 0 if connect_closed or not cap else int(cap_faces.shape[0]) // 3
    faces = wp.empty((2 * n_slices * n_boundary + 2 * n_cap) * 3, dtype=wp.int32, device=device)
    wp.launch(
        kernel_creation.sweep_wall_faces,
        dim=(n_slices, n_boundary),
        inputs=[
            boundary,
            wp.int32(stride),
            wp.int32(n_vertices),
            faces[: 2 * n_slices * n_boundary * 3],
        ],
        device=device,
    )
    if n_cap > 0:
        base = 2 * n_slices * n_boundary * 3
        wp.launch(
            kernel_creation.offset_cap_faces_both,
            dim=(2, n_cap),
            inputs=[cap_faces, wp.int32(stride * n_slices), faces[base : base + 2 * n_cap * 3]],
            device=device,
        )

    return vertices, faces

truncated_prisms

truncated_prisms(
    vertices: array[vec3],
    faces: array[int32],
    origin: vec3 | None = None,
    normal: vec3 | None = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]

Extrude every triangle of a mesh down onto a plane, as one watertight prism each.

Parameters:

Name Type Description Default
vertices array[vec3]

(n_vertices,) mesh vertex positions.

required
faces array[int32]

Length-3 * n_faces flat triangle index buffer.

required
origin vec3 | None

Point on the truncation plane. None truncates against the z = 0 plane.

None
normal vec3 | None

Unit normal of the truncation plane. Required when origin is given.

None

Returns:

Type Description
tuple[array[vec3], array[int32]]

(vertices, faces) with 6 * n_faces vertices and 8 * n_faces triangles, on vertices.device.

Raises:

Type Description
ValueError

If faces is not a flat multiple of 3, or origin is given without normal.

RuntimeError

If vertices and faces are not all on one device.

Notes

Takes an indexed mesh rather than trimesh.creation.truncated_prisms's (n, 3, 3) triangle soup, matching the (vertices, faces) convention used throughout triwarp. Vertices are not merged, so each prism stays a separate body — the same choice trimesh makes with process=False.

See Also

extrude_triangulation trimesh.creation.truncated_prisms

Source code in triwarp/creation.py
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
def truncated_prisms(
    vertices: wp.array[wp.vec3],
    faces: wp.array[wp.int32],
    origin: wp.vec3 | None = None,
    normal: wp.vec3 | None = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]:
    """
    Extrude every triangle of a mesh down onto a plane, as one watertight prism each.

    Parameters
    ----------
    vertices
        ``(n_vertices,)`` mesh vertex positions.
    faces
        Length-``3 * n_faces`` flat triangle index buffer.
    origin
        Point on the truncation plane. ``None`` truncates against the ``z = 0`` plane.
    normal
        Unit normal of the truncation plane. Required when ``origin`` is given.

    Returns
    -------
    tuple[wp.array[wp.vec3], wp.array[wp.int32]]
        ``(vertices, faces)`` with ``6 * n_faces`` vertices and ``8 * n_faces`` triangles, on
        ``vertices.device``.

    Raises
    ------
    ValueError
        If ``faces`` is not a flat multiple of 3, or ``origin`` is given without ``normal``.
    RuntimeError
        If ``vertices`` and ``faces`` are not all on one device.

    Notes
    -----
    Takes an indexed mesh rather than [`trimesh.creation.truncated_prisms`][]'s ``(n, 3, 3)``
    triangle soup, matching the ``(vertices, faces)`` convention used throughout triwarp. Vertices
    are *not* merged, so each prism stays a separate body — the same choice trimesh makes with
    ``process=False``.

    See Also
    --------
    [`extrude_triangulation`][triwarp.creation.extrude_triangulation]
    [`trimesh.creation.truncated_prisms`][]
    """
    require_same_device(vertices=vertices, faces=faces)
    twt.ensure_ndim(faces, 1, dtype=wp.int32)
    device = vertices.device
    if int(faces.shape[0]) % 3 != 0:
        raise ValueError(f"faces size must be a multiple of 3, got {int(faces.shape[0])}")
    n_faces = int(faces.shape[0]) // 3
    if n_faces == 0:
        return wp.empty(0, dtype=wp.vec3, device=device), wp.empty(0, dtype=wp.int32, device=device)

    if origin is None:
        transform_np = np.eye(4)
    else:
        if normal is None:
            raise ValueError("normal is required when origin is given")
        transform_np = _plane_transform(np.array(origin), np.array(normal))

    out_vertices = wp.empty(6 * n_faces, dtype=wp.vec3, device=device)
    out_faces = wp.empty(24 * n_faces, dtype=wp.int32, device=device)
    to_plane = wp.mat44(*transform_np.flatten())
    wp.launch(
        kernel_creation.truncated_prism_geometry,
        dim=n_faces,
        inputs=[vertices, faces, to_plane, wp.inverse(to_plane), out_vertices, out_faces],
        device=device,
    )
    return out_vertices, out_faces

axis

axis(
    origin_size: float = 0.04,
    transform: mat44 | array[mat44] | None = None,
    axis_radius: float | None = None,
    axis_length: float | None = None,
    device: DeviceLike = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]

Create an XYZ axis marker: a ball at the origin and one cylinder along each axis.

Parameters:

Name Type Description Default
origin_size float

Radius of the ball marking the origin. The other defaults are derived from it.

0.04
transform mat44 | array[mat44] | None

Transform applied to the whole marker, as a (1,) wp.mat44 array or a scalar wp.mat44.

None
axis_radius float | None

Radius of the three axis cylinders. Defaults to origin_size / 5.

None
axis_length float | None

Length of the three axis cylinders. Defaults to origin_size * 10.

None
device DeviceLike

Warp device for the result. Defaults to the current device.

None

Returns:

Type Description
tuple[array[vec3], array[int32]]

(vertices, faces) on device, with the ball and the three cylinders concatenated in X, Y, Z order after the ball.

Notes

trimesh.creation.axis colors the ball white and the cylinders red/green/blue for X/Y/Z. triwarp carries no visual attributes, so this returns geometry only and takes no origin_color; the pieces are concatenated in a documented order so a caller can assign per-face colors itself.

See Also

icosphere cylinder concatenate trimesh.creation.axis

Source code in triwarp/creation.py
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
def axis(
    origin_size: float = 0.04,
    transform: wp.mat44 | wp.array[wp.mat44] | None = None,
    axis_radius: float | None = None,
    axis_length: float | None = None,
    device: wp.DeviceLike = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]:
    """
    Create an XYZ axis marker: a ball at the origin and one cylinder along each axis.

    Parameters
    ----------
    origin_size
        Radius of the ball marking the origin. The other defaults are derived from it.
    transform
        Transform applied to the whole marker, as a ``(1,)`` ``wp.mat44`` array or a scalar
        ``wp.mat44``.
    axis_radius
        Radius of the three axis cylinders. Defaults to ``origin_size / 5``.
    axis_length
        Length of the three axis cylinders. Defaults to ``origin_size * 10``.
    device
        Warp device for the result. Defaults to the current device.

    Returns
    -------
    tuple[wp.array[wp.vec3], wp.array[wp.int32]]
        ``(vertices, faces)`` on ``device``, with the ball and the three cylinders concatenated in
        X, Y, Z order after the ball.

    Notes
    -----
    [`trimesh.creation.axis`][] colors the ball white and the cylinders red/green/blue for X/Y/Z.
    triwarp carries no visual attributes, so this returns geometry only and takes no
    ``origin_color``; the pieces are concatenated in a documented order so a caller can assign
    per-face colors itself.

    See Also
    --------
    [`icosphere`][triwarp.creation.icosphere]
    [`cylinder`][triwarp.creation.cylinder]
    [`concatenate`][triwarp.combine.concatenate]
    [`trimesh.creation.axis`][]
    """
    origin_size_f = float(origin_size)
    if axis_radius is None:
        axis_radius = origin_size_f / 5.0
    if axis_length is None:
        axis_length = origin_size_f * 10.0

    # Each cylinder is built centered on the origin, so it has to be pushed out along its own axis
    # by half its length. The marker transform is applied once, to the assembled result.
    shift = np.eye(4)
    shift[2, 3] = float(axis_length) / 2.0

    parts = [icosphere(radius=origin_size_f, device=device)]
    for direction in (_UNIT_X, _UNIT_Y, _UNIT_Z):
        placement = _align_vectors(_UNIT_Z, direction).dot(shift)
        parts.append(
            cylinder(
                radius=float(axis_radius),
                height=float(axis_length),
                transform=wp.mat44(*placement.flatten()),
                device=device,
            )
        )
    return _apply_transform(*tw.combine.concatenate(parts), transform)

parametric_surface

parametric_surface(
    kind: ParametricSurfaceKind,
    u_resolution: int = 40,
    v_resolution: int = 40,
    device: DeviceLike = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]

Create one of sixteen classical analytic surfaces by sampling its parameterization.

These are the meshes with topology the rest of the module cannot produce: four are closed and non-orientable, two are non-orientable with a boundary, one is closed of genus 1, and the other nine are open patches, several with strongly graded triangles. Their Euler characteristics are 0 and 1 and their bounding-box diagonals span 1.7 to 28.3, so they are the package's inputs for is_orientable, homology_generators and anything whose behaviour should not depend on the scale of its input.

Parameters:

Name Type Description Default
kind ParametricSurfaceKind

Which surface to build:

  • non-orientable and closed — "boy" and "cross_cap" (the two immersions of the real projective plane, both with Euler characteristic 1), "figure8_klein" (the Klein bottle) and "roman" (Steiner's surface, with three double lines).
  • non-orientable with a boundary — "mobius" and "henneberg".
  • closed of genus 1 — "bohemian_dome", and see super_toroid.
  • open, with two boundary loops — "klein" (which is not a Klein bottle as VTK parameterizes it: it is orientable and has a boundary), "plucker_conoid" and "pseudosphere".
  • open, with one boundary loop — "bour", "catalan_minimal", "conic_spiral", "dini", "enneper" and "kuen". "dini" and "enneper" are the strongly graded ones.
required
u_resolution int

Number of samples along each parameter direction, including both ends of the domain. At least 2.

40
v_resolution int

Number of samples along each parameter direction, including both ends of the domain. At least 2.

40
device DeviceLike

Warp device for the result. Defaults to the current device.

None

Returns:

Type Description
tuple[array[vec3], array[int32]]

(vertices, faces) on device. The vertex count is below u_resolution * v_resolution wherever the surface glues, and the face count is 2 * (u_resolution - 1) * (v_resolution - 1) less one triangle per pole-adjacent cell.

Raises:

Type Description
ValueError

If kind is not one of the listed names, or either resolution is below 2 — or below 3 along an axis the surface wraps without a twist, where a resolution of 2 would identify every cell's two rows and leave no faces at all.

Notes

Ported from VTK's vtkParametric* classes, which pyvista exposes as pv.Parametric*; the maps are evaluated in VTK's own frame, so the two agree pointwise to float32.

The identification along a seam or at a pole is combinatorial — it is a fact about the map, applied to the index buffer — where VTK welds its raw lattice by distance afterwards. Two consequences worth knowing. The topology here is exact and identical at every resolution and on every device, and it does not depend on remove_duplicated_vertices, which several of these surfaces exist to test. And where an immersed surface merely crosses itself, the sheets stay separate: on Catalan's minimal surface VTK merges 40 lattice points that the parameterization does not identify, and drops the 2 triangles that thereby became degenerate.

Examples:

vertices, faces = tw.creation.parametric_surface("boy")
print(tw.measures.euler_characteristic(faces), tw.validation.is_orientable(faces))
See Also

super_ellipsoid super_toroid random_hills grid

Source code in triwarp/creation.py
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
def parametric_surface(
    kind: ParametricSurfaceKind,
    u_resolution: int = 40,
    v_resolution: int = 40,
    device: wp.DeviceLike = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]:
    """
    Create one of sixteen classical analytic surfaces by sampling its parameterization.

    These are the meshes with topology the rest of the module cannot produce: four are closed and
    **non-orientable**, two are non-orientable with a boundary, one is closed of genus 1, and the
    other nine are open patches, several with strongly graded triangles. Their Euler characteristics
    are 0 and 1 and their bounding-box diagonals span 1.7 to 28.3, so they are the package's inputs
    for
    [`is_orientable`][triwarp.validation.is_orientable],
    [`homology_generators`][triwarp.homology.homology_generators] and anything whose behaviour
    should not depend on the scale of its input.

    Parameters
    ----------
    kind
        Which surface to build:

        - non-orientable and closed — ``"boy"`` and ``"cross_cap"`` (the two immersions of the real
          projective plane, both with Euler characteristic 1), ``"figure8_klein"`` (the Klein
          bottle) and ``"roman"`` (Steiner's surface, with three double lines).
        - non-orientable with a boundary — ``"mobius"`` and ``"henneberg"``.
        - closed of genus 1 — ``"bohemian_dome"``, and see
          [`super_toroid`][triwarp.creation.super_toroid].
        - open, with two boundary loops — ``"klein"`` (which is *not* a Klein bottle as VTK
          parameterizes it: it is orientable and has a boundary), ``"plucker_conoid"`` and
          ``"pseudosphere"``.
        - open, with one boundary loop — ``"bour"``, ``"catalan_minimal"``, ``"conic_spiral"``,
          ``"dini"``, ``"enneper"`` and ``"kuen"``. ``"dini"`` and ``"enneper"`` are the strongly
          graded ones.
    u_resolution, v_resolution
        Number of samples along each parameter direction, including both ends of the domain. At
        least 2.
    device
        Warp device for the result. Defaults to the current device.

    Returns
    -------
    tuple[wp.array[wp.vec3], wp.array[wp.int32]]
        ``(vertices, faces)`` on ``device``. The vertex count is below
        ``u_resolution * v_resolution`` wherever the surface glues, and the face count is
        ``2 * (u_resolution - 1) * (v_resolution - 1)`` less one triangle per pole-adjacent cell.

    Raises
    ------
    ValueError
        If ``kind`` is not one of the listed names, or either resolution is below 2 — or below
        3 along an axis the surface wraps without a twist, where a resolution of 2 would identify
        every cell's two rows and leave no faces at all.

    Notes
    -----
    Ported from VTK's ``vtkParametric*`` classes, which pyvista exposes as ``pv.Parametric*``; the
    maps are evaluated in VTK's own frame, so the two agree pointwise to float32.

    The identification along a seam or at a pole is **combinatorial** — it is a fact about the map,
    applied to the index buffer — where VTK welds its raw lattice by distance afterwards. Two
    consequences worth knowing. The topology here is exact and identical at every resolution and on
    every device, and it does not depend on
    [`remove_duplicated_vertices`][triwarp.repair.remove_duplicated_vertices], which several of
    these surfaces exist to test. And where an immersed surface merely *crosses* itself, the sheets
    stay separate: on Catalan's minimal surface VTK merges 40 lattice points that the
    parameterization does not identify, and drops the 2 triangles that thereby became degenerate.

    Examples
    --------
    ```python
    vertices, faces = tw.creation.parametric_surface("boy")
    print(tw.measures.euler_characteristic(faces), tw.validation.is_orientable(faces))
    ```

    See Also
    --------
    [`super_ellipsoid`][triwarp.creation.super_ellipsoid]
    [`super_toroid`][triwarp.creation.super_toroid]
    [`random_hills`][triwarp.creation.random_hills]
    [`grid`][triwarp.creation.grid]
    """
    if kind not in _PARAMETRIC_SPECS:
        raise ValueError(f"unknown kind {kind!r}, expected one of {sorted(_PARAMETRIC_SPECS)}")
    return _parametric_surface(_PARAMETRIC_SPECS[kind], u_resolution, v_resolution, device)

super_ellipsoid

super_ellipsoid(
    n1: float = 1.0,
    n2: float = 1.0,
    radii: tuple[float, float, float] = (1.0, 1.0, 1.0),
    u_resolution: int = 40,
    v_resolution: int = 40,
    device: DeviceLike = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]

Create a superquadric ellipsoid: a closed genus-0 surface with a squareness axis.

n1 = n2 = 1 is the ellipsoid — the unit sphere at the default radii — and lowering either exponent flattens the surface towards a box, so a sweep of n1 takes one connectivity from smooth through creased to nearly sharp-edged. That axis is why this is a builder in its own right rather than one entry of parametric_surface's enum.

Parameters:

Name Type Description Default
n1 float

Squareness exponent along the v (latitude) direction. 1 is the ellipsoid; below 1 the surface creases at the equator, above 1 it pinches towards the poles.

1.0
n2 float

Squareness exponent along the u (longitude) direction, creasing the horizontal section the same way.

1.0
radii tuple[float, float, float]

(3,) semi-axes, applied after the shape exponents.

(1.0, 1.0, 1.0)
u_resolution int

Number of samples along each parameter direction, including both ends of the domain. At least 2.

40
v_resolution int

Number of samples along each parameter direction, including both ends of the domain. At least 2.

40
device DeviceLike

Warp device for the result. Defaults to the current device.

None

Returns:

Type Description
tuple[array[vec3], array[int32]]

(vertices, faces) on device, closed and watertight: the u direction wraps and both v extremes are poles.

Raises:

Type Description
ValueError

If radii does not have shape (3,), if v_resolution is below 2, or if u_resolution is below 3 — the u direction wraps, and a resolution of 2 there would identify every cell's two rows and leave no faces at all.

Notes

VTK's vtkParametricSuperEllipsoid, which pyvista exposes as pv.ParametricSuperEllipsoid and pv.Superquadric. The exponents apply through a signed power sign(x) |x| ** n, so the surface stays symmetric about every coordinate plane.

See Also

uv_sphere super_toroid parametric_surface

Source code in triwarp/creation.py
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
def super_ellipsoid(
    n1: float = 1.0,
    n2: float = 1.0,
    radii: tuple[float, float, float] = (1.0, 1.0, 1.0),
    u_resolution: int = 40,
    v_resolution: int = 40,
    device: wp.DeviceLike = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]:
    """
    Create a superquadric ellipsoid: a closed genus-0 surface with a squareness axis.

    ``n1 = n2 = 1`` is the ellipsoid — the unit sphere at the default ``radii`` — and lowering
    either exponent flattens the surface towards a box, so a sweep of ``n1`` takes one connectivity
    from smooth through creased to nearly sharp-edged. That axis is why this is a builder in its own
    right rather than one entry of
    [`parametric_surface`][triwarp.creation.parametric_surface]'s enum.

    Parameters
    ----------
    n1
        Squareness exponent along the v (latitude) direction. ``1`` is the ellipsoid; below ``1``
        the surface creases at the equator, above ``1`` it pinches towards the poles.
    n2
        Squareness exponent along the u (longitude) direction, creasing the horizontal section the
        same way.
    radii
        ``(3,)`` semi-axes, applied after the shape exponents.
    u_resolution, v_resolution
        Number of samples along each parameter direction, including both ends of the domain. At
        least 2.
    device
        Warp device for the result. Defaults to the current device.

    Returns
    -------
    tuple[wp.array[wp.vec3], wp.array[wp.int32]]
        ``(vertices, faces)`` on ``device``, closed and watertight: the u direction wraps and both
        v extremes are poles.

    Raises
    ------
    ValueError
        If ``radii`` does not have shape ``(3,)``, if ``v_resolution`` is below 2, or if
        ``u_resolution`` is below 3 — the u direction wraps, and a resolution of 2 there would
        identify every cell's two rows and leave no faces at all.

    Notes
    -----
    VTK's ``vtkParametricSuperEllipsoid``, which pyvista exposes as ``pv.ParametricSuperEllipsoid``
    and ``pv.Superquadric``. The exponents apply through a signed power ``sign(x) |x| ** n``, so the
    surface stays symmetric about every coordinate plane.

    See Also
    --------
    [`uv_sphere`][triwarp.creation.uv_sphere]
    [`super_toroid`][triwarp.creation.super_toroid]
    [`parametric_surface`][triwarp.creation.parametric_surface]
    """
    radii_np = np.asanyarray(radii, dtype=np.float64)
    if radii_np.shape != (3,):
        raise ValueError(f"radii must be (3,) float, got {radii_np.shape}")
    vertices, faces = _parametric_surface(
        _SUPER_ELLIPSOID_SPEC, u_resolution, v_resolution, device, n1=n1, n2=n2
    )
    if not np.array_equal(radii_np, np.ones(3)):
        return _apply_transform(vertices, faces, wp.mat44(*np.diag([*radii_np, 1.0]).ravel()))
    return vertices, faces

super_toroid

super_toroid(
    n1: float = 1.0,
    n2: float = 1.0,
    u_resolution: int = 40,
    v_resolution: int = 40,
    device: DeviceLike = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]

Create a superquadric torus: a closed genus-1 surface with a squareness axis.

The genus-1 counterpart of super_ellipsoid. n1 = n2 = 1 is the ordinary torus of major radius 1 and minor radius 0.5, and the exponents square off the tube's cross-section (n1) and the ring (n2) independently.

Parameters:

Name Type Description Default
n1 float

Squareness exponent of the tube cross-section.

1.0
n2 float

Squareness exponent of the ring.

1.0
u_resolution int

Number of samples along each parameter direction, including both ends of the domain. At least 2.

40
v_resolution int

Number of samples along each parameter direction, including both ends of the domain. At least 2.

40
device DeviceLike

Warp device for the result. Defaults to the current device.

None

Returns:

Type Description
tuple[array[vec3], array[int32]]

(vertices, faces) on device, closed and watertight with Euler characteristic 0: both parameter directions wrap.

Raises:

Type Description
ValueError

If either resolution is below 3. Both parameter directions wrap, and a resolution of 2 would identify every cell's two rows and leave no faces at all.

Notes

VTK's vtkParametricSuperToroid, which pyvista exposes as pv.ParametricSuperToroid. The radii are VTK's and are not exposed: torus is the builder for that axis, and this one exists for the exponents.

See Also

torus super_ellipsoid parametric_surface

Source code in triwarp/creation.py
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
def super_toroid(
    n1: float = 1.0,
    n2: float = 1.0,
    u_resolution: int = 40,
    v_resolution: int = 40,
    device: wp.DeviceLike = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]:
    """
    Create a superquadric torus: a closed genus-1 surface with a squareness axis.

    The genus-1 counterpart of [`super_ellipsoid`][triwarp.creation.super_ellipsoid]. ``n1 = n2 =
    1`` is the ordinary torus of major radius 1 and minor radius 0.5, and the exponents square off
    the tube's cross-section (``n1``) and the ring (``n2``) independently.

    Parameters
    ----------
    n1
        Squareness exponent of the tube cross-section.
    n2
        Squareness exponent of the ring.
    u_resolution, v_resolution
        Number of samples along each parameter direction, including both ends of the domain. At
        least 2.
    device
        Warp device for the result. Defaults to the current device.

    Returns
    -------
    tuple[wp.array[wp.vec3], wp.array[wp.int32]]
        ``(vertices, faces)`` on ``device``, closed and watertight with Euler characteristic 0:
        both parameter directions wrap.

    Raises
    ------
    ValueError
        If either resolution is below 3. Both parameter directions wrap, and a resolution of 2
        would identify every cell's two rows and leave no faces at all.

    Notes
    -----
    VTK's ``vtkParametricSuperToroid``, which pyvista exposes as ``pv.ParametricSuperToroid``. The
    radii are VTK's and are not exposed: [`torus`][triwarp.creation.torus] is the builder for that
    axis, and this one exists for the exponents.

    See Also
    --------
    [`torus`][triwarp.creation.torus]
    [`super_ellipsoid`][triwarp.creation.super_ellipsoid]
    [`parametric_surface`][triwarp.creation.parametric_surface]
    """
    return _parametric_surface(_SUPER_TOROID_SPEC, u_resolution, v_resolution, device, n1=n1, n2=n2)

random_hills

random_hills(
    n_hills: int = 30,
    amplitude: float = 2.0,
    x_variance: float = 2.5,
    y_variance: float = 2.5,
    seed: int | None = None,
    u_resolution: int = 40,
    v_resolution: int = 40,
    device: DeviceLike = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]

Create a smooth random height field over the square [-10, 10] ** 2.

A sum of n_hills Gaussian bumps at seeded random centres, sampled on a regular lattice: an open patch with one boundary loop, everywhere smooth, and with curvature that varies across it. That makes it the module's input for smoothing, curvature and remeshing, where the Platonic solids and the surfaces of revolution are too uniform to distinguish two implementations.

Parameters:

Name Type Description Default
n_hills int

Number of Gaussian bumps summed into the height.

30
amplitude float

Height of a single isolated bump. Overlapping bumps add.

2.0
x_variance float

Variances of each bump along X and Y, in the units of the [-10, 10] domain.

2.5
y_variance float

Variances of each bump along X and Y, in the units of the [-10, 10] domain.

2.5
seed int | None

RNG seed for the bump centres. A random one is drawn when omitted.

None
u_resolution int

Number of samples along X and Y, including both ends of the domain. At least 2.

40
v_resolution int

Number of samples along X and Y, including both ends of the domain. At least 2.

40
device DeviceLike

Warp device for the result. Defaults to the current device.

None

Returns:

Type Description
tuple[array[vec3], array[int32]]

(vertices, faces) on device with u_resolution * v_resolution vertices — nothing is identified — and 2 * (u_resolution - 1) * (v_resolution - 1) triangles.

Raises:

Type Description
ValueError

If either resolution is below 2, or either variance is not positive.

Notes

Named for VTK's vtkParametricRandomHills, and it is the same idea over the same domain, but the heights are not comparable: VTK draws each hill's amplitude and variance from its own generator, where this takes them as parameters and draws only the centres. The seed goes through resolve_seed, so a given seed reproduces a given mesh.

See Also

grid random_soup parametric_surface

Source code in triwarp/creation.py
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
def random_hills(
    n_hills: int = 30,
    amplitude: float = 2.0,
    x_variance: float = 2.5,
    y_variance: float = 2.5,
    seed: int | None = None,
    u_resolution: int = 40,
    v_resolution: int = 40,
    device: wp.DeviceLike = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]:
    """
    Create a smooth random height field over the square ``[-10, 10] ** 2``.

    A sum of ``n_hills`` Gaussian bumps at seeded random centres, sampled on a regular lattice: an
    open patch with one boundary loop, everywhere smooth, and with curvature that varies across it.
    That makes it the module's input for smoothing, curvature and remeshing, where the Platonic
    solids and the surfaces of revolution are too uniform to distinguish two implementations.

    Parameters
    ----------
    n_hills
        Number of Gaussian bumps summed into the height.
    amplitude
        Height of a single isolated bump. Overlapping bumps add.
    x_variance, y_variance
        Variances of each bump along X and Y, in the units of the ``[-10, 10]`` domain.
    seed
        RNG seed for the bump centres. A random one is drawn when omitted.
    u_resolution, v_resolution
        Number of samples along X and Y, including both ends of the domain. At least 2.
    device
        Warp device for the result. Defaults to the current device.

    Returns
    -------
    tuple[wp.array[wp.vec3], wp.array[wp.int32]]
        ``(vertices, faces)`` on ``device`` with ``u_resolution * v_resolution`` vertices — nothing
        is identified — and ``2 * (u_resolution - 1) * (v_resolution - 1)`` triangles.

    Raises
    ------
    ValueError
        If either resolution is below 2, or either variance is not positive.

    Notes
    -----
    Named for VTK's ``vtkParametricRandomHills``, and it is the same idea over the same domain, but
    the heights are not comparable: VTK draws each hill's amplitude and variance from its own
    generator, where this takes them as parameters and draws only the centres. The seed goes
    through [`resolve_seed`][triwarp.sample.resolve_seed], so a given seed reproduces a given mesh.

    See Also
    --------
    [`grid`][triwarp.creation.grid]
    [`random_soup`][triwarp.creation.random_soup]
    [`parametric_surface`][triwarp.creation.parametric_surface]
    """
    if float(x_variance) <= 0.0 or float(y_variance) <= 0.0:
        raise ValueError(f"variances must be positive, got {(x_variance, y_variance)}")
    sample_u, sample_v, faces = _parametric_samples(_RANDOM_HILLS_SPEC, u_resolution, v_resolution)
    generator = np.random.default_rng(tw.sample.resolve_seed(seed))
    centers = generator.uniform(
        low=(_RANDOM_HILLS_SPEC.u_range[0], _RANDOM_HILLS_SPEC.v_range[0]),
        high=(_RANDOM_HILLS_SPEC.u_range[1], _RANDOM_HILLS_SPEC.v_range[1]),
        size=(max(int(n_hills), 0), 2),
    )

    vertices = wp.empty(sample_u.shape[0], dtype=wp.vec3, device=device)
    wp.launch(
        kernel_creation.random_hills_vertices,
        dim=int(vertices.shape[0]),
        inputs=[
            wp.float32(amplitude),
            wp.float32(x_variance),
            wp.float32(y_variance),
            wp.array(centers, dtype=wp.vec2, device=device),
            wp.array(sample_u, dtype=wp.float32, device=device),
            wp.array(sample_v, dtype=wp.float32, device=device),
            vertices,
        ],
        device=device,
    )
    return vertices, wp.array(faces, dtype=wp.int32, device=device)

random_soup

random_soup(
    face_count: int = 100,
    seed: int | None = None,
    device: DeviceLike = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]

Create a soup of random, unconnected triangles inside the unit cube around the origin.

Parameters:

Name Type Description Default
face_count int

Number of triangles. Each gets its own three vertices, so nothing is shared.

100
seed int | None

RNG seed. A random one is drawn when omitted.

None
device DeviceLike

Warp device for the result. Defaults to the current device.

None

Returns:

Type Description
tuple[array[vec3], array[int32]]

(vertices, faces) with 3 * face_count vertices in [-0.5, 0.5] ** 3 and faces == arange(3 * face_count).

Notes

Takes an explicit seed where trimesh.creation.random_soup draws from NumPy's global RNG, so results are reproducible. The point values come from Warp's per-thread generator and do not match NumPy's stream.

See Also

trimesh.creation.random_soup

Source code in triwarp/creation.py
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
def random_soup(
    face_count: int = 100, seed: int | None = None, device: wp.DeviceLike = None
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]:
    """
    Create a soup of random, unconnected triangles inside the unit cube around the origin.

    Parameters
    ----------
    face_count
        Number of triangles. Each gets its own three vertices, so nothing is shared.
    seed
        RNG seed. A random one is drawn when omitted.
    device
        Warp device for the result. Defaults to the current device.

    Returns
    -------
    tuple[wp.array[wp.vec3], wp.array[wp.int32]]
        ``(vertices, faces)`` with ``3 * face_count`` vertices in ``[-0.5, 0.5] ** 3`` and
        ``faces == arange(3 * face_count)``.

    Notes
    -----
    Takes an explicit ``seed`` where [`trimesh.creation.random_soup`][] draws from NumPy's global
    RNG, so results are reproducible. The point values come from Warp's per-thread generator and do
    not match NumPy's stream.

    See Also
    --------
    [`trimesh.creation.random_soup`][]
    """
    n = 3 * max(int(face_count), 0)
    vertices = wp.empty(n, dtype=wp.vec3, device=device)
    if n > 0:
        resolved_seed = tw.sample.resolve_seed(seed)
        wp.launch(
            kernel_creation.random_soup_vertices,
            dim=n,
            inputs=[wp.int32(resolved_seed), vertices],
            device=device,
        )
    return vertices, tw.array.arange(n, device=vertices.device)