gputex 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -9
- package/dist/index.d.ts +34 -1
- package/dist/index.js +343 -15
- package/dist/three.d.ts +36 -3
- package/dist/three.js +397 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# GPUtex | On-the-fly GPU texture encoding
|
|
2
2
|
|
|
3
|
-
Runtime GPU texture compression via WebGPU compute shaders, with a WebGL2 fragment-shader fallback. Feed it a PNG/JPG/WebP/AVIF and get back a GPU-compressed texture (BC7, BC5, ASTC 4x4, or BC1) ready for Three.js or React Three Fiber.
|
|
3
|
+
Runtime GPU texture compression via WebGPU compute shaders, with a WebGL2 fragment-shader fallback. Feed it a PNG/JPG/WebP/AVIF — or an SVG, rasterised on the fly — and get back a GPU-compressed texture (BC7, BC5, ASTC 4x4, or BC1) ready for Three.js or React Three Fiber.
|
|
4
4
|
|
|
5
5
|
⚠️ 100% vibe-coded. The code is completely unreviewed and under-tested. Do not use for anything important.
|
|
6
6
|
|
|
@@ -33,10 +33,17 @@ bun add gputex
|
|
|
33
33
|
| **BC7** | 16 (8 bpp) | Color / RGBA on desktop (`texture-compression-bc`) |
|
|
34
34
|
| **BC5** | 16 (8 bpp) | Normal maps — RG only (`texture-compression-bc`) |
|
|
35
35
|
| **ASTC 4x4** | 16 (8 bpp) | Color / RGBA on mobile / iOS (`texture-compression-astc`) |
|
|
36
|
-
| **BC1** | 8 (4 bpp) |
|
|
36
|
+
| **BC1** | 8 (4 bpp) | Opaque color at half BC7's size (opt-in) |
|
|
37
37
|
|
|
38
38
|
Format selection is automatic: BC7/BC5 on desktop, ASTC on mobile, uncompressed RGBA8 fallback otherwise.
|
|
39
39
|
|
|
40
|
+
BC1 is never picked by default — it's half the memory of BC7 but visibly lower
|
|
41
|
+
quality, a trade-off only the application can make. Opt in per-texture with
|
|
42
|
+
`preferredFormat: 'bc1'`: on BC-capable devices the texture encodes as BC1;
|
|
43
|
+
everywhere else (e.g. ASTC-only mobile) selection proceeds as normal. The
|
|
44
|
+
preference is only honoured with `hint: 'color'`, since BC1 can't carry real
|
|
45
|
+
alpha or a normal map.
|
|
46
|
+
|
|
40
47
|
## WebGL fallback
|
|
41
48
|
|
|
42
49
|
WebGPU is the primary path. When it's unavailable (older Safari, Firefox without WebGPU, locked-down environments) `compressTexture()` automatically falls back to a **WebGL2** path that runs the same family of block encoders as fragment shaders — each 4×4 block is computed in one fragment, written to an `RGBA32UI` render target, and read back. The WebGPU fast paths have since been rewritten for speed (projection assignment, f16), so the two backends are no longer byte-identical, but they implement the same algorithms at the same quality level and the resulting `CompressedTexture` looks the same under either renderer.
|
|
@@ -86,6 +93,32 @@ material.map = texture
|
|
|
86
93
|
(byte-identical on >96% of blocks; the rest are equal-error FP tie-breaks,
|
|
87
94
|
enforced by the GPU test suite).
|
|
88
95
|
|
|
96
|
+
#### SVG sources
|
|
97
|
+
|
|
98
|
+
SVGs work anywhere a raster image does — as a URL, a Blob/File, an inline
|
|
99
|
+
markup string (detected by a leading `<`), or an `<img>` element. The vector
|
|
100
|
+
is rasterised before encoding, at the SVG's intrinsic size by default
|
|
101
|
+
(absolute `width`/`height` attributes, else the `viewBox` dimensions). Use
|
|
102
|
+
`svgSize` to pick the raster size — the browser renders the vector directly
|
|
103
|
+
at that size, so upscaling stays crisp:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
// Longest side 1024, aspect ratio preserved:
|
|
107
|
+
const { texture } = await compressTexture('/logo.svg', { svgSize: 1024 })
|
|
108
|
+
|
|
109
|
+
// Exact size (aspect mismatches follow the SVG's preserveAspectRatio rules):
|
|
110
|
+
await compressTexture('/icon.svg', { svgSize: { width: 512, height: 512 } })
|
|
111
|
+
|
|
112
|
+
// Inline markup:
|
|
113
|
+
await compressTexture('<svg viewBox="0 0 32 32">…</svg>', { svgSize: 256 })
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
An SVG with no `width`/`height` **and** no `viewBox` has no intrinsic size;
|
|
117
|
+
`svgSize` is required for those. Rasterisation needs a DOM `Image`, so SVG
|
|
118
|
+
sources are main-thread only. Non-Three.js users get the same rasteriser as
|
|
119
|
+
a standalone helper: `rasterizeSvg(source, { size })` from the core `gputex`
|
|
120
|
+
entry returns an `ImageBitmap` ready for `encodeToBytes()`.
|
|
121
|
+
|
|
89
122
|
### `GputexLoader` — Three.js Loader
|
|
90
123
|
|
|
91
124
|
```ts
|
|
@@ -204,13 +237,15 @@ const tex = buildCompressedTexture([bytes], TextureFormat.BC7_SRGB)
|
|
|
204
237
|
|
|
205
238
|
### `compressTexture` options
|
|
206
239
|
|
|
207
|
-
| Option
|
|
208
|
-
|
|
|
209
|
-
| `hint`
|
|
210
|
-
| `
|
|
211
|
-
| `
|
|
212
|
-
| `
|
|
213
|
-
| `
|
|
240
|
+
| Option | Type | Default | Description |
|
|
241
|
+
| ----------------- | ----------------------------- | --------- | ------------------------------------------------------------------------------------------------ |
|
|
242
|
+
| `hint` | `TextureHint` | `'color'` | `'color'`, `'colorWithAlpha'`, or `'normal'` |
|
|
243
|
+
| `preferredFormat` | `'bc1'` | — | Prefer BC1 (half of BC7's size) when supported; normal selection otherwise. `hint: 'color'` only |
|
|
244
|
+
| `colorSpace` | `'srgb' \| 'linear'` | `'srgb'` | Use the sRGB or linear variant of the chosen format |
|
|
245
|
+
| `svgSize` | `number \| { width, height }` | intrinsic | Raster size for SVG sources: longest side (aspect preserved) or exact size |
|
|
246
|
+
| `flipY` | `boolean` | `true` | Flip vertically (matches Three.js convention) |
|
|
247
|
+
| `mipmaps` | `boolean` | `false` | Generate full mip chain down to 1x1 |
|
|
248
|
+
| `device` | `GPUDevice` | — | Reuse an existing WebGPU device instead of creating one |
|
|
214
249
|
|
|
215
250
|
## Benchmarks
|
|
216
251
|
|
package/dist/index.d.ts
CHANGED
|
@@ -377,9 +377,19 @@ declare function detectWebGLCapabilities(gl: ExtensionProvider): WebGLCapabiliti
|
|
|
377
377
|
* • 'normal' — tangent-space normal map (R=x, G=y, z reconstructed).
|
|
378
378
|
*/
|
|
379
379
|
type TextureHint = 'color' | 'colorWithAlpha' | 'normal';
|
|
380
|
+
/**
|
|
381
|
+
* Optional format preference — a wish, not a demand. Applied when the
|
|
382
|
+
* device supports the format and the hint is compatible; otherwise
|
|
383
|
+
* selection proceeds normally (BC7 → ASTC → null). Currently only
|
|
384
|
+
* 'bc1': half the memory of BC7 (0.5 vs 1 byte/pixel) for opaque
|
|
385
|
+
* colour, at visibly lower quality on smooth content.
|
|
386
|
+
*/
|
|
387
|
+
type PreferredFormat = 'bc1';
|
|
380
388
|
interface SelectFormatOptions {
|
|
381
389
|
/** Pick the sRGB variant when the format has one. Default 'srgb'. */
|
|
382
390
|
colorSpace?: 'srgb' | 'linear';
|
|
391
|
+
/** Prefer a specific format when supported. See `PreferredFormat`. */
|
|
392
|
+
preferredFormat?: PreferredFormat;
|
|
383
393
|
}
|
|
384
394
|
interface FormatSelection {
|
|
385
395
|
/** null = no compressed path on this adapter; caller should fall back. */
|
|
@@ -430,4 +440,27 @@ declare function generateMipChain(level0: MipLevel): MipLevel[];
|
|
|
430
440
|
*/
|
|
431
441
|
declare function padToBlockMultiple(level: MipLevel): MipLevel;
|
|
432
442
|
|
|
433
|
-
|
|
443
|
+
/**
|
|
444
|
+
* Target raster size for an SVG source. A number scales the SVG so its
|
|
445
|
+
* longest side matches (aspect ratio preserved); an object rasterises at
|
|
446
|
+
* exactly that size. When omitted, the SVG's intrinsic size is used
|
|
447
|
+
* (absolute width/height attributes, else the viewBox dimensions).
|
|
448
|
+
*/
|
|
449
|
+
type SvgRasterSize = number | {
|
|
450
|
+
width: number;
|
|
451
|
+
height: number;
|
|
452
|
+
};
|
|
453
|
+
interface RasterizeSvgOptions {
|
|
454
|
+
/** Target raster size. Default: the SVG's intrinsic size. */
|
|
455
|
+
size?: SvgRasterSize;
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Rasterise an SVG (markup string or Blob/File) to an `ImageBitmap`.
|
|
459
|
+
*
|
|
460
|
+
* Used automatically by `compressTexture()` for SVG sources; exported for
|
|
461
|
+
* callers driving the core encoders directly — the returned bitmap is a
|
|
462
|
+
* valid `EncoderImageSource`. Main-thread only (needs `Image`).
|
|
463
|
+
*/
|
|
464
|
+
declare function rasterizeSvg(source: string | Blob, options?: RasterizeSvgOptions): Promise<ImageBitmap>;
|
|
465
|
+
|
|
466
|
+
export { ASTC4x4Encoder, ASTC4x4WebGLEncoder, BC1Encoder, BC1WebGLEncoder, BC5Encoder, BC5WebGLEncoder, BC7Encoder, BC7WebGLEncoder, type Capabilities, type EncodeBytesResult, type EncodeCallOptions, type EncodeQuality, Encoder, type EncoderConstructor, type EncoderImageSource, type EncoderOptions, type ExtensionProvider, type FeatureProvider, type FormatSelection, type FormatVariant, type MipLevel, type PreferredFormat, type RasterizeSvgOptions, type RawPixelSource, type SelectFormatOptions, type SvgRasterSize, TextureFormat, type TextureHint, WebGLBlockEncoder, type WebGLCapabilities, type WebGLEncodeBytesResult, type WebGLEncoderConstructor, type WebGLEncoderImageSource, type WebGLEncoderOptions, type WebGLFormatSelection, WebGPUFeature, createWebGLContext, detectCapabilities, detectWebGLCapabilities, generateMipChain, getSharedWebGLContext, isWebGLAvailable, padToBlockMultiple, rasterizeSvg, selectFormat, selectWebGLFormat };
|
package/dist/index.js
CHANGED
|
@@ -631,7 +631,171 @@ var BC5Encoder = class extends Encoder {
|
|
|
631
631
|
var bc7_default = "// BC7 (BPTC) mode 6 compute shader encoder.\n//\n// One invocation per 4\xD74 block. Emits 16 bytes = 4 u32s into the storage\n// buffer at `dst[block_index * 4 .. + 3]`.\n//\n// QUALITY LEVELS (pipeline-overridable constant `QUALITY_HIGH`)\n// fast (0, default): O(N) bounding-box seed \u2192 one fused pass that projects\n// each pixel onto the endpoint line (the 16 palette entries are colinear,\n// so the nearest index is the rounded projection \u2014 no palette build, no\n// 16-entry search) while accumulating the least-squares refit sums, then\n// a reprojection against the quantised refit endpoints for the final\n// indices, packed on the fly into two nibble words.\n// high (1): farthest-pair seed, exhaustive p-bit search over all four\n// (p0,p1) \u2208 {0,1}\xB2 combos, full 16-entry nearest search, one LSQ refit \u2014\n// matches bc7_ref.ts up to FP tie-breaks.\n//\n// The fast/high branch is selected at pipeline-compile time, so the driver\n// eliminates the unused code entirely.\n//\n// MODE 6 LAYOUT (LSB-first, bit 0 = byte 0's bit 0)\n// bits 0..6 mode field (0b0000001 \u2014 only bit 6 is 1)\n// bits 7..13 R0 (7-bit) bits 14..20 R1 bits 21..27 G0 bits 28..34 G1\n// bits 35..41 B0 bits 42..48 B1 bits 49..55 A0 bits 56..62 A1\n// bit 63 P0 bit 64 P1\n// bits 65..67 pixel 0 index (3 bits; anchor, MSB implicit 0)\n// bits 68..71 pixel 1 index (4 bits) ... bits 124..127 pixel 15 index\n//\n// Effective 8-bit endpoint channel = (7_bit_value << 1) | p_bit.\n// Palette[i] = ((64 \u2212 W4[i]) \xD7 e0_8 + W4[i] \xD7 e1_8 + 32) >> 6, integer.\n//\n// The block is assembled with straight-line constant shifts (see the layout\n// summary in bc7_fast_f16.wgsl) \u2014 a generic write_bits() helper's dynamic\n// word indexing keeps the output array out of registers.\n\n// 0 = fast (default), 1 = exhaustive/high-quality. Set via pipeline constants.\noverride QUALITY_HIGH: u32 = 0u;\n\nstruct Params {\n blocks_x: u32,\n blocks_y: u32,\n width: u32,\n height: u32,\n};\n\n@group(0) @binding(0) var src_tex: texture_2d<f32>;\n@group(0) @binding(1) var<storage, read_write> dst: array<u32>;\n@group(0) @binding(2) var<uniform> params: Params;\n\n// Mode 6 interpolation weights (\xD7 1/64), fixed by the spec (`W4` in bc7_ref.ts).\nfn w4(i: u32) -> u32 {\n switch i {\n case 0u: { return 0u; }\n case 1u: { return 4u; }\n case 2u: { return 9u; }\n case 3u: { return 13u; }\n case 4u: { return 17u; }\n case 5u: { return 21u; }\n case 6u: { return 26u; }\n case 7u: { return 30u; }\n case 8u: { return 34u; }\n case 9u: { return 38u; }\n case 10u: { return 43u; }\n case 11u: { return 47u; }\n case 12u: { return 51u; }\n case 13u: { return 55u; }\n case 14u: { return 60u; }\n default: { return 64u; } // case 15u\n }\n}\n\nfn interp4(e0: vec4<i32>, e1: vec4<i32>, w: i32) -> vec4<i32> {\n return ((64 - w) * e0 + w * e1 + vec4<i32>(32)) >> vec4<u32>(6u);\n}\n\nfn to8(v: vec4<f32>) -> vec4<i32> {\n return vec4<i32>(clamp(floor(v * 255.0 + 0.5), vec4<f32>(0.0), vec4<f32>(255.0)));\n}\n\nfn dist2(a: vec4<i32>, b: vec4<i32>) -> i32 {\n let d = a - b;\n let e = d * d;\n return e.x + e.y + e.z + e.w;\n}\n\n// Quantize an 8-bit ideal endpoint to (7-bit value, reconstructed 8-bit) under\n// a fixed p-bit, all four channels at once. q7 = round((ideal8 \u2212 p)/2); used by\n// both paths.\nstruct QuantPair { seven: vec4<i32>, eight: vec4<i32> };\nfn quantize_endpoint(ideal8: vec4<i32>, p: u32) -> QuantPair {\n let q = vec4<i32>(clamp(\n floor((vec4<f32>(ideal8) - f32(p)) / 2.0 + 0.5),\n vec4<f32>(0.0), vec4<f32>(127.0),\n ));\n let eff = (q << vec4<u32>(1u)) | vec4<i32>(i32(p));\n return QuantPair(q, eff);\n}\n\n// ============================ FAST PATH ================================ //\n\n// Endpoint with its chosen p-bit, picked by minimum quantisation error.\nstruct Ep { seven: vec4<i32>, eight: vec4<i32>, p: u32 };\nfn pick_ep(ideal: vec4<i32>) -> Ep {\n let a = quantize_endpoint(ideal, 0u);\n let b = quantize_endpoint(ideal, 1u);\n if (dist2(b.eight, ideal) < dist2(a.eight, ideal)) { return Ep(b.seven, b.eight, 1u); }\n return Ep(a.seven, a.eight, 0u);\n}\n\n// One pass over the block: project every pixel onto the e0\u2192e1 line and\n// accumulate the least-squares normal-equation sums; solve for the refit\n// endpoints (in 8-bit space). Indices are not produced here \u2014 the caller\n// reprojects against the quantised refit endpoints anyway.\nstruct Fit { e0: vec4<i32>, e1: vec4<i32>, valid: bool };\nfn proj_fit(pixels: ptr<function, array<vec4<i32>, 16>>, e0: vec4<i32>, e1: vec4<i32>) -> Fit {\n var out: Fit;\n out.valid = false;\n let dir = vec4<f32>(e1 - e0);\n let dd = dot(dir, dir);\n if (dd == 0.0) { return out; }\n let e0f = vec4<f32>(e0);\n let inv = 15.0 / dd;\n var sAA: f32 = 0.0; var sBB: f32 = 0.0; var sAB: f32 = 0.0;\n var sAV: vec4<f32> = vec4<f32>(0.0); var sBV: vec4<f32> = vec4<f32>(0.0);\n var s_min = 15.0; var s_max = 0.0;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let v = vec4<f32>((*pixels)[k]);\n let s = clamp(floor(dot(v - e0f, dir) * inv + 0.5), 0.0, 15.0);\n s_min = min(s_min, s); s_max = max(s_max, s);\n let b = s * (1.0 / 15.0); let a = 1.0 - b;\n sAA = sAA + a * a; sBB = sBB + b * b; sAB = sAB + a * b;\n sAV = sAV + a * v; sBV = sBV + b * v;\n }\n // Rank-1 guard: if every pixel projects to ONE level the system is\n // singular \u2014 det and the numerators are pure float rounding noise and the\n // solve returns garbage endpoints. With \u22652 levels det \u2265 15/225 \u2248 0.067.\n if (s_min == s_max) { return out; }\n let det = sAA * sBB - sAB * sAB;\n if (abs(det) < 1e-3) { return out; }\n out.e0 = vec4<i32>(clamp(round((sBB * sAV - sAB * sBV) / det), vec4<f32>(0.0), vec4<f32>(255.0)));\n out.e1 = vec4<i32>(clamp(round((sAA * sBV - sAB * sAV) / det), vec4<f32>(0.0), vec4<f32>(255.0)));\n out.valid = true;\n return out;\n}\n\n// ============================ HIGH PATH ================================ //\n\nstruct Pair { a: vec4<i32>, b: vec4<i32> };\nfn farthest_pair(pixels: ptr<function, array<vec4<i32>, 16>>) -> Pair {\n var best_d: i32 = 0;\n var pa = (*pixels)[0];\n var pb = (*pixels)[1];\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let xi = (*pixels)[i];\n for (var j: u32 = i + 1u; j < 16u; j = j + 1u) {\n let d = dist2(xi, (*pixels)[j]);\n if (d > best_d) { best_d = d; pa = xi; pb = (*pixels)[j]; }\n }\n }\n return Pair(pa, pb);\n}\n\nfn build_palette_6(e0: vec4<i32>, e1: vec4<i32>, pal: ptr<function, array<vec4<i32>, 16>>) {\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n (*pal)[i] = interp4(e0, e1, i32(w4(i)));\n }\n}\n\nfn assign_all(\n pixels: ptr<function, array<vec4<i32>, 16>>,\n pal: ptr<function, array<vec4<i32>, 16>>,\n out_idx: ptr<function, array<u32, 16>>,\n) -> i32 {\n var err: i32 = 0;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let px = (*pixels)[k];\n var best_i: u32 = 0u;\n var best_d: i32 = 2147483647;\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let d = dist2(px, (*pal)[i]);\n if (d < best_d) { best_d = d; best_i = i; }\n }\n (*out_idx)[k] = best_i;\n err = err + best_d;\n }\n return err;\n}\n\nstruct BestMode6 {\n e0_7: vec4<i32>, e1_7: vec4<i32>,\n p0: u32, p1: u32,\n indices: array<u32, 16>,\n err: i32,\n};\n\n// Exhaustive p-bit search (high path); commits to `*best` only on improvement.\nfn try_pbit_combos(\n pixels: ptr<function, array<vec4<i32>, 16>>,\n ideal0: vec4<i32>,\n ideal1: vec4<i32>,\n best: ptr<function, BestMode6>,\n) {\n var local_best = (*best).err;\n var pal: array<vec4<i32>, 16>;\n var tmp: array<u32, 16>;\n for (var p0: u32 = 0u; p0 < 2u; p0 = p0 + 1u) {\n let q0 = quantize_endpoint(ideal0, p0);\n for (var p1: u32 = 0u; p1 < 2u; p1 = p1 + 1u) {\n let q1 = quantize_endpoint(ideal1, p1);\n build_palette_6(q0.eight, q1.eight, &pal);\n let err = assign_all(pixels, &pal, &tmp);\n if (err < local_best) {\n local_best = err;\n (*best).e0_7 = q0.seven;\n (*best).e1_7 = q1.seven;\n (*best).p0 = p0;\n (*best).p1 = p1;\n (*best).indices = tmp;\n (*best).err = err;\n }\n }\n }\n}\n\n// Exact-weight LSQ refit (high path); matches bc7_ref.ts `refitEndpointsMode6`.\nstruct RefitResult { e0: vec4<i32>, e1: vec4<i32>, valid: bool };\nfn refit_endpoints(\n pixels: ptr<function, array<vec4<i32>, 16>>,\n indices: ptr<function, array<u32, 16>>,\n) -> RefitResult {\n var sAA: f32 = 0.0;\n var sBB: f32 = 0.0;\n var sAB: f32 = 0.0;\n var sAV: vec4<f32> = vec4<f32>(0.0);\n var sBV: vec4<f32> = vec4<f32>(0.0);\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let i = (*indices)[k];\n let a = f32(64u - w4(i)) / 64.0;\n let b = f32(w4(i)) / 64.0;\n let v = vec4<f32>((*pixels)[k]);\n sAA = sAA + a * a;\n sBB = sBB + b * b;\n sAB = sAB + a * b;\n sAV = sAV + a * v;\n sBV = sBV + b * v;\n }\n let det = sAA * sBB - sAB * sAB;\n var out: RefitResult;\n if (abs(det) < 1e-9) {\n out.valid = false;\n return out;\n }\n let e0f = (sBB * sAV - sAB * sBV) / det;\n let e1f = (sAA * sBV - sAB * sAV) / det;\n out.e0 = vec4<i32>(clamp(round(e0f), vec4<f32>(0.0), vec4<f32>(255.0)));\n out.e1 = vec4<i32>(clamp(round(e1f), vec4<f32>(0.0), vec4<f32>(255.0)));\n out.valid = true;\n return out;\n}\n\n// ------------------------------- Entry --------------------------------- //\n\n@compute @workgroup_size(8, 8, 1)\nfn encode(@builtin(global_invocation_id) gid: vec3<u32>) {\n if (gid.x >= params.blocks_x || gid.y >= params.blocks_y) {\n return;\n }\n\n let bx = gid.x;\n let by = gid.y;\n let block_index = by * params.blocks_x + bx;\n\n let base = vec2<i32>(i32(bx) * 4, i32(by) * 4);\n let max_xy = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);\n\n // Load 16 RGBA pixels (8-bit integer domain) and the per-channel bbox.\n var pixels: array<vec4<i32>, 16>;\n var lo = vec4<i32>(255);\n var hi = vec4<i32>(0);\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let lx = i32(i & 3u);\n let ly = i32(i >> 2u);\n let p = clamp(base + vec2<i32>(lx, ly), vec2<i32>(0, 0), max_xy);\n let px = to8(textureLoad(src_tex, p, 0));\n pixels[i] = px;\n lo = min(lo, px);\n hi = max(hi, px);\n }\n\n // Both branches produce: 7-bit endpoints + p-bits, and the 16 4-bit indices\n // packed LSB-first into two nibble words (pixel k \u2192 bits 4k..4k+3).\n var e0_7: vec4<i32>;\n var e1_7: vec4<i32>;\n var p0: u32;\n var p1: u32;\n var ilo: u32 = 0u;\n var ihi: u32 = 0u;\n\n if (QUALITY_HIGH != 0u) {\n let fp = farthest_pair(&pixels);\n var best: BestMode6;\n best.err = 2147483647;\n try_pbit_combos(&pixels, fp.a, fp.b, &best);\n let refit = refit_endpoints(&pixels, &best.indices);\n if (refit.valid) {\n try_pbit_combos(&pixels, refit.e0, refit.e1, &best);\n }\n e0_7 = best.e0_7; e1_7 = best.e1_7; p0 = best.p0; p1 = best.p1;\n for (var k: u32 = 0u; k < 8u; k = k + 1u) {\n ilo = ilo | (best.indices[k] << (k * 4u));\n }\n for (var k: u32 = 8u; k < 16u; k = k + 1u) {\n ihi = ihi | (best.indices[k] << ((k - 8u) * 4u));\n }\n } else {\n // Seed the fused LSQ fit from the raw bbox, then quantise the refit\n // endpoints and reproject for the final indices.\n let r = proj_fit(&pixels, lo, hi);\n var ep0: Ep;\n var ep1: Ep;\n if (r.valid) { ep0 = pick_ep(r.e0); ep1 = pick_ep(r.e1); }\n else { ep0 = pick_ep(lo); ep1 = pick_ep(hi); }\n let dir = vec4<f32>(ep1.eight - ep0.eight);\n let dd = dot(dir, dir);\n if (dd > 0.0) {\n let e0f = vec4<f32>(ep0.eight);\n let inv = 15.0 / dd;\n for (var k: u32 = 0u; k < 8u; k = k + 1u) {\n let s = clamp(floor(dot(vec4<f32>(pixels[k]) - e0f, dir) * inv + 0.5), 0.0, 15.0);\n ilo = ilo | (u32(s) << (k * 4u));\n }\n for (var k: u32 = 8u; k < 16u; k = k + 1u) {\n let s = clamp(floor(dot(vec4<f32>(pixels[k]) - e0f, dir) * inv + 0.5), 0.0, 15.0);\n ihi = ihi | (u32(s) << ((k - 8u) * 4u));\n }\n }\n e0_7 = ep0.seven; e1_7 = ep1.seven; p0 = ep0.p; p1 = ep1.p;\n }\n\n // Anchor rule \u2014 pixel 0's index MSB must be 0. Swapping endpoints reflects\n // every index (i \u2192 15\u2212i), which on packed nibbles is a bitwise NOT.\n if ((ilo & 0x8u) != 0u) {\n let t7 = e0_7; e0_7 = e1_7; e1_7 = t7;\n let tp = p0; p0 = p1; p1 = tp;\n ilo = ~ilo; ihi = ~ihi;\n }\n\n // Straight-line mode-6 packing (see layout at the top of the file).\n let e0 = vec4<u32>(e0_7);\n let e1 = vec4<u32>(e1_7);\n let w0 = 0x40u | (e0.x << 7u) | (e1.x << 14u) | (e0.y << 21u) | (e1.y << 28u);\n let w1 = (e1.y >> 4u) | (e0.z << 3u) | (e1.z << 10u) | (e0.w << 17u) | (e1.w << 24u) | (p0 << 31u);\n let w2 = p1 | ((ilo & 0x7u) << 1u) | (ilo & 0xFFFFFFF0u);\n let w3 = ihi;\n\n let out = block_index * 4u;\n dst[out + 0u] = w0;\n dst[out + 1u] = w1;\n dst[out + 2u] = w2;\n dst[out + 3u] = w3;\n}\n";
|
|
632
632
|
|
|
633
633
|
// src/bc7_fast_f16.wgsl
|
|
634
|
-
var bc7_fast_f16_default =
|
|
634
|
+
var bc7_fast_f16_default = `// bc7 "fast" encoder \u2014 f16 variant (requires the shader-f16 feature).
|
|
635
|
+
// Same algorithm family as the f32 fast path in bc7.wgsl (bbox seed \u2192
|
|
636
|
+
// projection-based index assignment with a fused least-squares refit \u2192
|
|
637
|
+
// reproject), tuned for throughput:
|
|
638
|
+
//
|
|
639
|
+
// \u2022 All projection / refit math in f16 ([0,1] domain). ~2\xD7 ALU throughput
|
|
640
|
+
// on f16-capable GPUs. The projection direction is pre-scaled by 32:
|
|
641
|
+
// a shallow block (endpoints ~1/255 apart) has dd = dot(dir,dir) \u2248 1.5e-5,
|
|
642
|
+
// where 15/dd \u2248 10\u2076 overflows f16 (max 65504) to +inf and the products
|
|
643
|
+
// inside the projection dot are subnormal \u2014 the indices and the LSQ refit
|
|
644
|
+
// feeding on them turn to garbage (visible as banding on smooth
|
|
645
|
+
// gradients). Scaling dir by 32 multiplies the dots by 32 and dd by 1024;
|
|
646
|
+
// s = dot\xB7(32\xB715/dd\u2083\u2082) is the same quantity with every intermediate in
|
|
647
|
+
// f16's normal range (worst case inv = 480/0.0157 \u2248 3.0e4 < 65504).
|
|
648
|
+
// \u2022 The LSQ seed pass projects against the RAW bbox endpoints \u2014 quantising
|
|
649
|
+
// the seed first (pick_ep) costs two extra quantisation searches and
|
|
650
|
+
// doesn't measurably change where the refit lands.
|
|
651
|
+
// \u2022 Indices are packed into two u32 nibble words ON THE FLY during the
|
|
652
|
+
// final projection pass \u2014 no array<u32,16> private array. The BC7 anchor
|
|
653
|
+
// reflection (i \u2192 15\u2212i) is then just a bitwise NOT of both words.
|
|
654
|
+
// \u2022 The 128-bit block is assembled with straight-line constant shifts
|
|
655
|
+
// instead of a generic write_bits() helper (whose dynamic word indexing
|
|
656
|
+
// defeats register promotion of the output array).
|
|
657
|
+
//
|
|
658
|
+
// The host selects this module only when the device reports shader-f16,
|
|
659
|
+
// falling back to bc7.wgsl otherwise. "high" never uses this.
|
|
660
|
+
//
|
|
661
|
+
// MODE 6 BIT LAYOUT (LSB-first): see bc7.wgsl. Summary:
|
|
662
|
+
// w0: mode(7 bits, 0x40) R0 R1 G0 G1[3:0]
|
|
663
|
+
// w1: G1[6:4] B0 B1 A0 A1 P0
|
|
664
|
+
// w2: P1, pixel0 index (3 bits), pixels 1..7 (4 bits each)
|
|
665
|
+
// w3: pixels 8..15 (4 bits each)
|
|
666
|
+
enable f16;
|
|
667
|
+
struct Params { blocks_x: u32, blocks_y: u32, width: u32, height: u32, };
|
|
668
|
+
@group(0) @binding(0) var src_tex: texture_2d<f32>;
|
|
669
|
+
@group(0) @binding(1) var<storage, read_write> dst: array<u32>;
|
|
670
|
+
@group(0) @binding(2) var<uniform> params: Params;
|
|
671
|
+
alias h = f16;
|
|
672
|
+
alias h4 = vec4<f16>;
|
|
673
|
+
|
|
674
|
+
// Quantise an ideal endpoint (h4 in [0,1]) to 7-bit + p-bit, choosing the
|
|
675
|
+
// p-bit with the lower quantisation error. \`eight\` is the decoded value the
|
|
676
|
+
// hardware will interpolate with, back in [0,1].
|
|
677
|
+
struct Ep { seven: vec4<u32>, eight: h4, p: u32 };
|
|
678
|
+
fn pick_ep(ideal01: h4) -> Ep {
|
|
679
|
+
let ideal = ideal01 * h(255.0);
|
|
680
|
+
let q0 = clamp(floor(ideal * h(0.5) + h(0.5)), h4(0.0), h4(127.0)); // p=0
|
|
681
|
+
let e0 = q0 * h(2.0);
|
|
682
|
+
let q1 = clamp(floor((ideal - h(1.0)) * h(0.5) + h(0.5)), h4(0.0), h4(127.0)); // p=1
|
|
683
|
+
let e1 = q1 * h(2.0) + h(1.0);
|
|
684
|
+
let d0 = e0 - ideal; let d1 = e1 - ideal;
|
|
685
|
+
if (dot(d1, d1) < dot(d0, d0)) { return Ep(vec4<u32>(q1), e1 * h(1.0 / 255.0), 1u); }
|
|
686
|
+
return Ep(vec4<u32>(q0), e0 * h(1.0 / 255.0), 0u);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// One pass over the block: project every pixel onto the e0\u2192e1 line and
|
|
690
|
+
// accumulate the least-squares normal-equation sums; solve for the refit
|
|
691
|
+
// endpoints. Indices are NOT produced here \u2014 the caller reprojects against
|
|
692
|
+
// the quantised refit endpoints anyway.
|
|
693
|
+
//
|
|
694
|
+
// The value sums accumulate v \u2212 e0, not v: the basis is affine (a + b = 1),
|
|
695
|
+
// so fitting the shifted data and adding e0 back is the same fit, but the
|
|
696
|
+
// accumulators scale with the block's span instead of its absolute level \u2014
|
|
697
|
+
// on a shallow dark block, f16 rounding of absolute sums (ulp \u2248 0.12 of an
|
|
698
|
+
// 8-bit level per add) drifts the refit endpoints by \xB11 level.
|
|
699
|
+
struct Fit { e0: h4, e1: h4, valid: bool };
|
|
700
|
+
fn proj_fit(pix: ptr<function, array<h4, 16>>, e0: h4, e1: h4) -> Fit {
|
|
701
|
+
var out: Fit;
|
|
702
|
+
out.valid = false;
|
|
703
|
+
// dir pre-scaled by 32 to keep dd and the projection dots in f16's normal
|
|
704
|
+
// range (see header). Spans below ~0.7 of an 8-bit step (dd\u2083\u2082 < 0.008,
|
|
705
|
+
// possible only for non-8-bit sources) are treated as flat \u2014 encoding them
|
|
706
|
+
// flat is under half a level of error, while running the math on them risks
|
|
707
|
+
// inv overflowing to +inf.
|
|
708
|
+
let dir = (e1 - e0) * h(32.0);
|
|
709
|
+
let dd = dot(dir, dir);
|
|
710
|
+
if (dd < h(0.008)) { return out; }
|
|
711
|
+
let inv = h(480.0) / dd; // 32\xB715/dd\u2083\u2082 \u2261 15/dd
|
|
712
|
+
var sAA = h(0.0); var sBB = h(0.0); var sAB = h(0.0);
|
|
713
|
+
var sAV = h4(0.0); var sBV = h4(0.0);
|
|
714
|
+
var s_min = h(15.0); var s_max = h(0.0);
|
|
715
|
+
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
716
|
+
let vr = (*pix)[k] - e0;
|
|
717
|
+
let s = clamp(floor(dot(vr, dir) * inv + h(0.5)), h(0.0), h(15.0));
|
|
718
|
+
s_min = min(s_min, s); s_max = max(s_max, s);
|
|
719
|
+
let b = s * h(1.0 / 15.0); let a = h(1.0) - b;
|
|
720
|
+
sAA = sAA + a * a; sBB = sBB + b * b; sAB = sAB + a * b;
|
|
721
|
+
sAV = sAV + a * vr; sBV = sBV + b * vr;
|
|
722
|
+
}
|
|
723
|
+
// Rank-1 guard: if every pixel projects to ONE level the system is
|
|
724
|
+
// singular \u2014 det/numerators are pure f16 rounding noise and the solve
|
|
725
|
+
// returns garbage endpoints. With \u22652 distinct levels
|
|
726
|
+
// det = \u03A3_i<j (b_j \u2212 b_i)\xB2 \u2265 15/225 \u2248 0.067, so 0.02 is a safe floor.
|
|
727
|
+
if (s_min == s_max) { return out; }
|
|
728
|
+
let det = sAA * sBB - sAB * sAB;
|
|
729
|
+
if (abs(det) < h(0.02)) { return out; }
|
|
730
|
+
out.e0 = clamp(e0 + (sBB * sAV - sAB * sBV) / det, h4(0.0), h4(1.0));
|
|
731
|
+
out.e1 = clamp(e0 + (sAA * sBV - sAB * sAV) / det, h4(0.0), h4(1.0));
|
|
732
|
+
out.valid = true;
|
|
733
|
+
return out;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
@compute @workgroup_size(8, 8, 1)
|
|
737
|
+
fn encode(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
738
|
+
if (gid.x >= params.blocks_x || gid.y >= params.blocks_y) { return; }
|
|
739
|
+
let bi = gid.y * params.blocks_x + gid.x;
|
|
740
|
+
let base = vec2<i32>(i32(gid.x) * 4, i32(gid.y) * 4);
|
|
741
|
+
let mx = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);
|
|
742
|
+
|
|
743
|
+
var pix: array<h4, 16>;
|
|
744
|
+
var lo = h4(1.0);
|
|
745
|
+
var hi = h4(0.0);
|
|
746
|
+
for (var i: u32 = 0u; i < 16u; i = i + 1u) {
|
|
747
|
+
let p = clamp(base + vec2<i32>(i32(i & 3u), i32(i >> 2u)), vec2<i32>(0), mx);
|
|
748
|
+
let px = h4(textureLoad(src_tex, p, 0));
|
|
749
|
+
pix[i] = px; lo = min(lo, px); hi = max(hi, px);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Seed fit from the raw bbox, then quantise the refit endpoints.
|
|
753
|
+
let r = proj_fit(&pix, lo, hi);
|
|
754
|
+
var ep0: Ep;
|
|
755
|
+
var ep1: Ep;
|
|
756
|
+
if (r.valid) { ep0 = pick_ep(r.e0); ep1 = pick_ep(r.e1); }
|
|
757
|
+
else { ep0 = pick_ep(lo); ep1 = pick_ep(hi); }
|
|
758
|
+
|
|
759
|
+
// Final projection against the decoded endpoints, packing the 4-bit indices
|
|
760
|
+
// into two nibble words as we go (pixel k \u2192 bits 4k..4k+3 of ilo/ihi).
|
|
761
|
+
var ilo: u32 = 0u;
|
|
762
|
+
var ihi: u32 = 0u;
|
|
763
|
+
// Same \xD732 pre-scale as proj_fit; distinct quantised endpoints are \u22651/255
|
|
764
|
+
// apart, i.e. dd\u2083\u2082 \u2265 0.0157, so the flat-block threshold only catches
|
|
765
|
+
// truly identical endpoints.
|
|
766
|
+
let dir = (ep1.eight - ep0.eight) * h(32.0);
|
|
767
|
+
let dd = dot(dir, dir);
|
|
768
|
+
if (dd >= h(0.008)) {
|
|
769
|
+
let inv = h(480.0) / dd;
|
|
770
|
+
for (var k: u32 = 0u; k < 8u; k = k + 1u) {
|
|
771
|
+
let s = clamp(floor(dot(pix[k] - ep0.eight, dir) * inv + h(0.5)), h(0.0), h(15.0));
|
|
772
|
+
ilo = ilo | (u32(s) << (k * 4u));
|
|
773
|
+
}
|
|
774
|
+
for (var k: u32 = 8u; k < 16u; k = k + 1u) {
|
|
775
|
+
let s = clamp(floor(dot(pix[k] - ep0.eight, dir) * inv + h(0.5)), h(0.0), h(15.0));
|
|
776
|
+
ihi = ihi | (u32(s) << ((k - 8u) * 4u));
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Anchor rule \u2014 pixel 0's index MSB must be 0. Swapping endpoints reflects
|
|
781
|
+
// every index (i \u2192 15\u2212i), which on packed nibbles is a bitwise NOT.
|
|
782
|
+
if ((ilo & 0x8u) != 0u) {
|
|
783
|
+
let t = ep0; ep0 = ep1; ep1 = t;
|
|
784
|
+
ilo = ~ilo; ihi = ~ihi;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// Straight-line mode-6 packing (see layout above).
|
|
788
|
+
let e0 = ep0.seven;
|
|
789
|
+
let e1 = ep1.seven;
|
|
790
|
+
let w0 = 0x40u | (e0.x << 7u) | (e1.x << 14u) | (e0.y << 21u) | (e1.y << 28u);
|
|
791
|
+
let w1 = (e1.y >> 4u) | (e0.z << 3u) | (e1.z << 10u) | (e0.w << 17u) | (e1.w << 24u) | (ep0.p << 31u);
|
|
792
|
+
let w2 = ep1.p | ((ilo & 0x7u) << 1u) | (ilo & 0xFFFFFFF0u);
|
|
793
|
+
let w3 = ihi;
|
|
794
|
+
|
|
795
|
+
let o = bi * 4u;
|
|
796
|
+
dst[o] = w0; dst[o + 1u] = w1; dst[o + 2u] = w2; dst[o + 3u] = w3;
|
|
797
|
+
}
|
|
798
|
+
`;
|
|
635
799
|
|
|
636
800
|
// src/BC7Encoder.ts
|
|
637
801
|
var BC7Encoder = class extends Encoder {
|
|
@@ -669,7 +833,13 @@ var astc4x4_fast_f16_default = `// astc4x4 "fast" encoder \u2014 f16 variant (re
|
|
|
669
833
|
// projection weight assignment with a fused least-squares refit \u2192
|
|
670
834
|
// reproject), tuned for throughput:
|
|
671
835
|
//
|
|
672
|
-
// \u2022 All projection / refit math in f16 ([0,1] domain).
|
|
836
|
+
// \u2022 All projection / refit math in f16 ([0,1] domain). The projection
|
|
837
|
+
// direction is pre-scaled by 32 \u2014 a shallow block (endpoints ~1/255
|
|
838
|
+
// apart) has dd \u2248 1.5e-5, where 3/dd \u2248 2e5 overflows f16 (max 65504) to
|
|
839
|
+
// +inf and the projection dots go subnormal, turning weights and the LSQ
|
|
840
|
+
// refit to garbage (banding on smooth gradients). Scaling dir by 32 puts
|
|
841
|
+
// every intermediate in f16's normal range; s = dot\xB7(32\xB73/dd\u2083\u2082) is the
|
|
842
|
+
// same quantity (worst case inv = 96/0.0157 \u2248 6.1e3).
|
|
673
843
|
// \u2022 The seed pass only accumulates the LSQ sums (no weight output) \u2014 the
|
|
674
844
|
// final weights come from reprojecting against the refit endpoints.
|
|
675
845
|
// \u2022 Endpoint ordering (the blue-contraction rule: sum(e0.rgb) must not
|
|
@@ -696,20 +866,27 @@ struct Fit { e0: h4, e1: h4, valid: bool };
|
|
|
696
866
|
fn proj_fit(pix: ptr<function, array<h4, 16>>, e0: h4, e1: h4) -> Fit {
|
|
697
867
|
var out: Fit;
|
|
698
868
|
out.valid = false;
|
|
699
|
-
|
|
869
|
+
// dir pre-scaled by 32 to keep dd and the projection dots in f16's normal
|
|
870
|
+
// range (see header). Spans below ~0.7 of an 8-bit step (dd\u2083\u2082 < 0.008,
|
|
871
|
+
// possible only for non-8-bit sources) are treated as flat.
|
|
872
|
+
let dir = (e1 - e0) * h(32.0);
|
|
700
873
|
let dd = dot(dir, dir);
|
|
701
|
-
if (dd
|
|
702
|
-
let inv = h(
|
|
874
|
+
if (dd < h(0.008)) { return out; }
|
|
875
|
+
let inv = h(96.0) / dd; // 32\xB73/dd\u2083\u2082 \u2261 3/dd
|
|
703
876
|
var sAA = h(0.0); var sBB = h(0.0); var sAB = h(0.0);
|
|
704
877
|
var sAV = h4(0.0); var sBV = h4(0.0);
|
|
705
878
|
var s_min = h(3.0); var s_max = h(0.0);
|
|
879
|
+
// Value sums accumulate v \u2212 e0 (basis is affine, a + b = 1, so the fit
|
|
880
|
+
// commutes with the shift): accumulators scale with the block span, keeping
|
|
881
|
+
// f16 rounding a fraction of the span instead of \xB11 level at high absolute
|
|
882
|
+
// values.
|
|
706
883
|
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
707
|
-
let
|
|
708
|
-
let s = clamp(floor(dot(
|
|
884
|
+
let vr = (*pix)[k] - e0;
|
|
885
|
+
let s = clamp(floor(dot(vr, dir) * inv + h(0.5)), h(0.0), h(3.0));
|
|
709
886
|
s_min = min(s_min, s); s_max = max(s_max, s);
|
|
710
887
|
let b = s * h(1.0 / 3.0); let a = h(1.0) - b;
|
|
711
888
|
sAA = sAA + a * a; sBB = sBB + b * b; sAB = sAB + a * b;
|
|
712
|
-
sAV = sAV + a *
|
|
889
|
+
sAV = sAV + a * vr; sBV = sBV + b * vr;
|
|
713
890
|
}
|
|
714
891
|
// Rank-1 guard: if every pixel projects to ONE level the system is
|
|
715
892
|
// singular \u2014 det/numerators are pure f16 rounding noise and the solve
|
|
@@ -718,8 +895,8 @@ fn proj_fit(pix: ptr<function, array<h4, 16>>, e0: h4, e1: h4) -> Fit {
|
|
|
718
895
|
if (s_min == s_max) { return out; }
|
|
719
896
|
let det = sAA * sBB - sAB * sAB;
|
|
720
897
|
if (abs(det) < h(0.5)) { return out; }
|
|
721
|
-
out.e0 = clamp((sBB * sAV - sAB * sBV) / det, h4(0.0), h4(1.0));
|
|
722
|
-
out.e1 = clamp((sAA * sBV - sAB * sAV) / det, h4(0.0), h4(1.0));
|
|
898
|
+
out.e0 = clamp(e0 + (sBB * sAV - sAB * sBV) / det, h4(0.0), h4(1.0));
|
|
899
|
+
out.e1 = clamp(e0 + (sAA * sBV - sAB * sAV) / det, h4(0.0), h4(1.0));
|
|
723
900
|
out.valid = true;
|
|
724
901
|
return out;
|
|
725
902
|
}
|
|
@@ -762,10 +939,12 @@ fn encode(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
|
762
939
|
// Weight pass, packing on the fly: weight k's lsb at bit 31\u22122k of the last
|
|
763
940
|
// word, msb at bit 30\u22122k.
|
|
764
941
|
var w3: u32 = 0u;
|
|
765
|
-
|
|
942
|
+
// Same \xD732 pre-scale as proj_fit; distinct 8-bit endpoints are \u22651/255
|
|
943
|
+
// apart (dd\u2083\u2082 \u2265 0.0157), so the threshold only catches identical ones.
|
|
944
|
+
let dir = (d1 - d0) * h(32.0);
|
|
766
945
|
let dd = dot(dir, dir);
|
|
767
|
-
if (dd
|
|
768
|
-
let inv = h(
|
|
946
|
+
if (dd >= h(0.008)) {
|
|
947
|
+
let inv = h(96.0) / dd;
|
|
769
948
|
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
770
949
|
let s = u32(clamp(floor(dot(pix[k] - d0, dir) * inv + h(0.5)), h(0.0), h(3.0)));
|
|
771
950
|
w3 = w3 | ((s & 1u) << (31u - 2u * k)) | (((s >> 1u) & 1u) << (30u - 2u * k));
|
|
@@ -1114,13 +1293,26 @@ function detectWebGLCapabilities(gl) {
|
|
|
1114
1293
|
// src/webgl/selectWebGLFormat.ts
|
|
1115
1294
|
var NONE = { format: null, encoderClass: null, astcNormalRemap: false };
|
|
1116
1295
|
function selectWebGLFormat(caps, hint, options = {}) {
|
|
1117
|
-
const { colorSpace = "srgb" } = options;
|
|
1296
|
+
const { colorSpace = "srgb", preferredFormat } = options;
|
|
1118
1297
|
const srgb = colorSpace === "srgb";
|
|
1119
1298
|
const astc = (astcNormalRemap) => ({
|
|
1120
1299
|
format: srgb ? TextureFormat.ASTC_4x4_SRGB : TextureFormat.ASTC_4x4,
|
|
1121
1300
|
encoderClass: ASTC4x4WebGLEncoder,
|
|
1122
1301
|
astcNormalRemap
|
|
1123
1302
|
});
|
|
1303
|
+
if (preferredFormat === "bc1") {
|
|
1304
|
+
if (hint !== "color") {
|
|
1305
|
+
console.warn(
|
|
1306
|
+
`[gputex] preferredFormat 'bc1' ignored for hint '${hint}' \u2014 BC1 has no real alpha channel and is unsuitable for normal maps.`
|
|
1307
|
+
);
|
|
1308
|
+
} else if (srgb ? caps.s3tcSrgb : caps.s3tc) {
|
|
1309
|
+
return {
|
|
1310
|
+
format: srgb ? TextureFormat.BC1_SRGB : TextureFormat.BC1,
|
|
1311
|
+
encoderClass: BC1WebGLEncoder,
|
|
1312
|
+
astcNormalRemap: false
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1124
1316
|
if (hint === "normal") {
|
|
1125
1317
|
if (caps.rgtc) return { format: TextureFormat.BC5, encoderClass: BC5WebGLEncoder, astcNormalRemap: false };
|
|
1126
1318
|
if (caps.astc) return astc(true);
|
|
@@ -1147,9 +1339,22 @@ function selectWebGLFormat(caps, hint, options = {}) {
|
|
|
1147
1339
|
|
|
1148
1340
|
// src/selectFormat.ts
|
|
1149
1341
|
function selectFormat(adapter, hint, options = {}) {
|
|
1150
|
-
const { colorSpace = "srgb" } = options;
|
|
1342
|
+
const { colorSpace = "srgb", preferredFormat } = options;
|
|
1151
1343
|
const srgb = colorSpace === "srgb";
|
|
1152
1344
|
const caps = detectCapabilities(adapter);
|
|
1345
|
+
if (preferredFormat === "bc1") {
|
|
1346
|
+
if (hint !== "color") {
|
|
1347
|
+
console.warn(
|
|
1348
|
+
`[gputex] preferredFormat 'bc1' ignored for hint '${hint}' \u2014 BC1 has no real alpha channel and is unsuitable for normal maps.`
|
|
1349
|
+
);
|
|
1350
|
+
} else if (caps.bc) {
|
|
1351
|
+
return {
|
|
1352
|
+
format: srgb ? TextureFormat.BC1_SRGB : TextureFormat.BC1,
|
|
1353
|
+
encoderClass: BC1Encoder,
|
|
1354
|
+
astcNormalRemap: false
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1153
1358
|
if (caps.bc) {
|
|
1154
1359
|
if (hint === "normal") {
|
|
1155
1360
|
return { format: TextureFormat.BC5, encoderClass: BC5Encoder, astcNormalRemap: false };
|
|
@@ -1236,6 +1441,128 @@ function padToBlockMultiple(level) {
|
|
|
1236
1441
|
}
|
|
1237
1442
|
return { data: out, width: pw, height: ph };
|
|
1238
1443
|
}
|
|
1444
|
+
|
|
1445
|
+
// src/svg.ts
|
|
1446
|
+
var ROOT_TAG_RE = /<svg(?=[\s/>])[^>]*>/;
|
|
1447
|
+
function getAttr(tag, name) {
|
|
1448
|
+
const m = new RegExp(`\\s${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`).exec(tag);
|
|
1449
|
+
return m ? m[1] ?? m[2] ?? "" : null;
|
|
1450
|
+
}
|
|
1451
|
+
function removeAttr(tag, name) {
|
|
1452
|
+
return tag.replace(new RegExp(`\\s${name}\\s*=\\s*(?:"[^"]*"|'[^']*')`, "g"), "");
|
|
1453
|
+
}
|
|
1454
|
+
function parseAbsoluteLength(value) {
|
|
1455
|
+
if (value == null) return null;
|
|
1456
|
+
const m = /^\s*\+?(\d+(?:\.\d+)?|\.\d+)(?:px)?\s*$/i.exec(value);
|
|
1457
|
+
if (!m) return null;
|
|
1458
|
+
const n = Number(m[1]);
|
|
1459
|
+
return n > 0 ? n : null;
|
|
1460
|
+
}
|
|
1461
|
+
function parseSvgDimensions(svgText) {
|
|
1462
|
+
const m = ROOT_TAG_RE.exec(svgText);
|
|
1463
|
+
if (!m) return null;
|
|
1464
|
+
const tag = m[0];
|
|
1465
|
+
const dims = {
|
|
1466
|
+
width: parseAbsoluteLength(getAttr(tag, "width")),
|
|
1467
|
+
height: parseAbsoluteLength(getAttr(tag, "height")),
|
|
1468
|
+
viewBoxWidth: null,
|
|
1469
|
+
viewBoxHeight: null
|
|
1470
|
+
};
|
|
1471
|
+
const viewBox = getAttr(tag, "viewBox");
|
|
1472
|
+
if (viewBox) {
|
|
1473
|
+
const parts = viewBox.trim().split(/[\s,]+/).map(Number);
|
|
1474
|
+
if (parts.length === 4 && parts.every(Number.isFinite) && parts[2] > 0 && parts[3] > 0) {
|
|
1475
|
+
dims.viewBoxWidth = parts[2];
|
|
1476
|
+
dims.viewBoxHeight = parts[3];
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
return dims;
|
|
1480
|
+
}
|
|
1481
|
+
function resolveSvgRasterSize(dims, size) {
|
|
1482
|
+
if (size !== void 0 && typeof size === "object") {
|
|
1483
|
+
const width = Math.round(size.width);
|
|
1484
|
+
const height = Math.round(size.height);
|
|
1485
|
+
if (!(width >= 1) || !(height >= 1)) {
|
|
1486
|
+
throw new Error(`rasterizeSvg: svgSize must be \u22651\xD71 (got ${size.width}\xD7${size.height})`);
|
|
1487
|
+
}
|
|
1488
|
+
return { width, height };
|
|
1489
|
+
}
|
|
1490
|
+
let w = dims.width;
|
|
1491
|
+
let h = dims.height;
|
|
1492
|
+
if (dims.viewBoxWidth != null && dims.viewBoxHeight != null) {
|
|
1493
|
+
if (w == null && h != null) w = h * dims.viewBoxWidth / dims.viewBoxHeight;
|
|
1494
|
+
else if (h == null && w != null) h = w * dims.viewBoxHeight / dims.viewBoxWidth;
|
|
1495
|
+
else if (w == null && h == null) {
|
|
1496
|
+
w = dims.viewBoxWidth;
|
|
1497
|
+
h = dims.viewBoxHeight;
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
if (typeof size === "number") {
|
|
1501
|
+
if (!(size >= 1)) {
|
|
1502
|
+
throw new Error(`rasterizeSvg: svgSize must be \u22651 (got ${size})`);
|
|
1503
|
+
}
|
|
1504
|
+
const aspect = w != null && h != null ? w / h : 1;
|
|
1505
|
+
return aspect >= 1 ? { width: Math.round(size), height: Math.max(1, Math.round(size / aspect)) } : { width: Math.max(1, Math.round(size * aspect)), height: Math.round(size) };
|
|
1506
|
+
}
|
|
1507
|
+
if (w == null || h == null) {
|
|
1508
|
+
throw new Error(
|
|
1509
|
+
"rasterizeSvg: the SVG has no intrinsic size (no absolute width/height attributes and no viewBox) \u2014 pass svgSize to choose a rasterisation size"
|
|
1510
|
+
);
|
|
1511
|
+
}
|
|
1512
|
+
return { width: Math.max(1, Math.round(w)), height: Math.max(1, Math.round(h)) };
|
|
1513
|
+
}
|
|
1514
|
+
function setSvgRootSize(svgText, width, height) {
|
|
1515
|
+
const m = ROOT_TAG_RE.exec(svgText);
|
|
1516
|
+
if (!m) {
|
|
1517
|
+
throw new Error("rasterizeSvg: no <svg> root element found in source");
|
|
1518
|
+
}
|
|
1519
|
+
let tag = m[0];
|
|
1520
|
+
const origWidth = parseAbsoluteLength(getAttr(tag, "width"));
|
|
1521
|
+
const origHeight = parseAbsoluteLength(getAttr(tag, "height"));
|
|
1522
|
+
const hasViewBox = getAttr(tag, "viewBox") != null;
|
|
1523
|
+
tag = removeAttr(removeAttr(tag, "width"), "height");
|
|
1524
|
+
let inject = ` width="${width}" height="${height}"`;
|
|
1525
|
+
if (!hasViewBox && origWidth != null && origHeight != null) {
|
|
1526
|
+
inject += ` viewBox="0 0 ${origWidth} ${origHeight}"`;
|
|
1527
|
+
}
|
|
1528
|
+
tag = `<svg${inject}${tag.slice("<svg".length)}`;
|
|
1529
|
+
return svgText.slice(0, m.index) + tag + svgText.slice(m.index + m[0].length);
|
|
1530
|
+
}
|
|
1531
|
+
async function rasterizeSvg(source, options = {}) {
|
|
1532
|
+
if (typeof Image === "undefined") {
|
|
1533
|
+
throw new Error(
|
|
1534
|
+
"rasterizeSvg: SVG rasterisation needs a DOM Image element and cannot run in this environment (e.g. a worker)"
|
|
1535
|
+
);
|
|
1536
|
+
}
|
|
1537
|
+
const svgText = typeof source === "string" ? source : await source.text();
|
|
1538
|
+
const dims = parseSvgDimensions(svgText);
|
|
1539
|
+
if (!dims) {
|
|
1540
|
+
throw new Error("rasterizeSvg: no <svg> root element found in source");
|
|
1541
|
+
}
|
|
1542
|
+
const { width, height } = resolveSvgRasterSize(dims, options.size);
|
|
1543
|
+
const sized = setSvgRootSize(svgText, width, height);
|
|
1544
|
+
const url = URL.createObjectURL(new Blob([sized], { type: "image/svg+xml;charset=utf-8" }));
|
|
1545
|
+
try {
|
|
1546
|
+
const img = new Image();
|
|
1547
|
+
img.decoding = "async";
|
|
1548
|
+
await new Promise((resolve, reject) => {
|
|
1549
|
+
img.onload = () => resolve();
|
|
1550
|
+
img.onerror = () => reject(new Error("rasterizeSvg: the browser failed to decode the SVG"));
|
|
1551
|
+
img.src = url;
|
|
1552
|
+
});
|
|
1553
|
+
await img.decode().catch(() => {
|
|
1554
|
+
});
|
|
1555
|
+
const canvas = typeof OffscreenCanvas !== "undefined" ? new OffscreenCanvas(width, height) : Object.assign(document.createElement("canvas"), { width, height });
|
|
1556
|
+
const ctx = canvas.getContext("2d");
|
|
1557
|
+
if (!ctx) {
|
|
1558
|
+
throw new Error("rasterizeSvg: no 2D context available");
|
|
1559
|
+
}
|
|
1560
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
1561
|
+
return await createImageBitmap(canvas, { colorSpaceConversion: "none", premultiplyAlpha: "none" });
|
|
1562
|
+
} finally {
|
|
1563
|
+
URL.revokeObjectURL(url);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1239
1566
|
export {
|
|
1240
1567
|
ASTC4x4Encoder,
|
|
1241
1568
|
ASTC4x4WebGLEncoder,
|
|
@@ -1256,6 +1583,7 @@ export {
|
|
|
1256
1583
|
getSharedWebGLContext,
|
|
1257
1584
|
isWebGLAvailable,
|
|
1258
1585
|
padToBlockMultiple,
|
|
1586
|
+
rasterizeSvg,
|
|
1259
1587
|
selectFormat,
|
|
1260
1588
|
selectWebGLFormat
|
|
1261
1589
|
};
|
package/dist/three.d.ts
CHANGED
|
@@ -1,18 +1,39 @@
|
|
|
1
|
-
import { TextureHint, EncodeQuality, TextureFormat, Encoder, EncoderImageSource } from './index.js';
|
|
2
|
-
export { ASTC4x4Encoder, ASTC4x4WebGLEncoder, BC1Encoder, BC1WebGLEncoder, BC5Encoder, BC5WebGLEncoder, BC7Encoder, BC7WebGLEncoder, Capabilities, EncodeBytesResult, EncodeCallOptions, EncoderConstructor, EncoderOptions, ExtensionProvider, FeatureProvider, FormatSelection, FormatVariant, MipLevel, RawPixelSource, SelectFormatOptions, WebGLBlockEncoder, WebGLCapabilities, WebGLEncodeBytesResult, WebGLEncoderConstructor, WebGLEncoderImageSource, WebGLEncoderOptions, WebGLFormatSelection, WebGPUFeature, createWebGLContext, detectCapabilities, detectWebGLCapabilities, generateMipChain, getSharedWebGLContext, isWebGLAvailable, padToBlockMultiple, selectFormat, selectWebGLFormat } from './index.js';
|
|
1
|
+
import { TextureHint, PreferredFormat, SvgRasterSize, EncodeQuality, TextureFormat, Encoder, EncoderImageSource } from './index.js';
|
|
2
|
+
export { ASTC4x4Encoder, ASTC4x4WebGLEncoder, BC1Encoder, BC1WebGLEncoder, BC5Encoder, BC5WebGLEncoder, BC7Encoder, BC7WebGLEncoder, Capabilities, EncodeBytesResult, EncodeCallOptions, EncoderConstructor, EncoderOptions, ExtensionProvider, FeatureProvider, FormatSelection, FormatVariant, MipLevel, RasterizeSvgOptions, RawPixelSource, SelectFormatOptions, WebGLBlockEncoder, WebGLCapabilities, WebGLEncodeBytesResult, WebGLEncoderConstructor, WebGLEncoderImageSource, WebGLEncoderOptions, WebGLFormatSelection, WebGPUFeature, createWebGLContext, detectCapabilities, detectWebGLCapabilities, generateMipChain, getSharedWebGLContext, isWebGLAvailable, padToBlockMultiple, rasterizeSvg, selectFormat, selectWebGLFormat } from './index.js';
|
|
3
3
|
import { Texture, CompressedTexture, Loader, CompressedPixelFormat } from 'three';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Everything `compressTexture()` can take as an image source. A superset
|
|
7
7
|
* of `EncoderImageSource` (see Encoder.ts) that also accepts URL strings
|
|
8
8
|
* and Blob / File objects — the common cases in a web app.
|
|
9
|
+
*
|
|
10
|
+
* SVG works through all of these: a URL to an `.svg` file, a string of
|
|
11
|
+
* inline SVG markup (detected by a leading `<`), an SVG Blob/File, or an
|
|
12
|
+
* HTMLImageElement whose src is SVG. Vector sources are rasterised to RGBA
|
|
13
|
+
* before encoding — see the `svgSize` option.
|
|
9
14
|
*/
|
|
10
15
|
type CompressTextureSource = string | Blob | File | ImageBitmap | HTMLImageElement | HTMLCanvasElement | OffscreenCanvas | ImageData;
|
|
11
16
|
interface CompressOptions {
|
|
12
17
|
/** How the texture will be used. Drives format selection. Default 'color'. */
|
|
13
18
|
hint?: TextureHint;
|
|
19
|
+
/**
|
|
20
|
+
* Prefer a specific format over the default choice when the device
|
|
21
|
+
* supports it; falls back to the normal selection (BC7 → ASTC → RGBA8)
|
|
22
|
+
* when it doesn't. Currently only 'bc1': half the memory of BC7 for
|
|
23
|
+
* opaque colour textures, at lower quality. Only honoured with
|
|
24
|
+
* `hint: 'color'` — BC1 can't carry real alpha or normal maps.
|
|
25
|
+
*/
|
|
26
|
+
preferredFormat?: PreferredFormat;
|
|
14
27
|
/** Pick the sRGB or linear variant of the chosen format. Default 'srgb'. */
|
|
15
28
|
colorSpace?: 'srgb' | 'linear';
|
|
29
|
+
/**
|
|
30
|
+
* Rasterisation size for SVG sources. A number scales the SVG so its
|
|
31
|
+
* longest side matches (aspect ratio preserved); `{ width, height }`
|
|
32
|
+
* rasterises at exactly that size. Default: the SVG's intrinsic size
|
|
33
|
+
* (absolute width/height attributes, else the viewBox dimensions).
|
|
34
|
+
* Ignored for non-SVG sources.
|
|
35
|
+
*/
|
|
36
|
+
svgSize?: SvgRasterSize;
|
|
16
37
|
/** Flip the image vertically before encoding. Default true (matches Three.js convention). */
|
|
17
38
|
flipY?: boolean;
|
|
18
39
|
/** Generate a full mip chain down to 1×1 on the CPU, encode every level. */
|
|
@@ -63,8 +84,20 @@ declare function compressTexture(source: CompressTextureSource, options?: Compre
|
|
|
63
84
|
declare class GputexLoader extends Loader<Texture> {
|
|
64
85
|
/** Format-selection hint. Default 'color'. */
|
|
65
86
|
hint: TextureHint;
|
|
87
|
+
/**
|
|
88
|
+
* Prefer a specific format (currently only 'bc1') when the device
|
|
89
|
+
* supports it; normal selection otherwise. See
|
|
90
|
+
* `CompressOptions.preferredFormat`.
|
|
91
|
+
*/
|
|
92
|
+
preferredFormat?: PreferredFormat;
|
|
66
93
|
/** Pick the sRGB or linear variant of the chosen format. Default 'srgb'. */
|
|
67
94
|
colorSpace: 'srgb' | 'linear';
|
|
95
|
+
/**
|
|
96
|
+
* Rasterisation size for SVG URLs — a number (longest side, aspect
|
|
97
|
+
* preserved) or exact `{ width, height }`. Default: the SVG's intrinsic
|
|
98
|
+
* size. See `CompressOptions.svgSize`.
|
|
99
|
+
*/
|
|
100
|
+
svgSize?: SvgRasterSize;
|
|
68
101
|
/** Flip the image vertically before encoding. Default true (matches Three.js convention). */
|
|
69
102
|
flipY: boolean;
|
|
70
103
|
/** Generate + encode a full mip chain. Default false. */
|
|
@@ -138,4 +171,4 @@ interface EncodeToTextureOptions {
|
|
|
138
171
|
*/
|
|
139
172
|
declare function encodeToTexture(encoder: Encoder, source: EncoderImageSource, { colorSpace, quality, flipY }?: EncodeToTextureOptions): Promise<EncodeResult>;
|
|
140
173
|
|
|
141
|
-
export { type CompressOptions, type CompressResult, type CompressTextureSource, EncodeQuality, type EncodeResult, type EncodeToTextureOptions, Encoder, EncoderImageSource, GputexLoader, TextureFormat, TextureHint, buildCompressedTexture, compressTexture, encodeToTexture, threeFormatFor };
|
|
174
|
+
export { type CompressOptions, type CompressResult, type CompressTextureSource, EncodeQuality, type EncodeResult, type EncodeToTextureOptions, Encoder, EncoderImageSource, GputexLoader, PreferredFormat, SvgRasterSize, TextureFormat, TextureHint, buildCompressedTexture, compressTexture, encodeToTexture, threeFormatFor };
|
package/dist/three.js
CHANGED
|
@@ -635,7 +635,171 @@ var BC5Encoder = class extends Encoder {
|
|
|
635
635
|
var bc7_default = "// BC7 (BPTC) mode 6 compute shader encoder.\n//\n// One invocation per 4\xD74 block. Emits 16 bytes = 4 u32s into the storage\n// buffer at `dst[block_index * 4 .. + 3]`.\n//\n// QUALITY LEVELS (pipeline-overridable constant `QUALITY_HIGH`)\n// fast (0, default): O(N) bounding-box seed \u2192 one fused pass that projects\n// each pixel onto the endpoint line (the 16 palette entries are colinear,\n// so the nearest index is the rounded projection \u2014 no palette build, no\n// 16-entry search) while accumulating the least-squares refit sums, then\n// a reprojection against the quantised refit endpoints for the final\n// indices, packed on the fly into two nibble words.\n// high (1): farthest-pair seed, exhaustive p-bit search over all four\n// (p0,p1) \u2208 {0,1}\xB2 combos, full 16-entry nearest search, one LSQ refit \u2014\n// matches bc7_ref.ts up to FP tie-breaks.\n//\n// The fast/high branch is selected at pipeline-compile time, so the driver\n// eliminates the unused code entirely.\n//\n// MODE 6 LAYOUT (LSB-first, bit 0 = byte 0's bit 0)\n// bits 0..6 mode field (0b0000001 \u2014 only bit 6 is 1)\n// bits 7..13 R0 (7-bit) bits 14..20 R1 bits 21..27 G0 bits 28..34 G1\n// bits 35..41 B0 bits 42..48 B1 bits 49..55 A0 bits 56..62 A1\n// bit 63 P0 bit 64 P1\n// bits 65..67 pixel 0 index (3 bits; anchor, MSB implicit 0)\n// bits 68..71 pixel 1 index (4 bits) ... bits 124..127 pixel 15 index\n//\n// Effective 8-bit endpoint channel = (7_bit_value << 1) | p_bit.\n// Palette[i] = ((64 \u2212 W4[i]) \xD7 e0_8 + W4[i] \xD7 e1_8 + 32) >> 6, integer.\n//\n// The block is assembled with straight-line constant shifts (see the layout\n// summary in bc7_fast_f16.wgsl) \u2014 a generic write_bits() helper's dynamic\n// word indexing keeps the output array out of registers.\n\n// 0 = fast (default), 1 = exhaustive/high-quality. Set via pipeline constants.\noverride QUALITY_HIGH: u32 = 0u;\n\nstruct Params {\n blocks_x: u32,\n blocks_y: u32,\n width: u32,\n height: u32,\n};\n\n@group(0) @binding(0) var src_tex: texture_2d<f32>;\n@group(0) @binding(1) var<storage, read_write> dst: array<u32>;\n@group(0) @binding(2) var<uniform> params: Params;\n\n// Mode 6 interpolation weights (\xD7 1/64), fixed by the spec (`W4` in bc7_ref.ts).\nfn w4(i: u32) -> u32 {\n switch i {\n case 0u: { return 0u; }\n case 1u: { return 4u; }\n case 2u: { return 9u; }\n case 3u: { return 13u; }\n case 4u: { return 17u; }\n case 5u: { return 21u; }\n case 6u: { return 26u; }\n case 7u: { return 30u; }\n case 8u: { return 34u; }\n case 9u: { return 38u; }\n case 10u: { return 43u; }\n case 11u: { return 47u; }\n case 12u: { return 51u; }\n case 13u: { return 55u; }\n case 14u: { return 60u; }\n default: { return 64u; } // case 15u\n }\n}\n\nfn interp4(e0: vec4<i32>, e1: vec4<i32>, w: i32) -> vec4<i32> {\n return ((64 - w) * e0 + w * e1 + vec4<i32>(32)) >> vec4<u32>(6u);\n}\n\nfn to8(v: vec4<f32>) -> vec4<i32> {\n return vec4<i32>(clamp(floor(v * 255.0 + 0.5), vec4<f32>(0.0), vec4<f32>(255.0)));\n}\n\nfn dist2(a: vec4<i32>, b: vec4<i32>) -> i32 {\n let d = a - b;\n let e = d * d;\n return e.x + e.y + e.z + e.w;\n}\n\n// Quantize an 8-bit ideal endpoint to (7-bit value, reconstructed 8-bit) under\n// a fixed p-bit, all four channels at once. q7 = round((ideal8 \u2212 p)/2); used by\n// both paths.\nstruct QuantPair { seven: vec4<i32>, eight: vec4<i32> };\nfn quantize_endpoint(ideal8: vec4<i32>, p: u32) -> QuantPair {\n let q = vec4<i32>(clamp(\n floor((vec4<f32>(ideal8) - f32(p)) / 2.0 + 0.5),\n vec4<f32>(0.0), vec4<f32>(127.0),\n ));\n let eff = (q << vec4<u32>(1u)) | vec4<i32>(i32(p));\n return QuantPair(q, eff);\n}\n\n// ============================ FAST PATH ================================ //\n\n// Endpoint with its chosen p-bit, picked by minimum quantisation error.\nstruct Ep { seven: vec4<i32>, eight: vec4<i32>, p: u32 };\nfn pick_ep(ideal: vec4<i32>) -> Ep {\n let a = quantize_endpoint(ideal, 0u);\n let b = quantize_endpoint(ideal, 1u);\n if (dist2(b.eight, ideal) < dist2(a.eight, ideal)) { return Ep(b.seven, b.eight, 1u); }\n return Ep(a.seven, a.eight, 0u);\n}\n\n// One pass over the block: project every pixel onto the e0\u2192e1 line and\n// accumulate the least-squares normal-equation sums; solve for the refit\n// endpoints (in 8-bit space). Indices are not produced here \u2014 the caller\n// reprojects against the quantised refit endpoints anyway.\nstruct Fit { e0: vec4<i32>, e1: vec4<i32>, valid: bool };\nfn proj_fit(pixels: ptr<function, array<vec4<i32>, 16>>, e0: vec4<i32>, e1: vec4<i32>) -> Fit {\n var out: Fit;\n out.valid = false;\n let dir = vec4<f32>(e1 - e0);\n let dd = dot(dir, dir);\n if (dd == 0.0) { return out; }\n let e0f = vec4<f32>(e0);\n let inv = 15.0 / dd;\n var sAA: f32 = 0.0; var sBB: f32 = 0.0; var sAB: f32 = 0.0;\n var sAV: vec4<f32> = vec4<f32>(0.0); var sBV: vec4<f32> = vec4<f32>(0.0);\n var s_min = 15.0; var s_max = 0.0;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let v = vec4<f32>((*pixels)[k]);\n let s = clamp(floor(dot(v - e0f, dir) * inv + 0.5), 0.0, 15.0);\n s_min = min(s_min, s); s_max = max(s_max, s);\n let b = s * (1.0 / 15.0); let a = 1.0 - b;\n sAA = sAA + a * a; sBB = sBB + b * b; sAB = sAB + a * b;\n sAV = sAV + a * v; sBV = sBV + b * v;\n }\n // Rank-1 guard: if every pixel projects to ONE level the system is\n // singular \u2014 det and the numerators are pure float rounding noise and the\n // solve returns garbage endpoints. With \u22652 levels det \u2265 15/225 \u2248 0.067.\n if (s_min == s_max) { return out; }\n let det = sAA * sBB - sAB * sAB;\n if (abs(det) < 1e-3) { return out; }\n out.e0 = vec4<i32>(clamp(round((sBB * sAV - sAB * sBV) / det), vec4<f32>(0.0), vec4<f32>(255.0)));\n out.e1 = vec4<i32>(clamp(round((sAA * sBV - sAB * sAV) / det), vec4<f32>(0.0), vec4<f32>(255.0)));\n out.valid = true;\n return out;\n}\n\n// ============================ HIGH PATH ================================ //\n\nstruct Pair { a: vec4<i32>, b: vec4<i32> };\nfn farthest_pair(pixels: ptr<function, array<vec4<i32>, 16>>) -> Pair {\n var best_d: i32 = 0;\n var pa = (*pixels)[0];\n var pb = (*pixels)[1];\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let xi = (*pixels)[i];\n for (var j: u32 = i + 1u; j < 16u; j = j + 1u) {\n let d = dist2(xi, (*pixels)[j]);\n if (d > best_d) { best_d = d; pa = xi; pb = (*pixels)[j]; }\n }\n }\n return Pair(pa, pb);\n}\n\nfn build_palette_6(e0: vec4<i32>, e1: vec4<i32>, pal: ptr<function, array<vec4<i32>, 16>>) {\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n (*pal)[i] = interp4(e0, e1, i32(w4(i)));\n }\n}\n\nfn assign_all(\n pixels: ptr<function, array<vec4<i32>, 16>>,\n pal: ptr<function, array<vec4<i32>, 16>>,\n out_idx: ptr<function, array<u32, 16>>,\n) -> i32 {\n var err: i32 = 0;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let px = (*pixels)[k];\n var best_i: u32 = 0u;\n var best_d: i32 = 2147483647;\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let d = dist2(px, (*pal)[i]);\n if (d < best_d) { best_d = d; best_i = i; }\n }\n (*out_idx)[k] = best_i;\n err = err + best_d;\n }\n return err;\n}\n\nstruct BestMode6 {\n e0_7: vec4<i32>, e1_7: vec4<i32>,\n p0: u32, p1: u32,\n indices: array<u32, 16>,\n err: i32,\n};\n\n// Exhaustive p-bit search (high path); commits to `*best` only on improvement.\nfn try_pbit_combos(\n pixels: ptr<function, array<vec4<i32>, 16>>,\n ideal0: vec4<i32>,\n ideal1: vec4<i32>,\n best: ptr<function, BestMode6>,\n) {\n var local_best = (*best).err;\n var pal: array<vec4<i32>, 16>;\n var tmp: array<u32, 16>;\n for (var p0: u32 = 0u; p0 < 2u; p0 = p0 + 1u) {\n let q0 = quantize_endpoint(ideal0, p0);\n for (var p1: u32 = 0u; p1 < 2u; p1 = p1 + 1u) {\n let q1 = quantize_endpoint(ideal1, p1);\n build_palette_6(q0.eight, q1.eight, &pal);\n let err = assign_all(pixels, &pal, &tmp);\n if (err < local_best) {\n local_best = err;\n (*best).e0_7 = q0.seven;\n (*best).e1_7 = q1.seven;\n (*best).p0 = p0;\n (*best).p1 = p1;\n (*best).indices = tmp;\n (*best).err = err;\n }\n }\n }\n}\n\n// Exact-weight LSQ refit (high path); matches bc7_ref.ts `refitEndpointsMode6`.\nstruct RefitResult { e0: vec4<i32>, e1: vec4<i32>, valid: bool };\nfn refit_endpoints(\n pixels: ptr<function, array<vec4<i32>, 16>>,\n indices: ptr<function, array<u32, 16>>,\n) -> RefitResult {\n var sAA: f32 = 0.0;\n var sBB: f32 = 0.0;\n var sAB: f32 = 0.0;\n var sAV: vec4<f32> = vec4<f32>(0.0);\n var sBV: vec4<f32> = vec4<f32>(0.0);\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let i = (*indices)[k];\n let a = f32(64u - w4(i)) / 64.0;\n let b = f32(w4(i)) / 64.0;\n let v = vec4<f32>((*pixels)[k]);\n sAA = sAA + a * a;\n sBB = sBB + b * b;\n sAB = sAB + a * b;\n sAV = sAV + a * v;\n sBV = sBV + b * v;\n }\n let det = sAA * sBB - sAB * sAB;\n var out: RefitResult;\n if (abs(det) < 1e-9) {\n out.valid = false;\n return out;\n }\n let e0f = (sBB * sAV - sAB * sBV) / det;\n let e1f = (sAA * sBV - sAB * sAV) / det;\n out.e0 = vec4<i32>(clamp(round(e0f), vec4<f32>(0.0), vec4<f32>(255.0)));\n out.e1 = vec4<i32>(clamp(round(e1f), vec4<f32>(0.0), vec4<f32>(255.0)));\n out.valid = true;\n return out;\n}\n\n// ------------------------------- Entry --------------------------------- //\n\n@compute @workgroup_size(8, 8, 1)\nfn encode(@builtin(global_invocation_id) gid: vec3<u32>) {\n if (gid.x >= params.blocks_x || gid.y >= params.blocks_y) {\n return;\n }\n\n let bx = gid.x;\n let by = gid.y;\n let block_index = by * params.blocks_x + bx;\n\n let base = vec2<i32>(i32(bx) * 4, i32(by) * 4);\n let max_xy = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);\n\n // Load 16 RGBA pixels (8-bit integer domain) and the per-channel bbox.\n var pixels: array<vec4<i32>, 16>;\n var lo = vec4<i32>(255);\n var hi = vec4<i32>(0);\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let lx = i32(i & 3u);\n let ly = i32(i >> 2u);\n let p = clamp(base + vec2<i32>(lx, ly), vec2<i32>(0, 0), max_xy);\n let px = to8(textureLoad(src_tex, p, 0));\n pixels[i] = px;\n lo = min(lo, px);\n hi = max(hi, px);\n }\n\n // Both branches produce: 7-bit endpoints + p-bits, and the 16 4-bit indices\n // packed LSB-first into two nibble words (pixel k \u2192 bits 4k..4k+3).\n var e0_7: vec4<i32>;\n var e1_7: vec4<i32>;\n var p0: u32;\n var p1: u32;\n var ilo: u32 = 0u;\n var ihi: u32 = 0u;\n\n if (QUALITY_HIGH != 0u) {\n let fp = farthest_pair(&pixels);\n var best: BestMode6;\n best.err = 2147483647;\n try_pbit_combos(&pixels, fp.a, fp.b, &best);\n let refit = refit_endpoints(&pixels, &best.indices);\n if (refit.valid) {\n try_pbit_combos(&pixels, refit.e0, refit.e1, &best);\n }\n e0_7 = best.e0_7; e1_7 = best.e1_7; p0 = best.p0; p1 = best.p1;\n for (var k: u32 = 0u; k < 8u; k = k + 1u) {\n ilo = ilo | (best.indices[k] << (k * 4u));\n }\n for (var k: u32 = 8u; k < 16u; k = k + 1u) {\n ihi = ihi | (best.indices[k] << ((k - 8u) * 4u));\n }\n } else {\n // Seed the fused LSQ fit from the raw bbox, then quantise the refit\n // endpoints and reproject for the final indices.\n let r = proj_fit(&pixels, lo, hi);\n var ep0: Ep;\n var ep1: Ep;\n if (r.valid) { ep0 = pick_ep(r.e0); ep1 = pick_ep(r.e1); }\n else { ep0 = pick_ep(lo); ep1 = pick_ep(hi); }\n let dir = vec4<f32>(ep1.eight - ep0.eight);\n let dd = dot(dir, dir);\n if (dd > 0.0) {\n let e0f = vec4<f32>(ep0.eight);\n let inv = 15.0 / dd;\n for (var k: u32 = 0u; k < 8u; k = k + 1u) {\n let s = clamp(floor(dot(vec4<f32>(pixels[k]) - e0f, dir) * inv + 0.5), 0.0, 15.0);\n ilo = ilo | (u32(s) << (k * 4u));\n }\n for (var k: u32 = 8u; k < 16u; k = k + 1u) {\n let s = clamp(floor(dot(vec4<f32>(pixels[k]) - e0f, dir) * inv + 0.5), 0.0, 15.0);\n ihi = ihi | (u32(s) << ((k - 8u) * 4u));\n }\n }\n e0_7 = ep0.seven; e1_7 = ep1.seven; p0 = ep0.p; p1 = ep1.p;\n }\n\n // Anchor rule \u2014 pixel 0's index MSB must be 0. Swapping endpoints reflects\n // every index (i \u2192 15\u2212i), which on packed nibbles is a bitwise NOT.\n if ((ilo & 0x8u) != 0u) {\n let t7 = e0_7; e0_7 = e1_7; e1_7 = t7;\n let tp = p0; p0 = p1; p1 = tp;\n ilo = ~ilo; ihi = ~ihi;\n }\n\n // Straight-line mode-6 packing (see layout at the top of the file).\n let e0 = vec4<u32>(e0_7);\n let e1 = vec4<u32>(e1_7);\n let w0 = 0x40u | (e0.x << 7u) | (e1.x << 14u) | (e0.y << 21u) | (e1.y << 28u);\n let w1 = (e1.y >> 4u) | (e0.z << 3u) | (e1.z << 10u) | (e0.w << 17u) | (e1.w << 24u) | (p0 << 31u);\n let w2 = p1 | ((ilo & 0x7u) << 1u) | (ilo & 0xFFFFFFF0u);\n let w3 = ihi;\n\n let out = block_index * 4u;\n dst[out + 0u] = w0;\n dst[out + 1u] = w1;\n dst[out + 2u] = w2;\n dst[out + 3u] = w3;\n}\n";
|
|
636
636
|
|
|
637
637
|
// src/bc7_fast_f16.wgsl
|
|
638
|
-
var bc7_fast_f16_default =
|
|
638
|
+
var bc7_fast_f16_default = `// bc7 "fast" encoder \u2014 f16 variant (requires the shader-f16 feature).
|
|
639
|
+
// Same algorithm family as the f32 fast path in bc7.wgsl (bbox seed \u2192
|
|
640
|
+
// projection-based index assignment with a fused least-squares refit \u2192
|
|
641
|
+
// reproject), tuned for throughput:
|
|
642
|
+
//
|
|
643
|
+
// \u2022 All projection / refit math in f16 ([0,1] domain). ~2\xD7 ALU throughput
|
|
644
|
+
// on f16-capable GPUs. The projection direction is pre-scaled by 32:
|
|
645
|
+
// a shallow block (endpoints ~1/255 apart) has dd = dot(dir,dir) \u2248 1.5e-5,
|
|
646
|
+
// where 15/dd \u2248 10\u2076 overflows f16 (max 65504) to +inf and the products
|
|
647
|
+
// inside the projection dot are subnormal \u2014 the indices and the LSQ refit
|
|
648
|
+
// feeding on them turn to garbage (visible as banding on smooth
|
|
649
|
+
// gradients). Scaling dir by 32 multiplies the dots by 32 and dd by 1024;
|
|
650
|
+
// s = dot\xB7(32\xB715/dd\u2083\u2082) is the same quantity with every intermediate in
|
|
651
|
+
// f16's normal range (worst case inv = 480/0.0157 \u2248 3.0e4 < 65504).
|
|
652
|
+
// \u2022 The LSQ seed pass projects against the RAW bbox endpoints \u2014 quantising
|
|
653
|
+
// the seed first (pick_ep) costs two extra quantisation searches and
|
|
654
|
+
// doesn't measurably change where the refit lands.
|
|
655
|
+
// \u2022 Indices are packed into two u32 nibble words ON THE FLY during the
|
|
656
|
+
// final projection pass \u2014 no array<u32,16> private array. The BC7 anchor
|
|
657
|
+
// reflection (i \u2192 15\u2212i) is then just a bitwise NOT of both words.
|
|
658
|
+
// \u2022 The 128-bit block is assembled with straight-line constant shifts
|
|
659
|
+
// instead of a generic write_bits() helper (whose dynamic word indexing
|
|
660
|
+
// defeats register promotion of the output array).
|
|
661
|
+
//
|
|
662
|
+
// The host selects this module only when the device reports shader-f16,
|
|
663
|
+
// falling back to bc7.wgsl otherwise. "high" never uses this.
|
|
664
|
+
//
|
|
665
|
+
// MODE 6 BIT LAYOUT (LSB-first): see bc7.wgsl. Summary:
|
|
666
|
+
// w0: mode(7 bits, 0x40) R0 R1 G0 G1[3:0]
|
|
667
|
+
// w1: G1[6:4] B0 B1 A0 A1 P0
|
|
668
|
+
// w2: P1, pixel0 index (3 bits), pixels 1..7 (4 bits each)
|
|
669
|
+
// w3: pixels 8..15 (4 bits each)
|
|
670
|
+
enable f16;
|
|
671
|
+
struct Params { blocks_x: u32, blocks_y: u32, width: u32, height: u32, };
|
|
672
|
+
@group(0) @binding(0) var src_tex: texture_2d<f32>;
|
|
673
|
+
@group(0) @binding(1) var<storage, read_write> dst: array<u32>;
|
|
674
|
+
@group(0) @binding(2) var<uniform> params: Params;
|
|
675
|
+
alias h = f16;
|
|
676
|
+
alias h4 = vec4<f16>;
|
|
677
|
+
|
|
678
|
+
// Quantise an ideal endpoint (h4 in [0,1]) to 7-bit + p-bit, choosing the
|
|
679
|
+
// p-bit with the lower quantisation error. \`eight\` is the decoded value the
|
|
680
|
+
// hardware will interpolate with, back in [0,1].
|
|
681
|
+
struct Ep { seven: vec4<u32>, eight: h4, p: u32 };
|
|
682
|
+
fn pick_ep(ideal01: h4) -> Ep {
|
|
683
|
+
let ideal = ideal01 * h(255.0);
|
|
684
|
+
let q0 = clamp(floor(ideal * h(0.5) + h(0.5)), h4(0.0), h4(127.0)); // p=0
|
|
685
|
+
let e0 = q0 * h(2.0);
|
|
686
|
+
let q1 = clamp(floor((ideal - h(1.0)) * h(0.5) + h(0.5)), h4(0.0), h4(127.0)); // p=1
|
|
687
|
+
let e1 = q1 * h(2.0) + h(1.0);
|
|
688
|
+
let d0 = e0 - ideal; let d1 = e1 - ideal;
|
|
689
|
+
if (dot(d1, d1) < dot(d0, d0)) { return Ep(vec4<u32>(q1), e1 * h(1.0 / 255.0), 1u); }
|
|
690
|
+
return Ep(vec4<u32>(q0), e0 * h(1.0 / 255.0), 0u);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// One pass over the block: project every pixel onto the e0\u2192e1 line and
|
|
694
|
+
// accumulate the least-squares normal-equation sums; solve for the refit
|
|
695
|
+
// endpoints. Indices are NOT produced here \u2014 the caller reprojects against
|
|
696
|
+
// the quantised refit endpoints anyway.
|
|
697
|
+
//
|
|
698
|
+
// The value sums accumulate v \u2212 e0, not v: the basis is affine (a + b = 1),
|
|
699
|
+
// so fitting the shifted data and adding e0 back is the same fit, but the
|
|
700
|
+
// accumulators scale with the block's span instead of its absolute level \u2014
|
|
701
|
+
// on a shallow dark block, f16 rounding of absolute sums (ulp \u2248 0.12 of an
|
|
702
|
+
// 8-bit level per add) drifts the refit endpoints by \xB11 level.
|
|
703
|
+
struct Fit { e0: h4, e1: h4, valid: bool };
|
|
704
|
+
fn proj_fit(pix: ptr<function, array<h4, 16>>, e0: h4, e1: h4) -> Fit {
|
|
705
|
+
var out: Fit;
|
|
706
|
+
out.valid = false;
|
|
707
|
+
// dir pre-scaled by 32 to keep dd and the projection dots in f16's normal
|
|
708
|
+
// range (see header). Spans below ~0.7 of an 8-bit step (dd\u2083\u2082 < 0.008,
|
|
709
|
+
// possible only for non-8-bit sources) are treated as flat \u2014 encoding them
|
|
710
|
+
// flat is under half a level of error, while running the math on them risks
|
|
711
|
+
// inv overflowing to +inf.
|
|
712
|
+
let dir = (e1 - e0) * h(32.0);
|
|
713
|
+
let dd = dot(dir, dir);
|
|
714
|
+
if (dd < h(0.008)) { return out; }
|
|
715
|
+
let inv = h(480.0) / dd; // 32\xB715/dd\u2083\u2082 \u2261 15/dd
|
|
716
|
+
var sAA = h(0.0); var sBB = h(0.0); var sAB = h(0.0);
|
|
717
|
+
var sAV = h4(0.0); var sBV = h4(0.0);
|
|
718
|
+
var s_min = h(15.0); var s_max = h(0.0);
|
|
719
|
+
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
720
|
+
let vr = (*pix)[k] - e0;
|
|
721
|
+
let s = clamp(floor(dot(vr, dir) * inv + h(0.5)), h(0.0), h(15.0));
|
|
722
|
+
s_min = min(s_min, s); s_max = max(s_max, s);
|
|
723
|
+
let b = s * h(1.0 / 15.0); let a = h(1.0) - b;
|
|
724
|
+
sAA = sAA + a * a; sBB = sBB + b * b; sAB = sAB + a * b;
|
|
725
|
+
sAV = sAV + a * vr; sBV = sBV + b * vr;
|
|
726
|
+
}
|
|
727
|
+
// Rank-1 guard: if every pixel projects to ONE level the system is
|
|
728
|
+
// singular \u2014 det/numerators are pure f16 rounding noise and the solve
|
|
729
|
+
// returns garbage endpoints. With \u22652 distinct levels
|
|
730
|
+
// det = \u03A3_i<j (b_j \u2212 b_i)\xB2 \u2265 15/225 \u2248 0.067, so 0.02 is a safe floor.
|
|
731
|
+
if (s_min == s_max) { return out; }
|
|
732
|
+
let det = sAA * sBB - sAB * sAB;
|
|
733
|
+
if (abs(det) < h(0.02)) { return out; }
|
|
734
|
+
out.e0 = clamp(e0 + (sBB * sAV - sAB * sBV) / det, h4(0.0), h4(1.0));
|
|
735
|
+
out.e1 = clamp(e0 + (sAA * sBV - sAB * sAV) / det, h4(0.0), h4(1.0));
|
|
736
|
+
out.valid = true;
|
|
737
|
+
return out;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
@compute @workgroup_size(8, 8, 1)
|
|
741
|
+
fn encode(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
742
|
+
if (gid.x >= params.blocks_x || gid.y >= params.blocks_y) { return; }
|
|
743
|
+
let bi = gid.y * params.blocks_x + gid.x;
|
|
744
|
+
let base = vec2<i32>(i32(gid.x) * 4, i32(gid.y) * 4);
|
|
745
|
+
let mx = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);
|
|
746
|
+
|
|
747
|
+
var pix: array<h4, 16>;
|
|
748
|
+
var lo = h4(1.0);
|
|
749
|
+
var hi = h4(0.0);
|
|
750
|
+
for (var i: u32 = 0u; i < 16u; i = i + 1u) {
|
|
751
|
+
let p = clamp(base + vec2<i32>(i32(i & 3u), i32(i >> 2u)), vec2<i32>(0), mx);
|
|
752
|
+
let px = h4(textureLoad(src_tex, p, 0));
|
|
753
|
+
pix[i] = px; lo = min(lo, px); hi = max(hi, px);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Seed fit from the raw bbox, then quantise the refit endpoints.
|
|
757
|
+
let r = proj_fit(&pix, lo, hi);
|
|
758
|
+
var ep0: Ep;
|
|
759
|
+
var ep1: Ep;
|
|
760
|
+
if (r.valid) { ep0 = pick_ep(r.e0); ep1 = pick_ep(r.e1); }
|
|
761
|
+
else { ep0 = pick_ep(lo); ep1 = pick_ep(hi); }
|
|
762
|
+
|
|
763
|
+
// Final projection against the decoded endpoints, packing the 4-bit indices
|
|
764
|
+
// into two nibble words as we go (pixel k \u2192 bits 4k..4k+3 of ilo/ihi).
|
|
765
|
+
var ilo: u32 = 0u;
|
|
766
|
+
var ihi: u32 = 0u;
|
|
767
|
+
// Same \xD732 pre-scale as proj_fit; distinct quantised endpoints are \u22651/255
|
|
768
|
+
// apart, i.e. dd\u2083\u2082 \u2265 0.0157, so the flat-block threshold only catches
|
|
769
|
+
// truly identical endpoints.
|
|
770
|
+
let dir = (ep1.eight - ep0.eight) * h(32.0);
|
|
771
|
+
let dd = dot(dir, dir);
|
|
772
|
+
if (dd >= h(0.008)) {
|
|
773
|
+
let inv = h(480.0) / dd;
|
|
774
|
+
for (var k: u32 = 0u; k < 8u; k = k + 1u) {
|
|
775
|
+
let s = clamp(floor(dot(pix[k] - ep0.eight, dir) * inv + h(0.5)), h(0.0), h(15.0));
|
|
776
|
+
ilo = ilo | (u32(s) << (k * 4u));
|
|
777
|
+
}
|
|
778
|
+
for (var k: u32 = 8u; k < 16u; k = k + 1u) {
|
|
779
|
+
let s = clamp(floor(dot(pix[k] - ep0.eight, dir) * inv + h(0.5)), h(0.0), h(15.0));
|
|
780
|
+
ihi = ihi | (u32(s) << ((k - 8u) * 4u));
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Anchor rule \u2014 pixel 0's index MSB must be 0. Swapping endpoints reflects
|
|
785
|
+
// every index (i \u2192 15\u2212i), which on packed nibbles is a bitwise NOT.
|
|
786
|
+
if ((ilo & 0x8u) != 0u) {
|
|
787
|
+
let t = ep0; ep0 = ep1; ep1 = t;
|
|
788
|
+
ilo = ~ilo; ihi = ~ihi;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Straight-line mode-6 packing (see layout above).
|
|
792
|
+
let e0 = ep0.seven;
|
|
793
|
+
let e1 = ep1.seven;
|
|
794
|
+
let w0 = 0x40u | (e0.x << 7u) | (e1.x << 14u) | (e0.y << 21u) | (e1.y << 28u);
|
|
795
|
+
let w1 = (e1.y >> 4u) | (e0.z << 3u) | (e1.z << 10u) | (e0.w << 17u) | (e1.w << 24u) | (ep0.p << 31u);
|
|
796
|
+
let w2 = ep1.p | ((ilo & 0x7u) << 1u) | (ilo & 0xFFFFFFF0u);
|
|
797
|
+
let w3 = ihi;
|
|
798
|
+
|
|
799
|
+
let o = bi * 4u;
|
|
800
|
+
dst[o] = w0; dst[o + 1u] = w1; dst[o + 2u] = w2; dst[o + 3u] = w3;
|
|
801
|
+
}
|
|
802
|
+
`;
|
|
639
803
|
|
|
640
804
|
// src/BC7Encoder.ts
|
|
641
805
|
var BC7Encoder = class extends Encoder {
|
|
@@ -673,7 +837,13 @@ var astc4x4_fast_f16_default = `// astc4x4 "fast" encoder \u2014 f16 variant (re
|
|
|
673
837
|
// projection weight assignment with a fused least-squares refit \u2192
|
|
674
838
|
// reproject), tuned for throughput:
|
|
675
839
|
//
|
|
676
|
-
// \u2022 All projection / refit math in f16 ([0,1] domain).
|
|
840
|
+
// \u2022 All projection / refit math in f16 ([0,1] domain). The projection
|
|
841
|
+
// direction is pre-scaled by 32 \u2014 a shallow block (endpoints ~1/255
|
|
842
|
+
// apart) has dd \u2248 1.5e-5, where 3/dd \u2248 2e5 overflows f16 (max 65504) to
|
|
843
|
+
// +inf and the projection dots go subnormal, turning weights and the LSQ
|
|
844
|
+
// refit to garbage (banding on smooth gradients). Scaling dir by 32 puts
|
|
845
|
+
// every intermediate in f16's normal range; s = dot\xB7(32\xB73/dd\u2083\u2082) is the
|
|
846
|
+
// same quantity (worst case inv = 96/0.0157 \u2248 6.1e3).
|
|
677
847
|
// \u2022 The seed pass only accumulates the LSQ sums (no weight output) \u2014 the
|
|
678
848
|
// final weights come from reprojecting against the refit endpoints.
|
|
679
849
|
// \u2022 Endpoint ordering (the blue-contraction rule: sum(e0.rgb) must not
|
|
@@ -700,20 +870,27 @@ struct Fit { e0: h4, e1: h4, valid: bool };
|
|
|
700
870
|
fn proj_fit(pix: ptr<function, array<h4, 16>>, e0: h4, e1: h4) -> Fit {
|
|
701
871
|
var out: Fit;
|
|
702
872
|
out.valid = false;
|
|
703
|
-
|
|
873
|
+
// dir pre-scaled by 32 to keep dd and the projection dots in f16's normal
|
|
874
|
+
// range (see header). Spans below ~0.7 of an 8-bit step (dd\u2083\u2082 < 0.008,
|
|
875
|
+
// possible only for non-8-bit sources) are treated as flat.
|
|
876
|
+
let dir = (e1 - e0) * h(32.0);
|
|
704
877
|
let dd = dot(dir, dir);
|
|
705
|
-
if (dd
|
|
706
|
-
let inv = h(
|
|
878
|
+
if (dd < h(0.008)) { return out; }
|
|
879
|
+
let inv = h(96.0) / dd; // 32\xB73/dd\u2083\u2082 \u2261 3/dd
|
|
707
880
|
var sAA = h(0.0); var sBB = h(0.0); var sAB = h(0.0);
|
|
708
881
|
var sAV = h4(0.0); var sBV = h4(0.0);
|
|
709
882
|
var s_min = h(3.0); var s_max = h(0.0);
|
|
883
|
+
// Value sums accumulate v \u2212 e0 (basis is affine, a + b = 1, so the fit
|
|
884
|
+
// commutes with the shift): accumulators scale with the block span, keeping
|
|
885
|
+
// f16 rounding a fraction of the span instead of \xB11 level at high absolute
|
|
886
|
+
// values.
|
|
710
887
|
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
711
|
-
let
|
|
712
|
-
let s = clamp(floor(dot(
|
|
888
|
+
let vr = (*pix)[k] - e0;
|
|
889
|
+
let s = clamp(floor(dot(vr, dir) * inv + h(0.5)), h(0.0), h(3.0));
|
|
713
890
|
s_min = min(s_min, s); s_max = max(s_max, s);
|
|
714
891
|
let b = s * h(1.0 / 3.0); let a = h(1.0) - b;
|
|
715
892
|
sAA = sAA + a * a; sBB = sBB + b * b; sAB = sAB + a * b;
|
|
716
|
-
sAV = sAV + a *
|
|
893
|
+
sAV = sAV + a * vr; sBV = sBV + b * vr;
|
|
717
894
|
}
|
|
718
895
|
// Rank-1 guard: if every pixel projects to ONE level the system is
|
|
719
896
|
// singular \u2014 det/numerators are pure f16 rounding noise and the solve
|
|
@@ -722,8 +899,8 @@ fn proj_fit(pix: ptr<function, array<h4, 16>>, e0: h4, e1: h4) -> Fit {
|
|
|
722
899
|
if (s_min == s_max) { return out; }
|
|
723
900
|
let det = sAA * sBB - sAB * sAB;
|
|
724
901
|
if (abs(det) < h(0.5)) { return out; }
|
|
725
|
-
out.e0 = clamp((sBB * sAV - sAB * sBV) / det, h4(0.0), h4(1.0));
|
|
726
|
-
out.e1 = clamp((sAA * sBV - sAB * sAV) / det, h4(0.0), h4(1.0));
|
|
902
|
+
out.e0 = clamp(e0 + (sBB * sAV - sAB * sBV) / det, h4(0.0), h4(1.0));
|
|
903
|
+
out.e1 = clamp(e0 + (sAA * sBV - sAB * sAV) / det, h4(0.0), h4(1.0));
|
|
727
904
|
out.valid = true;
|
|
728
905
|
return out;
|
|
729
906
|
}
|
|
@@ -766,10 +943,12 @@ fn encode(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
|
766
943
|
// Weight pass, packing on the fly: weight k's lsb at bit 31\u22122k of the last
|
|
767
944
|
// word, msb at bit 30\u22122k.
|
|
768
945
|
var w3: u32 = 0u;
|
|
769
|
-
|
|
946
|
+
// Same \xD732 pre-scale as proj_fit; distinct 8-bit endpoints are \u22651/255
|
|
947
|
+
// apart (dd\u2083\u2082 \u2265 0.0157), so the threshold only catches identical ones.
|
|
948
|
+
let dir = (d1 - d0) * h(32.0);
|
|
770
949
|
let dd = dot(dir, dir);
|
|
771
|
-
if (dd
|
|
772
|
-
let inv = h(
|
|
950
|
+
if (dd >= h(0.008)) {
|
|
951
|
+
let inv = h(96.0) / dd;
|
|
773
952
|
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
774
953
|
let s = u32(clamp(floor(dot(pix[k] - d0, dir) * inv + h(0.5)), h(0.0), h(3.0)));
|
|
775
954
|
w3 = w3 | ((s & 1u) << (31u - 2u * k)) | (((s >> 1u) & 1u) << (30u - 2u * k));
|
|
@@ -1118,13 +1297,26 @@ function detectWebGLCapabilities(gl) {
|
|
|
1118
1297
|
// src/webgl/selectWebGLFormat.ts
|
|
1119
1298
|
var NONE = { format: null, encoderClass: null, astcNormalRemap: false };
|
|
1120
1299
|
function selectWebGLFormat(caps, hint, options = {}) {
|
|
1121
|
-
const { colorSpace = "srgb" } = options;
|
|
1300
|
+
const { colorSpace = "srgb", preferredFormat } = options;
|
|
1122
1301
|
const srgb = colorSpace === "srgb";
|
|
1123
1302
|
const astc = (astcNormalRemap) => ({
|
|
1124
1303
|
format: srgb ? TextureFormat.ASTC_4x4_SRGB : TextureFormat.ASTC_4x4,
|
|
1125
1304
|
encoderClass: ASTC4x4WebGLEncoder,
|
|
1126
1305
|
astcNormalRemap
|
|
1127
1306
|
});
|
|
1307
|
+
if (preferredFormat === "bc1") {
|
|
1308
|
+
if (hint !== "color") {
|
|
1309
|
+
console.warn(
|
|
1310
|
+
`[gputex] preferredFormat 'bc1' ignored for hint '${hint}' \u2014 BC1 has no real alpha channel and is unsuitable for normal maps.`
|
|
1311
|
+
);
|
|
1312
|
+
} else if (srgb ? caps.s3tcSrgb : caps.s3tc) {
|
|
1313
|
+
return {
|
|
1314
|
+
format: srgb ? TextureFormat.BC1_SRGB : TextureFormat.BC1,
|
|
1315
|
+
encoderClass: BC1WebGLEncoder,
|
|
1316
|
+
astcNormalRemap: false
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1128
1320
|
if (hint === "normal") {
|
|
1129
1321
|
if (caps.rgtc) return { format: TextureFormat.BC5, encoderClass: BC5WebGLEncoder, astcNormalRemap: false };
|
|
1130
1322
|
if (caps.astc) return astc(true);
|
|
@@ -1151,9 +1343,22 @@ function selectWebGLFormat(caps, hint, options = {}) {
|
|
|
1151
1343
|
|
|
1152
1344
|
// src/selectFormat.ts
|
|
1153
1345
|
function selectFormat(adapter, hint, options = {}) {
|
|
1154
|
-
const { colorSpace = "srgb" } = options;
|
|
1346
|
+
const { colorSpace = "srgb", preferredFormat } = options;
|
|
1155
1347
|
const srgb = colorSpace === "srgb";
|
|
1156
1348
|
const caps = detectCapabilities(adapter);
|
|
1349
|
+
if (preferredFormat === "bc1") {
|
|
1350
|
+
if (hint !== "color") {
|
|
1351
|
+
console.warn(
|
|
1352
|
+
`[gputex] preferredFormat 'bc1' ignored for hint '${hint}' \u2014 BC1 has no real alpha channel and is unsuitable for normal maps.`
|
|
1353
|
+
);
|
|
1354
|
+
} else if (caps.bc) {
|
|
1355
|
+
return {
|
|
1356
|
+
format: srgb ? TextureFormat.BC1_SRGB : TextureFormat.BC1,
|
|
1357
|
+
encoderClass: BC1Encoder,
|
|
1358
|
+
astcNormalRemap: false
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1157
1362
|
if (caps.bc) {
|
|
1158
1363
|
if (hint === "normal") {
|
|
1159
1364
|
return { format: TextureFormat.BC5, encoderClass: BC5Encoder, astcNormalRemap: false };
|
|
@@ -1241,6 +1446,140 @@ function padToBlockMultiple(level) {
|
|
|
1241
1446
|
return { data: out, width: pw, height: ph };
|
|
1242
1447
|
}
|
|
1243
1448
|
|
|
1449
|
+
// src/svg.ts
|
|
1450
|
+
function isSvgMarkup(source) {
|
|
1451
|
+
return source.trimStart().startsWith("<");
|
|
1452
|
+
}
|
|
1453
|
+
function hasSvgExtension(url) {
|
|
1454
|
+
return /\.svg$/i.test(url.split(/[?#]/, 1)[0]);
|
|
1455
|
+
}
|
|
1456
|
+
function isSvgBlob(blob) {
|
|
1457
|
+
if (blob.type) {
|
|
1458
|
+
return blob.type.split(";", 1)[0].trim().toLowerCase() === "image/svg+xml";
|
|
1459
|
+
}
|
|
1460
|
+
return typeof File !== "undefined" && blob instanceof File && hasSvgExtension(blob.name);
|
|
1461
|
+
}
|
|
1462
|
+
var ROOT_TAG_RE = /<svg(?=[\s/>])[^>]*>/;
|
|
1463
|
+
function getAttr(tag, name) {
|
|
1464
|
+
const m = new RegExp(`\\s${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`).exec(tag);
|
|
1465
|
+
return m ? m[1] ?? m[2] ?? "" : null;
|
|
1466
|
+
}
|
|
1467
|
+
function removeAttr(tag, name) {
|
|
1468
|
+
return tag.replace(new RegExp(`\\s${name}\\s*=\\s*(?:"[^"]*"|'[^']*')`, "g"), "");
|
|
1469
|
+
}
|
|
1470
|
+
function parseAbsoluteLength(value) {
|
|
1471
|
+
if (value == null) return null;
|
|
1472
|
+
const m = /^\s*\+?(\d+(?:\.\d+)?|\.\d+)(?:px)?\s*$/i.exec(value);
|
|
1473
|
+
if (!m) return null;
|
|
1474
|
+
const n = Number(m[1]);
|
|
1475
|
+
return n > 0 ? n : null;
|
|
1476
|
+
}
|
|
1477
|
+
function parseSvgDimensions(svgText) {
|
|
1478
|
+
const m = ROOT_TAG_RE.exec(svgText);
|
|
1479
|
+
if (!m) return null;
|
|
1480
|
+
const tag = m[0];
|
|
1481
|
+
const dims = {
|
|
1482
|
+
width: parseAbsoluteLength(getAttr(tag, "width")),
|
|
1483
|
+
height: parseAbsoluteLength(getAttr(tag, "height")),
|
|
1484
|
+
viewBoxWidth: null,
|
|
1485
|
+
viewBoxHeight: null
|
|
1486
|
+
};
|
|
1487
|
+
const viewBox = getAttr(tag, "viewBox");
|
|
1488
|
+
if (viewBox) {
|
|
1489
|
+
const parts = viewBox.trim().split(/[\s,]+/).map(Number);
|
|
1490
|
+
if (parts.length === 4 && parts.every(Number.isFinite) && parts[2] > 0 && parts[3] > 0) {
|
|
1491
|
+
dims.viewBoxWidth = parts[2];
|
|
1492
|
+
dims.viewBoxHeight = parts[3];
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
return dims;
|
|
1496
|
+
}
|
|
1497
|
+
function resolveSvgRasterSize(dims, size) {
|
|
1498
|
+
if (size !== void 0 && typeof size === "object") {
|
|
1499
|
+
const width = Math.round(size.width);
|
|
1500
|
+
const height = Math.round(size.height);
|
|
1501
|
+
if (!(width >= 1) || !(height >= 1)) {
|
|
1502
|
+
throw new Error(`rasterizeSvg: svgSize must be \u22651\xD71 (got ${size.width}\xD7${size.height})`);
|
|
1503
|
+
}
|
|
1504
|
+
return { width, height };
|
|
1505
|
+
}
|
|
1506
|
+
let w = dims.width;
|
|
1507
|
+
let h = dims.height;
|
|
1508
|
+
if (dims.viewBoxWidth != null && dims.viewBoxHeight != null) {
|
|
1509
|
+
if (w == null && h != null) w = h * dims.viewBoxWidth / dims.viewBoxHeight;
|
|
1510
|
+
else if (h == null && w != null) h = w * dims.viewBoxHeight / dims.viewBoxWidth;
|
|
1511
|
+
else if (w == null && h == null) {
|
|
1512
|
+
w = dims.viewBoxWidth;
|
|
1513
|
+
h = dims.viewBoxHeight;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
if (typeof size === "number") {
|
|
1517
|
+
if (!(size >= 1)) {
|
|
1518
|
+
throw new Error(`rasterizeSvg: svgSize must be \u22651 (got ${size})`);
|
|
1519
|
+
}
|
|
1520
|
+
const aspect = w != null && h != null ? w / h : 1;
|
|
1521
|
+
return aspect >= 1 ? { width: Math.round(size), height: Math.max(1, Math.round(size / aspect)) } : { width: Math.max(1, Math.round(size * aspect)), height: Math.round(size) };
|
|
1522
|
+
}
|
|
1523
|
+
if (w == null || h == null) {
|
|
1524
|
+
throw new Error(
|
|
1525
|
+
"rasterizeSvg: the SVG has no intrinsic size (no absolute width/height attributes and no viewBox) \u2014 pass svgSize to choose a rasterisation size"
|
|
1526
|
+
);
|
|
1527
|
+
}
|
|
1528
|
+
return { width: Math.max(1, Math.round(w)), height: Math.max(1, Math.round(h)) };
|
|
1529
|
+
}
|
|
1530
|
+
function setSvgRootSize(svgText, width, height) {
|
|
1531
|
+
const m = ROOT_TAG_RE.exec(svgText);
|
|
1532
|
+
if (!m) {
|
|
1533
|
+
throw new Error("rasterizeSvg: no <svg> root element found in source");
|
|
1534
|
+
}
|
|
1535
|
+
let tag = m[0];
|
|
1536
|
+
const origWidth = parseAbsoluteLength(getAttr(tag, "width"));
|
|
1537
|
+
const origHeight = parseAbsoluteLength(getAttr(tag, "height"));
|
|
1538
|
+
const hasViewBox = getAttr(tag, "viewBox") != null;
|
|
1539
|
+
tag = removeAttr(removeAttr(tag, "width"), "height");
|
|
1540
|
+
let inject = ` width="${width}" height="${height}"`;
|
|
1541
|
+
if (!hasViewBox && origWidth != null && origHeight != null) {
|
|
1542
|
+
inject += ` viewBox="0 0 ${origWidth} ${origHeight}"`;
|
|
1543
|
+
}
|
|
1544
|
+
tag = `<svg${inject}${tag.slice("<svg".length)}`;
|
|
1545
|
+
return svgText.slice(0, m.index) + tag + svgText.slice(m.index + m[0].length);
|
|
1546
|
+
}
|
|
1547
|
+
async function rasterizeSvg(source, options = {}) {
|
|
1548
|
+
if (typeof Image === "undefined") {
|
|
1549
|
+
throw new Error(
|
|
1550
|
+
"rasterizeSvg: SVG rasterisation needs a DOM Image element and cannot run in this environment (e.g. a worker)"
|
|
1551
|
+
);
|
|
1552
|
+
}
|
|
1553
|
+
const svgText = typeof source === "string" ? source : await source.text();
|
|
1554
|
+
const dims = parseSvgDimensions(svgText);
|
|
1555
|
+
if (!dims) {
|
|
1556
|
+
throw new Error("rasterizeSvg: no <svg> root element found in source");
|
|
1557
|
+
}
|
|
1558
|
+
const { width, height } = resolveSvgRasterSize(dims, options.size);
|
|
1559
|
+
const sized = setSvgRootSize(svgText, width, height);
|
|
1560
|
+
const url = URL.createObjectURL(new Blob([sized], { type: "image/svg+xml;charset=utf-8" }));
|
|
1561
|
+
try {
|
|
1562
|
+
const img = new Image();
|
|
1563
|
+
img.decoding = "async";
|
|
1564
|
+
await new Promise((resolve, reject) => {
|
|
1565
|
+
img.onload = () => resolve();
|
|
1566
|
+
img.onerror = () => reject(new Error("rasterizeSvg: the browser failed to decode the SVG"));
|
|
1567
|
+
img.src = url;
|
|
1568
|
+
});
|
|
1569
|
+
await img.decode().catch(() => {
|
|
1570
|
+
});
|
|
1571
|
+
const canvas = typeof OffscreenCanvas !== "undefined" ? new OffscreenCanvas(width, height) : Object.assign(document.createElement("canvas"), { width, height });
|
|
1572
|
+
const ctx = canvas.getContext("2d");
|
|
1573
|
+
if (!ctx) {
|
|
1574
|
+
throw new Error("rasterizeSvg: no 2D context available");
|
|
1575
|
+
}
|
|
1576
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
1577
|
+
return await createImageBitmap(canvas, { colorSpaceConversion: "none", premultiplyAlpha: "none" });
|
|
1578
|
+
} finally {
|
|
1579
|
+
URL.revokeObjectURL(url);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1244
1583
|
// src/three/compressTexture.ts
|
|
1245
1584
|
import { LinearFilter as LinearFilter2, LinearSRGBColorSpace as LinearSRGBColorSpace2, RepeatWrapping as RepeatWrapping2, SRGBColorSpace as SRGBColorSpace2, Texture } from "three";
|
|
1246
1585
|
|
|
@@ -1313,27 +1652,49 @@ async function encodeToTexture(encoder, source, { colorSpace = "srgb", quality =
|
|
|
1313
1652
|
}
|
|
1314
1653
|
|
|
1315
1654
|
// src/three/compressTexture.ts
|
|
1316
|
-
async function sourceToBitmap(source) {
|
|
1655
|
+
async function sourceToBitmap(source, svgSize) {
|
|
1317
1656
|
const opts = {
|
|
1318
1657
|
colorSpaceConversion: "none",
|
|
1319
1658
|
premultiplyAlpha: "none"
|
|
1320
1659
|
};
|
|
1321
1660
|
if (typeof source === "string") {
|
|
1661
|
+
if (isSvgMarkup(source)) {
|
|
1662
|
+
return rasterizeSvg(source, { size: svgSize });
|
|
1663
|
+
}
|
|
1322
1664
|
const resp = await fetch(source);
|
|
1323
1665
|
if (!resp.ok) {
|
|
1324
1666
|
throw new Error(`compressTexture: fetch ${source} failed (${resp.status})`);
|
|
1325
1667
|
}
|
|
1326
1668
|
const blob = await resp.blob();
|
|
1669
|
+
if (isSvgBlob(blob) || !isImageMimeType(blob.type) && hasSvgExtension(source)) {
|
|
1670
|
+
return rasterizeSvg(blob, { size: svgSize });
|
|
1671
|
+
}
|
|
1327
1672
|
return createImageBitmap(blob, opts);
|
|
1328
1673
|
}
|
|
1329
1674
|
if (source instanceof Blob) {
|
|
1675
|
+
if (isSvgBlob(source)) {
|
|
1676
|
+
return rasterizeSvg(source, { size: svgSize });
|
|
1677
|
+
}
|
|
1330
1678
|
return createImageBitmap(source, opts);
|
|
1331
1679
|
}
|
|
1332
1680
|
if (source instanceof ImageBitmap) {
|
|
1333
1681
|
return source;
|
|
1334
1682
|
}
|
|
1683
|
+
if (typeof HTMLImageElement !== "undefined" && source instanceof HTMLImageElement) {
|
|
1684
|
+
const src = source.currentSrc || source.src;
|
|
1685
|
+
if (src && (hasSvgExtension(src) || /^data:image\/svg\+xml/i.test(src))) {
|
|
1686
|
+
const resp = await fetch(src);
|
|
1687
|
+
if (!resp.ok) {
|
|
1688
|
+
throw new Error(`compressTexture: fetch ${src} failed (${resp.status})`);
|
|
1689
|
+
}
|
|
1690
|
+
return rasterizeSvg(await resp.blob(), { size: svgSize });
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1335
1693
|
return createImageBitmap(source, opts);
|
|
1336
1694
|
}
|
|
1695
|
+
function isImageMimeType(type) {
|
|
1696
|
+
return /^image\//i.test(type) && !/svg/i.test(type);
|
|
1697
|
+
}
|
|
1337
1698
|
function bitmapToMipLevel(bitmap, flipY) {
|
|
1338
1699
|
const w = bitmap.width, h = bitmap.height;
|
|
1339
1700
|
const canvas = typeof OffscreenCanvas !== "undefined" ? new OffscreenCanvas(w, h) : Object.assign(document.createElement("canvas"), { width: w, height: h });
|
|
@@ -1366,7 +1727,9 @@ function wrapUncompressed(bitmap, srgb, flipY) {
|
|
|
1366
1727
|
async function compressTexture(source, options = {}) {
|
|
1367
1728
|
const {
|
|
1368
1729
|
hint = "color",
|
|
1730
|
+
preferredFormat,
|
|
1369
1731
|
colorSpace = "srgb",
|
|
1732
|
+
svgSize,
|
|
1370
1733
|
flipY = true,
|
|
1371
1734
|
mipmaps = false,
|
|
1372
1735
|
quality = "fast",
|
|
@@ -1374,7 +1737,7 @@ async function compressTexture(source, options = {}) {
|
|
|
1374
1737
|
adapter: providedAdapter
|
|
1375
1738
|
} = options;
|
|
1376
1739
|
const srgb = colorSpace === "srgb";
|
|
1377
|
-
const bitmap = await sourceToBitmap(source);
|
|
1740
|
+
const bitmap = await sourceToBitmap(source, svgSize);
|
|
1378
1741
|
const viaWebGPU = await encodeViaWebGPU();
|
|
1379
1742
|
if (viaWebGPU) return viaWebGPU;
|
|
1380
1743
|
const viaWebGL = encodeViaWebGL();
|
|
@@ -1401,7 +1764,7 @@ async function compressTexture(source, options = {}) {
|
|
|
1401
1764
|
if (!("gpu" in navigator)) return null;
|
|
1402
1765
|
const adapter = providedAdapter ?? await navigator.gpu.requestAdapter();
|
|
1403
1766
|
if (!adapter) return null;
|
|
1404
|
-
const selection = selectFormat(adapter, hint, { colorSpace });
|
|
1767
|
+
const selection = selectFormat(adapter, hint, { colorSpace, preferredFormat });
|
|
1405
1768
|
if (!selection.format || !selection.encoderClass) return null;
|
|
1406
1769
|
let encoder;
|
|
1407
1770
|
if (providedDevice) {
|
|
@@ -1474,7 +1837,7 @@ async function compressTexture(source, options = {}) {
|
|
|
1474
1837
|
const gl = getSharedWebGLContext();
|
|
1475
1838
|
if (!gl) return null;
|
|
1476
1839
|
const caps = detectWebGLCapabilities(gl);
|
|
1477
|
-
const selection = selectWebGLFormat(caps, hint, { colorSpace });
|
|
1840
|
+
const selection = selectWebGLFormat(caps, hint, { colorSpace, preferredFormat });
|
|
1478
1841
|
if (!selection.format || !selection.encoderClass) return null;
|
|
1479
1842
|
const encoder = selection.encoderClass.create(gl);
|
|
1480
1843
|
try {
|
|
@@ -1536,8 +1899,20 @@ import { Loader } from "three";
|
|
|
1536
1899
|
var GputexLoader = class extends Loader {
|
|
1537
1900
|
/** Format-selection hint. Default 'color'. */
|
|
1538
1901
|
hint = "color";
|
|
1902
|
+
/**
|
|
1903
|
+
* Prefer a specific format (currently only 'bc1') when the device
|
|
1904
|
+
* supports it; normal selection otherwise. See
|
|
1905
|
+
* `CompressOptions.preferredFormat`.
|
|
1906
|
+
*/
|
|
1907
|
+
preferredFormat;
|
|
1539
1908
|
/** Pick the sRGB or linear variant of the chosen format. Default 'srgb'. */
|
|
1540
1909
|
colorSpace = "srgb";
|
|
1910
|
+
/**
|
|
1911
|
+
* Rasterisation size for SVG URLs — a number (longest side, aspect
|
|
1912
|
+
* preserved) or exact `{ width, height }`. Default: the SVG's intrinsic
|
|
1913
|
+
* size. See `CompressOptions.svgSize`.
|
|
1914
|
+
*/
|
|
1915
|
+
svgSize;
|
|
1541
1916
|
/** Flip the image vertically before encoding. Default true (matches Three.js convention). */
|
|
1542
1917
|
flipY = true;
|
|
1543
1918
|
/** Generate + encode a full mip chain. Default false. */
|
|
@@ -1567,7 +1942,9 @@ var GputexLoader = class extends Loader {
|
|
|
1567
1942
|
this.manager.itemStart(url);
|
|
1568
1943
|
compressTexture(url, {
|
|
1569
1944
|
hint: this.hint,
|
|
1945
|
+
preferredFormat: this.preferredFormat,
|
|
1570
1946
|
colorSpace: this.colorSpace,
|
|
1947
|
+
svgSize: this.svgSize,
|
|
1571
1948
|
flipY: this.flipY,
|
|
1572
1949
|
mipmaps: this.mipmaps,
|
|
1573
1950
|
quality: this.quality,
|
|
@@ -1623,6 +2000,7 @@ export {
|
|
|
1623
2000
|
getSharedWebGLContext,
|
|
1624
2001
|
isWebGLAvailable,
|
|
1625
2002
|
padToBlockMultiple,
|
|
2003
|
+
rasterizeSvg,
|
|
1626
2004
|
selectFormat,
|
|
1627
2005
|
selectWebGLFormat,
|
|
1628
2006
|
threeFormatFor
|