gputex 0.1.0 → 0.1.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 +18 -4
- package/dist/index.d.ts +178 -4
- package/dist/index.js +554 -132
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# GPUtex
|
|
2
2
|
|
|
3
|
-
Runtime GPU texture compression via WebGPU compute shaders. Feed it a PNG/JPG/WebP/AVIF and get back a GPU-compressed texture (BC7, BC5, ASTC 4x4, or BC1) ready for Three.js or React Three Fiber.
|
|
3
|
+
Runtime GPU texture compression via WebGPU compute shaders, with a WebGL2 fragment-shader fallback. Feed it a PNG/JPG/WebP/AVIF and get back a GPU-compressed texture (BC7, BC5, ASTC 4x4, or BC1) ready for Three.js or React Three Fiber.
|
|
4
4
|
|
|
5
5
|
⚠️: This is 100% vibe-coded. The code is completely unreviewed, under-tested, it will probably crash on many devices, and this library is very likely to go unmaintained. Do not use for anything important.
|
|
6
6
|
|
|
@@ -27,6 +27,18 @@ bun add gputex
|
|
|
27
27
|
|
|
28
28
|
Format selection is automatic: BC7/BC5 on desktop, ASTC on mobile, uncompressed RGBA8 fallback otherwise.
|
|
29
29
|
|
|
30
|
+
## WebGL fallback
|
|
31
|
+
|
|
32
|
+
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 block encoders as fragment shaders — each 4×4 block is computed in one fragment, written to an `RGBA32UI` render target, and read back. The output bytes are identical to the WebGPU encoders, so the resulting `CompressedTexture` looks the same under either renderer.
|
|
33
|
+
|
|
34
|
+
The fallback chain is **WebGPU → WebGL2 → uncompressed RGBA8**. The `backend` field on the result (`'webgpu' | 'webgl' | 'none'`) tells you which path ran.
|
|
35
|
+
|
|
36
|
+
Notes on the WebGL path:
|
|
37
|
+
|
|
38
|
+
- It needs the matching WebGL2 compressed-texture extension to be sampleable: `EXT_texture_compression_bptc` (BC7), `EXT_texture_compression_rgtc` (BC5), `WEBGL_compressed_texture_astc` (ASTC), or `WEBGL_compressed_texture_s3tc` (BC1). Selection mirrors the WebGPU side, with BC1 added as a broadly-available last resort for **opaque** colour when neither BPTC nor ASTC is present.
|
|
39
|
+
- It always uses the **fast** encoders — the `quality: 'high'` option and the `device` / `adapter` options apply to the WebGPU path only.
|
|
40
|
+
- All encoding happens on one shared, off-screen WebGL2 context; nothing is drawn to a visible canvas.
|
|
41
|
+
|
|
30
42
|
## Usage
|
|
31
43
|
|
|
32
44
|
### `compressTexture` — direct API
|
|
@@ -171,9 +183,11 @@ encoder.destroy()
|
|
|
171
183
|
|
|
172
184
|
## Requirements
|
|
173
185
|
|
|
174
|
-
-
|
|
175
|
-
-
|
|
176
|
-
-
|
|
186
|
+
- WebGPU (primary) **or** WebGL2 (fallback) — almost every current browser has at least one
|
|
187
|
+
- A compressed-texture capability for compressed output:
|
|
188
|
+
- WebGPU: `texture-compression-bc` (desktop) or `texture-compression-astc` (mobile)
|
|
189
|
+
- WebGL2: `EXT_texture_compression_bptc` / `_rgtc`, `WEBGL_compressed_texture_astc`, or `WEBGL_compressed_texture_s3tc`
|
|
190
|
+
- Falls back to uncompressed RGBA8 when no compressed format is available on either backend
|
|
177
191
|
|
|
178
192
|
## Device-specific workarounds
|
|
179
193
|
|
package/dist/index.d.ts
CHANGED
|
@@ -244,6 +244,163 @@ declare class ASTC4x4Encoder extends Encoder {
|
|
|
244
244
|
threeTextureFormat(): CompressedPixelFormat;
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
/** One encoded mip level. The fields both encoder backends already produce. */
|
|
248
|
+
interface EncodedLevel {
|
|
249
|
+
/** Logical (pre-padding) dimensions, surfaced on the texture's userData. */
|
|
250
|
+
width: number;
|
|
251
|
+
height: number;
|
|
252
|
+
/** Block-aligned dimensions the compressed `data` actually covers. */
|
|
253
|
+
paddedWidth: number;
|
|
254
|
+
paddedHeight: number;
|
|
255
|
+
data: Uint8Array;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Raw RGBA8 pixel data (e.g. a CPU-generated mip level). */
|
|
259
|
+
interface RawPixelSource {
|
|
260
|
+
data: ArrayBufferView;
|
|
261
|
+
width: number;
|
|
262
|
+
height: number;
|
|
263
|
+
}
|
|
264
|
+
/** Anything the WebGL encoders can upload as the source image. */
|
|
265
|
+
type WebGLEncoderImageSource = ImageBitmap | ImageData | HTMLImageElement | HTMLCanvasElement | OffscreenCanvas | RawPixelSource;
|
|
266
|
+
interface WebGLEncodeBytesResult {
|
|
267
|
+
width: number;
|
|
268
|
+
height: number;
|
|
269
|
+
paddedWidth: number;
|
|
270
|
+
paddedHeight: number;
|
|
271
|
+
data: Uint8Array;
|
|
272
|
+
encodeMs: number;
|
|
273
|
+
}
|
|
274
|
+
interface WebGLEncoderOptions {
|
|
275
|
+
gl: WebGL2RenderingContext;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Constructor shape for the concrete subclasses; lets the static `create()`
|
|
279
|
+
* narrow its return type to the subclass (as the WebGPU `EncoderConstructor`
|
|
280
|
+
* does).
|
|
281
|
+
*/
|
|
282
|
+
type WebGLEncoderConstructor<T extends WebGLBlockEncoder = WebGLBlockEncoder> = {
|
|
283
|
+
new (opts: WebGLEncoderOptions): T;
|
|
284
|
+
create(gl?: WebGL2RenderingContext | null): T;
|
|
285
|
+
};
|
|
286
|
+
declare abstract class WebGLBlockEncoder {
|
|
287
|
+
/**
|
|
288
|
+
* Create an encoder on the shared process-wide context (or a caller-supplied
|
|
289
|
+
* one). Throws when WebGL2 is unavailable. The `this:` annotation lets
|
|
290
|
+
* `BC7WebGLEncoder.create()` return `BC7WebGLEncoder`.
|
|
291
|
+
*/
|
|
292
|
+
static create<T extends WebGLBlockEncoder>(this: WebGLEncoderConstructor<T>, gl?: WebGL2RenderingContext | null): T;
|
|
293
|
+
readonly gl: WebGL2RenderingContext;
|
|
294
|
+
protected _program: WebGLProgram;
|
|
295
|
+
protected _vao: WebGLVertexArrayObject;
|
|
296
|
+
protected _uSrc: WebGLUniformLocation | null;
|
|
297
|
+
protected _uSrcSize: WebGLUniformLocation | null;
|
|
298
|
+
protected _uFlipY: WebGLUniformLocation | null;
|
|
299
|
+
constructor({ gl }: WebGLEncoderOptions);
|
|
300
|
+
/** Short lowercase identifier for labels / errors. */
|
|
301
|
+
abstract get label(): string;
|
|
302
|
+
/** 8 for BC1, 16 for BC5/BC7/ASTC 4×4. */
|
|
303
|
+
abstract get bytesPerBlock(): number;
|
|
304
|
+
/** Whether this format has an sRGB variant (false for BC5). */
|
|
305
|
+
abstract get supportsSrgb(): boolean;
|
|
306
|
+
/** GLSL ES 3.00 fragment-shader source. */
|
|
307
|
+
abstract fragSource(): string;
|
|
308
|
+
/** Three.js `CompressedPixelFormat` constant; sRGB is carried by colorSpace. */
|
|
309
|
+
abstract threeTextureFormat(): CompressedPixelFormat;
|
|
310
|
+
protected _buildProgram(): void;
|
|
311
|
+
/** Release the GL program + VAO. The shared context itself is left intact. */
|
|
312
|
+
destroy(): void;
|
|
313
|
+
/**
|
|
314
|
+
* Upload the source image to a freshly created RGBA8 texture bound on unit 0.
|
|
315
|
+
* Raw pixel sources (ImageData / mip levels) go through the typed-array
|
|
316
|
+
* overload; DOM sources (ImageBitmap / canvas / image) through the element
|
|
317
|
+
* overload. No flip / premultiply / colour conversion — flipY is applied in
|
|
318
|
+
* the shader so each mip level flips by its own height.
|
|
319
|
+
*/
|
|
320
|
+
protected _uploadSource(source: WebGLEncoderImageSource, width: number, height: number): WebGLTexture;
|
|
321
|
+
/**
|
|
322
|
+
* Encode one image source to raw compressed bytes. `flipY` samples the source
|
|
323
|
+
* bottom-up (matching Three.js's convention) and is applied in the shader;
|
|
324
|
+
* the high-level mipped path bakes the flip into level 0 and passes false.
|
|
325
|
+
*/
|
|
326
|
+
encodeToBytes(source: WebGLEncoderImageSource, { flipY }?: {
|
|
327
|
+
flipY?: boolean;
|
|
328
|
+
}): WebGLEncodeBytesResult;
|
|
329
|
+
/** Wrap pre-encoded levels into a CompressedTexture. Shared with the WebGPU path. */
|
|
330
|
+
buildMippedTexture(levels: readonly EncodedLevel[], { colorSpace }?: {
|
|
331
|
+
colorSpace?: 'srgb' | 'linear';
|
|
332
|
+
}): CompressedTexture;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
declare class BC1WebGLEncoder extends WebGLBlockEncoder {
|
|
336
|
+
get label(): string;
|
|
337
|
+
get bytesPerBlock(): number;
|
|
338
|
+
get supportsSrgb(): boolean;
|
|
339
|
+
fragSource(): string;
|
|
340
|
+
threeTextureFormat(): CompressedPixelFormat;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
declare class BC5WebGLEncoder extends WebGLBlockEncoder {
|
|
344
|
+
get label(): string;
|
|
345
|
+
get bytesPerBlock(): number;
|
|
346
|
+
get supportsSrgb(): boolean;
|
|
347
|
+
fragSource(): string;
|
|
348
|
+
threeTextureFormat(): CompressedPixelFormat;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
declare class BC7WebGLEncoder extends WebGLBlockEncoder {
|
|
352
|
+
get label(): string;
|
|
353
|
+
get bytesPerBlock(): number;
|
|
354
|
+
get supportsSrgb(): boolean;
|
|
355
|
+
fragSource(): string;
|
|
356
|
+
threeTextureFormat(): CompressedPixelFormat;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
declare class ASTC4x4WebGLEncoder extends WebGLBlockEncoder {
|
|
360
|
+
get label(): string;
|
|
361
|
+
get bytesPerBlock(): number;
|
|
362
|
+
get supportsSrgb(): boolean;
|
|
363
|
+
fragSource(): string;
|
|
364
|
+
threeTextureFormat(): CompressedPixelFormat;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Create a fresh WebGL2 context backed by an off-screen 1×1 canvas. Returns
|
|
369
|
+
* null when neither `OffscreenCanvas` nor `document` is available (e.g. a
|
|
370
|
+
* non-DOM worker without OffscreenCanvas) or when the platform has no WebGL2.
|
|
371
|
+
*/
|
|
372
|
+
declare function createWebGLContext(): WebGL2RenderingContext | null;
|
|
373
|
+
/**
|
|
374
|
+
* Lazily-created context shared across the high-level fallback path. Re-created
|
|
375
|
+
* if the previous one was lost (tab backgrounding, GPU reset). Returns null
|
|
376
|
+
* when WebGL2 is unavailable on this platform.
|
|
377
|
+
*/
|
|
378
|
+
declare function getSharedWebGLContext(): WebGL2RenderingContext | null;
|
|
379
|
+
/** True when a WebGL2 context can be created on this platform. */
|
|
380
|
+
declare function isWebGLAvailable(): boolean;
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Minimal structural type for the extension source: only `getExtension` is
|
|
384
|
+
* touched, so tests can pass a `{ getExtension }` stub instead of a real
|
|
385
|
+
* `WebGL2RenderingContext`.
|
|
386
|
+
*/
|
|
387
|
+
interface ExtensionProvider {
|
|
388
|
+
getExtension(name: string): unknown;
|
|
389
|
+
}
|
|
390
|
+
interface WebGLCapabilities {
|
|
391
|
+
/** EXT_texture_compression_bptc → BC7 (BPTC). */
|
|
392
|
+
bptc: boolean;
|
|
393
|
+
/** EXT_texture_compression_rgtc → BC5 (RGTC2). */
|
|
394
|
+
rgtc: boolean;
|
|
395
|
+
/** WEBGL_compressed_texture_s3tc → BC1 (DXT1). */
|
|
396
|
+
s3tc: boolean;
|
|
397
|
+
/** WEBGL_compressed_texture_s3tc_srgb → sRGB DXT1 (separate extension). */
|
|
398
|
+
s3tcSrgb: boolean;
|
|
399
|
+
/** WEBGL_compressed_texture_astc → ASTC 4×4 (and other footprints). */
|
|
400
|
+
astc: boolean;
|
|
401
|
+
}
|
|
402
|
+
declare function detectWebGLCapabilities(gl: ExtensionProvider): WebGLCapabilities;
|
|
403
|
+
|
|
247
404
|
/**
|
|
248
405
|
* How the texture will be used in the renderer. Drives format choice.
|
|
249
406
|
* • 'color' — RGB albedo-like data (alpha optional / ignored).
|
|
@@ -269,6 +426,16 @@ interface FormatSelection {
|
|
|
269
426
|
}
|
|
270
427
|
declare function selectFormat(adapter: FeatureProvider, hint: TextureHint, options?: SelectFormatOptions): FormatSelection;
|
|
271
428
|
|
|
429
|
+
interface WebGLFormatSelection {
|
|
430
|
+
/** null = no compressed path available on this WebGL context. */
|
|
431
|
+
format: TextureFormat | null;
|
|
432
|
+
/** null when `format` is null. */
|
|
433
|
+
encoderClass: WebGLEncoderConstructor | null;
|
|
434
|
+
/** True when the chosen path is ASTC *and* the hint is 'normal' (caller must pre-swizzle). */
|
|
435
|
+
astcNormalRemap: boolean;
|
|
436
|
+
}
|
|
437
|
+
declare function selectWebGLFormat(caps: WebGLCapabilities, hint: TextureHint, options?: SelectFormatOptions): WebGLFormatSelection;
|
|
438
|
+
|
|
272
439
|
/**
|
|
273
440
|
* Everything `compressTexture()` can take as an image source. A superset
|
|
274
441
|
* of `EncoderImageSource` (see Encoder.ts) that also accepts URL strings
|
|
@@ -287,21 +454,28 @@ interface CompressOptions {
|
|
|
287
454
|
/**
|
|
288
455
|
* Encode quality / speed trade-off. 'fast' (default) is ~2–4× faster for a
|
|
289
456
|
* ≤0.36 dB PSNR cost; 'high' runs the exhaustive search (output identical to
|
|
290
|
-
* the CPU reference encoders). No effect on BC1
|
|
457
|
+
* the CPU reference encoders). No effect on BC1 or on the WebGL fallback
|
|
458
|
+
* (which always uses the fast encoders).
|
|
291
459
|
*/
|
|
292
460
|
quality?: EncodeQuality;
|
|
293
461
|
/** Reuse an existing device (e.g. Three.js's renderer device) instead
|
|
294
|
-
* of creating a new one. When provided, the encoder
|
|
462
|
+
* of creating a new one. WebGPU path only. When provided, the encoder
|
|
463
|
+
* never destroys it. */
|
|
295
464
|
device?: GPUDevice;
|
|
296
465
|
adapter?: GPUAdapter;
|
|
297
466
|
}
|
|
298
467
|
interface CompressResult {
|
|
299
|
-
/** CompressedTexture on
|
|
468
|
+
/** CompressedTexture on a compressed path; Texture on RGBA8 fallback. */
|
|
300
469
|
texture: Texture | CompressedTexture;
|
|
301
470
|
/** The compressed format selected, or null when we fell back to RGBA8. */
|
|
302
471
|
format: TextureFormat | null;
|
|
303
472
|
/** True iff we returned an uncompressed Texture because no encoder fit. */
|
|
304
473
|
fallbackUncompressed: boolean;
|
|
474
|
+
/**
|
|
475
|
+
* Which backend produced the result. 'webgpu' = compute path, 'webgl' =
|
|
476
|
+
* fragment-shader fallback, 'none' = uncompressed RGBA8.
|
|
477
|
+
*/
|
|
478
|
+
backend: 'webgpu' | 'webgl' | 'none';
|
|
305
479
|
/**
|
|
306
480
|
* True iff the chosen format is ASTC and the hint was 'normal'. The
|
|
307
481
|
* caller must apply the (R, W) → (x, y) swizzle in the material — ASTC
|
|
@@ -376,4 +550,4 @@ declare function generateMipChain(level0: MipLevel): MipLevel[];
|
|
|
376
550
|
*/
|
|
377
551
|
declare function padToBlockMultiple(level: MipLevel): MipLevel;
|
|
378
552
|
|
|
379
|
-
export { ASTC4x4Encoder, BC1Encoder, BC5Encoder, BC7Encoder, type Capabilities, type CompressOptions, type CompressResult, type CompressTextureSource, type EncodeBytesResult, type EncodeCallOptions, type EncodeQuality, type EncodeResult, Encoder, type EncoderConstructor, type EncoderImageSource, type EncoderOptions, type FeatureProvider, type FormatSelection, type FormatVariant, GputexLoader, type MipLevel, type SelectFormatOptions, TextureFormat, type TextureHint, WebGPUFeature, compressTexture, detectCapabilities, generateMipChain, padToBlockMultiple, selectFormat };
|
|
553
|
+
export { ASTC4x4Encoder, ASTC4x4WebGLEncoder, BC1Encoder, BC1WebGLEncoder, BC5Encoder, BC5WebGLEncoder, BC7Encoder, BC7WebGLEncoder, type Capabilities, type CompressOptions, type CompressResult, type CompressTextureSource, type EncodeBytesResult, type EncodeCallOptions, type EncodeQuality, type EncodeResult, Encoder, type EncoderConstructor, type EncoderImageSource, type EncoderOptions, type ExtensionProvider, type FeatureProvider, type FormatSelection, type FormatVariant, GputexLoader, type MipLevel, type RawPixelSource, type SelectFormatOptions, TextureFormat, type TextureHint, WebGLBlockEncoder, type WebGLCapabilities, type WebGLEncodeBytesResult, type WebGLEncoderConstructor, type WebGLEncoderImageSource, type WebGLEncoderOptions, type WebGLFormatSelection, WebGPUFeature, compressTexture, createWebGLContext, detectCapabilities, detectWebGLCapabilities, generateMipChain, getSharedWebGLContext, isWebGLAvailable, padToBlockMultiple, selectFormat, selectWebGLFormat };
|
package/dist/index.js
CHANGED
|
@@ -42,14 +42,39 @@ function detectCapabilities(adapter) {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
// src/Encoder.ts
|
|
45
|
+
import { CompressedTexture as CompressedTexture2, LinearFilter as LinearFilter2, LinearSRGBColorSpace as LinearSRGBColorSpace2, SRGBColorSpace as SRGBColorSpace2, RepeatWrapping as RepeatWrapping2 } from "three";
|
|
46
|
+
|
|
47
|
+
// src/textureAssembly.ts
|
|
45
48
|
import {
|
|
46
49
|
CompressedTexture,
|
|
47
50
|
LinearFilter,
|
|
48
51
|
LinearMipmapLinearFilter,
|
|
49
52
|
LinearSRGBColorSpace,
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
RepeatWrapping,
|
|
54
|
+
SRGBColorSpace
|
|
52
55
|
} from "three";
|
|
56
|
+
function assembleCompressedTexture(levels, threeFormat, effectiveSrgb) {
|
|
57
|
+
if (levels.length === 0) {
|
|
58
|
+
throw new Error("assembleCompressedTexture: no levels provided");
|
|
59
|
+
}
|
|
60
|
+
const mipmaps = levels.map((l) => ({
|
|
61
|
+
data: l.data,
|
|
62
|
+
width: l.paddedWidth,
|
|
63
|
+
height: l.paddedHeight
|
|
64
|
+
}));
|
|
65
|
+
const base = levels[0];
|
|
66
|
+
const texture = new CompressedTexture(mipmaps, base.paddedWidth, base.paddedHeight, threeFormat);
|
|
67
|
+
texture.colorSpace = effectiveSrgb ? SRGBColorSpace : LinearSRGBColorSpace;
|
|
68
|
+
texture.magFilter = LinearFilter;
|
|
69
|
+
texture.minFilter = levels.length > 1 ? LinearMipmapLinearFilter : LinearFilter;
|
|
70
|
+
texture.generateMipmaps = false;
|
|
71
|
+
texture.wrapS = texture.wrapT = RepeatWrapping;
|
|
72
|
+
texture.needsUpdate = true;
|
|
73
|
+
texture.userData.logicalWidth = base.width;
|
|
74
|
+
texture.userData.logicalHeight = base.height;
|
|
75
|
+
texture.userData.mipLevels = levels.length;
|
|
76
|
+
return texture;
|
|
77
|
+
}
|
|
53
78
|
|
|
54
79
|
// src/workarounds.ts
|
|
55
80
|
function needsWriteTextureWorkaround(adapter) {
|
|
@@ -228,12 +253,12 @@ var Encoder = class {
|
|
|
228
253
|
width: bytes.paddedWidth,
|
|
229
254
|
height: bytes.paddedHeight
|
|
230
255
|
};
|
|
231
|
-
const texture = new
|
|
232
|
-
texture.colorSpace = effectiveSrgb ?
|
|
233
|
-
texture.magFilter =
|
|
234
|
-
texture.minFilter =
|
|
256
|
+
const texture = new CompressedTexture2([mip], bytes.paddedWidth, bytes.paddedHeight, threeFormat);
|
|
257
|
+
texture.colorSpace = effectiveSrgb ? SRGBColorSpace2 : LinearSRGBColorSpace2;
|
|
258
|
+
texture.magFilter = LinearFilter2;
|
|
259
|
+
texture.minFilter = LinearFilter2;
|
|
235
260
|
texture.generateMipmaps = false;
|
|
236
|
-
texture.wrapS = texture.wrapT =
|
|
261
|
+
texture.wrapS = texture.wrapT = RepeatWrapping2;
|
|
237
262
|
texture.needsUpdate = true;
|
|
238
263
|
texture.userData.logicalWidth = bytes.width;
|
|
239
264
|
texture.userData.logicalHeight = bytes.height;
|
|
@@ -341,23 +366,7 @@ var Encoder = class {
|
|
|
341
366
|
}
|
|
342
367
|
const effectiveSrgb = colorSpace === "srgb" && this.supportsSrgb;
|
|
343
368
|
const threeFormat = this.threeTextureFormat({ colorSpace: effectiveSrgb ? "srgb" : "linear" });
|
|
344
|
-
|
|
345
|
-
data: l.data,
|
|
346
|
-
width: l.paddedWidth,
|
|
347
|
-
height: l.paddedHeight
|
|
348
|
-
}));
|
|
349
|
-
const base = levels[0];
|
|
350
|
-
const texture = new CompressedTexture(mipmaps, base.paddedWidth, base.paddedHeight, threeFormat);
|
|
351
|
-
texture.colorSpace = effectiveSrgb ? SRGBColorSpace : LinearSRGBColorSpace;
|
|
352
|
-
texture.magFilter = LinearFilter;
|
|
353
|
-
texture.minFilter = levels.length > 1 ? LinearMipmapLinearFilter : LinearFilter;
|
|
354
|
-
texture.generateMipmaps = false;
|
|
355
|
-
texture.wrapS = texture.wrapT = RepeatWrapping;
|
|
356
|
-
texture.needsUpdate = true;
|
|
357
|
-
texture.userData.logicalWidth = base.width;
|
|
358
|
-
texture.userData.logicalHeight = base.height;
|
|
359
|
-
texture.userData.mipLevels = levels.length;
|
|
360
|
-
return texture;
|
|
369
|
+
return assembleCompressedTexture(levels, threeFormat, effectiveSrgb);
|
|
361
370
|
}
|
|
362
371
|
};
|
|
363
372
|
|
|
@@ -585,6 +594,373 @@ var ASTC4x4Encoder = class extends Encoder {
|
|
|
585
594
|
}
|
|
586
595
|
};
|
|
587
596
|
|
|
597
|
+
// src/webgl/glsl/fullscreen.vert.glsl
|
|
598
|
+
var fullscreen_vert_default = "#version 300 es\n// Fullscreen-triangle vertex shader for the WebGL block encoders.\n//\n// Draws a single oversized triangle covering the viewport from gl_VertexID\n// alone \u2014 no vertex buffers / attributes needed (drawArrays(TRIANGLES, 0, 3)).\n// The encoder sets the viewport to (blocks_x \xD7 blocks_y), so each rasterised\n// fragment corresponds to exactly one 4\xD74 output block.\n//\n// id 0 -> (-1,-1) id 1 -> ( 3,-1) id 2 -> (-1, 3)\n\nvoid main() {\n vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));\n gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);\n}\n";
|
|
599
|
+
|
|
600
|
+
// src/webgl/webglContext.ts
|
|
601
|
+
var CONTEXT_ATTRS = {
|
|
602
|
+
alpha: false,
|
|
603
|
+
antialias: false,
|
|
604
|
+
depth: false,
|
|
605
|
+
stencil: false,
|
|
606
|
+
premultipliedAlpha: false,
|
|
607
|
+
preserveDrawingBuffer: false,
|
|
608
|
+
// Encoding is GPU-bound; prefer the discrete GPU when the browser exposes a
|
|
609
|
+
// choice. Ignored where unsupported.
|
|
610
|
+
powerPreference: "high-performance"
|
|
611
|
+
};
|
|
612
|
+
function createWebGLContext() {
|
|
613
|
+
if (typeof OffscreenCanvas !== "undefined") {
|
|
614
|
+
const gl = new OffscreenCanvas(1, 1).getContext("webgl2", CONTEXT_ATTRS);
|
|
615
|
+
return gl ?? null;
|
|
616
|
+
}
|
|
617
|
+
if (typeof document !== "undefined") {
|
|
618
|
+
return document.createElement("canvas").getContext("webgl2", CONTEXT_ATTRS);
|
|
619
|
+
}
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
var sharedContext;
|
|
623
|
+
function getSharedWebGLContext() {
|
|
624
|
+
if (sharedContext === void 0 || sharedContext !== null && sharedContext.isContextLost()) {
|
|
625
|
+
sharedContext = createWebGLContext();
|
|
626
|
+
}
|
|
627
|
+
return sharedContext;
|
|
628
|
+
}
|
|
629
|
+
function isWebGLAvailable() {
|
|
630
|
+
return getSharedWebGLContext() !== null;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// src/webgl/WebGLBlockEncoder.ts
|
|
634
|
+
function compileShader(gl, type, source, label) {
|
|
635
|
+
const shader = gl.createShader(type);
|
|
636
|
+
if (!shader) throw new Error(`${label}: gl.createShader failed`);
|
|
637
|
+
gl.shaderSource(shader, source);
|
|
638
|
+
gl.compileShader(shader);
|
|
639
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
640
|
+
const log = gl.getShaderInfoLog(shader);
|
|
641
|
+
gl.deleteShader(shader);
|
|
642
|
+
const kind = type === gl.VERTEX_SHADER ? "vertex" : "fragment";
|
|
643
|
+
throw new Error(`${label}: ${kind} shader compile failed: ${log}`);
|
|
644
|
+
}
|
|
645
|
+
return shader;
|
|
646
|
+
}
|
|
647
|
+
var WebGLBlockEncoder = class {
|
|
648
|
+
/**
|
|
649
|
+
* Create an encoder on the shared process-wide context (or a caller-supplied
|
|
650
|
+
* one). Throws when WebGL2 is unavailable. The `this:` annotation lets
|
|
651
|
+
* `BC7WebGLEncoder.create()` return `BC7WebGLEncoder`.
|
|
652
|
+
*/
|
|
653
|
+
static create(gl) {
|
|
654
|
+
const ctx = gl ?? getSharedWebGLContext();
|
|
655
|
+
if (!ctx) throw new Error("WebGL2 not available in this environment");
|
|
656
|
+
return new this({ gl: ctx });
|
|
657
|
+
}
|
|
658
|
+
gl;
|
|
659
|
+
// Set in _buildProgram(), which the constructor calls.
|
|
660
|
+
_program;
|
|
661
|
+
_vao;
|
|
662
|
+
_uSrc = null;
|
|
663
|
+
_uSrcSize = null;
|
|
664
|
+
_uFlipY = null;
|
|
665
|
+
constructor({ gl }) {
|
|
666
|
+
this.gl = gl;
|
|
667
|
+
this._buildProgram();
|
|
668
|
+
}
|
|
669
|
+
_buildProgram() {
|
|
670
|
+
const gl = this.gl;
|
|
671
|
+
const program = gl.createProgram();
|
|
672
|
+
const vao = gl.createVertexArray();
|
|
673
|
+
if (!program || !vao) throw new Error(`${this.label}: failed to allocate WebGL program/VAO`);
|
|
674
|
+
const vert = compileShader(gl, gl.VERTEX_SHADER, fullscreen_vert_default, this.label);
|
|
675
|
+
const frag = compileShader(gl, gl.FRAGMENT_SHADER, this.fragSource(), this.label);
|
|
676
|
+
gl.attachShader(program, vert);
|
|
677
|
+
gl.attachShader(program, frag);
|
|
678
|
+
gl.linkProgram(program);
|
|
679
|
+
gl.deleteShader(vert);
|
|
680
|
+
gl.deleteShader(frag);
|
|
681
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
682
|
+
const log = gl.getProgramInfoLog(program);
|
|
683
|
+
gl.deleteProgram(program);
|
|
684
|
+
throw new Error(`${this.label}: WebGL program link failed: ${log}`);
|
|
685
|
+
}
|
|
686
|
+
this._program = program;
|
|
687
|
+
this._vao = vao;
|
|
688
|
+
this._uSrc = gl.getUniformLocation(program, "uSrc");
|
|
689
|
+
this._uSrcSize = gl.getUniformLocation(program, "uSrcSize");
|
|
690
|
+
this._uFlipY = gl.getUniformLocation(program, "uFlipY");
|
|
691
|
+
}
|
|
692
|
+
/** Release the GL program + VAO. The shared context itself is left intact. */
|
|
693
|
+
destroy() {
|
|
694
|
+
const gl = this.gl;
|
|
695
|
+
if (gl.isContextLost()) return;
|
|
696
|
+
gl.deleteProgram(this._program);
|
|
697
|
+
gl.deleteVertexArray(this._vao);
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Upload the source image to a freshly created RGBA8 texture bound on unit 0.
|
|
701
|
+
* Raw pixel sources (ImageData / mip levels) go through the typed-array
|
|
702
|
+
* overload; DOM sources (ImageBitmap / canvas / image) through the element
|
|
703
|
+
* overload. No flip / premultiply / colour conversion — flipY is applied in
|
|
704
|
+
* the shader so each mip level flips by its own height.
|
|
705
|
+
*/
|
|
706
|
+
_uploadSource(source, width, height) {
|
|
707
|
+
const gl = this.gl;
|
|
708
|
+
const tex = gl.createTexture();
|
|
709
|
+
if (!tex) throw new Error(`${this.label}: gl.createTexture failed`);
|
|
710
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
711
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
712
|
+
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
|
|
713
|
+
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
|
|
714
|
+
gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, gl.NONE);
|
|
715
|
+
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
|
716
|
+
const raw = source;
|
|
717
|
+
if (raw.data && ArrayBuffer.isView(raw.data)) {
|
|
718
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, raw.data);
|
|
719
|
+
} else {
|
|
720
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, gl.RGBA, gl.UNSIGNED_BYTE, source);
|
|
721
|
+
}
|
|
722
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
723
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
724
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
725
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
726
|
+
return tex;
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Encode one image source to raw compressed bytes. `flipY` samples the source
|
|
730
|
+
* bottom-up (matching Three.js's convention) and is applied in the shader;
|
|
731
|
+
* the high-level mipped path bakes the flip into level 0 and passes false.
|
|
732
|
+
*/
|
|
733
|
+
encodeToBytes(source, { flipY = false } = {}) {
|
|
734
|
+
const gl = this.gl;
|
|
735
|
+
if (gl.isContextLost()) throw new Error(`${this.label}WebGLEncoder: WebGL context lost`);
|
|
736
|
+
const width = source.width;
|
|
737
|
+
const height = source.height;
|
|
738
|
+
if (!width || !height) {
|
|
739
|
+
throw new Error(`${this.label}WebGLEncoder: source has no dimensions`);
|
|
740
|
+
}
|
|
741
|
+
const paddedWidth = width + 3 & ~3;
|
|
742
|
+
const paddedHeight = height + 3 & ~3;
|
|
743
|
+
const blocksX = paddedWidth >> 2;
|
|
744
|
+
const blocksY = paddedHeight >> 2;
|
|
745
|
+
const blockCount = blocksX * blocksY;
|
|
746
|
+
const outByteLen = blockCount * this.bytesPerBlock;
|
|
747
|
+
const t0 = performance.now();
|
|
748
|
+
const srcTex = this._uploadSource(source, width, height);
|
|
749
|
+
const outTex = gl.createTexture();
|
|
750
|
+
const fbo = gl.createFramebuffer();
|
|
751
|
+
if (!outTex || !fbo) {
|
|
752
|
+
gl.deleteTexture(srcTex);
|
|
753
|
+
if (outTex) gl.deleteTexture(outTex);
|
|
754
|
+
if (fbo) gl.deleteFramebuffer(fbo);
|
|
755
|
+
throw new Error(`${this.label}: failed to allocate output texture/framebuffer`);
|
|
756
|
+
}
|
|
757
|
+
gl.bindTexture(gl.TEXTURE_2D, outTex);
|
|
758
|
+
gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA32UI, blocksX, blocksY);
|
|
759
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
760
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
761
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
|
|
762
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, outTex, 0);
|
|
763
|
+
const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
|
|
764
|
+
if (status !== gl.FRAMEBUFFER_COMPLETE) {
|
|
765
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
766
|
+
gl.deleteFramebuffer(fbo);
|
|
767
|
+
gl.deleteTexture(outTex);
|
|
768
|
+
gl.deleteTexture(srcTex);
|
|
769
|
+
throw new Error(`${this.label}: integer framebuffer incomplete (0x${status.toString(16)})`);
|
|
770
|
+
}
|
|
771
|
+
gl.useProgram(this._program);
|
|
772
|
+
gl.bindVertexArray(this._vao);
|
|
773
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
774
|
+
gl.bindTexture(gl.TEXTURE_2D, srcTex);
|
|
775
|
+
gl.uniform1i(this._uSrc, 0);
|
|
776
|
+
gl.uniform2i(this._uSrcSize, width, height);
|
|
777
|
+
gl.uniform1i(this._uFlipY, flipY ? 1 : 0);
|
|
778
|
+
gl.disable(gl.BLEND);
|
|
779
|
+
gl.disable(gl.DEPTH_TEST);
|
|
780
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
781
|
+
gl.viewport(0, 0, blocksX, blocksY);
|
|
782
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
783
|
+
const words = new Uint32Array(blockCount * 4);
|
|
784
|
+
gl.readPixels(0, 0, blocksX, blocksY, gl.RGBA_INTEGER, gl.UNSIGNED_INT, words);
|
|
785
|
+
let data;
|
|
786
|
+
if (this.bytesPerBlock === 16) {
|
|
787
|
+
data = new Uint8Array(words.buffer, 0, outByteLen);
|
|
788
|
+
} else {
|
|
789
|
+
const packed = new Uint32Array(blockCount * 2);
|
|
790
|
+
for (let k = 0; k < blockCount; k++) {
|
|
791
|
+
packed[k * 2] = words[k * 4];
|
|
792
|
+
packed[k * 2 + 1] = words[k * 4 + 1];
|
|
793
|
+
}
|
|
794
|
+
data = new Uint8Array(packed.buffer, 0, outByteLen);
|
|
795
|
+
}
|
|
796
|
+
const encodeMs = performance.now() - t0;
|
|
797
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
798
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
799
|
+
gl.bindVertexArray(null);
|
|
800
|
+
gl.deleteFramebuffer(fbo);
|
|
801
|
+
gl.deleteTexture(outTex);
|
|
802
|
+
gl.deleteTexture(srcTex);
|
|
803
|
+
return { width, height, paddedWidth, paddedHeight, data, encodeMs };
|
|
804
|
+
}
|
|
805
|
+
/** Wrap pre-encoded levels into a CompressedTexture. Shared with the WebGPU path. */
|
|
806
|
+
buildMippedTexture(levels, { colorSpace = "srgb" } = {}) {
|
|
807
|
+
if (levels.length === 0) {
|
|
808
|
+
throw new Error(`${this.label}WebGLEncoder.buildMippedTexture: no levels provided`);
|
|
809
|
+
}
|
|
810
|
+
const effectiveSrgb = colorSpace === "srgb" && this.supportsSrgb;
|
|
811
|
+
return assembleCompressedTexture(levels, this.threeTextureFormat(), effectiveSrgb);
|
|
812
|
+
}
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
// src/webgl/BC1WebGLEncoder.ts
|
|
816
|
+
import { RGBA_S3TC_DXT1_Format as RGBA_S3TC_DXT1_Format2 } from "three";
|
|
817
|
+
|
|
818
|
+
// 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. Algorithm mirrors bc1.wgsl line-for-line:\n// bbox endpoints, 1/16 inset, RGB565 quantisation, forced 4-colour mode, full\n// L2 index search. See bc1.wgsl for the detailed rationale.\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\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 float r8 = (r * 527.0 + 23.0) / 256.0;\n float g8 = (g * 259.0 + 33.0) / 256.0;\n float b8 = (b * 527.0 + 23.0) / 256.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 bounding box by ~half an RGB565 cell (1/16) to tighten the\n // quantised 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\n // 4-colour mode requires c0 > c1.\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 vec3 p0 = from565(c0);\n vec3 p1 = from565(c1);\n vec3 p2 = (2.0 * p0 + p1) * (1.0 / 3.0);\n vec3 p3 = (p0 + 2.0 * p1) * (1.0 / 3.0);\n\n uint indices = 0u;\n for (int i = 0; i < 16; i++) {\n vec3 c = pixels[i];\n float d0 = dot(c - p0, c - p0);\n float d1 = dot(c - p1, c - p1);\n float d2 = dot(c - p2, c - p2);\n float d3 = dot(c - p3, c - p3);\n\n float bestD = d0;\n uint bestI = 0u;\n if (d1 < bestD) { bestD = d1; bestI = 1u; }\n if (d2 < bestD) { bestD = d2; bestI = 2u; }\n if (d3 < bestD) { bestD = d3; bestI = 3u; }\n\n indices = indices | (bestI << (i * 2));\n }\n\n outColor = uvec4(c0 | (c1 << 16), indices, 0u, 0u);\n}\n";
|
|
820
|
+
|
|
821
|
+
// src/webgl/BC1WebGLEncoder.ts
|
|
822
|
+
var BC1WebGLEncoder = class extends WebGLBlockEncoder {
|
|
823
|
+
get label() {
|
|
824
|
+
return "bc1";
|
|
825
|
+
}
|
|
826
|
+
get bytesPerBlock() {
|
|
827
|
+
return 8;
|
|
828
|
+
}
|
|
829
|
+
get supportsSrgb() {
|
|
830
|
+
return true;
|
|
831
|
+
}
|
|
832
|
+
fragSource() {
|
|
833
|
+
return bc1_frag_default;
|
|
834
|
+
}
|
|
835
|
+
threeTextureFormat() {
|
|
836
|
+
return RGBA_S3TC_DXT1_Format2;
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
// src/webgl/BC5WebGLEncoder.ts
|
|
841
|
+
import { RED_GREEN_RGTC2_Format as RED_GREEN_RGTC2_Format2 } from "three";
|
|
842
|
+
|
|
843
|
+
// src/webgl/glsl/bc5.frag.glsl
|
|
844
|
+
var bc5_frag_default = "#version 300 es\n// BC5 (RGTC2) fragment-shader encoder \u2014 WebGL2 port of bc5.wgsl (fast path).\n//\n// One fragment per 4\xD74 block \u2192 16-byte BC5 block as 4 \xD7 u32 in outColor.\n// BC5 = two BC4 halves (R then G). This is the *fast* path only: bbox\n// endpoints + a single full-L2 index assignment per channel, no LSQ refit\n// (the WGSL `QUALITY_HIGH` branch). Always emits 6-interpolation mode\n// (red0 > red1). See bc5.wgsl for the full derivation.\n\nprecision highp float;\nprecision highp int;\n\nuniform sampler2D uSrc;\nuniform ivec2 uSrcSize;\nuniform int uFlipY;\n\nlayout(location = 0) out uvec4 outColor;\n\n// 6-interpolation-mode palette weights: pal[j] = W0_6[j]*r0 + W1_6[j]*r1.\nconst float W0_6[8] = float[8](1.0, 0.0, 6.0 / 7.0, 5.0 / 7.0, 4.0 / 7.0, 3.0 / 7.0, 2.0 / 7.0, 1.0 / 7.0);\nconst float W1_6[8] = float[8](0.0, 1.0, 1.0 / 7.0, 2.0 / 7.0, 3.0 / 7.0, 4.0 / 7.0, 5.0 / 7.0, 6.0 / 7.0);\n\nuint quantize8(float v) {\n return uint(clamp(floor(v * 255.0 + 0.5), 0.0, 255.0));\n}\n\n// Encode 16 single-channel values into an 8-byte BC4 block (two little-endian\n// u32s). Mirrors encode_bc4() in bc5.wgsl with the refit pass omitted.\nuvec2 encodeBC4(float values[16]) {\n float vmin = 1.0;\n float vmax = 0.0;\n for (int k = 0; k < 16; k++) {\n vmin = min(vmin, values[k]);\n vmax = max(vmax, values[k]);\n }\n uint r0 = quantize8(vmax);\n uint r1 = quantize8(vmin);\n if (r0 == r1) {\n if (r1 > 0u) { r1 = r1 - 1u; } else { r0 = r0 + 1u; }\n }\n\n float pal[8];\n float r0f = float(r0) / 255.0;\n float r1f = float(r1) / 255.0;\n for (int j = 0; j < 8; j++) {\n pal[j] = W0_6[j] * r0f + W1_6[j] * r1f;\n }\n\n uint indices[16];\n for (int k = 0; k < 16; k++) {\n float v = values[k];\n uint bestJ = 0u;\n float bestD = 1e20;\n for (int j = 0; j < 8; j++) {\n float d = pal[j] - v;\n float d2 = d * d;\n if (d2 < bestD) { bestD = d2; bestJ = uint(j); }\n }\n indices[k] = bestJ;\n }\n\n // Pack the 48-bit index field (bytes 2..7) split across two u32 halves.\n uint idxLo = 0u;\n uint idxHi = 0u;\n for (int k = 0; k < 16; k++) {\n uint bit = 3u * uint(k);\n uint v = indices[k] & 7u;\n if (bit + 3u <= 32u) {\n idxLo = idxLo | (v << bit);\n } else if (bit >= 32u) {\n idxHi = idxHi | (v << (bit - 32u));\n } else {\n idxLo = idxLo | (v << bit);\n idxHi = idxHi | (v >> (32u - bit));\n }\n }\n\n uint outLo = r0 | (r1 << 8) | ((idxLo & 0xFFFFu) << 16);\n uint outHi = (idxLo >> 16) | (idxHi << 16);\n return uvec2(outLo, outHi);\n}\n\nvoid main() {\n ivec2 base = ivec2(gl_FragCoord.xy) * 4;\n ivec2 maxXY = uSrcSize - ivec2(1);\n\n float rValues[16];\n float gValues[16];\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 vec4 c = texelFetch(uSrc, ivec2(p.x, sy), 0);\n rValues[i] = c.r;\n gValues[i] = c.g;\n }\n\n uvec2 rBlock = encodeBC4(rValues);\n uvec2 gBlock = encodeBC4(gValues);\n outColor = uvec4(rBlock.x, rBlock.y, gBlock.x, gBlock.y);\n}\n";
|
|
845
|
+
|
|
846
|
+
// src/webgl/BC5WebGLEncoder.ts
|
|
847
|
+
var BC5WebGLEncoder = class extends WebGLBlockEncoder {
|
|
848
|
+
get label() {
|
|
849
|
+
return "bc5";
|
|
850
|
+
}
|
|
851
|
+
get bytesPerBlock() {
|
|
852
|
+
return 16;
|
|
853
|
+
}
|
|
854
|
+
get supportsSrgb() {
|
|
855
|
+
return false;
|
|
856
|
+
}
|
|
857
|
+
fragSource() {
|
|
858
|
+
return bc5_frag_default;
|
|
859
|
+
}
|
|
860
|
+
threeTextureFormat() {
|
|
861
|
+
return RED_GREEN_RGTC2_Format2;
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
// src/webgl/BC7WebGLEncoder.ts
|
|
866
|
+
import { RGBA_BPTC_Format as RGBA_BPTC_Format2 } from "three";
|
|
867
|
+
|
|
868
|
+
// src/webgl/glsl/bc7.frag.glsl
|
|
869
|
+
var bc7_frag_default = "#version 300 es\n// BC7 (BPTC) mode-6 fragment-shader encoder \u2014 WebGL2 port of bc7.wgsl (fast).\n//\n// One fragment per 4\xD74 block \u2192 16-byte block as 4 \xD7 u32 in outColor. Fast path\n// only: O(N) bbox seed \u2192 endpoints fitted by a single least-squares pass whose\n// normal-equation sums are accumulated during a projection-based index\n// assignment (palette is colinear, so the nearest entry is found by projecting\n// onto the endpoint line \u2014 O(1) per pixel). Mirrors the `QUALITY_HIGH == 0`\n// branch of bc7.wgsl; see that file for the mode-6 bit layout and rationale.\n//\n// Determinism note: the WGSL refit uses round() (half-to-even); here we use\n// floor(x + 0.5) for portability. The two differ only at exact .5 ties, a\n// sub-LSB endpoint nudge that is visually identical.\n\nprecision highp float;\nprecision highp int;\n\nuniform sampler2D uSrc;\nuniform ivec2 uSrcSize;\nuniform int uFlipY;\n\nlayout(location = 0) out uvec4 outColor;\n\n// Per-invocation scratch (mirrors the WGSL function-scope arrays passed by ptr).\nivec4 gPixels[16];\nuint gIdx[16];\n\nstruct QuantPair { ivec4 seven; ivec4 eight; };\nstruct Ep { ivec4 seven; ivec4 eight; uint p; };\nstruct Fit { ivec4 e0; ivec4 e1; bool valid; };\n\nivec4 to8(vec4 v) {\n return ivec4(clamp(floor(v * 255.0 + 0.5), vec4(0.0), vec4(255.0)));\n}\n\nint dist2(ivec4 a, ivec4 b) {\n ivec4 d = a - b;\n ivec4 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.\nQuantPair quantizeEndpoint(ivec4 ideal8, uint p) {\n ivec4 q = ivec4(clamp(floor((vec4(ideal8) - float(p)) / 2.0 + 0.5), vec4(0.0), vec4(127.0)));\n // eff = (q << 1) | p. q*2 is even and p \u2208 {0,1}, so q*2 + p is identical and\n // avoids any vector-shift-by-scalar portability question.\n ivec4 eff = q * 2 + ivec4(int(p));\n return QuantPair(q, eff);\n}\n\n// Endpoint with its chosen p-bit, picked by minimum quantisation error.\nEp pickEp(ivec4 ideal) {\n QuantPair a = quantizeEndpoint(ideal, 0u);\n QuantPair b = quantizeEndpoint(ideal, 1u);\n if (dist2(b.eight, ideal) < dist2(a.eight, ideal)) {\n return Ep(b.seven, b.eight, 1u);\n }\n return Ep(a.seven, a.eight, 0u);\n}\n\n// Projection index assignment over gPixels \u2192 gIdx. When `fit`, accumulate the\n// LSQ normal-equation sums in the same pass and return refitted endpoints.\nFit projAssign(ivec4 pe0, ivec4 pe1, bool fit) {\n Fit res;\n res.e0 = ivec4(0);\n res.e1 = ivec4(0);\n res.valid = false;\n ivec4 dir = pe1 - pe0;\n int dd = dir.x * dir.x + dir.y * dir.y + dir.z * dir.z + dir.w * dir.w;\n if (dd == 0) {\n for (int k = 0; k < 16; k++) { gIdx[k] = 0u; }\n return res;\n }\n float inv = 15.0 / float(dd);\n float sAA = 0.0, sBB = 0.0, sAB = 0.0;\n vec4 sAV = vec4(0.0), sBV = vec4(0.0);\n for (int k = 0; k < 16; k++) {\n ivec4 q = gPixels[k] - pe0;\n float proj = float(q.x * dir.x + q.y * dir.y + q.z * dir.z + q.w * dir.w) * inv;\n float s = clamp(floor(proj + 0.5), 0.0, 15.0);\n gIdx[k] = uint(s);\n if (fit) {\n vec4 v = vec4(gPixels[k]);\n float b = s / 15.0;\n float a = 1.0 - b;\n sAA += a * a; sBB += b * b; sAB += a * b; sAV += a * v; sBV += b * v;\n }\n }\n if (!fit) { return res; }\n float det = sAA * sBB - sAB * sAB;\n if (abs(det) < 1e-9) { return res; }\n res.e0 = ivec4(clamp(floor((sBB * sAV - sAB * sBV) / det + 0.5), vec4(0.0), vec4(255.0)));\n res.e1 = ivec4(clamp(floor((sAA * sBV - sAB * sAV) / det + 0.5), vec4(0.0), vec4(255.0)));\n res.valid = true;\n return res;\n}\n\nvoid writeBits(inout uint block[4], uint pos, uint nbits, uint value) {\n uint v = value & ((1u << nbits) - 1u);\n uint wordLo = pos / 32u;\n uint bitLo = pos % 32u;\n uint bitsInLo = min(nbits, 32u - bitLo);\n uint maskLo = ((1u << bitsInLo) - 1u) << bitLo;\n block[wordLo] = (block[wordLo] & ~maskLo) | ((v << bitLo) & maskLo);\n if (bitsInLo < nbits) {\n uint bitsInHi = nbits - bitsInLo;\n uint maskHi = (1u << bitsInHi) - 1u;\n uint valHi = v >> bitsInLo;\n block[wordLo + 1u] = (block[wordLo + 1u] & ~maskHi) | (valHi & maskHi);\n }\n}\n\nvoid main() {\n ivec2 base = ivec2(gl_FragCoord.xy) * 4;\n ivec2 maxXY = uSrcSize - ivec2(1);\n\n ivec4 lo = ivec4(255);\n ivec4 hi = ivec4(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 ivec4 px = to8(texelFetch(uSrc, ivec2(p.x, sy), 0));\n gPixels[i] = px;\n lo = min(lo, px);\n hi = max(hi, px);\n }\n\n Ep ep0 = pickEp(lo);\n Ep ep1 = pickEp(hi);\n Fit r = projAssign(ep0.eight, ep1.eight, true);\n if (r.valid) {\n ep0 = pickEp(r.e0);\n ep1 = pickEp(r.e1);\n projAssign(ep0.eight, ep1.eight, false);\n }\n ivec4 e0_7 = ep0.seven;\n ivec4 e1_7 = ep1.seven;\n uint p0 = ep0.p;\n uint p1 = ep1.p;\n\n // Anchor rule \u2014 pixel 0's index MSB must be 0; otherwise swap endpoints and\n // reflect every index (decoded image unchanged).\n if ((gIdx[0] & 0x8u) != 0u) {\n ivec4 t = e0_7; e0_7 = e1_7; e1_7 = t;\n uint tp = p0; p0 = p1; p1 = tp;\n for (int k = 0; k < 16; k++) { gIdx[k] = 15u - gIdx[k]; }\n }\n\n uint block[4];\n block[0] = 0u; block[1] = 0u; block[2] = 0u; block[3] = 0u;\n uint pos = 0u;\n writeBits(block, pos, 7u, 0x40u); pos += 7u;\n writeBits(block, pos, 7u, uint(e0_7.x)); pos += 7u;\n writeBits(block, pos, 7u, uint(e1_7.x)); pos += 7u;\n writeBits(block, pos, 7u, uint(e0_7.y)); pos += 7u;\n writeBits(block, pos, 7u, uint(e1_7.y)); pos += 7u;\n writeBits(block, pos, 7u, uint(e0_7.z)); pos += 7u;\n writeBits(block, pos, 7u, uint(e1_7.z)); pos += 7u;\n writeBits(block, pos, 7u, uint(e0_7.w)); pos += 7u;\n writeBits(block, pos, 7u, uint(e1_7.w)); pos += 7u;\n writeBits(block, pos, 1u, p0); pos += 1u;\n writeBits(block, pos, 1u, p1); pos += 1u;\n writeBits(block, pos, 3u, gIdx[0] & 0x7u); pos += 3u;\n for (int k = 1; k < 16; k++) {\n writeBits(block, pos, 4u, gIdx[k] & 0xFu);\n pos += 4u;\n }\n\n outColor = uvec4(block[0], block[1], block[2], block[3]);\n}\n";
|
|
870
|
+
|
|
871
|
+
// src/webgl/BC7WebGLEncoder.ts
|
|
872
|
+
var BC7WebGLEncoder = class extends WebGLBlockEncoder {
|
|
873
|
+
get label() {
|
|
874
|
+
return "bc7";
|
|
875
|
+
}
|
|
876
|
+
get bytesPerBlock() {
|
|
877
|
+
return 16;
|
|
878
|
+
}
|
|
879
|
+
get supportsSrgb() {
|
|
880
|
+
return true;
|
|
881
|
+
}
|
|
882
|
+
fragSource() {
|
|
883
|
+
return bc7_frag_default;
|
|
884
|
+
}
|
|
885
|
+
threeTextureFormat() {
|
|
886
|
+
return RGBA_BPTC_Format2;
|
|
887
|
+
}
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
// src/webgl/ASTC4x4WebGLEncoder.ts
|
|
891
|
+
import { RGBA_ASTC_4x4_Format as RGBA_ASTC_4x4_Format2 } from "three";
|
|
892
|
+
|
|
893
|
+
// src/webgl/glsl/astc4x4.frag.glsl
|
|
894
|
+
var astc4x4_frag_default = "#version 300 es\n// ASTC 4\xD74 LDR fragment-shader encoder \u2014 WebGL2 port of astc4x4.wgsl (fast).\n//\n// One fragment per 4\xD74 block \u2192 16-byte block as 4 \xD7 u32 in outColor. Restricted\n// subset: single partition, no dual-plane, CEM 12 (LDR RGBA direct), 4\xD74 weight\n// grid with 2-bit weights (QUANT_4), 8-bit endpoints (QUANT_256). Fast path:\n// bbox seed \u2192 one LSQ refit fused into a projection weight assignment (4 colinear\n// levels). Mirrors the `QUALITY_HIGH == 0` branch of astc4x4.wgsl; see that file\n// for the 128-bit block layout.\n//\n// Determinism note: floor(x + 0.5) replaces WGSL round() for the refit endpoints\n// (sub-LSB difference at exact .5 ties only).\n\nprecision highp float;\nprecision highp int;\n\nuniform sampler2D uSrc;\nuniform ivec2 uSrcSize;\nuniform int uFlipY;\n\nlayout(location = 0) out uvec4 outColor;\n\nivec4 gPixels[16];\nuint gIdx[16];\n\nstruct Fit { ivec4 e0; ivec4 e1; bool valid; };\n\nivec4 to8(vec4 v) {\n return ivec4(clamp(floor(v * 255.0 + 0.5), vec4(0.0), vec4(255.0)));\n}\n\n// Projection weight assignment over 4 levels (QUANT_4 \u2248 thirds), with the LSQ\n// normal-equation sums accumulated in the same pass for a fused refit.\nFit projAssign(ivec4 pe0, ivec4 pe1, bool fit) {\n Fit res;\n res.e0 = ivec4(0);\n res.e1 = ivec4(0);\n res.valid = false;\n ivec4 dir = pe1 - pe0;\n int dd = dir.x * dir.x + dir.y * dir.y + dir.z * dir.z + dir.w * dir.w;\n if (dd == 0) {\n for (int k = 0; k < 16; k++) { gIdx[k] = 0u; }\n return res;\n }\n float inv = 3.0 / float(dd);\n float sAA = 0.0, sBB = 0.0, sAB = 0.0;\n vec4 sAV = vec4(0.0), sBV = vec4(0.0);\n for (int k = 0; k < 16; k++) {\n ivec4 q = gPixels[k] - pe0;\n float proj = float(q.x * dir.x + q.y * dir.y + q.z * dir.z + q.w * dir.w) * inv;\n float s = clamp(floor(proj + 0.5), 0.0, 3.0);\n gIdx[k] = uint(s);\n if (fit) {\n vec4 v = vec4(gPixels[k]);\n float b = s / 3.0;\n float a = 1.0 - b;\n sAA += a * a; sBB += b * b; sAB += a * b; sAV += a * v; sBV += b * v;\n }\n }\n if (!fit) { return res; }\n float det = sAA * sBB - sAB * sAB;\n if (abs(det) < 1e-9) { return res; }\n res.e0 = ivec4(clamp(floor((sBB * sAV - sAB * sBV) / det + 0.5), vec4(0.0), vec4(255.0)));\n res.e1 = ivec4(clamp(floor((sAA * sBV - sAB * sAV) / det + 0.5), vec4(0.0), vec4(255.0)));\n res.valid = true;\n return res;\n}\n\nvoid writeBits(inout uint block[4], uint pos, uint nbits, uint value) {\n uint v = value & ((1u << nbits) - 1u);\n uint wordLo = pos / 32u;\n uint bitLo = pos % 32u;\n uint bitsInLo = min(nbits, 32u - bitLo);\n uint maskLo = ((1u << bitsInLo) - 1u) << bitLo;\n block[wordLo] = (block[wordLo] & ~maskLo) | ((v << bitLo) & maskLo);\n if (bitsInLo < nbits) {\n uint bitsInHi = nbits - bitsInLo;\n uint maskHi = (1u << bitsInHi) - 1u;\n uint valHi = v >> bitsInLo;\n block[wordLo + 1u] = (block[wordLo + 1u] & ~maskHi) | (valHi & maskHi);\n }\n}\n\nvoid main() {\n ivec2 base = ivec2(gl_FragCoord.xy) * 4;\n ivec2 maxXY = uSrcSize - ivec2(1);\n\n ivec4 lo = ivec4(255);\n ivec4 hi = ivec4(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 ivec4 px = to8(texelFetch(uSrc, ivec2(p.x, sy), 0));\n gPixels[i] = px;\n lo = min(lo, px);\n hi = max(hi, px);\n }\n\n ivec4 e0 = lo;\n ivec4 e1 = hi;\n Fit r = projAssign(e0, e1, true);\n if (r.valid) {\n e0 = r.e0;\n e1 = r.e1;\n projAssign(e0, e1, false);\n }\n\n // Endpoint ordering so the decoder doesn't apply blue contraction.\n if (e0.x + e0.y + e0.z > e1.x + e1.y + e1.z) {\n ivec4 t = e0; e0 = e1; e1 = t;\n for (int k = 0; k < 16; k++) { gIdx[k] = 3u - gIdx[k]; }\n }\n\n uint block[4];\n block[0] = 0u; block[1] = 0u; block[2] = 0u; block[3] = 0u;\n writeBits(block, 0u, 11u, 0x042u);\n writeBits(block, 11u, 2u, 0u);\n writeBits(block, 13u, 4u, 12u);\n writeBits(block, 17u + 0u * 8u, 8u, uint(e0.x));\n writeBits(block, 17u + 1u * 8u, 8u, uint(e1.x));\n writeBits(block, 17u + 2u * 8u, 8u, uint(e0.y));\n writeBits(block, 17u + 3u * 8u, 8u, uint(e1.y));\n writeBits(block, 17u + 4u * 8u, 8u, uint(e0.z));\n writeBits(block, 17u + 5u * 8u, 8u, uint(e1.z));\n writeBits(block, 17u + 6u * 8u, 8u, uint(e0.w));\n writeBits(block, 17u + 7u * 8u, 8u, uint(e1.w));\n\n uint w3 = 0u;\n for (int k = 0; k < 16; k++) {\n uint w = gIdx[k] & 0x3u;\n w3 = w3 | ((w & 1u) << (31u - 2u * uint(k))) | (((w >> 1u) & 1u) << (30u - 2u * uint(k)));\n }\n block[3] = w3;\n\n outColor = uvec4(block[0], block[1], block[2], block[3]);\n}\n";
|
|
895
|
+
|
|
896
|
+
// src/webgl/ASTC4x4WebGLEncoder.ts
|
|
897
|
+
var ASTC4x4WebGLEncoder = class extends WebGLBlockEncoder {
|
|
898
|
+
get label() {
|
|
899
|
+
return "astc4x4";
|
|
900
|
+
}
|
|
901
|
+
get bytesPerBlock() {
|
|
902
|
+
return 16;
|
|
903
|
+
}
|
|
904
|
+
get supportsSrgb() {
|
|
905
|
+
return true;
|
|
906
|
+
}
|
|
907
|
+
fragSource() {
|
|
908
|
+
return astc4x4_frag_default;
|
|
909
|
+
}
|
|
910
|
+
threeTextureFormat() {
|
|
911
|
+
return RGBA_ASTC_4x4_Format2;
|
|
912
|
+
}
|
|
913
|
+
};
|
|
914
|
+
|
|
915
|
+
// src/webgl/webglCapabilities.ts
|
|
916
|
+
function detectWebGLCapabilities(gl) {
|
|
917
|
+
if (!gl || typeof gl.getExtension !== "function") {
|
|
918
|
+
throw new TypeError("detectWebGLCapabilities: a WebGL2 context (or { getExtension }) is required");
|
|
919
|
+
}
|
|
920
|
+
const has = (name) => gl.getExtension(name) != null;
|
|
921
|
+
return {
|
|
922
|
+
bptc: has("EXT_texture_compression_bptc"),
|
|
923
|
+
rgtc: has("EXT_texture_compression_rgtc"),
|
|
924
|
+
s3tc: has("WEBGL_compressed_texture_s3tc"),
|
|
925
|
+
s3tcSrgb: has("WEBGL_compressed_texture_s3tc_srgb"),
|
|
926
|
+
astc: has("WEBGL_compressed_texture_astc")
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// src/webgl/selectWebGLFormat.ts
|
|
931
|
+
var NONE = { format: null, encoderClass: null, astcNormalRemap: false };
|
|
932
|
+
function selectWebGLFormat(caps, hint, options = {}) {
|
|
933
|
+
const { colorSpace = "srgb" } = options;
|
|
934
|
+
const srgb = colorSpace === "srgb";
|
|
935
|
+
const astc = (astcNormalRemap) => ({
|
|
936
|
+
format: srgb ? TextureFormat.ASTC_4x4_SRGB : TextureFormat.ASTC_4x4,
|
|
937
|
+
encoderClass: ASTC4x4WebGLEncoder,
|
|
938
|
+
astcNormalRemap
|
|
939
|
+
});
|
|
940
|
+
if (hint === "normal") {
|
|
941
|
+
if (caps.rgtc) return { format: TextureFormat.BC5, encoderClass: BC5WebGLEncoder, astcNormalRemap: false };
|
|
942
|
+
if (caps.astc) return astc(true);
|
|
943
|
+
return NONE;
|
|
944
|
+
}
|
|
945
|
+
if (caps.bptc) {
|
|
946
|
+
return {
|
|
947
|
+
format: srgb ? TextureFormat.BC7_SRGB : TextureFormat.BC7,
|
|
948
|
+
encoderClass: BC7WebGLEncoder,
|
|
949
|
+
astcNormalRemap: false
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
if (caps.astc) return astc(false);
|
|
953
|
+
if (hint === "color") {
|
|
954
|
+
if (srgb && caps.s3tcSrgb) {
|
|
955
|
+
return { format: TextureFormat.BC1_SRGB, encoderClass: BC1WebGLEncoder, astcNormalRemap: false };
|
|
956
|
+
}
|
|
957
|
+
if (!srgb && caps.s3tc) {
|
|
958
|
+
return { format: TextureFormat.BC1, encoderClass: BC1WebGLEncoder, astcNormalRemap: false };
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
return NONE;
|
|
962
|
+
}
|
|
963
|
+
|
|
588
964
|
// src/selectFormat.ts
|
|
589
965
|
function selectFormat(adapter, hint, options = {}) {
|
|
590
966
|
const { colorSpace = "srgb" } = options;
|
|
@@ -612,7 +988,7 @@ function selectFormat(adapter, hint, options = {}) {
|
|
|
612
988
|
}
|
|
613
989
|
|
|
614
990
|
// src/compressTexture.ts
|
|
615
|
-
import { LinearFilter as
|
|
991
|
+
import { LinearFilter as LinearFilter3, LinearSRGBColorSpace as LinearSRGBColorSpace3, RepeatWrapping as RepeatWrapping3, SRGBColorSpace as SRGBColorSpace3, Texture } from "three";
|
|
616
992
|
|
|
617
993
|
// src/mipgen.ts
|
|
618
994
|
function generateMipChain(level0) {
|
|
@@ -722,10 +1098,10 @@ function mipLevelToImageData(level) {
|
|
|
722
1098
|
}
|
|
723
1099
|
function wrapUncompressed(bitmap, srgb, flipY) {
|
|
724
1100
|
const tex = new Texture(bitmap);
|
|
725
|
-
tex.colorSpace = srgb ?
|
|
726
|
-
tex.magFilter =
|
|
727
|
-
tex.minFilter =
|
|
728
|
-
tex.wrapS = tex.wrapT =
|
|
1101
|
+
tex.colorSpace = srgb ? SRGBColorSpace3 : LinearSRGBColorSpace3;
|
|
1102
|
+
tex.magFilter = LinearFilter3;
|
|
1103
|
+
tex.minFilter = LinearFilter3;
|
|
1104
|
+
tex.wrapS = tex.wrapT = RepeatWrapping3;
|
|
729
1105
|
tex.generateMipmaps = false;
|
|
730
1106
|
tex.flipY = flipY;
|
|
731
1107
|
tex.needsUpdate = true;
|
|
@@ -743,124 +1119,159 @@ async function compressTexture(source, options = {}) {
|
|
|
743
1119
|
} = options;
|
|
744
1120
|
const srgb = colorSpace === "srgb";
|
|
745
1121
|
const bitmap = await sourceToBitmap(source);
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
1122
|
+
const viaWebGPU = await encodeViaWebGPU();
|
|
1123
|
+
if (viaWebGPU) return viaWebGPU;
|
|
1124
|
+
const viaWebGL = encodeViaWebGL();
|
|
1125
|
+
if (viaWebGL) return viaWebGL;
|
|
1126
|
+
console.warn(
|
|
1127
|
+
"[compressTexture] No compressed path available (WebGPU and WebGL2 both lack a usable compressed-texture format); returning uncompressed RGBA8."
|
|
1128
|
+
);
|
|
1129
|
+
const tex = wrapUncompressed(bitmap, srgb, flipY);
|
|
1130
|
+
return {
|
|
1131
|
+
texture: tex,
|
|
1132
|
+
format: null,
|
|
1133
|
+
fallbackUncompressed: true,
|
|
1134
|
+
backend: "none",
|
|
1135
|
+
astcNormalRemap: false,
|
|
1136
|
+
width: bitmap.width,
|
|
1137
|
+
height: bitmap.height,
|
|
1138
|
+
mipLevels: 1,
|
|
1139
|
+
encodeMs: 0,
|
|
1140
|
+
destroy: () => {
|
|
1141
|
+
tex.dispose();
|
|
1142
|
+
}
|
|
1143
|
+
};
|
|
1144
|
+
async function encodeViaWebGPU() {
|
|
1145
|
+
if (!("gpu" in navigator)) return null;
|
|
1146
|
+
const adapter = providedAdapter ?? await navigator.gpu.requestAdapter();
|
|
1147
|
+
if (!adapter) return null;
|
|
1148
|
+
const selection = selectFormat(adapter, hint, { colorSpace });
|
|
1149
|
+
if (!selection.format || !selection.encoderClass) return null;
|
|
1150
|
+
let encoder;
|
|
1151
|
+
if (providedDevice) {
|
|
1152
|
+
const EncoderCtor = selection.encoderClass;
|
|
1153
|
+
encoder = new EncoderCtor({ device: providedDevice, adapter, ownsDevice: false });
|
|
1154
|
+
} else {
|
|
1155
|
+
encoder = await selection.encoderClass.create();
|
|
1156
|
+
}
|
|
1157
|
+
try {
|
|
1158
|
+
const needsWriteTexture = needsWriteTextureWorkaround(adapter);
|
|
1159
|
+
if (!mipmaps) {
|
|
1160
|
+
let bytes;
|
|
1161
|
+
if (needsWriteTexture) {
|
|
1162
|
+
const level02 = bitmapToMipLevel(bitmap, flipY);
|
|
1163
|
+
const imageData = mipLevelToImageData(level02);
|
|
1164
|
+
bytes = await encoder.encodeToBytes(imageData, { quality });
|
|
1165
|
+
} else {
|
|
1166
|
+
bytes = await encoder.encodeToBytes(bitmap, { flipY, quality });
|
|
1167
|
+
}
|
|
1168
|
+
const tex3 = encoder.buildMippedTexture([bytes], { colorSpace });
|
|
1169
|
+
return {
|
|
1170
|
+
texture: tex3,
|
|
1171
|
+
format: selection.format,
|
|
1172
|
+
fallbackUncompressed: false,
|
|
1173
|
+
backend: "webgpu",
|
|
1174
|
+
astcNormalRemap: selection.astcNormalRemap,
|
|
1175
|
+
width: bytes.width,
|
|
1176
|
+
height: bytes.height,
|
|
1177
|
+
mipLevels: 1,
|
|
1178
|
+
encodeMs: bytes.encodeMs,
|
|
1179
|
+
destroy: () => {
|
|
1180
|
+
tex3.dispose();
|
|
1181
|
+
encoder.destroy();
|
|
1182
|
+
}
|
|
1183
|
+
};
|
|
760
1184
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
astcNormalRemap: false,
|
|
772
|
-
width: bitmap.width,
|
|
773
|
-
height: bitmap.height,
|
|
774
|
-
mipLevels: 1,
|
|
775
|
-
encodeMs: 0,
|
|
776
|
-
destroy: () => {
|
|
777
|
-
tex.dispose();
|
|
1185
|
+
const level0 = bitmapToMipLevel(bitmap, flipY);
|
|
1186
|
+
const chain = generateMipChain(level0);
|
|
1187
|
+
const encodedLevels = [];
|
|
1188
|
+
let totalEncodeMs = 0;
|
|
1189
|
+
for (const level of chain) {
|
|
1190
|
+
const padded = padToBlockMultiple(level);
|
|
1191
|
+
const imageData = mipLevelToImageData(padded);
|
|
1192
|
+
const bytes = await encoder.encodeToBytes(imageData, { quality });
|
|
1193
|
+
encodedLevels.push(bytes);
|
|
1194
|
+
totalEncodeMs += bytes.encodeMs;
|
|
778
1195
|
}
|
|
779
|
-
|
|
1196
|
+
const tex2 = encoder.buildMippedTexture(encodedLevels, { colorSpace });
|
|
1197
|
+
return {
|
|
1198
|
+
texture: tex2,
|
|
1199
|
+
format: selection.format,
|
|
1200
|
+
fallbackUncompressed: false,
|
|
1201
|
+
backend: "webgpu",
|
|
1202
|
+
astcNormalRemap: selection.astcNormalRemap,
|
|
1203
|
+
width: level0.width,
|
|
1204
|
+
height: level0.height,
|
|
1205
|
+
mipLevels: encodedLevels.length,
|
|
1206
|
+
encodeMs: totalEncodeMs,
|
|
1207
|
+
destroy: () => {
|
|
1208
|
+
tex2.dispose();
|
|
1209
|
+
encoder.destroy();
|
|
1210
|
+
}
|
|
1211
|
+
};
|
|
1212
|
+
} catch (e) {
|
|
1213
|
+
encoder.destroy();
|
|
1214
|
+
throw e;
|
|
1215
|
+
}
|
|
780
1216
|
}
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
);
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
1217
|
+
function encodeViaWebGL() {
|
|
1218
|
+
const gl = getSharedWebGLContext();
|
|
1219
|
+
if (!gl) return null;
|
|
1220
|
+
const caps = detectWebGLCapabilities(gl);
|
|
1221
|
+
const selection = selectWebGLFormat(caps, hint, { colorSpace });
|
|
1222
|
+
if (!selection.format || !selection.encoderClass) return null;
|
|
1223
|
+
const encoder = selection.encoderClass.create(gl);
|
|
1224
|
+
try {
|
|
1225
|
+
if (!mipmaps) {
|
|
1226
|
+
const bytes = encoder.encodeToBytes(bitmap, { flipY });
|
|
1227
|
+
const tex3 = encoder.buildMippedTexture([bytes], { colorSpace });
|
|
1228
|
+
return {
|
|
1229
|
+
texture: tex3,
|
|
1230
|
+
format: selection.format,
|
|
1231
|
+
fallbackUncompressed: false,
|
|
1232
|
+
backend: "webgl",
|
|
1233
|
+
astcNormalRemap: selection.astcNormalRemap,
|
|
1234
|
+
width: bytes.width,
|
|
1235
|
+
height: bytes.height,
|
|
1236
|
+
mipLevels: 1,
|
|
1237
|
+
encodeMs: bytes.encodeMs,
|
|
1238
|
+
destroy: () => {
|
|
1239
|
+
tex3.dispose();
|
|
1240
|
+
encoder.destroy();
|
|
1241
|
+
}
|
|
1242
|
+
};
|
|
798
1243
|
}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
try {
|
|
809
|
-
const needsWriteTexture = needsWriteTextureWorkaround(adapter);
|
|
810
|
-
if (!mipmaps) {
|
|
811
|
-
let bytes;
|
|
812
|
-
if (needsWriteTexture) {
|
|
813
|
-
const level02 = bitmapToMipLevel(bitmap, flipY);
|
|
814
|
-
const imageData = mipLevelToImageData(level02);
|
|
815
|
-
bytes = await encoder.encodeToBytes(imageData, { quality });
|
|
816
|
-
} else {
|
|
817
|
-
bytes = await encoder.encodeToBytes(bitmap, { flipY, quality });
|
|
1244
|
+
const level0 = bitmapToMipLevel(bitmap, flipY);
|
|
1245
|
+
const chain = generateMipChain(level0);
|
|
1246
|
+
const encodedLevels = [];
|
|
1247
|
+
let totalEncodeMs = 0;
|
|
1248
|
+
for (const level of chain) {
|
|
1249
|
+
const padded = padToBlockMultiple(level);
|
|
1250
|
+
const bytes = encoder.encodeToBytes(padded);
|
|
1251
|
+
encodedLevels.push(bytes);
|
|
1252
|
+
totalEncodeMs += bytes.encodeMs;
|
|
818
1253
|
}
|
|
819
|
-
const tex2 = encoder.buildMippedTexture(
|
|
1254
|
+
const tex2 = encoder.buildMippedTexture(encodedLevels, { colorSpace });
|
|
820
1255
|
return {
|
|
821
1256
|
texture: tex2,
|
|
822
1257
|
format: selection.format,
|
|
823
1258
|
fallbackUncompressed: false,
|
|
1259
|
+
backend: "webgl",
|
|
824
1260
|
astcNormalRemap: selection.astcNormalRemap,
|
|
825
|
-
width:
|
|
826
|
-
height:
|
|
827
|
-
mipLevels:
|
|
828
|
-
encodeMs:
|
|
1261
|
+
width: level0.width,
|
|
1262
|
+
height: level0.height,
|
|
1263
|
+
mipLevels: encodedLevels.length,
|
|
1264
|
+
encodeMs: totalEncodeMs,
|
|
829
1265
|
destroy: () => {
|
|
830
1266
|
tex2.dispose();
|
|
831
1267
|
encoder.destroy();
|
|
832
1268
|
}
|
|
833
1269
|
};
|
|
1270
|
+
} catch (e) {
|
|
1271
|
+
encoder.destroy();
|
|
1272
|
+
console.warn("[compressTexture] WebGL fallback encode failed; returning uncompressed RGBA8.", e);
|
|
1273
|
+
return null;
|
|
834
1274
|
}
|
|
835
|
-
const level0 = bitmapToMipLevel(bitmap, flipY);
|
|
836
|
-
const chain = generateMipChain(level0);
|
|
837
|
-
const encodedLevels = [];
|
|
838
|
-
let totalEncodeMs = 0;
|
|
839
|
-
for (const level of chain) {
|
|
840
|
-
const padded = padToBlockMultiple(level);
|
|
841
|
-
const imageData = mipLevelToImageData(padded);
|
|
842
|
-
const bytes = await encoder.encodeToBytes(imageData, { quality });
|
|
843
|
-
encodedLevels.push(bytes);
|
|
844
|
-
totalEncodeMs += bytes.encodeMs;
|
|
845
|
-
}
|
|
846
|
-
const tex = encoder.buildMippedTexture(encodedLevels, { colorSpace });
|
|
847
|
-
return {
|
|
848
|
-
texture: tex,
|
|
849
|
-
format: selection.format,
|
|
850
|
-
fallbackUncompressed: false,
|
|
851
|
-
astcNormalRemap: selection.astcNormalRemap,
|
|
852
|
-
width: level0.width,
|
|
853
|
-
height: level0.height,
|
|
854
|
-
mipLevels: encodedLevels.length,
|
|
855
|
-
encodeMs: totalEncodeMs,
|
|
856
|
-
destroy: () => {
|
|
857
|
-
tex.dispose();
|
|
858
|
-
encoder.destroy();
|
|
859
|
-
}
|
|
860
|
-
};
|
|
861
|
-
} catch (e) {
|
|
862
|
-
encoder.destroy();
|
|
863
|
-
throw e;
|
|
864
1275
|
}
|
|
865
1276
|
}
|
|
866
1277
|
|
|
@@ -913,6 +1324,7 @@ var GputexLoader = class extends Loader {
|
|
|
913
1324
|
result.texture.userData.gputex = {
|
|
914
1325
|
format: result.format,
|
|
915
1326
|
fallbackUncompressed: result.fallbackUncompressed,
|
|
1327
|
+
backend: result.backend,
|
|
916
1328
|
astcNormalRemap: result.astcNormalRemap,
|
|
917
1329
|
width: result.width,
|
|
918
1330
|
height: result.height,
|
|
@@ -933,16 +1345,26 @@ var GputexLoader = class extends Loader {
|
|
|
933
1345
|
};
|
|
934
1346
|
export {
|
|
935
1347
|
ASTC4x4Encoder,
|
|
1348
|
+
ASTC4x4WebGLEncoder,
|
|
936
1349
|
BC1Encoder,
|
|
1350
|
+
BC1WebGLEncoder,
|
|
937
1351
|
BC5Encoder,
|
|
1352
|
+
BC5WebGLEncoder,
|
|
938
1353
|
BC7Encoder,
|
|
1354
|
+
BC7WebGLEncoder,
|
|
939
1355
|
Encoder,
|
|
940
1356
|
GputexLoader,
|
|
941
1357
|
TextureFormat,
|
|
1358
|
+
WebGLBlockEncoder,
|
|
942
1359
|
WebGPUFeature,
|
|
943
1360
|
compressTexture,
|
|
1361
|
+
createWebGLContext,
|
|
944
1362
|
detectCapabilities,
|
|
1363
|
+
detectWebGLCapabilities,
|
|
945
1364
|
generateMipChain,
|
|
1365
|
+
getSharedWebGLContext,
|
|
1366
|
+
isWebGLAvailable,
|
|
946
1367
|
padToBlockMultiple,
|
|
947
|
-
selectFormat
|
|
1368
|
+
selectFormat,
|
|
1369
|
+
selectWebGLFormat
|
|
948
1370
|
};
|