gputex 0.1.1 → 0.1.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/dist/index.d.ts +9 -6
- package/dist/index.js +7 -4
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -48,8 +48,9 @@ interface EncoderOptions {
|
|
|
48
48
|
/**
|
|
49
49
|
* Encoder quality level. 'fast' (default) uses the cheaper search paths in the
|
|
50
50
|
* shaders — measured ~2–4× faster for ≤0.36 dB PSNR. 'high' runs the exhaustive
|
|
51
|
-
* search, producing output byte-identical to the CPU reference encoders
|
|
52
|
-
*
|
|
51
|
+
* search, producing output byte-identical to the CPU reference encoders (BC5/
|
|
52
|
+
* BC7/ASTC). BC1's 'high' adds a principal-axis endpoint seed and iterative
|
|
53
|
+
* refit on top of the 'fast' bbox+refit path.
|
|
53
54
|
*/
|
|
54
55
|
type EncodeQuality = 'fast' | 'high';
|
|
55
56
|
interface EncodeCallOptions {
|
|
@@ -142,8 +143,8 @@ declare abstract class Encoder {
|
|
|
142
143
|
get supportsSrgb(): boolean;
|
|
143
144
|
/**
|
|
144
145
|
* Whether the shader declares a `QUALITY_HIGH` pipeline-overridable constant
|
|
145
|
-
* (i.e. has distinct fast/high search paths).
|
|
146
|
-
*
|
|
146
|
+
* (i.e. has distinct fast/high search paths). Default false (e.g. a stub or a
|
|
147
|
+
* format with a single path); BC1/BC5/BC7/ASTC override it to true.
|
|
147
148
|
*/
|
|
148
149
|
get supportsQuality(): boolean;
|
|
149
150
|
/**
|
|
@@ -200,6 +201,7 @@ declare class BC1Encoder extends Encoder {
|
|
|
200
201
|
get label(): string;
|
|
201
202
|
get bytesPerBlock(): number;
|
|
202
203
|
get supportsSrgb(): boolean;
|
|
204
|
+
get supportsQuality(): boolean;
|
|
203
205
|
wgslSource(): string;
|
|
204
206
|
gpuTextureFormat({ colorSpace }: FormatVariant): GPUTextureFormat;
|
|
205
207
|
threeTextureFormat(): CompressedPixelFormat;
|
|
@@ -454,8 +456,9 @@ interface CompressOptions {
|
|
|
454
456
|
/**
|
|
455
457
|
* Encode quality / speed trade-off. 'fast' (default) is ~2–4× faster for a
|
|
456
458
|
* ≤0.36 dB PSNR cost; 'high' runs the exhaustive search (output identical to
|
|
457
|
-
* the CPU reference encoders
|
|
458
|
-
* (which always uses the fast
|
|
459
|
+
* the CPU reference encoders; for BC1, a principal-axis seed + iterative
|
|
460
|
+
* refit). No effect on the WebGL fallback (which always uses the fast
|
|
461
|
+
* encoders).
|
|
459
462
|
*/
|
|
460
463
|
quality?: EncodeQuality;
|
|
461
464
|
/** Reuse an existing device (e.g. Three.js's renderer device) instead
|
package/dist/index.js
CHANGED
|
@@ -214,8 +214,8 @@ var Encoder = class {
|
|
|
214
214
|
}
|
|
215
215
|
/**
|
|
216
216
|
* Whether the shader declares a `QUALITY_HIGH` pipeline-overridable constant
|
|
217
|
-
* (i.e. has distinct fast/high search paths).
|
|
218
|
-
*
|
|
217
|
+
* (i.e. has distinct fast/high search paths). Default false (e.g. a stub or a
|
|
218
|
+
* format with a single path); BC1/BC5/BC7/ASTC override it to true.
|
|
219
219
|
*/
|
|
220
220
|
get supportsQuality() {
|
|
221
221
|
return false;
|
|
@@ -374,7 +374,7 @@ var Encoder = class {
|
|
|
374
374
|
import { RGBA_S3TC_DXT1_Format } from "three";
|
|
375
375
|
|
|
376
376
|
// src/bc1.wgsl
|
|
377
|
-
var bc1_default = "// BC1 (DXT1) compute shader encoder.\n//\n// Each invocation encodes one 4x4 pixel block into an 8-byte BC1 block\n// written as 2 x u32 into the destination storage buffer.\n//\n// BC1 block layout (little-endian):\n// u32[0]: color0 (low 16) | color1 (high 16) both in RGB565\n// u32[1]: 16 x 2-bit indices, pixel 0 = bits 0..1, pixel 15 = bits 30..31\n//\n// When color0 > color1 (numeric 16-bit), the 4-color mode is used:\n// idx 0 -> color0\n// idx 1 -> color1\n// idx 2 -> (2*color0 + color1) / 3\n// idx 3 -> ( color0 + 2*color1) / 3\n// We always force the 4-color mode here.\n//\n// Algorithm:\n// 1. Compute the bounding box (min/max RGB) of the block.\n// 2. Inset slightly to account for endpoint quantization rounding;\n// this is a well-known heuristic from rygorous/stb_dxt that\n// improves quality cheaply.\n// 3. Quantize endpoints to RGB565 and ensure color0 > color1.\n// 4. Reconstruct the 4-color palette in floating point and assign\n// the closest palette entry to each pixel (full L2 search).\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\nfn to565(c: vec3<f32>) -> u32 {\n // Round-to-nearest quantization into 5-6-5.\n let r = u32(clamp(floor(c.r * 31.0 + 0.5), 0.0, 31.0));\n let g = u32(clamp(floor(c.g * 63.0 + 0.5), 0.0, 63.0));\n let b = u32(clamp(floor(c.b * 31.0 + 0.5), 0.0, 31.0));\n return (r << 11u) | (g << 5u) | b;\n}\n\nfn from565(c: u32) -> vec3<f32> {\n let r = f32((c >> 11u) & 31u);\n let g = f32((c >> 5u) & 63u);\n let b = f32( c & 31u);\n // Expand to 8-bit then normalize, matching typical BC1 decoder behavior.\n let r8 = (r * 527.0 + 23.0) / 256.0; // = round(r * 255 / 31)\n let g8 = (g * 259.0 + 33.0) / 256.0; // = round(g * 255 / 63)\n let b8 = (b * 527.0 + 23.0) / 256.0;\n return vec3<f32>(r8, g8, b8) / 255.0;\n}\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 var pixels: array<vec3<f32>, 16>;\n var bb_min = vec3<f32>(1.0, 1.0, 1.0);\n var bb_max = vec3<f32>(0.0, 0.0, 0.0);\n\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let lx = i32(i & 3u);\n let ly = i32(i >> 2u);\n // Clamp to edge for non-multiple-of-4 textures.\n let p = clamp(base + vec2<i32>(lx, ly), vec2<i32>(0, 0), max_xy);\n let c = textureLoad(src_tex, p, 0).rgb;\n pixels[i] = c;\n bb_min = min(bb_min, c);\n bb_max = max(bb_max, c);\n }\n\n // Inset the bounding box. The magic constant 1/16 approximates half\n // the width of an RGB565 quantization cell; insetting by that much\n // moves the endpoints toward each other so the quantized 4-color\n // palette covers the real data range more tightly.\n let inset = (bb_max - bb_min) / 16.0;\n var hi = clamp(bb_max - inset, vec3<f32>(0.0), vec3<f32>(1.0));\n var lo = clamp(bb_min + inset, vec3<f32>(0.0), vec3<f32>(1.0));\n\n var c0 = to565(hi);\n var c1 = to565(lo);\n\n // 4-color mode requires c0 > c1. If equal (flat block), we still use\n // 4-color mode by nudging c1 down when possible; if c1 == 0 the block\n // is truly black so all indices stay 0 and the decoded value is 0.\n if (c0 == c1) {\n if (c1 > 0u) {\n c1 = c1 - 1u;\n } else {\n c0 = c0 + 1u;\n }\n } else if (c0 < c1) {\n let tmp = c0;\n c0 = c1;\n c1 = tmp;\n }\n\n // Build the palette in the decoded colour space so index selection\n // matches what the hardware decoder will produce.\n let p0 = from565(c0);\n let p1 = from565(c1);\n let p2 = (2.0 * p0 + p1) * (1.0 / 3.0);\n let p3 = (p0 + 2.0 * p1) * (1.0 / 3.0);\n\n var indices: u32 = 0u;\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let c = pixels[i];\n let d0 = dot(c - p0, c - p0);\n let d1 = dot(c - p1, c - p1);\n let d2 = dot(c - p2, c - p2);\n let d3 = dot(c - p3, c - p3);\n\n var best_d: f32 = d0;\n var best_i: u32 = 0u;\n if (d1 < best_d) { best_d = d1; best_i = 1u; }\n if (d2 < best_d) { best_d = d2; best_i = 2u; }\n if (d3 < best_d) { best_d = d3; best_i = 3u; }\n\n indices = indices | (best_i << (i * 2u));\n }\n\n let out = block_index * 2u;\n dst[out] = c0 | (c1 << 16u);\n dst[out + 1u] = indices;\n}\n";
|
|
377
|
+
var bc1_default = "// BC1 (DXT1) compute shader encoder.\n//\n// Each invocation encodes one 4x4 pixel block into an 8-byte BC1 block\n// written as 2 x u32 into the destination storage buffer.\n//\n// BC1 block layout (little-endian):\n// u32[0]: color0 (low 16) | color1 (high 16) both in RGB565\n// u32[1]: 16 x 2-bit indices, pixel 0 = bits 0..1, pixel 15 = bits 30..31\n//\n// We always force the 4-color mode (color0 > color1, numeric 16-bit):\n// idx 0 -> color0\n// idx 1 -> color1\n// idx 2 -> (2*color0 + color1) / 3\n// idx 3 -> ( color0 + 2*color1) / 3\n//\n// QUALITY LEVELS (pipeline-overridable constant `QUALITY_HIGH`)\n// fast (0, default): bounding-box endpoints, inset by ~half a 565 cell, then\n// a single least-squares endpoint refit (the refit is accepted only if it\n// lowers the block's squared error). This is what the WebGL2 fragment\n// fallback runs too.\n// high (1): endpoints are seeded from the block's principal colour axis\n// (covariance power-iteration) as well as the bbox diagonal, each refined by\n// several least-squares passes; the lower-error family wins. Mirrors\n// bc1_ref.ts. Strictly \u2265 fast in quality, at the cost of the eigen-solve.\n//\n// Algorithm per block:\n// 1. Load the 16 pixels; compute the bounding box (and, for high, the mean).\n// 2. Seed endpoints (bbox diagonal; high also tries the principal axis).\n// 3. Quantize to RGB565, force 4-color mode, assign each pixel its nearest\n// palette entry (full 4-entry L2 search in the decoded colour space).\n// 4. Least-squares refit: re-solve the endpoints for the current indices,\n// re-quantize, re-assign; keep the result only when error decreases.\n\n// 0 = fast (default), 1 = 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\nfn to565(c: vec3<f32>) -> u32 {\n // Round-to-nearest quantization into 5-6-5.\n let r = u32(clamp(floor(c.r * 31.0 + 0.5), 0.0, 31.0));\n let g = u32(clamp(floor(c.g * 63.0 + 0.5), 0.0, 63.0));\n let b = u32(clamp(floor(c.b * 31.0 + 0.5), 0.0, 31.0));\n return (r << 11u) | (g << 5u) | b;\n}\n\nfn from565(c: u32) -> vec3<f32> {\n let r = f32((c >> 11u) & 31u);\n let g = f32((c >> 5u) & 63u);\n let b = f32( c & 31u);\n // 5/6-bit -> 8-bit. floor((x*527+23)/64) == (x<<3)|(x>>2), i.e. the exact\n // bit-replication a BC1 decoder performs (white -> 255). The inputs are\n // small integers and /64 is exact in f32, so this matches the hardware and\n // is portable. Selecting indices against this palette is what makes the\n // encoder agree with what the GPU will actually sample.\n let r8 = floor((r * 527.0 + 23.0) / 64.0);\n let g8 = floor((g * 259.0 + 33.0) / 64.0);\n let b8 = floor((b * 527.0 + 23.0) / 64.0);\n return vec3<f32>(r8, g8, b8) / 255.0;\n}\n\n// 4-color-mode interpolation weights: palette[j] = wa(j)*c0 + wb(j)*c1.\nfn wa(j: u32) -> f32 {\n switch j {\n case 0u: { return 1.0; }\n case 1u: { return 0.0; }\n case 2u: { return 2.0 / 3.0; }\n default: { return 1.0 / 3.0; } // case 3u\n }\n}\nfn wb(j: u32) -> f32 {\n switch j {\n case 0u: { return 0.0; }\n case 1u: { return 1.0; }\n case 2u: { return 1.0 / 3.0; }\n default: { return 2.0 / 3.0; } // case 3u\n }\n}\n\nfn build_palette(c0: u32, c1: u32, pal: ptr<function, array<vec3<f32>, 4>>) {\n let p0 = from565(c0);\n let p1 = from565(c1);\n for (var j: u32 = 0u; j < 4u; j = j + 1u) {\n (*pal)[j] = wa(j) * p0 + wb(j) * p1;\n }\n}\n\n// Assign each of the 16 pixels its nearest palette entry (full 4-entry L2),\n// writing indices into `out_idx` and returning the total squared error.\nfn assign_indices(\n pixels: ptr<function, array<vec3<f32>, 16>>,\n pal: ptr<function, array<vec3<f32>, 4>>,\n out_idx: ptr<function, array<u32, 16>>,\n) -> f32 {\n var err: f32 = 0.0;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let c = (*pixels)[k];\n var best_j: u32 = 0u;\n var best_d: f32 = 1e30;\n for (var j: u32 = 0u; j < 4u; j = j + 1u) {\n let d = (*pal)[j] - c;\n let d2 = dot(d, d);\n if (d2 < best_d) {\n best_d = d2;\n best_j = j;\n }\n }\n (*out_idx)[k] = best_j;\n err = err + best_d;\n }\n return err;\n}\n\n// One least-squares refit pass: solve the 2x2 normal equations for the endpoint\n// colours that minimise \u03A3\u2016wa\xB7e0 + wb\xB7e1 \u2212 c\u2016\xB2 under the current indices. The\n// three channels share the scalar sums, so it's one 2x2 solve with vec3 RHS.\nstruct RefitResult { e0: vec3<f32>, e1: vec3<f32>, valid: bool };\nfn refit(\n pixels: ptr<function, array<vec3<f32>, 16>>,\n indices: ptr<function, array<u32, 16>>,\n) -> RefitResult {\n var sAA: f32 = 0.0; var sBB: f32 = 0.0; var sAB: f32 = 0.0;\n var sAV: vec3<f32> = vec3<f32>(0.0);\n var sBV: vec3<f32> = vec3<f32>(0.0);\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let a = wa((*indices)[k]);\n let b = wb((*indices)[k]);\n let v = (*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 var out: RefitResult;\n let det = sAA * sBB - sAB * sAB;\n if (abs(det) < 1e-9) {\n out.valid = false;\n return out;\n }\n out.e0 = clamp((sBB * sAV - sAB * sBV) / det, vec3<f32>(0.0), vec3<f32>(1.0));\n out.e1 = clamp((sAA * sBV - sAB * sAV) / det, vec3<f32>(0.0), vec3<f32>(1.0));\n out.valid = true;\n return out;\n}\n\n// Candidate solution tracked across endpoint seeds / refit passes.\nstruct Best { c0: u32, c1: u32, indices: array<u32, 16>, err: f32 };\n\n// Quantize (hi, lo) to 565, force 4-color mode, assign indices, then refine with\n// up to `max_refits` least-squares passes. Commits to `*best` only on strict\n// improvement.\nfn fit_from_endpoints(\n pixels: ptr<function, array<vec3<f32>, 16>>,\n hi: vec3<f32>,\n lo: vec3<f32>,\n max_refits: u32,\n best: ptr<function, Best>,\n) {\n var c0 = to565(hi);\n var c1 = to565(lo);\n // 4-color mode requires color0 > color1.\n if (c0 == c1) {\n if (c1 > 0u) { c1 = c1 - 1u; } else { c0 = c0 + 1u; }\n } else if (c0 < c1) {\n let t = c0; c0 = c1; c1 = t;\n }\n\n var pal: array<vec3<f32>, 4>;\n var idx: array<u32, 16>;\n build_palette(c0, c1, &pal);\n var err = assign_indices(pixels, &pal, &idx);\n if (err < (*best).err) {\n (*best).c0 = c0; (*best).c1 = c1; (*best).indices = idx; (*best).err = err;\n }\n\n for (var rp: u32 = 0u; rp < max_refits; rp = rp + 1u) {\n let r = refit(pixels, &idx);\n if (!r.valid) { break; }\n var nc0 = to565(r.e0);\n var nc1 = to565(r.e1);\n // A refit that flips/equalises the endpoints would change decode mode;\n // keep 4-color mode, and stop once it stops moving.\n if (nc0 < nc1) { let t = nc0; nc0 = nc1; nc1 = t; }\n if (nc0 == nc1) { break; }\n if (nc0 == c0 && nc1 == c1) { break; }\n build_palette(nc0, nc1, &pal);\n let nerr = assign_indices(pixels, &pal, &idx);\n c0 = nc0; c1 = nc1; err = nerr;\n if (nerr < (*best).err) {\n (*best).c0 = nc0; (*best).c1 = nc1; (*best).indices = idx; (*best).err = nerr;\n }\n }\n}\n\n// Principal colour axis via covariance power-iteration, seeded with the bbox\n// diagonal. Returns a unit axis, or vec3(0) for a degenerate (constant) block.\nfn principal_axis(\n pixels: ptr<function, array<vec3<f32>, 16>>,\n mean: vec3<f32>,\n seed: vec3<f32>,\n) -> vec3<f32> {\n // Symmetric 3x3 covariance, stored as its three rows.\n var c0v = vec3<f32>(0.0);\n var c1v = vec3<f32>(0.0);\n var c2v = vec3<f32>(0.0);\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let d = (*pixels)[k] - mean;\n c0v = c0v + d.x * d;\n c1v = c1v + d.y * d;\n c2v = c2v + d.z * d;\n }\n var v = seed;\n var len = length(v);\n if (len < 1e-9) { return vec3<f32>(0.0); }\n v = v / len;\n for (var iter: u32 = 0u; iter < 8u; iter = iter + 1u) {\n let nv = vec3<f32>(dot(c0v, v), dot(c1v, v), dot(c2v, v));\n len = length(nv);\n if (len < 1e-12) { return vec3<f32>(0.0); }\n v = nv / len;\n }\n return v;\n}\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 var pixels: array<vec3<f32>, 16>;\n var bb_min = vec3<f32>(1.0, 1.0, 1.0);\n var bb_max = vec3<f32>(0.0, 0.0, 0.0);\n var mean = vec3<f32>(0.0);\n\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let lx = i32(i & 3u);\n let ly = i32(i >> 2u);\n // Clamp to edge for non-multiple-of-4 textures.\n let p = clamp(base + vec2<i32>(lx, ly), vec2<i32>(0, 0), max_xy);\n let c = textureLoad(src_tex, p, 0).rgb;\n pixels[i] = c;\n bb_min = min(bb_min, c);\n bb_max = max(bb_max, c);\n mean = mean + c;\n }\n mean = mean * (1.0 / 16.0);\n\n // Inset the bounding box by ~half an RGB565 cell (1/16) so the quantized\n // 4-color palette covers the real data range more tightly (stb_dxt heuristic).\n let inset = (bb_max - bb_min) / 16.0;\n let bbox_hi = clamp(bb_max - inset, vec3<f32>(0.0), vec3<f32>(1.0));\n let bbox_lo = clamp(bb_min + inset, vec3<f32>(0.0), vec3<f32>(1.0));\n\n var best: Best;\n best.err = 1e30;\n\n if (QUALITY_HIGH != 0u) {\n // Seed from the principal colour axis: project all texels onto it, take the\n // extreme projections as endpoints, inset along the axis. Then also try the\n // bbox seed and keep whichever family yields the lower error.\n let axis = principal_axis(&pixels, mean, bb_max - bb_min);\n if (dot(axis, axis) > 0.0) {\n var t_min: f32 = 1e30;\n var t_max: f32 = -1e30;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let t = dot(pixels[k] - mean, axis);\n t_min = min(t_min, t);\n t_max = max(t_max, t);\n }\n let pad = (t_max - t_min) / 16.0;\n let pca_hi = clamp(mean + (t_max - pad) * axis, vec3<f32>(0.0), vec3<f32>(1.0));\n let pca_lo = clamp(mean + (t_min + pad) * axis, vec3<f32>(0.0), vec3<f32>(1.0));\n fit_from_endpoints(&pixels, pca_hi, pca_lo, 3u, &best);\n }\n fit_from_endpoints(&pixels, bbox_hi, bbox_lo, 3u, &best);\n } else {\n fit_from_endpoints(&pixels, bbox_hi, bbox_lo, 1u, &best);\n }\n\n var indices: u32 = 0u;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n indices = indices | ((best.indices[k] & 3u) << (k * 2u));\n }\n\n let out = block_index * 2u;\n dst[out] = best.c0 | (best.c1 << 16u);\n dst[out + 1u] = indices;\n}\n";
|
|
378
378
|
|
|
379
379
|
// src/BC1Encoder.ts
|
|
380
380
|
var BC1Encoder = class extends Encoder {
|
|
@@ -389,6 +389,9 @@ var BC1Encoder = class extends Encoder {
|
|
|
389
389
|
get supportsSrgb() {
|
|
390
390
|
return true;
|
|
391
391
|
}
|
|
392
|
+
get supportsQuality() {
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
392
395
|
wgslSource() {
|
|
393
396
|
return bc1_default;
|
|
394
397
|
}
|
|
@@ -816,7 +819,7 @@ var WebGLBlockEncoder = class {
|
|
|
816
819
|
import { RGBA_S3TC_DXT1_Format as RGBA_S3TC_DXT1_Format2 } from "three";
|
|
817
820
|
|
|
818
821
|
// src/webgl/glsl/bc1.frag.glsl
|
|
819
|
-
var bc1_frag_default = "#version 300 es\n// BC1 (DXT1) fragment-shader encoder \u2014 WebGL2 port of bc1.wgsl.\n//\n// One fragment per 4\xD74 block. Output is the 8-byte BC1 block as 2 \xD7 u32 in\n// outColor.rg (outColor.ba unused); the encoder reads back RGBA32UI and keeps\n// the low two words per block.
|
|
822
|
+
var bc1_frag_default = "#version 300 es\n// BC1 (DXT1) fragment-shader encoder \u2014 WebGL2 port of bc1.wgsl (fast path).\n//\n// One fragment per 4\xD74 block. Output is the 8-byte BC1 block as 2 \xD7 u32 in\n// outColor.rg (outColor.ba unused); the encoder reads back RGBA32UI and keeps\n// the low two words per block. This is the *fast* path only (the WGSL\n// `QUALITY_HIGH == 0` branch): bbox endpoints, 1/16 inset, RGB565 quantisation,\n// forced 4-colour mode, full 4-entry L2 index search, then a single\n// least-squares endpoint refit accepted only when it lowers the block's error.\n// See bc1.wgsl for the full derivation.\n\nprecision highp float;\nprecision highp int;\n\nuniform sampler2D uSrc;\nuniform ivec2 uSrcSize; // original (unpadded) width, height\nuniform int uFlipY; // 1 = sample bottom-up (matches Three.js flipY)\n\nlayout(location = 0) out uvec4 outColor;\n\n// 4-colour-mode interpolation weights: pal[j] = WA[j]*c0 + WB[j]*c1.\nconst float WA[4] = float[4](1.0, 0.0, 2.0 / 3.0, 1.0 / 3.0);\nconst float WB[4] = float[4](0.0, 1.0, 1.0 / 3.0, 2.0 / 3.0);\n\nuint to565(vec3 c) {\n uint r = uint(clamp(floor(c.r * 31.0 + 0.5), 0.0, 31.0));\n uint g = uint(clamp(floor(c.g * 63.0 + 0.5), 0.0, 63.0));\n uint b = uint(clamp(floor(c.b * 31.0 + 0.5), 0.0, 31.0));\n return (r << 11) | (g << 5) | b;\n}\n\nvec3 from565(uint c) {\n float r = float((c >> 11) & 31u);\n float g = float((c >> 5) & 63u);\n float b = float(c & 31u);\n // 5/6-bit \u2192 8-bit. floor((x*527+23)/64) == (x<<3)|(x>>2): exact hardware\n // bit-replication (white \u2192 255), so index selection matches the GPU decode.\n float r8 = floor((r * 527.0 + 23.0) / 64.0);\n float g8 = floor((g * 259.0 + 33.0) / 64.0);\n float b8 = floor((b * 527.0 + 23.0) / 64.0);\n return vec3(r8, g8, b8) / 255.0;\n}\n\nvoid main() {\n ivec2 base = ivec2(gl_FragCoord.xy) * 4;\n ivec2 maxXY = uSrcSize - ivec2(1);\n\n vec3 pixels[16];\n vec3 bbMin = vec3(1.0);\n vec3 bbMax = vec3(0.0);\n for (int i = 0; i < 16; i++) {\n ivec2 p = clamp(base + ivec2(i & 3, i >> 2), ivec2(0), maxXY);\n int sy = (uFlipY != 0) ? (uSrcSize.y - 1 - p.y) : p.y;\n vec3 c = texelFetch(uSrc, ivec2(p.x, sy), 0).rgb;\n pixels[i] = c;\n bbMin = min(bbMin, c);\n bbMax = max(bbMax, c);\n }\n\n // Inset the bbox by ~half an RGB565 cell (1/16) to tighten the quantised\n // 4-colour palette around the real data range.\n vec3 inset = (bbMax - bbMin) / 16.0;\n vec3 hi = clamp(bbMax - inset, vec3(0.0), vec3(1.0));\n vec3 lo = clamp(bbMin + inset, vec3(0.0), vec3(1.0));\n\n uint c0 = to565(hi);\n uint c1 = to565(lo);\n // 4-colour mode requires color0 > color1.\n if (c0 == c1) {\n if (c1 > 0u) { c1 = c1 - 1u; } else { c0 = c0 + 1u; }\n } else if (c0 < c1) {\n uint tmp = c0; c0 = c1; c1 = tmp;\n }\n\n // Build the palette in decoded space, assign each pixel its nearest entry.\n vec3 pal[4];\n vec3 p0 = from565(c0);\n vec3 p1 = from565(c1);\n for (int j = 0; j < 4; j++) pal[j] = WA[j] * p0 + WB[j] * p1;\n\n uint idx[16];\n float err = 0.0;\n for (int k = 0; k < 16; k++) {\n vec3 c = pixels[k];\n uint bestJ = 0u;\n float bestD = 1e30;\n for (int j = 0; j < 4; j++) {\n vec3 d = pal[j] - c;\n float d2 = dot(d, d);\n if (d2 < bestD) { bestD = d2; bestJ = uint(j); }\n }\n idx[k] = bestJ;\n err += bestD;\n }\n\n // One least-squares refit: re-solve the endpoints for the current indices,\n // re-quantise, re-assign; keep it only if the squared error drops.\n float sAA = 0.0, sBB = 0.0, sAB = 0.0;\n vec3 sAV = vec3(0.0), sBV = vec3(0.0);\n for (int k = 0; k < 16; k++) {\n float a = WA[int(idx[k])];\n float b = WB[int(idx[k])];\n vec3 v = pixels[k];\n sAA += a * a; sBB += b * b; sAB += a * b; sAV += a * v; sBV += b * v;\n }\n float det = sAA * sBB - sAB * sAB;\n if (abs(det) > 1e-9) {\n vec3 e0 = clamp((sBB * sAV - sAB * sBV) / det, vec3(0.0), vec3(1.0));\n vec3 e1 = clamp((sAA * sBV - sAB * sAV) / det, vec3(0.0), vec3(1.0));\n uint nc0 = to565(e0);\n uint nc1 = to565(e1);\n if (nc0 < nc1) { uint t = nc0; nc0 = nc1; nc1 = t; }\n if (nc0 != nc1 && !(nc0 == c0 && nc1 == c1)) {\n vec3 q0 = from565(nc0);\n vec3 q1 = from565(nc1);\n vec3 pal2[4];\n for (int j = 0; j < 4; j++) pal2[j] = WA[j] * q0 + WB[j] * q1;\n uint idx2[16];\n float nerr = 0.0;\n for (int k = 0; k < 16; k++) {\n vec3 c = pixels[k];\n uint bestJ = 0u;\n float bestD = 1e30;\n for (int j = 0; j < 4; j++) {\n vec3 d = pal2[j] - c;\n float d2 = dot(d, d);\n if (d2 < bestD) { bestD = d2; bestJ = uint(j); }\n }\n idx2[k] = bestJ;\n nerr += bestD;\n }\n if (nerr < err) {\n c0 = nc0; c1 = nc1;\n for (int k = 0; k < 16; k++) idx[k] = idx2[k];\n }\n }\n }\n\n uint indices = 0u;\n for (int k = 0; k < 16; k++) indices |= (idx[k] & 3u) << (uint(k) * 2u);\n\n outColor = uvec4(c0 | (c1 << 16), indices, 0u, 0u);\n}\n";
|
|
820
823
|
|
|
821
824
|
// src/webgl/BC1WebGLEncoder.ts
|
|
822
825
|
var BC1WebGLEncoder = class extends WebGLBlockEncoder {
|