I added a quick and hacky compressed texture approach that only supports PVR ASTC as of now locally as a test
CompressedGLTexture class
import { Texture, Disposable, Restorable, TextureFilter, TextureWrap } from "@esotericsoftware/spine-core";
import { ManagedWebGLRenderingContext } from "@esotericsoftware/spine-webgl";
import * as Phaser from "phaser";
export class CompressedGLTexture extends Texture implements Disposable, Restorable {
context: ManagedWebGLRenderingContext;
private texture: WebGLTexture | null = null;
private boundUnit = 0;
private useMipMaps = false;
private sourceData: Phaser.Types.Textures.CompressedTextureData | null = null;
public static DISABLE_UNPACK_PREMULTIPLIED_ALPHA_WEBGL = false;
private width: number = 0;
private height: number = 0;
constructor(context: ManagedWebGLRenderingContext | WebGLRenderingContext,
sourceData: Phaser.Types.Textures.CompressedTextureData,
useMipMaps: boolean = false,
) {
const image = {};
super(image);
this.context = context instanceof ManagedWebGLRenderingContext ? context : new ManagedWebGLRenderingContext(context);
this.useMipMaps = useMipMaps;
this.sourceData = sourceData;
if (sourceData ) {
this.width = sourceData.width;
this.height = sourceData.height;
this.setSize(this.width, this.height);
}
this.restore();
this.context.addRestorable(this);
}
private setSize(width: number, height: number) {
this.width = width;
this.height = height;
(this.getImage() as any).width = width;
(this.getImage() as any).height = height;
}
setFilters(minFilter: TextureFilter, magFilter: TextureFilter) {
let gl = this.context.gl;
this.bind();
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, minFilter);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, CompressedGLTexture.validateMagFilter(magFilter));
this.useMipMaps = CompressedGLTexture.usesMipMaps(minFilter);
if (this.useMipMaps && this.sourceData && this.sourceData.mipmaps && this.sourceData.mipmaps.length > 1) {
// Only generate mipmaps if we don't already have them in the sourceData
if (!this.sourceData.mipmaps || this.sourceData.mipmaps.length <= 1) {
gl.generateMipmap(gl.TEXTURE_2D);
}
}
}
static validateMagFilter(magFilter: TextureFilter) {
switch (magFilter) {
case TextureFilter.MipMapLinearLinear:
case TextureFilter.MipMapLinearNearest:
case TextureFilter.MipMapNearestLinear:
case TextureFilter.MipMapNearestNearest:
return TextureFilter.Linear;
default:
return magFilter;
}
}
static usesMipMaps(filter: TextureFilter) {
switch (filter) {
case TextureFilter.MipMapLinearLinear:
case TextureFilter.MipMapLinearNearest:
case TextureFilter.MipMapNearestLinear:
case TextureFilter.MipMapNearestNearest:
return true;
default:
return false;
}
}
setWraps(uWrap: TextureWrap, vWrap: TextureWrap) {
let gl = this.context.gl;
this.bind();
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, uWrap);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, vWrap);
}
update(useMipMaps: boolean) {
let gl = this.context.gl;
this.bind();
const minFilter = useMipMaps ? gl.LINEAR_MIPMAP_LINEAR : gl.LINEAR;
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, minFilter);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
if (useMipMaps && (!this.sourceData || !this.sourceData.mipmaps || this.sourceData.mipmaps.length <= 1)) {
gl.generateMipmap(gl.TEXTURE_2D);
}
this.useMipMaps = useMipMaps;
}
restore() {
if (!this.texture && this.sourceData) {
this.reloadFromCompressedData();
return;
}
if (this.texture) {
let gl = this.context.gl;
this.bind();
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, this.useMipMaps ? gl.LINEAR_MIPMAP_LINEAR : gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
if (this.useMipMaps && (!this.sourceData || !this.sourceData.mipmaps || this.sourceData.mipmaps.length <= 1)) {
gl.generateMipmap(gl.TEXTURE_2D);
}
}
}
private reloadFromCompressedData() {
if (!this.sourceData) {
console.error("No source data available to reload texture");
return;
}
const gl = this.context.gl;
if (!this.texture) {
this.texture = gl.createTexture();
}
this.bind();
if (this.sourceData.internalFormat) {
const format = this.sourceData.internalFormat;
const mipmaps = this.sourceData.mipmaps || [];
if (mipmaps.length > 0) {
for (let i = 0; i < mipmaps.length; i++) {
const mipmap = mipmaps[i];
console.dir(mipmap)
gl.compressedTexImage2D(
gl.TEXTURE_2D,
i,
format,
mipmap.width,
mipmap.height,
0,
mipmap.data
);
}
this.useMipMaps = mipmaps.length > 1;
} else {
console.error("No mipmap data available in sourceData");
}
} else {
console.error("No format specified in compressed texture data");
}
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, this.useMipMaps ? gl.LINEAR_MIPMAP_LINEAR : gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
}
bind(unit: number = 0) {
let gl = this.context.gl;
this.boundUnit = unit;
gl.activeTexture(gl.TEXTURE0 + unit);
if (!this.texture) {
if (this.sourceData) {
this.reloadFromCompressedData();
} else {
console.error("No valid texture to bind");
return;
}
}
gl.bindTexture(gl.TEXTURE_2D, this.texture);
}
unbind() {
let gl = this.context.gl;
gl.activeTexture(gl.TEXTURE0 + this.boundUnit);
gl.bindTexture(gl.TEXTURE_2D, null);
}
dispose() {
this.context.removeRestorable(this);
this.texture = null;
this.sourceData = null;
}
}
updated getAtlas function
for (let atlasPage of atlas.pages) {
const texture = this.game.textures.get(atlasKey + "!" + atlasPage.name);
if(texture.getSourceImage() ) {
atlasPage.setTexture(new GLTexture(gl, this.game.textures.get(atlasKey + "!" + atlasPage.name).getSourceImage() as HTMLImageElement | ImageBitmap, false));
} else {
atlasPage.setTexture(new CompressedGLTexture(gl, texture.source[0].source as Phaser.Types.Textures.CompressedTextureData, false));
}
}
onFileComplete and addToCache to handle compressed texture loading for .pvr
onFileComplete (file: Phaser.Loader.File) {
if (this.files.indexOf(file) != -1) {
this.pending--;
if (file.type == "text") {
var lines = file.data.split(/\r\n|\r|\n/);
let textures = [];
textures.push(lines[0]);
for (var t = 1; t < lines.length; t++) {
var line = lines[t];
if (line.trim() === '' && t < lines.length - 1) {
line = lines[t + 1];
textures.push(line);
}
}
let basePath = file.src.match(/^.*\//) ?? "";
for (var i = 0; i < textures.length; i++) {
var url:string = basePath + textures[i];
var key = file.key + "!" + textures[i];
if(url.endsWith(".pvr")) {
const compressedTextureEntry:Phaser.Types.Loader.FileTypes.CompressedTextureFileEntry = {
textureURL: url,
format: "ASTC",
type: "PVR"
}
const texture = new Phaser.Loader.FileTypes.CompressedTextureFile(
this.loader,key,compressedTextureEntry
)
for(const file of texture.files) {
if (!this.loader.keyExists(file)) {
file.config = compressedTextureEntry
this.addToMultiFile(file);
this.loader.addFile(file);
}
}
} else {
var image = new Phaser.Loader.FileTypes.ImageFile(this.loader, key, url);
if (!this.loader.keyExists(image)) {
this.addToMultiFile(image);
this.loader.addFile(image);
}
}
}
}
}
}
addToCache () {
if (this.isReadyToProcess()) {
let textureManager = this.loader.textureManager;
for (let file of this.files) {
if (file.type == "image") {
if (!textureManager.exists(file.key)) {
textureManager.addImage(file.key, file.data);
}
} else if(file.type ==="binary") {
if(!textureManager.exists(file.key)) {
const entry = file.config as Phaser.Types.Loader.FileTypes.CompressedTextureFileEntry;
if(entry.format !== "ASTC") {
console.log("Compressed texture format not supported", entry.format);
return;
}
const textureData:Phaser.Types.Textures.CompressedTextureData = Phaser.Textures.Parsers.PVRParser(file.data);
textureManager.addCompressedTexture(file.key, textureData);
}
}else {
this.premultipliedAlpha = this.premultipliedAlpha ?? (file.data.indexOf("pma: true") >= 0 || file.data.indexOf("pma:true") >= 0);
file.data = {
data: file.data,
premultipliedAlpha: this.premultipliedAlpha,
};
file.addToCache();
}
}
}
}