You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

780 lines
23 KiB
JavaScript

import { Cartesian3, Model, when } from "../../Source/Cesium.js";
import createScene from "../createScene.js";
import pollToPromise from "../pollToPromise.js";
import GltfBuilder from "./GltfBuilder.js";
describe(
"Scene/ModelOutlineLoader",
function () {
var scene;
var primitives;
beforeAll(function () {
scene = createScene();
primitives = scene.primitives;
});
afterAll(function () {
scene.destroyForSpecs();
});
it("does nothing if no primitives are outlined", function () {
var vertices = [];
var indices = [];
var edges = [];
createModel(vertices, indices, edges, 1, true, true, true);
var builder = createGltfBuilder();
var bufferBuilder = builder.buffer();
bufferBuilder
.vertexBuffer("vertices")
.vec3("position")
.vec3("normal")
.scalar("batchID")
.data(vertices);
bufferBuilder.indexBuffer("indices").scalar("index").data(indices);
var meshBuilder = builder.mesh();
var primitiveBuilder = meshBuilder.primitive();
primitiveBuilder
.triangles()
.material("default")
.attribute("POSITION", "position")
.attribute("NORMAL", "normal")
.attribute("_BATCHID", "batchID")
.indices("index");
var gltf = builder.toGltf();
var model = new Model({
gltf: gltf,
});
primitives.add(model);
return waitForReady(scene, model).then(function () {
var gltf = model.gltf;
expect(gltf.buffers.length).toBe(1);
expect(gltf.accessors.length).toBe(4);
expect(gltf.accessors[0].count).toBe(9);
expect(gltf.accessors[1].count).toBe(9);
expect(gltf.accessors[2].count).toBe(9);
expect(
Object.keys(gltf.meshes[0].primitives[0].attributes).length
).toBe(3);
expect(gltf.accessors[3].count).toBe(indices.length);
builder.destroy();
});
});
it("duplicates vertices as needed and adds outline attribute", function () {
var vertices = [];
var indices = [];
var edges = [];
createTrickyModel(vertices, indices, edges, 2, true, true, true);
createModel(vertices, indices, edges, 1, true, true, true);
var builder = createGltfBuilder();
var bufferBuilder = builder.buffer();
bufferBuilder
.vertexBuffer("vertices")
.vec3("position")
.vec3("normal")
.scalar("batchID")
.data(vertices);
bufferBuilder.indexBuffer("indices").scalar("index").data(indices);
bufferBuilder.indexBuffer("edgeIndices").scalar("edgeIndex").data(edges);
var meshBuilder = builder.mesh();
var primitiveBuilder = meshBuilder.primitive();
primitiveBuilder
.triangles()
.material("default")
.attribute("POSITION", "position")
.attribute("NORMAL", "normal")
.attribute("_BATCHID", "batchID")
.indices("index");
var gltf = builder.toGltf();
gltf.extensionsUsed.push("CESIUM_primitive_outline");
gltf.meshes[0].primitives[0].extensions = {
CESIUM_primitive_outline: {
indices: gltf.accessors.length - 1,
},
};
var model = new Model({
gltf: gltf,
});
primitives.add(model);
return waitForReady(scene, model).then(function () {
var gltf = model.gltf;
var primitive = gltf.meshes[0].primitives[0];
expect(gltf.accessors.length).toBe(6);
expect(gltf.accessors[0].count).toBeGreaterThan(9);
expect(gltf.accessors[1].count).toBeGreaterThan(9);
expect(gltf.accessors[2].count).toBeGreaterThan(9);
expect(gltf.accessors[0].count).toBe(gltf.accessors[1].count);
expect(gltf.accessors[1].count).toBe(gltf.accessors[2].count);
expect(Object.keys(primitive.attributes).length).toBe(4);
expect(primitive.attributes._OUTLINE_COORDINATES).toBeDefined();
expect(gltf.accessors[3].count).toBe(indices.length);
// Make sure the outline coordinates match the edges.
var accessorId = primitive.attributes._OUTLINE_COORDINATES;
var accessor = gltf.accessors[accessorId];
var bufferView = gltf.bufferViews[accessor.bufferView];
var buffer = gltf.buffers[bufferView.buffer];
var outlineCoordinates = new Float32Array(
buffer.extras._pipeline.source,
bufferView.byteOffset,
accessor.count * 3
);
var triangleAccessor = gltf.accessors.filter(function (accessor) {
return accessor.name === "index";
})[0];
var triangleBufferView = gltf.bufferViews[triangleAccessor.bufferView];
var triangleBuffer = gltf.buffers[triangleBufferView.buffer];
var triangleIndices = new Uint16Array(
triangleBuffer.extras._pipeline.source,
triangleBufferView.byteOffset,
triangleAccessor.count
);
for (var i = 0; i < triangleIndices.length; i += 3) {
var i0 = triangleIndices[i];
var i1 = triangleIndices[i + 1];
var i2 = triangleIndices[i + 2];
var i0Original = indices[i];
var i1Original = indices[i + 1];
var i2Original = indices[i + 2];
expect(
hasCoordinates(outlineCoordinates, i0, i1, i2, 1.0, 1.0, 0.0)
).toBe(hasEdge(edges, i0Original, i1Original));
expect(
hasCoordinates(outlineCoordinates, i0, i1, i2, 0.0, 1.0, 1.0)
).toBe(hasEdge(edges, i1Original, i2Original));
expect(
hasCoordinates(outlineCoordinates, i0, i1, i2, 1.0, 0.0, 1.0)
).toBe(hasEdge(edges, i2Original, i0Original));
expect(
hasCoordinates(outlineCoordinates, i0, i1, i2, 1.0, 0.0, 0.0)
).toBe(false);
expect(
hasCoordinates(outlineCoordinates, i0, i1, i2, 0.0, 1.0, 0.0)
).toBe(false);
expect(
hasCoordinates(outlineCoordinates, i0, i1, i2, 0.0, 0.0, 1.0)
).toBe(false);
expect(
hasCoordinates(outlineCoordinates, i0, i1, i2, 1.0, 1.0, 1.0)
).toBe(false);
}
builder.destroy();
});
});
function hasEdge(edges, i0, i1) {
var min = Math.min(i0, i1);
var max = Math.max(i0, i1);
for (var i = 0; i < edges.length; i += 2) {
var low = Math.min(edges[i], edges[i + 1]);
var high = Math.max(edges[i], edges[i + 1]);
if (low === min && high === max) {
return true;
}
}
return false;
}
function hasCoordinates(outlineCoordinates, i0, i1, i2, o0, o1, o2) {
var a0 = outlineCoordinates[i0 * 3];
var b0 = outlineCoordinates[i0 * 3 + 1];
var c0 = outlineCoordinates[i0 * 3 + 2];
var a1 = outlineCoordinates[i1 * 3];
var b1 = outlineCoordinates[i1 * 3 + 1];
var c1 = outlineCoordinates[i1 * 3 + 2];
var a2 = outlineCoordinates[i2 * 3];
var b2 = outlineCoordinates[i2 * 3 + 1];
var c2 = outlineCoordinates[i2 * 3 + 2];
return (
(a0 === o0 && a1 === o1 && a2 === o2) ||
(b0 === o0 && b1 === o1 && b2 === o2) ||
(c0 === o0 && c1 === o1 && c2 === o2)
);
}
it("ignores extension on primitive if it's not in extensionsUsed", function () {
var vertices = [];
var indices = [];
var edges = [];
createModel(vertices, indices, edges, 1, true, true, true);
var builder = createGltfBuilder();
var bufferBuilder = builder.buffer();
bufferBuilder
.vertexBuffer("vertices")
.vec3("position")
.vec3("normal")
.scalar("batchID")
.data(vertices);
bufferBuilder.indexBuffer("indices").scalar("index").data(indices);
bufferBuilder.indexBuffer("edgeIndices").scalar("edgeIndex").data(edges);
var meshBuilder = builder.mesh();
var primitiveBuilder = meshBuilder.primitive();
primitiveBuilder
.triangles()
.material("default")
.attribute("POSITION", "position")
.attribute("NORMAL", "normal")
.attribute("_BATCHID", "batchID")
.indices("index");
var gltf = builder.toGltf();
gltf.meshes[0].primitives[0].extensions = {
CESIUM_primitive_outline: {
indices: gltf.accessors.length - 1,
},
};
var model = new Model({
gltf: gltf,
});
primitives.add(model);
return waitForReady(scene, model).then(function () {
var gltf = model.gltf;
expect(gltf.buffers.length).toBe(1);
expect(gltf.accessors.length).toBe(5);
expect(gltf.accessors[0].count).toBe(9);
expect(gltf.accessors[1].count).toBe(9);
expect(gltf.accessors[2].count).toBe(9);
expect(
Object.keys(gltf.meshes[0].primitives[0].attributes).length
).toBe(3);
expect(gltf.accessors[3].count).toBe(indices.length);
builder.destroy();
});
});
it("doesn't break if extensionsUsed lists the extension but it's not really used", function () {
var vertices = [];
var indices = [];
var edges = [];
createModel(vertices, indices, edges, 1, true, true, true);
var builder = createGltfBuilder();
var bufferBuilder = builder.buffer();
bufferBuilder
.vertexBuffer("vertices")
.vec3("position")
.vec3("normal")
.scalar("batchID")
.data(vertices);
bufferBuilder.indexBuffer("indices").scalar("index").data(indices);
var meshBuilder = builder.mesh();
var primitiveBuilder = meshBuilder.primitive();
primitiveBuilder
.triangles()
.material("default")
.attribute("POSITION", "position")
.attribute("NORMAL", "normal")
.attribute("_BATCHID", "batchID")
.indices("index");
var gltf = builder.toGltf();
gltf.extensionsUsed.push("CESIUM_primitive_outline");
var model = new Model({
gltf: gltf,
});
primitives.add(model);
return waitForReady(scene, model).then(function () {
var gltf = model.gltf;
expect(gltf.buffers.length).toBe(1);
expect(gltf.accessors.length).toBe(4);
expect(gltf.accessors[0].count).toBe(9);
expect(gltf.accessors[1].count).toBe(9);
expect(gltf.accessors[2].count).toBe(9);
expect(
Object.keys(gltf.meshes[0].primitives[0].attributes).length
).toBe(3);
expect(gltf.accessors[3].count).toBe(indices.length);
builder.destroy();
});
});
it("handles vertices that are shared between two outlined primitives", function () {
var vertices = [];
var indices = [];
var edges = [];
createModel(vertices, indices, edges, 1, true, true, true);
var builder = createGltfBuilder();
var bufferBuilder = builder.buffer();
bufferBuilder
.vertexBuffer("vertices")
.vec3("position")
.vec3("normal")
.scalar("batchID")
.data(vertices);
bufferBuilder.indexBuffer("indices").scalar("index").data(indices);
bufferBuilder.indexBuffer("edgeIndices").scalar("edgeIndex").data(edges);
var meshBuilder = builder.mesh();
// Create two primitives, both using the same vertex buffer, but the
// second one only uses the positions.
var primitiveBuilder = meshBuilder.primitive();
primitiveBuilder
.triangles()
.material("default")
.attribute("POSITION", "position")
.attribute("NORMAL", "normal")
.attribute("_BATCHID", "batchID")
.indices("index");
var secondPrimitiveBuilder = meshBuilder.primitive();
secondPrimitiveBuilder
.triangles()
.material("default")
.attribute("POSITION", "position")
.indices("index");
var gltf = builder.toGltf();
// Add the outline extension to both primitives.
gltf.extensionsUsed.push("CESIUM_primitive_outline");
gltf.meshes[0].primitives[0].extensions = {
CESIUM_primitive_outline: {
indices: gltf.accessors.length - 1,
},
};
gltf.meshes[0].primitives[1].extensions = {
CESIUM_primitive_outline: {
indices: gltf.accessors.length - 1,
},
};
var model = new Model({
gltf: gltf,
});
primitives.add(model);
return waitForReady(scene, model).then(function () {
var gltf = model.gltf;
expect(gltf.accessors.length).toBe(6);
expect(gltf.accessors[0].count).toBeGreaterThan(9);
expect(gltf.accessors[1].count).toBeGreaterThan(9);
expect(gltf.accessors[2].count).toBeGreaterThan(9);
expect(gltf.accessors[0].count).toBe(gltf.accessors[1].count);
expect(gltf.accessors[1].count).toBe(gltf.accessors[2].count);
var firstPrimitive = gltf.meshes[0].primitives[0];
expect(Object.keys(firstPrimitive.attributes).length).toBe(4);
expect(firstPrimitive.attributes._OUTLINE_COORDINATES).toBeDefined();
var secondPrimitive = gltf.meshes[0].primitives[1];
expect(Object.keys(secondPrimitive.attributes).length).toBe(2);
expect(secondPrimitive.attributes._OUTLINE_COORDINATES).toBeDefined();
expect(gltf.accessors[3].count).toBe(indices.length);
builder.destroy();
});
});
it("handles vertices that are shared between an outlined and a non-outlined primitive", function () {
var vertices = [];
var indices = [];
var edges = [];
createModel(vertices, indices, edges, 1, true, true, true);
var builder = createGltfBuilder();
var bufferBuilder = builder.buffer();
bufferBuilder
.vertexBuffer("vertices")
.vec3("position")
.vec3("normal")
.scalar("batchID")
.data(vertices);
bufferBuilder.indexBuffer("indices").scalar("index").data(indices);
bufferBuilder.indexBuffer("edgeIndices").scalar("edgeIndex").data(edges);
var meshBuilder = builder.mesh();
// Create two primitives, both using the same vertex buffer, but the
// second one only uses the positions.
var primitiveBuilder = meshBuilder.primitive();
primitiveBuilder
.triangles()
.material("default")
.attribute("POSITION", "position")
.attribute("NORMAL", "normal")
.attribute("_BATCHID", "batchID")
.indices("index");
var secondPrimitiveBuilder = meshBuilder.primitive();
secondPrimitiveBuilder
.triangles()
.material("default")
.attribute("POSITION", "position")
.indices("index");
var gltf = builder.toGltf();
// Add the outline extension only to the first primitive.
gltf.extensionsUsed.push("CESIUM_primitive_outline");
gltf.meshes[0].primitives[0].extensions = {
CESIUM_primitive_outline: {
indices: gltf.accessors.length - 1,
},
};
var model = new Model({
gltf: gltf,
});
primitives.add(model);
return waitForReady(scene, model).then(function () {
var gltf = model.gltf;
expect(gltf.accessors.length).toBe(6);
expect(gltf.accessors[0].count).toBeGreaterThan(9);
expect(gltf.accessors[1].count).toBeGreaterThan(9);
expect(gltf.accessors[2].count).toBeGreaterThan(9);
expect(gltf.accessors[0].count).toBe(gltf.accessors[1].count);
expect(gltf.accessors[1].count).toBe(gltf.accessors[2].count);
var firstPrimitive = gltf.meshes[0].primitives[0];
expect(Object.keys(firstPrimitive.attributes).length).toBe(4);
expect(firstPrimitive.attributes._OUTLINE_COORDINATES).toBeDefined();
var secondPrimitive = gltf.meshes[0].primitives[1];
expect(Object.keys(secondPrimitive.attributes).length).toBe(1);
expect(secondPrimitive.attributes._OUTLINE_COORDINATES).toBeUndefined();
expect(gltf.accessors[3].count).toBe(indices.length);
builder.destroy();
});
});
it("switches to 32-bit indices if more than 65535 vertices are required", function () {
if (!scene.context.elementIndexUint) {
// This extension is supported everywhere these days, except possibly
// in our mocked WebGL context used in the tests on Travis. Consistent
// with the approach in ModelSpec.js, `loads a gltf with uint32 indices`,
// we'll just give this test a pass if uint indices aren't supported.
return;
}
var vertices = [];
var indices = [];
var edges = [];
// Tricky model is 9 vertices. Add copies of it until we're just under 65636 vertices.
for (var i = 0; vertices.length / 7 + 9 <= 65535; ++i) {
createTrickyModel(vertices, indices, edges, 2, true, true, true);
}
var builder = createGltfBuilder();
var bufferBuilder = builder.buffer();
bufferBuilder
.vertexBuffer("vertices")
.vec3("position")
.vec3("normal")
.scalar("batchID")
.data(vertices);
bufferBuilder.indexBuffer("indices").scalar("index").data(indices);
bufferBuilder.indexBuffer("edgeIndices").scalar("edgeIndex").data(edges);
var meshBuilder = builder.mesh();
var primitiveBuilder = meshBuilder.primitive();
primitiveBuilder
.triangles()
.material("default")
.attribute("POSITION", "position")
.attribute("NORMAL", "normal")
.attribute("_BATCHID", "batchID")
.indices("index");
var gltf = builder.toGltf();
gltf.extensionsUsed.push("CESIUM_primitive_outline");
gltf.meshes[0].primitives[0].extensions = {
CESIUM_primitive_outline: {
indices: gltf.accessors.length - 1,
},
};
var model = new Model({
gltf: gltf,
});
primitives.add(model);
return waitForReady(scene, model).then(function () {
var gltf = model.gltf;
var primitive = gltf.meshes[0].primitives[0];
var triangleIndexAccessor = gltf.accessors[primitive.indices];
// The accessor should now be 32-bit and reference higher-numbered vertices.
expect(triangleIndexAccessor.componentType).toBe(5125); // UNSIGNED_INT
expect(triangleIndexAccessor.max[0]).toBeGreaterThan(65536);
expect(triangleIndexAccessor.byteOffset).toBe(0);
var bufferView = gltf.bufferViews[triangleIndexAccessor.bufferView];
var buffer = gltf.buffers[bufferView.buffer];
var data = buffer.extras._pipeline.source;
var indexBuffer = new Uint32Array(
data,
data.byteOffset + bufferView.byteOffset,
triangleIndexAccessor.count
);
// All the original indices should be the same.
for (var i = 0; i < indices.length; ++i) {
// All indices in the original range should match the original ones
if (indexBuffer[i] < vertices.length / 7) {
expect(indexBuffer[i]).toBe(indices[i]);
}
}
var rendererIndexBuffer =
model._rendererResources.buffers[triangleIndexAccessor.bufferView];
expect(rendererIndexBuffer.bytesPerIndex).toBe(4);
builder.destroy();
});
});
},
"WebGL"
);
function createGltfBuilder() {
var builder = new GltfBuilder();
builder.material("default").json({
pbrMetallicRoughness: {
baseColorFactor: [1.0, 1.0, 1.0, 1.0],
metallicFactor: 0.5,
roughnessFactor: 0.125,
},
});
return builder;
}
function createModel(
vertices,
indices,
edges,
batchID,
includePositions,
includeNormals,
includeBatchID
) {
var vertexStride =
(includePositions ? 3 : 0) +
(includeNormals ? 3 : 0) +
(includeBatchID ? 1 : 0);
var normal = new Cartesian3(0.0, 0.0, 1.0);
function addVertex(position) {
if (includePositions) {
vertices.push(position.x, position.y, position.z);
}
if (includeNormals) {
vertices.push(normal.x, normal.y, normal.z);
}
if (includeBatchID) {
vertices.push(batchID);
}
return vertices.length / vertexStride - 1;
}
// 6---7---8
// | \ | / |
// 3---4---5
// | / | \ |
// 0---1---2
var p0 = new Cartesian3(-1.0, -1.0, 0.0);
var p1 = new Cartesian3(0.0, -1.0, 0.0);
var p2 = new Cartesian3(1.0, -1.0, 0.0);
var p3 = new Cartesian3(-1.0, 0.0, 0.0);
var p4 = new Cartesian3(0.0, 0.0, 0.0);
var p5 = new Cartesian3(1.0, 0.0, 0.0);
var p6 = new Cartesian3(-1.0, 1.0, 0.0);
var p7 = new Cartesian3(0.0, 1.0, 0.0);
var p8 = new Cartesian3(1.0, 1.0, 0.0);
var i0 = addVertex(p0);
var i1 = addVertex(p1);
var i2 = addVertex(p2);
var i3 = addVertex(p3);
var i4 = addVertex(p4);
var i5 = addVertex(p5);
var i6 = addVertex(p6);
var i7 = addVertex(p7);
var i8 = addVertex(p8);
indices.push(i0, i1, i4);
indices.push(i0, i4, i3);
indices.push(i1, i2, i4);
indices.push(i4, i2, i5);
indices.push(i3, i4, i6);
indices.push(i6, i4, i7);
indices.push(i4, i5, i8);
indices.push(i4, i8, i7);
edges.push(i0, i1);
edges.push(i1, i2);
edges.push(i2, i5);
edges.push(i5, i8);
edges.push(i8, i7);
edges.push(i7, i6);
edges.push(i6, i3);
}
function createTrickyModel(
vertices,
indices,
edges,
batchID,
includePositions,
includeNormals,
includeBatchID
) {
// This model is carefully constructed to require tricky vertex duplication
// for outlining.
var vertexStride =
(includePositions ? 3 : 0) +
(includeNormals ? 3 : 0) +
(includeBatchID ? 1 : 0);
var normal = new Cartesian3(0.0, 0.0, 1.0);
function addVertex(position) {
if (includePositions) {
vertices.push(position.x, position.y, position.z);
}
if (includeNormals) {
vertices.push(normal.x, normal.y, normal.z);
}
if (includeBatchID) {
vertices.push(batchID);
}
return vertices.length / vertexStride - 1;
}
// 1-2 5-4
// \| |/
// 0-----3
// \ /
// \ /
// 6
// / \
// / \
// 7-----8
var p0 = new Cartesian3(-1.0, 1.0, 0.0);
var p1 = new Cartesian3(-2.0, 2.0, 0.0);
var p2 = new Cartesian3(-1.0, 2.0, 0.0);
var p3 = new Cartesian3(1.0, 1.0, 0.0);
var p4 = new Cartesian3(2.0, 2.0, 0.0);
var p5 = new Cartesian3(1.0, 2.0, 0.0);
var p6 = new Cartesian3(0.0, 0.0, 0.0);
var p7 = new Cartesian3(-1.0, -1.0, 0.0);
var p8 = new Cartesian3(-1.0, 1.0, 0.0);
var i0 = addVertex(p0);
var i1 = addVertex(p1);
var i2 = addVertex(p2);
var i3 = addVertex(p3);
var i4 = addVertex(p4);
var i5 = addVertex(p5);
var i6 = addVertex(p6);
var i7 = addVertex(p7);
var i8 = addVertex(p8);
indices.push(i0, i2, i1);
indices.push(i3, i4, i5);
indices.push(i6, i7, i8);
indices.push(i0, i6, i3);
edges.push(i0, i1);
edges.push(i0, i2);
edges.push(i3, i5);
edges.push(i3, i4);
edges.push(i6, i7);
edges.push(i6, i8);
edges.push(i0, i3);
edges.push(i0, i6);
edges.push(i3, i6);
}
function waitForReady(scene, model) {
return pollToPromise(
function () {
// Render scene to progressively load the model
scene.renderForSpecs();
return model.ready;
},
{ timeout: 10000 }
)
.then(function () {
return model;
})
.otherwise(function () {
return when.reject(model);
});
}