Skip to content

Commit

Permalink
Texture Compression (#143)
Browse files Browse the repository at this point in the history
This PR introduces initial support for .KTX and .PVR contained
compressed textures. The format will be internally indicated.
  • Loading branch information
erikhaandrikman authored Feb 1, 2024
2 parents 7a31ae6 + f242258 commit 4d32b86
Show file tree
Hide file tree
Showing 12 changed files with 327 additions and 8 deletions.
Binary file added examples/assets/test-etc1.pvr
Binary file not shown.
Binary file added examples/assets/test-s3tc.ktx
Binary file not shown.
62 changes: 62 additions & 0 deletions examples/tests/tx-compression.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* If not stated otherwise in this file or this component's LICENSE file the
* following copyright and licenses apply:
*
* Copyright 2023 Comcast Cable Communications Management, LLC.
*
* Licensed under the Apache License, Version 2.0 (the License);
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import type { ExampleSettings } from '../common/ExampleSettings.js';

export default async function ({ renderer, testRoot }: ExampleSettings) {
renderer.createTextNode({
x: 100,
y: 100,
color: 0xffffffff,
alpha: 1.0,
text: 'etc1 compression in .pvr',
fontFamily: 'Ubuntu',
fontSize: 30,
parent: testRoot,
});

renderer.createNode({
x: 100,
y: 170,
width: 550,
height: 550,
src: '../assets/test-etc1.pvr',
parent: testRoot,
});

renderer.createTextNode({
x: 800,
y: 100,
color: 0xffffffff,
alpha: 1.0,
text: 's3tc compression in .ktx',
fontFamily: 'Ubuntu',
fontSize: 30,
parent: testRoot,
});

renderer.createNode({
x: 800,
y: 170,
width: 400,
height: 400,
src: '../assets/test-s3tc.ktx',
parent: testRoot,
});
}
27 changes: 27 additions & 0 deletions src/core/lib/WebGlContextWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,34 @@ export class WebGlContextWrapper {
);
}
}
/**
* ```
* gl.compressedTexImage2D(gl.TEXTURE_2D, level, internalFormat, width, height, border, data);
* ```
*
* @remarks
* **WebGL Difference**: Bind target is always `gl.TEXTURE_2D`
*/

compressedTexImage2D(
level: GLint,
internalformat: GLenum,
width: GLsizei,
height: GLsizei,
border: GLint,
data?: ArrayBufferView,
): void {
const { gl } = this;
gl.compressedTexImage2D(
gl.TEXTURE_2D,
level,
internalformat,
width,
height,
border,
data as ArrayBufferView,
);
}
/**
* ```
* gl.pixelStorei(pname, param);
Expand Down
152 changes: 152 additions & 0 deletions src/core/lib/textureCompression.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/*
* If not stated otherwise in this file or this component's LICENSE file the
* following copyright and licenses apply:
*
* Copyright 2023 Comcast Cable Communications Management, LLC.
*
* Licensed under the Apache License, Version 2.0 (the License);
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { type TextureData } from '../textures/Texture.js';

/**
* Tests if the given location is a compressed texture container
* @param url
* @remarks
* This function is used to determine if the given image url is a compressed
* and only supports the following extensions: .ktx and .pvr
* @returns
*/
export function isCompressedTextureContainer(url: string): boolean {
return /\.(ktx|pvr)$/.test(url);
}

/**
* Loads a compressed texture container
* @param url
* @returns
*/
export const loadCompressedTexture = async (
url: string,
): Promise<TextureData> => {
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();

if (url.indexOf('.ktx') !== -1) {
return loadKTXData(arrayBuffer);
}

return loadPVRData(arrayBuffer);
};

/**
* Loads a KTX texture container and returns the texture data
* @param buffer
* @returns
*/
const loadKTXData = async (buffer: ArrayBuffer): Promise<TextureData> => {
const view = new DataView(buffer);
const littleEndian = view.getUint32(12) === 16909060 ? true : false;
const mipmaps = [];

const data = {
glInternalFormat: view.getUint32(28, littleEndian),
pixelWidth: view.getUint32(36, littleEndian),
pixelHeight: view.getUint32(40, littleEndian),
numberOfMipmapLevels: view.getUint32(56, littleEndian),
bytesOfKeyValueData: view.getUint32(60, littleEndian),
};

let offset = 64;

// Key Value Pairs of data start at byte offset 64
// But the only known kvp is the API version, so skipping parsing.
offset += data.bytesOfKeyValueData;

for (let i = 0; i < data.numberOfMipmapLevels; i++) {
const imageSize = view.getUint32(offset);
offset += 4;

mipmaps.push(view.buffer.slice(offset, imageSize));
offset += imageSize;
}

return {
data: {
glInternalFormat: data.glInternalFormat,
mipmaps,
width: data.pixelWidth || 0,
height: data.pixelHeight || 0,
type: 'ktx',
},
premultiplyAlpha: false,
};
};

/**
* Loads a PVR texture container and returns the texture data
* @param buffer
* @returns
*/
const loadPVRData = async (buffer: ArrayBuffer): Promise<TextureData> => {
// pvr header length in 32 bits
const pvrHeaderLength = 13;
// for now only we only support: COMPRESSED_RGB_ETC1_WEBGL
const pvrFormatEtc1 = 0x8d64;
const pvrWidth = 7;
const pvrHeight = 6;
const pvrMipmapCount = 11;
const pvrMetadata = 12;
const arrayBuffer = buffer;
const header = new Int32Array(arrayBuffer, 0, pvrHeaderLength);

// @ts-expect-error Object possibly undefined
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
const dataOffset = header[pvrMetadata] + 52;
const pvrtcData = new Uint8Array(arrayBuffer, dataOffset);
const mipmaps = [];
const data = {
pixelWidth: header[pvrWidth],
pixelHeight: header[pvrHeight],
numberOfMipmapLevels: header[pvrMipmapCount] || 0,
};

let offset = 0;
let width = data.pixelWidth || 0;
let height = data.pixelHeight || 0;

for (let i = 0; i < data.numberOfMipmapLevels; i++) {
const level = ((width + 3) >> 2) * ((height + 3) >> 2) * 8;
const view = new Uint8Array(
arrayBuffer,
pvrtcData.byteOffset + offset,
level,
);

mipmaps.push(view);
offset += level;
width = width >> 1;
height = height >> 1;
}

return {
data: {
glInternalFormat: pvrFormatEtc1,
mipmaps: mipmaps,
width: data.pixelWidth || 0,
height: data.pixelHeight || 0,
type: 'pvr',
},
premultiplyAlpha: false,
};
};
20 changes: 20 additions & 0 deletions src/core/renderers/webgl/WebGlCoreCtxTexture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,26 @@ export class WebGlCoreCtxTexture extends CoreContextTexture {
glw.UNSIGNED_BYTE,
TRANSPARENT_TEXTURE_DATA,
);
} else if ('mipmaps' in textureData.data && textureData.data.mipmaps) {
const {
mipmaps,
width = 0,
height = 0,
type,
glInternalFormat,
} = textureData.data;
const view =
type === 'ktx'
? new DataView(mipmaps[0] ?? new ArrayBuffer(0))
: (mipmaps[0] as unknown as ArrayBufferView);

glw.bindTexture(this._nativeCtxTexture);
glw.compressedTexImage2D(0, glInternalFormat, width, height, 0, view);

glw.texParameteri(glw.TEXTURE_WRAP_S, glw.CLAMP_TO_EDGE);
glw.texParameteri(glw.TEXTURE_WRAP_T, glw.CLAMP_TO_EDGE);
glw.texParameteri(glw.TEXTURE_MAG_FILTER, glw.LINEAR);
glw.texParameteri(glw.TEXTURE_MIN_FILTER, glw.LINEAR);
} else {
console.error(
`WebGlCoreCtxTexture.onLoadRequest: Unexpected textureData returned`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,10 @@ export class SdfTrFontFace<
});
// We know `data` is defined here, because we just set it
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
(this.shaper as FontShaper) = new SdfFontShaper(this.data!, this.glyphMap);
(this.shaper as FontShaper) = new SdfFontShaper(
this.data!,
this.glyphMap,
);
this.checkLoaded();
})
.catch(console.error);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ sdfData.chars.forEach((glyph) => {

describe('SdfFontShaper', () => {
it('should be able to shape text.', () => {
const shaper = new SdfFontShaper(sdfData as unknown as SdfFontData, glyphMap);
const shaper = new SdfFontShaper(
sdfData as unknown as SdfFontData,
glyphMap,
);
const peekableCodepoints = new PeekableIterator(
getUnicodeCodepoints('Hi!'),
);
Expand Down Expand Up @@ -88,7 +91,10 @@ describe('SdfFontShaper', () => {
});

it('should be able to shape text that we know have kerning pairs.', () => {
const shaper = new SdfFontShaper(sdfData as unknown as SdfFontData, glyphMap);
const shaper = new SdfFontShaper(
sdfData as unknown as SdfFontData,
glyphMap,
);
const peekableCodepoints = new PeekableIterator(
getUnicodeCodepoints('WeVo'),
);
Expand Down Expand Up @@ -130,8 +136,10 @@ describe('SdfFontShaper', () => {
});

it('should be able to shape text with letterSpacing.', () => {

const shaper = new SdfFontShaper(sdfData as unknown as SdfFontData, glyphMap);
const shaper = new SdfFontShaper(
sdfData as unknown as SdfFontData,
glyphMap,
);
const peekableCodepoints = new PeekableIterator(
getUnicodeCodepoints('We!'),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ export class SdfFontShaper extends FontShaper {
private readonly glyphMap: Map<number, SdfFontData['chars'][0]>;
private readonly kernings: KerningTable;

constructor(data: SdfFontData, glyphMap: Map<number, SdfFontData['chars'][0]>) {
constructor(
data: SdfFontData,
glyphMap: Map<number, SdfFontData['chars'][0]>,
) {
super();
this.data = data;
this.glyphMap = glyphMap;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ sdfData.chars.forEach((glyph) => {
describe('measureText', () => {
it('should measure text width', () => {
const PERIOD_WIDTH = 10.332;
const shaper = new SdfFontShaper(sdfData as unknown as SdfFontData, glyphMap);
const shaper = new SdfFontShaper(
sdfData as unknown as SdfFontData,
glyphMap,
);
expect(measureText('', { letterSpacing: 0 }, shaper)).toBe(0);
expect(measureText('.', { letterSpacing: 0 }, shaper)).toBe(PERIOD_WIDTH);
expect(measureText('..', { letterSpacing: 0 }, shaper)).toBeCloseTo(
Expand Down
9 changes: 9 additions & 0 deletions src/core/textures/ImageTexture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@

import type { CoreTextureManager } from '../CoreTextureManager.js';
import { Texture, type TextureData } from './Texture.js';
import {
isCompressedTextureContainer,
loadCompressedTexture,
} from '../lib/textureCompression.js';

/**
* Properties of the {@link ImageTexture}
Expand Down Expand Up @@ -84,6 +88,11 @@ export class ImageTexture extends Texture {
};
}

// Handle compressed textures
if (isCompressedTextureContainer(src)) {
return loadCompressedTexture(src);
}

if (this.txManager.imageWorkerManager.imageWorkersEnabled) {
return await this.txManager.imageWorkerManager.getImage(
src,
Expand Down
Loading

0 comments on commit 4d32b86

Please sign in to comment.