How to load a GLTF that uses KTX2 textures? Getting error: setKTX2Loader must be called before loading KTX2 textures

Hi,

Anyone got an example that loads and displays a GLTF that uses KTX2 textures?

The KTX2 demo on the threejs website is just loading a texture and applying it to a mesh.

I have a GLTF which I modified to use KTX2 textures (trimmed down version of the json in the GLTF is below).

The following code which works for normal GLTF and Draco but doesn’t if I use KTX2, it errors with:

THREE.GLTFLoader: setKTX2Loader must be called before loading KTX2 textures

Anyone got any ideas and even better a working example online with source code I can review? The example on the threejs website is just loading a texture in and applying it to
a mesh, I don’t really want to setup all the textures for complex models manually on every GLTF I get supplied from the designers.

Anyone got any ideas or working examples?


Here's the code (trimmed down for pasting here)
import * as THREE from '../../node_modules/three/build/three.module';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import { KTX2Loader } from "three/examples/jsm/loaders/KTX2Loader";


class Preview {

   // var defs

    constructor(){
        // do lots of things setup three etc
        await this.loadModel();
        // do more things
    }


    async loadModel() {
        log(`Preview.loadModel()`);

        this.ktx2Loader = new KTX2Loader();
        this.ktx2Loader.setTranscoderPath('./includes/js/libs/basis/');
        this.ktx2Loader.detectSupport(this.renderer);

        const loader = new GLTFLoader();
        loader.setKTX2Loader = this.ktx2Loader;

        // Works for Draco
        // const dracoLoader = new DRACOLoader();
        // dracoLoader.setDecoderPath('./includes/js/libs/draco/');
        // loader.setDRACOLoader(dracoLoader);       

        await loader.load(
            "./assets/models/" + this.resource,
            // loaded
            async (gltf) => {

                this.meshes.gallery = gltf.scene;
                this.scene.add(this.meshes.gallery);

            },
            (xhr) => {
                let p = Math.ceil(xhr.loaded / xhr.total * 100);
                log((xhr.loaded / xhr.total * 100) + '% loaded');
                if (p < 100) {
                    this.dom.preloaderMessage.innerHTML = `Loading ${p}%`;
                }

            }
        );

    }

}

Here’s the GLTF (Trimmed down for pasting here)

{
    "asset": {
        "generator": "Khronos glTF Blender I/O v1.7.33",
        "version": "2.0"
    },
    "extensionsUsed": [
        "KHR_texture_basisu"
    ],
    "extensionsRequired": [
        "KHR_texture_basisu"
    ],
    "scene": 0,
    "scenes": [
        {
            "name": "Scene",
            "nodes": [
                0,
                "TRIMMED ARRAY 0 to 24 FOR PASTING ON WEBSITE",
                24
            ]
        }
    ],
    "nodes": ["REMOVED FOR PREVIEW - USUAL STUFF"],
    "materials": [
        {
            "name": "TableSupports",
            "pbrMetallicRoughness": {
                "baseColorTexture": {
                    "index": 0
                },
                "metallicFactor": 0,
                "roughnessFactor": 0.5
            }
        },
        {
            "name": "TablesMid",
            "pbrMetallicRoughness": {
                "baseColorTexture": {
                    "index": 1
                },
                "metallicFactor": 0
            }
        },
        {
            "name": "BenchesMid",
            "pbrMetallicRoughness": {
                "baseColorTexture": {
                    "index": 2
                },
                "metallicFactor": 0
            }
        },
        {
            "name": "Walls",
            "pbrMetallicRoughness": {
                "baseColorTexture": {
                    "index": 3
                },
                "metallicFactor": 0,
                "roughnessFactor": 0.5
            }
        },
        "AND MORE"
    ],
    "meshes": [
        {
            "name": "Cylinder.007",
            "primitives": [
                {
                    "attributes": {
                        "POSITION": 0,
                        "NORMAL": 1,
                        "TEXCOORD_0": 2
                    },
                    "indices": 3,
                    "material": 0
                }
            ]
        },
        {
            "name": "Cube.042",
            "primitives": [
                {
                    "attributes": {
                        "POSITION": 4,
                        "NORMAL": 5,
                        "TEXCOORD_0": 6
                    },
                    "indices": 7,
                    "material": 1
                }
            ]
        },
        {
            "name": "Cube.043",
            "primitives": [
                {
                    "attributes": {
                        "POSITION": 8,
                        "NORMAL": 9,
                        "TEXCOORD_0": 10
                    },
                    "indices": 11,
                    "material": 2
                }
            ]
        },
        {
            "name": "Cube.045",
            "primitives": [
                {
                    "attributes": {
                        "POSITION": 12,
                        "NORMAL": 13,
                        "TEXCOORD_0": 14
                    },
                    "indices": 15,
                    "material": 3
                }
            ]
        },
        {
            "name": "Cube.048",
            "primitives": [
                {
                    "attributes": {
                        "POSITION": 16,
                        "NORMAL": 17,
                        "TEXCOORD_0": 18
                    },
                    "indices": 19,
                    "material": 4
                }
            ]
        },
        "AND MORE...."
    ],
    "textures": [
        {
            "sampler": 0,
            "source": 0,
            "idx": 0,
            "extensions": {
                "KHR_texture_basisu": {
                    "source": 0
                }
            }
        },
        {
            "sampler": 0,
            "source": 1,
            "idx": 1,
            "extensions": {
                "KHR_texture_basisu": {
                    "source": 1
                }
            }
        },
        {
            "sampler": 0,
            "source": 2,
            "idx": 2,
            "extensions": {
                "KHR_texture_basisu": {
                    "source": 2
                }
            }
        },
        "THERE ARE MORE..........."
    ],
    "images": [
        {
            "mimeType": "image/ktx2",
            "name": "TableSupports_BC",
            "uri": "TableSupports_BC.ktx2",
            "idx": 0
        },
        {
            "mimeType": "image/ktx2",
            "name": "TablesMid_BC",
            "uri": "TablesMid_BC.ktx2",
            "idx": 1
        },
        {
            "mimeType": "image/ktx2",
            "name": "BenchesMid_BC",
            "uri": "BenchesMid_BC.ktx2",
            "idx": 2
        },
        "THERE ARE MORE..........."
    ],
    "accessors": ["REMOVED FOR PREVIEW - USUAL STUFF"],
    "bufferViews": ["REMOVED FOR PREVIEW - USUAL STUFF"],
    "samplers": ["REMOVED FOR PREVIEW - USUAL STUFF"],
    "buffers": ["REMOVED FOR PREVIEW - USUAL STUFF"]
}
```

setKTX2Loader is a method and not a property – you’ll want to do this:

loader.setKTX2Loader(this.ktx2Loader);

The webgl / loaders / gltf / compressed example is a good reference as well.

oh lol :man_facepalming: Thanks for that :+1:

I was very tired when I was working on that, it’s always the little things…

And now for the next error(s)…

:neutral_face:

You may need to look at your browser’s network tab, and see where it is trying to find these textures? The error might mean the textures are not in the correct place relative to the .gltf file. If you pack everything into a .glb this can be simpler.

1st thing I tested, and tried fully qualified paths just encase in the gltf json, no joy.

I need to dive into the GLTFLoader class and put some console.log’s in there to see what it’s picking up and where it’s looking for uri, as it’s saying uri is undefined, so the GLTF JSON structure may be formatted incorrectly, but it shouldn’t be as I’ve just manually replaces image/png with image/ktx and the paths to the pngs replacing the extension with ktx2

It may also be my PNG to KTX2 conversion process

// Refs
// https://github.com/BinomialLLC/basis_universal
// https://skia.googlesource.com/external/github.com/BinomialLLC/basis_universal/+/refs/heads/gltf-demo/README.md
// For the maximum possible achievable quality with the current format and encoder, use:
// basisu x.png -slower -max_endpoints 16128 -max_selectors 16128 -no_selector_rdo -no_endpoint_rdo

    const exe = path.resolve("./exe/basisu.exe");
    const { stdout, stderr } = await execFileAsync(exe, [
        '-q', quality,
        '-slower',
        '-max_endpoints', '16128',
        '-max_selectors', '16128',
        '-no_selector_rdo',
        '-no_endpoint_rdo',
        // '-uastc', // error command not found
        // '-uastc_level', '2',
        // '-comp_level', '1',
        // '-comp_type', 'cTFBC7',
        '-file', inputPath,
        '-output_file', outputPath
    ])

Ah, you might have an easier time doing the KTX2 conversion in tools that can work with glTF files. There’s more to it than replacing the URLs. For example you could install KTX-Software from the “Assets” section of the release notes, and then run:

npm install --global @gltf-transform/cli

gltf-transform etc1s input.glb output.glb

Interesting, so that package is a wrapper for “basisu.exe” (that’s the exe from the KTX-Software, used in my code above). It fails to install correctly on windows as it’s paths are too long, which is why I have the exe in a folder relative to my node conversion script.

@gltf-transform/cli npmjs.org page details exe / codec commands I can try. I was going for gltf so I can tweak texture compression settings by doing the conversion myself, but may no longer be needed if that wrapper has options for that.

RE: GLTF Structure
I followed their GLTF structural changes (Link), but maybe not as close as I should have as I’m removing fallback png’s (maybe I’m mapping the index wrong, I will need to re-check).

I am detecting the device type in the web application and the appropriate GLTF path is used, 1 for desktops and android and 1 for shaitPhones, ermm I mean iPhones, as they just crash all the time, with apple only allowing webgl to have access to about 128bytes of ram.

I’m hoping ktx2 can help as the designers wont compromise with their models and textures. The webgl application runs at a rock solid 60fps with no crashes on an old Galaxy S7, however on iPhone it either crashes the browser or reboots the whole phone! And that’s on the latest iPhones depending on OS version. Apple :man_facepalming:

GLTF change docs they got on github below:
Link

Thanks for the link will run some tests with it :+1: