@ludicon/spark.js 0.0.6 → 0.0.8
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 +35 -3
- package/dist/spark.esm.js +38 -18
- package/package.json +13 -2
- package/src/three-gltf.js +189 -0
package/README.md
CHANGED
|
@@ -62,7 +62,7 @@ To run local examples:
|
|
|
62
62
|
npm run dev
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
This will open `http://localhost:5174/examples/index.thml` where you can browse the examples.
|
|
66
66
|
|
|
67
67
|
> Note: Browsers treat http://localhost as a secure context, so HTTPS is not required when testing locally on the same machine. However, to access the dev server from another device you must enable HTTPS for WebGPU features to work.
|
|
68
68
|
>
|
|
@@ -81,8 +81,8 @@ Load an image and encode it to a compressed GPU texture.
|
|
|
81
81
|
|
|
82
82
|
#### Parameters
|
|
83
83
|
|
|
84
|
-
- **`source`** (`
|
|
85
|
-
The image to encode. Can be a
|
|
84
|
+
- **`source`** (`string | HTMLImageElement | ImageBitmap | GPUtexture`)
|
|
85
|
+
The image to encode. Can be a GPUTexture, URL, DOM image or ImageBitmap.
|
|
86
86
|
|
|
87
87
|
- **`options`** *(optional object)*
|
|
88
88
|
Configuration options for encoding:
|
|
@@ -118,6 +118,38 @@ Load an image and encode it to a compressed GPU texture.
|
|
|
118
118
|
- `Promise<GPUTexture>` — the compressed GPU texture, ready for use in WebGPU.
|
|
119
119
|
|
|
120
120
|
|
|
121
|
+
## Integration with three.js
|
|
122
|
+
|
|
123
|
+
Using spark.js with [three.js](https://threejs.org/) is straightforward. You can encode textures with Spark and expose them to three.js as external textures:
|
|
124
|
+
|
|
125
|
+
```js
|
|
126
|
+
// Load and encode texture using spark:
|
|
127
|
+
const gpuTexture = await spark.encodeTexture(textureUrl, { srgb: true, flipY: true });
|
|
128
|
+
|
|
129
|
+
// Wrap the GPUTexture for three.js
|
|
130
|
+
const externalTex = new THREE.ExternalTexture(gpuTexture);
|
|
131
|
+
|
|
132
|
+
// Then use as any other texture:
|
|
133
|
+
const material = new THREE.MeshBasicMaterial({ map: externalTex });
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
To facilitate the use of Spark when loading GLTF assets, import the provided helper:
|
|
138
|
+
|
|
139
|
+
```jsd
|
|
140
|
+
import { registerSparkLoader } from "@ludicon/spark.js/three-gltf";
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Then register Spark with an existing GLTFLoader instance:
|
|
144
|
+
|
|
145
|
+
```js
|
|
146
|
+
const loader = new GLTFLoader()
|
|
147
|
+
registerSparkLoader(loader, spark)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
After registration, the loader will automatically encode textures with Spark whenever applicable.
|
|
151
|
+
|
|
152
|
+
|
|
121
153
|
## License
|
|
122
154
|
|
|
123
155
|
*spark.js* is free for non-commercial use.
|
package/dist/spark.esm.js
CHANGED
|
@@ -412,17 +412,35 @@ function imageToByteArray(image) {
|
|
|
412
412
|
const imageData = ctx.getImageData(0, 0, image.width, image.height);
|
|
413
413
|
return new Uint8Array(imageData.data.buffer);
|
|
414
414
|
}
|
|
415
|
-
function
|
|
416
|
-
return
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
415
|
+
function isSvgUrl(url) {
|
|
416
|
+
return /\.svg(?:$|\?)/i.test(url) || /^data:image\/svg\+xml[,;]/i.test(url);
|
|
417
|
+
}
|
|
418
|
+
function loadImageElement(url) {
|
|
419
|
+
return new Promise((resolve, reject) => {
|
|
420
|
+
const img = new Image();
|
|
421
|
+
img.crossOrigin = "anonymous";
|
|
422
|
+
img.decoding = "async";
|
|
423
|
+
img.onload = () => resolve(img);
|
|
424
|
+
img.onerror = reject;
|
|
425
|
+
img.src = url;
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
async function loadImageBitmap(url, opts = {}) {
|
|
429
|
+
const res = await fetch(url, { mode: "cors" });
|
|
430
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`);
|
|
431
|
+
const blob = await res.blob();
|
|
432
|
+
return createImageBitmap(blob, {
|
|
433
|
+
imageOrientation: opts.flipY ? "flipY" : "none",
|
|
434
|
+
colorSpaceConversion: opts.colorSpaceConversion ?? "none"
|
|
424
435
|
});
|
|
425
436
|
}
|
|
437
|
+
function loadImage(url) {
|
|
438
|
+
if (isSvgUrl(url)) {
|
|
439
|
+
return loadImageElement(url);
|
|
440
|
+
} else {
|
|
441
|
+
return loadImageBitmap(url);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
426
444
|
const BYTES_PER_ROW_ALIGNMENT = 256;
|
|
427
445
|
const MIN_MIP_SIZE = 4;
|
|
428
446
|
function computeMipmapLayout(w, h, blockSize, mipmaps) {
|
|
@@ -457,6 +475,7 @@ class Spark {
|
|
|
457
475
|
#querySet;
|
|
458
476
|
#queryBuffer;
|
|
459
477
|
#queryReadbackBuffer;
|
|
478
|
+
#encodeCounter = 0;
|
|
460
479
|
/**
|
|
461
480
|
* Initialize the encoder by detecting available compression formats.
|
|
462
481
|
* @param {GPUDevice} device - WebGPU device.
|
|
@@ -550,13 +569,13 @@ class Spark {
|
|
|
550
569
|
* Try to determine the best compression options automatically. Do not use this in production, this is
|
|
551
570
|
* for the convenience of the spark.js image viewer only.
|
|
552
571
|
*
|
|
553
|
-
* @param {string | HTMLImageElement |
|
|
572
|
+
* @param {string | HTMLImageElement | ImageBitmap | GPUTexture} source - Image input.
|
|
554
573
|
* @param {Object} options - Encoding options.
|
|
555
574
|
* @returns {Object} - Recommended encoding options with an explicit encoding format.
|
|
556
575
|
*/
|
|
557
576
|
async selectPreferredOptions(source, options = {}) {
|
|
558
577
|
if (options.format == void 0 || options.format == "auto") {
|
|
559
|
-
const image = source instanceof Image || source instanceof GPUTexture ? source : await loadImage(source);
|
|
578
|
+
const image = source instanceof Image || source instanceof ImageBitmap || source instanceof GPUTexture ? source : await loadImage(source);
|
|
560
579
|
options.format = "auto";
|
|
561
580
|
const format = await this.#getBestMatchingFormat(options, image);
|
|
562
581
|
options.format = SparkFormatName[format];
|
|
@@ -572,8 +591,8 @@ class Spark {
|
|
|
572
591
|
/**
|
|
573
592
|
* Load an image and encode it to a compressed GPU texture.
|
|
574
593
|
*
|
|
575
|
-
* @param {
|
|
576
|
-
* The image to encode. Can be a GPUTexture, URL, DOM image
|
|
594
|
+
* @param {string | HTMLImageElement | ImageBitmap | GPUTexture} source
|
|
595
|
+
* The image to encode. Can be a GPUTexture, URL, DOM image or ImageBitmap.
|
|
577
596
|
*
|
|
578
597
|
* @param {Object} [options] - Optional configuration for encoding.
|
|
579
598
|
*
|
|
@@ -610,7 +629,7 @@ class Spark {
|
|
|
610
629
|
*/
|
|
611
630
|
async encodeTexture(source, options = {}) {
|
|
612
631
|
assert(this.#device, "Spark is not initialized");
|
|
613
|
-
const image = source instanceof Image || source instanceof GPUTexture ? source : await loadImage(source);
|
|
632
|
+
const image = source instanceof Image || source instanceof ImageBitmap || source instanceof GPUTexture ? source : await loadImage(source);
|
|
614
633
|
console.log("Loaded image", image);
|
|
615
634
|
const format = await this.#getBestMatchingFormat(options, image);
|
|
616
635
|
const pipelinePromise = this.#loadPipeline(format);
|
|
@@ -622,7 +641,8 @@ class Spark {
|
|
|
622
641
|
const srgb = (options.srgb || options.format?.endsWith("srgb")) && SparkFormatIsRGB[format];
|
|
623
642
|
const webgpuFormat = SparkWebGPUFormats[format] + (srgb ? "-srgb" : "");
|
|
624
643
|
const viewFormats = srgb ? ["rgba8unorm", "rgba8unorm-srgb"] : ["rgba8unorm"];
|
|
625
|
-
|
|
644
|
+
const counter = this.#encodeCounter++;
|
|
645
|
+
console.time("create input texture #" + counter);
|
|
626
646
|
let inputUsage = GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.STORAGE_BINDING;
|
|
627
647
|
const needsProcessing = options.flipY || width != image.width || height != image.height;
|
|
628
648
|
if (!needsProcessing && !(image instanceof GPUTexture)) {
|
|
@@ -678,7 +698,7 @@ class Spark {
|
|
|
678
698
|
this.#generateMipmaps(commandEncoder, inputTexture, mipmapCount, width, height, srgb);
|
|
679
699
|
}
|
|
680
700
|
commandEncoder.popDebugGroup?.();
|
|
681
|
-
console.timeEnd("create input texture");
|
|
701
|
+
console.timeEnd("create input texture #" + counter);
|
|
682
702
|
const outputTexture = this.#device.createTexture({
|
|
683
703
|
size: [width, height, 1],
|
|
684
704
|
mipLevelCount: mipmapCount,
|
|
@@ -689,7 +709,7 @@ class Spark {
|
|
|
689
709
|
size: outputSize,
|
|
690
710
|
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
|
|
691
711
|
});
|
|
692
|
-
console.time("dispatch compute shader");
|
|
712
|
+
console.time("dispatch compute shader #" + counter);
|
|
693
713
|
commandEncoder.pushDebugGroup?.("spark encode texture");
|
|
694
714
|
let args = {};
|
|
695
715
|
if (this.#querySet && typeof commandEncoder.writeTimestamp !== "function") {
|
|
@@ -757,7 +777,7 @@ class Spark {
|
|
|
757
777
|
}
|
|
758
778
|
commandEncoder.popDebugGroup?.();
|
|
759
779
|
this.#device.queue.submit([commandEncoder.finish()]);
|
|
760
|
-
console.timeEnd("dispatch compute shader");
|
|
780
|
+
console.timeEnd("dispatch compute shader #" + counter);
|
|
761
781
|
tmpTexture?.destroy();
|
|
762
782
|
if (inputTexture != image) {
|
|
763
783
|
inputTexture?.destroy();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ludicon/spark.js",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "Real-Time GPU Texture Codecs for the Web",
|
|
5
5
|
"main": "dist/spark.esm.js",
|
|
6
6
|
"module": "dist/spark.esm.js",
|
|
@@ -8,10 +8,12 @@
|
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
10
|
"import": "./dist/spark.esm.js"
|
|
11
|
-
}
|
|
11
|
+
},
|
|
12
|
+
"./three-gltf": "./src/three-gltf.js"
|
|
12
13
|
},
|
|
13
14
|
"files": [
|
|
14
15
|
"dist",
|
|
16
|
+
"src/three-gltf.js",
|
|
15
17
|
"README.md",
|
|
16
18
|
"LICENSE"
|
|
17
19
|
],
|
|
@@ -61,8 +63,17 @@
|
|
|
61
63
|
"npm-run-all": "^4.1.5",
|
|
62
64
|
"prettier": "^3.0.0",
|
|
63
65
|
"rimraf": "^5.0.0",
|
|
66
|
+
"three": "^0.180.0",
|
|
64
67
|
"vite": "^7.0.0"
|
|
65
68
|
},
|
|
69
|
+
"peerDependencies": {
|
|
70
|
+
"three": "^0.180.0"
|
|
71
|
+
},
|
|
72
|
+
"peerDependenciesMeta": {
|
|
73
|
+
"three": {
|
|
74
|
+
"optional": true
|
|
75
|
+
}
|
|
76
|
+
},
|
|
66
77
|
"publishConfig": {
|
|
67
78
|
"access": "public"
|
|
68
79
|
},
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import * as THREE from "three/webgpu"
|
|
2
|
+
|
|
3
|
+
const Channel = {
|
|
4
|
+
R: 1, // 0001
|
|
5
|
+
G: 2, // 0010
|
|
6
|
+
B: 4, // 0100
|
|
7
|
+
A: 8, // 1000
|
|
8
|
+
RG: 3, // 0011
|
|
9
|
+
RGB: 7, // 0111
|
|
10
|
+
RGBA: 15 // 1111
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
class GLTFSparkPlugin {
|
|
14
|
+
constructor(name, parser, spark) {
|
|
15
|
+
this.name = name
|
|
16
|
+
this.parser = parser
|
|
17
|
+
|
|
18
|
+
this.loaders = {
|
|
19
|
+
["rgba"]: new SparkLoader(parser.fileLoader.manager, spark, "rgba"),
|
|
20
|
+
["rgba-srgb"]: new SparkLoader(parser.fileLoader.manager, spark, "rgba", THREE.SRGBColorSpace),
|
|
21
|
+
["rgb"]: new SparkLoader(parser.fileLoader.manager, spark, "rgb"),
|
|
22
|
+
["rgb-srgb"]: new SparkLoader(parser.fileLoader.manager, spark, "rgb", THREE.SRGBColorSpace),
|
|
23
|
+
["rg"]: new SparkLoader(parser.fileLoader.manager, spark, "rg"),
|
|
24
|
+
["r"]: new SparkLoader(parser.fileLoader.manager, spark, "r"),
|
|
25
|
+
[""]: new THREE.TextureLoader()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const textureCount = this.parser.json.textures?.length || 0
|
|
29
|
+
const textureColorSpaces = new Array(textureCount).fill(THREE.NoColorSpace)
|
|
30
|
+
const textureChannels = new Array(textureCount).fill(0)
|
|
31
|
+
const textureIsNormal = new Array(textureCount).fill(false)
|
|
32
|
+
const textureIsUncompressed = new Array(textureCount).fill(false)
|
|
33
|
+
|
|
34
|
+
function assignTexture(index, channels, colorSpace, isNormal, isUncompressed) {
|
|
35
|
+
if (index === undefined) return
|
|
36
|
+
|
|
37
|
+
textureChannels[index] |= channels
|
|
38
|
+
|
|
39
|
+
if (colorSpace) {
|
|
40
|
+
textureColorSpaces[index] = colorSpace
|
|
41
|
+
}
|
|
42
|
+
if (isNormal) {
|
|
43
|
+
textureIsNormal[index] = true
|
|
44
|
+
|
|
45
|
+
// Normal map unpacking not supported in three.js prior to r181
|
|
46
|
+
if (!("NormalRGPacking" in THREE)) {
|
|
47
|
+
textureChannels[index] |= Channel.RGB
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (isUncompressed) {
|
|
51
|
+
textureIsUncompressed[index] = true
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const materialDef of this.parser.json.materials) {
|
|
56
|
+
const baseColorTextureIndex = materialDef.pbrMetallicRoughness?.baseColorTexture?.index
|
|
57
|
+
if (baseColorTextureIndex !== undefined) {
|
|
58
|
+
textureColorSpaces[baseColorTextureIndex] = THREE.SRGBColorSpace
|
|
59
|
+
textureChannels[baseColorTextureIndex] |= Channel.RGB
|
|
60
|
+
|
|
61
|
+
// Base color texture expects alpha when alpha mode is MASK or BLEND.
|
|
62
|
+
if (materialDef.alphaMode == "MASK" || materialDef.alphaMode == "BLEND") {
|
|
63
|
+
textureChannels[baseColorTextureIndex] |= Channel.A
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
assignTexture(materialDef.normalTexture?.index, Channel.RG, THREE.NoColorSpace, true)
|
|
68
|
+
assignTexture(materialDef.emissiveTexture?.index, Channel.RGB, THREE.SRGBColorSpace)
|
|
69
|
+
assignTexture(materialDef.occlusionTexture?.index, Channel.R)
|
|
70
|
+
assignTexture(materialDef.pbrMetallicRoughness?.metallicRoughnessTexture?.index, Channel.G | Channel.B)
|
|
71
|
+
|
|
72
|
+
// KHR_materials_anisotropy - RG contains direction, B contains strength.
|
|
73
|
+
assignTexture(materialDef.anisotropyTexture?.index, Channel.RGB)
|
|
74
|
+
|
|
75
|
+
// KHR_materials_clearcoat
|
|
76
|
+
assignTexture(materialDef.clearcoatTexture?.index, Channel.RGB, THREE.SRGBColorSpace)
|
|
77
|
+
assignTexture(materialDef.clearcoatRoughnessTexture?.index, Channel.R)
|
|
78
|
+
assignTexture(materialDef.clearcoatNormalTexture?.index, Channel.RG, THREE.NoColorSpace, true)
|
|
79
|
+
|
|
80
|
+
// KHR_materials_diffuse_transmission
|
|
81
|
+
assignTexture(materialDef.diffuseTransmissionTexture?.index, Channel.A)
|
|
82
|
+
assignTexture(materialDef.diffuseTransmissionColorTexture?.index, Channel.RGB, THREE.SRGBColorSpace)
|
|
83
|
+
|
|
84
|
+
// KHR_materials_iridescence
|
|
85
|
+
assignTexture(materialDef.iridescenceTexture?.index, Channel.R)
|
|
86
|
+
assignTexture(materialDef.iridescenceThicknessTexture?.index, Channel.G)
|
|
87
|
+
|
|
88
|
+
// KHR_materials_sheen
|
|
89
|
+
assignTexture(materialDef.sheenColorTexture?.index, Channel.RGB, THREE.SRGBColorSpace)
|
|
90
|
+
assignTexture(materialDef.sheenRoughnessTextureIndex?.index, Channel.A)
|
|
91
|
+
|
|
92
|
+
// KHR_materials_specular
|
|
93
|
+
assignTexture(materialDef.specularTexture?.index, Channel.RGB, THREE.SRGBColorSpace)
|
|
94
|
+
assignTexture(materialDef.specularColorTexture?.index, Channel.A)
|
|
95
|
+
|
|
96
|
+
// KHR_materials_transmission
|
|
97
|
+
assignTexture(materialDef.transmissionTexture?.index, Channel.R)
|
|
98
|
+
|
|
99
|
+
// KHR_materials_volume
|
|
100
|
+
assignTexture(materialDef.thicknessTexture?.index, Channel.G)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
this.textureColorSpaces = textureColorSpaces
|
|
104
|
+
this.textureChannels = textureChannels
|
|
105
|
+
this.textureIsNormal = textureIsNormal
|
|
106
|
+
this.textureIsUncompressed = textureIsUncompressed
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
loadTexture(textureIndex) {
|
|
110
|
+
const tex = this.parser.json.textures[textureIndex]
|
|
111
|
+
const imageIndex = tex.source ?? tex.extensions.EXT_texture_webp?.source ?? tex.extensions.EXT_texture_avif?.source
|
|
112
|
+
const colorSpace = this.textureColorSpaces[textureIndex]
|
|
113
|
+
const channels = this.textureChannels[textureIndex]
|
|
114
|
+
const isUncompressed = this.textureIsUncompressed[textureIndex]
|
|
115
|
+
|
|
116
|
+
let format = "rgba" // Default to 'rgba'
|
|
117
|
+
if ((channels & Channel.R) == channels) {
|
|
118
|
+
format = "r"
|
|
119
|
+
} else if ((channels & Channel.RG) == channels) {
|
|
120
|
+
format = "rg"
|
|
121
|
+
} else if ((channels & Channel.RGB) == channels) {
|
|
122
|
+
format = "rgb" + (colorSpace === THREE.SRGBColorSpace ? "-srgb" : "")
|
|
123
|
+
} else {
|
|
124
|
+
format = "rgba" + (colorSpace === THREE.SRGBColorSpace ? "-srgb" : "")
|
|
125
|
+
}
|
|
126
|
+
if (isUncompressed) {
|
|
127
|
+
format = ""
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const loader = this.loaders[format]
|
|
131
|
+
|
|
132
|
+
return this.parser.loadTextureImage(textureIndex, imageIndex, loader)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
class SparkLoader extends THREE.TextureLoader {
|
|
137
|
+
constructor(manager, spark, format, colorSpace = THREE.NoColorSpace) {
|
|
138
|
+
super(manager)
|
|
139
|
+
this.spark = spark
|
|
140
|
+
this.format = format
|
|
141
|
+
this.colorSpace = colorSpace
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
load(url, onLoad, onProgress, onError) {
|
|
145
|
+
const format = this.format
|
|
146
|
+
const srgb = this.colorSpace === THREE.SRGBColorSpace
|
|
147
|
+
const mips = true
|
|
148
|
+
|
|
149
|
+
this.spark
|
|
150
|
+
.encodeTexture(url, { format, srgb, mips })
|
|
151
|
+
.then(gpuTexture => {
|
|
152
|
+
const texture = new THREE.ExternalTexture(gpuTexture)
|
|
153
|
+
if (this.format == "rg" && "NormalRGPacking" in THREE) {
|
|
154
|
+
texture.userData.unpackNormal = THREE.NormalRGPacking
|
|
155
|
+
}
|
|
156
|
+
onLoad(texture)
|
|
157
|
+
})
|
|
158
|
+
.catch(err => {
|
|
159
|
+
// Fallback: load the original image uncompressed
|
|
160
|
+
super.load(
|
|
161
|
+
url,
|
|
162
|
+
tex => {
|
|
163
|
+
tex.colorSpace = this.colorSpace
|
|
164
|
+
onLoad?.(tex)
|
|
165
|
+
},
|
|
166
|
+
onProgress,
|
|
167
|
+
// If the fallback also fails, surface the original encoder error first
|
|
168
|
+
fallbackErr => onError?.(err ?? fallbackErr)
|
|
169
|
+
)
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function registerSparkLoader(loader, spark) {
|
|
175
|
+
// Remove existing webp and avif plugins:
|
|
176
|
+
for (let i = 0; i < loader.pluginCallbacks.length; i++) {
|
|
177
|
+
const plugin = loader.pluginCallbacks[i](loader)
|
|
178
|
+
|
|
179
|
+
if (plugin.name == "EXT_texture_webp" || plugin.name == "EXT_texture_avif") {
|
|
180
|
+
loader.unregister(loader.pluginCallbacks[i])
|
|
181
|
+
i--
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Install plugin for standard textures, and textures using webp and avif extensions.
|
|
186
|
+
loader.register(parser => new GLTFSparkPlugin("spark", parser, spark))
|
|
187
|
+
loader.register(parser => new GLTFSparkPlugin("EXT_texture_webp", parser, spark))
|
|
188
|
+
loader.register(parser => new GLTFSparkPlugin("EXT_texture_avif", parser, spark))
|
|
189
|
+
}
|