@ludicon/spark.js 0.0.7 → 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.
Files changed (2) hide show
  1. package/package.json +2 -1
  2. package/src/three-gltf.js +189 -0
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.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",
@@ -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
  ],
@@ -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
+ }