gputex 0.1.2 → 0.2.0

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
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
- ⚠️: 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.
5
+ ⚠️: This is 100% vibe-coded. The code is completely unreviewed and under-tested. Do not use for anything important.
6
6
 
7
7
  ## Install
8
8
 
@@ -14,7 +14,14 @@ pnpm add gputex
14
14
  bun add gputex
15
15
  ```
16
16
 
17
- `three` is a peer dependency (`>=0.180`).
17
+ ### Entry points
18
+
19
+ `gputex` ships two entry points:
20
+
21
+ - **`gputex`** — the engine-agnostic core: the `*Encoder` classes, capability / format detection, and mip helpers. Nothing here imports `three`, so it works with Babylon.js, raw WebGPU/WebGL, workers, etc. Encoders return raw compressed block bytes via `encodeToBytes()`.
22
+ - **`gputex/three`** — the Three.js layer. Re-exports the entire core **plus** `compressTexture()`, `GputexLoader`, and `buildCompressedTexture()` / `encodeToTexture()`. This is the only entry that imports `three`.
23
+
24
+ `three` is an **optional** peer dependency (`>=0.170`): install it only if you import `gputex/three`. Pure-core consumers (e.g. Babylon.js) can skip it entirely.
18
25
 
19
26
  ## Formats
20
27
 
@@ -44,7 +51,7 @@ Notes on the WebGL path:
44
51
  ### `compressTexture` — direct API
45
52
 
46
53
  ```ts
47
- import { compressTexture } from 'gputex'
54
+ import { compressTexture } from 'gputex/three'
48
55
 
49
56
  const { texture, format } = await compressTexture('/cobblestone.avif', {
50
57
  hint: 'color', // 'color' | 'colorWithAlpha' | 'normal'
@@ -75,7 +82,7 @@ material.map = texture
75
82
  ### `GputexLoader` — Three.js Loader
76
83
 
77
84
  ```ts
78
- import { GputexLoader } from 'gputex'
85
+ import { GputexLoader } from 'gputex/three'
79
86
 
80
87
  const loader = new GputexLoader()
81
88
  loader.hint = 'normal'
@@ -90,7 +97,7 @@ The `GputexLoader` works with R3F's `useLoader`:
90
97
 
91
98
  ```tsx
92
99
  import { useLoader } from '@react-three/fiber'
93
- import { GputexLoader } from 'gputex'
100
+ import { GputexLoader } from 'gputex/three'
94
101
 
95
102
  function Scene() {
96
103
  const texture = useLoader(GputexLoader, '/cobblestone.avif', loader => {
@@ -113,7 +120,7 @@ For a reusable hook with metadata access:
113
120
  ```tsx
114
121
  import { useLayoutEffect } from 'react'
115
122
  import { useLoader } from '@react-three/fiber'
116
- import { GputexLoader } from 'gputex'
123
+ import { GputexLoader } from 'gputex/three'
117
124
  import type { TextureHint } from 'gputex'
118
125
 
119
126
  function useGputex(url: string, options?: { hint?: TextureHint; colorSpace?: 'srgb' | 'linear'; mipmaps?: boolean }) {
@@ -157,18 +164,35 @@ function Scene() {
157
164
  }
158
165
  ```
159
166
 
160
- ### Low-level encoders
167
+ ### Low-level encoders (any engine)
161
168
 
162
- Individual encoder classes are exported for direct control:
169
+ The individual encoder classes live in the engine-agnostic core (`gputex`). `encodeToBytes()` returns raw compressed block bytes with no Three.js involvement — feed them into whatever compressed-texture upload your engine exposes (Babylon.js, raw WebGPU/WebGL, …):
163
170
 
164
171
  ```ts
165
172
  import { BC7Encoder, BC5Encoder, ASTC4x4Encoder, BC1Encoder } from 'gputex'
166
173
 
167
174
  const encoder = await BC7Encoder.create()
168
- const { data, width, height } = await encoder.encodeToBytes(imageBitmap)
175
+ const { data, width, height, paddedWidth, paddedHeight } = await encoder.encodeToBytes(imageBitmap)
176
+ // `data` is a Uint8Array of BC7 blocks covering paddedWidth × paddedHeight.
169
177
  encoder.destroy()
170
178
  ```
171
179
 
180
+ To turn an encoder's output into a Three.js `CompressedTexture` directly, use the helpers in `gputex/three`:
181
+
182
+ ```ts
183
+ import { BC7Encoder, TextureFormat } from 'gputex'
184
+ import { encodeToTexture, buildCompressedTexture } from 'gputex/three'
185
+
186
+ const encoder = await BC7Encoder.create()
187
+
188
+ // One-shot: image → CompressedTexture (plus the raw byte metadata)
189
+ const { texture } = await encodeToTexture(encoder, imageBitmap, { colorSpace: 'srgb' })
190
+
191
+ // …or assemble a texture from bytes you already have (e.g. a mip chain):
192
+ const bytes = await encoder.encodeToBytes(imageBitmap)
193
+ const tex = buildCompressedTexture([bytes], TextureFormat.BC7_SRGB)
194
+ ```
195
+
172
196
  ## Options
173
197
 
174
198
  ### `compressTexture` options
package/dist/index.d.ts CHANGED
@@ -1,5 +1,3 @@
1
- import { CompressedPixelFormat, CompressedTexture, Texture, Loader } from 'three';
2
-
3
1
  declare const TextureFormat: {
4
2
  readonly BC1: "BC1";
5
3
  readonly BC1_SRGB: "BC1_SRGB";
@@ -59,19 +57,11 @@ interface EncodeCallOptions {
59
57
  /** Encode quality / speed trade-off. Default 'fast'. */
60
58
  quality?: EncodeQuality;
61
59
  }
62
- interface EncodeResult {
63
- width: number;
64
- height: number;
65
- paddedWidth: number;
66
- paddedHeight: number;
67
- data: Uint8Array;
68
- texture: CompressedTexture;
69
- encodeMs: number;
70
- }
71
60
  /**
72
- * Result of a raw bytes-only encode (no wrapping in a `CompressedTexture`).
73
- * Used by the mipped encode path to bundle multiple levels into a single
74
- * `CompressedTexture` at the end.
61
+ * Result of a raw bytes-only encode. This is the encoder's native output: the
62
+ * compressed block bytes plus dimensions, with no Three.js (or any engine)
63
+ * involvement. Feed `data` into whatever renderer's compressed-texture upload
64
+ * you like, or use `buildCompressedTexture()` from `gputex/three`.
75
65
  */
76
66
  interface EncodeBytesResult {
77
67
  width: number;
@@ -96,6 +86,8 @@ interface FormatVariant {
96
86
  type EncoderConstructor<T extends Encoder = Encoder> = {
97
87
  new (opts: EncoderOptions): T;
98
88
  requiredFeature: GPUFeatureName | null;
89
+ /** Logical formats this encoder can emit (linear first, then sRGB variant). */
90
+ readonly textureFormats: readonly TextureFormat[];
99
91
  create(): Promise<T>;
100
92
  };
101
93
  declare abstract class Encoder {
@@ -159,40 +151,23 @@ declare abstract class Encoder {
159
151
  abstract wgslSource(): string;
160
152
  /** e.g. 'bc1-rgba-unorm-srgb'. */
161
153
  abstract gpuTextureFormat(opts: FormatVariant): GPUTextureFormat;
162
- /** Three.js `CompressedPixelFormat` constant for `CompressedTexture`. */
163
- abstract threeTextureFormat(opts: FormatVariant): CompressedPixelFormat;
164
154
  /**
165
155
  * True if the device reports the feature the output texture needs.
166
156
  * The encoder itself only writes to a storage buffer, so this is about
167
157
  * whether the result can actually be sampled.
168
158
  */
169
159
  get supportsSampling(): boolean;
170
- encode(source: EncoderImageSource, { colorSpace, quality }?: EncodeCallOptions): Promise<EncodeResult>;
171
160
  /**
172
- * Encode one image source to raw compressed bytes, skipping the
173
- * `CompressedTexture` wrap. Used by the public `encode()` above and by
174
- * the mipped encode path in `compressTexture()` so N mip levels end up
175
- * in a single `CompressedTexture` instead of N throwaway wrappers.
176
- *
177
- * Public (not protected) because `compressTexture()` calls it across the
178
- * encoder boundary. Still safe to call from outside — it just does
179
- * less work than `encode()` and the caller assembles the texture.
161
+ * Encode one image source to raw compressed bytes. This is the encoder's
162
+ * native, engine-agnostic output. `compressTexture()` and
163
+ * `encodeToTexture()` (both in `gputex/three`) call it and then wrap the
164
+ * bytes into a `CompressedTexture`; callers targeting another engine feed
165
+ * `data` into that engine's compressed-texture upload directly.
180
166
  */
181
167
  encodeToBytes(source: EncoderImageSource, { flipY, quality }?: {
182
168
  flipY?: boolean;
183
169
  quality?: EncodeQuality;
184
170
  }): Promise<EncodeBytesResult>;
185
- /**
186
- * Assemble a `CompressedTexture` from pre-encoded mip levels. Called
187
- * by `compressTexture()` after it has run each level through
188
- * `encodeToBytes()`. Centralised here so the single-level and mipped
189
- * paths share the same format / colour-space / wrap settings.
190
- *
191
- * `levels[0]` is the base level; its padded dimensions become the
192
- * texture's overall size. Filter setup assumes at least 2 levels →
193
- * trilinear; 1 level → bilinear.
194
- */
195
- buildMippedTexture(levels: readonly EncodeBytesResult[], { colorSpace }?: EncodeCallOptions): CompressedTexture;
196
171
  }
197
172
 
198
173
  declare class BC1Encoder extends Encoder {
@@ -204,7 +179,6 @@ declare class BC1Encoder extends Encoder {
204
179
  get supportsQuality(): boolean;
205
180
  wgslSource(): string;
206
181
  gpuTextureFormat({ colorSpace }: FormatVariant): GPUTextureFormat;
207
- threeTextureFormat(): CompressedPixelFormat;
208
182
  }
209
183
 
210
184
  declare class BC5Encoder extends Encoder {
@@ -217,7 +191,6 @@ declare class BC5Encoder extends Encoder {
217
191
  wgslSource(): string;
218
192
  wgslSourceFastF16(): string;
219
193
  gpuTextureFormat(): GPUTextureFormat;
220
- threeTextureFormat(): CompressedPixelFormat;
221
194
  }
222
195
 
223
196
  declare class BC7Encoder extends Encoder {
@@ -230,7 +203,6 @@ declare class BC7Encoder extends Encoder {
230
203
  wgslSource(): string;
231
204
  wgslSourceFastF16(): string;
232
205
  gpuTextureFormat({ colorSpace }: FormatVariant): GPUTextureFormat;
233
- threeTextureFormat(): CompressedPixelFormat;
234
206
  }
235
207
 
236
208
  declare class ASTC4x4Encoder extends Encoder {
@@ -243,18 +215,6 @@ declare class ASTC4x4Encoder extends Encoder {
243
215
  wgslSource(): string;
244
216
  wgslSourceFastF16(): string;
245
217
  gpuTextureFormat({ colorSpace }: FormatVariant): GPUTextureFormat;
246
- threeTextureFormat(): CompressedPixelFormat;
247
- }
248
-
249
- /** One encoded mip level. The fields both encoder backends already produce. */
250
- interface EncodedLevel {
251
- /** Logical (pre-padding) dimensions, surfaced on the texture's userData. */
252
- width: number;
253
- height: number;
254
- /** Block-aligned dimensions the compressed `data` actually covers. */
255
- paddedWidth: number;
256
- paddedHeight: number;
257
- data: Uint8Array;
258
218
  }
259
219
 
260
220
  /** Raw RGBA8 pixel data (e.g. a CPU-generated mip level). */
@@ -307,8 +267,6 @@ declare abstract class WebGLBlockEncoder {
307
267
  abstract get supportsSrgb(): boolean;
308
268
  /** GLSL ES 3.00 fragment-shader source. */
309
269
  abstract fragSource(): string;
310
- /** Three.js `CompressedPixelFormat` constant; sRGB is carried by colorSpace. */
311
- abstract threeTextureFormat(): CompressedPixelFormat;
312
270
  protected _buildProgram(): void;
313
271
  /** Release the GL program + VAO. The shared context itself is left intact. */
314
272
  destroy(): void;
@@ -328,10 +286,6 @@ declare abstract class WebGLBlockEncoder {
328
286
  encodeToBytes(source: WebGLEncoderImageSource, { flipY }?: {
329
287
  flipY?: boolean;
330
288
  }): WebGLEncodeBytesResult;
331
- /** Wrap pre-encoded levels into a CompressedTexture. Shared with the WebGPU path. */
332
- buildMippedTexture(levels: readonly EncodedLevel[], { colorSpace }?: {
333
- colorSpace?: 'srgb' | 'linear';
334
- }): CompressedTexture;
335
289
  }
336
290
 
337
291
  declare class BC1WebGLEncoder extends WebGLBlockEncoder {
@@ -339,7 +293,6 @@ declare class BC1WebGLEncoder extends WebGLBlockEncoder {
339
293
  get bytesPerBlock(): number;
340
294
  get supportsSrgb(): boolean;
341
295
  fragSource(): string;
342
- threeTextureFormat(): CompressedPixelFormat;
343
296
  }
344
297
 
345
298
  declare class BC5WebGLEncoder extends WebGLBlockEncoder {
@@ -347,7 +300,6 @@ declare class BC5WebGLEncoder extends WebGLBlockEncoder {
347
300
  get bytesPerBlock(): number;
348
301
  get supportsSrgb(): boolean;
349
302
  fragSource(): string;
350
- threeTextureFormat(): CompressedPixelFormat;
351
303
  }
352
304
 
353
305
  declare class BC7WebGLEncoder extends WebGLBlockEncoder {
@@ -355,7 +307,6 @@ declare class BC7WebGLEncoder extends WebGLBlockEncoder {
355
307
  get bytesPerBlock(): number;
356
308
  get supportsSrgb(): boolean;
357
309
  fragSource(): string;
358
- threeTextureFormat(): CompressedPixelFormat;
359
310
  }
360
311
 
361
312
  declare class ASTC4x4WebGLEncoder extends WebGLBlockEncoder {
@@ -363,7 +314,6 @@ declare class ASTC4x4WebGLEncoder extends WebGLBlockEncoder {
363
314
  get bytesPerBlock(): number;
364
315
  get supportsSrgb(): boolean;
365
316
  fragSource(): string;
366
- threeTextureFormat(): CompressedPixelFormat;
367
317
  }
368
318
 
369
319
  /**
@@ -438,96 +388,6 @@ interface WebGLFormatSelection {
438
388
  }
439
389
  declare function selectWebGLFormat(caps: WebGLCapabilities, hint: TextureHint, options?: SelectFormatOptions): WebGLFormatSelection;
440
390
 
441
- /**
442
- * Everything `compressTexture()` can take as an image source. A superset
443
- * of `EncoderImageSource` (see Encoder.ts) that also accepts URL strings
444
- * and Blob / File objects — the common cases in a web app.
445
- */
446
- type CompressTextureSource = string | Blob | File | ImageBitmap | HTMLImageElement | HTMLCanvasElement | OffscreenCanvas | ImageData;
447
- interface CompressOptions {
448
- /** How the texture will be used. Drives format selection. Default 'color'. */
449
- hint?: TextureHint;
450
- /** Pick the sRGB or linear variant of the chosen format. Default 'srgb'. */
451
- colorSpace?: 'srgb' | 'linear';
452
- /** Flip the image vertically before encoding. Default true (matches Three.js convention). */
453
- flipY?: boolean;
454
- /** Generate a full mip chain down to 1×1 on the CPU, encode every level. */
455
- mipmaps?: boolean;
456
- /**
457
- * Encode quality / speed trade-off. 'fast' (default) is ~2–4× faster for a
458
- * ≤0.36 dB PSNR cost; 'high' runs the exhaustive search (output identical to
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).
462
- */
463
- quality?: EncodeQuality;
464
- /** Reuse an existing device (e.g. Three.js's renderer device) instead
465
- * of creating a new one. WebGPU path only. When provided, the encoder
466
- * never destroys it. */
467
- device?: GPUDevice;
468
- adapter?: GPUAdapter;
469
- }
470
- interface CompressResult {
471
- /** CompressedTexture on a compressed path; Texture on RGBA8 fallback. */
472
- texture: Texture | CompressedTexture;
473
- /** The compressed format selected, or null when we fell back to RGBA8. */
474
- format: TextureFormat | null;
475
- /** True iff we returned an uncompressed Texture because no encoder fit. */
476
- fallbackUncompressed: boolean;
477
- /**
478
- * Which backend produced the result. 'webgpu' = compute path, 'webgl' =
479
- * fragment-shader fallback, 'none' = uncompressed RGBA8.
480
- */
481
- backend: 'webgpu' | 'webgl' | 'none';
482
- /**
483
- * True iff the chosen format is ASTC and the hint was 'normal'. The
484
- * caller must apply the (R, W) → (x, y) swizzle in the material — ASTC
485
- * has no 2-channel mode, so normal maps ride the RGBA path.
486
- */
487
- astcNormalRemap: boolean;
488
- width: number;
489
- height: number;
490
- mipLevels: number;
491
- /** Wall-clock time of GPU encoding, summed across mip levels. */
492
- encodeMs: number;
493
- /** Release the encoder's internal GPU resources. No-op if `device` was
494
- * passed in by the caller. */
495
- destroy(): void;
496
- }
497
- declare function compressTexture(source: CompressTextureSource, options?: CompressOptions): Promise<CompressResult>;
498
-
499
- declare class GputexLoader extends Loader<Texture> {
500
- /** Format-selection hint. Default 'color'. */
501
- hint: TextureHint;
502
- /** Pick the sRGB or linear variant of the chosen format. Default 'srgb'. */
503
- colorSpace: 'srgb' | 'linear';
504
- /** Flip the image vertically before encoding. Default true (matches Three.js convention). */
505
- flipY: boolean;
506
- /** Generate + encode a full mip chain. Default false. */
507
- mipmaps: boolean;
508
- /** Encode quality / speed trade-off. Default 'fast' (~2–4× faster, ≤0.36 dB). */
509
- quality: EncodeQuality;
510
- /**
511
- * Optional pre-existing WebGPU device. Reusing the renderer's device
512
- * avoids spinning up a second WebGPU context for encoding.
513
- */
514
- device?: GPUDevice;
515
- adapter?: GPUAdapter;
516
- /**
517
- * Most recent full encode result. Useful when the caller wants format
518
- * / mipLevels / astcNormalRemap metadata without threading a separate
519
- * callback through `load()`. Cleared when a new load starts.
520
- */
521
- lastResult: CompressResult | null;
522
- /**
523
- * THREE.Loader contract: returns void, drives callbacks. `loadAsync`
524
- * (inherited from the base class) wraps this with Promise semantics.
525
- * Errors routed through `manager.itemError` so the LoadingManager's
526
- * aggregate state stays accurate.
527
- */
528
- load(url: string, onLoad?: (texture: Texture) => void, _onProgress?: (event: ProgressEvent) => void, onError?: (err: unknown) => void): void;
529
- }
530
-
531
391
  /** One level of a mip chain. 4 bytes per pixel (RGBA8). */
532
392
  interface MipLevel {
533
393
  data: Uint8ClampedArray;
@@ -553,4 +413,4 @@ declare function generateMipChain(level0: MipLevel): MipLevel[];
553
413
  */
554
414
  declare function padToBlockMultiple(level: MipLevel): MipLevel;
555
415
 
556
- 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 };
416
+ 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 };