import { BufferLoader, clone, GltfJsonLoader, Resource, ResourceCache, when, } from "../../Source/Cesium.js"; import generateJsonBuffer from "../generateJsonBuffer.js"; describe("Scene/GltfJsonLoader", function () { var gltfUri = "https://example.com/model.glb"; var gltfResource = new Resource({ url: gltfUri, }); var bufferResource = new Resource({ url: "https://example.com/external.bin", }); var gltf1 = { asset: { version: "1.0", }, buffers: { buffer: { uri: "external.bin", }, }, bufferViews: { bufferView: { buffer: "buffer", byteOffset: 0, }, }, accessors: { accessor: { bufferView: "bufferView", byteOffset: 0, componentType: 5126, type: "VEC3", count: 1, }, }, meshes: { mesh: { primitives: [ { attributes: { POSITION: "accessor", }, }, ], }, }, nodes: { node: { meshes: ["mesh"], }, }, scene: "scene", scenes: { scene: { nodes: ["node"], }, }, }; var gltf1Updated = { asset: { version: "2.0", }, buffers: [ { uri: "external.bin", name: "buffer", byteLength: 12, }, ], bufferViews: [ { buffer: 0, byteOffset: 0, name: "bufferView", byteLength: 12, byteStride: 12, target: 34962, }, ], accessors: [ { bufferView: 0, byteOffset: 0, componentType: 5126, type: "VEC3", count: 1, name: "accessor", min: [0, 0, 0], max: [0, 0, 0], normalized: false, }, ], meshes: [ { primitives: [ { attributes: { POSITION: 0, }, mode: 4, material: 0, }, ], name: "mesh", }, ], nodes: [ { name: "node", mesh: 0, matrix: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], }, ], scene: 0, scenes: [ { nodes: [0], name: "scene", }, ], materials: [ { name: "default", emissiveFactor: [0, 0, 0], alphaMode: "OPAQUE", doubleSided: false, }, ], }; var gltf2 = { asset: { version: "2.0", }, buffers: [ { uri: "external.bin", byteLength: 12, }, ], bufferViews: [ { buffer: 0, byteOffset: 0, byteLength: 12, }, ], accessors: [ { bufferView: 0, byteOffset: 0, componentType: 5126, type: "VEC3", count: 1, min: [0, 0, 0], max: [0, 0, 0], }, ], meshes: [ { primitives: [ { attributes: { POSITION: 0, }, }, ], }, ], nodes: [ { mesh: 0, matrix: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], }, ], scene: 0, scenes: [ { nodes: [0], }, ], }; var gltf2Updated = { asset: { version: "2.0", }, buffers: [ { uri: "external.bin", byteLength: 12, }, ], bufferViews: [ { buffer: 0, byteOffset: 0, byteLength: 12, byteStride: 12, target: 34962, }, ], accessors: [ { bufferView: 0, byteOffset: 0, componentType: 5126, type: "VEC3", count: 1, min: [0, 0, 0], max: [0, 0, 0], normalized: false, }, ], meshes: [ { primitives: [ { attributes: { POSITION: 0, }, mode: 4, material: 0, }, ], }, ], nodes: [ { mesh: 0, matrix: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], }, ], scene: 0, scenes: [ { nodes: [0], }, ], materials: [ { name: "default", emissiveFactor: [0, 0, 0], alphaMode: "OPAQUE", doubleSided: false, }, ], }; function createGlb1(json) { var jsonBuffer = generateJsonBuffer(json, 12, 4); var positionBuffer = new Float32Array([0.0, 0.0, 0.0]); var binaryBuffer = new Uint8Array(positionBuffer.buffer); var glbLength = 20 + jsonBuffer.byteLength + binaryBuffer.byteLength; var glb = new Uint8Array(glbLength); var dataView = new DataView(glb.buffer, glb.byteOffset, glb.byteLength); // Write binary glTF header (magic, version, length) var byteOffset = 0; dataView.setUint32(byteOffset, 0x46546c67, true); byteOffset += 4; dataView.setUint32(byteOffset, 1, true); byteOffset += 4; dataView.setUint32(byteOffset, glbLength, true); byteOffset += 4; dataView.setUint32(byteOffset, jsonBuffer.byteLength, true); byteOffset += 4; dataView.setUint32(byteOffset, 0, true); byteOffset += 4; // Write JSON Chunk glb.set(jsonBuffer, byteOffset); byteOffset += jsonBuffer.byteLength; // Write Binary Chunk glb.set(binaryBuffer, byteOffset); return glb; } function createGlb2(json) { var jsonBuffer = generateJsonBuffer(json, 12, 4); var positionBuffer = new Float32Array([0.0, 0.0, 0.0]); var binaryBuffer = new Uint8Array(positionBuffer.buffer); var glbLength = 12 + 8 + jsonBuffer.byteLength + 8 + binaryBuffer.byteLength; var glb = new Uint8Array(glbLength); var dataView = new DataView(glb.buffer, glb.byteOffset, glb.byteLength); // Write binary glTF header (magic, version, length) var byteOffset = 0; dataView.setUint32(byteOffset, 0x46546c67, true); byteOffset += 4; dataView.setUint32(byteOffset, 2, true); byteOffset += 4; dataView.setUint32(byteOffset, glbLength, true); byteOffset += 4; // Write JSON Chunk header (length, type) dataView.setUint32(byteOffset, jsonBuffer.byteLength, true); byteOffset += 4; dataView.setUint32(byteOffset, 0x4e4f534a, true); byteOffset += 4; // Write JSON Chunk glb.set(jsonBuffer, byteOffset); byteOffset += jsonBuffer.byteLength; // Write Binary Chunk header (length, type) dataView.setUint32(byteOffset, binaryBuffer.byteLength); byteOffset += 4; dataView.setUint32(byteOffset, 0x004e4942, true); byteOffset += 4; // Write Binary Chunk glb.set(binaryBuffer, byteOffset); return glb; } afterEach(function () { ResourceCache.clearForSpecs(); }); it("throws if resourceCache is undefined", function () { expect(function () { return new GltfJsonLoader({ resourceCache: undefined, gltfResource: gltfResource, baseResource: gltfResource, }); }).toThrowDeveloperError(); }); it("throws if gltfResource is undefined", function () { expect(function () { return new GltfJsonLoader({ resourceCache: ResourceCache, gltfResource: undefined, baseResource: gltfResource, }); }).toThrowDeveloperError(); }); it("throws if baseResource is undefined", function () { expect(function () { return new GltfJsonLoader({ resourceCache: ResourceCache, gltfResource: gltfResource, baseResource: undefined, }); }).toThrowDeveloperError(); }); it("rejects promise if resource fails to load", function () { var error = new Error("404 Not Found"); spyOn(GltfJsonLoader.prototype, "_fetchGltf").and.returnValue( when.reject(error) ); var gltfJsonLoader = new GltfJsonLoader({ resourceCache: ResourceCache, gltfResource: gltfResource, baseResource: gltfResource, }); gltfJsonLoader.load(); return gltfJsonLoader.promise .then(function (gltfJsonLoader) { fail(); }) .otherwise(function (runtimeError) { expect(runtimeError.message).toBe( "Failed to load glTF: https://example.com/model.glb\n404 Not Found" ); }); }); it("rejects promise if glTF fails to process", function () { var arrayBuffer = generateJsonBuffer(gltf1).buffer; spyOn(GltfJsonLoader.prototype, "_fetchGltf").and.returnValue( when.resolve(arrayBuffer) ); var error = new Error("404 Not Found"); spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue( when.reject(error) ); var gltfJsonLoader = new GltfJsonLoader({ resourceCache: ResourceCache, gltfResource: gltfResource, baseResource: gltfResource, }); gltfJsonLoader.load(); return gltfJsonLoader.promise .then(function (gltfJsonLoader) { fail(); }) .otherwise(function (runtimeError) { expect(runtimeError.message).toBe( "Failed to load glTF: https://example.com/model.glb\nFailed to load external buffer: https://example.com/external.bin\n404 Not Found" ); }); }); it("rejects promise if glTF fails to process from typed array", function () { var typedArray = createGlb1(gltf1); var error = new Error("404 Not Found"); spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue( when.reject(error) ); var gltfJsonLoader = new GltfJsonLoader({ resourceCache: ResourceCache, gltfResource: gltfResource, baseResource: gltfResource, typedArray: typedArray, }); gltfJsonLoader.load(); return gltfJsonLoader.promise .then(function (gltfJsonLoader) { fail(); }) .otherwise(function (runtimeError) { expect(runtimeError.message).toBe( "Failed to load glTF: https://example.com/model.glb\nFailed to load external buffer: https://example.com/external.bin\n404 Not Found" ); }); }); it("loads glTF 1.0", function () { var arrayBuffer = generateJsonBuffer(gltf1).buffer; spyOn(GltfJsonLoader.prototype, "_fetchGltf").and.returnValue( when.resolve(arrayBuffer) ); spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue( when.resolve(new Float32Array([0.0, 0.0, 0.0]).buffer) ); var gltfJsonLoader = new GltfJsonLoader({ resourceCache: ResourceCache, gltfResource: gltfResource, baseResource: gltfResource, }); gltfJsonLoader.load(); return gltfJsonLoader.promise.then(function (gltfJsonLoader) { var gltf = gltfJsonLoader.gltf; expect(gltf).toEqual(gltf1Updated); }); }); it("loads glTF 1.0 binary", function () { var gltf1Binary = clone(gltf1, true); gltf1Binary.buffers = { binary_glTF: { type: "arraybuffer", byteLength: 12, uri: "data:,", }, }; gltf1Binary.extensionsUsed = ["KHR_binary_glTF"]; gltf1Binary.bufferViews.bufferView.buffer = "binary_glTF"; var gltf1BinaryUpdated = clone(gltf1Updated, true); gltf1BinaryUpdated.buffers[0].name = "binary_glTF"; delete gltf1BinaryUpdated.buffers[0].uri; var arrayBuffer = createGlb1(gltf1Binary).buffer; spyOn(GltfJsonLoader.prototype, "_fetchGltf").and.returnValue( when.resolve(arrayBuffer) ); var gltfJsonLoader = new GltfJsonLoader({ resourceCache: ResourceCache, gltfResource: gltfResource, baseResource: gltfResource, }); gltfJsonLoader.load(); return gltfJsonLoader.promise.then(function (gltfJsonLoader) { var gltf = gltfJsonLoader.gltf; expect(gltf).toEqual(gltf1BinaryUpdated); }); }); it("loads glTF 1.0 with data uri", function () { var gltf1DataUri = clone(gltf1, true); gltf1DataUri.buffers.buffer = { uri: "data:application/octet-stream;base64,AAAAAAAAAAAAAAAA", }; var gltf1DataUriUpdated = clone(gltf1Updated, true); delete gltf1DataUriUpdated.buffers[0].uri; var arrayBuffer = generateJsonBuffer(gltf1DataUri).buffer; spyOn(GltfJsonLoader.prototype, "_fetchGltf").and.returnValue( when.resolve(arrayBuffer) ); var gltfJsonLoader = new GltfJsonLoader({ resourceCache: ResourceCache, gltfResource: gltfResource, baseResource: gltfResource, }); gltfJsonLoader.load(); return gltfJsonLoader.promise.then(function (gltfJsonLoader) { var gltf = gltfJsonLoader.gltf; expect(gltf).toEqual(gltf1DataUriUpdated); }); }); it("loads glTF 2.0", function () { var arrayBuffer = generateJsonBuffer(gltf2).buffer; spyOn(GltfJsonLoader.prototype, "_fetchGltf").and.returnValue( when.resolve(arrayBuffer) ); spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue( when.resolve(new Float32Array([0.0, 0.0, 0.0]).buffer) ); var gltfJsonLoader = new GltfJsonLoader({ resourceCache: ResourceCache, gltfResource: gltfResource, baseResource: gltfResource, }); gltfJsonLoader.load(); return gltfJsonLoader.promise.then(function (gltfJsonLoader) { var gltf = gltfJsonLoader.gltf; expect(gltf).toEqual(gltf2Updated); }); }); it("loads glTF 2.0 binary", function () { var gltf2Binary = clone(gltf2, true); delete gltf2Binary.buffers[0].uri; var gltf2BinaryUpdated = clone(gltf2Updated, true); delete gltf2BinaryUpdated.buffers[0].uri; var arrayBuffer = createGlb2(gltf2Binary).buffer; spyOn(GltfJsonLoader.prototype, "_fetchGltf").and.returnValue( when.resolve(arrayBuffer) ); var gltfJsonLoader = new GltfJsonLoader({ resourceCache: ResourceCache, gltfResource: gltfResource, baseResource: gltfResource, }); gltfJsonLoader.load(); return gltfJsonLoader.promise.then(function (gltfJsonLoader) { var gltf = gltfJsonLoader.gltf; expect(gltf).toEqual(gltf2BinaryUpdated); }); }); it("loads glTF 2.0 with data uri", function () { var gltf2DataUri = clone(gltf2, true); gltf2DataUri.buffers[0].uri = "data:application/octet-stream;base64,AAAAAAAAAAAAAAAA"; var gltf2DataUriUpdated = clone(gltf2Updated, true); delete gltf2DataUriUpdated.buffers[0].uri; var arrayBuffer = generateJsonBuffer(gltf2DataUri).buffer; spyOn(GltfJsonLoader.prototype, "_fetchGltf").and.returnValue( when.resolve(arrayBuffer) ); var gltfJsonLoader = new GltfJsonLoader({ resourceCache: ResourceCache, gltfResource: gltfResource, baseResource: gltfResource, }); gltfJsonLoader.load(); return gltfJsonLoader.promise.then(function (gltfJsonLoader) { var gltf = gltfJsonLoader.gltf; expect(gltf).toEqual(gltf2DataUriUpdated); }); }); it("loads typed array", function () { var gltf2Binary = clone(gltf2, true); delete gltf2Binary.buffers[0].uri; var gltf2BinaryUpdated = clone(gltf2Updated, true); delete gltf2BinaryUpdated.buffers[0].uri; var typedArray = createGlb2(gltf2Binary); var gltfJsonLoader = new GltfJsonLoader({ resourceCache: ResourceCache, gltfResource: gltfResource, baseResource: gltfResource, typedArray: typedArray, }); gltfJsonLoader.load(); return gltfJsonLoader.promise.then(function (gltfJsonLoader) { var gltf = gltfJsonLoader.gltf; expect(gltf).toEqual(gltf2BinaryUpdated); }); }); it("loads JSON directly", function () { var gltf = clone(gltf2, true); spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue( when.resolve(new Float32Array([0.0, 0.0, 0.0]).buffer) ); var gltfJsonLoader = new GltfJsonLoader({ resourceCache: ResourceCache, gltfResource: gltfResource, baseResource: gltfResource, gltfJson: gltf, }); gltfJsonLoader.load(); return gltfJsonLoader.promise.then(function (gltfJsonLoader) { var gltf = gltfJsonLoader.gltf; expect(gltf).toEqual(gltf2Updated); }); }); it("destroys", function () { var gltf2Binary = clone(gltf2, true); delete gltf2Binary.buffers[0].uri; var arrayBuffer = createGlb2(gltf2Binary).buffer; spyOn(GltfJsonLoader.prototype, "_fetchGltf").and.returnValue( when.resolve(arrayBuffer) ); var unloadBuffer = spyOn( BufferLoader.prototype, "unload" ).and.callThrough(); var gltfJsonLoader = new GltfJsonLoader({ resourceCache: ResourceCache, gltfResource: gltfResource, baseResource: gltfResource, }); gltfJsonLoader.load(); return gltfJsonLoader.promise.then(function (gltfJsonLoader) { expect(gltfJsonLoader.gltf).toBeDefined(); expect(gltfJsonLoader.isDestroyed()).toBe(false); gltfJsonLoader.destroy(); expect(gltfJsonLoader.gltf).not.toBeDefined(); expect(gltfJsonLoader.isDestroyed()).toBe(true); expect(unloadBuffer).toHaveBeenCalled(); }); }); function resolvesGltfAfterDestroy(reject) { var deferredPromise = when.defer(); spyOn(GltfJsonLoader.prototype, "_fetchGltf").and.returnValue( deferredPromise.promise ); var arrayBuffer = generateJsonBuffer(gltf2).buffer; var gltfJsonLoader = new GltfJsonLoader({ resourceCache: ResourceCache, gltfResource: gltfResource, baseResource: gltfResource, }); expect(gltfJsonLoader.gltf).not.toBeDefined(); gltfJsonLoader.load(); gltfJsonLoader.destroy(); if (reject) { deferredPromise.reject(new Error()); } else { deferredPromise.resolve(arrayBuffer); } expect(gltfJsonLoader.gltf).not.toBeDefined(); expect(gltfJsonLoader.isDestroyed()).toBe(true); } it("handles resolving glTF after destroy", function () { resolvesGltfAfterDestroy(false); }); it("handles rejecting glTF after destroy", function () { resolvesGltfAfterDestroy(true); }); function resolvesProcessedGltfAfterDestroy(reject) { spyOn(GltfJsonLoader.prototype, "_fetchGltf").and.returnValue( when.resolve(generateJsonBuffer(gltf2).buffer) ); var buffer = new Float32Array([0.0, 0.0, 0.0]).buffer; var deferredPromise = when.defer(); spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue( deferredPromise.promise ); var gltfJsonLoader = new GltfJsonLoader({ resourceCache: ResourceCache, gltfResource: gltfResource, baseResource: gltfResource, }); expect(gltfJsonLoader.gltf).not.toBeDefined(); gltfJsonLoader.load(); gltfJsonLoader.destroy(); deferredPromise.resolve(buffer); expect(gltfJsonLoader.gltf).not.toBeDefined(); expect(gltfJsonLoader.isDestroyed()).toBe(true); } it("handles resolving processed glTF after destroy", function () { resolvesProcessedGltfAfterDestroy(false); }); it("handles rejecting processed glTF after destroy", function () { resolvesProcessedGltfAfterDestroy(true); }); function resolvesTypedArrayAfterDestroy(reject) { var typedArray = generateJsonBuffer(gltf1); var buffer = new Float32Array([0.0, 0.0, 0.0]).buffer; var deferredPromise = when.defer(); spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue( deferredPromise.promise ); // Load a copy of the buffer into the cache so that the buffer loader // promise resolves even if the glTF loader is destroyed var bufferLoaderCopy = ResourceCache.loadExternalBuffer({ resource: bufferResource, }); var gltfJsonLoader = new GltfJsonLoader({ resourceCache: ResourceCache, gltfResource: gltfResource, baseResource: gltfResource, typedArray: typedArray, }); expect(gltfJsonLoader.gltf).not.toBeDefined(); gltfJsonLoader.load(); gltfJsonLoader.destroy(); if (reject) { deferredPromise.reject(new Error()); } else { deferredPromise.resolve(buffer); } expect(gltfJsonLoader.gltf).not.toBeDefined(); expect(gltfJsonLoader.isDestroyed()).toBe(true); ResourceCache.unload(bufferLoaderCopy); } it("handles resolving typed array after destroy", function () { return resolvesTypedArrayAfterDestroy(false); }); it("handles rejecting typed array after destroy", function () { return resolvesTypedArrayAfterDestroy(true); }); });