/**
* for description see https://www.khronos.org/opengles/sdk/tools/KTX/
* for file layout see https://www.khronos.org/opengles/sdk/tools/KTX/file_format_spec/
* ported from https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/js/loaders/KTXLoader.js
*/
import Class from '../core/Class';
import BasicLoader from './BasicLoader';
import Loader from './Loader';
import Texture from '../texture/Texture';
import extensions from '../renderer/extensions';
import log from '../utils/log';
/**
* @class
* @private
*/
const KhronosTextureContainer = Class.create(/** @lends KhronosTextureContainer.prototype */{
Statics: {
HEADER_LEN: 12 + (13 * 4), // identifier + header elements (not including key value meta-data pairs)
COMPRESSED_2D: 0, // uses a gl.compressedTexImage2D()
COMPRESSED_3D: 1, // uses a gl.compressedTexImage3D()
TEX_2D: 2, // uses a gl.texImage2D()
TEX_3D: 3, // uses a gl.texImage3D()
},
isKhronosTextureContainer: true,
className: 'KhronosTextureContainer',
/**
* @constructs
* @param {ArrayBuffer} arrayBuffer contents of the KTX container file
* @param {number} facesExpected should be either 1 or 6, based whether a cube texture or or
*/
constructor(arrayBuffer, facesExpected, baseOffset = 0) {
this.arrayBuffer = arrayBuffer;
this.baseOffset = baseOffset;
// Test that it is a ktx formatted file, based on the first 12 bytes, character representation is:
// '´', 'K', 'T', 'X', ' ', '1', '1', 'ª', '\r', '\n', '\x1A', '\n'
// 0xAB, 0x4B, 0x54, 0x58, 0x20, 0x31, 0x31, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A
const identifier = new Uint8Array(this.arrayBuffer, this.baseOffset, 12);
if (identifier[0] !== 0xAB
|| identifier[1] !== 0x4B
|| identifier[2] !== 0x54
|| identifier[3] !== 0x58
|| identifier[4] !== 0x20
|| identifier[5] !== 0x31
|| identifier[6] !== 0x31
|| identifier[7] !== 0xBB
|| identifier[8] !== 0x0D
|| identifier[9] !== 0x0A
|| identifier[10] !== 0x1A
|| identifier[11] !== 0x0A) {
log.error('texture missing KTX identifier');
return;
}
// load the reset of the header in native 32 bit uint
const dataSize = Uint32Array.BYTES_PER_ELEMENT;
const headerDataView = new DataView(this.arrayBuffer, this.baseOffset + 12, 13 * dataSize);
const endianness = headerDataView.getUint32(0, true);
const littleEndian = endianness === 0x04030201;
this.glType = headerDataView.getUint32(1 * dataSize, littleEndian); // must be 0 for compressed textures
this.glTypeSize = headerDataView.getUint32(2 * dataSize, littleEndian); // must be 1 for compressed textures
this.glFormat = headerDataView.getUint32(3 * dataSize, littleEndian); // must be 0 for compressed textures
this.glInternalFormat = headerDataView.getUint32(4 * dataSize, littleEndian); // the value of arg passed to gl.compressedTexImage2D(,,x,,,,)
this.glBaseInternalFormat = headerDataView.getUint32(5 * dataSize, littleEndian); // specify GL_RGB, GL_RGBA, GL_ALPHA, etc (un-compressed only)
this.pixelWidth = headerDataView.getUint32(6 * dataSize, littleEndian); // level 0 value of arg passed to gl.compressedTexImage2D(,,,x,,,)
this.pixelHeight = headerDataView.getUint32(7 * dataSize, littleEndian); // level 0 value of arg passed to gl.compressedTexImage2D(,,,,x,,)
this.pixelDepth = headerDataView.getUint32(8 * dataSize, littleEndian); // level 0 value of arg passed to gl.compressedTexImage3D(,,,,,x,,)
this.numberOfArrayElements = headerDataView.getUint32(9 * dataSize, littleEndian); // used for texture arrays
this.numberOfFaces = headerDataView.getUint32(10 * dataSize, littleEndian); // used for cubemap textures, should either be 1 or 6
this.numberOfMipmapLevels = headerDataView.getUint32(11 * dataSize, littleEndian); // number of levels; disregard possibility of 0 for compressed textures
this.bytesOfKeyValueData = headerDataView.getUint32(12 * dataSize, littleEndian); // the amount of space after the header for meta-data
// value of zero is an indication to generate mipmaps @ runtime. Not usually allowed for compressed, so disregard.
this.numberOfMipmapLevels = Math.max(1, this.numberOfMipmapLevels);
if (this.pixelHeight === 0 || this.pixelDepth !== 0) {
log.warn('only 2D textures currently supported');
return;
}
if (this.numberOfArrayElements !== 0) {
log.warn('texture arrays not currently supported');
return;
}
if (this.numberOfFaces !== facesExpected) {
log.warn('number of faces expected' + facesExpected + ', but found ' + this.numberOfFaces);
return;
}
// we now have a completely validated file, so could use existence of loadType as success
// would need to make this more elaborate & adjust checks above to support more than one load type
if (this.glType === 0) {
this.loadType = KhronosTextureContainer.COMPRESSED_2D;
} else {
this.loadType = KhronosTextureContainer.TEX_2D;
}
},
// return mipmaps
mipmaps(loadMipmaps) {
let mipmaps = [];
// initialize width & height for level 1
let dataOffset = KhronosTextureContainer.HEADER_LEN + this.bytesOfKeyValueData;
let width = this.pixelWidth;
let height = this.pixelHeight;
let mipmapCount = loadMipmaps ? this.numberOfMipmapLevels : 1;
for (let level = 0; level < mipmapCount; level++) {
let imageSize = new Int32Array(this.arrayBuffer, this.baseOffset + dataOffset, 1)[0]; // size per face, since not supporting array cubemaps
for (let face = 0; face < this.numberOfFaces; face++) {
let byteArray = new Uint8Array(this.arrayBuffer, this.baseOffset + dataOffset + 4, imageSize);
mipmaps.push({
data: byteArray,
width,
height
});
dataOffset += imageSize + 4; // size of the image + 4 for the imageSize field
dataOffset += 3 - ((imageSize + 3) % 4); // add padding for odd sized image
}
width = Math.max(1.0, width * 0.5);
height = Math.max(1.0, height * 0.5);
}
return mipmaps;
}
});
/**
* KTX 加载器
* @class
*/
const KTXLoader = Class.create(/** @lends KTXLoader.prototype */{
Extends: BasicLoader,
Statics: {
/**
* astc
* @memberOf KTXLoader
* @type {String}
* @readOnly
* @default WEBGL_compressed_texture_astc
*/
astc: 'WEBGL_compressed_texture_astc',
/**
* etc
* @memberOf KTXLoader
* @type {String}
* @readOnly
* @default WEBGL_compressed_texture_etc
*/
etc: 'WEBGL_compressed_texture_etc',
/**
* etc1
* @memberOf KTXLoader
* @type {String}
* @readOnly
* @default WEBGL_compressed_texture_etc1
*/
etc1: 'WEBGL_compressed_texture_etc1',
/**
* pvrtc
* @memberOf KTXLoader
* @type {String}
* @readOnly
* @default WEBGL_compressed_texture_pvrtc
*/
pvrtc: 'WEBGL_compressed_texture_pvrtc',
/**
* s3tc
* @memberOf KTXLoader
* @type {String}
* @readOnly
* @default WEBGL_compressed_texture_s3tc
*/
s3tc: 'WEBGL_compressed_texture_s3tc'
},
/**
* @type {boolean}
* @default true
*/
isKTXLoader: true,
/**
* 类名
* @type {string}
* @default KTXLoader
*/
className: 'KTXLoader',
constructor() {
extensions.use(KTXLoader.astc);
extensions.use(KTXLoader.atc);
extensions.use(KTXLoader.etc);
extensions.use(KTXLoader.etc1);
extensions.use(KTXLoader.pvrtc);
extensions.use(KTXLoader.s3tc);
extensions.use(KTXLoader.s3tc_srgb);
KTXLoader.superclass.constructor.call(this);
},
/**
* load
* @param {Object} params
*/
load(params) {
if (params.src instanceof ArrayBuffer) {
return Promise.resolve(this.createTexture(params, params.src));
}
if (ArrayBuffer.isView(params.src)) {
return Promise.resolve(this.createTexture(params, params.src.buffer, params.src.byteOffset));
}
return this.loadRes(params.src, 'buffer')
.then((buffer) => {
return this.createTexture(params, buffer);
});
},
createTexture(params, buffer, baseOffset = 0) {
const ktx = new KhronosTextureContainer(buffer, 1, baseOffset);
const data = {
compressed: ktx.glType === 0,
type: ktx.glType,
width: ktx.pixelWidth,
height: ktx.pixelHeight,
internalFormat: ktx.glInternalFormat,
format: ktx.glFormat,
isCubemap: ktx.numberOfFaces === 6
};
if (ktx.numberOfMipmapLevels >= Math.floor(Math.log2(Math.max(data.width, data.height)) + 1)) {
data.mipmaps = ktx.mipmaps(true);
data.image = data.mipmaps[0].data;
} else {
data.mipmaps = null;
data.image = ktx.mipmaps(false)[0].data;
}
return new Texture(data);
}
});
Loader.addLoader('ktx', KTXLoader);
export default KTXLoader;