[+] Chuni Userbox with Assets

Co-authored-by: split / May  <split@split.pet>
This commit is contained in:
Raymond
2025-01-01 06:16:16 -05:00
parent 8fb443d41d
commit 8aa829ab02
8 changed files with 1070 additions and 33 deletions

View File

@@ -0,0 +1,314 @@
/*
A simplified DDS parser with Chusan userbox in mind.
There are some issues on Safari. I don't really care, to be honest.
Authored by Raymond and May.
DDS header parsing based off of https://gist.github.com/brett19/13c83c2e5e38933757c2
*/
function makeFourCC(string: string) {
return string.charCodeAt(0) +
(string.charCodeAt(1) << 8) +
(string.charCodeAt(2) << 16) +
(string.charCodeAt(3) << 24);
};
/**
* @description Magic bytes for the DDS file format (see https://en.wikipedia.org/wiki/Magic_number_(programming))
*/
const DDS_MAGIC_BYTES = 0x20534444;
/*
to get around the fact that TS's builtin Object.fromEntries() typing
doesn't persist strict types and instead only uses broad types
without creating a new function to get around it...
sorry, this is a really ugly solution, but it's not my problem
*/
/**
* @description List of compression type markers used in DDS
*/
const DDS_COMPRESSION_TYPE_MARKERS = ["DXT1", "DXT3", "DXT5"] as const;
/**
* @description Object mapping string versions of DDS compression type markers to their value in uint32s
*/
const DDS_COMPRESSION_TYPE_MARKERS_MAP = Object.fromEntries(
DDS_COMPRESSION_TYPE_MARKERS
.map(e => [e, makeFourCC(e)] as [typeof e, number])
) as Record<typeof DDS_COMPRESSION_TYPE_MARKERS[number], number>
const DDS_DECOMPRESS_VERTEX_SHADER = `
attribute vec2 aPosition;
varying highp vec2 vTextureCoord;
void main() {
gl_Position = vec4(aPosition, 0.0, 1.0);
vTextureCoord = ((aPosition * vec2(1.0, -1.0)) / 2.0 + 0.5);
}`;
const DDS_DECOMPRESS_FRAGMENT_SHADER = `
varying highp vec2 vTextureCoord;
uniform sampler2D uTexture;
void main() {
gl_FragColor = texture2D(uTexture, vTextureCoord);
}`
export class DDS {
constructor(db: IDBDatabase | undefined) {
this.db = db
let gl = this.canvasGL.getContext("webgl");
if (!gl) throw new Error("Failed to get WebGL rendering context") // TODO: make it switch to Classic userbox
this.gl = gl;
let ctx = this.canvas2D.getContext("2d");
if (!ctx) throw new Error("Failed to reach minimum system requirements") // TODO: make it switch to Classic userbox
this.ctx = ctx;
let ext =
gl.getExtension("WEBGL_compressed_texture_s3tc") ||
gl.getExtension("MOZ_WEBGL_compressed_texture_s3tc") ||
gl.getExtension("WEBKIT_WEBGL_compressed_texture_s3tc");
if (!ext) throw new Error("Browser is not supported."); // TODO: make it switch to Classic userbox
this.ext = ext;
/* Initialize shaders */
this.compileShaders();
this.gl.useProgram(this.shader);
/* Setup position buffer */
let attributeLocation = this.gl.getAttribLocation(this.shader ?? 0, "aPosition");
let positionBuffer = this.gl.createBuffer();
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, positionBuffer);
this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array([1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, -1.0]), this.gl.STATIC_DRAW);
this.gl.vertexAttribPointer(
attributeLocation,
2, this.gl.FLOAT,
false, 0, 0
);
this.gl.enableVertexAttribArray(attributeLocation)
}
/**
* @description Loads a DDS file into the internal canvas object.
* @param buffer Uint8Array to load DDS from.
* @returns String if failed to load, void if success
*/
load(buffer: Uint8Array) {
let header = this.loadHeader(buffer);
if (!header) return;
let compressionMode: GLenum = this.ext.COMPRESSED_RGBA_S3TC_DXT1_EXT;
if (header.pixelFormat.flags & 0x4) {
switch (header.pixelFormat.type) {
case DDS_COMPRESSION_TYPE_MARKERS_MAP.DXT1:
compressionMode = this.ext.COMPRESSED_RGBA_S3TC_DXT1_EXT;
break;
case DDS_COMPRESSION_TYPE_MARKERS_MAP.DXT3:
compressionMode = this.ext.COMPRESSED_RGBA_S3TC_DXT3_EXT;
break;
case DDS_COMPRESSION_TYPE_MARKERS_MAP.DXT5:
compressionMode = this.ext.COMPRESSED_RGBA_S3TC_DXT5_EXT;
break;
};
} else return;
/* Initialize and configure the texture */
let texture = this.gl.createTexture();
this.gl.activeTexture(this.gl.TEXTURE0);
this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR);
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR);
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
this.gl.compressedTexImage2D(
this.gl.TEXTURE_2D,
0,
compressionMode,
header.width,
header.height,
0,
buffer.slice(128)
);
this.gl.uniform1i(this.gl.getUniformLocation(this.shader || 0, "uTexture"), 0);
/* Prepare the canvas for drawing */
this.canvasGL.width = header.width;
this.canvasGL.height = header.height
this.gl.viewport(0, 0, this.canvasGL.width, this.canvasGL.height);
this.gl.clearColor(0.0, 0.0, 0.0, 0.0);
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
this.gl.deleteTexture(texture);
};
/**
* @description Export a Blob from the parsed DDS texture
* @returns DDS texture in specified format
* @param inFormat Mime type to export in
*/
getBlob(inFormat?: string): Promise<Blob | null> {
return new Promise(res => this.canvasGL.toBlob(res, inFormat))
}
get2DBlob(inFormat?: string): Promise<Blob | null> {
return new Promise(res => this.canvas2D.toBlob(res, inFormat))
}
/**
* @description Helper function to load in a Blob
* @input Blob to use
*/
async fromBlob(input: Blob) {
this.load(new Uint8Array(await input.arrayBuffer()));
}
/**
* @description Read a DDS file header
* @param buffer Uint8Array of the DDS file's contents
*/
loadHeader(buffer: Uint8Array) {
if (this.getUint32(buffer, 0) !== DDS_MAGIC_BYTES) return;
return {
size: this.getUint32(buffer, 4),
flags: this.getUint32(buffer, 8),
height: this.getUint32(buffer, 12),
width: this.getUint32(buffer, 16),
mipmaps: this.getUint32(buffer, 24),
/* TODO: figure out if we can cut any of this out (we totally can btw) */
pixelFormat: {
size: this.getUint32(buffer, 76),
flags: this.getUint32(buffer, 80),
type: this.getUint32(buffer, 84),
}
}
};
loadFile(path: string) : Promise<boolean> {
return new Promise(async r => {
if (!this.db)
return r(false);
let transaction = this.db.transaction(["dds"], "readonly");
let objectStore = transaction.objectStore("dds");
let request = objectStore.get(path);
request.onsuccess = async (e) => {
if (request.result)
if (request.result.blob) {
await this.fromBlob(request.result.blob)
return r(true);
}
r(false);
}
request.onerror = () => r(false);
})
};
async getFile(path: string, fallback?: string) : Promise<string> {
if (this.urlCache[path])
return this.urlCache[path]
if (!await this.loadFile(path))
if (fallback) {
if (!await this.loadFile(fallback))
return "";
} else
return "";
let url = URL.createObjectURL(await this.getBlob("image/png") ?? new Blob([]));
this.urlCache[path] = url;
return url
};
async getFileFromSheet(path: string, x: number, y: number, w: number, h: number, s?: number): Promise<string> {
if (!await this.loadFile(path))
return "";
this.canvas2D.width = w * (s ?? 1);
this.canvas2D.height = h * (s ?? 1);
this.ctx.drawImage(this.canvasGL, x, y, w, h, 0, 0, w * (s ?? 1), h * (s ?? 1));
return URL.createObjectURL(await this.get2DBlob("image/png") ?? new Blob([]));
};
async getFileScaled(path: string, s: number, fallback?: string): Promise<string> {
if (this.urlCache[path])
return this.urlCache[path]
if (!await this.loadFile(path))
if (fallback) {
if (!await this.loadFile(fallback))
return "";
} else
return "";
this.canvas2D.width = this.canvasGL.width * (s ?? 1);
this.canvas2D.height = this.canvasGL.height * (s ?? 1);
this.ctx.drawImage(this.canvasGL, 0, 0, this.canvasGL.width, this.canvasGL.height, 0, 0, this.canvasGL.width * (s ?? 1), this.canvasGL.height * (s ?? 1));
let url = URL.createObjectURL(await this.get2DBlob("image/png") ?? new Blob([]));
this.urlCache[path] = url;
return url;
};
/**
* @description Retrieve a Uint32 from a Uint8Array at the specified offset
* @param buffer Uint8Array to retrieve the Uint32 from
* @param offset Offset at which to retrieve bytes
*/
getUint32(buffer: Uint8Array, offset: number) {
return (buffer[offset + 0] << 0) +
(buffer[offset + 1] << 8) +
(buffer[offset + 2] << 16) +
(buffer[offset + 3] << 24);
};
private compileShaders() {
let vertexShader = this.gl.createShader(this.gl.VERTEX_SHADER);
let fragmentShader = this.gl.createShader(this.gl.FRAGMENT_SHADER);
if (!vertexShader || !fragmentShader) return;
this.gl.shaderSource(vertexShader, DDS_DECOMPRESS_VERTEX_SHADER);
this.gl.compileShader(vertexShader);
if (!this.gl.getShaderParameter(vertexShader, this.gl.COMPILE_STATUS))
throw new Error(
`An error occurred compiling vertex shader: ${this.gl.getShaderInfoLog(vertexShader)}`,
);
this.gl.shaderSource(fragmentShader, DDS_DECOMPRESS_FRAGMENT_SHADER);
this.gl.compileShader(fragmentShader);
if (!this.gl.getShaderParameter(fragmentShader, this.gl.COMPILE_STATUS))
throw new Error(
`An error occurred compiling fragment shader: ${this.gl.getShaderInfoLog(fragmentShader)}`,
);
let program = this.gl.createProgram();
if (!program) return;
this.shader = program;
this.gl.attachShader(program, vertexShader);
this.gl.attachShader(program, fragmentShader);
this.gl.linkProgram(program);
if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS))
throw new Error(
`An error occurred linking the program: ${this.gl.getProgramInfoLog(program)}`,
);
};
canvas2D: HTMLCanvasElement = document.createElement("canvas");
canvasGL: HTMLCanvasElement = document.createElement("canvas");
urlCache: Record<string, string> = {};
ctx: CanvasRenderingContext2D;
gl: WebGLRenderingContext;
ext: ReturnType<typeof this.gl.getExtension>;
shader: WebGLShader | null = null;
db: IDBDatabase | undefined;
};