@ludicon/spark.js 0.0.7 → 0.0.9

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
@@ -92,17 +92,17 @@ Load an image and encode it to a compressed GPU texture.
92
92
 
93
93
  - A channel mask indicating the number of channels in your input: `"rgba"`, `"rgb"`, `"rg"` or `"r"`, the actual format is selected based on the device capabilities.
94
94
 
95
- - An explicit WebGPU BC, ETC or ASTC format name, or an abbreviated form such as `"bc7"` or `"astc"`. Note, spark.js only supports 4x4 and LDR formats. By default
95
+ - An explicit WebGPU BC, ETC or ASTC format name, or an abbreviated form such as `"bc7"` or `"astc"`. Note: only 4x4 LDR formats are supported.
96
96
 
97
97
  - If you specify `auto`, the input texture is analyzed to detect the necessary number of channels. This has some overhead, it's always recommended to specify the format through one of the other methods.
98
98
 
99
99
  Default: `rgb`.
100
100
 
101
101
  - **`alpha`**
102
- Hint for the format selector. When an explicit channel mask is not provided, the channel mask is assumed to be `"rgb"`, providing
102
+ Hint for the automatic format selector. When no explicit format is provided, the format is assumed to be `"rgb"`. Supplying `alpha: true` will default to "rgba" instead.
103
103
 
104
104
  - **`mips`** or **`generateMipmaps`** (`boolean`)
105
- Whether to generate mipmaps. Currently mipmap generation uses a basic box filter in linear space. Default: `false`.
105
+ Whether to generate mipmaps. Mipmaps are generated with a basic box filter in linear space. Default: `false`.
106
106
 
107
107
  - **`srgb`** (`boolean`)
108
108
  Whether to encode the image using an as sRGB format. This also affects mipmap generation. The `srgb` mode can also be inferred from the `format`. Default: `false`.
@@ -115,7 +115,8 @@ Load an image and encode it to a compressed GPU texture.
115
115
 
116
116
  #### Returns
117
117
 
118
- - `Promise<GPUTexture>` — the compressed GPU texture, ready for use in WebGPU.
118
+ - `Promise<GPUTexture>`
119
+ A promise resolving to the encoded WebGPU texture.
119
120
 
120
121
 
121
122
  ## Integration with three.js
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ludicon/spark.js",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
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",
@@ -13,6 +13,7 @@
13
13
  },
14
14
  "files": [
15
15
  "dist",
16
+ "src/three-gltf.js",
16
17
  "README.md",
17
18
  "LICENSE"
18
19
  ],
@@ -66,7 +67,7 @@
66
67
  "vite": "^7.0.0"
67
68
  },
68
69
  "peerDependencies": {
69
- "three": "^0.180.0"
70
+ "three": ">=0.180.0"
70
71
  },
71
72
  "peerDependenciesMeta": {
72
73
  "three": {
@@ -0,0 +1,213 @@
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
+ const anisotropyDef = materialDef.extensions?.KHR_materials_anisotropy
74
+ if (anisotropyDef) {
75
+ assignTexture(anisotropyDef.anisotropyTexture?.index, Channel.RGB)
76
+ }
77
+
78
+ // KHR_materials_clearcoat
79
+ const clearcoatDef = materialDef.extensions?.KHR_materials_clearcoat
80
+ if (clearcoatDef) {
81
+ assignTexture(clearcoatDef.clearcoatTexture?.index, Channel.RGB, THREE.SRGBColorSpace)
82
+ assignTexture(clearcoatDef.clearcoatRoughnessTexture?.index, Channel.R)
83
+ assignTexture(clearcoatDef.clearcoatNormalTexture?.index, Channel.RG, THREE.NoColorSpace, true)
84
+ }
85
+
86
+ // KHR_materials_diffuse_transmission
87
+ const diffuseTransmissionDef = materialDef.extensions?.KHR_materials_diffuse_transmission
88
+ if (diffuseTransmissionDef) {
89
+ assignTexture(diffuseTransmissionDef.diffuseTransmissionTexture?.index, Channel.A)
90
+ assignTexture(diffuseTransmissionDef.diffuseTransmissionColorTexture?.index, Channel.RGB, THREE.SRGBColorSpace)
91
+ }
92
+
93
+ // KHR_materials_iridescence
94
+ const iridescenceDef = materialDef.extensions?.KHR_materials_iridescence
95
+ if (iridescenceDef) {
96
+ assignTexture(iridescenceDef.iridescenceTexture?.index, Channel.R)
97
+ assignTexture(iridescenceDef.iridescenceThicknessTexture?.index, Channel.G)
98
+ }
99
+
100
+ // KHR_materials_sheen
101
+ const sheenDef = materialDef.extensions?.KHR_materials_sheen
102
+ if (sheenDef) {
103
+ assignTexture(sheenDef.sheenColorTexture?.index, Channel.RGB, THREE.SRGBColorSpace)
104
+ assignTexture(sheenDef.sheenRoughnessTextureIndex?.index, Channel.A)
105
+ }
106
+
107
+ // KHR_materials_specular
108
+ const specularDef = materialDef.extensions?.KHR_materials_specular
109
+ if (specularDef) {
110
+ assignTexture(specularDef.specularTexture?.index, Channel.RGB, THREE.SRGBColorSpace)
111
+ assignTexture(specularDef.specularColorTexture?.index, Channel.A)
112
+ }
113
+
114
+ // KHR_materials_transmission
115
+ const transmissionDef = materialDef.extensions?.KHR_materials_transmission
116
+ if (transmissionDef) {
117
+ assignTexture(transmissionDef.transmissionTexture?.index, Channel.R)
118
+ }
119
+
120
+ // KHR_materials_volume
121
+ const volumeDef = materialDef.extensions?.KHR_materials_volume
122
+ if (volumeDef) {
123
+ assignTexture(volumeDef.thicknessTexture?.index, Channel.G)
124
+ }
125
+ }
126
+
127
+ this.textureColorSpaces = textureColorSpaces
128
+ this.textureChannels = textureChannels
129
+ this.textureIsNormal = textureIsNormal
130
+ this.textureIsUncompressed = textureIsUncompressed
131
+ }
132
+
133
+ loadTexture(textureIndex) {
134
+ const tex = this.parser.json.textures[textureIndex]
135
+ const imageIndex = tex.source ?? tex.extensions.EXT_texture_webp?.source ?? tex.extensions.EXT_texture_avif?.source
136
+ const colorSpace = this.textureColorSpaces[textureIndex]
137
+ const channels = this.textureChannels[textureIndex]
138
+ const isUncompressed = this.textureIsUncompressed[textureIndex]
139
+
140
+ let format = "rgba" // Default to 'rgba'
141
+ if ((channels & Channel.R) == channels) {
142
+ format = "r"
143
+ } else if ((channels & Channel.RG) == channels) {
144
+ format = "rg"
145
+ } else if ((channels & Channel.RGB) == channels) {
146
+ format = "rgb" + (colorSpace === THREE.SRGBColorSpace ? "-srgb" : "")
147
+ } else {
148
+ format = "rgba" + (colorSpace === THREE.SRGBColorSpace ? "-srgb" : "")
149
+ }
150
+ if (isUncompressed) {
151
+ format = ""
152
+ }
153
+
154
+ const loader = this.loaders[format]
155
+
156
+ return this.parser.loadTextureImage(textureIndex, imageIndex, loader)
157
+ }
158
+ }
159
+
160
+ class SparkLoader extends THREE.TextureLoader {
161
+ constructor(manager, spark, format, colorSpace = THREE.NoColorSpace) {
162
+ super(manager)
163
+ this.spark = spark
164
+ this.format = format
165
+ this.colorSpace = colorSpace
166
+ }
167
+
168
+ load(url, onLoad, onProgress, onError) {
169
+ const format = this.format
170
+ const srgb = this.colorSpace === THREE.SRGBColorSpace
171
+ const mips = true
172
+
173
+ this.spark
174
+ .encodeTexture(url, { format, srgb, mips })
175
+ .then(gpuTexture => {
176
+ const texture = new THREE.ExternalTexture(gpuTexture)
177
+ if (this.format == "rg" && "NormalRGPacking" in THREE) {
178
+ texture.userData.unpackNormal = THREE.NormalRGPacking
179
+ }
180
+ onLoad(texture)
181
+ })
182
+ .catch(err => {
183
+ // Fallback: load the original image uncompressed
184
+ super.load(
185
+ url,
186
+ tex => {
187
+ tex.colorSpace = this.colorSpace
188
+ onLoad?.(tex)
189
+ },
190
+ onProgress,
191
+ // If the fallback also fails, surface the original encoder error first
192
+ fallbackErr => onError?.(err ?? fallbackErr)
193
+ )
194
+ })
195
+ }
196
+ }
197
+
198
+ export function registerSparkLoader(loader, spark) {
199
+ // Remove existing webp and avif plugins:
200
+ for (let i = 0; i < loader.pluginCallbacks.length; i++) {
201
+ const plugin = loader.pluginCallbacks[i](loader)
202
+
203
+ if (plugin.name == "EXT_texture_webp" || plugin.name == "EXT_texture_avif") {
204
+ loader.unregister(loader.pluginCallbacks[i])
205
+ i--
206
+ }
207
+ }
208
+
209
+ // Install plugin for standard textures, and textures using webp and avif extensions.
210
+ loader.register(parser => new GLTFSparkPlugin("spark", parser, spark))
211
+ loader.register(parser => new GLTFSparkPlugin("EXT_texture_webp", parser, spark))
212
+ loader.register(parser => new GLTFSparkPlugin("EXT_texture_avif", parser, spark))
213
+ }