@ludicon/spark.js
Version:
Real-Time GPU Texture Codecs for the Web
220 lines (189 loc) • 8.64 kB
JavaScript
import * as THREE from "three/webgpu"
const Channel = {
R: 1, // 0001
G: 2, // 0010
B: 4, // 0100
A: 8, // 1000
RG: 3, // 0011
RGB: 7, // 0111
RGBA: 15 // 1111
}
class GLTFSparkPlugin {
constructor(name, parser, spark, options) {
this.name = name
this.parser = parser
this.loaders = {
["rgba"]: new SparkLoader(parser.fileLoader.manager, spark, options, "rgba"),
["rgba-srgb"]: new SparkLoader(parser.fileLoader.manager, spark, options, "rgba", THREE.SRGBColorSpace),
["rgb"]: new SparkLoader(parser.fileLoader.manager, spark, options, "rgb"),
["rgb-srgb"]: new SparkLoader(parser.fileLoader.manager, spark, options, "rgb", THREE.SRGBColorSpace),
["rg"]: new SparkLoader(parser.fileLoader.manager, spark, options, "rg"),
["r"]: new SparkLoader(parser.fileLoader.manager, spark, options, "r"),
[""]: new THREE.TextureLoader()
}
const textureCount = this.parser.json.textures?.length || 0
const textureColorSpaces = new Array(textureCount).fill(THREE.NoColorSpace)
const textureChannels = new Array(textureCount).fill(0)
const textureIsNormal = new Array(textureCount).fill(false)
const textureIsUncompressed = new Array(textureCount).fill(false)
function assignTexture(index, channels, colorSpace, isNormal, isUncompressed) {
if (index === undefined) return
textureChannels[index] |= channels
if (colorSpace) {
textureColorSpaces[index] = colorSpace
}
if (isNormal) {
textureIsNormal[index] = true
// Normal map unpacking not supported in three.js prior to r182
if (!("NormalRGPacking" in THREE)) {
textureChannels[index] |= Channel.RGB
}
}
if (isUncompressed) {
textureIsUncompressed[index] = true
}
}
for (const materialDef of this.parser.json.materials) {
const baseColorTextureIndex = materialDef.pbrMetallicRoughness?.baseColorTexture?.index
if (baseColorTextureIndex !== undefined) {
textureColorSpaces[baseColorTextureIndex] = THREE.SRGBColorSpace
textureChannels[baseColorTextureIndex] |= Channel.RGB
// Base color texture expects alpha when alpha mode is MASK or BLEND.
if (materialDef.alphaMode == "MASK" || materialDef.alphaMode == "BLEND") {
textureChannels[baseColorTextureIndex] |= Channel.A
}
}
assignTexture(materialDef.normalTexture?.index, Channel.RG, THREE.NoColorSpace, true)
assignTexture(materialDef.emissiveTexture?.index, Channel.RGB, THREE.SRGBColorSpace)
assignTexture(materialDef.occlusionTexture?.index, Channel.R)
assignTexture(materialDef.pbrMetallicRoughness?.metallicRoughnessTexture?.index, Channel.G | Channel.B)
// KHR_materials_anisotropy - RG contains direction, B contains strength.
const anisotropyDef = materialDef.extensions?.KHR_materials_anisotropy
if (anisotropyDef) {
assignTexture(anisotropyDef.anisotropyTexture?.index, Channel.RGB)
}
// KHR_materials_clearcoat
const clearcoatDef = materialDef.extensions?.KHR_materials_clearcoat
if (clearcoatDef) {
assignTexture(clearcoatDef.clearcoatTexture?.index, Channel.RGB, THREE.SRGBColorSpace)
assignTexture(clearcoatDef.clearcoatRoughnessTexture?.index, Channel.R)
assignTexture(clearcoatDef.clearcoatNormalTexture?.index, Channel.RG, THREE.NoColorSpace, true)
}
// KHR_materials_diffuse_transmission
const diffuseTransmissionDef = materialDef.extensions?.KHR_materials_diffuse_transmission
if (diffuseTransmissionDef) {
assignTexture(diffuseTransmissionDef.diffuseTransmissionTexture?.index, Channel.A)
assignTexture(diffuseTransmissionDef.diffuseTransmissionColorTexture?.index, Channel.RGB, THREE.SRGBColorSpace)
}
// KHR_materials_iridescence
const iridescenceDef = materialDef.extensions?.KHR_materials_iridescence
if (iridescenceDef) {
assignTexture(iridescenceDef.iridescenceTexture?.index, Channel.R)
assignTexture(iridescenceDef.iridescenceThicknessTexture?.index, Channel.G)
}
// KHR_materials_sheen
const sheenDef = materialDef.extensions?.KHR_materials_sheen
if (sheenDef) {
assignTexture(sheenDef.sheenColorTexture?.index, Channel.RGB, THREE.SRGBColorSpace)
assignTexture(sheenDef.sheenRoughnessTextureIndex?.index, Channel.A)
}
// KHR_materials_specular
const specularDef = materialDef.extensions?.KHR_materials_specular
if (specularDef) {
assignTexture(specularDef.specularTexture?.index, Channel.RGB, THREE.SRGBColorSpace)
assignTexture(specularDef.specularColorTexture?.index, Channel.A)
}
// KHR_materials_transmission
const transmissionDef = materialDef.extensions?.KHR_materials_transmission
if (transmissionDef) {
assignTexture(transmissionDef.transmissionTexture?.index, Channel.R)
}
// KHR_materials_volume
const volumeDef = materialDef.extensions?.KHR_materials_volume
if (volumeDef) {
assignTexture(volumeDef.thicknessTexture?.index, Channel.G)
}
}
this.textureColorSpaces = textureColorSpaces
this.textureChannels = textureChannels
this.textureIsNormal = textureIsNormal
this.textureIsUncompressed = textureIsUncompressed
}
loadTexture(textureIndex) {
const tex = this.parser.json.textures[textureIndex]
const imageIndex = tex.source ?? tex.extensions.EXT_texture_webp?.source ?? tex.extensions.EXT_texture_avif?.source
const colorSpace = this.textureColorSpaces[textureIndex]
const channels = this.textureChannels[textureIndex]
const isUncompressed = this.textureIsUncompressed[textureIndex]
let format = "rgba" // Default to 'rgba'
if ((channels & Channel.R) == channels) {
format = "r"
} else if ((channels & Channel.RG) == channels) {
format = "rg"
} else if ((channels & Channel.RGB) == channels) {
format = "rgb" + (colorSpace === THREE.SRGBColorSpace ? "-srgb" : "")
} else {
format = "rgba" + (colorSpace === THREE.SRGBColorSpace ? "-srgb" : "")
}
if (isUncompressed) {
format = ""
}
const loader = this.loaders[format]
return this.parser.loadTextureImage(textureIndex, imageIndex, loader)
}
}
class SparkLoader extends THREE.TextureLoader {
constructor(manager, spark, options, format, colorSpace = THREE.NoColorSpace) {
super(manager)
this.spark = spark
this.format = format
this.colorSpace = colorSpace
this.options = options
}
load(url, onLoad, onProgress, onError) {
const format = this.format
const srgb = this.colorSpace === THREE.SRGBColorSpace
const mips = true
const normal = this.format == "rg"
this.spark
.encodeTexture(url, { format, srgb, mips, normal, preferLowQuality: this.options.preferLowQuality })
.then(gpuTexture => {
const texture = new THREE.ExternalTexture(gpuTexture)
if (this.format == "rg" && "NormalRGPacking" in THREE) {
// This is not understood by stock three.js
// texture.userData.unpackNormal = THREE.NormalRGPacking
if (texture.format == "bc5-rg-unorm") texture.format = THREE.RED_GREEN_RGTC2_Format
else if (texture.format == "eac-rg11unorm") texture.format = THREE.RG11_EAC_Format
else texture.format = THREE.RGFormat
}
onLoad(texture)
})
.catch(err => {
// Fallback: load the original image uncompressed
super.load(
url,
tex => {
tex.colorSpace = this.colorSpace
onLoad?.(tex)
},
onProgress,
// If the fallback also fails, surface the original encoder error first
fallbackErr => onError?.(err ?? fallbackErr)
)
})
}
}
export function registerSparkLoader(loader, spark, options = {}) {
// Remove existing webp and avif plugins:
for (let i = 0; i < loader.pluginCallbacks.length; i++) {
const plugin = loader.pluginCallbacks[i](loader)
if (plugin.name == "EXT_texture_webp" || plugin.name == "EXT_texture_avif") {
loader.unregister(loader.pluginCallbacks[i])
i--
}
}
// Install plugin for standard textures, and textures using webp and avif extensions.
loader.register(parser => new GLTFSparkPlugin("spark", parser, spark, options))
loader.register(parser => new GLTFSparkPlugin("EXT_texture_webp", parser, spark, options))
loader.register(parser => new GLTFSparkPlugin("EXT_texture_avif", parser, spark, options))
}