Skip to content

texture

triwarp.texture

Bake per-vertex attributes into UV-space textures and sample them back.

The four functions here operate on an existing UV map (from a disk parameterization or a mesh file, e.g. loaded via load_mesh): they rasterize per-vertex attributes into a square texture and sample a texture back to per-vertex values. They are a Warp software-rasterizer replacement for an OpenGL/moderngl pipeline, keeping all data on the input Warp device.

Computing a UV map from mesh connectivity (parameterization) is a separate concern and is not part of this module.

See rasterize_attribute, rasterize_discrete_attribute, remap_attribute_from_uv, and remap_discrete_attribute_from_uv.

rasterize_attribute

rasterize_attribute(
    uv: array[vec2],
    faces: array[int32],
    attribute: Array2dFloat32,
    resolution: int,
) -> twt.Array3dFloat32

Rasterize a per-vertex attribute into a square UV-space texture.

Each triangle is rasterized in UV space and its per-vertex attribute is barycentrically interpolated at every covered pixel center. Pixels not covered by any triangle stay 0. Where triangles overlap (degenerate for a valid parameterization), the lowest-index face wins.

Parameters:

Name Type Description Default
uv array[vec2]

(n_vertices,) per-vertex UV coordinates as wp.vec2 in [0, 1]. Non-finite rows (e.g. unreferenced vertices left NaN by a disk parameterization) are ignored; by construction no face indexes them.

required
faces array[int32]

Flat length-3 * n_faces wp.int32 triangle index buffer.

required
attribute Array2dFloat32

(n_vertices, n_channels) float32 per-vertex attribute to interpolate.

required
resolution int

Output image size in pixels (square).

required

Returns:

Type Description
Array3dFloat32

(resolution, resolution, n_channels) float32 image on uv.device. Row 0 corresponds to v = 1 (vertical flip), matching the sampling convention of remap_attribute_from_uv.

Raises:

Type Description
ValueError

If resolution is not positive, if uv and attribute disagree on their row count, or if any finite UV lies outside [0, 1].

RuntimeError

If uv, faces and attribute are not all on one device.

See Also

remap_attribute_from_uv rasterize_discrete_attribute

Source code in triwarp/texture.py
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
def rasterize_attribute(
    uv: wp.array[wp.vec2], faces: wp.array[wp.int32], attribute: twt.Array2dFloat32, resolution: int
) -> twt.Array3dFloat32:
    """
    Rasterize a per-vertex attribute into a square UV-space texture.

    Each triangle is rasterized in UV space and its per-vertex attribute is barycentrically
    interpolated at every covered pixel center. Pixels not covered by any triangle stay ``0``.
    Where triangles overlap (degenerate for a valid parameterization), the lowest-index face wins.

    Parameters
    ----------
    uv
        ``(n_vertices,)`` per-vertex UV coordinates as ``wp.vec2`` in ``[0, 1]``. Non-finite
        rows (e.g. unreferenced vertices left ``NaN`` by a disk parameterization) are ignored;
        by construction no face indexes them.
    faces
        Flat length-``3 * n_faces`` ``wp.int32`` triangle index buffer.
    attribute
        ``(n_vertices, n_channels)`` ``float32`` per-vertex attribute to interpolate.
    resolution
        Output image size in pixels (square).

    Returns
    -------
    Array3dFloat32
        ``(resolution, resolution, n_channels)`` ``float32`` image on ``uv.device``. Row ``0``
        corresponds to ``v = 1`` (vertical flip), matching the sampling convention of
        [`remap_attribute_from_uv`][triwarp.texture.remap_attribute_from_uv].

    Raises
    ------
    ValueError
        If ``resolution`` is not positive, if ``uv`` and ``attribute`` disagree on their row
        count, or if any finite UV lies outside ``[0, 1]``.
    RuntimeError
        If ``uv``, ``faces`` and ``attribute`` are not all on one device.

    See Also
    --------
    [`remap_attribute_from_uv`][triwarp.texture.remap_attribute_from_uv]
    [`rasterize_discrete_attribute`][triwarp.texture.rasterize_discrete_attribute]
    """
    require_same_device(uv=uv, faces=faces, attribute=attribute)
    twt.ensure_ndim(attribute, 2, dtype=wp.float32)
    n_channels = int(attribute.shape[1])
    _check_rasterize_inputs(uv, int(attribute.shape[0]), resolution)

    device = uv.device
    image = wp.zeros((resolution, resolution, n_channels), dtype=wp.float32, device=device)
    n_faces = int(faces.shape[0]) // 3
    if n_faces == 0:
        return twt.as_array3d(image, wp.float32)

    owner = _rasterize_owner(uv, faces, resolution)
    wp.launch(
        kernel_texture.rasterize_scatter,
        dim=n_faces,
        inputs=[uv, faces, attribute, n_channels, owner, image],
        device=device,
    )
    return twt.as_array3d(image, wp.float32)

rasterize_discrete_attribute

rasterize_discrete_attribute(
    uv: array[vec2],
    faces: array[int32],
    attribute: array[int32],
    resolution: int,
) -> twt.Array2dInt32

Rasterize a per-vertex discrete label into a square UV-space class image.

At each covered pixel the label with the greatest barycentric weight is emitted (weights of vertices sharing a label sum; ties resolve to the lowest label value). This is equivalent to one-hot encoding the labels, interpolating, and taking the per-pixel argmax. Pixels not covered by any triangle are set to -1.

Parameters:

Name Type Description Default
uv array[vec2]

(n_vertices,) per-vertex UV coordinates as wp.vec2 in [0, 1]. Non-finite rows are ignored.

required
faces array[int32]

Flat length-3 * n_faces wp.int32 triangle index buffer.

required
attribute array[int32]

(n_vertices,) int32 per-vertex labels, all >= 0.

required
resolution int

Output image size in pixels (square).

required

Returns:

Type Description
Array2dInt32

(resolution, resolution) int32 class image with values in [-1, n_classes - 1] (-1 marks uncovered pixels), on uv.device.

Raises:

Type Description
ValueError

If any entry of attribute is negative, if resolution is not positive, if uv and attribute disagree on their row count, or if any finite UV lies outside [0, 1].

RuntimeError

If uv, faces and attribute are not all on one device.

See Also

remap_discrete_attribute_from_uv rasterize_attribute

Source code in triwarp/texture.py
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
def rasterize_discrete_attribute(
    uv: wp.array[wp.vec2], faces: wp.array[wp.int32], attribute: wp.array[wp.int32], resolution: int
) -> twt.Array2dInt32:
    """
    Rasterize a per-vertex discrete label into a square UV-space class image.

    At each covered pixel the label with the greatest barycentric weight is emitted (weights of
    vertices sharing a label sum; ties resolve to the lowest label value). This is equivalent to
    one-hot encoding the labels, interpolating, and taking the per-pixel ``argmax``. Pixels not
    covered by any triangle are set to ``-1``.

    Parameters
    ----------
    uv
        ``(n_vertices,)`` per-vertex UV coordinates as ``wp.vec2`` in ``[0, 1]``. Non-finite
        rows are ignored.
    faces
        Flat length-``3 * n_faces`` ``wp.int32`` triangle index buffer.
    attribute
        ``(n_vertices,)`` ``int32`` per-vertex labels, all ``>= 0``.
    resolution
        Output image size in pixels (square).

    Returns
    -------
    Array2dInt32
        ``(resolution, resolution)`` ``int32`` class image with values in ``[-1, n_classes - 1]``
        (``-1`` marks uncovered pixels), on ``uv.device``.

    Raises
    ------
    ValueError
        If any entry of ``attribute`` is negative, if ``resolution`` is not positive, if ``uv``
        and ``attribute`` disagree on their row count, or if any finite UV lies outside
        ``[0, 1]``.
    RuntimeError
        If ``uv``, ``faces`` and ``attribute`` are not all on one device.

    See Also
    --------
    [`remap_discrete_attribute_from_uv`][triwarp.texture.remap_discrete_attribute_from_uv]
    [`rasterize_attribute`][triwarp.texture.rasterize_attribute]
    """
    require_same_device(uv=uv, faces=faces, attribute=attribute)
    twt.ensure_ndim(attribute, 1, dtype=wp.int32)
    n_vertices = int(attribute.shape[0])
    _check_rasterize_inputs(uv, n_vertices, resolution)
    if n_vertices > 0 and tw.reduce.min(cast(twt.Array1dInt32, attribute)) < 0:
        raise ValueError("Attribute values must be greater than or equal to 0")

    device = uv.device
    labels_image = wp.full((resolution, resolution), -1, dtype=wp.int32, device=device)
    n_faces = int(faces.shape[0]) // 3
    if n_faces == 0:
        return twt.as_array2d(labels_image, wp.int32)

    owner = _rasterize_owner(uv, faces, resolution)
    wp.launch(
        kernel_texture.rasterize_labels,
        dim=n_faces,
        inputs=[uv, faces, attribute, owner, labels_image],
        device=device,
    )
    return twt.as_array2d(labels_image, wp.int32)

remap_attribute_from_uv

remap_attribute_from_uv(
    uv: array[vec2],
    image: Array2dFloat32 | Array3dFloat32,
    *,
    order: Literal[0, 1] = 1,
) -> twt.Array2dFloat32

Sample a UV-space texture back to per-vertex values.

Inverse of rasterize_attribute. Sampling uses the same pixel-center convention as rasterize_attribute (col = u * W - 0.5, row = (1 - v) * H - 0.5) and clamps to the nearest edge pixel, so vertices on the coverage boundary read the boundary value instead of blending toward 0. Vertices whose UV is non-finite are not sampled; their output row is NaN on every channel.

Parameters:

Name Type Description Default
uv array[vec2]

(n_vertices,) per-vertex UV coordinates as wp.vec2 in [0, 1]. Non-finite rows yield NaN output rows.

required
image Array2dFloat32 | Array3dFloat32

UV-space texture to sample, (H, W) or (H, W, C) float32.

required
order Literal[0, 1]

Interpolation order: 1 for bilinear (default), 0 for nearest-neighbor.

1

Returns:

Type Description
Array2dFloat32

(n_vertices, C) float32 per-vertex values on uv.device (C == 1 for a 2D input image). Non-finite-UV rows are NaN.

Raises:

Type Description
TypeError

If image is neither rank 2 nor rank 3.

ValueError

If order is not 0 or 1, or if any finite UV lies outside [0, 1].

RuntimeError

If uv and image are not all on one device.

See Also

rasterize_attribute

Source code in triwarp/texture.py
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
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
def remap_attribute_from_uv(
    uv: wp.array[wp.vec2],
    image: twt.Array2dFloat32 | twt.Array3dFloat32,
    *,
    order: Literal[0, 1] = 1,
) -> twt.Array2dFloat32:
    """
    Sample a UV-space texture back to per-vertex values.

    Inverse of [`rasterize_attribute`][triwarp.texture.rasterize_attribute].
    Sampling uses the same pixel-center convention as
    [`rasterize_attribute`][triwarp.texture.rasterize_attribute] (``col = u * W - 0.5``,
    ``row = (1 - v) * H - 0.5``) and clamps to the nearest edge pixel, so vertices on the
    coverage boundary read the boundary value instead of blending toward ``0``. Vertices whose
    UV is non-finite are not sampled; their output row is ``NaN`` on every channel.

    Parameters
    ----------
    uv
        ``(n_vertices,)`` per-vertex UV coordinates as ``wp.vec2`` in ``[0, 1]``. Non-finite
        rows yield ``NaN`` output rows.
    image
        UV-space texture to sample, ``(H, W)`` or ``(H, W, C)`` ``float32``.
    order
        Interpolation order: ``1`` for bilinear (default), ``0`` for nearest-neighbor.

    Returns
    -------
    Array2dFloat32
        ``(n_vertices, C)`` ``float32`` per-vertex values on ``uv.device`` (``C == 1`` for a
        2D input image). Non-finite-UV rows are ``NaN``.

    Raises
    ------
    TypeError
        If ``image`` is neither rank 2 nor rank 3.
    ValueError
        If ``order`` is not ``0`` or ``1``, or if any finite UV lies outside ``[0, 1]``.
    RuntimeError
        If ``uv`` and ``image`` are not all on one device.

    See Also
    --------
    [`rasterize_attribute`][triwarp.texture.rasterize_attribute]
    """
    require_same_device(uv=uv, image=image)
    if order not in (0, 1):
        raise ValueError(f"order must be 0 (nearest) or 1 (bilinear), got {order!r}")
    if int(image.ndim) == 2:
        height, width = int(image.shape[0]), int(image.shape[1])
        n_channels = 1
        image3d = image.reshape((height, width, 1))
    elif int(image.ndim) == 3:
        n_channels = int(image.shape[2])
        image3d = image
    else:
        raise TypeError(f"image must be (H, W) or (H, W, C), got ndim={image.ndim}")
    twt.ensure_ndim(image3d, 3, dtype=wp.float32)
    _check_uv_in_range(uv)

    device = uv.device
    n_vertices = int(uv.shape[0])
    out_values = twt.empty_2d((n_vertices, n_channels), wp.float32, device=device)
    if n_vertices > 0:
        # ``order`` is rejected above rather than defaulted here: an ``if order == 1 else``
        # sampled nearest-neighbour for every off-menu value, so a caller whose interpolation
        # order arrived through a variable got a plausible image and no error.
        mode = kernel_texture.SAMPLE_BILINEAR if order == 1 else kernel_texture.SAMPLE_NEAREST
        wp.launch(
            kernel_texture.sample_texture,
            dim=n_vertices,
            inputs=[uv, image3d, n_channels, mode, out_values],
            device=device,
        )
    return twt.as_array2d(out_values, wp.float32)

remap_discrete_attribute_from_uv

remap_discrete_attribute_from_uv(
    uv: array[vec2], class_image: Array2dInt32
) -> twt.Array1dInt32

Sample a UV-space class image back to per-vertex labels.

Inverse of rasterize_discrete_attribute. Nearest-neighbor sampling is used so labels are never blended, including the -1 uncovered sentinel. Vertices whose UV is non-finite cannot be sampled and map to the same -1 sentinel.

Parameters:

Name Type Description Default
uv array[vec2]

(n_vertices,) per-vertex UV coordinates as wp.vec2 in [0, 1]. Non-finite rows yield -1.

required
class_image Array2dInt32

(H, W) int32 class image (values e.g. in [-1, n_classes - 1]).

required

Returns:

Type Description
Array1dInt32

(n_vertices,) int32 per-vertex labels on uv.device. Non-finite-UV rows are -1.

Raises:

Type Description
ValueError

If any finite UV lies outside [0, 1].

RuntimeError

If uv and class_image are not all on one device.

See Also

rasterize_discrete_attribute remap_attribute_from_uv

Source code in triwarp/texture.py
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
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
def remap_discrete_attribute_from_uv(
    uv: wp.array[wp.vec2], class_image: twt.Array2dInt32
) -> twt.Array1dInt32:
    """
    Sample a UV-space class image back to per-vertex labels.

    Inverse of [`rasterize_discrete_attribute`][triwarp.texture.rasterize_discrete_attribute].
    Nearest-neighbor sampling is used so labels are never blended, including the ``-1`` uncovered
    sentinel. Vertices whose UV is non-finite cannot be sampled and map to the same ``-1``
    sentinel.

    Parameters
    ----------
    uv
        ``(n_vertices,)`` per-vertex UV coordinates as ``wp.vec2`` in ``[0, 1]``. Non-finite
        rows yield ``-1``.
    class_image
        ``(H, W)`` ``int32`` class image (values e.g. in ``[-1, n_classes - 1]``).

    Returns
    -------
    Array1dInt32
        ``(n_vertices,)`` ``int32`` per-vertex labels on ``uv.device``. Non-finite-UV rows
        are ``-1``.

    Raises
    ------
    ValueError
        If any finite UV lies outside ``[0, 1]``.
    RuntimeError
        If ``uv`` and ``class_image`` are not all on one device.

    See Also
    --------
    [`rasterize_discrete_attribute`][triwarp.texture.rasterize_discrete_attribute]
    [`remap_attribute_from_uv`][triwarp.texture.remap_attribute_from_uv]
    """
    require_same_device(uv=uv, class_image=class_image)
    twt.ensure_ndim(class_image, 2, dtype=wp.int32)
    device = uv.device
    float_image = twt.as_array2d(tw.array.astype(class_image, wp.float32), wp.float32)

    sampled = remap_attribute_from_uv(uv, float_image, order=0)

    n_vertices = int(uv.shape[0])
    out_labels = wp.empty(n_vertices, dtype=wp.int32, device=device)
    # ``sampled`` is ``(n_vertices, 1)`` and contiguous, so ``flatten()`` is a reshape view.
    wp.map(kernel_texture.round_labels, sampled.flatten(), out=out_labels)
    return cast(twt.Array1dInt32, out_labels)