I’ve come up with a solution that seems to work fine for us.
I think it’s hard to come up with a better one without fundamentally changing the way the semantics are tied to the attribute locations. They’re pretty much static and making them dynamic would require some changes that would break existing code, especially code that relies on the generic SEMANTIC_ATTRx
.
So instead I’ve come up with a simple solution that adds four SEMANTIC_CUSTOMx
semantics. We could use something like SEMANTIC_GLTFATTRx
for naming. I made the attribute locations for these semantics to overlap with the last four texture coordinates semantics
semanticToLocation[SEMANTIC_POSITION] = 0;
semanticToLocation[SEMANTIC_NORMAL] = 1;
semanticToLocation[SEMANTIC_BLENDWEIGHT] = 2;
semanticToLocation[SEMANTIC_BLENDINDICES] = 3;
semanticToLocation[SEMANTIC_COLOR] = 4;
semanticToLocation[SEMANTIC_TEXCOORD0] = 5;
semanticToLocation[SEMANTIC_TEXCOORD1] = 6;
semanticToLocation[SEMANTIC_TEXCOORD2] = 7;
semanticToLocation[SEMANTIC_TEXCOORD3] = 8;
semanticToLocation[SEMANTIC_TEXCOORD4] = 9;
semanticToLocation[SEMANTIC_CUSTOM0] = 9;
semanticToLocation[SEMANTIC_TEXCOORD5] = 10;
semanticToLocation[SEMANTIC_CUSTOM1] = 10;
semanticToLocation[SEMANTIC_TEXCOORD6] = 11;
semanticToLocation[SEMANTIC_CUSTOM2] = 11;
semanticToLocation[SEMANTIC_TEXCOORD7] = 12;
semanticToLocation[SEMANTIC_CUSTOM3] = 12;
semanticToLocation[SEMANTIC_TANGENT] = 13;
and the changes to the glb-parser
are quite light, they only affect createVertexBuffer
const createVertexBuffer = (device, attributes, indices, accessors, bufferViews, flipV, vertexBufferDict) => {
// extract list of attributes to use
const useAttributes = {};
const customAttributes = {};
const attribIds = [];
let customAttributesCount = 0;
for (const attrib in attributes) {
if (attributes.hasOwnProperty(attrib) && (gltfToEngineSemanticMap.hasOwnProperty(attrib) ||
(attrib.startsWith('_') && (++customAttributesCount) && customAttributesCount < 4))) {
useAttributes[attrib] = attributes[attrib];
// build unique id for each attribute in format: Semantic:accessorIndex
attribIds.push(attrib + ':' + attributes[attrib]);
}
}
customAttributesCount = 0;
// sort unique ids and create unique vertex buffer ID
attribIds.sort();
const vbKey = attribIds.join();
// return already created vertex buffer if identical
let vb = vertexBufferDict[vbKey];
if (!vb) {
// build vertex buffer format desc and source
const sourceDesc = {};
for (const attrib in useAttributes) {
const accessor = accessors[attributes[attrib]];
const accessorData = getAccessorData(accessor, bufferViews);
const bufferView = bufferViews[accessor.bufferView];
let semantic = SEMANTIC_POSITION;
if (gltfToEngineSemanticMap.hasOwnProperty(attrib)) {
semantic = gltfToEngineSemanticMap[attrib];
} else {
semantic = SEMANTIC_CUSTOM + customAttributesCount;
customAttributesCount++;
customAttributes[attrib] = semantic;
}
const size = getNumComponents(accessor.type) * getComponentSizeInBytes(accessor.componentType);
const stride = bufferView && bufferView.hasOwnProperty('byteStride') ? bufferView.byteStride : size;
sourceDesc[semantic] = {
buffer: accessorData.buffer,
size: size,
offset: accessorData.byteOffset,
stride: stride,
count: accessor.count,
components: getNumComponents(accessor.type),
type: getComponentType(accessor.componentType),
normalize: accessor.normalized
};
}
// generate normals if they're missing (this should probably be a user option)
if (!sourceDesc.hasOwnProperty(SEMANTIC_NORMAL)) {
generateNormals(sourceDesc, indices);
}
// create and store it in the dictionary
vb = createVertexBufferInternal(device, sourceDesc, flipV);
vertexBufferDict[vbKey] = vb;
}
return vb;
};
and the attribute order used for sorting
// order vertexDesc to match the rest of the engine
const attributeOrder = {
[SEMANTIC_POSITION]: 0,
[SEMANTIC_NORMAL]: 1,
[SEMANTIC_TANGENT]: 2,
[SEMANTIC_COLOR]: 3,
[SEMANTIC_BLENDINDICES]: 4,
[SEMANTIC_BLENDWEIGHT]: 5,
[SEMANTIC_TEXCOORD0]: 6,
[SEMANTIC_TEXCOORD1]: 7,
[SEMANTIC_TEXCOORD2]: 8,
[SEMANTIC_TEXCOORD3]: 9,
[SEMANTIC_TEXCOORD4]: 10,
[SEMANTIC_CUSTOM0]: 10,
[SEMANTIC_TEXCOORD5]: 11,
[SEMANTIC_CUSTOM1]: 11,
[SEMANTIC_TEXCOORD6]: 12,
[SEMANTIC_CUSTOM2]: 12,
[SEMANTIC_TEXCOORD7]: 13,
[SEMANTIC_CUSTOM3]: 13
};
Then we can easily use the custom semantics afterwards in our material system
export class SimpleTreeMaterial extends CustomMaterial {
static materialType = `SimpleTreeMaterial`;
static shaderGenerator = new CustomShaderGenerator(
SimpleTreeMaterial.materialType,
{
aPosition: pc.SEMANTIC_POSITION,
aNormal: pc.SEMANTIC_NORMAL,
aUv: pc.SEMANTIC_TEXCOORD0,
aColor: pc.SEMANTIC_COLOR,
aPivot0: pc.SEMANTIC_CUSTOM0,
aPivot1: pc.SEMANTIC_CUSTOM1,
},
undefined,
{
Forward: { vshader: simpleTreeVS, fshader: simpleTreePS },
}
);
....
It has the downside that you can get overlapping attribute locations if you have more than four UV layers in your meshes but that is easily avoided since we define what attributes we have in our GLTF files. You can either have up to 8 UV layers or 4 of them and 4 custom vertex attributes to use however you like.