import {
|
Buffer,
|
ComponentDatatype,
|
DracoLoader,
|
GltfBufferViewLoader,
|
GltfDracoLoader,
|
GltfIndexBufferLoader,
|
JobScheduler,
|
Resource,
|
ResourceCache,
|
when,
|
} from "../../Source/Cesium.js";
|
import concatTypedArrays from "../concatTypedArrays.js";
|
import createScene from "../createScene.js";
|
import loaderProcess from "../loaderProcess.js";
|
import waitForLoaderProcess from "../waitForLoaderProcess.js";
|
|
describe(
|
"Scene/GltfIndexBufferLoader",
|
function () {
|
var dracoBufferTypedArray = new Uint8Array([1, 3, 7, 15, 31, 63, 127, 255]);
|
var dracoArrayBuffer = dracoBufferTypedArray.buffer;
|
|
var decodedPositions = new Uint16Array([0, 0, 0, 65535, 65535, 65535, 0, 65535, 0]); // prettier-ignore
|
var decodedNormals = new Uint8Array([0, 255, 128, 128, 255, 0]);
|
var decodedIndices = new Uint16Array([0, 1, 2]);
|
|
var positions = new Float32Array([-1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0]); // prettier-ignore
|
var normals = new Float32Array([-1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0]); // prettier-ignore
|
var indicesUint32 = new Uint32Array([0, 1, 2]);
|
var indicesUint16 = new Uint16Array([0, 1, 2]);
|
var indicesUint8 = new Uint8Array([0, 1, 2]);
|
|
var bufferViewTypedArray = concatTypedArrays([
|
positions,
|
normals,
|
indicesUint32,
|
indicesUint16,
|
indicesUint8,
|
]);
|
var arrayBuffer = bufferViewTypedArray.buffer;
|
|
var gltfUri = "https://example.com/model.glb";
|
var gltfResource = new Resource({
|
url: gltfUri,
|
});
|
|
var decodeDracoResults = {
|
indexArray: {
|
typedArray: decodedIndices,
|
numberOfIndices: decodedIndices.length,
|
},
|
attributeData: {
|
POSITION: {
|
array: decodedPositions,
|
data: {
|
byteOffset: 0,
|
byteStride: 6,
|
componentDatatype: ComponentDatatype.UNSIGNED_SHORT,
|
componentsPerAttribute: 3,
|
normalized: false,
|
quantization: {
|
octEncoded: false,
|
quantizationBits: 14,
|
minValues: [-1.0, -1.0, -1.0],
|
range: 2.0,
|
},
|
},
|
},
|
NORMAL: {
|
array: decodedNormals,
|
data: {
|
byteOffset: 0,
|
byteStride: 2,
|
componentDatatype: ComponentDatatype.UNSIGNED_BYTE,
|
componentsPerAttribute: 2,
|
normalized: false,
|
quantization: {
|
octEncoded: true,
|
quantizationBits: 10,
|
},
|
},
|
},
|
},
|
};
|
|
var gltfDraco = {
|
buffers: [
|
{
|
uri: "external.bin",
|
byteLength: 8,
|
},
|
],
|
bufferViews: [
|
{
|
buffer: 0,
|
byteOffset: 4,
|
byteLength: 4,
|
},
|
],
|
accessors: [
|
{
|
componentType: 5126,
|
count: 3,
|
max: [-1.0, -1.0, -1.0],
|
min: [1.0, 1.0, 1.0],
|
type: "VEC3",
|
},
|
{
|
componentType: 5126,
|
count: 3,
|
type: "VEC3",
|
},
|
{
|
componentType: 5123,
|
count: 3,
|
type: "SCALAR",
|
},
|
],
|
meshes: [
|
{
|
primitives: [
|
{
|
attributes: {
|
POSITION: 0,
|
NORMAL: 1,
|
},
|
indices: 2,
|
extensions: {
|
KHR_draco_mesh_compression: {
|
bufferView: 0,
|
attributes: {
|
POSITION: 0,
|
NORMAL: 1,
|
},
|
},
|
},
|
},
|
],
|
},
|
],
|
};
|
|
var dracoExtension =
|
gltfDraco.meshes[0].primitives[0].extensions.KHR_draco_mesh_compression;
|
|
var gltfUncompressed = {
|
buffers: [
|
{
|
uri: "external.bin",
|
byteLength: 78,
|
},
|
],
|
bufferViews: [
|
{
|
buffer: 0,
|
byteOffset: 0,
|
byteLength: 36,
|
},
|
{
|
buffer: 0,
|
byteOffset: 36,
|
byteLength: 36,
|
},
|
{
|
buffer: 0,
|
byteOffset: 72,
|
byteLength: 12,
|
},
|
{
|
buffer: 0,
|
byteOffset: 84,
|
byteLength: 6,
|
},
|
{
|
buffer: 0,
|
byteOffset: 90,
|
byteLength: 3,
|
},
|
],
|
accessors: [
|
{
|
componentType: 5126,
|
count: 3,
|
max: [-1.0, -1.0, -1.0],
|
min: [1.0, 1.0, 1.0],
|
type: "VEC3",
|
bufferView: 0,
|
byteOffset: 0,
|
},
|
{
|
componentType: 5126,
|
count: 3,
|
type: "VEC3",
|
bufferView: 1,
|
byteOffset: 0,
|
},
|
{
|
componentType: 5125, // UNSIGNED_INT
|
count: 3,
|
type: "SCALAR",
|
bufferView: 2,
|
byteOffset: 0,
|
},
|
{
|
componentType: 5123, // UNSIGNED_SHORT
|
count: 3,
|
type: "SCALAR",
|
bufferView: 3,
|
byteOffset: 0,
|
},
|
{
|
componentType: 5121, // UNSIGNED_BYTE
|
count: 3,
|
type: "SCALAR",
|
bufferView: 4,
|
byteOffset: 0,
|
},
|
],
|
meshes: [
|
{
|
primitives: [
|
{
|
attributes: {
|
POSITION: 0,
|
NORMAL: 1,
|
},
|
indices: 2,
|
},
|
{
|
attributes: {
|
POSITION: 0,
|
NORMAL: 1,
|
},
|
indices: 3,
|
},
|
{
|
attributes: {
|
POSITION: 0,
|
NORMAL: 1,
|
},
|
indices: 4,
|
},
|
],
|
},
|
],
|
};
|
|
var scene;
|
|
beforeAll(function () {
|
scene = createScene();
|
});
|
|
afterAll(function () {
|
scene.destroyForSpecs();
|
});
|
|
afterEach(function () {
|
ResourceCache.clearForSpecs();
|
});
|
|
it("throws if resourceCache is undefined", function () {
|
expect(function () {
|
return new GltfIndexBufferLoader({
|
resourceCache: undefined,
|
gltf: gltfUncompressed,
|
accessorId: 3,
|
gltfResource: gltfResource,
|
baseResource: gltfResource,
|
});
|
}).toThrowDeveloperError();
|
});
|
|
it("throws if gltf is undefined", function () {
|
expect(function () {
|
return new GltfIndexBufferLoader({
|
resourceCache: ResourceCache,
|
gltf: undefined,
|
accessorId: 3,
|
gltfResource: gltfResource,
|
baseResource: gltfResource,
|
});
|
}).toThrowDeveloperError();
|
});
|
|
it("throws if accessorId is undefined", function () {
|
expect(function () {
|
return new GltfIndexBufferLoader({
|
resourceCache: ResourceCache,
|
gltf: gltfUncompressed,
|
accessorId: undefined,
|
gltfResource: gltfResource,
|
baseResource: gltfResource,
|
});
|
}).toThrowDeveloperError();
|
});
|
|
it("throws if gltfResource is undefined", function () {
|
expect(function () {
|
return new GltfIndexBufferLoader({
|
resourceCache: ResourceCache,
|
gltf: gltfUncompressed,
|
accessorId: 3,
|
gltfResource: undefined,
|
baseResource: gltfResource,
|
});
|
}).toThrowDeveloperError();
|
});
|
|
it("throws if baseResource is undefined", function () {
|
expect(function () {
|
return new GltfIndexBufferLoader({
|
resourceCache: ResourceCache,
|
gltf: gltfUncompressed,
|
accessorId: 3,
|
gltfResource: gltfResource,
|
baseResource: undefined,
|
});
|
}).toThrowDeveloperError();
|
});
|
|
it("rejects promise if buffer view fails to load", function () {
|
var error = new Error("404 Not Found");
|
spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue(
|
when.reject(error)
|
);
|
|
var indexBufferLoader = new GltfIndexBufferLoader({
|
resourceCache: ResourceCache,
|
gltf: gltfUncompressed,
|
accessorId: 3,
|
gltfResource: gltfResource,
|
baseResource: gltfResource,
|
});
|
|
indexBufferLoader.load();
|
|
return indexBufferLoader.promise
|
.then(function (indexBufferLoader) {
|
fail();
|
})
|
.otherwise(function (runtimeError) {
|
expect(runtimeError.message).toBe(
|
"Failed to load index buffer\nFailed to load buffer view\nFailed to load external buffer: https://example.com/external.bin\n404 Not Found"
|
);
|
});
|
});
|
|
it("rejects promise if draco fails to load", function () {
|
spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue(
|
when.resolve(dracoArrayBuffer)
|
);
|
|
var error = new Error("Draco decode failed");
|
spyOn(DracoLoader, "decodeBufferView").and.returnValue(
|
when.reject(error)
|
);
|
|
var indexBufferLoader = new GltfIndexBufferLoader({
|
resourceCache: ResourceCache,
|
gltf: gltfDraco,
|
accessorId: 2,
|
gltfResource: gltfResource,
|
baseResource: gltfResource,
|
draco: dracoExtension,
|
});
|
|
indexBufferLoader.load();
|
|
return waitForLoaderProcess(indexBufferLoader, scene)
|
.then(function (indexBufferLoader) {
|
fail();
|
})
|
.otherwise(function (runtimeError) {
|
expect(runtimeError.message).toBe(
|
"Failed to load index buffer\nFailed to load Draco\nDraco decode failed"
|
);
|
});
|
});
|
|
it("loads from accessor", function () {
|
spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue(
|
when.resolve(arrayBuffer)
|
);
|
|
// Simulate JobScheduler not being ready for a few frames
|
var processCallsTotal = 3;
|
var processCallsCount = 0;
|
var jobScheduler = scene.frameState.jobScheduler;
|
var originalJobSchedulerExecute = jobScheduler.execute;
|
spyOn(JobScheduler.prototype, "execute").and.callFake(function (
|
job,
|
jobType
|
) {
|
if (processCallsCount++ >= processCallsTotal) {
|
return originalJobSchedulerExecute.call(jobScheduler, job, jobType);
|
}
|
return false;
|
});
|
|
var indexBufferLoader = new GltfIndexBufferLoader({
|
resourceCache: ResourceCache,
|
gltf: gltfUncompressed,
|
accessorId: 3,
|
gltfResource: gltfResource,
|
baseResource: gltfResource,
|
});
|
|
indexBufferLoader.load();
|
|
return waitForLoaderProcess(indexBufferLoader, scene).then(function (
|
indexBufferLoader
|
) {
|
loaderProcess(indexBufferLoader, scene); // Check that calling process after load doesn't break anything
|
expect(indexBufferLoader.buffer.sizeInBytes).toBe(
|
indicesUint16.byteLength
|
);
|
expect(indexBufferLoader.typedArray).toBeUndefined();
|
});
|
});
|
|
it("creates index buffer synchronously", function () {
|
spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue(
|
when.resolve(arrayBuffer)
|
);
|
|
var indexBufferLoader = new GltfIndexBufferLoader({
|
resourceCache: ResourceCache,
|
gltf: gltfUncompressed,
|
accessorId: 3,
|
gltfResource: gltfResource,
|
baseResource: gltfResource,
|
asynchronous: false,
|
});
|
|
indexBufferLoader.load();
|
|
return waitForLoaderProcess(indexBufferLoader, scene).then(function (
|
indexBufferLoader
|
) {
|
expect(indexBufferLoader.buffer.sizeInBytes).toBe(
|
indicesUint16.byteLength
|
);
|
});
|
});
|
|
it("loads as typed array", function () {
|
spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue(
|
when.resolve(arrayBuffer)
|
);
|
|
spyOn(Buffer, "createIndexBuffer").and.callThrough();
|
|
var indexBufferLoader = new GltfIndexBufferLoader({
|
resourceCache: ResourceCache,
|
gltf: gltfUncompressed,
|
accessorId: 3,
|
gltfResource: gltfResource,
|
baseResource: gltfResource,
|
loadAsTypedArray: true,
|
});
|
|
indexBufferLoader.load();
|
|
return waitForLoaderProcess(indexBufferLoader, scene).then(function (
|
indexBufferLoader
|
) {
|
expect(indexBufferLoader.typedArray.byteLength).toBe(
|
indicesUint16.byteLength
|
);
|
expect(indexBufferLoader.buffer).toBeUndefined();
|
expect(Buffer.createIndexBuffer.calls.count()).toBe(0);
|
});
|
});
|
|
function loadIndices(accessorId, expectedByteLength) {
|
spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue(
|
when.resolve(arrayBuffer)
|
);
|
|
var indexBufferLoader = new GltfIndexBufferLoader({
|
resourceCache: ResourceCache,
|
gltf: gltfUncompressed,
|
accessorId: accessorId,
|
gltfResource: gltfResource,
|
baseResource: gltfResource,
|
});
|
|
indexBufferLoader.load();
|
|
return waitForLoaderProcess(indexBufferLoader, scene).then(function (
|
indexBufferLoader
|
) {
|
expect(indexBufferLoader.buffer.sizeInBytes).toBe(expectedByteLength);
|
});
|
}
|
|
it("loads uint32 indices", function () {
|
if (!scene.frameState.context.elementIndexUint) {
|
return;
|
}
|
|
return loadIndices(2, indicesUint32.byteLength);
|
});
|
|
it("loads uint16 indices", function () {
|
return loadIndices(3, indicesUint16.byteLength);
|
});
|
|
it("loads uint8 indices", function () {
|
return loadIndices(4, indicesUint8.byteLength);
|
});
|
|
it("loads from draco", function () {
|
spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue(
|
when.resolve(arrayBuffer)
|
);
|
|
// Simulate decodeBufferView not being ready for a few frames
|
var processCallsTotal = 3;
|
var processCallsCount = 0;
|
spyOn(DracoLoader, "decodeBufferView").and.callFake(function () {
|
if (processCallsCount++ === processCallsTotal) {
|
return when.resolve(decodeDracoResults);
|
}
|
return undefined;
|
});
|
|
var indexBufferLoader = new GltfIndexBufferLoader({
|
resourceCache: ResourceCache,
|
gltf: gltfDraco,
|
accessorId: 2,
|
gltfResource: gltfResource,
|
baseResource: gltfResource,
|
draco: dracoExtension,
|
});
|
|
indexBufferLoader.load();
|
|
return waitForLoaderProcess(indexBufferLoader, scene).then(function (
|
indexBufferLoader
|
) {
|
loaderProcess(indexBufferLoader, scene); // Check that calling process after load doesn't break anything
|
expect(indexBufferLoader.buffer.sizeInBytes).toBe(
|
decodedIndices.byteLength
|
);
|
});
|
});
|
|
it("destroys index buffer loaded from buffer view", function () {
|
spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue(
|
when.resolve(arrayBuffer)
|
);
|
|
var unloadBufferView = spyOn(
|
GltfBufferViewLoader.prototype,
|
"unload"
|
).and.callThrough();
|
|
var destroyIndexBuffer = spyOn(
|
Buffer.prototype,
|
"destroy"
|
).and.callThrough();
|
|
var indexBufferLoader = new GltfIndexBufferLoader({
|
resourceCache: ResourceCache,
|
gltf: gltfUncompressed,
|
accessorId: 3,
|
gltfResource: gltfResource,
|
baseResource: gltfResource,
|
});
|
|
indexBufferLoader.load();
|
|
return waitForLoaderProcess(indexBufferLoader, scene).then(function (
|
indexBufferLoader
|
) {
|
expect(indexBufferLoader.buffer).toBeDefined();
|
expect(indexBufferLoader.isDestroyed()).toBe(false);
|
|
indexBufferLoader.destroy();
|
|
expect(indexBufferLoader.buffer).not.toBeDefined();
|
expect(indexBufferLoader.isDestroyed()).toBe(true);
|
expect(unloadBufferView).toHaveBeenCalled();
|
expect(destroyIndexBuffer).toHaveBeenCalled();
|
});
|
});
|
|
it("destroys index buffer loaded from draco", function () {
|
spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue(
|
when.resolve(arrayBuffer)
|
);
|
|
spyOn(DracoLoader, "decodeBufferView").and.returnValue(
|
when.resolve(decodeDracoResults)
|
);
|
|
var unloadDraco = spyOn(
|
GltfDracoLoader.prototype,
|
"unload"
|
).and.callThrough();
|
|
var destroyIndexBuffer = spyOn(
|
Buffer.prototype,
|
"destroy"
|
).and.callThrough();
|
|
var indexBufferLoader = new GltfIndexBufferLoader({
|
resourceCache: ResourceCache,
|
gltf: gltfDraco,
|
accessorId: 2,
|
gltfResource: gltfResource,
|
baseResource: gltfResource,
|
draco: dracoExtension,
|
});
|
|
indexBufferLoader.load();
|
|
return waitForLoaderProcess(indexBufferLoader, scene).then(function (
|
indexBufferLoader
|
) {
|
expect(indexBufferLoader.buffer).toBeDefined();
|
expect(indexBufferLoader.isDestroyed()).toBe(false);
|
|
indexBufferLoader.destroy();
|
|
expect(indexBufferLoader.buffer).not.toBeDefined();
|
expect(indexBufferLoader.isDestroyed()).toBe(true);
|
expect(unloadDraco).toHaveBeenCalled();
|
expect(destroyIndexBuffer).toHaveBeenCalled();
|
});
|
});
|
|
function resolveBufferViewAfterDestroy(reject) {
|
var deferredPromise = when.defer();
|
spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue(
|
deferredPromise.promise
|
);
|
|
// Load a copy of the buffer view into the cache so that the buffer view
|
// promise resolves even if the index buffer loader is destroyed
|
var bufferViewLoaderCopy = ResourceCache.loadBufferView({
|
gltf: gltfUncompressed,
|
bufferViewId: 3,
|
gltfResource: gltfResource,
|
baseResource: gltfResource,
|
});
|
|
var indexBufferLoader = new GltfIndexBufferLoader({
|
resourceCache: ResourceCache,
|
gltf: gltfUncompressed,
|
accessorId: 3,
|
gltfResource: gltfResource,
|
baseResource: gltfResource,
|
});
|
|
expect(indexBufferLoader.buffer).not.toBeDefined();
|
|
indexBufferLoader.load();
|
indexBufferLoader.destroy();
|
|
if (reject) {
|
deferredPromise.reject(new Error());
|
} else {
|
deferredPromise.resolve(arrayBuffer);
|
}
|
|
expect(indexBufferLoader.buffer).not.toBeDefined();
|
expect(indexBufferLoader.isDestroyed()).toBe(true);
|
|
ResourceCache.unload(bufferViewLoaderCopy);
|
}
|
|
it("handles resolving buffer view after destroy", function () {
|
resolveBufferViewAfterDestroy(false);
|
});
|
|
it("handles rejecting buffer view after destroy", function () {
|
resolveBufferViewAfterDestroy(true);
|
});
|
|
function resolveDracoAfterDestroy(reject) {
|
spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue(
|
when.resolve(arrayBuffer)
|
);
|
|
var deferredPromise = when.defer();
|
var decodeBufferView = spyOn(
|
DracoLoader,
|
"decodeBufferView"
|
).and.callFake(function () {
|
return deferredPromise.promise;
|
});
|
|
// Load a copy of the draco loader into the cache so that the draco loader
|
// promise resolves even if the index buffer loader is destroyed
|
var dracoLoaderCopy = ResourceCache.loadDraco({
|
gltf: gltfDraco,
|
draco: dracoExtension,
|
gltfResource: gltfResource,
|
baseResource: gltfResource,
|
});
|
|
var indexBufferLoader = new GltfIndexBufferLoader({
|
resourceCache: ResourceCache,
|
gltf: gltfDraco,
|
accessorId: 2,
|
gltfResource: gltfResource,
|
baseResource: gltfResource,
|
draco: dracoExtension,
|
});
|
|
expect(indexBufferLoader.buffer).not.toBeDefined();
|
|
indexBufferLoader.load();
|
loaderProcess(indexBufferLoader, scene);
|
expect(decodeBufferView).toHaveBeenCalled(); // Make sure the decode actually starts
|
|
indexBufferLoader.destroy();
|
|
if (reject) {
|
deferredPromise.reject(new Error());
|
} else {
|
deferredPromise.resolve(decodeDracoResults);
|
}
|
|
expect(indexBufferLoader.buffer).not.toBeDefined();
|
expect(indexBufferLoader.isDestroyed()).toBe(true);
|
|
ResourceCache.unload(dracoLoaderCopy);
|
}
|
|
it("handles resolving draco after destroy", function () {
|
resolveDracoAfterDestroy(false);
|
});
|
|
it("handles rejecting draco after destroy", function () {
|
resolveDracoAfterDestroy(true);
|
});
|
},
|
"WebGL"
|
);
|