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. 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())