Relative/absolute paths to texture images

I’m struggling with referencing texture images from a .GLB file using either relative or absolute URIs.

Given glTF content of:

...
images: [
  {
    name: 'copper_wires',
    mimeType: 'image/jpeg',
    uri: '../textures/copper_wires.jpg'
  },
]
...

I receive the console error
Unable to load from /3D/assemblies/5/optimized_model.glb: /images/0/uri: '../textures/copper_wires.jpg' is invalid
In this case the library doesn’t even attempt to fetch the file.

If instead I attempt an absolute URL:

...
images: [
  {
    name: 'copper_wires',
    mimeType: 'image/jpeg',
    uri: "https://server.com/3D/textures/copper_wires.jpg"
  },
]
...

Then the SceneLoader tries to fetch /3D/assemblies/5/https://server.com/3D/textures/copper_wires.jpg
and fails.

It looks like the loader tries to just append the uri to the base path of the current .GLB file.

I found this forum post where @Deltakosh advises against using relative URLs.

If I want to put image files somewhere other than below the GLB directory, what options do I have?

By way of background - I’d like to pull out textures from models and store them in a single /textures directory so that they can be cached between model loads. I’m already using a material library for comment textures across ALL models, but this will handle a situation where there’s a texture that’s only used in a handful.

I do not actually use my own .babylon / json exporter for Blender, but thought I could use a ‘…/…’ as an argument for FileLoader.Append().

I do use .json files for many other things though, and have had to take precautions to KEEP it from being cached & not showing my changes. Are you sure a .glb is not being cached?

Would defer to someone who actually uses FileLoader.

I’m sure I can pass relative URLs such as SceneLoader.Append('../model.glb') - the issue is with …/ URLs within the GLB file itself, referring to textures.

Caching the GLB is great but let’s say I’ve got three .GLB files which all refer to a 60kb texture image eg wires.png. If I embed it in the .GLB, each one will include its own copy. What I ideally want to do is to refer to a single /3D/textures/wires.png from each GLB and then the texture file itself will also be cached between the model loads.

As stated in the thread you pointed to, the use of “…” in the uri is not valid, so you can’t use such urls in a glb/gltf file.

cc @bghgary to find out if your use case is simply not supported or if there are other ways to achieve it.

[…]

What comes to mind is to use .gltf assets instead of .glb. That way you can have multiple .gltf files in the same directory and a textures/ subdirectory to which the uri of each .gltf file can point.

The glTF loader explicitly disallows “…” in the URI for security/privacy reasons. Perhaps we can add a flag to allow this.

If you want to use absolute URL in a glTF, the rootUrl passed to the SceneLoader must be empty string. We can perhaps update the loader to support absolute URLs even when rootUrl is not empty.

Another option is to structure the files such that the assets always access files below the current location (e.g., all the shared textures are in a texture folder with the glTF/glb in a root folder).

Thanks! Your comment pointed me in the direction of a hacky monkey patch that gets things working for me. I do think we could perhaps either improve the docs or add some support for this. This is my understanding:

According to the glTF spec a .glb file can contain resources (texture images) - these can be either Base64 encoded URIs, or relative or absolute URLs to the resource file.

For security/privacy reasons, BabylonJS only supports URIs below the rootUrl of the file being loaded. Absolute and relative URIs are not supported.

Given a file at https://example.com/3D/models/model.glb

...
images: [
  {
    name: 'copper_wires',
    mimeType: 'image/jpeg',
    uri: 'textures/copper_wires.jpg' 
  },
]
...

:white_check_mark: this works

...
images: [
  {
    name: 'copper_wires',
    mimeType: 'image/jpeg',
    uri: '../textures/copper_wires.jpg' 
  },
]
...

:x: This fails with an invalid URL error

...
images: [
  {
    name: 'copper_wires',
    mimeType: 'image/jpeg',
    uri: 'https://example.com/textures/copper_wires.jpg' 
  },
]
...

:x: This attempts to fetch the file at https://example.com/models/https://example.com/textures/copper_wires.jpg
and fails.

I have got this working with a ludicrously hacky solution that rewrites the URLs in the loader:

const loader = BABYLON.SceneLoader.Append("/3D/models", "model.glb", this.scene, (s) => {
  // Do something with loaded scene  
})! as any;

  
// Once the JSON in the loaded file has been parsed, rewrite the relative URLs to 
// something that will pass Babylon's URL validation and can later be intercepted 
// by the below preprocessUrlAsync function.
loader.onParsed = (data:{json:{images:{uri:string}[]}}) => {
  data.json.images = data.json.images.map( (image) => {
    return {
      ...image,
      uri: `${image.uri.replace('../../textures','//3D/textures')}`,
    }
  });
}

// Before loading a resource, take the previously rewritten URL that Babylon 
// is requesting and rewrite it.
loader.preprocessUrlAsync = (url:string):Promise<string> => {
  return Promise.resolve(`${url.split('//')[1]}`);
}

I think if you do this, you will be able to pass absolute urls. I haven’t tried though.

const loader = BABYLON.SceneLoader.Append("", "/3D/modelsmodel.glb", this.scene, (s) => {
  // Do something with loaded scene  
})! as any;

We don’t currently officially support absolute urls or .. in urls. If you want support for either of these, please file a feature request on GitHub or better yet contribute a change. :wink:

Just to close this off, I have it working perfectly with this little bit of hackery. I tried digging into the loader code in the hope of seeing a quick patch but it’s quite a complex execution path. I’ll continue working on this project and then come back to it and hopefully contribute a feature when I clean up the code. Thanks all!

Sorry for the necro, but I ran into this today when trying to have a single texture directory shared for all the gltf models. Each model was in a separate directory with a parallel directory for textures, so the path to the image was “…/textures/foo.png”.

I’ll see your hacked solution and raise (lower?) you one:

import {GLTFLoader} from "@babylonjs/loaders/glTF/2.0/glTFLoader"
//@ts-ignore
GLTFLoader._ValidateUri = function(uri: string) { return true; }

Have you finished?

Sorry to bump this up.
Ran into this issue today.
Why does babylonjs gltf loader prevents relative url?
gltf specs allows it.
Isn’t allowing access to various file the responsibility of the server?

The server is already the authority on what’s accessible — if a path is forbidden, the server returns 403. If the file doesn’t exist, it returns 404. The browser’s same-origin policy handles cross-origin concerns. There’s no real security gap that the loader is filling by blocking ../.

cc @bghgary

You’re right that for a browser hitting a properly‑configured same‑origin server, the server is the authority on access and .. is largely harmless there.

The loader blocks it anyway because it’s a library that doesn’t only run in that environment. The same glTF loader runs in Node, Electron, and Babylon Native, and it’s often pointed at untrusted, user‑supplied assets (drop‑in viewers, asset pipelines, etc.). In those cases there’s no HTTP server returning 403/404 and no same‑origin policy — a uri like ../../../something resolves against the local filesystem, and a malicious asset could read files the app never intended to expose. The loader can’t tell which environment it’s in, so it errs toward a conservative, safe‑by‑default rule for security/privacy rather than assuming every host is locked down.

On the spec: it doesn’t actually bless ... The portable URI types in glTF 2.0 §2.8 URIs are data URIs and scheme‑less relative paths (path-noscheme). http://, file://, and absolute paths are explicitly optional (“MAY”) and called out as “less portable.” And URIs SHOULD undergo syntax‑based normalization (RFC 3986 §6.2.2) on import/export — which removes ../. segments — with a note that non‑normalized URIs “may be unsupported or lead to unwanted side‑effects — such as security warnings or cache misses — on some platforms.” So leaning on .. is already outside the portable, normalized subset the spec encourages.

That said, I agree the strict default is inconvenient for controlled setups, which is why I mentioned earlier in this thread that we could add an opt‑in flag to relax it. If you’d like that, a feature request — or better, a PR — on GitHub is the way to move it forward.

In the meantime, one way to handle the shared‑textures case without ..: keep the uri clean (a filename or sub‑path) and remap it to wherever the file actually lives with preprocessUrlAsync. The restriction is only on the literal uri in the asset, not on where the bytes ultimately come from:

SceneLoader.OnPluginActivatedObservable.add((loader) => {
    if (loader.name === "gltf") {
        loader.preprocessUrlAsync = (url) => {
            // remap only your shared textures; leave the model's own .bin/.glb alone
            const file = url.substring(url.lastIndexOf("/") + 1);
            if (/\.(png|jpe?g|webp|ktx2)$/i.test(file)) {
                return Promise.resolve("https://server.com/shared/textures/" + file);
            }
            return Promise.resolve(url);
        };
    }
});

That gives you a single shared textures directory (and the cache reuse you’re after) without putting .. in the asset, and without monkey‑patching _ValidateUri like earlier in the thread.

@bghgary
Thanks for weighing in on this.
I have created a feature request.
Pardon the formality. I used AI to assist me with it.

Looking into doing a PR too.

Regards