gputex 0.5.0 → 0.6.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/dist/index.d.ts +138 -1
- package/dist/index.js +509 -13
- package/dist/three.d.ts +9 -130
- package/dist/three.js +126 -137
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -605,4 +605,141 @@ interface RasterizeSvgOptions {
|
|
|
605
605
|
*/
|
|
606
606
|
declare function rasterizeSvg(source: string | Blob, options?: RasterizeSvgOptions): Promise<ImageBitmap>;
|
|
607
607
|
|
|
608
|
-
|
|
608
|
+
/**
|
|
609
|
+
* Everything `compressTexture()` can take as an image source. A superset
|
|
610
|
+
* of `EncoderImageSource` (see Encoder.ts) that also accepts URL strings
|
|
611
|
+
* and Blob / File objects — the common cases in a web app.
|
|
612
|
+
*
|
|
613
|
+
* SVG works through all of these: a URL to an `.svg` file, a string of
|
|
614
|
+
* inline SVG markup (detected by a leading `<`), an SVG Blob/File, or an
|
|
615
|
+
* HTMLImageElement whose src is SVG. Vector sources are rasterised to RGBA
|
|
616
|
+
* before encoding — see the `svgSize` option.
|
|
617
|
+
*/
|
|
618
|
+
type CompressTextureSource = string | Blob | File | ImageBitmap | HTMLImageElement | HTMLCanvasElement | OffscreenCanvas | ImageData;
|
|
619
|
+
interface CompressOptions {
|
|
620
|
+
/** How the texture will be used. Drives format selection. Default 'color'. */
|
|
621
|
+
hint?: TextureHint;
|
|
622
|
+
/**
|
|
623
|
+
* Prefer a specific format over the default choice when the device
|
|
624
|
+
* supports it; falls back to the normal selection (BC7 → ASTC → ETC2 →
|
|
625
|
+
* RGBA8) when it doesn't. Currently only 'bc1': half the memory of BC7
|
|
626
|
+
* for opaque colour textures, at lower quality. Only honoured with
|
|
627
|
+
* `hint: 'color'` — BC1 can't carry real alpha or normal maps.
|
|
628
|
+
*/
|
|
629
|
+
preferredFormat?: PreferredFormat;
|
|
630
|
+
/**
|
|
631
|
+
* Memory/fidelity trade-off for opaque colour textures. Default 'high'
|
|
632
|
+
* (BC7 / ASTC 4×4, 1 byte/pixel). 'low' picks the 4-bpp formats when the
|
|
633
|
+
* device has one — BC1 on desktop-class GPUs, ETC2 RGB8 on mobile-class
|
|
634
|
+
* ones — halving GPU memory at visibly lower quality on smooth content.
|
|
635
|
+
* Ignored for `hint: 'colorWithAlpha'` and `hint: 'normal'` (the 4-bpp
|
|
636
|
+
* formats can't carry them). On the WebGL fallback tier only BC1 is
|
|
637
|
+
* available at 'low'.
|
|
638
|
+
*/
|
|
639
|
+
quality?: FormatQuality;
|
|
640
|
+
/** Pick the sRGB or linear variant of the chosen format. Default 'srgb'. */
|
|
641
|
+
colorSpace?: 'srgb' | 'linear';
|
|
642
|
+
/**
|
|
643
|
+
* Rasterisation size for SVG sources. A number scales the SVG so its
|
|
644
|
+
* longest side matches (aspect ratio preserved); `{ width, height }`
|
|
645
|
+
* rasterises at exactly that size. Default: the SVG's intrinsic size
|
|
646
|
+
* (absolute width/height attributes, else the viewBox dimensions).
|
|
647
|
+
* Ignored for non-SVG sources.
|
|
648
|
+
*/
|
|
649
|
+
svgSize?: SvgRasterSize;
|
|
650
|
+
/** Flip the image vertically before encoding. Default true (matches Three.js convention). */
|
|
651
|
+
flipY?: boolean;
|
|
652
|
+
/** Generate a full mip chain down to 1×1 on the CPU, encode every level. */
|
|
653
|
+
mipmaps?: boolean;
|
|
654
|
+
/** Reuse an existing device (e.g. Three.js's renderer device) instead
|
|
655
|
+
* of creating a new one. WebGPU path only. When provided, the encoder
|
|
656
|
+
* never destroys it. */
|
|
657
|
+
device?: GPUDevice;
|
|
658
|
+
adapter?: GPUAdapter;
|
|
659
|
+
/**
|
|
660
|
+
* Keep the compressed bytes in a session-scoped in-memory LRU and reuse
|
|
661
|
+
* them on repeat calls, skipping BOTH the image decode and the encode —
|
|
662
|
+
* the dominant costs. Re-loading a texture later in the session (e.g.
|
|
663
|
+
* two worlds sharing an atlas) becomes a few ms. Keyed by source
|
|
664
|
+
* identity + selected format + encode options; capped at 256 MiB of
|
|
665
|
+
* compressed bytes by default (`setTranscodeCacheLimit()` to tune) and
|
|
666
|
+
* never touches persistent storage. Default false.
|
|
667
|
+
*
|
|
668
|
+
* URL and Blob/File sources get an identity automatically (URL string or
|
|
669
|
+
* content hash). Pixel sources (ImageBitmap, canvas, ImageData) are only
|
|
670
|
+
* cached when `cacheKey` is provided.
|
|
671
|
+
*/
|
|
672
|
+
cache?: boolean;
|
|
673
|
+
/**
|
|
674
|
+
* Explicit cache identity for the source, overriding the derived one.
|
|
675
|
+
* Use when you already know a stable name (e.g. an asset path) and want
|
|
676
|
+
* to skip content hashing, or to make pixel sources cacheable.
|
|
677
|
+
*/
|
|
678
|
+
cacheKey?: string;
|
|
679
|
+
}
|
|
680
|
+
interface CompressResult {
|
|
681
|
+
/**
|
|
682
|
+
* Encoded compressed mip levels (`levels[0]` is the base level), ready to
|
|
683
|
+
* upload to a compressed texture. Null on the RGBA8 fallback path — use
|
|
684
|
+
* `fallbackBitmap` instead.
|
|
685
|
+
*/
|
|
686
|
+
levels: EncodedLevelBytes[] | null;
|
|
687
|
+
/**
|
|
688
|
+
* Decoded RGBA8 bitmap, set only when `fallbackUncompressed` (no compressed
|
|
689
|
+
* format was available on either backend). Upload it as a plain RGBA8
|
|
690
|
+
* texture; the caller applies colour space / flipY at the texture level.
|
|
691
|
+
*/
|
|
692
|
+
fallbackBitmap: ImageBitmap | null;
|
|
693
|
+
/** The compressed format selected, or null when we fell back to RGBA8. */
|
|
694
|
+
format: TextureFormat | null;
|
|
695
|
+
/** True iff we fell back to an uncompressed RGBA8 bitmap because no encoder fit. */
|
|
696
|
+
fallbackUncompressed: boolean;
|
|
697
|
+
/**
|
|
698
|
+
* Which backend produced the result. 'webgpu' = compute path, 'webgl' =
|
|
699
|
+
* fragment-shader fallback, 'none' = uncompressed RGBA8.
|
|
700
|
+
*/
|
|
701
|
+
backend: 'webgpu' | 'webgl' | 'none';
|
|
702
|
+
/**
|
|
703
|
+
* True iff the chosen format is ASTC and the hint was 'normal'. The
|
|
704
|
+
* caller must apply the (R, W) → (x, y) swizzle in the material — ASTC
|
|
705
|
+
* has no 2-channel mode, so normal maps ride the RGBA path.
|
|
706
|
+
*/
|
|
707
|
+
astcNormalRemap: boolean;
|
|
708
|
+
width: number;
|
|
709
|
+
height: number;
|
|
710
|
+
mipLevels: number;
|
|
711
|
+
/** Wall-clock time of GPU encoding, summed across mip levels. */
|
|
712
|
+
encodeMs: number;
|
|
713
|
+
/**
|
|
714
|
+
* Wall-clock time to turn the source into decoded RGBA pixels: fetch /
|
|
715
|
+
* base64 decode, image decode, SVG rasterisation. Usually the dominant
|
|
716
|
+
* cost for large images — when a load feels slower than `encodeMs`
|
|
717
|
+
* suggests, this is where the time went.
|
|
718
|
+
*/
|
|
719
|
+
decodeMs: number;
|
|
720
|
+
/** Wall-clock time of the whole `compressTexture()` call: decode + CPU
|
|
721
|
+
* mip generation + encode + texture assembly. */
|
|
722
|
+
totalMs: number;
|
|
723
|
+
/** True when the result came from the in-memory transcode cache (the
|
|
724
|
+
* `cache` option) — no decode or encode ran; decodeMs/encodeMs are 0. */
|
|
725
|
+
cacheHit: boolean;
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Destroy the WebGPU device and encoders that `compressTexture()` shares
|
|
729
|
+
* across calls (created lazily when neither the `device` nor the `adapter`
|
|
730
|
+
* option is passed). Safe to call at any time — in-flight encodes on the
|
|
731
|
+
* shared device will fail, and the next `compressTexture()` call recreates
|
|
732
|
+
* everything. No-op when nothing is cached.
|
|
733
|
+
*/
|
|
734
|
+
declare function releaseSharedGpuResources(): void;
|
|
735
|
+
declare function compressTextureToBytes(source: CompressTextureSource, options?: CompressOptions): Promise<CompressResult>;
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Cap the cache's total compressed payload in bytes (default 256 MiB).
|
|
739
|
+
* Lower it to evict immediately; 0 disables caching entirely.
|
|
740
|
+
*/
|
|
741
|
+
declare function setTranscodeCacheLimit(bytes: number): void;
|
|
742
|
+
/** Drop every cached transcode. Textures already built from entries are unaffected. */
|
|
743
|
+
declare function clearTranscodeCache(): void;
|
|
744
|
+
|
|
745
|
+
export { ASTC4x4Encoder, ASTC4x4WebGLEncoder, BC1Encoder, BC1WebGLEncoder, BC5Encoder, BC5WebGLEncoder, BC7Encoder, type BC7EncoderOptions, BC7WebGLEncoder, type Capabilities, type CompressOptions, type CompressResult, type CompressTextureSource, ETC2Encoder, type EncodeBytesResult, type EncodeCallOptions, type EncodeMipChainResult, type EncodedLevelBytes, Encoder, type EncoderConstructor, type EncoderImageSource, type EncoderOptions, type ExtensionProvider, type FeatureProvider, type FormatQuality, type FormatSelection, type FormatVariant, type MipLevel, type PreferredFormat, type RasterizeSvgOptions, type RawPixelSource, type SelectFormatOptions, type SvgRasterSize, TextureFormat, type TextureHint, WebGLBlockEncoder, type WebGLCapabilities, type WebGLEncodeBytesResult, type WebGLEncoderConstructor, type WebGLEncoderImageSource, type WebGLEncoderOptions, type WebGLFormatSelection, WebGPUFeature, clearTranscodeCache, compressTextureToBytes, createWebGLContext, detectCapabilities, detectWebGLCapabilities, generateGpuMipChain, generateMipChain, getSharedWebGLContext, gpuMipLevelCount, isWebGLAvailable, padToBlockMultiple, rasterizeSvg, releaseSharedGpuResources, selectFormat, selectWebGLFormat, setTranscodeCacheLimit };
|
package/dist/index.js
CHANGED
|
@@ -44,6 +44,10 @@ function detectCapabilities(adapter) {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
// src/workarounds.ts
|
|
47
|
+
function needsWriteTextureWorkaround(adapter) {
|
|
48
|
+
const { vendor, architecture } = adapter.info ?? {};
|
|
49
|
+
return vendor === "img-tec" && architecture === "d-series";
|
|
50
|
+
}
|
|
47
51
|
function uploadSourceTexture(device, srcTex, source, width, height, flipY) {
|
|
48
52
|
if (source instanceof ImageData && !flipY) {
|
|
49
53
|
if (srcTex.format === "rg8unorm") {
|
|
@@ -460,25 +464,25 @@ var Encoder = class {
|
|
|
460
464
|
if (useCache && (srcTexIsNew || dstIsNew || prepPlanesNew)) this._cachedBindGroup = null;
|
|
461
465
|
let bindGroup = useCache ? this._cachedBindGroup : null;
|
|
462
466
|
if (!bindGroup) {
|
|
463
|
-
const
|
|
467
|
+
const entries2 = [
|
|
464
468
|
{ binding: 0, resource: prepPlanes ? prepPlanes[0].createView() : srcTex.createView() },
|
|
465
469
|
{ binding: 1, resource: { buffer: dstBuffer } },
|
|
466
470
|
{ binding: 2, resource: { buffer: paramsBuffer } }
|
|
467
471
|
];
|
|
468
472
|
if (prepPlanes) {
|
|
469
|
-
|
|
473
|
+
entries2.push({ binding: 3, resource: prepPlanes[1].createView() });
|
|
470
474
|
} else if (this._usesSampler) {
|
|
471
475
|
this._sampler ??= device.createSampler({
|
|
472
476
|
label: `${this.label}-clamp-sampler`,
|
|
473
477
|
addressModeU: "clamp-to-edge",
|
|
474
478
|
addressModeV: "clamp-to-edge"
|
|
475
479
|
});
|
|
476
|
-
|
|
480
|
+
entries2.push({ binding: 3, resource: this._sampler });
|
|
477
481
|
}
|
|
478
482
|
bindGroup = device.createBindGroup({
|
|
479
483
|
label: `${this.label}-bg`,
|
|
480
484
|
layout: pipeline.getBindGroupLayout(0),
|
|
481
|
-
entries
|
|
485
|
+
entries: entries2
|
|
482
486
|
});
|
|
483
487
|
if (useCache) this._cachedBindGroup = bindGroup;
|
|
484
488
|
}
|
|
@@ -646,7 +650,7 @@ var Encoder = class {
|
|
|
646
650
|
(g, i) => this._createPrepBindGroup(prepPipeline, texs[i].createView(), planeSets[i], paramsBuf, i * CHAIN_ALIGN)
|
|
647
651
|
) : [];
|
|
648
652
|
bindGroups = geoms.map((g, i) => {
|
|
649
|
-
const
|
|
653
|
+
const entries2 = [
|
|
650
654
|
{
|
|
651
655
|
binding: 0,
|
|
652
656
|
resource: prepPipeline ? planeSets[i][0].createView() : texs[i].createView()
|
|
@@ -655,14 +659,14 @@ var Encoder = class {
|
|
|
655
659
|
{ binding: 2, resource: { buffer: paramsBuf, offset: i * CHAIN_ALIGN, size: 16 } }
|
|
656
660
|
];
|
|
657
661
|
if (prepPipeline) {
|
|
658
|
-
|
|
662
|
+
entries2.push({ binding: 3, resource: planeSets[i][1].createView() });
|
|
659
663
|
} else if (this._usesSampler) {
|
|
660
|
-
|
|
664
|
+
entries2.push({ binding: 3, resource: this._sampler });
|
|
661
665
|
}
|
|
662
666
|
return device.createBindGroup({
|
|
663
667
|
label: `${this.label}-chain-bg-${i}`,
|
|
664
668
|
layout: pipeline.getBindGroupLayout(0),
|
|
665
|
-
entries
|
|
669
|
+
entries: entries2
|
|
666
670
|
});
|
|
667
671
|
});
|
|
668
672
|
if (useCache) {
|
|
@@ -813,7 +817,7 @@ var Encoder = class {
|
|
|
813
817
|
)
|
|
814
818
|
) : null;
|
|
815
819
|
const bindGroups = geoms.map((g, i) => {
|
|
816
|
-
const
|
|
820
|
+
const entries2 = [
|
|
817
821
|
{
|
|
818
822
|
binding: 0,
|
|
819
823
|
resource: planes ? planes[i][0].createView() : srcTex.createView({ baseMipLevel: i, mipLevelCount: 1 })
|
|
@@ -822,14 +826,14 @@ var Encoder = class {
|
|
|
822
826
|
{ binding: 2, resource: { buffer: paramsBuf, offset: i * CHAIN_ALIGN, size: 16 } }
|
|
823
827
|
];
|
|
824
828
|
if (planes) {
|
|
825
|
-
|
|
829
|
+
entries2.push({ binding: 3, resource: planes[i][1].createView() });
|
|
826
830
|
} else if (this._usesSampler) {
|
|
827
|
-
|
|
831
|
+
entries2.push({ binding: 3, resource: this._sampler });
|
|
828
832
|
}
|
|
829
833
|
return device.createBindGroup({
|
|
830
834
|
label: `${this.label}-chain-bg-${i}`,
|
|
831
835
|
layout: pipeline.getBindGroupLayout(0),
|
|
832
|
-
entries
|
|
836
|
+
entries: entries2
|
|
833
837
|
});
|
|
834
838
|
});
|
|
835
839
|
return await this._submitChainAndRead(
|
|
@@ -2492,6 +2496,18 @@ async function generateGpuMipChain(device, source, { flipY = false } = {}) {
|
|
|
2492
2496
|
}
|
|
2493
2497
|
|
|
2494
2498
|
// src/svg.ts
|
|
2499
|
+
function isSvgMarkup(source) {
|
|
2500
|
+
return source.trimStart().startsWith("<");
|
|
2501
|
+
}
|
|
2502
|
+
function hasSvgExtension(url) {
|
|
2503
|
+
return /\.svg$/i.test(url.split(/[?#]/, 1)[0]);
|
|
2504
|
+
}
|
|
2505
|
+
function isSvgBlob(blob) {
|
|
2506
|
+
if (blob.type) {
|
|
2507
|
+
return blob.type.split(";", 1)[0].trim().toLowerCase() === "image/svg+xml";
|
|
2508
|
+
}
|
|
2509
|
+
return typeof File !== "undefined" && blob instanceof File && hasSvgExtension(blob.name);
|
|
2510
|
+
}
|
|
2495
2511
|
var ROOT_TAG_RE = /<svg(?=[\s/>])[^>]*>/;
|
|
2496
2512
|
function getAttr(tag, name) {
|
|
2497
2513
|
const m = new RegExp(`\\s${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`).exec(tag);
|
|
@@ -2612,6 +2628,482 @@ async function rasterizeSvg(source, options = {}) {
|
|
|
2612
2628
|
URL.revokeObjectURL(url);
|
|
2613
2629
|
}
|
|
2614
2630
|
}
|
|
2631
|
+
|
|
2632
|
+
// src/transcodeCache.ts
|
|
2633
|
+
var DEFAULT_LIMIT = 256 * 1024 * 1024;
|
|
2634
|
+
var maxBytes = DEFAULT_LIMIT;
|
|
2635
|
+
var totalBytes = 0;
|
|
2636
|
+
var entries = /* @__PURE__ */ new Map();
|
|
2637
|
+
async function sha256Hex(data) {
|
|
2638
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
2639
|
+
let hex = "";
|
|
2640
|
+
for (const b of new Uint8Array(digest)) hex += b.toString(16).padStart(2, "0");
|
|
2641
|
+
return hex;
|
|
2642
|
+
}
|
|
2643
|
+
async function sourceIdentity(source, cacheKey) {
|
|
2644
|
+
if (cacheKey) return `k:${cacheKey}`;
|
|
2645
|
+
const canHash = typeof crypto !== "undefined" && !!crypto.subtle;
|
|
2646
|
+
if (typeof source === "string") {
|
|
2647
|
+
if (source.length > 1024 || /^data:/i.test(source) || /^\s*</.test(source)) {
|
|
2648
|
+
return canHash ? `s:${await sha256Hex(new TextEncoder().encode(source))}` : null;
|
|
2649
|
+
}
|
|
2650
|
+
return `u:${source}`;
|
|
2651
|
+
}
|
|
2652
|
+
if (typeof Blob !== "undefined" && source instanceof Blob) {
|
|
2653
|
+
return canHash ? `b:${await sha256Hex(await source.arrayBuffer())}` : null;
|
|
2654
|
+
}
|
|
2655
|
+
return null;
|
|
2656
|
+
}
|
|
2657
|
+
async function buildTranscodeKey(source, cacheKey, fp) {
|
|
2658
|
+
if (maxBytes <= 0) return null;
|
|
2659
|
+
const id = await sourceIdentity(source, cacheKey);
|
|
2660
|
+
if (!id) return null;
|
|
2661
|
+
const fingerprint = [
|
|
2662
|
+
fp.format,
|
|
2663
|
+
fp.colorSpace,
|
|
2664
|
+
fp.flipY ? "flip" : "noflip",
|
|
2665
|
+
fp.mipmaps ? "mips" : "nomips",
|
|
2666
|
+
fp.svgSize === void 0 ? "" : JSON.stringify(fp.svgSize)
|
|
2667
|
+
].join("|");
|
|
2668
|
+
return `${fingerprint}\0${id}`;
|
|
2669
|
+
}
|
|
2670
|
+
function readTranscodeCache(key) {
|
|
2671
|
+
const hit = entries.get(key);
|
|
2672
|
+
if (!hit) return null;
|
|
2673
|
+
entries.delete(key);
|
|
2674
|
+
entries.set(key, hit);
|
|
2675
|
+
return hit.entry;
|
|
2676
|
+
}
|
|
2677
|
+
function writeTranscodeCache(key, entry) {
|
|
2678
|
+
const bytes = entry.levels.reduce((sum, l) => sum + l.data.byteLength, 0);
|
|
2679
|
+
if (bytes > maxBytes) return;
|
|
2680
|
+
const prev = entries.get(key);
|
|
2681
|
+
if (prev) {
|
|
2682
|
+
totalBytes -= prev.bytes;
|
|
2683
|
+
entries.delete(key);
|
|
2684
|
+
}
|
|
2685
|
+
entries.set(key, { entry, bytes });
|
|
2686
|
+
totalBytes += bytes;
|
|
2687
|
+
evictToLimit();
|
|
2688
|
+
}
|
|
2689
|
+
function evictToLimit() {
|
|
2690
|
+
for (const [key, value] of entries) {
|
|
2691
|
+
if (totalBytes <= maxBytes) break;
|
|
2692
|
+
entries.delete(key);
|
|
2693
|
+
totalBytes -= value.bytes;
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
function setTranscodeCacheLimit(bytes) {
|
|
2697
|
+
maxBytes = Math.max(0, bytes);
|
|
2698
|
+
evictToLimit();
|
|
2699
|
+
}
|
|
2700
|
+
function clearTranscodeCache() {
|
|
2701
|
+
entries.clear();
|
|
2702
|
+
totalBytes = 0;
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
// src/compressTexture.ts
|
|
2706
|
+
var sharedGpuPromise = null;
|
|
2707
|
+
var SHARED_DEVICE_FEATURES = [
|
|
2708
|
+
"texture-compression-bc",
|
|
2709
|
+
"texture-compression-astc",
|
|
2710
|
+
"texture-compression-etc2",
|
|
2711
|
+
"shader-f16",
|
|
2712
|
+
"timestamp-query"
|
|
2713
|
+
];
|
|
2714
|
+
async function createSharedGpu() {
|
|
2715
|
+
const adapter = await navigator.gpu.requestAdapter();
|
|
2716
|
+
if (!adapter) return null;
|
|
2717
|
+
const requiredFeatures = SHARED_DEVICE_FEATURES.filter((f) => adapter.features.has(f));
|
|
2718
|
+
const device = await adapter.requestDevice({ requiredFeatures });
|
|
2719
|
+
return { adapter, device, encoders: /* @__PURE__ */ new Map() };
|
|
2720
|
+
}
|
|
2721
|
+
function getSharedGpu() {
|
|
2722
|
+
if (!sharedGpuPromise) {
|
|
2723
|
+
const p = createSharedGpu();
|
|
2724
|
+
sharedGpuPromise = p;
|
|
2725
|
+
p.then((shared) => {
|
|
2726
|
+
if (!shared) return;
|
|
2727
|
+
void shared.device.lost.then(() => {
|
|
2728
|
+
shared.encoders.forEach((encoder) => encoder.destroy());
|
|
2729
|
+
shared.encoders.clear();
|
|
2730
|
+
if (sharedGpuPromise === p) sharedGpuPromise = null;
|
|
2731
|
+
});
|
|
2732
|
+
}).catch(() => {
|
|
2733
|
+
if (sharedGpuPromise === p) sharedGpuPromise = null;
|
|
2734
|
+
});
|
|
2735
|
+
}
|
|
2736
|
+
return sharedGpuPromise;
|
|
2737
|
+
}
|
|
2738
|
+
function releaseSharedGpuResources() {
|
|
2739
|
+
const p = sharedGpuPromise;
|
|
2740
|
+
sharedGpuPromise = null;
|
|
2741
|
+
void p?.then((shared) => {
|
|
2742
|
+
if (!shared) return;
|
|
2743
|
+
shared.encoders.forEach((encoder) => encoder.destroy());
|
|
2744
|
+
shared.encoders.clear();
|
|
2745
|
+
shared.device.destroy();
|
|
2746
|
+
}).catch(() => {
|
|
2747
|
+
});
|
|
2748
|
+
}
|
|
2749
|
+
async function sourceToBitmap(source, svgSize) {
|
|
2750
|
+
const opts = {
|
|
2751
|
+
colorSpaceConversion: "none",
|
|
2752
|
+
premultiplyAlpha: "none"
|
|
2753
|
+
};
|
|
2754
|
+
if (typeof source === "string") {
|
|
2755
|
+
if (isSvgMarkup(source)) {
|
|
2756
|
+
return rasterizeSvg(source, { size: svgSize });
|
|
2757
|
+
}
|
|
2758
|
+
if (/^data:/i.test(source)) {
|
|
2759
|
+
const blob2 = dataUrlToBlob(source);
|
|
2760
|
+
if (isSvgBlob(blob2)) {
|
|
2761
|
+
return rasterizeSvg(blob2, { size: svgSize });
|
|
2762
|
+
}
|
|
2763
|
+
return createImageBitmap(blob2, opts);
|
|
2764
|
+
}
|
|
2765
|
+
const resp = await fetch(source);
|
|
2766
|
+
if (!resp.ok) {
|
|
2767
|
+
throw new Error(`compressTexture: fetch ${source} failed (${resp.status})`);
|
|
2768
|
+
}
|
|
2769
|
+
const blob = await resp.blob();
|
|
2770
|
+
if (isSvgBlob(blob) || !isImageMimeType(blob.type) && hasSvgExtension(source)) {
|
|
2771
|
+
return rasterizeSvg(blob, { size: svgSize });
|
|
2772
|
+
}
|
|
2773
|
+
return createImageBitmap(blob, opts);
|
|
2774
|
+
}
|
|
2775
|
+
if (source instanceof Blob) {
|
|
2776
|
+
if (isSvgBlob(source)) {
|
|
2777
|
+
return rasterizeSvg(source, { size: svgSize });
|
|
2778
|
+
}
|
|
2779
|
+
return createImageBitmap(source, opts);
|
|
2780
|
+
}
|
|
2781
|
+
if (source instanceof ImageBitmap) {
|
|
2782
|
+
return source;
|
|
2783
|
+
}
|
|
2784
|
+
if (typeof HTMLImageElement !== "undefined" && source instanceof HTMLImageElement) {
|
|
2785
|
+
const src = source.currentSrc || source.src;
|
|
2786
|
+
if (src && (hasSvgExtension(src) || /^data:image\/svg\+xml/i.test(src))) {
|
|
2787
|
+
const resp = await fetch(src);
|
|
2788
|
+
if (!resp.ok) {
|
|
2789
|
+
throw new Error(`compressTexture: fetch ${src} failed (${resp.status})`);
|
|
2790
|
+
}
|
|
2791
|
+
return rasterizeSvg(await resp.blob(), { size: svgSize });
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
return createImageBitmap(source, opts);
|
|
2795
|
+
}
|
|
2796
|
+
function isImageMimeType(type) {
|
|
2797
|
+
return /^image\//i.test(type) && !/svg/i.test(type);
|
|
2798
|
+
}
|
|
2799
|
+
function dataUrlToBlob(url) {
|
|
2800
|
+
const comma = url.indexOf(",");
|
|
2801
|
+
if (comma < 0) {
|
|
2802
|
+
throw new Error("compressTexture: malformed data: URL (no comma)");
|
|
2803
|
+
}
|
|
2804
|
+
const header = url.slice(5, comma);
|
|
2805
|
+
const isBase64 = /;base64$/i.test(header);
|
|
2806
|
+
const type = header.replace(/;base64$/i, "");
|
|
2807
|
+
if (!isBase64) {
|
|
2808
|
+
return new Blob([decodeURIComponent(url.slice(comma + 1))], { type });
|
|
2809
|
+
}
|
|
2810
|
+
const payload = url.slice(comma + 1);
|
|
2811
|
+
const fromBase64 = Uint8Array.fromBase64;
|
|
2812
|
+
if (fromBase64) {
|
|
2813
|
+
return new Blob([fromBase64(payload)], { type });
|
|
2814
|
+
}
|
|
2815
|
+
const bin = atob(payload);
|
|
2816
|
+
const bytes = new Uint8Array(bin.length);
|
|
2817
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
2818
|
+
return new Blob([bytes], { type });
|
|
2819
|
+
}
|
|
2820
|
+
function bitmapToMipLevel(bitmap, flipY) {
|
|
2821
|
+
const w = bitmap.width, h = bitmap.height;
|
|
2822
|
+
const canvas = typeof OffscreenCanvas !== "undefined" ? new OffscreenCanvas(w, h) : Object.assign(document.createElement("canvas"), { width: w, height: h });
|
|
2823
|
+
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
2824
|
+
if (!ctx) {
|
|
2825
|
+
throw new Error("compressTexture: no 2D context available for mip generation");
|
|
2826
|
+
}
|
|
2827
|
+
if (flipY) {
|
|
2828
|
+
ctx.translate(0, h);
|
|
2829
|
+
ctx.scale(1, -1);
|
|
2830
|
+
}
|
|
2831
|
+
ctx.drawImage(bitmap, 0, 0);
|
|
2832
|
+
const imageData = ctx.getImageData(0, 0, w, h);
|
|
2833
|
+
return { data: imageData.data, width: w, height: h };
|
|
2834
|
+
}
|
|
2835
|
+
function mipLevelToImageData(level) {
|
|
2836
|
+
return new ImageData(level.data, level.width, level.height);
|
|
2837
|
+
}
|
|
2838
|
+
async function compressTextureToBytes(source, options = {}) {
|
|
2839
|
+
const {
|
|
2840
|
+
hint = "color",
|
|
2841
|
+
preferredFormat,
|
|
2842
|
+
quality = "high",
|
|
2843
|
+
colorSpace = "srgb",
|
|
2844
|
+
svgSize,
|
|
2845
|
+
flipY = true,
|
|
2846
|
+
mipmaps = false,
|
|
2847
|
+
cache = false,
|
|
2848
|
+
cacheKey,
|
|
2849
|
+
device: providedDevice,
|
|
2850
|
+
adapter: providedAdapter
|
|
2851
|
+
} = options;
|
|
2852
|
+
const t0 = performance.now();
|
|
2853
|
+
const gpu = await resolveWebGPU();
|
|
2854
|
+
const gl = gpu ? null : resolveWebGL();
|
|
2855
|
+
const activeFormat = gpu?.selection.format ?? gl?.selection.format ?? null;
|
|
2856
|
+
let transcodeKey = null;
|
|
2857
|
+
if (cache && activeFormat) {
|
|
2858
|
+
transcodeKey = await buildTranscodeKey(source, cacheKey, {
|
|
2859
|
+
format: activeFormat,
|
|
2860
|
+
colorSpace,
|
|
2861
|
+
flipY,
|
|
2862
|
+
mipmaps,
|
|
2863
|
+
svgSize
|
|
2864
|
+
});
|
|
2865
|
+
if (transcodeKey) {
|
|
2866
|
+
const hit = readTranscodeCache(transcodeKey);
|
|
2867
|
+
if (hit) {
|
|
2868
|
+
return {
|
|
2869
|
+
levels: hit.levels,
|
|
2870
|
+
fallbackBitmap: null,
|
|
2871
|
+
format: hit.format,
|
|
2872
|
+
fallbackUncompressed: false,
|
|
2873
|
+
backend: gpu ? "webgpu" : "webgl",
|
|
2874
|
+
astcNormalRemap: (gpu ?? gl).selection.astcNormalRemap,
|
|
2875
|
+
width: hit.width,
|
|
2876
|
+
height: hit.height,
|
|
2877
|
+
mipLevels: hit.levels.length,
|
|
2878
|
+
encodeMs: 0,
|
|
2879
|
+
decodeMs: 0,
|
|
2880
|
+
totalMs: performance.now() - t0,
|
|
2881
|
+
cacheHit: true
|
|
2882
|
+
};
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
const tDecode = performance.now();
|
|
2887
|
+
const bitmap = await sourceToBitmap(source, svgSize);
|
|
2888
|
+
const decodeMs = performance.now() - tDecode;
|
|
2889
|
+
if (gpu) return encodeViaWebGPU(gpu);
|
|
2890
|
+
const viaWebGL = gl ? encodeViaWebGL(gl) : null;
|
|
2891
|
+
if (viaWebGL) return viaWebGL;
|
|
2892
|
+
console.warn(
|
|
2893
|
+
"[compressTextureToBytes] No compressed path available (WebGPU and WebGL2 both lack a usable compressed-texture format); returning uncompressed RGBA8."
|
|
2894
|
+
);
|
|
2895
|
+
return {
|
|
2896
|
+
levels: null,
|
|
2897
|
+
fallbackBitmap: bitmap,
|
|
2898
|
+
format: null,
|
|
2899
|
+
fallbackUncompressed: true,
|
|
2900
|
+
backend: "none",
|
|
2901
|
+
astcNormalRemap: false,
|
|
2902
|
+
width: bitmap.width,
|
|
2903
|
+
height: bitmap.height,
|
|
2904
|
+
mipLevels: 1,
|
|
2905
|
+
encodeMs: 0,
|
|
2906
|
+
decodeMs,
|
|
2907
|
+
totalMs: performance.now() - t0,
|
|
2908
|
+
cacheHit: false
|
|
2909
|
+
};
|
|
2910
|
+
async function resolveWebGPU() {
|
|
2911
|
+
if (!("gpu" in navigator)) return null;
|
|
2912
|
+
let shared = null;
|
|
2913
|
+
let adapter;
|
|
2914
|
+
if (providedAdapter) {
|
|
2915
|
+
adapter = providedAdapter;
|
|
2916
|
+
} else if (providedDevice) {
|
|
2917
|
+
adapter = await navigator.gpu.requestAdapter();
|
|
2918
|
+
} else {
|
|
2919
|
+
shared = await getSharedGpu();
|
|
2920
|
+
adapter = shared?.adapter ?? null;
|
|
2921
|
+
}
|
|
2922
|
+
if (!adapter) return null;
|
|
2923
|
+
const selection = selectFormat(adapter, hint, { colorSpace, preferredFormat, quality });
|
|
2924
|
+
if (!selection.format || !selection.encoderClass) return null;
|
|
2925
|
+
return {
|
|
2926
|
+
adapter,
|
|
2927
|
+
shared,
|
|
2928
|
+
selection: { ...selection, format: selection.format, encoderClass: selection.encoderClass }
|
|
2929
|
+
};
|
|
2930
|
+
}
|
|
2931
|
+
async function encodeViaWebGPU({ adapter, shared, selection }) {
|
|
2932
|
+
const EncoderCtor = selection.encoderClass;
|
|
2933
|
+
let encoder;
|
|
2934
|
+
let sharedEncoder = false;
|
|
2935
|
+
if (providedDevice) {
|
|
2936
|
+
encoder = new EncoderCtor({ device: providedDevice, adapter, ownsDevice: false });
|
|
2937
|
+
} else if (shared) {
|
|
2938
|
+
sharedEncoder = true;
|
|
2939
|
+
let cached = shared.encoders.get(EncoderCtor);
|
|
2940
|
+
if (!cached) {
|
|
2941
|
+
cached = new EncoderCtor({ device: shared.device, adapter: shared.adapter, ownsDevice: false });
|
|
2942
|
+
shared.encoders.set(EncoderCtor, cached);
|
|
2943
|
+
}
|
|
2944
|
+
encoder = cached;
|
|
2945
|
+
} else {
|
|
2946
|
+
encoder = await EncoderCtor.create();
|
|
2947
|
+
}
|
|
2948
|
+
const destroyEncoder = sharedEncoder ? () => {
|
|
2949
|
+
} : () => encoder.destroy();
|
|
2950
|
+
try {
|
|
2951
|
+
const needsWriteTexture = needsWriteTextureWorkaround(adapter);
|
|
2952
|
+
if (!mipmaps) {
|
|
2953
|
+
let bytes;
|
|
2954
|
+
if (needsWriteTexture) {
|
|
2955
|
+
const level0 = bitmapToMipLevel(bitmap, flipY);
|
|
2956
|
+
const imageData = mipLevelToImageData(level0);
|
|
2957
|
+
bytes = await encoder.encodeToBytes(imageData);
|
|
2958
|
+
} else {
|
|
2959
|
+
bytes = await encoder.encodeToBytes(bitmap, { flipY });
|
|
2960
|
+
}
|
|
2961
|
+
if (transcodeKey) {
|
|
2962
|
+
writeTranscodeCache(transcodeKey, {
|
|
2963
|
+
format: selection.format,
|
|
2964
|
+
width: bytes.width,
|
|
2965
|
+
height: bytes.height,
|
|
2966
|
+
levels: [bytes]
|
|
2967
|
+
});
|
|
2968
|
+
}
|
|
2969
|
+
destroyEncoder();
|
|
2970
|
+
return {
|
|
2971
|
+
levels: [bytes],
|
|
2972
|
+
fallbackBitmap: null,
|
|
2973
|
+
format: selection.format,
|
|
2974
|
+
fallbackUncompressed: false,
|
|
2975
|
+
backend: "webgpu",
|
|
2976
|
+
astcNormalRemap: selection.astcNormalRemap,
|
|
2977
|
+
width: bytes.width,
|
|
2978
|
+
height: bytes.height,
|
|
2979
|
+
mipLevels: 1,
|
|
2980
|
+
encodeMs: bytes.encodeMs,
|
|
2981
|
+
decodeMs,
|
|
2982
|
+
totalMs: performance.now() - t0,
|
|
2983
|
+
cacheHit: false
|
|
2984
|
+
};
|
|
2985
|
+
}
|
|
2986
|
+
let chainResult;
|
|
2987
|
+
if (needsWriteTexture) {
|
|
2988
|
+
const level0 = bitmapToMipLevel(bitmap, flipY);
|
|
2989
|
+
chainResult = await encoder.encodeMipChainToBytes(generateMipChain(level0).map(padToBlockMultiple));
|
|
2990
|
+
} else {
|
|
2991
|
+
const chainTex = await generateGpuMipChain(encoder.device, bitmap, { flipY });
|
|
2992
|
+
try {
|
|
2993
|
+
chainResult = await encoder.encodeMipChainFromTexture(chainTex);
|
|
2994
|
+
} finally {
|
|
2995
|
+
chainTex.destroy();
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
const { levels, encodeMs } = chainResult;
|
|
2999
|
+
if (transcodeKey) {
|
|
3000
|
+
writeTranscodeCache(transcodeKey, {
|
|
3001
|
+
format: selection.format,
|
|
3002
|
+
width: bitmap.width,
|
|
3003
|
+
height: bitmap.height,
|
|
3004
|
+
levels
|
|
3005
|
+
});
|
|
3006
|
+
}
|
|
3007
|
+
destroyEncoder();
|
|
3008
|
+
return {
|
|
3009
|
+
levels,
|
|
3010
|
+
fallbackBitmap: null,
|
|
3011
|
+
format: selection.format,
|
|
3012
|
+
fallbackUncompressed: false,
|
|
3013
|
+
backend: "webgpu",
|
|
3014
|
+
astcNormalRemap: selection.astcNormalRemap,
|
|
3015
|
+
width: bitmap.width,
|
|
3016
|
+
height: bitmap.height,
|
|
3017
|
+
mipLevels: levels.length,
|
|
3018
|
+
encodeMs,
|
|
3019
|
+
decodeMs,
|
|
3020
|
+
totalMs: performance.now() - t0,
|
|
3021
|
+
cacheHit: false
|
|
3022
|
+
};
|
|
3023
|
+
} catch (e) {
|
|
3024
|
+
destroyEncoder();
|
|
3025
|
+
throw e;
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
function resolveWebGL() {
|
|
3029
|
+
const gl2 = getSharedWebGLContext();
|
|
3030
|
+
if (!gl2) return null;
|
|
3031
|
+
const caps = detectWebGLCapabilities(gl2);
|
|
3032
|
+
const selection = selectWebGLFormat(caps, hint, { colorSpace, preferredFormat, quality });
|
|
3033
|
+
if (!selection.format || !selection.encoderClass) return null;
|
|
3034
|
+
return { gl: gl2, selection: { ...selection, format: selection.format, encoderClass: selection.encoderClass } };
|
|
3035
|
+
}
|
|
3036
|
+
function encodeViaWebGL({ gl: gl2, selection }) {
|
|
3037
|
+
const encoder = selection.encoderClass.create(gl2);
|
|
3038
|
+
try {
|
|
3039
|
+
if (!mipmaps) {
|
|
3040
|
+
const bytes = encoder.encodeToBytes(bitmap, { flipY });
|
|
3041
|
+
if (transcodeKey) {
|
|
3042
|
+
writeTranscodeCache(transcodeKey, {
|
|
3043
|
+
format: selection.format,
|
|
3044
|
+
width: bytes.width,
|
|
3045
|
+
height: bytes.height,
|
|
3046
|
+
levels: [bytes]
|
|
3047
|
+
});
|
|
3048
|
+
}
|
|
3049
|
+
encoder.destroy();
|
|
3050
|
+
return {
|
|
3051
|
+
levels: [bytes],
|
|
3052
|
+
fallbackBitmap: null,
|
|
3053
|
+
format: selection.format,
|
|
3054
|
+
fallbackUncompressed: false,
|
|
3055
|
+
backend: "webgl",
|
|
3056
|
+
astcNormalRemap: selection.astcNormalRemap,
|
|
3057
|
+
width: bytes.width,
|
|
3058
|
+
height: bytes.height,
|
|
3059
|
+
mipLevels: 1,
|
|
3060
|
+
encodeMs: bytes.encodeMs,
|
|
3061
|
+
decodeMs,
|
|
3062
|
+
totalMs: performance.now() - t0,
|
|
3063
|
+
cacheHit: false
|
|
3064
|
+
};
|
|
3065
|
+
}
|
|
3066
|
+
const level0 = bitmapToMipLevel(bitmap, flipY);
|
|
3067
|
+
const chain = generateMipChain(level0);
|
|
3068
|
+
const encodedLevels = [];
|
|
3069
|
+
let totalEncodeMs = 0;
|
|
3070
|
+
for (const level of chain) {
|
|
3071
|
+
const padded = padToBlockMultiple(level);
|
|
3072
|
+
const bytes = encoder.encodeToBytes(padded);
|
|
3073
|
+
encodedLevels.push(bytes);
|
|
3074
|
+
totalEncodeMs += bytes.encodeMs;
|
|
3075
|
+
}
|
|
3076
|
+
if (transcodeKey) {
|
|
3077
|
+
writeTranscodeCache(transcodeKey, {
|
|
3078
|
+
format: selection.format,
|
|
3079
|
+
width: level0.width,
|
|
3080
|
+
height: level0.height,
|
|
3081
|
+
levels: encodedLevels
|
|
3082
|
+
});
|
|
3083
|
+
}
|
|
3084
|
+
encoder.destroy();
|
|
3085
|
+
return {
|
|
3086
|
+
levels: encodedLevels,
|
|
3087
|
+
fallbackBitmap: null,
|
|
3088
|
+
format: selection.format,
|
|
3089
|
+
fallbackUncompressed: false,
|
|
3090
|
+
backend: "webgl",
|
|
3091
|
+
astcNormalRemap: selection.astcNormalRemap,
|
|
3092
|
+
width: level0.width,
|
|
3093
|
+
height: level0.height,
|
|
3094
|
+
mipLevels: encodedLevels.length,
|
|
3095
|
+
encodeMs: totalEncodeMs,
|
|
3096
|
+
decodeMs,
|
|
3097
|
+
totalMs: performance.now() - t0,
|
|
3098
|
+
cacheHit: false
|
|
3099
|
+
};
|
|
3100
|
+
} catch (e) {
|
|
3101
|
+
encoder.destroy();
|
|
3102
|
+
console.warn("[compressTextureToBytes] WebGL fallback encode failed; returning uncompressed RGBA8.", e);
|
|
3103
|
+
return null;
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
2615
3107
|
export {
|
|
2616
3108
|
ASTC4x4Encoder,
|
|
2617
3109
|
ASTC4x4WebGLEncoder,
|
|
@@ -2626,6 +3118,8 @@ export {
|
|
|
2626
3118
|
TextureFormat,
|
|
2627
3119
|
WebGLBlockEncoder,
|
|
2628
3120
|
WebGPUFeature,
|
|
3121
|
+
clearTranscodeCache,
|
|
3122
|
+
compressTextureToBytes,
|
|
2629
3123
|
createWebGLContext,
|
|
2630
3124
|
detectCapabilities,
|
|
2631
3125
|
detectWebGLCapabilities,
|
|
@@ -2636,6 +3130,8 @@ export {
|
|
|
2636
3130
|
isWebGLAvailable,
|
|
2637
3131
|
padToBlockMultiple,
|
|
2638
3132
|
rasterizeSvg,
|
|
3133
|
+
releaseSharedGpuResources,
|
|
2639
3134
|
selectFormat,
|
|
2640
|
-
selectWebGLFormat
|
|
3135
|
+
selectWebGLFormat,
|
|
3136
|
+
setTranscodeCacheLimit
|
|
2641
3137
|
};
|
package/dist/three.d.ts
CHANGED
|
@@ -1,139 +1,18 @@
|
|
|
1
|
-
import { TextureHint, PreferredFormat, FormatQuality, SvgRasterSize, TextureFormat, Encoder, EncoderImageSource } from './index.js';
|
|
2
|
-
export { ASTC4x4Encoder, ASTC4x4WebGLEncoder, BC1Encoder, BC1WebGLEncoder, BC5Encoder, BC5WebGLEncoder, BC7Encoder, BC7EncoderOptions, BC7WebGLEncoder, Capabilities, ETC2Encoder, EncodeBytesResult, EncodeCallOptions, EncodeMipChainResult, EncodedLevelBytes, EncoderConstructor, EncoderOptions, ExtensionProvider, FeatureProvider, FormatSelection, FormatVariant, MipLevel, RasterizeSvgOptions, RawPixelSource, SelectFormatOptions, WebGLBlockEncoder, WebGLCapabilities, WebGLEncodeBytesResult, WebGLEncoderConstructor, WebGLEncoderImageSource, WebGLEncoderOptions, WebGLFormatSelection, WebGPUFeature, createWebGLContext, detectCapabilities, detectWebGLCapabilities, generateGpuMipChain, generateMipChain, getSharedWebGLContext, gpuMipLevelCount, isWebGLAvailable, padToBlockMultiple, rasterizeSvg, selectFormat, selectWebGLFormat } from './index.js';
|
|
1
|
+
import { CompressResult as CompressResult$1, CompressTextureSource, CompressOptions, TextureHint, PreferredFormat, FormatQuality, SvgRasterSize, TextureFormat, Encoder, EncoderImageSource } from './index.js';
|
|
2
|
+
export { ASTC4x4Encoder, ASTC4x4WebGLEncoder, BC1Encoder, BC1WebGLEncoder, BC5Encoder, BC5WebGLEncoder, BC7Encoder, BC7EncoderOptions, BC7WebGLEncoder, Capabilities, ETC2Encoder, EncodeBytesResult, EncodeCallOptions, EncodeMipChainResult, EncodedLevelBytes, EncoderConstructor, EncoderOptions, ExtensionProvider, FeatureProvider, FormatSelection, FormatVariant, MipLevel, RasterizeSvgOptions, RawPixelSource, SelectFormatOptions, WebGLBlockEncoder, WebGLCapabilities, WebGLEncodeBytesResult, WebGLEncoderConstructor, WebGLEncoderImageSource, WebGLEncoderOptions, WebGLFormatSelection, WebGPUFeature, clearTranscodeCache, compressTextureToBytes, createWebGLContext, detectCapabilities, detectWebGLCapabilities, generateGpuMipChain, generateMipChain, getSharedWebGLContext, gpuMipLevelCount, isWebGLAvailable, padToBlockMultiple, rasterizeSvg, releaseSharedGpuResources, selectFormat, selectWebGLFormat, setTranscodeCacheLimit } from './index.js';
|
|
3
3
|
import { Texture, CompressedTexture, Loader, CompressedPixelFormat } from 'three';
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
*
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
*
|
|
10
|
-
* SVG works through all of these: a URL to an `.svg` file, a string of
|
|
11
|
-
* inline SVG markup (detected by a leading `<`), an SVG Blob/File, or an
|
|
12
|
-
* HTMLImageElement whose src is SVG. Vector sources are rasterised to RGBA
|
|
13
|
-
* before encoding — see the `svgSize` option.
|
|
14
|
-
*/
|
|
15
|
-
type CompressTextureSource = string | Blob | File | ImageBitmap | HTMLImageElement | HTMLCanvasElement | OffscreenCanvas | ImageData;
|
|
16
|
-
interface CompressOptions {
|
|
17
|
-
/** How the texture will be used. Drives format selection. Default 'color'. */
|
|
18
|
-
hint?: TextureHint;
|
|
19
|
-
/**
|
|
20
|
-
* Prefer a specific format over the default choice when the device
|
|
21
|
-
* supports it; falls back to the normal selection (BC7 → ASTC → ETC2 →
|
|
22
|
-
* RGBA8) when it doesn't. Currently only 'bc1': half the memory of BC7
|
|
23
|
-
* for opaque colour textures, at lower quality. Only honoured with
|
|
24
|
-
* `hint: 'color'` — BC1 can't carry real alpha or normal maps.
|
|
25
|
-
*/
|
|
26
|
-
preferredFormat?: PreferredFormat;
|
|
27
|
-
/**
|
|
28
|
-
* Memory/fidelity trade-off for opaque colour textures. Default 'high'
|
|
29
|
-
* (BC7 / ASTC 4×4, 1 byte/pixel). 'low' picks the 4-bpp formats when the
|
|
30
|
-
* device has one — BC1 on desktop-class GPUs, ETC2 RGB8 on mobile-class
|
|
31
|
-
* ones — halving GPU memory at visibly lower quality on smooth content.
|
|
32
|
-
* Ignored for `hint: 'colorWithAlpha'` and `hint: 'normal'` (the 4-bpp
|
|
33
|
-
* formats can't carry them). On the WebGL fallback tier only BC1 is
|
|
34
|
-
* available at 'low'.
|
|
35
|
-
*/
|
|
36
|
-
quality?: FormatQuality;
|
|
37
|
-
/** Pick the sRGB or linear variant of the chosen format. Default 'srgb'. */
|
|
38
|
-
colorSpace?: 'srgb' | 'linear';
|
|
39
|
-
/**
|
|
40
|
-
* Rasterisation size for SVG sources. A number scales the SVG so its
|
|
41
|
-
* longest side matches (aspect ratio preserved); `{ width, height }`
|
|
42
|
-
* rasterises at exactly that size. Default: the SVG's intrinsic size
|
|
43
|
-
* (absolute width/height attributes, else the viewBox dimensions).
|
|
44
|
-
* Ignored for non-SVG sources.
|
|
45
|
-
*/
|
|
46
|
-
svgSize?: SvgRasterSize;
|
|
47
|
-
/** Flip the image vertically before encoding. Default true (matches Three.js convention). */
|
|
48
|
-
flipY?: boolean;
|
|
49
|
-
/** Generate a full mip chain down to 1×1 on the CPU, encode every level. */
|
|
50
|
-
mipmaps?: boolean;
|
|
51
|
-
/** Reuse an existing device (e.g. Three.js's renderer device) instead
|
|
52
|
-
* of creating a new one. WebGPU path only. When provided, the encoder
|
|
53
|
-
* never destroys it. */
|
|
54
|
-
device?: GPUDevice;
|
|
55
|
-
adapter?: GPUAdapter;
|
|
56
|
-
/**
|
|
57
|
-
* Keep the compressed bytes in a session-scoped in-memory LRU and reuse
|
|
58
|
-
* them on repeat calls, skipping BOTH the image decode and the encode —
|
|
59
|
-
* the dominant costs. Re-loading a texture later in the session (e.g.
|
|
60
|
-
* two worlds sharing an atlas) becomes a few ms. Keyed by source
|
|
61
|
-
* identity + selected format + encode options; capped at 256 MiB of
|
|
62
|
-
* compressed bytes by default (`setTranscodeCacheLimit()` to tune) and
|
|
63
|
-
* never touches persistent storage. Default false.
|
|
64
|
-
*
|
|
65
|
-
* URL and Blob/File sources get an identity automatically (URL string or
|
|
66
|
-
* content hash). Pixel sources (ImageBitmap, canvas, ImageData) are only
|
|
67
|
-
* cached when `cacheKey` is provided.
|
|
68
|
-
*/
|
|
69
|
-
cache?: boolean;
|
|
70
|
-
/**
|
|
71
|
-
* Explicit cache identity for the source, overriding the derived one.
|
|
72
|
-
* Use when you already know a stable name (e.g. an asset path) and want
|
|
73
|
-
* to skip content hashing, or to make pixel sources cacheable.
|
|
74
|
-
*/
|
|
75
|
-
cacheKey?: string;
|
|
76
|
-
}
|
|
77
|
-
interface CompressResult {
|
|
78
|
-
/** CompressedTexture on a compressed path; Texture on RGBA8 fallback. */
|
|
5
|
+
/** A `compressTexture()` result: a ready-to-use texture plus the same encode
|
|
6
|
+
* metadata `compressTextureToBytes()` returns (minus the raw `levels`). */
|
|
7
|
+
interface CompressResult extends Omit<CompressResult$1, 'levels' | 'fallbackBitmap'> {
|
|
8
|
+
/** `CompressedTexture` on a compressed path; a plain `Texture` on the RGBA8 fallback. */
|
|
79
9
|
texture: Texture | CompressedTexture;
|
|
80
|
-
/**
|
|
81
|
-
|
|
82
|
-
/** True iff we returned an uncompressed Texture because no encoder fit. */
|
|
83
|
-
fallbackUncompressed: boolean;
|
|
84
|
-
/**
|
|
85
|
-
* Which backend produced the result. 'webgpu' = compute path, 'webgl' =
|
|
86
|
-
* fragment-shader fallback, 'none' = uncompressed RGBA8.
|
|
87
|
-
*/
|
|
88
|
-
backend: 'webgpu' | 'webgl' | 'none';
|
|
89
|
-
/**
|
|
90
|
-
* True iff the chosen format is ASTC and the hint was 'normal'. The
|
|
91
|
-
* caller must apply the (R, W) → (x, y) swizzle in the material — ASTC
|
|
92
|
-
* has no 2-channel mode, so normal maps ride the RGBA path.
|
|
93
|
-
*/
|
|
94
|
-
astcNormalRemap: boolean;
|
|
95
|
-
width: number;
|
|
96
|
-
height: number;
|
|
97
|
-
mipLevels: number;
|
|
98
|
-
/** Wall-clock time of GPU encoding, summed across mip levels. */
|
|
99
|
-
encodeMs: number;
|
|
100
|
-
/**
|
|
101
|
-
* Wall-clock time to turn the source into decoded RGBA pixels: fetch /
|
|
102
|
-
* base64 decode, image decode, SVG rasterisation. Usually the dominant
|
|
103
|
-
* cost for large images — when a load feels slower than `encodeMs`
|
|
104
|
-
* suggests, this is where the time went.
|
|
105
|
-
*/
|
|
106
|
-
decodeMs: number;
|
|
107
|
-
/** Wall-clock time of the whole `compressTexture()` call: decode + CPU
|
|
108
|
-
* mip generation + encode + texture assembly. */
|
|
109
|
-
totalMs: number;
|
|
110
|
-
/** True when the result came from the in-memory transcode cache (the
|
|
111
|
-
* `cache` option) — no decode or encode ran; decodeMs/encodeMs are 0. */
|
|
112
|
-
cacheHit: boolean;
|
|
113
|
-
/** Dispose the texture and release GPU resources owned by this call.
|
|
114
|
-
* On the default path (no `device`/`adapter` option) the encoder and
|
|
115
|
-
* device are shared across `compressTexture()` calls and survive this —
|
|
116
|
-
* release those with `releaseSharedGpuResources()`. */
|
|
10
|
+
/** Dispose the texture. The shared encoder/device survive — release those
|
|
11
|
+
* with `releaseSharedGpuResources()`. */
|
|
117
12
|
destroy(): void;
|
|
118
13
|
}
|
|
119
|
-
/**
|
|
120
|
-
* Destroy the WebGPU device and encoders that `compressTexture()` shares
|
|
121
|
-
* across calls (created lazily when neither the `device` nor the `adapter`
|
|
122
|
-
* option is passed). Safe to call at any time — in-flight encodes on the
|
|
123
|
-
* shared device will fail, and the next `compressTexture()` call recreates
|
|
124
|
-
* everything. No-op when nothing is cached.
|
|
125
|
-
*/
|
|
126
|
-
declare function releaseSharedGpuResources(): void;
|
|
127
14
|
declare function compressTexture(source: CompressTextureSource, options?: CompressOptions): Promise<CompressResult>;
|
|
128
15
|
|
|
129
|
-
/**
|
|
130
|
-
* Cap the cache's total compressed payload in bytes (default 256 MiB).
|
|
131
|
-
* Lower it to evict immediately; 0 disables caching entirely.
|
|
132
|
-
*/
|
|
133
|
-
declare function setTranscodeCacheLimit(bytes: number): void;
|
|
134
|
-
/** Drop every cached transcode. Textures already built from entries are unaffected. */
|
|
135
|
-
declare function clearTranscodeCache(): void;
|
|
136
|
-
|
|
137
16
|
declare class GputexLoader extends Loader<Texture> {
|
|
138
17
|
/** Format-selection hint. Default 'color'. */
|
|
139
18
|
hint: TextureHint;
|
|
@@ -230,4 +109,4 @@ interface EncodeToTextureOptions {
|
|
|
230
109
|
*/
|
|
231
110
|
declare function encodeToTexture(encoder: Encoder, source: EncoderImageSource, { colorSpace, flipY }?: EncodeToTextureOptions): Promise<EncodeResult>;
|
|
232
111
|
|
|
233
|
-
export {
|
|
112
|
+
export { CompressOptions, type CompressResult, CompressTextureSource, type EncodeResult, type EncodeToTextureOptions, Encoder, EncoderImageSource, FormatQuality, GputexLoader, PreferredFormat, SvgRasterSize, TextureFormat, TextureHint, buildCompressedTexture, compressTexture, encodeToTexture, threeFormatFor };
|
package/dist/three.js
CHANGED
|
@@ -2629,87 +2629,7 @@ async function rasterizeSvg(source, options = {}) {
|
|
|
2629
2629
|
}
|
|
2630
2630
|
}
|
|
2631
2631
|
|
|
2632
|
-
// src/
|
|
2633
|
-
import { ClampToEdgeWrapping as ClampToEdgeWrapping2, LinearFilter as LinearFilter2, LinearSRGBColorSpace as LinearSRGBColorSpace2, SRGBColorSpace as SRGBColorSpace2, Texture } from "three";
|
|
2634
|
-
|
|
2635
|
-
// src/three/buildTexture.ts
|
|
2636
|
-
import {
|
|
2637
|
-
RED_GREEN_RGTC2_Format,
|
|
2638
|
-
RGB_ETC2_Format,
|
|
2639
|
-
RGBA_ASTC_4x4_Format,
|
|
2640
|
-
RGBA_BPTC_Format,
|
|
2641
|
-
RGBA_S3TC_DXT1_Format
|
|
2642
|
-
} from "three";
|
|
2643
|
-
|
|
2644
|
-
// src/three/textureAssembly.ts
|
|
2645
|
-
import {
|
|
2646
|
-
ClampToEdgeWrapping,
|
|
2647
|
-
CompressedTexture,
|
|
2648
|
-
LinearFilter,
|
|
2649
|
-
LinearMipmapLinearFilter,
|
|
2650
|
-
LinearSRGBColorSpace,
|
|
2651
|
-
SRGBColorSpace
|
|
2652
|
-
} from "three";
|
|
2653
|
-
function assembleCompressedTexture(levels, threeFormat, effectiveSrgb) {
|
|
2654
|
-
if (levels.length === 0) {
|
|
2655
|
-
throw new Error("assembleCompressedTexture: no levels provided");
|
|
2656
|
-
}
|
|
2657
|
-
const mipmaps = levels.map((l) => ({
|
|
2658
|
-
data: l.data,
|
|
2659
|
-
width: l.paddedWidth,
|
|
2660
|
-
height: l.paddedHeight
|
|
2661
|
-
}));
|
|
2662
|
-
const base = levels[0];
|
|
2663
|
-
const texture = new CompressedTexture(mipmaps, base.paddedWidth, base.paddedHeight, threeFormat);
|
|
2664
|
-
texture.colorSpace = effectiveSrgb ? SRGBColorSpace : LinearSRGBColorSpace;
|
|
2665
|
-
texture.magFilter = LinearFilter;
|
|
2666
|
-
texture.minFilter = levels.length > 1 ? LinearMipmapLinearFilter : LinearFilter;
|
|
2667
|
-
texture.generateMipmaps = false;
|
|
2668
|
-
texture.wrapS = texture.wrapT = ClampToEdgeWrapping;
|
|
2669
|
-
texture.needsUpdate = true;
|
|
2670
|
-
texture.userData.logicalWidth = base.width;
|
|
2671
|
-
texture.userData.logicalHeight = base.height;
|
|
2672
|
-
texture.userData.mipLevels = levels.length;
|
|
2673
|
-
return texture;
|
|
2674
|
-
}
|
|
2675
|
-
|
|
2676
|
-
// src/three/buildTexture.ts
|
|
2677
|
-
var THREE_FORMAT = {
|
|
2678
|
-
[TextureFormat.BC1]: RGBA_S3TC_DXT1_Format,
|
|
2679
|
-
[TextureFormat.BC1_SRGB]: RGBA_S3TC_DXT1_Format,
|
|
2680
|
-
[TextureFormat.BC5]: RED_GREEN_RGTC2_Format,
|
|
2681
|
-
[TextureFormat.BC7]: RGBA_BPTC_Format,
|
|
2682
|
-
[TextureFormat.BC7_SRGB]: RGBA_BPTC_Format,
|
|
2683
|
-
[TextureFormat.ASTC_4x4]: RGBA_ASTC_4x4_Format,
|
|
2684
|
-
[TextureFormat.ASTC_4x4_SRGB]: RGBA_ASTC_4x4_Format,
|
|
2685
|
-
[TextureFormat.ETC2_RGB8]: RGB_ETC2_Format,
|
|
2686
|
-
[TextureFormat.ETC2_RGB8_SRGB]: RGB_ETC2_Format
|
|
2687
|
-
};
|
|
2688
|
-
var SRGB_FORMATS = /* @__PURE__ */ new Set([
|
|
2689
|
-
TextureFormat.BC1_SRGB,
|
|
2690
|
-
TextureFormat.BC7_SRGB,
|
|
2691
|
-
TextureFormat.ASTC_4x4_SRGB,
|
|
2692
|
-
TextureFormat.ETC2_RGB8_SRGB
|
|
2693
|
-
]);
|
|
2694
|
-
function isSrgbFormat(format) {
|
|
2695
|
-
return SRGB_FORMATS.has(format);
|
|
2696
|
-
}
|
|
2697
|
-
function threeFormatFor(format) {
|
|
2698
|
-
return THREE_FORMAT[format];
|
|
2699
|
-
}
|
|
2700
|
-
function buildCompressedTexture(levels, format) {
|
|
2701
|
-
return assembleCompressedTexture(levels, threeFormatFor(format), isSrgbFormat(format));
|
|
2702
|
-
}
|
|
2703
|
-
async function encodeToTexture(encoder, source, { colorSpace = "srgb", flipY = false } = {}) {
|
|
2704
|
-
const formats = encoder.constructor.textureFormats;
|
|
2705
|
-
const wantSrgb = colorSpace === "srgb" && encoder.supportsSrgb;
|
|
2706
|
-
const format = formats.find((f) => isSrgbFormat(f) === wantSrgb) ?? formats[0];
|
|
2707
|
-
const bytes = await encoder.encodeToBytes(source, { flipY });
|
|
2708
|
-
const texture = buildCompressedTexture([bytes], format);
|
|
2709
|
-
return { ...bytes, texture };
|
|
2710
|
-
}
|
|
2711
|
-
|
|
2712
|
-
// src/three/transcodeCache.ts
|
|
2632
|
+
// src/transcodeCache.ts
|
|
2713
2633
|
var DEFAULT_LIMIT = 256 * 1024 * 1024;
|
|
2714
2634
|
var maxBytes = DEFAULT_LIMIT;
|
|
2715
2635
|
var totalBytes = 0;
|
|
@@ -2782,7 +2702,7 @@ function clearTranscodeCache() {
|
|
|
2782
2702
|
totalBytes = 0;
|
|
2783
2703
|
}
|
|
2784
2704
|
|
|
2785
|
-
// src/
|
|
2705
|
+
// src/compressTexture.ts
|
|
2786
2706
|
var sharedGpuPromise = null;
|
|
2787
2707
|
var SHARED_DEVICE_FEATURES = [
|
|
2788
2708
|
"texture-compression-bc",
|
|
@@ -2915,18 +2835,7 @@ function bitmapToMipLevel(bitmap, flipY) {
|
|
|
2915
2835
|
function mipLevelToImageData(level) {
|
|
2916
2836
|
return new ImageData(level.data, level.width, level.height);
|
|
2917
2837
|
}
|
|
2918
|
-
function
|
|
2919
|
-
const tex = new Texture(bitmap);
|
|
2920
|
-
tex.colorSpace = srgb ? SRGBColorSpace2 : LinearSRGBColorSpace2;
|
|
2921
|
-
tex.magFilter = LinearFilter2;
|
|
2922
|
-
tex.minFilter = LinearFilter2;
|
|
2923
|
-
tex.wrapS = tex.wrapT = ClampToEdgeWrapping2;
|
|
2924
|
-
tex.generateMipmaps = false;
|
|
2925
|
-
tex.flipY = flipY;
|
|
2926
|
-
tex.needsUpdate = true;
|
|
2927
|
-
return tex;
|
|
2928
|
-
}
|
|
2929
|
-
async function compressTexture(source, options = {}) {
|
|
2838
|
+
async function compressTextureToBytes(source, options = {}) {
|
|
2930
2839
|
const {
|
|
2931
2840
|
hint = "color",
|
|
2932
2841
|
preferredFormat,
|
|
@@ -2940,7 +2849,6 @@ async function compressTexture(source, options = {}) {
|
|
|
2940
2849
|
device: providedDevice,
|
|
2941
2850
|
adapter: providedAdapter
|
|
2942
2851
|
} = options;
|
|
2943
|
-
const srgb = colorSpace === "srgb";
|
|
2944
2852
|
const t0 = performance.now();
|
|
2945
2853
|
const gpu = await resolveWebGPU();
|
|
2946
2854
|
const gl = gpu ? null : resolveWebGL();
|
|
@@ -2957,9 +2865,9 @@ async function compressTexture(source, options = {}) {
|
|
|
2957
2865
|
if (transcodeKey) {
|
|
2958
2866
|
const hit = readTranscodeCache(transcodeKey);
|
|
2959
2867
|
if (hit) {
|
|
2960
|
-
const tex2 = buildCompressedTexture(hit.levels, hit.format);
|
|
2961
2868
|
return {
|
|
2962
|
-
|
|
2869
|
+
levels: hit.levels,
|
|
2870
|
+
fallbackBitmap: null,
|
|
2963
2871
|
format: hit.format,
|
|
2964
2872
|
fallbackUncompressed: false,
|
|
2965
2873
|
backend: gpu ? "webgpu" : "webgl",
|
|
@@ -2970,10 +2878,7 @@ async function compressTexture(source, options = {}) {
|
|
|
2970
2878
|
encodeMs: 0,
|
|
2971
2879
|
decodeMs: 0,
|
|
2972
2880
|
totalMs: performance.now() - t0,
|
|
2973
|
-
cacheHit: true
|
|
2974
|
-
destroy: () => {
|
|
2975
|
-
tex2.dispose();
|
|
2976
|
-
}
|
|
2881
|
+
cacheHit: true
|
|
2977
2882
|
};
|
|
2978
2883
|
}
|
|
2979
2884
|
}
|
|
@@ -2985,11 +2890,11 @@ async function compressTexture(source, options = {}) {
|
|
|
2985
2890
|
const viaWebGL = gl ? encodeViaWebGL(gl) : null;
|
|
2986
2891
|
if (viaWebGL) return viaWebGL;
|
|
2987
2892
|
console.warn(
|
|
2988
|
-
"[
|
|
2893
|
+
"[compressTextureToBytes] No compressed path available (WebGPU and WebGL2 both lack a usable compressed-texture format); returning uncompressed RGBA8."
|
|
2989
2894
|
);
|
|
2990
|
-
const tex = wrapUncompressed(bitmap, srgb, flipY);
|
|
2991
2895
|
return {
|
|
2992
|
-
|
|
2896
|
+
levels: null,
|
|
2897
|
+
fallbackBitmap: bitmap,
|
|
2993
2898
|
format: null,
|
|
2994
2899
|
fallbackUncompressed: true,
|
|
2995
2900
|
backend: "none",
|
|
@@ -3000,10 +2905,7 @@ async function compressTexture(source, options = {}) {
|
|
|
3000
2905
|
encodeMs: 0,
|
|
3001
2906
|
decodeMs,
|
|
3002
2907
|
totalMs: performance.now() - t0,
|
|
3003
|
-
cacheHit: false
|
|
3004
|
-
destroy: () => {
|
|
3005
|
-
tex.dispose();
|
|
3006
|
-
}
|
|
2908
|
+
cacheHit: false
|
|
3007
2909
|
};
|
|
3008
2910
|
async function resolveWebGPU() {
|
|
3009
2911
|
if (!("gpu" in navigator)) return null;
|
|
@@ -3056,7 +2958,6 @@ async function compressTexture(source, options = {}) {
|
|
|
3056
2958
|
} else {
|
|
3057
2959
|
bytes = await encoder.encodeToBytes(bitmap, { flipY });
|
|
3058
2960
|
}
|
|
3059
|
-
const tex3 = buildCompressedTexture([bytes], selection.format);
|
|
3060
2961
|
if (transcodeKey) {
|
|
3061
2962
|
writeTranscodeCache(transcodeKey, {
|
|
3062
2963
|
format: selection.format,
|
|
@@ -3065,8 +2966,10 @@ async function compressTexture(source, options = {}) {
|
|
|
3065
2966
|
levels: [bytes]
|
|
3066
2967
|
});
|
|
3067
2968
|
}
|
|
2969
|
+
destroyEncoder();
|
|
3068
2970
|
return {
|
|
3069
|
-
|
|
2971
|
+
levels: [bytes],
|
|
2972
|
+
fallbackBitmap: null,
|
|
3070
2973
|
format: selection.format,
|
|
3071
2974
|
fallbackUncompressed: false,
|
|
3072
2975
|
backend: "webgpu",
|
|
@@ -3077,11 +2980,7 @@ async function compressTexture(source, options = {}) {
|
|
|
3077
2980
|
encodeMs: bytes.encodeMs,
|
|
3078
2981
|
decodeMs,
|
|
3079
2982
|
totalMs: performance.now() - t0,
|
|
3080
|
-
cacheHit: false
|
|
3081
|
-
destroy: () => {
|
|
3082
|
-
tex3.dispose();
|
|
3083
|
-
destroyEncoder();
|
|
3084
|
-
}
|
|
2983
|
+
cacheHit: false
|
|
3085
2984
|
};
|
|
3086
2985
|
}
|
|
3087
2986
|
let chainResult;
|
|
@@ -3097,7 +2996,6 @@ async function compressTexture(source, options = {}) {
|
|
|
3097
2996
|
}
|
|
3098
2997
|
}
|
|
3099
2998
|
const { levels, encodeMs } = chainResult;
|
|
3100
|
-
const tex2 = buildCompressedTexture(levels, selection.format);
|
|
3101
2999
|
if (transcodeKey) {
|
|
3102
3000
|
writeTranscodeCache(transcodeKey, {
|
|
3103
3001
|
format: selection.format,
|
|
@@ -3106,8 +3004,10 @@ async function compressTexture(source, options = {}) {
|
|
|
3106
3004
|
levels
|
|
3107
3005
|
});
|
|
3108
3006
|
}
|
|
3007
|
+
destroyEncoder();
|
|
3109
3008
|
return {
|
|
3110
|
-
|
|
3009
|
+
levels,
|
|
3010
|
+
fallbackBitmap: null,
|
|
3111
3011
|
format: selection.format,
|
|
3112
3012
|
fallbackUncompressed: false,
|
|
3113
3013
|
backend: "webgpu",
|
|
@@ -3118,11 +3018,7 @@ async function compressTexture(source, options = {}) {
|
|
|
3118
3018
|
encodeMs,
|
|
3119
3019
|
decodeMs,
|
|
3120
3020
|
totalMs: performance.now() - t0,
|
|
3121
|
-
cacheHit: false
|
|
3122
|
-
destroy: () => {
|
|
3123
|
-
tex2.dispose();
|
|
3124
|
-
destroyEncoder();
|
|
3125
|
-
}
|
|
3021
|
+
cacheHit: false
|
|
3126
3022
|
};
|
|
3127
3023
|
} catch (e) {
|
|
3128
3024
|
destroyEncoder();
|
|
@@ -3142,7 +3038,6 @@ async function compressTexture(source, options = {}) {
|
|
|
3142
3038
|
try {
|
|
3143
3039
|
if (!mipmaps) {
|
|
3144
3040
|
const bytes = encoder.encodeToBytes(bitmap, { flipY });
|
|
3145
|
-
const tex3 = buildCompressedTexture([bytes], selection.format);
|
|
3146
3041
|
if (transcodeKey) {
|
|
3147
3042
|
writeTranscodeCache(transcodeKey, {
|
|
3148
3043
|
format: selection.format,
|
|
@@ -3151,8 +3046,10 @@ async function compressTexture(source, options = {}) {
|
|
|
3151
3046
|
levels: [bytes]
|
|
3152
3047
|
});
|
|
3153
3048
|
}
|
|
3049
|
+
encoder.destroy();
|
|
3154
3050
|
return {
|
|
3155
|
-
|
|
3051
|
+
levels: [bytes],
|
|
3052
|
+
fallbackBitmap: null,
|
|
3156
3053
|
format: selection.format,
|
|
3157
3054
|
fallbackUncompressed: false,
|
|
3158
3055
|
backend: "webgl",
|
|
@@ -3163,11 +3060,7 @@ async function compressTexture(source, options = {}) {
|
|
|
3163
3060
|
encodeMs: bytes.encodeMs,
|
|
3164
3061
|
decodeMs,
|
|
3165
3062
|
totalMs: performance.now() - t0,
|
|
3166
|
-
cacheHit: false
|
|
3167
|
-
destroy: () => {
|
|
3168
|
-
tex3.dispose();
|
|
3169
|
-
encoder.destroy();
|
|
3170
|
-
}
|
|
3063
|
+
cacheHit: false
|
|
3171
3064
|
};
|
|
3172
3065
|
}
|
|
3173
3066
|
const level0 = bitmapToMipLevel(bitmap, flipY);
|
|
@@ -3180,7 +3073,6 @@ async function compressTexture(source, options = {}) {
|
|
|
3180
3073
|
encodedLevels.push(bytes);
|
|
3181
3074
|
totalEncodeMs += bytes.encodeMs;
|
|
3182
3075
|
}
|
|
3183
|
-
const tex2 = buildCompressedTexture(encodedLevels, selection.format);
|
|
3184
3076
|
if (transcodeKey) {
|
|
3185
3077
|
writeTranscodeCache(transcodeKey, {
|
|
3186
3078
|
format: selection.format,
|
|
@@ -3189,8 +3081,10 @@ async function compressTexture(source, options = {}) {
|
|
|
3189
3081
|
levels: encodedLevels
|
|
3190
3082
|
});
|
|
3191
3083
|
}
|
|
3084
|
+
encoder.destroy();
|
|
3192
3085
|
return {
|
|
3193
|
-
|
|
3086
|
+
levels: encodedLevels,
|
|
3087
|
+
fallbackBitmap: null,
|
|
3194
3088
|
format: selection.format,
|
|
3195
3089
|
fallbackUncompressed: false,
|
|
3196
3090
|
backend: "webgl",
|
|
@@ -3201,20 +3095,114 @@ async function compressTexture(source, options = {}) {
|
|
|
3201
3095
|
encodeMs: totalEncodeMs,
|
|
3202
3096
|
decodeMs,
|
|
3203
3097
|
totalMs: performance.now() - t0,
|
|
3204
|
-
cacheHit: false
|
|
3205
|
-
destroy: () => {
|
|
3206
|
-
tex2.dispose();
|
|
3207
|
-
encoder.destroy();
|
|
3208
|
-
}
|
|
3098
|
+
cacheHit: false
|
|
3209
3099
|
};
|
|
3210
3100
|
} catch (e) {
|
|
3211
3101
|
encoder.destroy();
|
|
3212
|
-
console.warn("[
|
|
3102
|
+
console.warn("[compressTextureToBytes] WebGL fallback encode failed; returning uncompressed RGBA8.", e);
|
|
3213
3103
|
return null;
|
|
3214
3104
|
}
|
|
3215
3105
|
}
|
|
3216
3106
|
}
|
|
3217
3107
|
|
|
3108
|
+
// src/three/compressTexture.ts
|
|
3109
|
+
import { ClampToEdgeWrapping as ClampToEdgeWrapping2, LinearFilter as LinearFilter2, LinearSRGBColorSpace as LinearSRGBColorSpace2, SRGBColorSpace as SRGBColorSpace2, Texture } from "three";
|
|
3110
|
+
|
|
3111
|
+
// src/three/buildTexture.ts
|
|
3112
|
+
import {
|
|
3113
|
+
RED_GREEN_RGTC2_Format,
|
|
3114
|
+
RGB_ETC2_Format,
|
|
3115
|
+
RGBA_ASTC_4x4_Format,
|
|
3116
|
+
RGBA_BPTC_Format,
|
|
3117
|
+
RGBA_S3TC_DXT1_Format
|
|
3118
|
+
} from "three";
|
|
3119
|
+
|
|
3120
|
+
// src/three/textureAssembly.ts
|
|
3121
|
+
import {
|
|
3122
|
+
ClampToEdgeWrapping,
|
|
3123
|
+
CompressedTexture,
|
|
3124
|
+
LinearFilter,
|
|
3125
|
+
LinearMipmapLinearFilter,
|
|
3126
|
+
LinearSRGBColorSpace,
|
|
3127
|
+
SRGBColorSpace
|
|
3128
|
+
} from "three";
|
|
3129
|
+
function assembleCompressedTexture(levels, threeFormat, effectiveSrgb) {
|
|
3130
|
+
if (levels.length === 0) {
|
|
3131
|
+
throw new Error("assembleCompressedTexture: no levels provided");
|
|
3132
|
+
}
|
|
3133
|
+
const mipmaps = levels.map((l) => ({
|
|
3134
|
+
data: l.data,
|
|
3135
|
+
width: l.paddedWidth,
|
|
3136
|
+
height: l.paddedHeight
|
|
3137
|
+
}));
|
|
3138
|
+
const base = levels[0];
|
|
3139
|
+
const texture = new CompressedTexture(mipmaps, base.paddedWidth, base.paddedHeight, threeFormat);
|
|
3140
|
+
texture.colorSpace = effectiveSrgb ? SRGBColorSpace : LinearSRGBColorSpace;
|
|
3141
|
+
texture.magFilter = LinearFilter;
|
|
3142
|
+
texture.minFilter = levels.length > 1 ? LinearMipmapLinearFilter : LinearFilter;
|
|
3143
|
+
texture.generateMipmaps = false;
|
|
3144
|
+
texture.wrapS = texture.wrapT = ClampToEdgeWrapping;
|
|
3145
|
+
texture.needsUpdate = true;
|
|
3146
|
+
texture.userData.logicalWidth = base.width;
|
|
3147
|
+
texture.userData.logicalHeight = base.height;
|
|
3148
|
+
texture.userData.mipLevels = levels.length;
|
|
3149
|
+
return texture;
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3152
|
+
// src/three/buildTexture.ts
|
|
3153
|
+
var THREE_FORMAT = {
|
|
3154
|
+
[TextureFormat.BC1]: RGBA_S3TC_DXT1_Format,
|
|
3155
|
+
[TextureFormat.BC1_SRGB]: RGBA_S3TC_DXT1_Format,
|
|
3156
|
+
[TextureFormat.BC5]: RED_GREEN_RGTC2_Format,
|
|
3157
|
+
[TextureFormat.BC7]: RGBA_BPTC_Format,
|
|
3158
|
+
[TextureFormat.BC7_SRGB]: RGBA_BPTC_Format,
|
|
3159
|
+
[TextureFormat.ASTC_4x4]: RGBA_ASTC_4x4_Format,
|
|
3160
|
+
[TextureFormat.ASTC_4x4_SRGB]: RGBA_ASTC_4x4_Format,
|
|
3161
|
+
[TextureFormat.ETC2_RGB8]: RGB_ETC2_Format,
|
|
3162
|
+
[TextureFormat.ETC2_RGB8_SRGB]: RGB_ETC2_Format
|
|
3163
|
+
};
|
|
3164
|
+
var SRGB_FORMATS = /* @__PURE__ */ new Set([
|
|
3165
|
+
TextureFormat.BC1_SRGB,
|
|
3166
|
+
TextureFormat.BC7_SRGB,
|
|
3167
|
+
TextureFormat.ASTC_4x4_SRGB,
|
|
3168
|
+
TextureFormat.ETC2_RGB8_SRGB
|
|
3169
|
+
]);
|
|
3170
|
+
function isSrgbFormat(format) {
|
|
3171
|
+
return SRGB_FORMATS.has(format);
|
|
3172
|
+
}
|
|
3173
|
+
function threeFormatFor(format) {
|
|
3174
|
+
return THREE_FORMAT[format];
|
|
3175
|
+
}
|
|
3176
|
+
function buildCompressedTexture(levels, format) {
|
|
3177
|
+
return assembleCompressedTexture(levels, threeFormatFor(format), isSrgbFormat(format));
|
|
3178
|
+
}
|
|
3179
|
+
async function encodeToTexture(encoder, source, { colorSpace = "srgb", flipY = false } = {}) {
|
|
3180
|
+
const formats = encoder.constructor.textureFormats;
|
|
3181
|
+
const wantSrgb = colorSpace === "srgb" && encoder.supportsSrgb;
|
|
3182
|
+
const format = formats.find((f) => isSrgbFormat(f) === wantSrgb) ?? formats[0];
|
|
3183
|
+
const bytes = await encoder.encodeToBytes(source, { flipY });
|
|
3184
|
+
const texture = buildCompressedTexture([bytes], format);
|
|
3185
|
+
return { ...bytes, texture };
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
// src/three/compressTexture.ts
|
|
3189
|
+
function wrapUncompressed(bitmap, srgb, flipY) {
|
|
3190
|
+
const tex = new Texture(bitmap);
|
|
3191
|
+
tex.colorSpace = srgb ? SRGBColorSpace2 : LinearSRGBColorSpace2;
|
|
3192
|
+
tex.magFilter = LinearFilter2;
|
|
3193
|
+
tex.minFilter = LinearFilter2;
|
|
3194
|
+
tex.wrapS = tex.wrapT = ClampToEdgeWrapping2;
|
|
3195
|
+
tex.generateMipmaps = false;
|
|
3196
|
+
tex.flipY = flipY;
|
|
3197
|
+
tex.needsUpdate = true;
|
|
3198
|
+
return tex;
|
|
3199
|
+
}
|
|
3200
|
+
async function compressTexture(source, options = {}) {
|
|
3201
|
+
const { levels, fallbackBitmap, ...rest } = await compressTextureToBytes(source, options);
|
|
3202
|
+
const texture = fallbackBitmap !== null ? wrapUncompressed(fallbackBitmap, options.colorSpace !== "linear", options.flipY ?? true) : buildCompressedTexture(levels, rest.format);
|
|
3203
|
+
return { ...rest, texture, destroy: () => texture.dispose() };
|
|
3204
|
+
}
|
|
3205
|
+
|
|
3218
3206
|
// src/three/GputexLoader.ts
|
|
3219
3207
|
import { Loader } from "three";
|
|
3220
3208
|
var GputexLoader = class extends Loader {
|
|
@@ -3327,6 +3315,7 @@ export {
|
|
|
3327
3315
|
buildCompressedTexture,
|
|
3328
3316
|
clearTranscodeCache,
|
|
3329
3317
|
compressTexture,
|
|
3318
|
+
compressTextureToBytes,
|
|
3330
3319
|
createWebGLContext,
|
|
3331
3320
|
detectCapabilities,
|
|
3332
3321
|
detectWebGLCapabilities,
|