Incompatibility in how relative URLs are resolved, when used together with EXTERNPROTO - michaliskambi/x3d-tests GitHub Wiki

We have uncovered a serious incompatibility in handling of relative URLs when combined with EXTERNPROTO.

The incompatibility only affects browsers that handle EXTERNPROTO, so testing X3DOM is not relevant here.

The issue (and what to do forward) is made even more difficult because, in my mind, both approaches are somewhat wrong, i.e. surprising and undesired in some (but different) scenarios.

Table of Contents:

Testcases

This repository contains a ready testcases for everything described below: https://github.com/michaliskambi/x3d-tests/tree/master/relative_urls_in_externproto .

We have also a much more elaborate testcase, exercising various edge-cases around this, inside the Castle Game Engine demo-models, subdirectory prototypes/relative_urls_test: https://github.com/castle-engine/demo-models/tree/master/prototypes/relative_urls_test . You probably don't want to look there immediately -- the testcase we created there got quite complicated :)

Instant Reality, view3dscene (and Castle Game Engine) and X_ITE approach

Instant Reality player, view3dscene and X_ITE resolve URLs with respect to the file that contains the node that contains this URL. Note that in some edge-cases (see below) this is not the same thing as "file that contains the URL".

This case is handled in a natural way:

  • One X3D file prototype.x3dv defines a PROTO and refers to a texture with relative URL:
    #X3D V3.2 utf8
    PROFILE Interchange
    PROTO MyGeometry [ ] {
      Shape {
        geometry Box { }
        appearance Appearance { texture ImageTexture { url "texture.png" } }
      }
    }
    
  • The file prototype.x3dv and the accompanying texture.png are in a subdirectory, like textures/.
  • Now another X3D file, let us call it parent_scene.x3dv, placed as a sibling of the textures/ directory (so, one level above prototype.x3dv and texture.png) instantiates the prototype:
    #X3D V3.2 utf8
    PROFILE Interchange
    
    EXTERNPROTO MyGeometry [
    ] [ "textures/prototype.x3dv#MyGeometry" ]
    
    MyGeometry { }
    

The above works (in Instant Reality / view3dscene / X_ITE), and seems natural for the X3D author.

However, if the prototype exposes an MFString field to configure the texture URL, it is not longer intuitive. This is what works (in Instant Reality / view3dscene / X_ITE), but is not natural to the X3D author:

  • One X3D file prototype.x3dv defines a PROTO and refers to a texture with a configrable URL:
    #X3D V3.2 utf8
    PROFILE Interchange
    PROTO MyGeometry [ 
      inputOutput MFString textureUrl ""
    ] {
      Shape {
        geometry Box { }
        appearance Appearance { texture ImageTexture { url IS textureUrl } }
      }
    }
    
  • Just like before, the file prototype.x3dv and the accompanying texture.png are in a subdirectory, like textures/.
  • Now another X3D file, parent_scene.x3dv, placed as a sibling of the textures/ directory (so, one level above prototype.x3dv and texture.png) instantiates the prototype:
    #X3D V3.2 utf8
    PROFILE Interchange
    
    EXTERNPROTO MyGeometry [
      inputOutput MFString textureUrl
    ] [ "textures/prototype.x3dv#MyGeometry" ]
    
    MyGeometry { textureUrl "texture.png" }
    

What's unnatural above? parent_scene.x3dv refers to prototype.x3dv using a relative URL like this: textures/prototype.x3dv. However, it needs to refer to the texture using a relative URL like this: texture.png (without the textures/ subdirectory). That is because the URL is actually copied to the ImageTexture.url inside textures/prototype.x3dv, and it is resolved with respect to the textures/prototype.x3dv (not with respect to the parent_scene.x3dv).

So it works, but it's surprising for a normal user. It would be more natural to be able to write

MyGeometry { textureUrl "textures/texture.png" }

... but it doesn't work (in Instant Reality / view3dscene / X_ITE).

The problem is most dire when the prototype.x3dv is on a different server than the parent_scene.x3dv. Like, http://proto-definitions.org/prototype.x3dv and http://proto-instances.org/parent_scene.x3dv. In this case, parent_scene.x3dv has no way to pass a relative URL to a texture on it's own server (proto-instances.org). All relative URLs are resolved on server proto-definitions.org. So parent_scene.x3dv is forced to use absolute URLs.

BS Contact and Cortona approach

BS Contact and Cortona override the base URLs in expanded prototype (used to resolve relative URLs) to match the URL of the file with EXTERNPROTO declaration. This makes this case behave naturally:

  • textures/prototype.x3dv looks like this:
    #X3D V3.2 utf8
    PROFILE Interchange
    PROTO MyGeometry [ 
      inputOutput MFString textureUrl ""
    ] {
      Shape {
        geometry Box { }
        appearance Appearance { texture ImageTexture { url IS textureUrl } }
      }
    }
    
  • parent_scene.x3dv, looks like this:
    #X3D V3.2 utf8
    PROFILE Interchange
    
    EXTERNPROTO MyGeometry [
      inputOutput MFString textureUrl
    ] [ "textures/prototype.x3dv#MyGeometry" ]
    
    MyGeometry { textureUrl "textures/texture.png" }
    

Note that parent_scene.x3dv now, intuitively, can refer to resources using relative URLs versus it's own directory. So textures/prototype.x3dv or textures/texture.png.

However, on the other hand, the unnatural (and uncomfortable for authors) behavior arises when you try to use a relative URL inside a prototype definition. You need to write this:

  • textures/prototype.x3dv looks like this:
    #X3D V3.2 utf8
    PROFILE Interchange
    PROTO MyGeometry [ 
    ] {
      Shape {
        geometry Box { }
        appearance Appearance { texture ImageTexture { url "textures/texture.png" } }
      }
    }
    
  • parent_scene.x3dv, looks like this:
    #X3D V3.2 utf8
    PROFILE Interchange
    
    EXTERNPROTO MyGeometry [
    ] [ "textures/prototype.x3dv#MyGeometry" ]
    
    MyGeometry { }
    

What is weird above is that prototype.x3dv has to refer to the texture (that resides in the same directory as prototype.x3dv!) using a path textures/texture.png. Why? Because when the prototype is expanded, the URL will be resolved within parent_scene.x3dv.

So the PROTO author cannot reliably use relative URLs to refer to resources relative to the file that defines the prototype. If the PROTO author wants to use a texture on proto-definitions.org server, it cannot use relative URLs for this, even though the texture and prototype.x3dv are both on proto-definitions.org server. The URLs are resolved with respect to the file that instantiates this prototype, so each instance may refer to a different file (texture, inlined 3d model etc.).

We added in view3dscene a special new flag RebaseRelativeUrlsInProtos to make it (partially) compatible with BS Contact in this regard. By default, view3dscene (past and future) is compatible 100% with Instant Player and X_ITE in this regard.

See also https://github.com/michaliskambi/x3d-tests/issues/3 for tests on more X3D browsers.

A solution for X3D authors that works in all browsers intutively

How to specify a relative URL inside a parent_scene.x3dv in a way that works in both browsers?

One workaround is to provide alternatives relative URLs like this:

  MyGeometry { textureUrl [ "textures/texture.png" "texture.png" ] }

The better solution, that works in all browsers and is intuitive for authors, is to never pass MFString as prototype parameter for URLs. Instead pass texture node as SFNode parameter. This works intuitively in all browsers. It looks like this:

  • In textures/prototype.x3dv defines a PROTO with a configrable texture node:
    #X3D V3.2 utf8
    PROFILE Interchange
    PROTO MyGeometry [ 
      inputOutput SFNode textureNode NULL
    ] {
      Shape {
        geometry Box { }
        appearance Appearance { texture IS textureNode }
      }
    }
    
  • In parent_scene.x3dv you can instantiate it like this:
    #X3D V3.2 utf8
    PROFILE Interchange
    
    EXTERNPROTO MyGeometry [
      inputOutput SFNode textureNode
    ] [ "textures/prototype.x3dv#MyGeometry" ]
    
    MyGeometry { textureNode ImageTexture { url "textures/texture.png" } }
    

This will work OK, because the PROTO parameter is now a complete node, and it's BaseUrl will behave in an intuitive way always: { url "textures/texture.png" } is relative to the file that instantiates the prototype, under both Instant Reality, view3dscene, BS Contact and Cortona.

How should the specification look like?

The X3D specification says this . It's not clear from this description which approach is right.

The perfect behavior (but this is something that noone implements, and specification doesn't allow it definitely) is to resolve URLs always with respect to the file in which the URL is written. But this requires storing base URL at each field (not only at node) to make all cases presented above OK. So it does make implementation more complicated, and possibly more memory-hungry (additional info per every field is a significant cost in some scenes). But it would make things always behave correctly.