Skip to content

Commit 4d32b86

Browse files
author
erikhaandrikman
authored
Texture Compression (#143)
This PR introduces initial support for .KTX and .PVR contained compressed textures. The format will be internally indicated.
2 parents 7a31ae6 + f242258 commit 4d32b86

File tree

12 files changed

+327
-8
lines changed

12 files changed

+327
-8
lines changed

examples/assets/test-etc1.pvr

171 KB
Binary file not shown.

examples/assets/test-s3tc.ktx

44.1 KB
Binary file not shown.

examples/tests/tx-compression.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* If not stated otherwise in this file or this component's LICENSE file the
3+
* following copyright and licenses apply:
4+
*
5+
* Copyright 2023 Comcast Cable Communications Management, LLC.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the License);
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
20+
import type { ExampleSettings } from '../common/ExampleSettings.js';
21+
22+
export default async function ({ renderer, testRoot }: ExampleSettings) {
23+
renderer.createTextNode({
24+
x: 100,
25+
y: 100,
26+
color: 0xffffffff,
27+
alpha: 1.0,
28+
text: 'etc1 compression in .pvr',
29+
fontFamily: 'Ubuntu',
30+
fontSize: 30,
31+
parent: testRoot,
32+
});
33+
34+
renderer.createNode({
35+
x: 100,
36+
y: 170,
37+
width: 550,
38+
height: 550,
39+
src: '../assets/test-etc1.pvr',
40+
parent: testRoot,
41+
});
42+
43+
renderer.createTextNode({
44+
x: 800,
45+
y: 100,
46+
color: 0xffffffff,
47+
alpha: 1.0,
48+
text: 's3tc compression in .ktx',
49+
fontFamily: 'Ubuntu',
50+
fontSize: 30,
51+
parent: testRoot,
52+
});
53+
54+
renderer.createNode({
55+
x: 800,
56+
y: 170,
57+
width: 400,
58+
height: 400,
59+
src: '../assets/test-s3tc.ktx',
60+
parent: testRoot,
61+
});
62+
}

src/core/lib/WebGlContextWrapper.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,34 @@ export class WebGlContextWrapper {
335335
);
336336
}
337337
}
338+
/**
339+
* ```
340+
* gl.compressedTexImage2D(gl.TEXTURE_2D, level, internalFormat, width, height, border, data);
341+
* ```
342+
*
343+
* @remarks
344+
* **WebGL Difference**: Bind target is always `gl.TEXTURE_2D`
345+
*/
338346

347+
compressedTexImage2D(
348+
level: GLint,
349+
internalformat: GLenum,
350+
width: GLsizei,
351+
height: GLsizei,
352+
border: GLint,
353+
data?: ArrayBufferView,
354+
): void {
355+
const { gl } = this;
356+
gl.compressedTexImage2D(
357+
gl.TEXTURE_2D,
358+
level,
359+
internalformat,
360+
width,
361+
height,
362+
border,
363+
data as ArrayBufferView,
364+
);
365+
}
339366
/**
340367
* ```
341368
* gl.pixelStorei(pname, param);

src/core/lib/textureCompression.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/*
2+
* If not stated otherwise in this file or this component's LICENSE file the
3+
* following copyright and licenses apply:
4+
*
5+
* Copyright 2023 Comcast Cable Communications Management, LLC.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the License);
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
20+
import { type TextureData } from '../textures/Texture.js';
21+
22+
/**
23+
* Tests if the given location is a compressed texture container
24+
* @param url
25+
* @remarks
26+
* This function is used to determine if the given image url is a compressed
27+
* and only supports the following extensions: .ktx and .pvr
28+
* @returns
29+
*/
30+
export function isCompressedTextureContainer(url: string): boolean {
31+
return /\.(ktx|pvr)$/.test(url);
32+
}
33+
34+
/**
35+
* Loads a compressed texture container
36+
* @param url
37+
* @returns
38+
*/
39+
export const loadCompressedTexture = async (
40+
url: string,
41+
): Promise<TextureData> => {
42+
const response = await fetch(url);
43+
const arrayBuffer = await response.arrayBuffer();
44+
45+
if (url.indexOf('.ktx') !== -1) {
46+
return loadKTXData(arrayBuffer);
47+
}
48+
49+
return loadPVRData(arrayBuffer);
50+
};
51+
52+
/**
53+
* Loads a KTX texture container and returns the texture data
54+
* @param buffer
55+
* @returns
56+
*/
57+
const loadKTXData = async (buffer: ArrayBuffer): Promise<TextureData> => {
58+
const view = new DataView(buffer);
59+
const littleEndian = view.getUint32(12) === 16909060 ? true : false;
60+
const mipmaps = [];
61+
62+
const data = {
63+
glInternalFormat: view.getUint32(28, littleEndian),
64+
pixelWidth: view.getUint32(36, littleEndian),
65+
pixelHeight: view.getUint32(40, littleEndian),
66+
numberOfMipmapLevels: view.getUint32(56, littleEndian),
67+
bytesOfKeyValueData: view.getUint32(60, littleEndian),
68+
};
69+
70+
let offset = 64;
71+
72+
// Key Value Pairs of data start at byte offset 64
73+
// But the only known kvp is the API version, so skipping parsing.
74+
offset += data.bytesOfKeyValueData;
75+
76+
for (let i = 0; i < data.numberOfMipmapLevels; i++) {
77+
const imageSize = view.getUint32(offset);
78+
offset += 4;
79+
80+
mipmaps.push(view.buffer.slice(offset, imageSize));
81+
offset += imageSize;
82+
}
83+
84+
return {
85+
data: {
86+
glInternalFormat: data.glInternalFormat,
87+
mipmaps,
88+
width: data.pixelWidth || 0,
89+
height: data.pixelHeight || 0,
90+
type: 'ktx',
91+
},
92+
premultiplyAlpha: false,
93+
};
94+
};
95+
96+
/**
97+
* Loads a PVR texture container and returns the texture data
98+
* @param buffer
99+
* @returns
100+
*/
101+
const loadPVRData = async (buffer: ArrayBuffer): Promise<TextureData> => {
102+
// pvr header length in 32 bits
103+
const pvrHeaderLength = 13;
104+
// for now only we only support: COMPRESSED_RGB_ETC1_WEBGL
105+
const pvrFormatEtc1 = 0x8d64;
106+
const pvrWidth = 7;
107+
const pvrHeight = 6;
108+
const pvrMipmapCount = 11;
109+
const pvrMetadata = 12;
110+
const arrayBuffer = buffer;
111+
const header = new Int32Array(arrayBuffer, 0, pvrHeaderLength);
112+
113+
// @ts-expect-error Object possibly undefined
114+
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
115+
const dataOffset = header[pvrMetadata] + 52;
116+
const pvrtcData = new Uint8Array(arrayBuffer, dataOffset);
117+
const mipmaps = [];
118+
const data = {
119+
pixelWidth: header[pvrWidth],
120+
pixelHeight: header[pvrHeight],
121+
numberOfMipmapLevels: header[pvrMipmapCount] || 0,
122+
};
123+
124+
let offset = 0;
125+
let width = data.pixelWidth || 0;
126+
let height = data.pixelHeight || 0;
127+
128+
for (let i = 0; i < data.numberOfMipmapLevels; i++) {
129+
const level = ((width + 3) >> 2) * ((height + 3) >> 2) * 8;
130+
const view = new Uint8Array(
131+
arrayBuffer,
132+
pvrtcData.byteOffset + offset,
133+
level,
134+
);
135+
136+
mipmaps.push(view);
137+
offset += level;
138+
width = width >> 1;
139+
height = height >> 1;
140+
}
141+
142+
return {
143+
data: {
144+
glInternalFormat: pvrFormatEtc1,
145+
mipmaps: mipmaps,
146+
width: data.pixelWidth || 0,
147+
height: data.pixelHeight || 0,
148+
type: 'pvr',
149+
},
150+
premultiplyAlpha: false,
151+
};
152+
};

src/core/renderers/webgl/WebGlCoreCtxTexture.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,26 @@ export class WebGlCoreCtxTexture extends CoreContextTexture {
168168
glw.UNSIGNED_BYTE,
169169
TRANSPARENT_TEXTURE_DATA,
170170
);
171+
} else if ('mipmaps' in textureData.data && textureData.data.mipmaps) {
172+
const {
173+
mipmaps,
174+
width = 0,
175+
height = 0,
176+
type,
177+
glInternalFormat,
178+
} = textureData.data;
179+
const view =
180+
type === 'ktx'
181+
? new DataView(mipmaps[0] ?? new ArrayBuffer(0))
182+
: (mipmaps[0] as unknown as ArrayBufferView);
183+
184+
glw.bindTexture(this._nativeCtxTexture);
185+
glw.compressedTexImage2D(0, glInternalFormat, width, height, 0, view);
186+
187+
glw.texParameteri(glw.TEXTURE_WRAP_S, glw.CLAMP_TO_EDGE);
188+
glw.texParameteri(glw.TEXTURE_WRAP_T, glw.CLAMP_TO_EDGE);
189+
glw.texParameteri(glw.TEXTURE_MAG_FILTER, glw.LINEAR);
190+
glw.texParameteri(glw.TEXTURE_MIN_FILTER, glw.LINEAR);
171191
} else {
172192
console.error(
173193
`WebGlCoreCtxTexture.onLoadRequest: Unexpected textureData returned`,

src/core/text-rendering/font-face-types/SdfTrFontFace/SdfTrFontFace.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,10 @@ export class SdfTrFontFace<
9898
});
9999
// We know `data` is defined here, because we just set it
100100
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
101-
(this.shaper as FontShaper) = new SdfFontShaper(this.data!, this.glyphMap);
101+
(this.shaper as FontShaper) = new SdfFontShaper(
102+
this.data!,
103+
this.glyphMap,
104+
);
102105
this.checkLoaded();
103106
})
104107
.catch(console.error);

src/core/text-rendering/font-face-types/SdfTrFontFace/internal/SdfFontShaper.test.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ sdfData.chars.forEach((glyph) => {
3434

3535
describe('SdfFontShaper', () => {
3636
it('should be able to shape text.', () => {
37-
const shaper = new SdfFontShaper(sdfData as unknown as SdfFontData, glyphMap);
37+
const shaper = new SdfFontShaper(
38+
sdfData as unknown as SdfFontData,
39+
glyphMap,
40+
);
3841
const peekableCodepoints = new PeekableIterator(
3942
getUnicodeCodepoints('Hi!'),
4043
);
@@ -88,7 +91,10 @@ describe('SdfFontShaper', () => {
8891
});
8992

9093
it('should be able to shape text that we know have kerning pairs.', () => {
91-
const shaper = new SdfFontShaper(sdfData as unknown as SdfFontData, glyphMap);
94+
const shaper = new SdfFontShaper(
95+
sdfData as unknown as SdfFontData,
96+
glyphMap,
97+
);
9298
const peekableCodepoints = new PeekableIterator(
9399
getUnicodeCodepoints('WeVo'),
94100
);
@@ -130,8 +136,10 @@ describe('SdfFontShaper', () => {
130136
});
131137

132138
it('should be able to shape text with letterSpacing.', () => {
133-
134-
const shaper = new SdfFontShaper(sdfData as unknown as SdfFontData, glyphMap);
139+
const shaper = new SdfFontShaper(
140+
sdfData as unknown as SdfFontData,
141+
glyphMap,
142+
);
135143
const peekableCodepoints = new PeekableIterator(
136144
getUnicodeCodepoints('We!'),
137145
);

src/core/text-rendering/font-face-types/SdfTrFontFace/internal/SdfFontShaper.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@ export class SdfFontShaper extends FontShaper {
3939
private readonly glyphMap: Map<number, SdfFontData['chars'][0]>;
4040
private readonly kernings: KerningTable;
4141

42-
constructor(data: SdfFontData, glyphMap: Map<number, SdfFontData['chars'][0]>) {
42+
constructor(
43+
data: SdfFontData,
44+
glyphMap: Map<number, SdfFontData['chars'][0]>,
45+
) {
4346
super();
4447
this.data = data;
4548
this.glyphMap = glyphMap;

src/core/text-rendering/renderers/SdfTextRenderer/internal/measureText.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@ sdfData.chars.forEach((glyph) => {
3333
describe('measureText', () => {
3434
it('should measure text width', () => {
3535
const PERIOD_WIDTH = 10.332;
36-
const shaper = new SdfFontShaper(sdfData as unknown as SdfFontData, glyphMap);
36+
const shaper = new SdfFontShaper(
37+
sdfData as unknown as SdfFontData,
38+
glyphMap,
39+
);
3740
expect(measureText('', { letterSpacing: 0 }, shaper)).toBe(0);
3841
expect(measureText('.', { letterSpacing: 0 }, shaper)).toBe(PERIOD_WIDTH);
3942
expect(measureText('..', { letterSpacing: 0 }, shaper)).toBeCloseTo(

0 commit comments

Comments
 (0)