"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); }); });