feat(paint-slots): KTX2 finish library — fabric/leather/concrete/metal (phase 4)

Wire KTX2 into the catalog finish path and add 15 textured finishes across four
new families, all as GPU-compressed KTX2 (512px) + a webp picker thumbnail.

- Shared `ktx2-loader.ts`: one KTX2Loader (Basis transcoder) used by both the
  GLB loader and catalog textures; `ensureKtx2Support(renderer)` runs once at
  viewer init (GPUDeviceWatcher) so `.ktx2` finishes load even with no GLB in
  the scene. `use-gltf-ktx2` now reuses the shared instance.
- `materials.ts`: texture loaders pick the KTX2 loader for `.ktx2` urls, the
  image loader otherwise (all three load paths).
- `material-library.ts`: 15 entries (fabric ×6, leather ×2, concrete ×4,
  metal ×3). Neutral albedo for tinting, `flipY: false` (compressed textures
  can't flip), `repeat` per real-world tile size, metals `metalness: 1`.
  Normals encoded UASTC, data maps ETC1S/linear.

Assets generated from raw sources via the new community
`scripts/build-material-textures.ts`. 1024/256 tiers + raws kept out of git.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-06-16 14:37:23 -04:00
co-authored by Claude Opus 4.8
parent 0e8a8c3d14
commit 042f855586
74 changed files with 629 additions and 28 deletions
@@ -13,6 +13,7 @@ import * as THREE from 'three/webgpu'
import { hasDrawableGeometry } from '../../lib/drawable-geometry'
import { PERF_OVERLAY_ENABLED, pushGpuSample } from '../../lib/gpu-perf'
import { applyIsolation, clearIsolation } from '../../lib/isolation'
import { ensureKtx2Support } from '../../lib/ktx2-loader'
import type { ColorPreset, RenderShading } from '../../lib/materials'
import { getSceneTheme } from '../../lib/scene-themes'
import useViewer, { type RenderContext } from '../../store/use-viewer'
@@ -144,6 +145,11 @@ function GPUDeviceWatcher() {
const gl = useThree((s) => s.gl)
useEffect(() => {
// Detect KTX2 transcode support as soon as the renderer exists, so catalog
// `.ktx2` finish textures load even in scenes with no GLB items (whose
// loader would otherwise be the only thing to call this).
ensureKtx2Support(gl)
const backend = (gl as any).backend
const device = backend?.device as WebGPUDeviceLike | undefined