gputex 0.3.0 → 0.3.1
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 +16 -8
- package/dist/index.d.ts +11 -1
- package/dist/index.js +220 -15
- package/dist/three.d.ts +16 -2
- package/dist/three.js +230 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -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.
|
|
@@ -204,13 +211,14 @@ const tex = buildCompressedTexture([bytes], TextureFormat.BC7_SRGB)
|
|
|
204
211
|
|
|
205
212
|
### `compressTexture` options
|
|
206
213
|
|
|
207
|
-
| Option
|
|
208
|
-
|
|
|
209
|
-
| `hint`
|
|
210
|
-
| `
|
|
211
|
-
| `
|
|
212
|
-
| `
|
|
213
|
-
| `
|
|
214
|
+
| Option | Type | Default | Description |
|
|
215
|
+
| ----------------- | -------------------- | --------- | ------------------------------------------------------------------------------------------------ |
|
|
216
|
+
| `hint` | `TextureHint` | `'color'` | `'color'`, `'colorWithAlpha'`, or `'normal'` |
|
|
217
|
+
| `preferredFormat` | `'bc1'` | — | Prefer BC1 (half of BC7's size) when supported; normal selection otherwise. `hint: 'color'` only |
|
|
218
|
+
| `colorSpace` | `'srgb' \| 'linear'` | `'srgb'` | Use the sRGB or linear variant of the chosen format |
|
|
219
|
+
| `flipY` | `boolean` | `true` | Flip vertically (matches Three.js convention) |
|
|
220
|
+
| `mipmaps` | `boolean` | `false` | Generate full mip chain down to 1x1 |
|
|
221
|
+
| `device` | `GPUDevice` | — | Reuse an existing WebGPU device instead of creating one |
|
|
214
222
|
|
|
215
223
|
## Benchmarks
|
|
216
224
|
|
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,4 @@ declare function generateMipChain(level0: MipLevel): MipLevel[];
|
|
|
430
440
|
*/
|
|
431
441
|
declare function padToBlockMultiple(level: MipLevel): MipLevel;
|
|
432
442
|
|
|
433
|
-
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 RawPixelSource, type SelectFormatOptions, TextureFormat, type TextureHint, WebGLBlockEncoder, type WebGLCapabilities, type WebGLEncodeBytesResult, type WebGLEncoderConstructor, type WebGLEncoderImageSource, type WebGLEncoderOptions, type WebGLFormatSelection, WebGPUFeature, createWebGLContext, detectCapabilities, detectWebGLCapabilities, generateMipChain, getSharedWebGLContext, isWebGLAvailable, padToBlockMultiple, selectFormat, selectWebGLFormat };
|
|
443
|
+
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 RawPixelSource, type SelectFormatOptions, TextureFormat, type TextureHint, WebGLBlockEncoder, type WebGLCapabilities, type WebGLEncodeBytesResult, type WebGLEncoderConstructor, type WebGLEncoderImageSource, type WebGLEncoderOptions, type WebGLFormatSelection, WebGPUFeature, createWebGLContext, detectCapabilities, detectWebGLCapabilities, generateMipChain, getSharedWebGLContext, isWebGLAvailable, padToBlockMultiple, 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 };
|
package/dist/three.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TextureHint, EncodeQuality, TextureFormat, Encoder, EncoderImageSource } from './index.js';
|
|
1
|
+
import { TextureHint, PreferredFormat, EncodeQuality, TextureFormat, Encoder, EncoderImageSource } from './index.js';
|
|
2
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';
|
|
3
3
|
import { Texture, CompressedTexture, Loader, CompressedPixelFormat } from 'three';
|
|
4
4
|
|
|
@@ -11,6 +11,14 @@ type CompressTextureSource = string | Blob | File | ImageBitmap | HTMLImageEleme
|
|
|
11
11
|
interface CompressOptions {
|
|
12
12
|
/** How the texture will be used. Drives format selection. Default 'color'. */
|
|
13
13
|
hint?: TextureHint;
|
|
14
|
+
/**
|
|
15
|
+
* Prefer a specific format over the default choice when the device
|
|
16
|
+
* supports it; falls back to the normal selection (BC7 → ASTC → RGBA8)
|
|
17
|
+
* when it doesn't. Currently only 'bc1': half the memory of BC7 for
|
|
18
|
+
* opaque colour textures, at lower quality. Only honoured with
|
|
19
|
+
* `hint: 'color'` — BC1 can't carry real alpha or normal maps.
|
|
20
|
+
*/
|
|
21
|
+
preferredFormat?: PreferredFormat;
|
|
14
22
|
/** Pick the sRGB or linear variant of the chosen format. Default 'srgb'. */
|
|
15
23
|
colorSpace?: 'srgb' | 'linear';
|
|
16
24
|
/** Flip the image vertically before encoding. Default true (matches Three.js convention). */
|
|
@@ -63,6 +71,12 @@ declare function compressTexture(source: CompressTextureSource, options?: Compre
|
|
|
63
71
|
declare class GputexLoader extends Loader<Texture> {
|
|
64
72
|
/** Format-selection hint. Default 'color'. */
|
|
65
73
|
hint: TextureHint;
|
|
74
|
+
/**
|
|
75
|
+
* Prefer a specific format (currently only 'bc1') when the device
|
|
76
|
+
* supports it; normal selection otherwise. See
|
|
77
|
+
* `CompressOptions.preferredFormat`.
|
|
78
|
+
*/
|
|
79
|
+
preferredFormat?: PreferredFormat;
|
|
66
80
|
/** Pick the sRGB or linear variant of the chosen format. Default 'srgb'. */
|
|
67
81
|
colorSpace: 'srgb' | 'linear';
|
|
68
82
|
/** Flip the image vertically before encoding. Default true (matches Three.js convention). */
|
|
@@ -138,4 +152,4 @@ interface EncodeToTextureOptions {
|
|
|
138
152
|
*/
|
|
139
153
|
declare function encodeToTexture(encoder: Encoder, source: EncoderImageSource, { colorSpace, quality, flipY }?: EncodeToTextureOptions): Promise<EncodeResult>;
|
|
140
154
|
|
|
141
|
-
export { type CompressOptions, type CompressResult, type CompressTextureSource, EncodeQuality, type EncodeResult, type EncodeToTextureOptions, Encoder, EncoderImageSource, GputexLoader, TextureFormat, TextureHint, buildCompressedTexture, compressTexture, encodeToTexture, threeFormatFor };
|
|
155
|
+
export { type CompressOptions, type CompressResult, type CompressTextureSource, EncodeQuality, type EncodeResult, type EncodeToTextureOptions, Encoder, EncoderImageSource, GputexLoader, PreferredFormat, 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 };
|
|
@@ -1366,6 +1571,7 @@ function wrapUncompressed(bitmap, srgb, flipY) {
|
|
|
1366
1571
|
async function compressTexture(source, options = {}) {
|
|
1367
1572
|
const {
|
|
1368
1573
|
hint = "color",
|
|
1574
|
+
preferredFormat,
|
|
1369
1575
|
colorSpace = "srgb",
|
|
1370
1576
|
flipY = true,
|
|
1371
1577
|
mipmaps = false,
|
|
@@ -1401,7 +1607,7 @@ async function compressTexture(source, options = {}) {
|
|
|
1401
1607
|
if (!("gpu" in navigator)) return null;
|
|
1402
1608
|
const adapter = providedAdapter ?? await navigator.gpu.requestAdapter();
|
|
1403
1609
|
if (!adapter) return null;
|
|
1404
|
-
const selection = selectFormat(adapter, hint, { colorSpace });
|
|
1610
|
+
const selection = selectFormat(adapter, hint, { colorSpace, preferredFormat });
|
|
1405
1611
|
if (!selection.format || !selection.encoderClass) return null;
|
|
1406
1612
|
let encoder;
|
|
1407
1613
|
if (providedDevice) {
|
|
@@ -1474,7 +1680,7 @@ async function compressTexture(source, options = {}) {
|
|
|
1474
1680
|
const gl = getSharedWebGLContext();
|
|
1475
1681
|
if (!gl) return null;
|
|
1476
1682
|
const caps = detectWebGLCapabilities(gl);
|
|
1477
|
-
const selection = selectWebGLFormat(caps, hint, { colorSpace });
|
|
1683
|
+
const selection = selectWebGLFormat(caps, hint, { colorSpace, preferredFormat });
|
|
1478
1684
|
if (!selection.format || !selection.encoderClass) return null;
|
|
1479
1685
|
const encoder = selection.encoderClass.create(gl);
|
|
1480
1686
|
try {
|
|
@@ -1536,6 +1742,12 @@ import { Loader } from "three";
|
|
|
1536
1742
|
var GputexLoader = class extends Loader {
|
|
1537
1743
|
/** Format-selection hint. Default 'color'. */
|
|
1538
1744
|
hint = "color";
|
|
1745
|
+
/**
|
|
1746
|
+
* Prefer a specific format (currently only 'bc1') when the device
|
|
1747
|
+
* supports it; normal selection otherwise. See
|
|
1748
|
+
* `CompressOptions.preferredFormat`.
|
|
1749
|
+
*/
|
|
1750
|
+
preferredFormat;
|
|
1539
1751
|
/** Pick the sRGB or linear variant of the chosen format. Default 'srgb'. */
|
|
1540
1752
|
colorSpace = "srgb";
|
|
1541
1753
|
/** Flip the image vertically before encoding. Default true (matches Three.js convention). */
|
|
@@ -1567,6 +1779,7 @@ var GputexLoader = class extends Loader {
|
|
|
1567
1779
|
this.manager.itemStart(url);
|
|
1568
1780
|
compressTexture(url, {
|
|
1569
1781
|
hint: this.hint,
|
|
1782
|
+
preferredFormat: this.preferredFormat,
|
|
1570
1783
|
colorSpace: this.colorSpace,
|
|
1571
1784
|
flipY: this.flipY,
|
|
1572
1785
|
mipmaps: this.mipmaps,
|