Skip to content

Commit

Permalink
[Fix] GltfExporter correctly exports buffer views for non-interleaved…
Browse files Browse the repository at this point in the history
… vertex data (#4699)

* [Fix] GltfExporter correctly exports buffer views for non-interleaved vertex data

* cleanup

* remove empty arrays to avoid validation error

* specify stride to avoid sharing error

* updates

* ignore unsupported textures without crashing

Co-authored-by: Martin Valigursky <mvaligursky@snapchat.com>
  • Loading branch information
mvaligursky and Martin Valigursky authored Oct 7, 2022
1 parent f3c44fd commit 3b8e93a
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 69 deletions.
8 changes: 5 additions & 3 deletions extras/exporters/core-exporter.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
class CoreExporter {
/**
* Converts a source image specified in multiple formats to a canvas.
* Converts a texture to a canvas.
*
* @param {any} image - The source image to be converted.
* @param {Texture} texture - The source texture to be converted.
* @param {object} options - Object for passing optional arguments.
* @param {Color} [options.color] - The tint color to modify the texture with.
* @param {number} [options.maxTextureSize] - Maximum texture size. Texture is resized if over the size.
* @returns {any} - The canvas element containing the image.
*/
imageToCanvas(image, options = {}) {
textureToCanvas(texture, options = {}) {

const image = texture.getSource();

if ((typeof HTMLImageElement !== 'undefined' && image instanceof HTMLImageElement) ||
(typeof HTMLCanvasElement !== 'undefined' && image instanceof HTMLCanvasElement) ||
Expand Down
151 changes: 93 additions & 58 deletions extras/exporters/gltf-exporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,10 @@ class GltfExporter extends CoreExporter {
textures: [],

// entry: { node, meshInstances}
entityMeshInstances: []
entityMeshInstances: [],

// maps a buffer (vertex or index) to an array of bufferview indices
bufferViewMap: new Map()
};

const { materials, buffers, entityMeshInstances, textures } = resources;
Expand Down Expand Up @@ -175,34 +178,58 @@ class GltfExporter extends CoreExporter {
}

writeBufferViews(resources, json) {
if (resources.buffers.length > 0) {
let offset = 0;

json.bufferViews = resources.buffers.map((buffer) => {
const arrayBuffer = buffer.lock();
json.bufferViews = [];
let offset = 0;

resources.buffers.forEach((buffer) => {

const addBufferView = (target, byteLength, byteOffset, byteStride) => {

const bufferView = {
const bufferView = {
target: target,
buffer: 0,
byteLength: arrayBuffer.byteLength,
byteOffset: offset
byteLength: byteLength,
byteOffset: byteOffset,
byteStride: byteStride
};

if (buffer instanceof pc.VertexBuffer) {
bufferView.target = ARRAY_BUFFER;
const format = buffer.getFormat();
return json.bufferViews.push(bufferView) - 1;
};

const arrayBuffer = buffer.lock();

if (buffer instanceof pc.VertexBuffer) {

const format = buffer.getFormat();
if (format.interleaved) {

const bufferViewIndex = addBufferView(ARRAY_BUFFER, arrayBuffer.byteLength, offset, format.size);
resources.bufferViewMap.set(buffer, [bufferViewIndex]);

if (format.interleaved) {
bufferView.byteStride = format.size;
}
} else {
bufferView.target = ELEMENT_ARRAY_BUFFER;

// generate buffer view per element
const bufferViewIndices = [];
format.elements.forEach((element) => {

const bufferViewIndex = addBufferView(ARRAY_BUFFER, element.size * format.vertexCount, offset + element.offset, element.size);
bufferViewIndices.push(bufferViewIndex);

});

resources.bufferViewMap.set(buffer, bufferViewIndices);
}

offset += arrayBuffer.byteLength;
} else { // index buffer

return bufferView;
});
}
const bufferViewIndex = addBufferView(ELEMENT_ARRAY_BUFFER, arrayBuffer.byteLength, offset);
resources.bufferViewMap.set(buffer, [bufferViewIndex]);

}

offset += arrayBuffer.byteLength;
});
}

writeCameras(resources, json) {
Expand Down Expand Up @@ -348,33 +375,32 @@ class GltfExporter extends CoreExporter {
// all mesh instances of a single node are stores as a single gltf mesh with multiple primitives
const meshInstances = entityMeshInstances.meshInstances;
meshInstances.forEach((meshInstance) => {
const indexBuffer = meshInstance.mesh.indexBuffer[0];
const vertexBuffer = meshInstance.mesh.vertexBuffer;
const vertexFormat = vertexBuffer.getFormat();
const numVertices = vertexBuffer.getNumVertices();

const primitive = {
attributes: {},
material: resources.materials.indexOf(meshInstance.material)
};
mesh.primitives.push(primitive);

// An accessor is a vertex attribute
const writeAccessor = (element) => {
// vertex buffer
const { vertexBuffer } = meshInstance.mesh;
const { format } = vertexBuffer;
const { interleaved, elements } = format;
const numVertices = vertexBuffer.getNumVertices();
elements.forEach((element, elementIndex) => {

const viewIndex = resources.bufferViewMap.get(vertexBuffer)[interleaved ? 0 : elementIndex];

const accessor = {
bufferView: resources.buffers.indexOf(vertexBuffer),
byteOffset: element.offset,
bufferView: viewIndex,
byteOffset: interleaved ? element.offset : 0,
componentType: getComponentType(element.dataType),
type: getAccessorType(element.numComponents),
count: numVertices
};

const idx = json.accessors.length;
json.accessors.push(accessor);

const semantic = getSemantic(element.name);
primitive.attributes[semantic] = idx;
const idx = json.accessors.push(accessor) - 1;
primitive.attributes[getSemantic(element.name)] = idx;

// Position accessor also requires min and max properties
if (element.name === pc.SEMANTIC_POSITION) {
Expand All @@ -389,24 +415,22 @@ class GltfExporter extends CoreExporter {
accessor.min = [min.x, min.y, min.z];
accessor.max = [max.x, max.y, max.z];
}
};

vertexFormat.elements.forEach(writeAccessor);
});

// index buffer
const indexBuffer = meshInstance.mesh.indexBuffer[0];
if (indexBuffer) {
const ibIdx = resources.buffers.indexOf(indexBuffer);

const viewIndex = resources.bufferViewMap.get(indexBuffer)[0];

const accessor = {
bufferView: ibIdx,
bufferView: viewIndex,
componentType: getIndexComponentType(indexBuffer.getFormat()),
count: indexBuffer.getNumIndices(),
type: "SCALAR"
};

json.accessors.push(accessor);

const idx = json.accessors.indexOf(accessor);

const idx = json.accessors.push(accessor) - 1;
primitive.indices = idx;
}
});
Expand All @@ -429,28 +453,34 @@ class GltfExporter extends CoreExporter {
const isRGBA = true;
const mimeType = isRGBA ? 'image/png' : 'image/jpeg';

// convert texture data to uri
const texture = textures[i];
const mipObject = texture._levels[0];
const canvas = this.textureToCanvas(texture, textureOptions);

// convert texture data to uri
const canvas = this.imageToCanvas(mipObject, textureOptions);
const uri = canvas.toDataURL(mimeType);
// if texture format is supported
if (canvas) {
const uri = canvas.toDataURL(mimeType);

json.images[i] = {
'uri': uri
};
json.images[i] = {
'uri': uri
};

json.samplers[i] = {
'minFilter': getFilter(texture.minFilter),
'magFilter': getFilter(texture.magFilter),
'wrapS': getWrap(texture.addressU),
'wrapT': getWrap(texture.addressV)
};
json.samplers[i] = {
'minFilter': getFilter(texture.minFilter),
'magFilter': getFilter(texture.magFilter),
'wrapS': getWrap(texture.addressU),
'wrapT': getWrap(texture.addressV)
};

json.textures[i] = {
'sampler': i,
'source': i
};
json.textures[i] = {
'sampler': i,
'source': i
};
} else {
// ignore it
console.log(`Export of texture ${texture.name} is not currently supported.`);
textures[i] = null;
}
}
}

Expand Down Expand Up @@ -484,6 +514,11 @@ class GltfExporter extends CoreExporter {
this.writeMeshes(resources, json);
this.convertTextures(resources.textures, json, options);

// delete unused properties
if (!json.images.length) delete json.images;
if (!json.samplers.length) delete json.samplers;
if (!json.textures.length) delete json.textures;

return json;
}

Expand Down
22 changes: 14 additions & 8 deletions extras/exporters/usdz-exporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,17 +166,23 @@ class UsdzExporter extends CoreExporter {
const isRGBA = true;
const mimeType = isRGBA ? 'image/png' : 'image/jpeg';

// convert texture data to canvas
const texture = textureArray[i];
const mipObject = texture._levels[0];
const canvas = this.textureToCanvas(texture, textureOptions);

// convert texture data to canvas
const canvas = this.imageToCanvas(mipObject, textureOptions);
// if texture format is supported
if (canvas) {

// async convert them to blog and then to array buffer
// eslint-disable-next-line no-promise-executor-return
promises.push(new Promise(resolve => canvas.toBlob(resolve, mimeType, 1)).then(
blob => blob.arrayBuffer()
));
// async convert them to blog and then to array buffer
// eslint-disable-next-line no-promise-executor-return
promises.push(new Promise(resolve => canvas.toBlob(resolve, mimeType, 1)).then(
blob => blob.arrayBuffer()
));

} else {
// ignore it
console.log(`Export of texture ${texture.name} is not currently supported.`);
}
}

// when all textures are converted
Expand Down

0 comments on commit 3b8e93a

Please sign in to comment.