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.

776 lines
23 KiB
JavaScript

import defined from "../Core/defined.js";
import PixelFormat from "../Core/PixelFormat.js";
import ContextLimits from "../Renderer/ContextLimits.js";
import Sampler from "../Renderer/Sampler.js";
import Texture from "../Renderer/Texture.js";
import TextureMagnificationFilter from "../Renderer/TextureMagnificationFilter.js";
import TextureMinificationFilter from "../Renderer/TextureMinificationFilter.js";
import TextureWrap from "../Renderer/TextureWrap.js";
import ForEach from "../ThirdParty/GltfPipeline/ForEach.js";
// glTF does not allow an index value of 65535 because this is the primitive
// restart value in some APIs.
var MAX_GLTF_UINT16_INDEX = 65534;
/**
* Creates face outlines for glTF primitives with the `CESIUM_primitive_outline` extension.
* @private
*/
function ModelOutlineLoader() {}
/**
* Returns true if the model uses or requires CESIUM_primitive_outline.
* @private
*/
ModelOutlineLoader.hasExtension = function (model) {
return (
defined(model.extensionsRequired.CESIUM_primitive_outline) ||
defined(model.extensionsUsed.CESIUM_primitive_outline)
);
};
/**
* Arranges to outline any primitives with the CESIUM_primitive_outline extension.
* It is expected that all buffer data is loaded and available in
* `extras._pipeline.source` before this function is called, and that vertex
* and index WebGL buffers are not yet created.
* @private
*/
ModelOutlineLoader.outlinePrimitives = function (model) {
if (!ModelOutlineLoader.hasExtension(model)) {
return;
}
var gltf = model.gltf;
// Assumption: A single bufferView contains a single zero-indexed range of vertices.
// No trickery with using large accessor byteOffsets to store multiple zero-based
// ranges of vertices in a single bufferView. Use separate bufferViews for that,
// you monster.
// Note that interleaved vertex attributes (e.g. position0, normal0, uv0,
// position1, normal1, uv1, ...) _are_ supported and should not be confused with
// the above.
var vertexNumberingScopes = [];
ForEach.mesh(gltf, function (mesh, meshId) {
ForEach.meshPrimitive(mesh, function (primitive, primitiveId) {
if (!defined(primitive.extensions)) {
return;
}
var outlineData = primitive.extensions.CESIUM_primitive_outline;
if (!defined(outlineData)) {
return;
}
var vertexNumberingScope = getVertexNumberingScope(model, primitive);
if (vertexNumberingScope === undefined) {
return;
}
if (vertexNumberingScopes.indexOf(vertexNumberingScope) < 0) {
vertexNumberingScopes.push(vertexNumberingScope);
}
// Add the outline to this primitive
addOutline(
model,
meshId,
primitiveId,
outlineData.indices,
vertexNumberingScope
);
});
});
// Update all relevant bufferViews to include the duplicate vertices that are
// needed for outlining.
for (var i = 0; i < vertexNumberingScopes.length; ++i) {
updateBufferViewsWithNewVertices(
model,
vertexNumberingScopes[i].bufferViews
);
}
// Remove data not referenced by any bufferViews anymore.
compactBuffers(model);
};
ModelOutlineLoader.createTexture = function (model, context) {
var cache = context.cache.modelOutliningCache;
if (!defined(cache)) {
cache = context.cache.modelOutliningCache = {};
}
if (defined(cache.outlineTexture)) {
return cache.outlineTexture;
}
var maxSize = Math.min(4096, ContextLimits.maximumTextureSize);
var size = maxSize;
var levelZero = createTexture(size);
var mipLevels = [];
while (size > 1) {
size >>= 1;
mipLevels.push(createTexture(size));
}
var texture = new Texture({
context: context,
source: {
arrayBufferView: levelZero,
mipLevels: mipLevels,
},
width: maxSize,
height: 1,
pixelFormat: PixelFormat.LUMINANCE,
sampler: new Sampler({
wrapS: TextureWrap.CLAMP_TO_EDGE,
wrapT: TextureWrap.CLAMP_TO_EDGE,
minificationFilter: TextureMinificationFilter.LINEAR_MIPMAP_LINEAR,
magnificationFilter: TextureMagnificationFilter.LINEAR,
}),
});
cache.outlineTexture = texture;
return texture;
};
function addOutline(
model,
meshId,
primitiveId,
edgeIndicesAccessorId,
vertexNumberingScope
) {
var vertexCopies = vertexNumberingScope.vertexCopies;
var extraVertices = vertexNumberingScope.extraVertices;
var outlineCoordinates = vertexNumberingScope.outlineCoordinates;
var gltf = model.gltf;
var mesh = gltf.meshes[meshId];
var primitive = mesh.primitives[primitiveId];
var accessors = gltf.accessors;
var bufferViews = gltf.bufferViews;
// Find the number of vertices in this primitive by looking at
// the first attribute. Others are required to be the same.
var numVertices;
for (var semantic in primitive.attributes) {
if (primitive.attributes.hasOwnProperty(semantic)) {
var attributeId = primitive.attributes[semantic];
var accessor = accessors[attributeId];
if (defined(accessor)) {
numVertices = accessor.count;
break;
}
}
}
if (!defined(numVertices)) {
return undefined;
}
var triangleIndexAccessorGltf = accessors[primitive.indices];
var triangleIndexBufferViewGltf =
bufferViews[triangleIndexAccessorGltf.bufferView];
var edgeIndexAccessorGltf = accessors[edgeIndicesAccessorId];
var edgeIndexBufferViewGltf = bufferViews[edgeIndexAccessorGltf.bufferView];
var loadResources = model._loadResources;
var triangleIndexBufferView = loadResources.getBuffer(
triangleIndexBufferViewGltf
);
var edgeIndexBufferView = loadResources.getBuffer(edgeIndexBufferViewGltf);
var triangleIndices =
triangleIndexAccessorGltf.componentType === 5123
? new Uint16Array(
triangleIndexBufferView.buffer,
triangleIndexBufferView.byteOffset +
triangleIndexAccessorGltf.byteOffset,
triangleIndexAccessorGltf.count
)
: new Uint32Array(
triangleIndexBufferView.buffer,
triangleIndexBufferView.byteOffset +
triangleIndexAccessorGltf.byteOffset,
triangleIndexAccessorGltf.count
);
var edgeIndices =
edgeIndexAccessorGltf.componentType === 5123
? new Uint16Array(
edgeIndexBufferView.buffer,
edgeIndexBufferView.byteOffset + edgeIndexAccessorGltf.byteOffset,
edgeIndexAccessorGltf.count
)
: new Uint32Array(
edgeIndexBufferView.buffer,
edgeIndexBufferView.byteOffset + edgeIndexAccessorGltf.byteOffset,
edgeIndexAccessorGltf.count
);
// Make a hash table for quick lookups of whether an edge exists between two
// vertices. The hash is a sparse array indexed by
// `smallerVertexIndex * totalNumberOfVertices + biggerVertexIndex`
// A value of 1 indicates an edge exists between the two vertex indices; any
// other value indicates that it does not. We store the
// `edgeSmallMultipler` - that is, the number of vertices in the equation
// above - at index 0 for easy access to it later.
var edgeSmallMultiplier = numVertices;
var edges = [edgeSmallMultiplier];
var i;
for (i = 0; i < edgeIndices.length; i += 2) {
var a = edgeIndices[i];
var b = edgeIndices[i + 1];
var small = Math.min(a, b);
var big = Math.max(a, b);
edges[small * edgeSmallMultiplier + big] = 1;
}
// For each triangle, adjust vertex data so that the correct edges are outlined.
for (i = 0; i < triangleIndices.length; i += 3) {
var i0 = triangleIndices[i];
var i1 = triangleIndices[i + 1];
var i2 = triangleIndices[i + 2];
var all = false; // set this to true to draw a full wireframe.
var has01 = all || isHighlighted(edges, i0, i1);
var has12 = all || isHighlighted(edges, i1, i2);
var has20 = all || isHighlighted(edges, i2, i0);
var unmatchableVertexIndex = matchAndStoreCoordinates(
outlineCoordinates,
i0,
i1,
i2,
has01,
has12,
has20
);
while (unmatchableVertexIndex >= 0) {
// Copy the unmatchable index and try again.
var copy;
if (unmatchableVertexIndex === i0) {
copy = vertexCopies[i0];
} else if (unmatchableVertexIndex === i1) {
copy = vertexCopies[i1];
} else {
copy = vertexCopies[i2];
}
if (copy === undefined) {
copy = numVertices + extraVertices.length;
var original = unmatchableVertexIndex;
while (original >= numVertices) {
original = extraVertices[original - numVertices];
}
extraVertices.push(original);
vertexCopies[unmatchableVertexIndex] = copy;
}
if (
copy > MAX_GLTF_UINT16_INDEX &&
triangleIndices instanceof Uint16Array
) {
// We outgrew a 16-bit index buffer, switch to 32-bit.
triangleIndices = new Uint32Array(triangleIndices);
triangleIndexAccessorGltf.componentType = 5125; // UNSIGNED_INT
triangleIndexBufferViewGltf.buffer =
gltf.buffers.push({
byteLength: triangleIndices.byteLength,
extras: {
_pipeline: {
source: triangleIndices.buffer,
},
},
}) - 1;
triangleIndexBufferViewGltf.byteLength = triangleIndices.byteLength;
triangleIndexBufferViewGltf.byteOffset = 0;
model._loadResources.buffers[
triangleIndexBufferViewGltf.buffer
] = new Uint8Array(
triangleIndices.buffer,
0,
triangleIndices.byteLength
);
// The index componentType is also squirreled away in ModelLoadResources.
// Hackily update it, or else we'll end up creating the wrong type
// of index buffer later.
loadResources.indexBuffersToCreate._array.forEach(function (toCreate) {
if (toCreate.id === triangleIndexAccessorGltf.bufferView) {
toCreate.componentType = triangleIndexAccessorGltf.componentType;
}
});
}
if (unmatchableVertexIndex === i0) {
i0 = copy;
triangleIndices[i] = copy;
} else if (unmatchableVertexIndex === i1) {
i1 = copy;
triangleIndices[i + 1] = copy;
} else {
i2 = copy;
triangleIndices[i + 2] = copy;
}
if (defined(triangleIndexAccessorGltf.max)) {
triangleIndexAccessorGltf.max[0] = Math.max(
triangleIndexAccessorGltf.max[0],
copy
);
}
unmatchableVertexIndex = matchAndStoreCoordinates(
outlineCoordinates,
i0,
i1,
i2,
has01,
has12,
has20
);
}
}
}
// Each vertex has three coordinates, a, b, and c.
// a is the coordinate that applies to edge 2-0 for the vertex.
// b is the coordinate that applies to edge 0-1 for the vertex.
// c is the coordinate that applies to edge 1-2 for the vertex.
// A single triangle with all edges highlighted:
//
// | a | b | c |
// | 1 | 1 | 0 |
// 0
// / \
// / \
// edge 0-1 / \ edge 2-0
// / \
// / \
// | a | b | c | 1-----------2 | a | b | c |
// | 0 | 1 | 1 | edge 1-2 | 1 | 0 | 1 |
//
// There are 6 possible orderings of coordinates a, b, and c:
// 0 - abc
// 1 - acb
// 2 - bac
// 3 - bca
// 4 - cab
// 5 - cba
// All vertices must use the _same ordering_ for the edges to be rendered
// correctly. So we compute a bitmask for each vertex, where the bit at
// each position indicates whether that ordering works (i.e. doesn't
// conflict with already-assigned coordinates) for that vertex.
// Then we can find an ordering that works for all three vertices with a
// bitwise AND.
function computeOrderMask(outlineCoordinates, vertexIndex, a, b, c) {
var startIndex = vertexIndex * 3;
var first = outlineCoordinates[startIndex];
var second = outlineCoordinates[startIndex + 1];
var third = outlineCoordinates[startIndex + 2];
if (first === undefined) {
// If one coordinate is undefined, they all are, and all orderings are fine.
return 63; // 0b111111;
}
return (
((first === a && second === b && third === c) << 0) +
((first === a && second === c && third === b) << 1) +
((first === b && second === a && third === c) << 2) +
((first === b && second === c && third === a) << 3) +
((first === c && second === a && third === b) << 4) +
((first === c && second === b && third === a) << 5)
);
}
// popcount for integers 0-63, inclusive.
// i.e. how many 1s are in the binary representation of the integer.
function popcount0to63(value) {
return (
(value & 1) +
((value >> 1) & 1) +
((value >> 2) & 1) +
((value >> 3) & 1) +
((value >> 4) & 1) +
((value >> 5) & 1)
);
}
function matchAndStoreCoordinates(
outlineCoordinates,
i0,
i1,
i2,
has01,
has12,
has20
) {
var a0 = has20 ? 1.0 : 0.0;
var b0 = has01 ? 1.0 : 0.0;
var c0 = 0.0;
var i0Mask = computeOrderMask(outlineCoordinates, i0, a0, b0, c0);
if (i0Mask === 0) {
return i0;
}
var a1 = 0.0;
var b1 = has01 ? 1.0 : 0.0;
var c1 = has12 ? 1.0 : 0.0;
var i1Mask = computeOrderMask(outlineCoordinates, i1, a1, b1, c1);
if (i1Mask === 0) {
return i1;
}
var a2 = has20 ? 1.0 : 0.0;
var b2 = 0.0;
var c2 = has12 ? 1.0 : 0.0;
var i2Mask = computeOrderMask(outlineCoordinates, i2, a2, b2, c2);
if (i2Mask === 0) {
return i2;
}
var workingOrders = i0Mask & i1Mask & i2Mask;
var a, b, c;
if (workingOrders & (1 << 0)) {
// 0 - abc
a = 0;
b = 1;
c = 2;
} else if (workingOrders & (1 << 1)) {
// 1 - acb
a = 0;
c = 1;
b = 2;
} else if (workingOrders & (1 << 2)) {
// 2 - bac
b = 0;
a = 1;
c = 2;
} else if (workingOrders & (1 << 3)) {
// 3 - bca
b = 0;
c = 1;
a = 2;
} else if (workingOrders & (1 << 4)) {
// 4 - cab
c = 0;
a = 1;
b = 2;
} else if (workingOrders & (1 << 5)) {
// 5 - cba
c = 0;
b = 1;
a = 2;
} else {
// No ordering works.
// Report the most constrained vertex as unmatched so we copy that one.
var i0Popcount = popcount0to63(i0Mask);
var i1Popcount = popcount0to63(i1Mask);
var i2Popcount = popcount0to63(i2Mask);
if (i0Popcount < i1Popcount && i0Popcount < i2Popcount) {
return i0;
} else if (i1Popcount < i2Popcount) {
return i1;
}
return i2;
}
var i0Start = i0 * 3;
outlineCoordinates[i0Start + a] = a0;
outlineCoordinates[i0Start + b] = b0;
outlineCoordinates[i0Start + c] = c0;
var i1Start = i1 * 3;
outlineCoordinates[i1Start + a] = a1;
outlineCoordinates[i1Start + b] = b1;
outlineCoordinates[i1Start + c] = c1;
var i2Start = i2 * 3;
outlineCoordinates[i2Start + a] = a2;
outlineCoordinates[i2Start + b] = b2;
outlineCoordinates[i2Start + c] = c2;
return -1;
}
function isHighlighted(edges, i0, i1) {
var edgeSmallMultiplier = edges[0];
var index = Math.min(i0, i1) * edgeSmallMultiplier + Math.max(i0, i1);
// If i0 and i1 are both 0, then our index will be 0 and we'll end up
// accessing the edgeSmallMultiplier that we've sneakily squirreled away
// in index 0. But it makes no sense to have an edge between vertex 0 and
// itself, so for any edgeSmallMultiplier other than 1 we'll return the
// correct answer: false. If edgeSmallMultiplier is 1, that means there is
// only a single vertex, so no danger of forming a meaningful triangle
// with that.
return edges[index] === 1;
}
function createTexture(size) {
var texture = new Uint8Array(size);
texture[size - 1] = 192;
if (size === 8) {
texture[size - 1] = 96;
} else if (size === 4) {
texture[size - 1] = 48;
} else if (size === 2) {
texture[size - 1] = 24;
} else if (size === 1) {
texture[size - 1] = 12;
}
return texture;
}
function updateBufferViewsWithNewVertices(model, bufferViews) {
var gltf = model.gltf;
var loadResources = model._loadResources;
var i, j;
for (i = 0; i < bufferViews.length; ++i) {
var bufferView = bufferViews[i];
var vertexNumberingScope = bufferView.extras._pipeline.vertexNumberingScope;
// Let the temporary data be garbage collected.
bufferView.extras._pipeline.vertexNumberingScope = undefined;
var newVertices = vertexNumberingScope.extraVertices;
var sourceData = loadResources.getBuffer(bufferView);
var byteStride = bufferView.byteStride || 4;
var newVerticesLength = newVertices.length;
var destData = new Uint8Array(
sourceData.byteLength + newVerticesLength * byteStride
);
// Copy the original vertices
destData.set(sourceData);
// Copy the vertices added for outlining
for (j = 0; j < newVerticesLength; ++j) {
var sourceIndex = newVertices[j] * byteStride;
var destIndex = sourceData.length + j * byteStride;
for (var k = 0; k < byteStride; ++k) {
destData[destIndex + k] = destData[sourceIndex + k];
}
}
// This bufferView is an independent buffer now. Update the model accordingly.
bufferView.byteOffset = 0;
bufferView.byteLength = destData.byteLength;
var bufferId =
gltf.buffers.push({
byteLength: destData.byteLength,
extras: {
_pipeline: {
source: destData.buffer,
},
},
}) - 1;
bufferView.buffer = bufferId;
loadResources.buffers[bufferId] = destData;
// Update the accessors to reflect the added vertices.
var accessors = vertexNumberingScope.accessors;
for (j = 0; j < accessors.length; ++j) {
var accessorId = accessors[j];
gltf.accessors[accessorId].count += newVerticesLength;
}
if (!vertexNumberingScope.createdOutlines) {
// Create the buffers, views, and accessors for the outline texture coordinates.
var outlineCoordinates = vertexNumberingScope.outlineCoordinates;
var outlineCoordinateBuffer = new Float32Array(outlineCoordinates);
var bufferIndex =
model.gltf.buffers.push({
byteLength: outlineCoordinateBuffer.byteLength,
extras: {
_pipeline: {
source: outlineCoordinateBuffer.buffer,
},
},
}) - 1;
loadResources.buffers[bufferIndex] = new Uint8Array(
outlineCoordinateBuffer.buffer,
0,
outlineCoordinateBuffer.byteLength
);
var bufferViewIndex =
model.gltf.bufferViews.push({
buffer: bufferIndex,
byteLength: outlineCoordinateBuffer.byteLength,
byteOffset: 0,
byteStride: 3 * Float32Array.BYTES_PER_ELEMENT,
target: 34962,
}) - 1;
var accessorIndex =
model.gltf.accessors.push({
bufferView: bufferViewIndex,
byteOffset: 0,
componentType: 5126,
count: outlineCoordinateBuffer.length / 3,
type: "VEC3",
min: [0.0, 0.0, 0.0],
max: [1.0, 1.0, 1.0],
}) - 1;
var primitives = vertexNumberingScope.primitives;
for (j = 0; j < primitives.length; ++j) {
primitives[j].attributes._OUTLINE_COORDINATES = accessorIndex;
}
loadResources.vertexBuffersToCreate.enqueue(bufferViewIndex);
vertexNumberingScope.createdOutlines = true;
}
}
}
function compactBuffers(model) {
var gltf = model.gltf;
var loadResources = model._loadResources;
var i;
for (i = 0; i < gltf.buffers.length; ++i) {
var buffer = gltf.buffers[i];
var bufferViewsUsingThisBuffer = gltf.bufferViews.filter(
usesBuffer.bind(undefined, i)
);
var newLength = bufferViewsUsingThisBuffer.reduce(function (
previous,
current
) {
return previous + current.byteLength;
},
0);
if (newLength === buffer.byteLength) {
continue;
}
var newBuffer = new Uint8Array(newLength);
var offset = 0;
for (var j = 0; j < bufferViewsUsingThisBuffer.length; ++j) {
var bufferView = bufferViewsUsingThisBuffer[j];
var sourceData = loadResources.getBuffer(bufferView);
newBuffer.set(sourceData, offset);
bufferView.byteOffset = offset;
offset += sourceData.byteLength;
}
loadResources.buffers[i] = newBuffer;
buffer.extras._pipeline.source = newBuffer.buffer;
buffer.byteLength = newLength;
}
}
function usesBuffer(bufferId, bufferView) {
return bufferView.buffer === bufferId;
}
function getVertexNumberingScope(model, primitive) {
var attributes = primitive.attributes;
if (attributes === undefined) {
return undefined;
}
var gltf = model.gltf;
var vertexNumberingScope;
// Initialize common details for all bufferViews used by this primitive's vertices.
// All bufferViews used by this primitive must use a common vertex numbering scheme.
for (var semantic in attributes) {
if (!attributes.hasOwnProperty(semantic)) {
continue;
}
var accessorId = attributes[semantic];
var accessor = gltf.accessors[accessorId];
var bufferViewId = accessor.bufferView;
var bufferView = gltf.bufferViews[bufferViewId];
if (!defined(bufferView.extras)) {
bufferView.extras = {};
}
if (!defined(bufferView.extras._pipeline)) {
bufferView.extras._pipeline = {};
}
if (!defined(bufferView.extras._pipeline.vertexNumberingScope)) {
bufferView.extras._pipeline.vertexNumberingScope = vertexNumberingScope || {
// Each element in this array is:
// a) undefined, if the vertex at this index has no copies
// b) the index of the copy.
vertexCopies: [],
// Extra vertices appended after the ones originally included in the model.
// Each element is the index of the vertex that this one is a copy of.
extraVertices: [],
// The texture coordinates used for outlining, three floats per vertex.
outlineCoordinates: [],
// The IDs of accessors that use this vertex numbering.
accessors: [],
// The IDs of bufferViews that use this vertex numbering.
bufferViews: [],
// The primitives that use this vertex numbering.
primitives: [],
// True if the buffer for the outlines has already been created.
createdOutlines: false,
};
} else if (
vertexNumberingScope !== undefined &&
bufferView.extras._pipeline.vertexNumberingScope !== vertexNumberingScope
) {
// Conflicting vertex numbering, let's give up.
return undefined;
}
vertexNumberingScope = bufferView.extras._pipeline.vertexNumberingScope;
if (vertexNumberingScope.bufferViews.indexOf(bufferView) < 0) {
vertexNumberingScope.bufferViews.push(bufferView);
}
if (vertexNumberingScope.accessors.indexOf(accessorId) < 0) {
vertexNumberingScope.accessors.push(accessorId);
}
}
vertexNumberingScope.primitives.push(primitive);
return vertexNumberingScope;
}
export default ModelOutlineLoader;