Compressed texture

load a UASTC .ktx2 onto a 3D mesh via Material.albedo_map.

▶ Run in browser

Tags: 3d graphics textures compressed

Loads a block-compressed UASTC KTX2 texture and applies it to a spinning cube. On the desktop the engine transcodes UASTC to the GPU’s preferred block format (BC7 / ASTC-4x4 / ETC2, chosen by a device probe) via the native basis_universal transcoder; if no block family is usable it CPU-decodes mip 0 to RGBA8. Either way Material.albedo_map takes the same .ktx2 path and the scene stays unchanged, so the HUD reports the source encoding rather than the per-device target. The fixture carries a full mip chain so mip sampling is exercised too.

The committed fixture assets/uastc_quadrants.ktx2 is a 64x64 UASTC LDR 4x4 texture (R/G/B/Y quadrants, 7 mip levels, sRGB). The canonical regeneration recipe with the Khronos tools is::

toktx --uastc --genmipmap --t2 out.ktx2 in.png

The vendored basis is transcoder-only (no UASTC encoder), so this repo ships a pure-Python generator instead: assets/_generate_uastc_fixture.py.

Usage: uv run python examples/features/3d/compressed_texture.py

Source

 1"""Compressed texture: load a UASTC .ktx2 onto a 3D mesh via Material.albedo_map.
 2
 3Loads a block-compressed UASTC KTX2 texture and applies it to a spinning cube.
 4On the desktop the engine transcodes UASTC to the GPU's preferred block format
 5(BC7 / ASTC-4x4 / ETC2, chosen by a device probe) via the native
 6basis_universal transcoder; if no block family is usable it CPU-decodes mip 0 to
 7RGBA8. Either way ``Material.albedo_map`` takes the same ``.ktx2`` path and the
 8scene stays unchanged, so the HUD reports the source encoding rather than the
 9per-device target. The fixture carries a full mip chain so mip sampling is
10exercised too.
11
12The committed fixture ``assets/uastc_quadrants.ktx2`` is a 64x64 UASTC LDR 4x4
13texture (R/G/B/Y quadrants, 7 mip levels, sRGB). The canonical regeneration
14recipe with the Khronos tools is::
15
16    toktx --uastc --genmipmap --t2 out.ktx2 in.png
17
18The vendored basis is transcoder-only (no UASTC encoder), so this repo ships a
19pure-Python generator instead: ``assets/_generate_uastc_fixture.py``.
20
21Usage:
22    uv run python examples/features/3d/compressed_texture.py
23
24# /// simvx
25# tags = ["graphics", "textures", "compressed"]
26# screenshot_frame = 30
27# ///
28"""
29
30from pathlib import Path
31
32import numpy as np
33
34from simvx.core import (
35    Camera3D,
36    Input,
37    InputMap,
38    Key,
39    Material,
40    Mesh,
41    MeshInstance3D,
42    Node,
43    Text2D,
44)
45from simvx.graphics import App
46
47_FIXTURE = (Path(__file__).parent / "assets" / "uastc_quadrants.ktx2").resolve()
48
49
50def _fallback_texture() -> np.ndarray:
51    """Solid magenta RGBA so the cube is never blank if the fixture is missing."""
52    img = np.zeros((64, 64, 4), dtype=np.uint8)
53    img[:, :] = (220, 40, 220, 255)
54    return img
55
56
57class CompressedTextureScene(Node):
58    def on_ready(self):
59        InputMap.add_action("quit", [Key.ESCAPE])
60
61        self.add_child(Camera3D(position=(0, -4, 1.5), look_at=(0, 0, 0), up=(0, 0, 1)))
62
63        # Material.albedo_map accepts a .ktx2 path transparently: the texture
64        # manager sniffs the suffix/magic and routes to the compressed path.
65        albedo: str | np.ndarray
66        if _FIXTURE.exists():
67            albedo = str(_FIXTURE)
68            status = f"UASTC .ktx2 -> device-chosen block target  ({_FIXTURE.name})"
69        else:
70            albedo = _fallback_texture()
71            status = "fixture missing: solid-colour fallback (run assets/_generate_uastc_fixture.py)"
72
73        mat = Material(colour=(1, 1, 1, 1), albedo_map=albedo)
74        self._cube = MeshInstance3D(mesh=Mesh.cube(), material=mat, position=(0, 0, 0))
75        self.add_child(self._cube)
76
77        self.add_child(Text2D(text="Compressed texture (UASTC .ktx2)", position=(12, 12), font_scale=1.4))
78        self.add_child(Text2D(text=status, position=(12, 40), font_scale=1.0))
79        self.add_child(Text2D(text="ESC: quit", position=(12, 64), font_scale=1.0))
80
81    def on_update(self, dt):
82        if Input.is_action_just_pressed("quit"):
83            self.app.quit()
84            return
85        self._cube.rotate_z(0.6 * dt)
86        self._cube.rotate_x(0.3 * dt)
87
88
89if __name__ == "__main__":
90    app = App(title="Compressed Texture", width=1280, height=720)
91    app.run(CompressedTextureScene())