Compressed texture¶
load a UASTC .ktx2 onto a 3D mesh via Material.albedo_map.
▶ Run in browserTags: 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())