GLTFExporter Breaks when using multiple materials

Hey all, I’m having issues with GLTFExporter only when multiple materials are used, single material exports work just fine. Wondering if anyone can help me figure this out.

Couple things to note, I just updated to the latest version of three so I could use the exporter with data textures. As part of that update, I just took my deprecated geometry and face3 uses and did geometry.toBufferGeometry on them. Didn’t see a more in-depth guide on how to use buffer geometry, so i left it at that. Not sure if that is related.

I have created custom model loaders that load assets from playstation games. Due to the way the playstation’s texturing works, many 3d models use multiple textures, and I therefore create multiple materials for them. This works just fine within threejs:

You can see the original three textures there on the left, which are converted to threejs materials. So far so good. However, after calling export with GLTFExporter, I get this:

This only occurs when multiple materials are used. See this model that only uses a single material:

Exported:

Any ideas what would cause the texture mapping to break on models with multiple materials?

Do you mind exporting a character model with multiple textures to three.jss JSON format via model.toJSON() and sharing the contents in this topic? This JSON should contain all model data including textures. I can then import this JSON on my side and give the glTF export a try.

Definitely, thanks!

The original parappa model in the above images is available here: three.json - Google Drive

However, I found a much simpler test model, made up of only two rectangles (split into triangles of course), but since it uses 2 materials, it is also broken. I made a complete standalone reproduction that renders the mesh and exports the gltf here: https://roblouie.com/gltf-test/

If you want to grab just the model json for that smaller test, its here: https://roblouie.com/gltf-test/three-skunk.json

So, I went and read the spec on the gltf file format. Took a bit but it was pretty straightforward and I was able to fix my file by editing it directly. I have attached the fixed file. While I now know what the problem is, fixing it in the pre-existing gltfexporter might be hard…I am going to take a look though.

I will for sure explain the problem and my solution here in detail, so that even if I don’t fix it hopefully it helps someone else fix it?

The main problem is that multiple primitives refer back to the same bufferView. From the skunk file:

"primitives": [
        {
          "mode": 4,
          "attributes": {
            "POSITION": 0,
            "NORMAL": 1,
            "COLOR_0": 2,
            "TEXCOORD_0": 3
          },
          "material": 0
        },
        {
          "mode": 4,
          "attributes": {
            "POSITION": 0,
            "NORMAL": 1,
            "COLOR_0": 2,
            "TEXCOORD_0": 3
          },
          "material": 1
        }
      ]

You can see that both primitives use bufferView 3 for the texcoords. I did write a script that went through buffer view 3 and validated that all the uvs are in fact in there. However, the uvs for the entire model are in there. I’ll be the first to admit I am not an expert at webgl and graphics buffers in general, but intuitively it felt wrong for both of these separate primitives to use the same texcoords (and in fact same position, normal, and color) index.

It would seem to me that instead, each primitive needs its own set of indexes for position, normal, color, and texcoords. For instance the skunk model has 96 uv entries in its array. However only the 0-47 apply to the first primitive, the next primitive needs to grab 48-95. In the current implementation, idk how the render would know that for primitive 2, use bufferView 3, but start halfway through it…same for position, normal, color, etc. (again not an expert, just seems wrong)

So, I split them out. I created a new buffer view for position, a new buffer view for normal, a new buffer view for color, and a new buffer view for texcoords. Now each primitive gets its own set of buffer views.

So buffer views go from:

{
      "buffer": 0,
      "byteOffset": 0,
      "byteLength": 576,
      "target": 34962,
      "byteStride": 12
    },
    {
      "buffer": 0,
      "byteOffset": 576,
      "byteLength": 576,
      "target": 34962,
      "byteStride": 12
    },
    {
      "buffer": 0,
      "byteOffset": 1152,
      "byteLength": 576,
      "target": 34962,
      "byteStride": 12
    },
    {
      "buffer": 0,
      "byteOffset": 1728,
      "byteLength": 384,
      "target": 34962,
      "byteStride": 8
    }

to

 {
      "buffer": 0,
      "byteOffset": 0,
      "byteLength": 288,
      "target": 34962,
      "byteStride": 12
    },
    {
      "buffer": 0,
      "byteOffset": 288,
      "byteLength": 288,
      "target": 34962,
      "byteStride": 12
    },
    {
      "buffer": 0,
      "byteOffset": 576,
      "byteLength": 288,
      "target": 34962,
      "byteStride": 12
    },
    {
      "buffer": 0,
      "byteOffset": 864,
      "byteLength": 288,
      "target": 34962,
      "byteStride": 12
    },
    {
      "buffer": 0,
      "byteOffset": 1152,
      "byteLength": 288,
      "target": 34962,
      "byteStride": 12
    },
    {
      "buffer": 0,
      "byteOffset": 1440,
      "byteLength": 288,
      "target": 34962,
      "byteStride": 12
    },
    {
      "buffer": 0,
      "byteOffset": 1728,
      "byteLength": 192,
      "target": 34962,
      "byteStride": 8
    },
    {
      "buffer": 0,
      "byteOffset": 1920,
      "byteLength": 192,
      "target": 34962,
      "byteStride": 8
    }

There are twice as many buffer views now, but each is half the length of the old one. Effectively giving me two separate views into the buffer for each attribute. Of course I had to update the accessors and indexes in the primitives as well. You can see the final result in the attached file.

I do get validation errors on min/max, because those are no longer correct since half the values now live in a different accessor, but thats a simple fix and does not stop the model from rendering correctly.

Now each primitive gets its own set of buffer views, with primitive two’s buffer views starting half way into the original larger one

On a separate note, it seems like the primitives come out on the wrong order, causing the one on the bottom to be drawn over the one at the top, so I swapped their order, leaving my primitive array looking like:

"primitives": [
      {
        "mode": 4,
        "attributes": {
          "POSITION": 1,
          "NORMAL": 3,
          "COLOR_0": 5,
          "TEXCOORD_0": 7
        },
        "material": 1
      },
        {
          "mode": 4,
          "attributes": {
            "POSITION": 0,
            "NORMAL": 2,
            "COLOR_0": 4,
            "TEXCOORD_0": 6
          },
          "material": 0
        }
      ]

Hopefully that helps somewhat to clear up whats going on? Hard to explain via post, and also, I only started reading about this file format last night so I could have something wrong, but the file does render correctly now.

model (2).gltf (63.5 KB)

As I’ve continued to look into this, I’ve learned that there is a simpler way. Accessors have a byteOffset as well, so there’s no need to create more bufferViews, simply more accessors, just giving every other one a different byte offset.

@Rob_L Do you mind offering advice on an approach to be able to edit the the pre-processing or post-processing of the object with multiple materials directly in the js code, rather than editing the file with a script? Did you end up doing this?