vpenades / SharpGLTF

glTF reader and writer for .NET Standard
MIT License
454 stars 72 forks source link

[QUESTION] Is there a more propper way to generate a mesh which contains primitives that have different vertex fragment types? #207

Closed FlyingBamboo closed 6 months ago

FlyingBamboo commented 6 months ago

I am using the toolkit to generate glTF files. It's really easy to use and works well except for one problem. In my case, it is necessary to create some primitives using materials with or without texture, which means they may have TEXCOORD_0 or not. The primitives may belong to one mesh. I can only use MeshBuilder<VertexPositionNormal, VertexTexture1> to create the meshBuilder to support both conditions, but in this way, unused TEXCOORD attribute is generated, the accessor point to some empty part of the buffer view. Please look at the example below showing the issue:

var sceneBuilder = new SceneBuilder();
var nodeBuilder = new NodeBuilder();
var meshBuilder = new MeshBuilder<VertexPositionNormal, VertexTexture1>();
var materialBuilder = new MaterialBuilder()
    .WithBaseColor(new Vector4(1, 1, 1, 1))
    .WithMetallicRoughnessShader()
    .WithMetallicRoughness(0, 1);
var primitive = meshBuilder.UsePrimitive(materialBuilder);
var v1 = new VertexPositionNormal(new Vector3(0, 0, 0), new Vector3(0, 0, 1));
var v2 = new VertexPositionNormal(new Vector3(0, 1, 0), new Vector3(0, 0, 1));
var v3 = new VertexPositionNormal(new Vector3(1, 1, 0), new Vector3(0, 0, 1));
primitive.AddTriangle(v1, v2, v3);

var materialBuilder2 = new MaterialBuilder()
    .WithBaseColor(new Vector4(1, 1, 1, 1))
    .WithChannelImage(KnownChannel.BaseColor, "./texture.jpg")
    .WithMetallicRoughnessShader()
    .WithMetallicRoughness(0, 1);
var primitive2 = meshBuilder.UsePrimitive(materialBuilder2);
var n1 = new VertexTexture1(new Vector2(0, 1));
var n2 = new VertexTexture1(new Vector2(1, 0));
var n3 = new VertexTexture1(new Vector2(1, 1));
primitive2.AddTriangle((v1, n1), (v2, n2), (v3, n3));

sceneBuilder.AddRigidMesh(meshBuilder, nodeBuilder);
sceneBuilder.ToGltf2().SaveGLTF("./model.gltf");

The generated glTF:

{
  "asset": {
    "copyright": "",
    "generator": "SharpGLTF 1.0.0\u002B681b03b863000800e71e4976735a5da95e6c71f0",
    "version": "2.0"
  },
  "accessors": [
    {
      "name": "POSITION",
      "bufferView": 0,
      "componentType": 5126,
      "count": 3,
      "max": [
        1,
        1,
        0
      ],
      "min": [
        0,
        0,
        0
      ],
      "type": "VEC3"
    },
    {
      "name": "NORMAL",
      "bufferView": 0,
      "byteOffset": 12,
      "componentType": 5126,
      "count": 3,
      "max": [
        0,
        0,
        1
      ],
      "min": [
        0,
        0,
        1
      ],
      "type": "VEC3"
    },
    {
      "name": "TEXCOORD_0",
      "bufferView": 0,
      "byteOffset": 24,
      "componentType": 5126,
      "count": 3,
      "max": [
        0,
        0
      ],
      "min": [
        0,
        0
      ],
      "type": "VEC2"
    },
    {
      "bufferView": 1,
      "componentType": 5123,
      "count": 3,
      "type": "SCALAR"
    },
    {
      "name": "POSITION",
      "bufferView": 0,
      "byteOffset": 96,
      "componentType": 5126,
      "count": 3,
      "max": [
        1,
        1,
        0
      ],
      "min": [
        0,
        0,
        0
      ],
      "type": "VEC3"
    },
    {
      "name": "NORMAL",
      "bufferView": 0,
      "byteOffset": 108,
      "componentType": 5126,
      "count": 3,
      "max": [
        0,
        0,
        1
      ],
      "min": [
        0,
        0,
        1
      ],
      "type": "VEC3"
    },
    {
      "name": "TEXCOORD_0",
      "bufferView": 0,
      "byteOffset": 120,
      "componentType": 5126,
      "count": 3,
      "max": [
        1,
        1
      ],
      "min": [
        0,
        0
      ],
      "type": "VEC2"
    },
    {
      "bufferView": 1,
      "byteOffset": 6,
      "componentType": 5123,
      "count": 3,
      "type": "SCALAR"
    }
  ],
  "bufferViews": [
    {
      "buffer": 0,
      "byteLength": 192,
      "byteStride": 32,
      "target": 34962
    },
    {
      "buffer": 0,
      "byteLength": 12,
      "byteOffset": 192,
      "target": 34963
    }
  ],
  "buffers": [
    {
      "byteLength": 204,
      "uri": "model.bin"
    }
  ],
  "images": [
    {
      "uri": "model.jpg"
    }
  ],
  "materials": [
    {
      "pbrMetallicRoughness": {
        "metallicFactor": 0
      }
    },
    {
      "pbrMetallicRoughness": {
        "baseColorTexture": {
          "index": 0
        },
        "metallicFactor": 0
      }
    }
  ],
  "meshes": [
    {
      "primitives": [
        {
          "attributes": {
            "POSITION": 0,
            "NORMAL": 1,
            "TEXCOORD_0": 2
          },
          "indices": 3,
          "material": 0
        },
        {
          "attributes": {
            "POSITION": 4,
            "NORMAL": 5,
            "TEXCOORD_0": 6
          },
          "indices": 7,
          "material": 1
        }
      ]
    }
  ],
  "nodes": [
    {
      "mesh": 0
    }
  ],
  "scene": 0,
  "scenes": [
    {
      "nodes": [
        0
      ]
    }
  ],
  "textures": [
    {
      "source": 0
    }
  ]
}

Is it possible to remove the TEXCOORD_0 in the first primitive?

vpenades commented 6 months ago

I'm afraid the only way to do what you want is to use the lower level APIs to construct the mesh on your own.

The way to do so is to create the vertex and index buffers, create the accessors, and then the mesh primitives and the mesh

FlyingBamboo commented 6 months ago

Thanks for your quick reply! I can't give up on using the toolkit, so I'm going to try modifying the generated ModelRoot before saving to a glTF file.

vpenades commented 6 months ago

MeshBuilder uses a unique vertex definition for the whole mesh for two reasons: 1- it simplifies the mesh building process. 2- a single vertex format guarantees that all the vertex buffers used by the mesh can be merged into a single vertex buffer. Actually, if all the meshes of the model share the same vertex format, it could lead to use a single vertex buffer for the whole model.

Having said that, the post processor that converts the meshbuilder to a glTF mesh could be improved to identify attributes that have been filled with zeros, and re-arrange the data to remove unused attributes. The bad news is that this is one of the trickiest parts of the whole library and it would not be easy to deal with.