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. The HUD states which path was taken. 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 uv run python examples/features/3d/compressed_texture.py –test
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. The HUD states which path was taken. The fixture carries a full mip chain
8so mip sampling is exercised too.
9
10The committed fixture ``assets/uastc_quadrants.ktx2`` is a 64x64 UASTC LDR 4x4
11texture (R/G/B/Y quadrants, 7 mip levels, sRGB). The canonical regeneration
12recipe with the Khronos tools is::
13
14 toktx --uastc --genmipmap --t2 out.ktx2 in.png
15
16The vendored basis is transcoder-only (no UASTC encoder), so this repo ships a
17pure-Python generator instead: ``assets/_generate_uastc_fixture.py``.
18
19Usage:
20 uv run python examples/features/3d/compressed_texture.py
21 uv run python examples/features/3d/compressed_texture.py --test
22
23# /// simvx
24# tags = ["graphics", "textures", "compressed"]
25# screenshot_frame = 30
26# ///
27"""
28
29import sys
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 if _FIXTURE.exists():
66 albedo: object = str(_FIXTURE)
67 status = f"UASTC .ktx2 -> device-chosen block target ({_FIXTURE.name})"
68 else:
69 albedo = _fallback_texture()
70 status = "fixture missing: solid-colour fallback (run assets/_generate_uastc_fixture.py)"
71
72 mat = Material(colour=(1, 1, 1, 1), albedo_map=albedo)
73 self._cube = MeshInstance3D(mesh=Mesh.cube(), material=mat, position=(0, 0, 0))
74 self.add_child(self._cube)
75
76 self.add_child(Text2D(text="Compressed texture (UASTC .ktx2)", x=12, y=12, font_scale=1.4))
77 self.add_child(Text2D(text=status, x=12, y=40, font_scale=1.0))
78 self.add_child(Text2D(text="ESC: quit", x=12, y=64, font_scale=1.0))
79
80 def on_process(self, dt):
81 if Input.is_action_just_pressed("quit"):
82 self.app.quit()
83 return
84 self._cube.rotate_z(0.6 * dt)
85 self._cube.rotate_x(0.3 * dt)
86
87
88def _run_test() -> int:
89 """Headless: render, save a PNG, and assert the textured cube is non-blank
90 and shows one of the fixture's quadrant colours (not the magenta fallback).
91 """
92 from simvx.graphics.testing import assert_not_blank, save_png
93
94 app = App(title="Compressed Texture Test", width=640, height=480, visible=False, mode="3d")
95 frames = app.run_headless(CompressedTextureScene(name="CompressedTextureScene"),
96 frames=40, capture_frames=[30])
97 frame = frames[-1]
98 save_png("/tmp/compressed_texture_test.png", frame)
99 assert_not_blank(frame)
100 if not _FIXTURE.exists():
101 print("WARNING: fixture missing; skipped the known-colour check")
102 return 0
103 # The cube fills the centre; assert a strong, saturated quadrant colour is
104 # present somewhere (R/G/B/Y), i.e. not the magenta fallback and not blank.
105 h, w = frame.shape[:2]
106 patch = frame[h // 3:2 * h // 3, w // 3:2 * w // 3, :3].reshape(-1, 3).astype(int)
107 # magenta fallback is high-R AND high-B together; a real quadrant is not.
108 magenta = ((patch[:, 0] > 150) & (patch[:, 2] > 150) & (patch[:, 1] < 90)).mean()
109 saturated = (patch.max(axis=1) - patch.min(axis=1) > 80).mean()
110 print(f"saturated-pixel ratio={saturated:.2f} magenta-ratio={magenta:.2f}")
111 assert saturated > 0.2, "expected saturated quadrant colours from the fixture"
112 assert magenta < 0.5, "centre looks like the magenta fallback, not the fixture"
113 print("Compressed texture render: PASSED")
114 return 0
115
116
117if __name__ == "__main__":
118 if "--test" in sys.argv:
119 sys.exit(_run_test())
120 app = App(title="Compressed Texture", width=1280, height=720, mode="3d")
121 app.run(CompressedTextureScene())