This commit is contained in:
2026-06-14 19:09:18 +01:00
parent 14bd1a9271
commit 13fa90a0e9
3958 changed files with 999286 additions and 4 deletions
+9
View File
@@ -0,0 +1,9 @@
# Copyright 2020 The Khronos Group Inc.
# SPDX-License-Identifier: Apache-2.0
libktx.js
libktx.wasm
libktx_read.js
libktx_read.wasm
msc_basis_transcoder.js
msc_basis_transcoder.wasm
File diff suppressed because it is too large Load Diff
+38
View File
@@ -0,0 +1,38 @@
<!doctype html>
<!--
Copyright 2020 Mark Callow
SPDX-License-Identifier: CC0-1.0
-->
<html lang="en">
<head>
<meta charset="utf-8">
<title>JS Binding Tests & Demos</title>
<link rel="shortcut icon" href="ktx_favicon.ico" type="image/x-icon" />
<style>
body {
max-width: 500px;
}
</style>
</head>
<h1>KTX2 with Basis WebGL tests</h1>
<p>
Example webpages to test the libktx Javascript wrappers and WASM
support for transcoding Basis Universal compressed textures. All
tests require WebAssembly and WebGL support.
</p>
<ul>
<li><a href="libktx-webgl">libktx and WebGL</a>: tests the
functionality of the read-write Javascript binding for libktx,
which has been compiled to Javascript using Emscripten. Among other
things, it reads a .png file, creates a KTX2 texture, encodes it to
the universal Basis-LZ format, transcodes it and renders it using
WebGL.</li>
<li><a href="libktx-read-webgl">Read only libktx and WebGL</a>: tests
the read-only JS binding to libktx and its embedded Basisu transcoder,
which are compiled to .wasm. It reads a KTX v2 file containing a Basis
Universal compressed texture and renders it using WebGL.</li>
<li><a href="llt-three/">transcodeImage with Pure JS KTX2 loader for three.js</a>:
transcodeImage is a BasisU transcoder method independent of .basis or any container
format written in C++ and compiled to .wasm.</li>
</ul>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

File diff suppressed because it is too large Load Diff
@@ -0,0 +1,491 @@
/**
* @author Don McCurdy / https://www.donmccurdy.com
* @author Austin Eng / https://github.com/austinEng
* @author Shrek Shao / https://github.com/shrekshao
* @author Mark Callow / https:://github.com/MarkCallow
*/
/**
* Loader for Texture Payloads in .ktx files including Basis Universal textures.
*
* Basis Universal is a "supercompressed" GPU texture and texture video
* compression system that outputs a highly compressed intermediate file format
* (.basis) that can be quickly transcoded to a wide variety of GPU texture
* compression formats.
*
* This loader parallelizes the transcoding process across a configurable number
* of web workers, before transferring the transcoded compressed texture back
* to the main thread.
*/
THREE.KTXTextureLoader = function ( manager ) {
THREE.Loader.call( this, manager );
this.useAlpha = true;
this.transcoderPath = '';
this.transcoderBinary = null;
this.transcoderPending = null;
this.workerLimit = 4;
this.workerPool = [];
this.workerNextTaskID = 1;
this.workerSourceURL = '';
this.workerConfig = {
format: null,
useAlpha: true,
astcSupported: false,
etcSupported: false,
dxtSupported: false,
pvrtcSupported: false,
};
};
THREE.KTXTextureLoader.prototype = Object.assign( Object.create( THREE.Loader.prototype ), {
constructor: THREE.KTXTextureLoader,
setTranscoderPath: function ( path ) {
this.transcoderPath = path;
return this;
},
setWorkerLimit: function ( workerLimit ) {
this.workerLimit = workerLimit;
return this;
},
detectSupport: function ( renderer ) {
var config = this.workerConfig;
config.astcSupported = !! renderer.extensions.get( 'WEBGL_compressed_texture_astc' );
config.etcSupported = !! renderer.extensions.get( 'WEBGL_compressed_texture_etc1' );
config.dxtSupported = !! renderer.extensions.get( 'WEBGL_compressed_texture_s3tc' );
config.pvrtcSupported = !! renderer.extensions.get( 'WEBGL_compressed_texture_pvrtc' )
|| !! renderer.extensions.get( 'WEBKIT_WEBGL_compressed_texture_pvrtc' );
if (config.astcSupported) {
config.format = THREE.KTXTextureLoader.BASIS_FORMAT.cTFASTC_4x4;
} else if ( config.dxtSupported ) {
config.format = this.useAlpha ? THREE.KTXTextureLoader.BASIS_FORMAT.cTFBC3 : THREE.KTXTextureLoader.BASIS_FORMAT.cTFBC1;
} else if ( config.pvrtcSupported ) {
config.format = this.useAlpha ? THREE.KTXTextureLoader.BASIS_FORMAT.cTFPVRTC1_4_RGBA : THREE.KTXTextureLoader.BASIS_FORMAT.cTFPVRTC1_4_RGB;
} else if ( config.etcSupported ) {
config.format = THREE.KTXTextureLoader.BASIS_FORMAT.cTFETC1;
} else {
throw new Error( 'THREE.KTXTextureLoader: No suitable compressed texture format found.' );
}
return this;
},
load: function ( url, onLoad, onProgress, onError ) {
var loader = new THREE.FileLoader( this.manager );
loader.setResponseType( 'arraybuffer' );
loader.load( url, ( buffer ) => {
this._createTexture( buffer )
.then( onLoad )
.catch( onError );
}, onProgress, onError );
},
/**
* @param {ArrayBuffer} buffer
* @return {Promise<THREE.CompressedTexture>}
*/
_createTexture: function ( buffer ) {
var worker;
var taskID;
var texturePending = this._getWorker()
.then( ( _worker ) => {
worker = _worker;
taskID = this.workerNextTaskID ++;
return new Promise( ( resolve, reject ) => {
worker._callbacks[ taskID ] = { resolve, reject };
worker._taskCosts[ taskID ] = buffer.byteLength;
worker._taskLoad += worker._taskCosts[ taskID ];
worker.postMessage( { type: 'transcode', id: taskID, buffer }, [ buffer ] );
} );
} )
.then( ( message ) => {
var config = this.workerConfig;
var { width, height, hasAlpha, mipmaps } = message;
var texture;
switch(config.format) {
case THREE.KTXTextureLoader.BASIS_FORMAT.cTFASTC_4x4:
texture = new THREE.CompressedTexture( mipmaps, width, height, THREE.KTXTextureLoader.COMPRESSED_RGBA_ASTC_4x4_KHR );
break;
case THREE.KTXTextureLoader.BASIS_FORMAT.cTFBC1:
case THREE.KTXTextureLoader.BASIS_FORMAT.cTFBC3:
texture = new THREE.CompressedTexture( mipmaps, width, height, THREE.KTXTextureLoader.DXT_FORMAT_MAP[ config.format ], THREE.UnsignedByteType );
break;
case THREE.KTXTextureLoader.BASIS_FORMAT.cTFETC1:
texture = new THREE.CompressedTexture( mipmaps, width, height, THREE.RGB_ETC1_Format );
break;
case THREE.KTXTextureLoader.BASIS_FORMAT.cTFPVRTC1_4_RGB:
texture = new THREE.CompressedTexture( mipmaps, width, height, THREE.RGB_PVRTC_4BPPV1_Format );
break;
case THREE.KTXTextureLoader.BASIS_FORMAT.cTFPVRTC1_4_RGBA:
texture = new THREE.CompressedTexture( mipmaps, width, height, THREE.RGBA_PVRTC_4BPPV1_Format );
break;
default:
throw new Error( 'THREE.KTXTextureLoader: No supported format available.' );
}
texture.minFilter = mipmaps.length === 1 ? THREE.LinearFilter : THREE.LinearMipmapLinearFilter;
texture.magFilter = THREE.LinearFilter;
texture.generateMipmaps = false;
texture.needsUpdate = true;
return texture;
} );
texturePending
.finally( () => {
if ( worker && taskID ) {
worker._taskLoad -= worker._taskCosts[ taskID ];
delete worker._callbacks[ taskID ];
delete worker._taskCosts[ taskID ];
}
} );
return texturePending;
},
_initTranscoder: function () {
if ( ! this.transcoderBinary ) {
// Load transcoder wrapper.
var jsLoader = new THREE.FileLoader( this.manager );
jsLoader.setPath( this.transcoderPath );
var jsContent = new Promise( ( resolve, reject ) => {
jsLoader.load( 'libktx.js', resolve, undefined, reject );
} );
// Load transcoder WASM binary.
var binaryLoader = new THREE.FileLoader( this.manager );
binaryLoader.setPath( this.transcoderPath );
binaryLoader.setResponseType( 'arraybuffer' );
var binaryContent = new Promise( ( resolve, reject ) => {
binaryLoader.load( 'libktx.wasm', resolve, undefined, reject );
} );
this.transcoderPending = Promise.all( [ jsContent, binaryContent ] )
.then( ( [ jsContent, binaryContent ] ) => {
var fn = THREE.KTXTextureLoader.BasisWorker.toString();
var body = [
'/* basis_transcoder.js */',
jsContent,
'/* worker */',
fn.substring( fn.indexOf( '{' ) + 1, fn.lastIndexOf( '}' ) )
].join( '\n' );
this.workerSourceURL = URL.createObjectURL( new Blob( [ body ] ) );
this.transcoderBinary = binaryContent;
} );
}
return this.transcoderPending;
},
_getWorker: function () {
return this._initTranscoder().then( () => {
if ( this.workerPool.length < this.workerLimit ) {
var worker = new Worker( this.workerSourceURL );
worker._callbacks = {};
worker._taskCosts = {};
worker._taskLoad = 0;
this.workerConfig.useAlpha = this.useAlpha;
worker.postMessage( {
type: 'init',
config: this.workerConfig,
transcoderBinary: this.transcoderBinary,
} );
worker.onmessage = function ( e ) {
var message = e.data;
switch ( message.type ) {
case 'transcode':
worker._callbacks[ message.id ].resolve( message );
break;
case 'error':
worker._callbacks[ message.id ].reject( message );
break;
default:
console.error( 'THREE.KTXTextureLoader: Unexpected message, "' + message.type + '"' );
}
};
this.workerPool.push( worker );
} else {
this.workerPool.sort( function ( a, b ) {
return a._taskLoad > b._taskLoad ? - 1 : 1;
} );
}
return this.workerPool[ this.workerPool.length - 1 ];
} );
},
dispose: function () {
for ( var i = 0; i < this.workerPool.length; i ++ ) {
this.workerPool[ i ].terminate();
}
this.workerPool.length = 0;
return this;
}
} );
/* CONSTANTS */
THREE.KTXTextureLoader.BASIS_FORMAT = {
cTFETC1: 0,
cTFETC2: 1,
cTFBC1: 2,
cTFBC3: 3,
cTFBC4: 4,
cTFBC5: 5,
cTFBC7_M6_OPAQUE_ONLY: 6,
cTFBC7_M5: 7,
cTFPVRTC1_4_RGB: 8,
cTFPVRTC1_4_RGBA: 9,
cTFASTC_4x4: 10,
cTFATC_RGB: 11,
cTFATC_RGBA_INTERPOLATED_ALPHA: 12,
cTFRGBA32: 13,
cTFRGB565: 14,
cTFBGR565: 15,
cTFRGBA4444: 16,
};
// DXT formats, from:
// http://www.khronos.org/registry/webgl/extensions/WEBGL_compressed_texture_s3tc/
THREE.KTXTextureLoader.DXT_FORMAT = {
COMPRESSED_RGB_S3TC_DXT1_EXT: 0x83F0,
COMPRESSED_RGBA_S3TC_DXT1_EXT: 0x83F1,
COMPRESSED_RGBA_S3TC_DXT3_EXT: 0x83F2,
COMPRESSED_RGBA_S3TC_DXT5_EXT: 0x83F3,
};
THREE.KTXTextureLoader.DXT_FORMAT_MAP = {};
THREE.KTXTextureLoader.DXT_FORMAT_MAP[ THREE.KTXTextureLoader.BASIS_FORMAT.cTFBC1 ] =
THREE.KTXTextureLoader.DXT_FORMAT.COMPRESSED_RGB_S3TC_DXT1_EXT;
THREE.KTXTextureLoader.DXT_FORMAT_MAP[ THREE.KTXTextureLoader.BASIS_FORMAT.cTFBC3 ] =
THREE.KTXTextureLoader.DXT_FORMAT.COMPRESSED_RGBA_S3TC_DXT5_EXT;
// ASTC formats, from:
// https://www.khronos.org/registry/webgl/extensions/WEBGL_compressed_texture_astc/
THREE.KTXTextureLoader.COMPRESSED_RGBA_ASTC_4x4_KHR = 0x93B0;
/* WEB WORKER */
THREE.KTXTextureLoader.BasisWorker = function () {
var config;
var transcoderPending;
var _BasisFile;
onmessage = function ( e ) {
var message = e.data;
switch ( message.type ) {
case 'init':
config = message.config;
init( message.transcoderBinary );
break;
case 'transcode':
transcoderPending.then( () => {
try {
var { width, height, hasAlpha, mipmaps } = transcode( message.buffer );
var buffers = [];
for ( var i = 0; i < mipmaps.length; ++ i ) {
buffers.push( mipmaps[ i ].data.buffer );
}
self.postMessage( { type: 'transcode', id: message.id, width, height, hasAlpha, mipmaps }, buffers );
} catch ( error ) {
console.error( error );
self.postMessage( { type: 'error', id: message.id, error: error.message } );
}
} );
break;
}
};
function init( wasmBinary ) {
var m;
transcoderPending = new Promise( ( resolve ) => {
m = { wasmBinary, onRuntimeInitialized: resolve };
BASIS(m);
} ).then( () => {
var { BasisFile, initializeBasis } = m;
_BasisFile = BasisFile;
initializeBasis();
} );
}
function transcode( buffer ) {
var basisFile = new _BasisFile( new Uint8Array( buffer ) );
var width = basisFile.getImageWidth( 0, 0 );
var height = basisFile.getImageHeight( 0, 0 );
var levels = basisFile.getNumLevels( 0 );
var hasAlpha = basisFile.getHasAlpha();
function cleanup() {
basisFile.close();
basisFile.delete();
}
if ( ! width || ! height || ! levels ) {
cleanup();
throw new Error( 'THREE.KTXTextureLoader: Invalid .basis file' );
}
if ( ! basisFile.startTranscoding() ) {
cleanup();
throw new Error( 'THREE.KTXTextureLoader: .startTranscoding failed' );
}
var mipmaps = [];
for ( var mip = 0; mip < levels; mip ++ ) {
var mipWidth = basisFile.getImageWidth( 0, mip );
var mipHeight = basisFile.getImageHeight( 0, mip );
var dst = new Uint8Array( basisFile.getImageTranscodedSizeInBytes( 0, mip, config.format ) );
var status = basisFile.transcodeImage(
dst,
0,
mip,
config.format,
config.useAlpha,
0
);
if ( ! status ) {
cleanup();
throw new Error( 'THREE.KTXTextureLoader: .transcodeImage failed.' );
}
mipmaps.push( { data: dst, width: mipWidth, height: mipHeight } );
}
cleanup();
return { width, height, hasAlpha, mipmaps };
}
};
+12
View File
@@ -0,0 +1,12 @@
## Not functional. Under Development.
## Credits
Based on BasisU gltf sample.
* Contributors:
* [Don McCurdy](https://www.donmccurdy.com)
* [Austin Eng](https://github.com/austinEng)
* [Shrek Shao](https://github.com/shrekshao)
* [Mark Callow](https://github.com/MarkCallow)
* Made with [three.js](https://threejs.org/).
+254
View File
@@ -0,0 +1,254 @@
{
"accessors": [
{
"byteOffset": 0,
"componentType": 5126,
"type": "VEC3",
"count": 55381,
"min": [
-527.3590087890625,
-104.39354705810547,
-495.3590087890625
],
"max": [
599.3590087890625,
-33.72303009033203,
561.3590087890625
],
"bufferView": 0,
"name": "mesh-0-0_Accessor_POSITION"
},
{
"byteOffset": 0,
"componentType": 5126,
"type": "VEC2",
"count": 55381,
"min": [
0.0000017028407910402166,
0.00000289904824057885
],
"max": [
0.9993386268615723,
0.9999556541442871
],
"bufferView": 1,
"name": "mesh-0-0_Accessor_TEXCOORD_0"
},
{
"byteOffset": 0,
"componentType": 5123,
"type": "SCALAR",
"count": 151158,
"bufferView": 4,
"name": "mesh-0-0_Accessor_indices"
},
{
"byteOffset": 0,
"componentType": 5126,
"type": "VEC3",
"count": 30806,
"min": [
-527.3590087890625,
-105.48443603515625,
-495.3590087890625
],
"max": [
599.3590087890625,
-26.287290573120117,
561.3590087890625
],
"bufferView": 2,
"name": "mesh-1-0_Accessor_POSITION"
},
{
"byteOffset": 0,
"componentType": 5126,
"type": "VEC2",
"count": 30806,
"min": [
0.00000964943137660157,
0.000003775880941248033
],
"max": [
0.9999449253082275,
0.9999914169311523
],
"bufferView": 3,
"name": "mesh-1-0_Accessor_TEXCOORD_0"
},
{
"byteOffset": 302316,
"componentType": 5123,
"type": "SCALAR",
"count": 58476,
"bufferView": 4,
"name": "mesh-1-0_Accessor_indices"
}
],
"asset": {
"generator": "obj2gltf",
"version": "2.0"
},
"buffers": [
{
"name": "mesh-0-0_Buffer_POSITION",
"byteLength": 2143008,
"uri": "mesh-0-0_Buffer_POSITION.bin"
}
],
"bufferViews": [
{
"buffer": 0,
"byteLength": 664572,
"byteOffset": 0,
"target": 34962,
"name": "bufferView_0",
"byteStride": 12
},
{
"buffer": 0,
"byteLength": 443048,
"byteOffset": 664572,
"target": 34962,
"name": "bufferView_0",
"byteStride": 8
},
{
"buffer": 0,
"byteLength": 369672,
"byteOffset": 1107620,
"target": 34962,
"name": "bufferView_0",
"byteStride": 12
},
{
"buffer": 0,
"byteLength": 246448,
"byteOffset": 1477292,
"target": 34962,
"name": "bufferView_0",
"byteStride": 8
},
{
"buffer": 0,
"byteLength": 419268,
"byteOffset": 1723740,
"target": 34963,
"name": "bufferView_1"
}
],
"materials": [
{
"name": "Texture",
"pbrMetallicRoughness": {
"baseColorTexture": {
"index": 0,
"texCoord": 0
},
"metallicFactor": 0,
"roughnessFactor": 1,
"baseColorFactor": [
1,
1,
1,
1
]
},
"emissiveTexture": {
"index": 0,
"texCoord": 0
},
"alphaMode": "OPAQUE",
"doubleSided": false,
"emissiveFactor": [
0,
0,
0
]
}
],
"meshes": [
{
"primitives": [
{
"attributes": {
"POSITION": 0,
"TEXCOORD_0": 1
},
"indices": 2,
"material": 0,
"mode": 4
}
],
"name": "mesh-split_1"
},
{
"primitives": [
{
"attributes": {
"POSITION": 3,
"TEXCOORD_0": 4
},
"indices": 5,
"material": 0,
"mode": 4
}
],
"name": "mesh-split_2"
}
],
"nodes": [
{
"children": [
1
],
"name": "rootNode_0",
"mesh": 0
},
{
"mesh": 1
}
],
"samplers": [
{
"magFilter": 9729,
"minFilter": 9729,
"wrapS": 33071,
"wrapT": 33071,
"name": "sampler_0"
}
],
"scene": 0,
"scenes": [
{
"nodes": [
0
],
"name": "scene"
}
],
"textures": [
{
"sampler": 0,
"name": "textureAtlas",
"extensions": {
"GOOGLE_texture_basis": {
"source": 0
}
}
}
],
"images": [
{
"name": "textureAtlasImage",
"mimeType": "image/basis",
"uri": "textureAtlasImage.basis"
}
],
"extensionsUsed": [
"GOOGLE_texture_basis"
],
"extensionsRequired": [
"GOOGLE_texture_basis"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,193 @@
{
"accessors" : [
{
"bufferView" : 0,
"byteOffset" : 0,
"componentType" : 5123,
"count" : 36,
"max" : [
35
],
"min" : [
0
],
"type" : "SCALAR"
},
{
"bufferView" : 1,
"byteOffset" : 0,
"componentType" : 5126,
"count" : 36,
"max" : [
1.000000,
1.000000,
1.000001
],
"min" : [
-1.000000,
-1.000000,
-1.000000
],
"type" : "VEC3"
},
{
"bufferView" : 2,
"byteOffset" : 0,
"componentType" : 5126,
"count" : 36,
"max" : [
1.000000,
1.000000,
1.000000
],
"min" : [
-1.000000,
-1.000000,
-1.000000
],
"type" : "VEC3"
},
{
"bufferView" : 3,
"byteOffset" : 0,
"componentType" : 5126,
"count" : 36,
"max" : [
1.000000,
-0.000000,
-0.000000,
1.000000
],
"min" : [
0.000000,
-0.000000,
-1.000000,
-1.000000
],
"type" : "VEC4"
},
{
"bufferView" : 4,
"byteOffset" : 0,
"componentType" : 5126,
"count" : 36,
"max" : [
1.000000,
1.000000
],
"min" : [
-1.000000,
-1.000000
],
"type" : "VEC2"
}
],
"asset" : {
"generator" : "VKTS glTF 2.0 exporter",
"version" : "2.0"
},
"bufferViews" : [
{
"buffer" : 0,
"byteLength" : 72,
"byteOffset" : 0,
"target" : 34963
},
{
"buffer" : 0,
"byteLength" : 432,
"byteOffset" : 72,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 432,
"byteOffset" : 504,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 576,
"byteOffset" : 936,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 288,
"byteOffset" : 1512,
"target" : 34962
}
],
"buffers" : [
{
"byteLength" : 1800,
"uri" : "Cube.bin"
}
],
"images" : [
{
"uri" : "Cube_BaseColor.png"
},
{
"uri" : "Cube_MetallicRoughness.png"
}
],
"materials" : [
{
"name" : "Cube",
"pbrMetallicRoughness" : {
"baseColorTexture" : {
"index" : 0
},
"metallicRoughnessTexture" : {
"index" : 1
}
}
}
],
"meshes" : [
{
"name" : "Cube",
"primitives" : [
{
"attributes" : {
"NORMAL" : 2,
"POSITION" : 1,
"TANGENT" : 3,
"TEXCOORD_0" : 4
},
"indices" : 0,
"material" : 0,
"mode" : 4
}
]
}
],
"nodes" : [
{
"mesh" : 0,
"name" : "Cube"
}
],
"samplers" : [
{}
],
"scene" : 0,
"scenes" : [
{
"nodes" : [
0
]
}
],
"textures" : [
{
"sampler" : 0,
"source" : 0
},
{
"sampler" : 0,
"source" : 1
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 871 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 B

Binary file not shown.
+130
View File
@@ -0,0 +1,130 @@
<!DOCTYPE html>
<head>
<script src="https://cdn.jsdelivr.net/npm/three@v0.108.0"></script>
<script src="GLTFLoader.js"></script>
<script src="KTXTextureLoader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@v0.104.0/examples/js/controls/OrbitControls.js"></script>
<!-- <script src="../transcoder/build/basis_transcoder.js"></script> -->
<style>
html, body { width:100%; height:100%; margin:0; padding:0; }
canvas { display:block; }
#panel { position: absolute; top: 10px; left: 10px; color: white; background-color:rgba(0.3, 0.3, 0.3, 0.3); padding: 0.5em; max-width: 400px;}
</style>
</head>
<body>
<div id="panel">
<strong>Transcoding a KTX2 file with a BasisU PayLoad glTF Demo</strong>
<div id="log"></div>
</div>
<script>
const renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.gammaOutput = true;
renderer.gammaFactor = 2.2;
const scene = new THREE.Scene();
scene.background = new THREE.Color( 0xf0f0f0 );
const light = new THREE.AmbientLight();
scene.add( light );
const light2 = new THREE.PointLight();
scene.add( light2 );
const camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 0.1, 100 );
camera.position.set( 8, 6, 5 );
camera.lookAt( new THREE.Vector3( 0, -2, 0 ) );
const controls = new THREE.OrbitControls( camera, renderer.domElement );
controls.autoRotate = true;
// Create BasisTextureLoader and detect supported target formats.
const ktxLoader = new THREE.KTXTextureLoader();
//basisLoader.setTranscoderPath( '../transcoder/build/' );
ktxLoader.setTranscoderPath( '..' );
//ktxLoader.useAlpha = false;
ktxLoader.detectSupport( renderer );
let formatName = 'Unknown format';
switch(basisLoader.workerConfig.format)
{
case THREE.BasisTextureLoader.BASIS_FORMAT.cTFASTC_4x4:
formatName = 'ASTC';
break;
case THREE.BasisTextureLoader.BASIS_FORMAT.cTFBC1:
formatName = 'BC1';
break;
case THREE.BasisTextureLoader.BASIS_FORMAT.cTFBC3:
formatName = 'BC3';
break;
case THREE.BasisTextureLoader.BASIS_FORMAT.cTFPVRTC1_4_RGB:
case THREE.BasisTextureLoader.BASIS_FORMAT.cTFPVRTC1_4_RGBA:
formatName = 'PVRTC';
break;
case THREE.BasisTextureLoader.BASIS_FORMAT.cTFETC1:
formatName = 'ETC1';
break;
default:
break;
}
log(`Transcode to: <strong>${formatName}</strong>`);
// Register KTXTextureLoader for .ktx and .ktx2 extension.
//THREE.Loader.Handlers.add( /\.basis$/, basisLoader );
THREE.Loader.Handlers.add( /\.ktx$/, ktxTexture );
THREE.Loader.Handlers.add( /\.ktx2$/, ktxTexture );
// Create GLTFLoader, load model, and render.
const loader = new THREE.GLTFLoader();
loader.load( 'assets/Cube.gltf', ( gltf ) => {
const model = gltf.scene;
model.scale.set( 0.01, 0.01, 0.01 );
scene.add( model );
document.body.appendChild( renderer.domElement );
animate();
}, undefined, ( e ) => console.error( e ) );
// Main render loop.
function animate() {
requestAnimationFrame( animate );
controls.update();
renderer.render( scene, camera );
}
// Support viewport resizing.
window.addEventListener( 'resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
}, false );
function log(s) {
const div = document.createElement('div');
div.innerHTML = s;
document.getElementById('log').appendChild(div);
}
</script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 563 KiB

@@ -0,0 +1,48 @@
<!doctype html>
<!-- Copyright 2024 Mark Callow.
SPDX-License-Identifier: Apache-2.0 -->
<html lang="en">
<head>
<meta charset="utf-8">
<title>libktx.js Demo with WebGL</title>
<link rel="stylesheet" href="../webgl.css" type="text/css">
<link rel="shortcut icon" href="../ktx_favicon.ico" type="image/x-icon" />
<style>
body {
/* Set margins to center the page. */
margin: 2em auto 2em auto;
max-width: 55em;
}
/* Center the canvas too. */
canvas {
display: block;
margin-left: auto;
margin-right: auto;
}
#panel {
color: white;
background-color:rgba(0.3, 0.3, 0.3, 0.3);
padding: 0.5em;
max-width: 55em;
}
</style>
</head>
<body>
<div id="panel">
<h2>libktx Read-only Javascript Binding Test</h2>
<p>This test uses the read-only Javascript binding for libktx,
which has been compiled to Javascript using Emscripten. It uses a
KTX2 file with a Basis Universal-compressed payload. It has
transcoded the images to <b id='format'>FORMAT</b>.
</p>
</div>
<p></p>
<canvas id="glcanvas" width="640" height="480"></canvas>
</body>
<script src="../gl-matrix.js"></script>
<script src="../libktx_read.js"></script>
<script src="libktx-read-test.js"></script>
</html>
Binary file not shown.
@@ -0,0 +1,733 @@
/*
* Copyright 2015-2020 MDN Contributors
* Copyright 2024 Mark Callow
* SPDX-License-Identifier: CC0-1.0
*/
/*
This code originated from sample 7 in the MDN WebGL examples
at https://github.com/mdn/webgl-examples which is licensed
under Creative Commons Zero v1.0 Universal. Modifications
made here are also licensed under CC0v1.
*/
var cubeRotation = 0.0;
var gl;
var texture;
var astcSupported = false;
var etcSupported = false;
var dxtSupported = false;
var pvrtcSupported = false;
//
// Start here
//
const canvas = document.querySelector('#glcanvas');
gl = canvas.getContext('webgl2');
// If we don't have a GL context, give up now
if (!gl) {
alert('Unable to initialize WebGL. Your browser or machine may not support it.');
} else {
createKtxReadModule({preinitializedWebGLContext: gl}).then(instance => {
window.ktx = instance;
// Make existing WebGL context current for Emscripten OpenGL.
ktx.GL.makeContextCurrent(
ktx.GL.createContext(document.getElementById("glcanvas"),
{ majorVersion: 2.0 })
);
main()
});
}
function main() {
texture = loadTexture(gl, 'ktx_app_basis.ktx2');
astcSupported = !!gl.getExtension('WEBGL_compressed_texture_astc');
etcSupported = !!gl.getExtension('WEBGL_compressed_texture_etc1');
dxtSupported = !!gl.getExtension('WEBGL_compressed_texture_s3tc');
pvrtcSupported = !!(gl.getExtension('WEBGL_compressed_texture_pvrtc')) || !!(gl.getExtension('WEBKIT_WEBGL_compressed_texture_pvrtc'));
// Vertex shader program
const vsSource = `
attribute vec4 aVertexPosition;
attribute vec3 aVertexNormal;
attribute vec3 aTextureCoord;
uniform mat4 uNormalMatrix;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
uniform mat3 uUVMatrix;
varying highp vec2 vTextureCoord;
varying highp vec3 vLighting;
void main(void) {
gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
//vTextureCoord.x = aTextureCoord.x;
// Invert Y coordinate to account for PNG top-left origin.
//vTextureCoord.y = aTextureCoord.y * -1.0 + 1.0;
vTextureCoord = vec2(uUVMatrix * aTextureCoord);
// Apply lighting effect
highp vec3 ambientLight = vec3(0.3, 0.3, 0.3);
highp vec3 directionalLightColor = vec3(1, 1, 1);
highp vec3 directionalVector = normalize(vec3(0.85, 0.8, 0.75));
highp vec4 transformedNormal = uNormalMatrix * vec4(aVertexNormal, 1.0);
highp float directional = max(dot(transformedNormal.xyz, directionalVector), 0.0);
vLighting = ambientLight + (directionalLightColor * directional);
}
`;
// Fragment shader program
const fsSource = `
varying highp vec2 vTextureCoord;
varying highp vec3 vLighting;
uniform sampler2D uSampler;
highp vec3 srgb_encode(highp vec3 color) {
highp float r = color.r < 0.0031308 ? 12.92 * color.r : 1.055 * pow(color.r, 1.0/2.4) - 0.055;
highp float g = color.g < 0.0031308 ? 12.92 * color.g : 1.055 * pow(color.g, 1.0/2.4) - 0.055;
highp float b = color.b < 0.0031308 ? 12.92 * color.b : 1.055 * pow(color.b, 1.0/2.4) - 0.055;
return vec3(r, g, b);
}
void main(void) {
highp vec3 vertexColor = vec3(0.9, 0.9, 0.9);
highp vec4 texelColor = texture2D(uSampler, vTextureCoord);
highp vec4 fragcolor;
// DECAL
fragcolor.rgb = vertexColor.rgb * (1.0 - texelColor.a) + texelColor.rgb * texelColor.a;
fragcolor.a = texelColor.a;
fragcolor.rgb *= vLighting;
fragcolor.rgb = srgb_encode(fragcolor.rgb);
gl_FragColor = fragcolor;
}
`;
// Initialize a shader program; this is where all the lighting
// for the vertices and so forth is established.
const shaderProgram = initShaderProgram(gl, vsSource, fsSource);
// Collect all the info needed to use the shader program.
// Look up which attributes our shader program is using
// for aVertexPosition, aVertexNormal, aTextureCoord,
// and look up uniform locations.
const programInfo = {
program: shaderProgram,
attribLocations: {
vertexPosition: gl.getAttribLocation(shaderProgram, 'aVertexPosition'),
vertexNormal: gl.getAttribLocation(shaderProgram, 'aVertexNormal'),
textureCoord: gl.getAttribLocation(shaderProgram, 'aTextureCoord'),
},
uniformLocations: {
projectionMatrix: gl.getUniformLocation(shaderProgram, 'uProjectionMatrix'),
modelViewMatrix: gl.getUniformLocation(shaderProgram, 'uModelViewMatrix'),
normalMatrix: gl.getUniformLocation(shaderProgram, 'uNormalMatrix'),
uvMatrix: gl.getUniformLocation(shaderProgram, 'uUVMatrix'),
uSampler: gl.getUniformLocation(shaderProgram, 'uSampler'),
},
};
// Here's where we call the routine that builds all the
// objects we'll be drawing.
const buffers = initBuffers(gl);
var then = 0;
// Draw the scene repeatedly
function render(now) {
now *= 0.001; // convert to seconds
const deltaTime = now - then;
then = now;
drawScene(gl, programInfo, buffers, texture, deltaTime);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
//
// initBuffers
//
// Initialize the buffers we'll need. For this demo, we just
// have one object -- a simple three-dimensional cube.
//
function initBuffers(gl) {
// Create a buffer for the cube's vertex positions.
const positionBuffer = gl.createBuffer();
// Select the positionBuffer as the one to apply buffer
// operations to from here out.
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
// Now create an array of positions for the cube.
const positions = [
// Front face
-1.0, -1.0, 1.0,
1.0, -1.0, 1.0,
1.0, 1.0, 1.0,
-1.0, 1.0, 1.0,
// Back face
-1.0, -1.0, -1.0,
-1.0, 1.0, -1.0,
1.0, 1.0, -1.0,
1.0, -1.0, -1.0,
// Top face
-1.0, 1.0, -1.0,
-1.0, 1.0, 1.0,
1.0, 1.0, 1.0,
1.0, 1.0, -1.0,
// Bottom face
-1.0, -1.0, -1.0,
1.0, -1.0, -1.0,
1.0, -1.0, 1.0,
-1.0, -1.0, 1.0,
// Right face
1.0, -1.0, -1.0,
1.0, 1.0, -1.0,
1.0, 1.0, 1.0,
1.0, -1.0, 1.0,
// Left face
-1.0, -1.0, -1.0,
-1.0, -1.0, 1.0,
-1.0, 1.0, 1.0,
-1.0, 1.0, -1.0,
];
// Now pass the list of positions into WebGL to build the
// shape. We do this by creating a Float32Array from the
// JavaScript array, then use it to fill the current buffer.
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
// Set up the normals for the vertices, so that we can compute lighting.
const normalBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
const vertexNormals = [
// Front
0.0, 0.0, 1.0,
0.0, 0.0, 1.0,
0.0, 0.0, 1.0,
0.0, 0.0, 1.0,
// Back
0.0, 0.0, -1.0,
0.0, 0.0, -1.0,
0.0, 0.0, -1.0,
0.0, 0.0, -1.0,
// Top
0.0, 1.0, 0.0,
0.0, 1.0, 0.0,
0.0, 1.0, 0.0,
0.0, 1.0, 0.0,
// Bottom
0.0, -1.0, 0.0,
0.0, -1.0, 0.0,
0.0, -1.0, 0.0,
0.0, -1.0, 0.0,
// Right
1.0, 0.0, 0.0,
1.0, 0.0, 0.0,
1.0, 0.0, 0.0,
1.0, 0.0, 0.0,
// Left
-1.0, 0.0, 0.0,
-1.0, 0.0, 0.0,
-1.0, 0.0, 0.0,
-1.0, 0.0, 0.0
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexNormals),
gl.STATIC_DRAW);
// Now set up the texture coordinates for the faces.
const textureCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, textureCoordBuffer);
const textureCoordinates = [
// Front
0.0, 0.0,
1.0, 0.0,
1.0, 1.0,
0.0, 1.0,
// Back
0.0, 0.0,
1.0, 0.0,
1.0, 1.0,
0.0, 1.0,
// Top
0.0, 0.0,
1.0, 0.0,
1.0, 1.0,
0.0, 1.0,
// Bottom
0.0, 0.0,
1.0, 0.0,
1.0, 1.0,
0.0, 1.0,
// Right
0.0, 0.0,
1.0, 0.0,
1.0, 1.0,
0.0, 1.0,
// Left
0.0, 0.0,
1.0, 0.0,
1.0, 1.0,
0.0, 1.0,
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoordinates),
gl.STATIC_DRAW);
// Build the element array buffer; this specifies the indices
// into the vertex arrays for each face's vertices.
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
// This array defines each face as two triangles, using the
// indices into the vertex array to specify each triangle's
// position.
const indices = [
0, 1, 2, 0, 2, 3, // front
4, 5, 6, 4, 6, 7, // back
8, 9, 10, 8, 10, 11, // top
12, 13, 14, 12, 14, 15, // bottom
16, 17, 18, 16, 18, 19, // right
20, 21, 22, 20, 22, 23, // left
];
// Now send the element array to GL
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,
new Uint16Array(indices), gl.STATIC_DRAW);
return {
position: positionBuffer,
normal: normalBuffer,
textureCoord: textureCoordBuffer,
indices: indexBuffer,
};
}
function elem(id) {
return document.getElementById(id);
}
// Upload content of a ktxTexture to WebGL.
//
// Returns the created WebGL texture object and matching texture target.
//
// needs Emscripten's OpenGL ES emulation.
function uploadTextureToGl(gl, ktexture) {
const { transcode_fmt } = ktx;
var formatString;
if (ktexture.needsTranscoding) {
var format;
if (astcSupported) {
formatString = 'ASTC';
format = transcode_fmt.ASTC_4x4_RGBA;
} else if (dxtSupported) {
formatString = ktexture.numComponents == 4 ? 'BC3' : 'BC1';
format = transcode_fmt.BC1_OR_3;
} else if (pvrtcSupported) {
formatString = 'PVRTC1';
format = transcode_fmt.PVRTC1_4_RGBA;
} else if (etcSupported) {
formatString = 'ETC';
format = transcode_fmt.ETC;
} else {
formatString = 'RGBA4444';
format = transcode_fmt.RGBA4444;
}
if (ktexture.transcodeBasis(format, 0) != ktx.error_code.SUCCESS) {
alert('Texture transcode failed. See console for details.');
return undefined;
}
}
const result = ktexture.glUpload();
if (result.error != gl.NO_ERROR) {
alert('WebGL error when uploading texture, code = '
+ result.error.toString(16));
return undefined;
}
if (result.object === undefined) {
alert('Texture upload failed. See console for details.');
return undefined;
}
if (result.target != gl.TEXTURE_2D) {
alert('Loaded texture is not a TEXTURE2D.');
return undefined;
}
return {
target: result.target,
object: result.object,
format: formatString,
uvMatrix: null
}
}
function createPlaceholderTexture(gl, color)
{
// // Must create texture via Emscripten so it knows of it.
// var texName;
// ktx.GL._glGenTextures(1, texName);
// texture = ktx.GL.textures[texName];
// Since it doesn't seem possible to get the above to work
// use a placeholder WebGLTexture object for a temporary
// image.
const placeholder = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, placeholder);
const level = 0;
const internalFormat = gl.RGBA;
const width = 1;
const height = 1;
const border = 0;
const srcFormat = gl.RGBA;
const srcType = gl.UNSIGNED_BYTE;
const pixel = new Uint8Array(color);
gl.texImage2D(gl.TEXTURE_2D, level, internalFormat,
width, height, border, srcFormat, srcType,
pixel);
return {
target: gl.TEXTURE_2D,
object: placeholder,
format: "",
uvMatrix: mat3.create()
};
}
//
// Sets the uvMatrix for a texture.
//
// The WebGL texture object is expected to have been created from the
// content of the ktxTexture object. The matrix is adjusted according
// to the orientation in the ktxTexture object.
//
function setUVMatrix(texture, inMatrix, ktexture) {
texture.uvMatrix = inMatrix;
if (ktexture.orientation.x == ktx.OrientationX.LEFT) {
mat3.translate(texture.uvMatrix, texture.uvMatrix, [1.0, 0.0]);
mat3.scale(texture.uvMatrix, texture.uvMatrix, [-1.0, 1.0]);
}
if (ktexture.orientation.y == ktx.OrientationY.DOWN) {
mat3.translate(texture.uvMatrix, texture.uvMatrix, [0.0, 1.0]);
mat3.scale(texture.uvMatrix, texture.uvMatrix, [1.0, -1.0]);
}
}
//
// Binds a texture and sets suitable texture parameters.
//
// The WebGL texture object is expected to have been created from the
// content of the ktxTexture object.
//
function setTexParameters(texture, ktexture) {
gl.bindTexture(texture.target, texture.object);
if (ktexture.numLevels > 1 || ktexture.generateMipmaps) {
// Enable bilinear mipmapping.
gl.texParameteri(texture.target,
gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_NEAREST);
} else {
gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
}
gl.texParameteri(texture.target, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.bindTexture(texture.target, null);
}
function loadTexture(gl, url)
{
// Because images have to be downloaded over the internet
// they might take a moment until they are ready. Until
// then temporarily fill the texture with a single pixel image
// so we can use it immediately. When the image has finished
// downloading we'll update texture to the new contents
const placeholder = createPlaceholderTexture(gl, [0, 0, 255, 255]);
gl.bindTexture(placeholder.target, placeholder.object);
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.responseType = "arraybuffer";
xhr.onload = function(){
var ktxdata = new Uint8Array(this.response);
ktexture = new ktx.texture(ktxdata);
const tex = uploadTextureToGl(gl, ktexture);
setUVMatrix(tex, mat3.create(), ktexture);
setTexParameters(tex, ktexture);
gl.bindTexture(tex.target, tex.object);
gl.deleteTexture(texture.object);
texture = tex;
elem('format').innerText = tex.format;
ktexture.delete();
};
//xhr.onprogress = runProgress;
//xhr.onloadstart = openProgress;
xhr.send();
return placeholder;
}
function isPowerOf2(value) {
return (value & (value - 1)) == 0;
}
//
// Draw the scene.
//
function drawScene(gl, programInfo, buffers, texture, deltaTime) {
gl.enable(gl.CULL_FACE);
gl.enable(gl.DEPTH_TEST);
gl.enable(gl.DEPTH_TEST); // Enable depth testing
gl.depthFunc(gl.LEQUAL); // Near things obscure far things
// In case the source image has translucent parts ...
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
gl.clearColor(0.0, 0.0, 0.0, 1.0); // Clear to black, fully opaque
gl.clearDepth(1.0); // Clear everything
// Clear the canvas before we start drawing on it.
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// Create a perspective matrix, a special matrix that is
// used to simulate the distortion of perspective in a camera.
// Our field of view is 45 degrees, with a width/height
// ratio that matches the display size of the canvas
// and we only want to see objects between 0.1 units
// and 100 units away from the camera.
const fieldOfView = 45 * Math.PI / 180; // in radians
const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
const zNear = 0.1;
const zFar = 100.0;
const projectionMatrix = mat4.create();
// note: glmatrix.js always has the first argument
// as the destination to receive the result.
mat4.perspective(projectionMatrix,
fieldOfView,
aspect,
zNear,
zFar);
// Set the drawing position to the "identity" point, which is
// the center of the scene.
const modelViewMatrix = mat4.create();
// Now move the drawing position a bit to where we want to
// start drawing the square.
mat4.translate(modelViewMatrix, // destination matrix
modelViewMatrix, // matrix to translate
[-0.0, 0.0, -6.0]); // amount to translate
mat4.rotate(modelViewMatrix, // destination matrix
modelViewMatrix, // matrix to rotate
cubeRotation, // amount to rotate in radians
[0, 0, 1]); // axis to rotate around (Z)
mat4.rotate(modelViewMatrix, // destination matrix
modelViewMatrix, // matrix to rotate
cubeRotation * .7,// amount to rotate in radians
[0, 1, 0]); // axis to rotate around (X)
const normalMatrix = mat4.create();
mat4.invert(normalMatrix, modelViewMatrix);
mat4.transpose(normalMatrix, normalMatrix);
// Tell WebGL how to pull out the positions from the position
// buffer into the vertexPosition attribute
{
const numComponents = 3;
const type = gl.FLOAT;
const normalize = false;
const stride = 0;
const offset = 0;
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.position);
gl.vertexAttribPointer(
programInfo.attribLocations.vertexPosition,
numComponents,
type,
normalize,
stride,
offset);
gl.enableVertexAttribArray(
programInfo.attribLocations.vertexPosition);
}
// Tell WebGL how to pull out the texture coordinates from
// the texture coordinate buffer into the textureCoord attribute.
{
const numComponents = 2;
const type = gl.FLOAT;
const normalize = false;
const stride = 0;
const offset = 0;
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.textureCoord);
gl.vertexAttribPointer(
programInfo.attribLocations.textureCoord,
numComponents,
type,
normalize,
stride,
offset);
gl.enableVertexAttribArray(
programInfo.attribLocations.textureCoord);
}
// Tell WebGL how to pull out the normals from
// the normal buffer into the vertexNormal attribute.
{
const numComponents = 3;
const type = gl.FLOAT;
const normalize = false;
const stride = 0;
const offset = 0;
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.normal);
gl.vertexAttribPointer(
programInfo.attribLocations.vertexNormal,
numComponents,
type,
normalize,
stride,
offset);
gl.enableVertexAttribArray(
programInfo.attribLocations.vertexNormal);
}
// Tell WebGL which indices to use to index the vertices
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffers.indices);
// Tell WebGL to use our program when drawing
gl.useProgram(programInfo.program);
// Set the shader uniforms
gl.uniformMatrix4fv(
programInfo.uniformLocations.projectionMatrix,
false,
projectionMatrix);
gl.uniformMatrix4fv(
programInfo.uniformLocations.modelViewMatrix,
false,
modelViewMatrix);
gl.uniformMatrix4fv(
programInfo.uniformLocations.normalMatrix,
false,
normalMatrix);
gl.uniformMatrix3fv(
programInfo.uniformLocations.uvMatrix,
false,
texture.uvMatrix);
// Specify the texture to map onto the faces.
// Tell WebGL we want to affect texture unit 0
gl.activeTexture(gl.TEXTURE0);
// Bind the texture to texture unit 0
gl.bindTexture(texture.target, texture.object);
// Tell the shader we bound the texture to texture unit 0
gl.uniform1i(programInfo.uniformLocations.uSampler, 0);
{
const vertexCount = 36;
const type = gl.UNSIGNED_SHORT;
const offset = 0;
gl.drawElements(gl.TRIANGLES, vertexCount, type, offset);
}
// Update the rotation for the next draw
cubeRotation += deltaTime;
}
//
// Initialize a shader program, so WebGL knows how to draw our data
//
function initShaderProgram(gl, vsSource, fsSource) {
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
// Create the shader program
const shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
// If creating the shader program failed, alert
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
alert('Unable to initialize the shader program: ' + gl.getProgramInfoLog(shaderProgram));
return null;
}
return shaderProgram;
}
//
// creates a shader of the given type, uploads the source and
// compiles it.
//
function loadShader(gl, type, source) {
const shader = gl.createShader(type);
// Send the source to the shader object
gl.shaderSource(shader, source);
// Compile the shader program
gl.compileShader(shader);
// See if it compiled successfully
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
alert('An error occurred compiling the shaders: ' + gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
+186
View File
@@ -0,0 +1,186 @@
<!doctype html>
<!-- Copyright 2024 Mark Callow.
SPDX-License-Identifier: Apache-2.0 -->
<html lang="en">
<head>
<meta charset="utf-8">
<title>libktx.js Demo with WebGL</title>
<link rel="stylesheet" href="../webgl.css" type="text/css">
<link rel="shortcut icon" href="../ktx_favicon.ico" type="image/x-icon" />
<style>
body {
/* Set margins to center the page. */
margin: 2em auto 2em auto;
max-width: 65em;
}
/* Center the canvas container too. */
#container {
display: block;
margin-left: auto;
margin-right: auto;
position: relative;
top: 0;
width: 100%;
height: 100vh;
}
#glcanvas {
border: none;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: white;
z-index: -1;
}
#glcontent {
position: relative;
margin: 10px;
}
#panel {
color: white;
background-color:rgba(0.3, 0.3, 0.3, 0.3);
padding: 0.5em;
max-width: 65em;
}
.item {
display: inline-block;
margin: 1em;
padding: 1em;
background: transparent;
vertical-align: top;
}
.label {
margin-top: 0.5em;
width: 256px;
}
/*
* The size of an image is changed when it is added to the div
* styled by this. So the size needs to be a multiple of 4 for
* compatibility with various compressed texture formats in WebGL.
*/
.view {
width: 256px;
height: 256px;
border: 1px solid black;
}
/* An img inside a .view should fill the view. */
.view img {
width: 100%;
height: 100%;
}
.pass {
color: green;
}
.fail {
color: red;
}
</style>
</head>
<body>
<div id="panel">
<h2>libktx Read-write Javascript Binding Test</h2>
<p>This tests the functionality of the read-write Javascript
binding for libktx, which has been compiled to Javascript using
Emscripten. Though loosely referred to as "write" the added
functionalty tested here is more about creating ktxTexture objects
from scratch than writing them out as files.
</p>
<p>Click the <i>Run Tests</i> button to run the tests.</p>
<p>The test reads a .png file then performs various operations
using the wrapper API including creating an uncompressed
ktxTexture2 object. The operations are shown in the list below.
</p>
</div>
<p id="test_ktx">
<input type="button" value="Run Tests" id='runtests' disabled='true'
onclick="runTests('ktx_app.png')">
</p>
<p>Create uncompressed ktxTexture2: <b id='create_result'></b></p>
<p>Copy input image to new texture: <b id='copy_image_result'></b></p>
<p>Check oetf and primaries are set correctly: <b id='colorspace_set_result'></b></p>
<p>Write and read metadata: <b id='metadata_result'></b></p>
<p>Get image from new texture: <b id='get_image_result'></b></p>
<p>Create copy of original texture: <b id='create_copy_result'></b></p>
<p>Compress ktxTexture2 with Basis: <b id='compress_basis_result'></b></p>
<p>Write KTX file to memory: <b id='write_to_memory_result'></b></p>
<p>Read KTX file from memory: <b id='read_from_memory_result'></b></p>
<p>ASTC inputSwizzle set: <b id ='swizzle_set_result'></b></p>
<p>Compress ktxTexture2 with ASTC: <b id='compress_astc_result'></b></p>
<div id="container">
<!-- <canvas id="glcanvas" width="1280" height="480"></canvas> -->
<canvas id="glcanvas"></canvas>
<div id="glcontent"></div>
</div>
</body>
<script src="../gl-matrix.js"></script>
<script src="../libktx.js"></script>
<script src="libktx-test.js"></script>
<script type="text/javascript">
function createElem(type, parent, className) {
const elem = document.createElement(type);
parent.appendChild(elem);
if (className) {
elem.className = className;
}
return elem;
}
function randArrayElement(array) {
return array[Math.random() * array.length | 0];
}
function rand(min, max) {
if (max === undefined) {
max = min;
min = 0;
}
return Math.random() * (max - min) + min;
}
// Make 3 elements over the canvas.
const contentElem = document.querySelector('#glcontent');
const items = [];
const numItems = 6;
const origImageItem = 0, uncompTextureItem = 1;
const copyTextureItem = 2, basisCompTextureItem = 3;
const writeReadTextureItem = 4, astcCompTextureItem = 5;
const labelText = [
'Input image',
'Uncompressed ktxTexture2',
'Copy of uncompressed ktxTexture2',
'Compressed to ETC1S/Basis-LZ',
'Written and Read .ktx2 file with uncompressed texture',
'ASTC compressed texture'
];
for (let i = 0; i < numItems; ++i) {
const outerElem = createElem('div', contentElem, 'item');
const viewElem = createElem('div', outerElem, 'view');
const labelElem = createElem('div', outerElem, 'label');
//labelElem.textContent = `Item ${i + 1}`;
labelElem.textContent = labelText[i];
const bgColor = [rand(1), rand(1), rand(1), 1];
var placeholder = {};
switch (i) {
case 0:
placeholder.target = null;
placeholder.object = null;
placeholder.uvMatrix = null;
break;
default:
const texColor = [rand(255), rand(255), rand(255), 255];
placeholder = createPlaceholderTexture(gl, texColor);
break;
}
items.push({
color: bgColor,
texture: placeholder,
element: viewElem,
label: labelElem
});
}
</script>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.
File diff suppressed because it is too large Load Diff
+635
View File
@@ -0,0 +1,635 @@
/**
* @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
*/
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 'https://unpkg.com/three@0.114.0/build/three.module.js';
const DFD = {
modelUastc: 166,
modelEtc1s: 163,
channelUastc: {
rgb: 0,
rgba: 3,
rrr: 4,
rrrg: 5
},
channelEtc1s: {
rgb: 0,
rrr: 3,
ggg: 4,
aaa: 15,
}
};
class KTX2Loader extends CompressedTextureLoader {
constructor ( manager ) {
super( manager );
this.basisModule = null;
this.transcoderConfig = {};
}
detectSupport ( renderer ) {
this.transcoderConfig = {
astcSupported: !! renderer.extensions.get( 'WEBGL_compressed_texture_astc' ),
etc1Supported: !! renderer.extensions.get( 'WEBGL_compressed_texture_etc1' ),
etc2Supported: !! renderer.extensions.get( 'WEBGL_compressed_texture_etc' ),
dxtSupported: !! renderer.extensions.get( 'WEBGL_compressed_texture_s3tc' ),
pvrtcSupported: !! renderer.extensions.get( 'WEBGL_compressed_texture_pvrtc' )
|| !! renderer.extensions.get( '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( 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 texFormat = ktx.dfd.colorModel == DFD.modelUastc ? TextureFormat.UASTC4x4
: TextureFormat.ETC1S;
// 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.
var hasAlpha = false;
if (texFormat == TextureFormat.UASTC4x4) {
var transcoder = new UastcImageTranscoder();
if ( (ktx.dfd.samples[0].channelId & 0xf) === DFD.channelUastc.rgba )
hasAlpha = true;
} else {
var transcoder = new BasisLzEtc1sImageTranscoder();
if ( ktx.dfd.numSamples == 2 && (ktx.dfd.samples[1].channelId & 0xf) === DFD.channelEtc1s.aaa ) {
hasAlpha = true;
}
}
ktx.initMipmaps( texFormat, hasAlpha, transcoder, this.basisModule, 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 ( arrayBuffer ) {
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,
},
/*
texelBlockDimension0: dfdReader.nextUint8(),
texelBlockDimension1: dfdReader.nextUint8(),
texelBlockDimension2: dfdReader.nextUint8(),
*/
bytesPlane0: dfdReader.nextUint8(),
numSamples: 0,
samples: [],
};
dfdReader.skip( 7 /* bytesPlane[1-7] */ );
this.dfd.numSamples = (this.dfd.descriptorBlockSize/4 - sampleStart) / sampleWords;
for (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 === DFD.modelUastc ) ) {
throw new Error( 'THREE.KTX2Loader: Only Basis Universal supercompression is currently supported.' );
}
///////////////////////////////////////////////////
// Key/Value Data (KVD)
///////////////////////////////////////////////////
// Not implemented.
this.kvd = {};
///////////////////////////////////////////////////
// Supercompression Global Data (SGD)
///////////////////////////////////////////////////
if (this.header.sgdByteLength > 0) {
var sgdReader = new KTX2BufferReader(
this.arrayBuffer,
this.header.sgdByteOffset,
this.header.sgdByteLength,
littleEndian
);
this.sgd = {
endpointCount: sgdReader.nextUint16(),
selectorCount: sgdReader.nextUint16(),
endpointsByteLength: sgdReader.nextUint32(),
selectorsByteLength: sgdReader.nextUint32(),
tablesByteLength: sgdReader.nextUint32(),
extendedByteLength: sgdReader.nextUint32(),
imageDescs: [],
endpointsData: null,
selectorsData: null,
tablesData: null,
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 );
} else {
this.sgd = {};
}
}
initMipmaps ( texFormat, hasAlpha, transcoder, basisModule, config ) {
var TranscodeTarget = basisModule.TranscodeTarget;
var TextureFormat = basisModule.TextureFormat;
var ImageInfo = basisModule.ImageInfo;
var scope = this;
var mipmaps = [];
var width = this.getWidth();
var height = this.getHeight();
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 ( !basisModule.isFormatSupported( targetFormat, texFormat ) ) {
throw new Error( 'THREE.KTX2Loader: Selected texture format not supported by current msc_basis_transcoder.js 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;
if ( texFormat == TextureFormat.UASTC4x4 ) {
imageInfo.flags = 0;
imageInfo.rgbByteOffset = 0;
imageInfo.rgbByteLength = levelImageByteLength;
imageInfo.alphaByteOffset = 0;
imageInfo.alphaByteLength = 0;
result = transcoder.transcodeImage(targetFormat,
new Uint8Array( this.arrayBuffer, this.levels[ level ].byteOffset + imageOffsetInLevel, levelImageByteLength),
imageInfo,
0,
hasAlpha,
isVideo
);
} else {
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;
result = transcoder.transcodeImage(targetFormat,
new Uint8Array( this.arrayBuffer, this.levels[ level ].byteOffset + imageDesc.rgbSliceByteOffset, imageDesc.rgbSliceByteLength + imageDesc.alphaSliceByteLength ),
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;
}
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 };
+191
View File
@@ -0,0 +1,191 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>msc_transcoder_test with three.js</title>
<link rel="shortcut icon" href="../ktx_favicon.ico" type="image/x-icon" />
</head>
<script src="../msc_basis_transcoder.js"></script>
<!--
<script defer src="https://unpkg.com/es-module-shims@0.4.6/dist/es-module-shims.js"></script>
-->
<!-- Use an import map shim to ensure KTX2Loader and the demo itself
use the same CDN-hosted copy of the threejs library. -->
<!--
<script type="importmap-shim">
{
"imports": {
"https://raw.githack.com/donmccurdy/three.js/feat-ktx2loader/build/three.module.js": "https://unpkg.com/three@0.114.0/build/three.module.js"
}
}
</script>
-->
<style>
html, body { width:100%; height:100%; margin:0; padding:0; }
canvas { display:block; }
h2 { margin-top: 0; }
#panel {
position: absolute;
color: white;
background-color:rgba(0.3, 0.3, 0.3, 0.3);
padding: 0.5em;
left: 50%;
transform: translate(-50%, 0);
max-width: 50em;
}
</style>
<div id="panel">
<h2>msc_basis_transcoder test</h2>
<p>
Loads and parses KTX2 files with a plain JS loader, transcodes the
Basis Universal payload with <code>msc_basis_transcoder</code>,
and renders the result with three.js. The first KTX2 file contains
BasisLZ/ETC1S format and the second UASTC format. These textures
have been transcoded to <b id='format'>FORMAT</b>.
</p>
<div id="log"></div>
</div>
<!-- <script type="module-shim"> -->
<script type="module">
import * as THREE from 'https://unpkg.com/three@0.114.0/build/three.module.js';
import { OrbitControls } from 'https://unpkg.com/three@0.114.0/examples/jsm/controls/OrbitControls.js';
/*
import { KTX2Loader } from 'https://raw.githack.com/donmccurdy/three.js/feat-ktx2loader/examples/jsm/loaders/KTX2Loader.js';
*/
import { KTX2Loader } from '/llt-three/KTX2Loader.js';
const width = window.innerWidth;
const height = window.innerHeight;
const renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setSize( width, height );
renderer.outputEncoding = THREE.sRGBEncoding;
document.body.appendChild( renderer.domElement );
const scene = new THREE.Scene();
scene.background = new THREE.Color( 0xF0F0F0 );
const camera = new THREE.PerspectiveCamera( 60, width / height, 0.1, 100 );
camera.position.set( 2, 1.5, 1 );
camera.lookAt( scene.position );
scene.add(camera);
const controls = new OrbitControls( camera, renderer.domElement );
controls.autoRotate = true;
// FIXME: compressed textures don't support? KTX2 compressed textures do.
// PlaneBufferGeometry UVs assume flipY=true, which compressed textures don't support.
const geometry = flipY( new THREE.PlaneBufferGeometry() );
const materialEtc1s = new THREE.MeshBasicMaterial( {
color: 0xFFFFFF,
side: THREE.DoubleSide,
defines: {DECAL: ""}
} );
/* This isn't working. No time for further investigation.
const myShader = [
'diffuseColor *= texelColor;',
'#ifndef DECAL',
' diffuseColor *= texelColor;',
'#else',
' diffuseColor = vec4( mix( diffuse, texelColor.rgb, texelColor.a ), opacity );',
'#endif',
].join( '\n' );
materialEtc1s.onBeforeCompile = function ( shader ) {
shader.fragmentShader = shader.fragmentShader.replace(
myShader
);
};
*/
const materialUastc = new THREE.MeshBasicMaterial( {
alphaTest: 1.0,
color: 0xFFFFFF,
side: THREE.DoubleSide
} );
var meshEtc1s = new THREE.Mesh(geometry, materialEtc1s);
meshEtc1s.position.z = -0.5;
var meshUastc = new THREE.Mesh(geometry, materialUastc);
meshUastc.position.z = +0.5;
scene.add(meshEtc1s, meshUastc);
const formats = [
'RGBA_ASTC_4x4_Format',
'RGB_S3TC_DXT1_Format',
'RGBA_S3TC_DXT5_Format',
'RGB_PVRTC_4BPPV1_Format',
'RGBA_PVRTC_4BPPV1_Format',
'RGB_ETC1_Format',
'RGB_ETC2_Format',
'RGBA_ETC2_EAC_Format',
];
const formatToString = ( format ) => formats.find( ( name ) => THREE[ name ] === format );
animate();
var loader = new KTX2Loader().detectSupport( renderer )
loader.load( '../libktx-webgl/ktx_app_basis.ktx2', ( texture ) => {
materialEtc1s.map = texture;
elem('format').innerText = formatToString( materialEtc1s.map.format );
materialEtc1s.needsUpdate = true;
}, ( p ) => console.log( `...${p}` ), ( e ) => console.error( e ) );
loader.load( '../libktx-webgl/ktx_document_uastc_rdo5.ktx2', ( texture ) => {
materialUastc.map = texture;
//elem('format').innerText = formatToString( materialEtc1s.map.format );
materialUastc.needsUpdate = true;
}, ( p ) => console.log( `...${p}` ), ( e ) => console.error( e ) );
function elem(id) {
return document.getElementById(id);
}
function animate() {
requestAnimationFrame( animate );
meshUastc.needsUpdate = true;
controls.update();
renderer.render( scene, camera );
}
window.addEventListener( 'resize', () => {
const width = window.innerWidth;
const height = window.innerHeight;
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize( width, height );
}, false );
// FIXME: read the orientation metadata and only do this if it indicates
// Y orientation is up.
/** 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>
</html>
+11
View File
@@ -0,0 +1,11 @@
/*
* Copyright 2020 Mark Callow
* SPDX-License-Identifier: Apache-2.0
*/
canvas {
border: 2px solid black;
background-color: black;
}
video {
display: none;
}