Pārlūkot izejas kodu

KTX2Loader: Initial commit.

Don McCurdy 5 gadi atpakaļ
vecāks
revīzija
c36a565f46

+ 1 - 0
examples/files.js

@@ -118,6 +118,7 @@ var files = {
 		"webgl_loader_texture_exr",
 		"webgl_loader_texture_hdr",
 		"webgl_loader_texture_ktx",
+		"webgl_loader_texture_ktx2",
 		"webgl_loader_texture_pvrtc",
 		"webgl_loader_texture_rgbm",
 		"webgl_loader_texture_tga",

+ 23 - 4
examples/js/libs/basis/README.md

@@ -7,9 +7,19 @@ a wide variety of GPU texture compression formats.
 
 [GitHub](https://github.com/BinomialLLC/basis_universal)
 
-## Contents
+## Transcoders
 
-This folder contains two files:
+Basis Universal texture data may be used in two different file formats:
+`.basis` and `.ktx2`. Texture data is identical in both cases, but different
+transcoders are required to read the two file types. Both transcoders are
+available in this folder now, but they may be merged in the future.
+
+For further documentation about the Basis compressor and transcoder, refer to
+the [Basis GitHub repository](https://github.com/BinomialLLC/basis_universal).
+
+### .basis
+
+The folder contains two files required for transcoding `.basis` textures:
 
 * `basis_transcoder.js` — JavaScript wrapper for the WebAssembly transcoder.
 * `basis_transcoder.wasm` — WebAssembly transcoder.
@@ -35,8 +45,17 @@ basisLoader.load( 'diffuse.basis', function ( texture ) {
 } );
 ```
 
-For further documentation about the Basis compressor and transcoder, refer to
-the [Basis GitHub repository](https://github.com/BinomialLLC/basis_universal).
+### .ktx2
+
+The folder contains two files required for transcoding `.ktx2` textures:
+
+* `msc_basis_transcoder.js` — JavaScript wrapper for the WebAssembly transcoder.
+* `msc_basis_transcoder.wasm` — WebAssembly transcoder.
+
+Currently, the `msc_basis_transcoder.js` file must be added to the page as a
+global script. The WASM transcoder will be downloaded from the same directory
+automatically. These will likely be replaced with ES modules, and merged with
+the `.basis` transcoder, in the future. See `KTX2Loader` for usage.
 
 ## License
 

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 7 - 0
examples/js/libs/basis/msc_basis_transcoder.js


BIN
examples/js/libs/basis/msc_basis_transcoder.wasm


+ 696 - 0
examples/jsm/loaders/KTX2Loader.js

@@ -0,0 +1,696 @@
+/**
+ * @author donmccurdy / https://www.donmccurdy.com
+ * @author MarkCallow / https://github.com/MarkCallow
+ *
+ * References:
+ * - KTX: http://github.khronos.org/KTX-Specification/
+ * - DFD: https://www.khronos.org/registry/DataFormat/specs/1.3/dataformat.1.3.html#basicdescriptor
+ *
+ * To do:
+ * - [ ] Cross-platform testing
+ * - [ ] Specify JS/WASM transcoder path
+ * - [ ] High-quality demo
+ * - [ ] Documentation
+ * - [ ] TypeScript definitions
+ * - [ ] (Optional) Include BC5
+ * - [ ] (Optional) Include EAC RG on mobile (WEBGL_compressed_texture_etc)
+ * - [ ] (Optional) Include two-texture output mode (see: clearcoat + clearcoatRoughness)
+ * - [ ] (Optional) Support Web Workers, after #18234
+ */
+
+import {
+	CompressedTexture,
+	CompressedTextureLoader,
+	FileLoader,
+	LinearEncoding,
+	LinearFilter,
+	LinearMipmapLinearFilter,
+	RGB_ETC1_Format,
+	RGB_ETC2_Format,
+	RGBA_ETC2_EAC_Format,
+	RGB_PVRTC_4BPPV1_Format,
+	RGB_S3TC_DXT1_Format,
+	RGBA_ASTC_4x4_Format,
+	RGBA_PVRTC_4BPPV1_Format,
+	RGBA_S3TC_DXT5_Format,
+	sRGBEncoding,
+	UnsignedByteType,
+} from '../../../build/three.module.js';
+
+// Data Format Descriptor (DFD) constants.
+
+const DFDModel = {
+	ETC1S: 163,
+	UASTC: 166,
+}
+
+const DFDChannel = {
+	ETC1S: {
+		RGB: 0,
+		RRR: 3,
+		GGG: 4,
+		AAA: 15,
+	},
+	UASTC: {
+		RGB: 0,
+		RGBA: 3,
+		RRR: 4,
+		RRRG: 5
+	},
+};
+
+//
+
+class KTX2Loader extends CompressedTextureLoader {
+
+	constructor ( manager ) {
+
+		super( manager );
+
+		this.basisModule = null;
+
+		this.transcoderConfig = {};
+
+	}
+
+	detectSupport ( renderer ) {
+
+		this.transcoderConfig = {
+			astcSupported: renderer.extensions.has( 'WEBGL_compressed_texture_astc' ),
+			etc1Supported: renderer.extensions.has( 'WEBGL_compressed_texture_etc1' ),
+			etc2Supported: renderer.extensions.has( 'WEBGL_compressed_texture_etc' ),
+			dxtSupported: renderer.extensions.has( 'WEBGL_compressed_texture_s3tc' ),
+			pvrtcSupported: renderer.extensions.has( 'WEBGL_compressed_texture_pvrtc' )
+				|| renderer.extensions.has( 'WEBKIT_WEBGL_compressed_texture_pvrtc' )
+		};
+
+		return this;
+
+	}
+
+	init () {
+
+		var scope = this;
+
+		// The Emscripten wrapper returns a fake Promise, which can cause
+		// infinite recursion when mixed with native Promises. Wrap the module
+		// initialization to return a native Promise.
+		return new Promise( function ( resolve ) {
+
+			MSC_TRANSCODER().then( function ( basisModule ) {
+
+				scope.basisModule = basisModule;
+
+				basisModule.initTranscoders();
+
+				resolve();
+
+			} );
+
+		} );
+
+	}
+
+	load ( url, onLoad, onProgress, onError ) {
+
+		var scope = this;
+
+		var texture = new CompressedTexture();
+
+		var bufferPending = new Promise( function (resolve, reject ) {
+
+			new FileLoader( scope.manager )
+				.setPath( scope.path )
+				.setResponseType( 'arraybuffer' )
+				.load( url, resolve, onProgress, reject );
+
+		} );
+
+		Promise.all( [ bufferPending, this.init() ] ).then( function ( [ buffer ] ) {
+
+			scope.parse( buffer, function ( _texture ) {
+
+				texture.copy( _texture );
+				texture.needsUpdate = true;
+
+				if ( onLoad ) onLoad( texture );
+
+			}, onError );
+
+		} );
+
+		return texture;
+
+	}
+
+	parse ( buffer, onLoad, onError ) {
+
+		var BasisLzEtc1sImageTranscoder = this.basisModule.BasisLzEtc1sImageTranscoder;
+		var UastcImageTranscoder = this.basisModule.UastcImageTranscoder;
+		var TextureFormat = this.basisModule.TextureFormat;
+
+		var ktx = new KTX2Container( this.basisModule, buffer );
+
+		// TODO(donmccurdy): Should test if texture is transcodable before attempting
+		// any transcoding. If supercompressionScheme is KTX_SS_BASIS_LZ and dfd
+		// colorModel is ETC1S (163) or if dfd colorModel is UASTCF (166)
+		// then texture must be transcoded.
+		var transcoder = ktx.getTexFormat() === TextureFormat.UASTC4x4
+			? new UastcImageTranscoder()
+			: new BasisLzEtc1sImageTranscoder();
+
+		ktx.initMipmaps( transcoder, this.transcoderConfig )
+			.then( function () {
+
+				var texture = new CompressedTexture(
+					ktx.mipmaps,
+					ktx.getWidth(),
+					ktx.getHeight(),
+					ktx.transcodedFormat,
+					UnsignedByteType
+				);
+
+				texture.encoding = ktx.getEncoding();
+				texture.premultiplyAlpha = ktx.getPremultiplyAlpha();
+				texture.minFilter = ktx.mipmaps.length === 1 ? LinearFilter : LinearMipmapLinearFilter;
+				texture.magFilter = LinearFilter;
+
+				onLoad( texture );
+
+			} )
+			.catch( onError );
+
+		return this;
+
+	}
+
+}
+
+class KTX2Container {
+
+	constructor ( basisModule, arrayBuffer ) {
+
+		this.basisModule = basisModule;
+		this.arrayBuffer = arrayBuffer;
+
+		this.mipmaps = null;
+		this.transcodedFormat = null;
+
+		// Confirm this is a KTX 2.0 file, based on the identifier in the first 12 bytes.
+		var idByteLength = 12;
+		var id = new Uint8Array( this.arrayBuffer, 0, idByteLength );
+		if ( id[ 0 ] !== 0xAB || // '´'
+				id[ 1 ] !== 0x4B ||  // 'K'
+				id[ 2 ] !== 0x54 ||  // 'T'
+				id[ 3 ] !== 0x58 ||  // 'X'
+				id[ 4 ] !== 0x20 ||  // ' '
+				id[ 5 ] !== 0x32 ||  // '2'
+				id[ 6 ] !== 0x30 ||  // '0'
+				id[ 7 ] !== 0xBB ||  // 'ª'
+				id[ 8 ] !== 0x0D ||  // '\r'
+				id[ 9 ] !== 0x0A ||  // '\n'
+				id[ 10 ] !== 0x1A || // '\x1A'
+				id[ 11 ] !== 0x0A    // '\n'
+			) {
+
+			throw new Error( 'THREE.KTX2Loader: Missing KTX 2.0 identifier.' );
+
+		}
+
+		// TODO(donmccurdy): If we need to support BE, derive this from typeSize.
+		var littleEndian = true;
+
+
+		///////////////////////////////////////////////////
+		// Header.
+		///////////////////////////////////////////////////
+
+		var headerByteLength = 17 * Uint32Array.BYTES_PER_ELEMENT;
+		var headerReader = new KTX2BufferReader( this.arrayBuffer, idByteLength, headerByteLength, littleEndian );
+
+		this.header = {
+
+			vkFormat: headerReader.nextUint32(),
+			typeSize: headerReader.nextUint32(),
+			pixelWidth: headerReader.nextUint32(),
+			pixelHeight: headerReader.nextUint32(),
+			pixelDepth: headerReader.nextUint32(),
+			arrayElementCount: headerReader.nextUint32(),
+			faceCount: headerReader.nextUint32(),
+			levelCount: headerReader.nextUint32(),
+
+			supercompressionScheme: headerReader.nextUint32(),
+
+			dfdByteOffset: headerReader.nextUint32(),
+			dfdByteLength: headerReader.nextUint32(),
+			kvdByteOffset: headerReader.nextUint32(),
+			kvdByteLength: headerReader.nextUint32(),
+			sgdByteOffset: headerReader.nextUint64(),
+			sgdByteLength: headerReader.nextUint64(),
+
+		};
+
+		if ( this.header.pixelDepth > 0 ) {
+
+			throw new Error( 'THREE.KTX2Loader: Only 2D textures are currently supported.' );
+
+		}
+
+		if ( this.header.arrayElementCount > 1 ) {
+
+			throw new Error( 'THREE.KTX2Loader: Array textures are not currently supported.' );
+
+		}
+
+		if ( this.header.faceCount > 1 ) {
+
+			throw new Error( 'THREE.KTX2Loader: Cube textures are not currently supported.' );
+
+		}
+
+
+		///////////////////////////////////////////////////
+		// Level index
+		///////////////////////////////////////////////////
+
+		var levelByteLength = this.header.levelCount * 3 * 8;
+		var levelReader = new KTX2BufferReader( this.arrayBuffer, idByteLength + headerByteLength, levelByteLength, littleEndian );
+
+		this.levels = [];
+
+		for ( var i = 0; i < this.header.levelCount; i ++ ) {
+
+			this.levels.push( {
+
+				byteOffset: levelReader.nextUint64(),
+				byteLength: levelReader.nextUint64(),
+				uncompressedByteLength: levelReader.nextUint64(),
+
+			} );
+
+		}
+
+
+		///////////////////////////////////////////////////
+		// Data Format Descriptor (DFD)
+		///////////////////////////////////////////////////
+
+		var dfdReader = new KTX2BufferReader(
+			this.arrayBuffer,
+			this.header.dfdByteOffset,
+			this.header.dfdByteLength,
+			littleEndian
+		);
+
+		const sampleStart = 6;
+		const sampleWords = 4;
+
+		this.dfd = {
+
+			vendorId: dfdReader.skip( 4 /* totalSize */ ).nextUint16(),
+			versionNumber: dfdReader.skip( 2 /* descriptorType */ ).nextUint16(),
+			descriptorBlockSize: dfdReader.nextUint16(),
+			colorModel: dfdReader.nextUint8(),
+			colorPrimaries: dfdReader.nextUint8(),
+			transferFunction: dfdReader.nextUint8(),
+			flags: dfdReader.nextUint8(),
+			texelBlockDimension: {
+				x: dfdReader.nextUint8() + 1,
+				y: dfdReader.nextUint8() + 1,
+				z: dfdReader.nextUint8() + 1,
+				w: dfdReader.nextUint8() + 1,
+			},
+			bytesPlane0: dfdReader.nextUint8(),
+			numSamples: 0,
+			samples: [],
+
+		};
+
+		this.dfd.numSamples = ( this.dfd.descriptorBlockSize / 4 - sampleStart ) / sampleWords;
+
+		dfdReader.skip( 7 /* bytesPlane[1-7] */ );
+
+		for ( var i = 0; i < this.dfd.numSamples; i++ ) {
+
+			this.dfd.samples[i] = {
+
+				channelID: dfdReader.skip( 3 /* bitOffset + bitLength */ ).nextUint8(),
+				// ... remainder not implemented.
+
+			};
+
+			dfdReader.skip( 12 /* samplePosition[0-3], lower, upper */ );
+
+		}
+
+		if ( this.header.vkFormat !== 0x00 /* VK_FORMAT_UNDEFINED */ &&
+			 ! ( this.header.supercompressionScheme === 1 /* BasisLZ */ ||
+				this.dfd.colorModel === DFDModel.UASTC ) ) {
+
+			throw new Error( 'THREE.KTX2Loader: Only Basis Universal supercompression is currently supported.' );
+
+		}
+
+
+		///////////////////////////////////////////////////
+		// Key/Value Data (KVD)
+		///////////////////////////////////////////////////
+
+		// Not implemented.
+		this.kvd = {};
+
+
+		///////////////////////////////////////////////////
+		// Supercompression Global Data (SGD)
+		///////////////////////////////////////////////////
+
+		this.sgd = {};
+
+		if ( this.header.sgdByteLength <= 0 ) return;
+
+		var sgdReader = new KTX2BufferReader(
+			this.arrayBuffer,
+			this.header.sgdByteOffset,
+			this.header.sgdByteLength,
+			littleEndian
+		);
+
+		this.sgd.endpointCount = sgdReader.nextUint16();
+		this.sgd.selectorCount = sgdReader.nextUint16();
+		this.sgd.endpointsByteLength = sgdReader.nextUint32();
+		this.sgd.selectorsByteLength = sgdReader.nextUint32();
+		this.sgd.tablesByteLength = sgdReader.nextUint32();
+		this.sgd.extendedByteLength = sgdReader.nextUint32();
+		this.sgd.imageDescs = [];
+		this.sgd.endpointsData = null;
+		this.sgd.selectorsData = null;
+		this.sgd.tablesData = null;
+		this.sgd.extendedData = null;
+
+		for ( var i = 0; i < this.header.levelCount; i ++ ) {
+
+			this.sgd.imageDescs.push( {
+
+				imageFlags: sgdReader.nextUint32(),
+				rgbSliceByteOffset: sgdReader.nextUint32(),
+				rgbSliceByteLength: sgdReader.nextUint32(),
+				alphaSliceByteOffset: sgdReader.nextUint32(),
+				alphaSliceByteLength: sgdReader.nextUint32(),
+
+			} );
+
+		}
+
+		var endpointsByteOffset = this.header.sgdByteOffset + sgdReader.offset;
+		var selectorsByteOffset = endpointsByteOffset + this.sgd.endpointsByteLength;
+		var tablesByteOffset = selectorsByteOffset + this.sgd.selectorsByteLength;
+		var extendedByteOffset = tablesByteOffset + this.sgd.tablesByteLength;
+
+		this.sgd.endpointsData = new Uint8Array( this.arrayBuffer, endpointsByteOffset, this.sgd.endpointsByteLength );
+		this.sgd.selectorsData = new Uint8Array( this.arrayBuffer, selectorsByteOffset, this.sgd.selectorsByteLength );
+		this.sgd.tablesData = new Uint8Array( this.arrayBuffer, tablesByteOffset, this.sgd.tablesByteLength );
+		this.sgd.extendedData = new Uint8Array( this.arrayBuffer, extendedByteOffset, this.sgd.extendedByteLength );
+
+	}
+
+	initMipmaps ( transcoder, config ) {
+
+		var TranscodeTarget = this.basisModule.TranscodeTarget;
+		var TextureFormat = this.basisModule.TextureFormat;
+		var ImageInfo = this.basisModule.ImageInfo;
+
+		var scope = this;
+
+		var mipmaps = [];
+		var width = this.getWidth();
+		var height = this.getHeight();
+		var texFormat = this.getTexFormat();
+		var hasAlpha = this.getAlpha();
+		var isVideo = false;
+
+		// For both ETC1S and UASTC4x4 formats.
+		if ( texFormat === TextureFormat.ETC1S ) {
+
+			var numEndpoints = this.sgd.endpointCount;
+			var numSelectors = this.sgd.selectorCount;
+			var endpoints = this.sgd.endpointsData;
+			var selectors = this.sgd.selectorsData;
+			var tables = this.sgd.tablesData;
+
+			transcoder.decodePalettes( numEndpoints, endpoints, numSelectors, selectors );
+			transcoder.decodeTables( tables );
+
+		}
+
+
+		var targetFormat;
+
+		if ( config.astcSupported ) {
+
+			targetFormat = TranscodeTarget.ASTC_4x4_RGBA;
+			this.transcodedFormat = RGBA_ASTC_4x4_Format;
+
+		} else if ( config.dxtSupported ) {
+
+			targetFormat = hasAlpha ? TranscodeTarget.BC3_RGBA : TranscodeTarget.BC1_RGB;
+			this.transcodedFormat = hasAlpha ? RGBA_S3TC_DXT5_Format : RGB_S3TC_DXT1_Format;
+
+		} else if ( config.pvrtcSupported ) {
+
+			targetFormat = hasAlpha ? TranscodeTarget.PVRTC1_4_RGBA : TranscodeTarget.PVRTC1_4_RGB;
+			this.transcodedFormat = hasAlpha ? RGBA_PVRTC_4BPPV1_Format : RGB_PVRTC_4BPPV1_Format;
+
+		} else if ( config.etc2Supported ) {
+
+			targetFormat = hasAlpha ? TranscodeTarget.ETC2_RGBA : TranscodeTarget.ETC1_RGB /* subset of ETC2 */;
+			this.transcodedFormat = hasAlpha ? RGBA_ETC2_EAC_Format : RGB_ETC2_Format;
+
+		} else if ( config.etc1Supported ) {
+
+			targetFormat = TranscodeTarget.ETC1_RGB;
+			this.transcodedFormat = RGB_ETC1_Format;
+
+		} else {
+
+			throw new Error( 'THREE.KTX2Loader: No suitable compressed texture format found.' );
+
+		}
+
+		if ( ! this.basisModule.isFormatSupported( targetFormat, texFormat ) ) {
+
+			throw new Error( 'THREE.KTX2Loader: Selected texture format not supported by current transcoder build.' );
+
+		}
+
+		var imageDescIndex = 0;
+
+		for ( var level = 0; level < this.header.levelCount; level ++ ) {
+
+			var levelWidth = width / Math.pow( 2, level );
+			var levelHeight = height / Math.pow( 2, level );
+
+			var numImagesInLevel = 1; // TODO(donmccurdy): Support cubemaps, arrays and 3D.
+			var imageOffsetInLevel = 0;
+			var imageInfo = new ImageInfo( texFormat, levelWidth, levelHeight, level );
+			var levelImageByteLength = imageInfo.numBlocksX * imageInfo.numBlocksY * this.dfd.bytesPlane0;
+
+			for ( var imageIndex = 0; imageIndex < numImagesInLevel; imageIndex ++ ) {
+
+				var result;
+				var encodedData;
+
+				if ( texFormat === TextureFormat.UASTC4x4 ) {
+
+					// UASTC
+
+					imageInfo.flags = 0;
+					imageInfo.rgbByteOffset = 0;
+					imageInfo.rgbByteLength = levelImageByteLength;
+					imageInfo.alphaByteOffset = 0;
+					imageInfo.alphaByteLength = 0;
+
+					encodedData = new Uint8Array( this.arrayBuffer, this.levels[ level ].byteOffset + imageOffsetInLevel, levelImageByteLength );
+
+					result = transcoder.transcodeImage( targetFormat, encodedData, imageInfo, 0, hasAlpha, isVideo );
+
+				} else {
+
+					// ETC1S
+
+					var imageDesc = this.sgd.imageDescs[ imageDescIndex++ ];
+
+					imageInfo.flags = imageDesc.imageFlags;
+					imageInfo.rgbByteOffset = 0;
+					imageInfo.rgbByteLength = imageDesc.rgbSliceByteLength;
+					imageInfo.alphaByteOffset = imageDesc.alphaSliceByteOffset > 0 ? imageDesc.rgbSliceByteLength : 0;
+					imageInfo.alphaByteLength = imageDesc.alphaSliceByteLength;
+
+					encodedData = new Uint8Array( this.arrayBuffer, this.levels[ level ].byteOffset + imageDesc.rgbSliceByteOffset, imageDesc.rgbSliceByteLength + imageDesc.alphaSliceByteLength );
+
+					result = transcoder.transcodeImage( targetFormat, encodedData, imageInfo, 0, isVideo );
+
+				}
+
+				if ( result.transcodedImage === undefined ) {
+
+					throw new Error( 'THREE.KTX2Loader: Unable to transcode image.' );
+
+				}
+
+				// Transcoded image is written in memory allocated by WASM. We could avoid copying
+				// the image by waiting until the image is uploaded to the GPU, then calling
+				// delete(). However, (1) we don't know if the user will later need to re-upload it
+				// e.g. after calling texture.clone(), and (2) this code will eventually be in a
+				// Web Worker, and transferring WASM's memory seems like a very bad idea.
+				var levelData = result.transcodedImage.get_typed_memory_view().slice();
+				result.transcodedImage.delete();
+
+				mipmaps.push( { data: levelData, width: levelWidth, height: levelHeight } );
+				imageOffsetInLevel += levelImageByteLength;
+
+			}
+
+		}
+
+		return new Promise( function ( resolve, reject ) {
+
+			scope.mipmaps = mipmaps;
+
+			resolve();
+
+		} );
+
+	}
+
+	getWidth () { return this.header.pixelWidth; }
+
+	getHeight () { return this.header.pixelHeight; }
+
+	getEncoding () {
+
+		return this.dfd.transferFunction === 2 /* KHR_DF_TRANSFER_SRGB */
+			? sRGBEncoding
+			: LinearEncoding;
+
+	}
+
+	getTexFormat () {
+
+		var TextureFormat = this.basisModule.TextureFormat;
+
+		return this.dfd.colorModel === DFDModel.UASTC ? TextureFormat.UASTC4x4 : TextureFormat.ETC1S;
+
+	}
+
+	getAlpha () {
+
+		var TextureFormat = this.basisModule.TextureFormat;
+
+		// TODO(donmccurdy): Handle all channelIDs (i.e. the R & R+G cases),
+		// choosing appropriate transcode target formats or providing queries
+		// for applications so they know what to do with the content.
+
+		if ( this.getTexFormat() === TextureFormat.UASTC4x4 ) {
+
+			// UASTC
+
+			if ( ( this.dfd.samples[ 0 ].channelID & 0xF ) === DFDChannel.UASTC.RGBA ) {
+
+				return true;
+
+			}
+
+			return false;
+
+		}
+
+		// ETC1S
+
+		if ( this.dfd.numSamples === 2 && ( this.dfd.samples[ 1 ].channelID & 0xF ) === DFDChannel.ETC1S.AAA ) {
+
+			return true;
+
+		}
+
+		return false;
+
+	}
+
+	getPremultiplyAlpha () {
+
+		return !! ( this.dfd.flags & 1 /* KHR_DF_FLAG_ALPHA_PREMULTIPLIED */ );
+
+	}
+
+}
+
+class KTX2BufferReader {
+
+	constructor ( arrayBuffer, byteOffset, byteLength, littleEndian ) {
+
+		this.dataView = new DataView( arrayBuffer, byteOffset, byteLength );
+		this.littleEndian = littleEndian;
+		this.offset = 0;
+
+	}
+
+	nextUint8 () {
+
+		var value = this.dataView.getUint8( this.offset, this.littleEndian );
+
+		this.offset += 1;
+
+		return value;
+
+	}
+
+	nextUint16 () {
+
+		var value = this.dataView.getUint16( this.offset, this.littleEndian );
+
+		this.offset += 2;
+
+		return value;
+
+	}
+
+	nextUint32 () {
+
+		var value = this.dataView.getUint32( this.offset, this.littleEndian );
+
+		this.offset += 4;
+
+		return value;
+
+	}
+
+	nextUint64 () {
+
+		// https://stackoverflow.com/questions/53103695/
+		var left =  this.dataView.getUint32( this.offset, this.littleEndian );
+		var right = this.dataView.getUint32( this.offset + 4, this.littleEndian );
+		var value = this.littleEndian ? left + ( 2 ** 32 * right ) : ( 2 ** 32 * left ) + right;
+
+		if ( ! Number.isSafeInteger( value ) ) {
+
+			console.warn( 'THREE.KTX2Loader: ' + value + ' exceeds MAX_SAFE_INTEGER. Precision may be lost.' );
+
+		}
+
+		this.offset += 8;
+
+		return value;
+
+	}
+
+	skip ( bytes ) {
+
+		this.offset += bytes;
+
+		return this;
+
+	}
+
+}
+
+export { KTX2Loader };

BIN
examples/textures/compressed/sample_etc1s.ktx2


BIN
examples/textures/compressed/sample_uastc.ktx2


+ 119 - 0
examples/webgl_loader_texture_ktx2.html

@@ -0,0 +1,119 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgl - KTX2 texture loader</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+		<link type="text/css" rel="stylesheet" href="main.css">
+	</head>
+	<body>
+
+		<div id="info">
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - webgl - KTX2 texture loader<br />
+			<a href="http://github.khronos.org/KTX-Specification/" target="_blank" rel="noopener">KTX2</a> with
+			<a href="https://github.com/binomialLLC/basis_universal" target="_blank">Basis Universal GPU Texture Codec</a>
+		</div>
+
+		<script src="./js/libs/basis/msc_basis_transcoder.js"></script>
+
+		<script type="module">
+
+			import * as THREE from '../build/three.module.js';
+
+			import { KTX2Loader } from './jsm/loaders/KTX2Loader.js';
+			import { OrbitControls } from './jsm/controls/OrbitControls.js';
+
+			var width = window.innerWidth;
+			var height = window.innerHeight;
+
+			var renderer = new THREE.WebGLRenderer( { antialias: true } );
+			renderer.setSize( width, height );
+			renderer.outputEncoding = THREE.sRGBEncoding;
+			document.body.appendChild( renderer.domElement );
+
+			var scene = new THREE.Scene();
+			scene.background = new THREE.Color( 0xF0F0F0 );
+
+			var camera = new THREE.PerspectiveCamera( 60, width / height, 0.1, 100 );
+			camera.position.set( 2, 1.5, 1 );
+			camera.lookAt( scene.position );
+			scene.add(camera);
+
+			var controls = new OrbitControls( camera, renderer.domElement );
+			controls.autoRotate = true;
+
+			// PlaneBufferGeometry UVs assume flipY=true, which compressed textures don't support.
+			var geometry = flipY( new THREE.PlaneBufferGeometry() );
+			var material = new THREE.MeshBasicMaterial( {
+				color: 0xFFFFFF,
+				side: THREE.DoubleSide
+			} );
+			var mesh = new THREE.Mesh(geometry, material);
+			scene.add(mesh);
+
+			var formatStrings = {
+					[THREE.RGBA_ASTC_4x4_Format]: "RGBA_ASTC_4x4",
+					[THREE.RGB_S3TC_DXT1_Format]: "RGB_S3TC_DXT1",
+					[THREE.RGBA_S3TC_DXT5_Format]: "RGBA_S3TC_DXT5",
+					[THREE.RGB_PVRTC_4BPPV1_Format]: "RGB_PVRTC_4BPPV1",
+					[THREE.RGBA_PVRTC_4BPPV1_Format]: "RGBA_PVRTC_4BPPV1",
+					[THREE.RGB_ETC1_Format]: "RGB_ETC1",
+					[THREE.RGB_ETC2_Format]: "RGB_ETC2",
+					[THREE.RGBA_ETC2_EAC_Format]: "RGB_ETC2_EAC",
+			};
+
+			// Samples: sample_etc1s.ktx2, sample_uastc.ktx2
+			new KTX2Loader()
+				.detectSupport( renderer )
+				.load( './textures/compressed/sample_uastc.ktx2', ( texture ) => {
+
+					console.info( `transcoded to ${formatStrings[texture.format]}` );
+
+					material.map = texture;
+
+					material.needsUpdate = true;
+
+				}, ( p ) => console.log( `...${p}` ), ( e ) => console.error( e ) );
+
+			animate();
+
+			function animate() {
+
+				requestAnimationFrame( animate );
+
+				controls.update();
+
+				renderer.render( scene, camera );
+
+			}
+
+			window.addEventListener( 'resize', () => {
+
+				var width = window.innerWidth;
+				var height = window.innerHeight;
+
+				camera.aspect = width / height;
+				camera.updateProjectionMatrix();
+				renderer.setSize( width, height );
+
+			}, false );
+
+			/** Correct UVs to be compatible with `flipY=false` textures. */
+			function flipY ( geometry ) {
+
+				var uv = geometry.attributes.uv;
+
+				for ( var i = 0; i < uv.count; i ++ ) {
+
+					uv.setY( i, 1 - uv.getY( i ) );
+
+				}
+
+				return geometry;
+
+			}
+
+		</script>
+
+	</body>
+</html>

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels