obj2gltf/specs/lib/createGltfSpec.js
Matthew Amato b87c761073 Monthly Maintenance
* Upgrade prettier to 3.0.3 and format files.
* Upgrade `eslint-config-cesium` and `eslint-config-prettier`.
* Switch from the defunct `eslint-plugin-node` to `eslint-plugin-n`
Since that is what `eslint-config-cesium` now uses.
* Switch from pretty-quick to lint-staged
2023-10-03 13:28:47 -04:00

309 lines
11 KiB
JavaScript

"use strict";
const Cesium = require("cesium");
const obj2gltf = require("../../lib/obj2gltf");
const createGltf = require("../../lib/createGltf");
const loadObj = require("../../lib/loadObj");
const { getDefaultMaterial } = require("../../lib/loadMtl");
const clone = Cesium.clone;
const defined = Cesium.defined;
const WebGLConstants = Cesium.WebGLConstants;
const boxObjPath = "specs/data/box/box.obj";
const groupObjPath =
"specs/data/box-objects-groups-materials/box-objects-groups-materials.obj";
const complexObjPath =
"specs/data/box-complex-material/box-complex-material.obj";
const noMaterialsObjPath = "specs/data/box-no-materials/box-no-materials.obj";
const mixedAttributesObjPath =
"specs/data/box-mixed-attributes-2/box-mixed-attributes-2.obj";
let options;
describe("createGltf", () => {
let boxObjData;
let groupObjData;
let complexObjData;
let noMaterialsObjData;
let mixedAttributesObjData;
beforeEach(async () => {
options = clone(obj2gltf.defaults);
options.overridingTextures = {};
options.logger = () => {};
boxObjData = await loadObj(boxObjPath, options);
groupObjData = await loadObj(groupObjPath, options);
complexObjData = await loadObj(complexObjPath, options);
noMaterialsObjData = await loadObj(noMaterialsObjPath, options);
mixedAttributesObjData = await loadObj(mixedAttributesObjPath, options);
});
it("simple gltf", () => {
const gltf = createGltf(boxObjData, options);
expect(gltf.materials.length).toBe(1);
expect(gltf.scene).toBe(0);
expect(gltf.scenes[0].nodes[0]).toBe(0);
expect(gltf.nodes.length).toBe(1);
expect(gltf.meshes.length).toBe(1);
const primitives = gltf.meshes[0].primitives;
const primitive = primitives[0];
const attributes = primitive.attributes;
const positionAccessor = gltf.accessors[attributes.POSITION];
const normalAccessor = gltf.accessors[attributes.NORMAL];
const uvAccessor = gltf.accessors[attributes.TEXCOORD_0];
const indexAccessor = gltf.accessors[primitive.indices];
expect(primitives.length).toBe(1);
expect(positionAccessor.count).toBe(24);
expect(normalAccessor.count).toBe(24);
expect(uvAccessor.count).toBe(24);
expect(indexAccessor.count).toBe(36);
});
it("does not combine buffers when that buffer would exceed the Node buffer size limit", () => {
spyOn(createGltf, "_getBufferMaxByteLength").and.returnValue(0);
const clonedOptions = clone(options, true);
clonedOptions.separate = true;
const gltf = createGltf(boxObjData, clonedOptions);
expect(gltf.accessors.length).toBe(4);
expect(gltf.buffers.length).toBe(4);
expect(gltf.bufferViews.length).toBe(4);
const length = gltf.buffers.length;
for (let i = 0; i < length; ++i) {
const accessor = gltf.accessors[i];
const bufferView = gltf.bufferViews[i];
const buffer = gltf.buffers[i];
expect(accessor.bufferView).toBe(i);
expect(accessor.byteOffset).toBe(0);
expect(bufferView.buffer).toBe(i);
expect(bufferView.byteOffset).toBe(0);
expect(bufferView.byteLength).toBe(buffer.byteLength);
}
});
it("multiple nodes, meshes, and primitives", () => {
const gltf = createGltf(groupObjData, options);
expect(gltf.materials.length).toBe(3);
expect(gltf.scene).toBe(0);
expect(gltf.scenes[0].nodes[0]).toBe(0);
expect(gltf.nodes.length).toBe(4);
expect(gltf.nodes[0].mesh).toBeUndefined();
expect(gltf.nodes[0].children.length).toBe(3);
expect(gltf.meshes.length).toBe(3);
// Check for two primitives in each mesh
const length = gltf.meshes.length;
for (let i = 0; i < length; ++i) {
const mesh = gltf.meshes[i];
expect(mesh.primitives.length).toBe(2);
}
});
it("multiple textures", () => {
const gltf = createGltf(complexObjData, options);
const material = gltf.materials[0];
const pbr = material.pbrMetallicRoughness;
const textures = [
pbr.metallicRoughnessTexture,
pbr.baseColorTexture,
material.emissiveTexture,
material.normalTexture,
material.occlusionTexture,
];
expect(
textures
.map((texture) => {
return texture.index;
})
.sort(),
).toEqual([0, 1, 2, 3, 4]);
expect(gltf.samplers[0]).toBeDefined();
});
it("creates default material", () => {
const gltf = createGltf(noMaterialsObjData, options);
const material = gltf.materials[0];
const pbr = material.pbrMetallicRoughness;
expect(material.name).toBe("default");
expect(pbr.baseColorTexture).toBeUndefined();
expect(pbr.metallicRoughnessTexture).toBeUndefined();
expect(pbr.baseColorFactor).toEqual([0.5, 0.5, 0.5, 1.0]);
expect(pbr.metallicFactor).toBe(0.0); // No metallic
expect(pbr.roughnessFactor).toBe(1.0); // Fully rough
expect(material.emissiveTexture).toBeUndefined();
expect(material.normalTexture).toBeUndefined();
expect(material.ambientTexture).toBeUndefined();
expect(material.emissiveFactor).toEqual([0.0, 0.0, 0.0]);
expect(material.alphaMode).toBe("OPAQUE");
expect(material.doubleSided).toBe(false);
});
it("adds KHR_materials_pbrSpecularGlossiness extension when specularGlossiness is set", () => {
options.specularGlossiness = true;
const gltf = createGltf(noMaterialsObjData, options);
expect(gltf.extensionsUsed).toEqual([
"KHR_materials_pbrSpecularGlossiness",
]);
expect(gltf.extensionsRequired).toEqual([
"KHR_materials_pbrSpecularGlossiness",
]);
});
it("adds KHR_materials_unlit extension when unlit is set", () => {
options.unlit = true;
const gltf = createGltf(noMaterialsObjData, options);
expect(gltf.extensionsUsed).toEqual(["KHR_materials_unlit"]);
expect(gltf.extensionsRequired).toEqual(["KHR_materials_unlit"]);
});
it("runs without normals", () => {
boxObjData.nodes[0].meshes[0].primitives[0].normals.length = 0;
const gltf = createGltf(boxObjData, options);
const attributes = gltf.meshes[0].primitives[0].attributes;
expect(attributes.POSITION).toBeDefined();
expect(attributes.NORMAL).toBeUndefined();
expect(attributes.TEXCOORD_0).toBeDefined();
});
it("runs without uvs", () => {
boxObjData.nodes[0].meshes[0].primitives[0].uvs.length = 0;
const gltf = createGltf(boxObjData, options);
const attributes = gltf.meshes[0].primitives[0].attributes;
expect(attributes.POSITION).toBeDefined();
expect(attributes.NORMAL).toBeDefined();
expect(attributes.TEXCOORD_0).toBeUndefined();
});
it("runs without uvs and normals", () => {
boxObjData.nodes[0].meshes[0].primitives[0].normals.length = 0;
boxObjData.nodes[0].meshes[0].primitives[0].uvs.length = 0;
const gltf = createGltf(boxObjData, options);
const attributes = gltf.meshes[0].primitives[0].attributes;
expect(attributes.POSITION).toBeDefined();
expect(attributes.NORMAL).toBeUndefined();
expect(attributes.TEXCOORD_0).toBeUndefined();
});
it("splits incompatible materials", () => {
const gltf = createGltf(mixedAttributesObjData, options);
const materials = gltf.materials;
const meshes = gltf.meshes;
const referenceMaterial = mixedAttributesObjData.materials[0];
delete referenceMaterial.name;
referenceMaterial.pbrMetallicRoughness.baseColorTexture = {
index: 0,
};
const referenceMaterialNoTextures = clone(referenceMaterial, true);
referenceMaterialNoTextures.pbrMetallicRoughness.baseColorTexture =
undefined;
const defaultMaterial = getDefaultMaterial(options);
delete defaultMaterial.name;
const materialNames = materials.map((material) => {
const name = material.name;
delete material.name;
return name;
});
// Expect three copies of each material for
// * positions/normals/uvs
// * positions/normals
// * positions/uvs
expect(materialNames).toEqual([
"default",
"default-2",
"default-3",
"Material",
"Material-2",
"Material-3",
"Missing",
"Missing-2",
"Missing-3",
]);
expect(materials.length).toBe(9);
expect(materials[0]).toEqual(defaultMaterial);
expect(materials[1]).toEqual(defaultMaterial);
expect(materials[2]).toEqual(defaultMaterial);
expect(materials[3]).toEqual(referenceMaterial);
expect(materials[4]).toEqual(referenceMaterial);
expect(materials[5]).toEqual(referenceMaterialNoTextures);
expect(materials[6]).toEqual(defaultMaterial);
expect(materials[7]).toEqual(defaultMaterial);
expect(materials[8]).toEqual(defaultMaterial);
// Test that primitives without uvs reference materials without textures
const meshesLength = meshes.length;
for (let i = 0; i < meshesLength; ++i) {
const mesh = meshes[i];
const primitives = mesh.primitives;
const primitivesLength = primitives.length;
for (let j = 0; j < primitivesLength; ++j) {
const primitive = primitives[j];
const material = materials[primitive.material];
if (!defined(primitive.attributes.TEXCOORD_0)) {
expect(
material.pbrMetallicRoughness.baseColorTexture,
).toBeUndefined();
}
}
}
});
function expandObjData(objData, duplicatesLength) {
const primitive = objData.nodes[0].meshes[0].primitives[0];
const indices = primitive.indices;
const positions = primitive.positions;
const normals = primitive.normals;
const uvs = primitive.uvs;
const indicesLength = indices.length;
const vertexCount = positions.length / 3;
for (let i = 1; i < duplicatesLength; ++i) {
for (let j = 0; j < vertexCount; ++j) {
positions.push(0.0);
positions.push(0.0);
positions.push(0.0);
normals.push(0.0);
normals.push(0.0);
normals.push(0.0);
uvs.push(0.0);
uvs.push(0.0);
}
for (let k = 0; k < indicesLength; ++k) {
indices.push(indices.get(k) + vertexCount * i);
}
}
}
it("detects need to use uint32 indices", () => {
expandObjData(boxObjData, 2731); // Right above 65536 limit
let primitive = boxObjData.nodes[0].meshes[0].primitives[0];
const indicesLength = primitive.indices.length;
const vertexCount = primitive.positions.length / 3;
const gltf = createGltf(boxObjData, options);
primitive = gltf.meshes[0].primitives[0];
const indicesAccessor = gltf.accessors[primitive.indices];
expect(indicesAccessor.count).toBe(indicesLength);
expect(indicesAccessor.max[0]).toBe(vertexCount - 1);
expect(indicesAccessor.componentType).toBe(WebGLConstants.UNSIGNED_INT);
const positionAccessor = gltf.accessors[primitive.attributes.POSITION];
expect(positionAccessor.count).toBe(vertexCount);
});
});