From 82166903da005102e4c7f474ccebd249e0e00f87 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Mon, 22 Jun 2020 17:02:51 +1000 Subject: [PATCH 01/17] Hack to always show model outlines. --- Source/Scene/ModelOutlineLoader.js | 21 ++++++++++++--------- Source/Scene/ModelUtility.js | 6 +++--- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/Source/Scene/ModelOutlineLoader.js b/Source/Scene/ModelOutlineLoader.js index d8561055255c..7d8e76eb5cef 100644 --- a/Source/Scene/ModelOutlineLoader.js +++ b/Source/Scene/ModelOutlineLoader.js @@ -23,10 +23,11 @@ function ModelOutlineLoader() {} * @private */ ModelOutlineLoader.hasExtension = function (model) { - return ( - defined(model.extensionsRequired.CESIUM_primitive_outline) || - defined(model.extensionsUsed.CESIUM_primitive_outline) - ); + return true; + // return ( + // defined(model.extensionsRequired.CESIUM_primitive_outline) || + // defined(model.extensionsUsed.CESIUM_primitive_outline) + // ); }; /** @@ -55,11 +56,13 @@ ModelOutlineLoader.outlinePrimitives = function (model) { ForEach.mesh(gltf, function (mesh, meshId) { ForEach.meshPrimitive(mesh, function (primitive, primitiveId) { - if (!defined(primitive.extensions)) { - return; - } + // if (!defined(primitive.extensions)) { + // return; + // } - var outlineData = primitive.extensions.CESIUM_primitive_outline; + var outlineData = { + indices: 0, + }; //primitive.extensions.CESIUM_primitive_outline; if (!defined(outlineData)) { return; } @@ -241,7 +244,7 @@ function addOutline( var i1 = triangleIndices[i + 1]; var i2 = triangleIndices[i + 2]; - var all = false; // set this to true to draw a full wireframe. + var all = true; // 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); diff --git a/Source/Scene/ModelUtility.js b/Source/Scene/ModelUtility.js index 7ede09e07575..c476dd16618f 100644 --- a/Source/Scene/ModelUtility.js +++ b/Source/Scene/ModelUtility.js @@ -82,9 +82,9 @@ ModelUtility.splitIncompatibleMaterials = function (gltf) { var hasNormals = defined(primitive.attributes.NORMAL); var hasTangents = defined(primitive.attributes.TANGENT); var hasTexCoords = defined(primitive.attributes.TEXCOORD_0); - var hasOutline = - defined(primitive.extensions) && - defined(primitive.extensions.CESIUM_primitive_outline); + var hasOutline = true; + // defined(primitive.extensions) && + // defined(primitive.extensions.CESIUM_primitive_outline); var primitiveInfo = primitiveInfoByMaterial[materialIndex]; if (!defined(primitiveInfo)) { From 3d5b78cd77cf9c19abbd478ac389754b8938d8b0 Mon Sep 17 00:00:00 2001 From: Emma Krantz Date: Tue, 11 Aug 2020 11:00:45 +0800 Subject: [PATCH 02/17] outlines now working for simple cube --- Source/Scene/ModelOutlineGenerator.js | 433 ++++++++++++++++++++++++++ Source/Scene/ModelOutlineLoader.js | 9 +- 2 files changed, 438 insertions(+), 4 deletions(-) create mode 100644 Source/Scene/ModelOutlineGenerator.js diff --git a/Source/Scene/ModelOutlineGenerator.js b/Source/Scene/ModelOutlineGenerator.js new file mode 100644 index 000000000000..19507a63e40d --- /dev/null +++ b/Source/Scene/ModelOutlineGenerator.js @@ -0,0 +1,433 @@ +import ForEach from "../ThirdParty/GltfPipeline/ForEach.js"; +import WebGLConstants from "../Core/WebGLConstants.js"; +import defined from "../Core/defined.js"; +import Cartesian3 from "../Core/Cartesian3.js"; +import ModelUtility from "../Scene/ModelUtility.js"; + +function ModelOutlineGenerator() {} + +ModelOutlineGenerator.generateOutlinesForModel = function (model) { + var gltf = model.gltf; + ForEach.mesh(gltf, function (mesh, meshId) { + ForEach.meshPrimitive(mesh, function (primitive, primitiveId) { + outlinePrimitive(model, meshId, primitiveId); + }); + }); +}; + +function outlinePrimitive(model, meshId, primitiveId) { + // TODO: Currently only works for indexed primitives + var gltf = model.gltf; + var mesh = gltf.meshes[meshId]; + var primitive = mesh.primitives[primitiveId]; + var accessors = gltf.accessors; + var bufferViews = gltf.bufferViews; + // TODO: handle unindexed tris + var triangleIndexAccessorGltf = accessors[primitive.indices]; + var triangleIndexBufferViewGltf = + bufferViews[triangleIndexAccessorGltf.bufferView]; + var positionAccessorGltf = accessors[primitive.attributes.POSITION]; + var positionBufferViewGltf = bufferViews[positionAccessorGltf.bufferView]; + // TODO: Error handling for no normals + var normalAccessorGltf = accessors[primitive.attributes.NORMAL]; + var normalBufferViewGltf = bufferViews[normalAccessorGltf.bufferView]; + + if (!defined(normalBufferViewGltf.byteStride)) { + normalBufferViewGltf.byteStride = 12; + } + if (!defined(positionBufferViewGltf.byteStride)) { + positionBufferViewGltf.byteStride = 12; + } + + var loadResources = model._loadResources; + var triangleIndexBufferView = loadResources.getBuffer( + triangleIndexBufferViewGltf + ); + var positionBufferView = loadResources.getBuffer(positionBufferViewGltf); + var positions = new Float32Array( + positionBufferView.buffer, + positionBufferView.byteOffset + positionAccessorGltf.byteOffset, + positionAccessorGltf.count * (positionBufferViewGltf.byteStride / 4) + ); + + var triangleIndices = + triangleIndexAccessorGltf.componentType === WebGLConstants.UNSIGNED_SHORT + ? new Uint16Array( + triangleIndexBufferView.buffer, + triangleIndexBufferView.byteOffset + + triangleIndexAccessorGltf.byteOffset, + triangleIndexAccessorGltf.count + ) + : new Uint32Array( + triangleIndexBufferView.buffer, + triangleIndexBufferView.byteOffset + + triangleIndexAccessorGltf.byteOffset, + triangleIndexAccessorGltf.count + ); + + var triangleGetter = generateVertexAttributeGetter(triangleIndices, 3); + + var halfEdgeMap = new Map(); + var vertexPositionGetter = generateVertexAttributeGetter( + positions, + positionBufferViewGltf.byteStride / 4 + ); + + for (let i = 0; i < triangleIndexAccessorGltf.count; i += 3) { + addIndexedTriangleToEdgeGraph( + halfEdgeMap, + i, + triangleIndices, + vertexPositionGetter + ); + } + // for (let i = 0; i < positionAccessorGltf.count; i += 3) { + // addTriangleToEdgeGraph(halfEdgeMap, i, vertexPositionGetter); + // } + + var normalBufferView = loadResources.getBuffer(normalBufferViewGltf); + var normals = new Float32Array( + normalBufferView.buffer, + normalBufferView.byteOffset + normalAccessorGltf.byteOffset, + normalAccessorGltf.count * (normalBufferViewGltf.byteStride / 4) + ); + var vertexNormalGetter = generateVertexAttributeGetter( + normals, + normalBufferViewGltf.byteStride / 4 + ); + + let outlineIndexBuffer = findEdgesToOutline( + halfEdgeMap, + vertexNormalGetter, + triangleIndices + ); + + // Add new buffer to gltf + var bufferId = + gltf.buffers.push({ + byteLength: outlineIndexBuffer.byteLength, + extras: { + _pipeline: { + source: outlineIndexBuffer.buffer, + }, + }, + }) - 1; + loadResources.buffers[bufferId] = outlineIndexBuffer; + + // Add new bufferview + let bufferViewId = + bufferViews.push({ + buffer: bufferId, + byteOffset: 0, + byteLength: outlineIndexBuffer.byteLength, + target: WebGLConstants.ELEMENT_ARRAY_BUFFER, + }) - 1; + + // Add new accessor + let accessorId = + accessors.push({ + bufferView: bufferViewId, + byteOffset: 0, + componentType: WebGLConstants.UNSIGNED_INT, + count: outlineIndexBuffer.length, // start and end for each line + }) - 1; + + mesh.primitives[primitiveId]["extensions"] = { + CESIUM_primitive_outline: { + indices: accessorId, + }, + }; + gltf.extensionsUsed.push("CESIUM_primitive_outline"); +} + +function generateVertexAttributeGetter(vertexArray, elementsPerVertex) { + return function (index) { + return [ + vertexArray[elementsPerVertex * index], + vertexArray[elementsPerVertex * index + 1], + vertexArray[elementsPerVertex * index + 2], + ]; + }; +} + +function addIndexedTriangleToEdgeGraph( + halfEdgeMap, + triangleStartIndex, + triangleIndices, + vertexPositionGetter +) { + let vertexIndexA = triangleIndices[triangleStartIndex]; + let vertexIndexB = triangleIndices[triangleStartIndex + 1]; + let vertexIndexC = triangleIndices[triangleStartIndex + 2]; + let first = addHalfEdge( + halfEdgeMap, + vertexPositionGetter, + vertexIndexA, + vertexIndexB, + triangleStartIndex + ); + let second = addHalfEdge( + halfEdgeMap, + vertexPositionGetter, + vertexIndexB, + vertexIndexC, + triangleStartIndex + ); + let last = addHalfEdge( + halfEdgeMap, + vertexPositionGetter, + vertexIndexC, + vertexIndexA, + triangleStartIndex + ); + + // and the other direction... + let first2 = addHalfEdge( + halfEdgeMap, + vertexPositionGetter, + vertexIndexC, + vertexIndexB, + triangleStartIndex + ); + let second2 = addHalfEdge( + halfEdgeMap, + vertexPositionGetter, + vertexIndexB, + vertexIndexA, + triangleStartIndex + ); + let last2 = addHalfEdge( + halfEdgeMap, + vertexPositionGetter, + vertexIndexA, + vertexIndexC, + triangleStartIndex + ); +} + +// function addTriangleToEdgeGraph( +// halfEdgeMap, +// triangleStartIndex, +// vertexPositionGetter +// ) { +// let first = addHalfEdge( +// halfEdgeMap, +// vertexPositionGetter, +// triangleStartIndex, +// triangleStartIndex + 1, +// undefined +// ); +// let second = addHalfEdge( +// halfEdgeMap, +// vertexPositionGetter, +// triangleStartIndex + 1, +// triangleStartIndex + 2 +// ); +// let last = addHalfEdge( +// halfEdgeMap, +// vertexPositionGetter, +// triangleStartIndex + 2, +// triangleStartIndex +// ); +// let first2 = addHalfEdge( +// halfEdgeMap, +// vertexPositionGetter, +// triangleStartIndex + 2, +// triangleStartIndex + 1 +// ); +// let second2 = addHalfEdge( +// halfEdgeMap, +// vertexPositionGetter, +// triangleStartIndex + 1, +// triangleStartIndex +// ); +// let last2 = addHalfEdge( +// halfEdgeMap, +// vertexPositionGetter, +// triangleStartIndex, +// triangleStartIndex + 2 +// ); +// } + +function addHalfEdge( + halfEdgeMap, + vertexPositionGetter, + sourceVertexIdx, + destinationVertexIdx, + triangleIndex +) { + const halfEdge = { + sourceVertex: vertexPositionGetter(sourceVertexIdx), + destinationVertex: vertexPositionGetter(destinationVertexIdx), + originalIdx: [sourceVertexIdx], + destinationIdx: [destinationVertexIdx], + }; + if (defined(triangleIndex)) { + halfEdge.triangleStartIndex = [triangleIndex]; + } + const mapIdx = generateMapKey( + halfEdge.sourceVertex, + halfEdge.destinationVertex + ); + const halfEdgeFromMap = halfEdgeMap.get(mapIdx); + if (halfEdgeFromMap) { + halfEdgeFromMap.originalIdx.push(sourceVertexIdx); + halfEdgeFromMap.destinationIdx.push(destinationVertexIdx); + if (defined(triangleIndex)) { + halfEdgeFromMap.triangleStartIndex.push(triangleIndex); + } + } else { + halfEdgeMap.set(mapIdx, halfEdge); + } + return halfEdge; +} + +function generateMapKey(sourceVertex, destinationVertex) { + return ( + "" + + sourceVertex[0] + + sourceVertex[1] + + sourceVertex[2] + + "#" + + destinationVertex[0] + + destinationVertex[1] + + destinationVertex[2] + ); +} + +function getNeighboringEdge(halfEdgeMap, edge) { + const neighborIdx = generateMapKey(edge.destinationVertex, edge.sourceVertex); + let neighbor = halfEdgeMap.get(neighborIdx); + const tolerance = Number.EPSILON; + if ( + neighbor && + (Math.abs(neighbor.destinationVertex[0] - edge.sourceVertex[0]) > + tolerance || + Math.abs(neighbor.destinationVertex[1] - edge.sourceVertex[1]) > + tolerance || + Math.abs(neighbor.destinationVertex[2] - edge.sourceVertex[2]) > + tolerance) + ) { + return undefined; + } + return neighbor; +} + +// Returns index of first vertex of triangle +function getFirstVertexOfFaces(halfEdge, triangleIndices) { + const faces = []; + if (halfEdge.triangleStartIndex) { + for (let index of halfEdge.triangleStartIndex) { + faces.push(triangleIndices[index]); + } + } else { + for (let index of halfEdge.originalIdx) { + const triangleStart = index - (index % 3); + faces.push(triangleStart); + } + } + return faces; +} + +function findEdgesToOutline(halfEdgeMap, vertexNormalGetter, triangleIndices) { + var outlineThese = []; + var minimumAngle = Math.PI / 20; + const checked = new Set(); + const allEdges = Array.from(halfEdgeMap.values()); + for (let i = 0; i < allEdges.length; i++) { + const edge = allEdges[i]; + if ( + checked.has(generateMapKey(edge.sourceVertex, edge.destinationVertex)) || + checked.has(generateMapKey(edge.destinationVertex, edge.sourceVertex)) + ) { + continue; + } + const neighbor = getNeighboringEdge(halfEdgeMap, edge); + if (!defined(neighbor)) { + continue; + } + const numIndicesToCheck = 21; + if (edge.originalIdx.length > numIndicesToCheck) { + edge.originalIdx = edge.originalIdx.slice(0, numIndicesToCheck); + } + if (neighbor.originalIdx.length > numIndicesToCheck) { + neighbor.originalIdx = neighbor.originalIdx.slice(0, numIndicesToCheck); + } + // FIXME + // there is something wrong with your face logic + // why do you need the first vertex of every face? + // why not just use the ones attached to the edge? + const primaryEdgeFaces = getFirstVertexOfFaces(edge, triangleIndices); + const neighbourEdgeFaces = getFirstVertexOfFaces(neighbor, triangleIndices); + let highlight = false; + let highlightStartVertex; + let highlightEndVertex; + for (let i = 0; i < primaryEdgeFaces.length; i++) { + if (highlight) { + break; + } + const faceNormal = vertexNormalGetter(primaryEdgeFaces[i]); + for (let j = 0; j < neighbourEdgeFaces.length; j++) { + if (primaryEdgeFaces[i] === neighbourEdgeFaces[j]) { + continue; + } + const neighborNormal = vertexNormalGetter(neighbourEdgeFaces[j]); + if (!defined(faceNormal) || !defined(neighborNormal)) { + continue; + } + let angleBetween; + try { + angleBetween = Cartesian3.angleBetween( + Cartesian3.fromArray(faceNormal), + Cartesian3.fromArray(neighborNormal) + ); + } catch (error) { + console.log( + "Error trying to find the angle between two faces' normals: " + + error + ); + continue; + } + if (angleBetween > minimumAngle && angleBetween < Math.PI - 0.01) { + highlight = true; + // TODO: make this work for unindexed triangles + + // highlightStartVertex = edge.originalIdx[0]; + // let allVerticesInTriangle = [ + // triangleIndices[edge.triangleStartIndex[i]], + // triangleIndices[edge.triangleStartIndex[i] + 1], + // triangleIndices[edge.triangleStartIndex[i] + 2], + // ]; + // let orderInTriangle = allVerticesInTriangle.indexOf( + // highlightStartVertex + // ); + // let destOrderInTriangle = (orderInTriangle + 1) % 3; + // highlightEndVertex = allVerticesInTriangle[destOrderInTriangle]; + + highlightStartVertex = edge.originalIdx[0]; + highlightEndVertex = edge.destinationIdx[0]; + outlineThese.push(highlightStartVertex); + outlineThese.push(highlightEndVertex); + highlight = true; + break; + // highlightStartVertex = neighbor.originalIdx[0]; + // highlightEndVertex = neighbor.destinationIdx[0]; + // outlineThese.push(highlightStartVertex); + // outlineThese.push(highlightEndVertex); + } + } + } + // if (highlight) { + // outlineThese.push(highlightStartVertex); + // outlineThese.push(highlightEndVertex); + // } + checked.add(generateMapKey(edge.sourceVertex, edge.destinationVertex)); + checked.add( + generateMapKey(neighbor.sourceVertex, neighbor.destinationVertex) + ); + } + // TODO: check how big the indices are, and if they can fit into a Uint16, use one + return new Uint32Array(outlineThese); +} + +export default ModelOutlineGenerator; diff --git a/Source/Scene/ModelOutlineLoader.js b/Source/Scene/ModelOutlineLoader.js index 7d8e76eb5cef..efbc3eeb28b1 100644 --- a/Source/Scene/ModelOutlineLoader.js +++ b/Source/Scene/ModelOutlineLoader.js @@ -7,6 +7,7 @@ import TextureMagnificationFilter from "../Renderer/TextureMagnificationFilter.j import TextureMinificationFilter from "../Renderer/TextureMinificationFilter.js"; import TextureWrap from "../Renderer/TextureWrap.js"; import ForEach from "../ThirdParty/GltfPipeline/ForEach.js"; +import ModelOutlineGenerator from "./ModelOutlineGenerator.js"; // glTF does not allow an index value of 65535 because this is the primitive // restart value in some APIs. @@ -44,6 +45,8 @@ ModelOutlineLoader.outlinePrimitives = function (model) { var gltf = model.gltf; + ModelOutlineGenerator.generateOutlinesForModel(model); + // 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, @@ -60,9 +63,7 @@ ModelOutlineLoader.outlinePrimitives = function (model) { // return; // } - var outlineData = { - indices: 0, - }; //primitive.extensions.CESIUM_primitive_outline; + var outlineData = primitive.extensions.CESIUM_primitive_outline; if (!defined(outlineData)) { return; } @@ -244,7 +245,7 @@ function addOutline( var i1 = triangleIndices[i + 1]; var i2 = triangleIndices[i + 2]; - var all = true; // set this to true to draw a full wireframe. + 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); From 0d7e152782a6a4dea6120c1b9037494cee3b7381 Mon Sep 17 00:00:00 2001 From: Emma Krantz Date: Wed, 12 Aug 2020 16:45:32 +0800 Subject: [PATCH 03/17] enable/disable outline generation --- Source/Scene/Model.js | 53 ++++++++++------- Source/Scene/ModelOutlineGenerator.js | 31 ++++++++-- Source/Scene/ModelOutlineLoader.js | 63 +++++++++++++++++---- Source/Scene/ModelUtility.js | 14 +++-- Source/Scene/OutlineGenerationMode.js | 18 ++++++ Source/Scene/processModelMaterialsCommon.js | 5 +- Source/Scene/processPbrMaterials.js | 5 +- 7 files changed, 143 insertions(+), 46 deletions(-) create mode 100644 Source/Scene/OutlineGenerationMode.js diff --git a/Source/Scene/Model.js b/Source/Scene/Model.js index d825ce77fdac..858dcc22f4e0 100644 --- a/Source/Scene/Model.js +++ b/Source/Scene/Model.js @@ -75,6 +75,7 @@ import processModelMaterialsCommon from "./processModelMaterialsCommon.js"; import processPbrMaterials from "./processPbrMaterials.js"; import SceneMode from "./SceneMode.js"; import ShadowMode from "./ShadowMode.js"; +import OutlineGenerationMode from "./OutlineGenerationMode.js"; var boundingSphereCartesian3Scratch = new Cartesian3(); @@ -317,6 +318,17 @@ function Model(options) { */ this.silhouetteSize = defaultValue(options.silhouetteSize, 0.0); + /** + * Determines whether outlines should be generated for this model. + * + * @type {OutlineGenerationMode} + * + * @default OutlineGenerationMode.USE_GLTF_SETTINGS + * + * @see ModelOutlineGenerator + */ + this.generateOutlines = OutlineGenerationMode.USE_GLTF_SETTINGS; + /** * The 4x4 transformation matrix that transforms the model from model to world coordinates. * When this is the identity matrix, the model is drawn in world coordinates, i.e., Earth's WGS84 coordinates. @@ -2454,16 +2466,15 @@ function createProgram(programToCreate, model, context) { var drawVS = modifyShader(vs, programId, model._vertexShaderLoaded); var drawFS = modifyShader(fs, programId, model._fragmentShaderLoaded); - - if (isOutline) { - drawFS = drawFS.replace( - "czm_writeLogDepth();", - " czm_writeLogDepth();\n" + - "#if defined(LOG_DEPTH) && !defined(DISABLE_LOG_DEPTH_FRAGMENT_WRITE)\n" + - " gl_FragDepthEXT -= 5e-5;\n" + - "#endif" - ); - } + if (isOutline) { + drawFS = drawFS.replace( + "czm_writeLogDepth();", + " czm_writeLogDepth();\n" + + "#if defined(LOG_DEPTH) && !defined(DISABLE_LOG_DEPTH_FRAGMENT_WRITE)\n" + + " gl_FragDepthEXT -= 5e-5;\n" + + "#endif" + ); + } if (!defined(model._uniformMapLoaded)) { drawFS = "uniform vec4 czm_pickColor;\n" + drawFS; } @@ -2582,17 +2593,16 @@ function recreateProgram(programToCreate, model, context) { var drawVS = modifyShader(vs, programId, model._vertexShaderLoaded); var drawFS = modifyShader(finalFS, programId, model._fragmentShaderLoaded); - - var isOutline = program.isOutline; - if (isOutline) { - drawFS = drawFS.replace( - "czm_writeLogDepth();", - " czm_writeLogDepth();\n" + - "#if defined(LOG_DEPTH) && !defined(DISABLE_LOG_DEPTH_FRAGMENT_WRITE)\n" + - " gl_FragDepthEXT -= 5e-5;\n" + - "#endif" - ); - } + var isOutline = program.isOutline; + if (isOutline) { + drawFS = drawFS.replace( + "czm_writeLogDepth();", + " czm_writeLogDepth();\n" + + "#if defined(LOG_DEPTH) && !defined(DISABLE_LOG_DEPTH_FRAGMENT_WRITE)\n" + + " gl_FragDepthEXT -= 5e-5;\n" + + "#endif" + ); + } if (!defined(model._uniformMapLoaded)) { drawFS = "uniform vec4 czm_pickColor;\n" + drawFS; } @@ -5276,6 +5286,7 @@ Model.prototype.update = function (frameState) { var options = { addBatchIdToGeneratedShaders: this._addBatchIdToGeneratedShaders, + outlineGenerationMode: this.generateOutlines, }; processModelMaterialsCommon(gltf, options); diff --git a/Source/Scene/ModelOutlineGenerator.js b/Source/Scene/ModelOutlineGenerator.js index 19507a63e40d..c728e5485083 100644 --- a/Source/Scene/ModelOutlineGenerator.js +++ b/Source/Scene/ModelOutlineGenerator.js @@ -47,7 +47,7 @@ function outlinePrimitive(model, meshId, primitiveId) { var positions = new Float32Array( positionBufferView.buffer, positionBufferView.byteOffset + positionAccessorGltf.byteOffset, - positionAccessorGltf.count * (positionBufferViewGltf.byteStride / 4) + positionAccessorGltf.count * 3 ); var triangleIndices = @@ -95,11 +95,26 @@ function outlinePrimitive(model, meshId, primitiveId) { normals, normalBufferViewGltf.byteStride / 4 ); + var minimumAngle = Math.PI / 20; + + if ( + defined(mesh.primitives[primitiveId].extensions) && + defined(mesh.primitives[primitiveId].extensions.CESIUM_primitive_outline) && + defined( + mesh.primitives[primitiveId].extensions.CESIUM_primitive_outline + .outlineWhenAngleBetweenFaceNormalsExceeds + ) + ) { + minimumAngle = + mesh.primitives[primitiveId].extensions.CESIUM_primitive_outline + .outlineWhenAngleBetweenFaceNormalsExceeds; + } let outlineIndexBuffer = findEdgesToOutline( halfEdgeMap, vertexNormalGetter, - triangleIndices + triangleIndices, + minimumAngle ); // Add new buffer to gltf @@ -132,7 +147,7 @@ function outlinePrimitive(model, meshId, primitiveId) { count: outlineIndexBuffer.length, // start and end for each line }) - 1; - mesh.primitives[primitiveId]["extensions"] = { + mesh.primitives[primitiveId].extensions = { CESIUM_primitive_outline: { indices: accessorId, }, @@ -329,9 +344,13 @@ function getFirstVertexOfFaces(halfEdge, triangleIndices) { return faces; } -function findEdgesToOutline(halfEdgeMap, vertexNormalGetter, triangleIndices) { +function findEdgesToOutline( + halfEdgeMap, + vertexNormalGetter, + triangleIndices, + minimumAngle +) { var outlineThese = []; - var minimumAngle = Math.PI / 20; const checked = new Set(); const allEdges = Array.from(halfEdgeMap.values()); for (let i = 0; i < allEdges.length; i++) { @@ -378,7 +397,7 @@ function findEdgesToOutline(halfEdgeMap, vertexNormalGetter, triangleIndices) { let angleBetween; try { angleBetween = Cartesian3.angleBetween( - Cartesian3.fromArray(faceNormal), + Cartesian3.fromArray(faceNormal), Cartesian3.fromArray(neighborNormal) ); } catch (error) { diff --git a/Source/Scene/ModelOutlineLoader.js b/Source/Scene/ModelOutlineLoader.js index efbc3eeb28b1..365ac01a4675 100644 --- a/Source/Scene/ModelOutlineLoader.js +++ b/Source/Scene/ModelOutlineLoader.js @@ -8,6 +8,7 @@ import TextureMinificationFilter from "../Renderer/TextureMinificationFilter.js" import TextureWrap from "../Renderer/TextureWrap.js"; import ForEach from "../ThirdParty/GltfPipeline/ForEach.js"; import ModelOutlineGenerator from "./ModelOutlineGenerator.js"; +import OutlineGenerationMode from "./OutlineGenerationMode.js"; // glTF does not allow an index value of 65535 because this is the primitive // restart value in some APIs. @@ -24,11 +25,46 @@ function ModelOutlineLoader() {} * @private */ ModelOutlineLoader.hasExtension = function (model) { - return true; - // return ( - // defined(model.extensionsRequired.CESIUM_primitive_outline) || - // defined(model.extensionsUsed.CESIUM_primitive_outline) - // ); + return ( + defined(model.extensionsRequired.CESIUM_primitive_outline) || + defined(model.extensionsUsed.CESIUM_primitive_outline) + ); +}; + +/** + * Returns true if outlines should be generated for the model. + * @private + */ +ModelOutlineLoader.shouldGenerateOutlines = function (model) { + if (model.generateOutlines === OutlineGenerationMode.ON) { + return true; + } + + var outlineGenerationRequestedInGltf = false; + if (ModelOutlineLoader.hasExtension(model)) { + ForEach.mesh(model.gltf, function (mesh, _meshId) { + ForEach.meshPrimitive(mesh, function (primitive, _primitiveId) { + if ( + defined(primitive.extensions) && + defined(primitive.extensions.CESIUM_primitive_outline) && + defined( + primitive.extensions.CESIUM_primitive_outline + .outlineWhenAngleBetweenFaceNormalsExceeds + ) + ) { + outlineGenerationRequestedInGltf = true; + } + }); + }); + } + if ( + model.generateOutlines === OutlineGenerationMode.USE_GLTF_SETTINGS && + outlineGenerationRequestedInGltf + ) { + return true; + } + + return false; }; /** @@ -39,13 +75,18 @@ ModelOutlineLoader.hasExtension = function (model) { * @private */ ModelOutlineLoader.outlinePrimitives = function (model) { - if (!ModelOutlineLoader.hasExtension(model)) { + if ( + !ModelOutlineLoader.hasExtension(model) && + !ModelOutlineLoader.shouldGenerateOutlines(model) + ) { return; } - var gltf = model.gltf; + if (ModelOutlineLoader.shouldGenerateOutlines(model)) { + ModelOutlineGenerator.generateOutlinesForModel(model); + } - ModelOutlineGenerator.generateOutlinesForModel(model); + 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 @@ -59,9 +100,9 @@ ModelOutlineLoader.outlinePrimitives = function (model) { ForEach.mesh(gltf, function (mesh, meshId) { ForEach.meshPrimitive(mesh, function (primitive, primitiveId) { - // if (!defined(primitive.extensions)) { - // return; - // } + if (!defined(primitive.extensions)) { + return; + } var outlineData = primitive.extensions.CESIUM_primitive_outline; if (!defined(outlineData)) { diff --git a/Source/Scene/ModelUtility.js b/Source/Scene/ModelUtility.js index c476dd16618f..9e95a49af748 100644 --- a/Source/Scene/ModelUtility.js +++ b/Source/Scene/ModelUtility.js @@ -16,6 +16,7 @@ import ForEach from "../ThirdParty/GltfPipeline/ForEach.js"; import hasExtension from "../ThirdParty/GltfPipeline/hasExtension.js"; import AttributeType from "./AttributeType.js"; import Axis from "./Axis.js"; +import OutlineGenerationMode from "./OutlineGenerationMode.js"; /** * @private @@ -59,7 +60,10 @@ ModelUtility.getAssetVersion = function (gltf) { * @param {Object} gltf A javascript object containing a glTF asset. * @returns {Object} The glTF asset with modified materials. */ -ModelUtility.splitIncompatibleMaterials = function (gltf) { +ModelUtility.splitIncompatibleMaterials = function ( + gltf, + outlineGenerationMode +) { var accessors = gltf.accessors; var materials = gltf.materials; var primitiveInfoByMaterial = {}; @@ -82,9 +86,11 @@ ModelUtility.splitIncompatibleMaterials = function (gltf) { var hasNormals = defined(primitive.attributes.NORMAL); var hasTangents = defined(primitive.attributes.TANGENT); var hasTexCoords = defined(primitive.attributes.TEXCOORD_0); - var hasOutline = true; - // defined(primitive.extensions) && - // defined(primitive.extensions.CESIUM_primitive_outline); + var hasOutline = + outlineGenerationMode === OutlineGenerationMode.ON || + (OutlineGenerationMode.USE_GLTF_SETTINGS && + defined(primitive.extensions) && + defined(primitive.extensions.CESIUM_primitive_outline)); var primitiveInfo = primitiveInfoByMaterial[materialIndex]; if (!defined(primitiveInfo)) { diff --git a/Source/Scene/OutlineGenerationMode.js b/Source/Scene/OutlineGenerationMode.js new file mode 100644 index 000000000000..c9c4bc5cfb1d --- /dev/null +++ b/Source/Scene/OutlineGenerationMode.js @@ -0,0 +1,18 @@ +/** + * Defines different modes for automatically generating outlines for models. + * + * USE_MODEL_SETTINGS will follow whatever is set in the glTF underlying the model. + * OFF forces outlines to not be generated, overriding what is specified in the model. + * ON forces outlines to be generated, overriding what is specified in the model. + * + * @enum {Number} + * + * @see Model.generateOutlines + */ +var OutlineGenerationMode = { + OFF: 0, + ON: 1, + USE_GLTF_SETTINGS: 2, +}; + +export default Object.freeze(OutlineGenerationMode); diff --git a/Source/Scene/processModelMaterialsCommon.js b/Source/Scene/processModelMaterialsCommon.js index 5ce0bf0f226a..226212d2e899 100644 --- a/Source/Scene/processModelMaterialsCommon.js +++ b/Source/Scene/processModelMaterialsCommon.js @@ -6,12 +6,13 @@ import addToArray from "../ThirdParty/GltfPipeline/addToArray.js"; import ForEach from "../ThirdParty/GltfPipeline/ForEach.js"; import hasExtension from "../ThirdParty/GltfPipeline/hasExtension.js"; import ModelUtility from "./ModelUtility.js"; +import OutlineGenerationMode from "./OutlineGenerationMode.js"; /** * @private */ function processModelMaterialsCommon(gltf, options) { - options = defaultValue(options, defaultValue.EMPTY_OBJECT); + options = defaultValue(options, {outlineGenerationMode: OutlineGenerationMode.USE_GLTF_SETTINGS}); if (!defined(gltf)) { return; @@ -41,7 +42,7 @@ function processModelMaterialsCommon(gltf, options) { var lightParameters = generateLightParameters(gltf); - var primitiveByMaterial = ModelUtility.splitIncompatibleMaterials(gltf); + var primitiveByMaterial = ModelUtility.splitIncompatibleMaterials(gltf, options.outlineGenerationMode); var techniques = {}; var generatedTechniques = false; diff --git a/Source/Scene/processPbrMaterials.js b/Source/Scene/processPbrMaterials.js index b43ebe17bbd4..ae030df1c50d 100644 --- a/Source/Scene/processPbrMaterials.js +++ b/Source/Scene/processPbrMaterials.js @@ -6,12 +6,13 @@ import addToArray from "../ThirdParty/GltfPipeline/addToArray.js"; import ForEach from "../ThirdParty/GltfPipeline/ForEach.js"; import hasExtension from "../ThirdParty/GltfPipeline/hasExtension.js"; import ModelUtility from "./ModelUtility.js"; +import OutlineGenerationMode from "./OutlineGenerationMode.js"; /** * @private */ function processPbrMaterials(gltf, options) { - options = defaultValue(options, defaultValue.EMPTY_OBJECT); + options = defaultValue(options, {outlineGenerationMode: OutlineGenerationMode.USE_GLTF_SETTINGS}); // No need to create new techniques if they already exist, // the shader should handle these values @@ -46,7 +47,7 @@ function processPbrMaterials(gltf, options) { gltf.extensionsUsed.push("KHR_techniques_webgl"); gltf.extensionsRequired.push("KHR_techniques_webgl"); - var primitiveByMaterial = ModelUtility.splitIncompatibleMaterials(gltf); + var primitiveByMaterial = ModelUtility.splitIncompatibleMaterials(gltf, options.outlineGenerationMode); ForEach.material(gltf, function (material, materialIndex) { var generatedMaterialValues = {}; From e94561ee8a970ddfff129bcf2ac39742c82f8dff Mon Sep 17 00:00:00 2001 From: Emma Krantz Date: Tue, 25 Aug 2020 11:13:48 +0800 Subject: [PATCH 04/17] on the fly outlines work for gltfs and 3dtiles --- Source/Scene/Batched3DModel3DTileContent.js | 2 ++ Source/Scene/Cesium3DTileset.js | 7 +++++++ Source/Scene/Model.js | 4 ++-- Source/Scene/ModelOutlineGenerator.js | 3 +++ Source/Scene/ModelOutlineLoader.js | 2 +- 5 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Source/Scene/Batched3DModel3DTileContent.js b/Source/Scene/Batched3DModel3DTileContent.js index 5e38ecf48cce..7d3b736e1747 100644 --- a/Source/Scene/Batched3DModel3DTileContent.js +++ b/Source/Scene/Batched3DModel3DTileContent.js @@ -433,6 +433,7 @@ function initialize(content, arrayBuffer, byteOffset) { sphericalHarmonicCoefficients: tileset.sphericalHarmonicCoefficients, specularEnvironmentMaps: tileset.specularEnvironmentMaps, backFaceCulling: tileset.backFaceCulling, + outlineGenerationMode: tileset.outlineGenerationMode }); content._model.readyPromise.then(function (model) { model.activeAnimations.addAll({ @@ -541,6 +542,7 @@ Batched3DModel3DTileContent.prototype.update = function (tileset, frameState) { this._model.sphericalHarmonicCoefficients = this._tileset.sphericalHarmonicCoefficients; this._model.specularEnvironmentMaps = this._tileset.specularEnvironmentMaps; this._model.backFaceCulling = this._tileset.backFaceCulling; + this._model.outlineGenerationMode = this._tileset.outlineGenerationMode; this._model.debugWireframe = this._tileset.debugWireframe; // Update clipping planes diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index b03ca969c1fd..b30add85d023 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -43,6 +43,7 @@ import StencilConstants from "./StencilConstants.js"; import TileBoundingRegion from "./TileBoundingRegion.js"; import TileBoundingSphere from "./TileBoundingSphere.js"; import TileOrientedBoundingBox from "./TileOrientedBoundingBox.js"; +import OutlineGenerationMode from "./OutlineGenerationMode.js"; /** * A {@link https://github.com/CesiumGS/3d-tiles/tree/master/specification|3D Tiles tileset}, @@ -90,6 +91,7 @@ import TileOrientedBoundingBox from "./TileOrientedBoundingBox.js"; * @param {Cartesian3[]} [options.sphericalHarmonicCoefficients] The third order spherical harmonic coefficients used for the diffuse color of image-based lighting. * @param {String} [options.specularEnvironmentMaps] A URL to a KTX file that contains a cube map of the specular lighting and the convoluted specular mipmaps. * @param {Boolean} [options.backFaceCulling=true] Whether to cull back-facing geometry. When true, back face culling is determined by the glTF material's doubleSided property; when false, back face culling is disabled. + * @param {OutlineGenerationMode} [options.outlineGenerationMode=OutlineGenerationMode.USE_GLTF_SETTINGS] * @param {String} [options.debugHeatmapTilePropertyName] The tile variable to colorize as a heatmap. All rendered tiles will be colorized relative to each other's specified variable value. * @param {Boolean} [options.debugFreezeFrame=false] For debugging only. Determines if only the tiles from last frame should be used for rendering. * @param {Boolean} [options.debugColorizeTiles=false] For debugging only. When true, assigns a random color to each tile. @@ -760,6 +762,11 @@ function Cesium3DTileset(options) { */ this.backFaceCulling = defaultValue(options.backFaceCulling, true); + this.outlineGenerationMode = defaultValue( + options.outlineGenerationMode, + OutlineGenerationMode.USE_GLTF_SETTINGS + ); + /** * This property is for debugging only; it is not optimized for production use. *

diff --git a/Source/Scene/Model.js b/Source/Scene/Model.js index 858dcc22f4e0..c707112a5a7e 100644 --- a/Source/Scene/Model.js +++ b/Source/Scene/Model.js @@ -327,7 +327,7 @@ function Model(options) { * * @see ModelOutlineGenerator */ - this.generateOutlines = OutlineGenerationMode.USE_GLTF_SETTINGS; + this.outlineGenerationMode = defaultValue(options.outlineGenerationMode, OutlineGenerationMode.USE_GLTF_SETTINGS); /** * The 4x4 transformation matrix that transforms the model from model to world coordinates. @@ -5286,7 +5286,7 @@ Model.prototype.update = function (frameState) { var options = { addBatchIdToGeneratedShaders: this._addBatchIdToGeneratedShaders, - outlineGenerationMode: this.generateOutlines, + outlineGenerationMode: this.outlineGenerationMode, }; processModelMaterialsCommon(gltf, options); diff --git a/Source/Scene/ModelOutlineGenerator.js b/Source/Scene/ModelOutlineGenerator.js index c728e5485083..075fa24f0134 100644 --- a/Source/Scene/ModelOutlineGenerator.js +++ b/Source/Scene/ModelOutlineGenerator.js @@ -30,6 +30,9 @@ function outlinePrimitive(model, meshId, primitiveId) { var positionBufferViewGltf = bufferViews[positionAccessorGltf.bufferView]; // TODO: Error handling for no normals var normalAccessorGltf = accessors[primitive.attributes.NORMAL]; + if (!defined(normalAccessorGltf.bufferView)) { + normalAccessorGltf.bufferView = 0; + } var normalBufferViewGltf = bufferViews[normalAccessorGltf.bufferView]; if (!defined(normalBufferViewGltf.byteStride)) { diff --git a/Source/Scene/ModelOutlineLoader.js b/Source/Scene/ModelOutlineLoader.js index 365ac01a4675..227e8487e57a 100644 --- a/Source/Scene/ModelOutlineLoader.js +++ b/Source/Scene/ModelOutlineLoader.js @@ -36,7 +36,7 @@ ModelOutlineLoader.hasExtension = function (model) { * @private */ ModelOutlineLoader.shouldGenerateOutlines = function (model) { - if (model.generateOutlines === OutlineGenerationMode.ON) { + if (model.outlineGenerationMode === OutlineGenerationMode.ON) { return true; } From 6019ad647974f17e3047147fe10dac363358ee9e Mon Sep 17 00:00:00 2001 From: Emma Krantz Date: Tue, 25 Aug 2020 16:22:27 +0800 Subject: [PATCH 05/17] OutlineGenerationMode -> ModelOutlineGenerationMode --- Source/Scene/Cesium3DTileset.js | 6 +++--- Source/Scene/Model.js | 8 ++++---- ...ineGenerationMode.js => ModelOutlineGenerationMode.js} | 4 ++-- Source/Scene/ModelOutlineLoader.js | 6 +++--- Source/Scene/ModelUtility.js | 6 +++--- Source/Scene/processModelMaterialsCommon.js | 4 ++-- Source/Scene/processPbrMaterials.js | 4 ++-- 7 files changed, 19 insertions(+), 19 deletions(-) rename Source/Scene/{OutlineGenerationMode.js => ModelOutlineGenerationMode.js} (82%) diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index b30add85d023..27cfbc9338af 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -43,7 +43,7 @@ import StencilConstants from "./StencilConstants.js"; import TileBoundingRegion from "./TileBoundingRegion.js"; import TileBoundingSphere from "./TileBoundingSphere.js"; import TileOrientedBoundingBox from "./TileOrientedBoundingBox.js"; -import OutlineGenerationMode from "./OutlineGenerationMode.js"; +import ModelOutlineGenerationMode from "./ModelOutlineGenerationMode.js"; /** * A {@link https://github.com/CesiumGS/3d-tiles/tree/master/specification|3D Tiles tileset}, @@ -91,7 +91,7 @@ import OutlineGenerationMode from "./OutlineGenerationMode.js"; * @param {Cartesian3[]} [options.sphericalHarmonicCoefficients] The third order spherical harmonic coefficients used for the diffuse color of image-based lighting. * @param {String} [options.specularEnvironmentMaps] A URL to a KTX file that contains a cube map of the specular lighting and the convoluted specular mipmaps. * @param {Boolean} [options.backFaceCulling=true] Whether to cull back-facing geometry. When true, back face culling is determined by the glTF material's doubleSided property; when false, back face culling is disabled. - * @param {OutlineGenerationMode} [options.outlineGenerationMode=OutlineGenerationMode.USE_GLTF_SETTINGS] + * @param {ModelOutlineGenerationMode} [options.outlineGenerationMode=ModelOutlineGenerationMode.USE_GLTF_SETTINGS] * @param {String} [options.debugHeatmapTilePropertyName] The tile variable to colorize as a heatmap. All rendered tiles will be colorized relative to each other's specified variable value. * @param {Boolean} [options.debugFreezeFrame=false] For debugging only. Determines if only the tiles from last frame should be used for rendering. * @param {Boolean} [options.debugColorizeTiles=false] For debugging only. When true, assigns a random color to each tile. @@ -764,7 +764,7 @@ function Cesium3DTileset(options) { this.outlineGenerationMode = defaultValue( options.outlineGenerationMode, - OutlineGenerationMode.USE_GLTF_SETTINGS + ModelOutlineGenerationMode.USE_GLTF_SETTINGS ); /** diff --git a/Source/Scene/Model.js b/Source/Scene/Model.js index c707112a5a7e..07ca2f74fa88 100644 --- a/Source/Scene/Model.js +++ b/Source/Scene/Model.js @@ -75,7 +75,7 @@ import processModelMaterialsCommon from "./processModelMaterialsCommon.js"; import processPbrMaterials from "./processPbrMaterials.js"; import SceneMode from "./SceneMode.js"; import ShadowMode from "./ShadowMode.js"; -import OutlineGenerationMode from "./OutlineGenerationMode.js"; +import ModelOutlineGenerationMode from "./ModelOutlineGenerationMode.js"; var boundingSphereCartesian3Scratch = new Cartesian3(); @@ -321,13 +321,13 @@ function Model(options) { /** * Determines whether outlines should be generated for this model. * - * @type {OutlineGenerationMode} + * @type {ModelOutlineGenerationMode} * - * @default OutlineGenerationMode.USE_GLTF_SETTINGS + * @default ModelOutlineGenerationMode.USE_GLTF_SETTINGS * * @see ModelOutlineGenerator */ - this.outlineGenerationMode = defaultValue(options.outlineGenerationMode, OutlineGenerationMode.USE_GLTF_SETTINGS); + this.outlineGenerationMode = defaultValue(options.outlineGenerationMode, ModelOutlineGenerationMode.USE_GLTF_SETTINGS); /** * The 4x4 transformation matrix that transforms the model from model to world coordinates. diff --git a/Source/Scene/OutlineGenerationMode.js b/Source/Scene/ModelOutlineGenerationMode.js similarity index 82% rename from Source/Scene/OutlineGenerationMode.js rename to Source/Scene/ModelOutlineGenerationMode.js index c9c4bc5cfb1d..f7047a1a3654 100644 --- a/Source/Scene/OutlineGenerationMode.js +++ b/Source/Scene/ModelOutlineGenerationMode.js @@ -9,10 +9,10 @@ * * @see Model.generateOutlines */ -var OutlineGenerationMode = { +var ModelOutlineGenerationMode = { OFF: 0, ON: 1, USE_GLTF_SETTINGS: 2, }; -export default Object.freeze(OutlineGenerationMode); +export default Object.freeze(ModelOutlineGenerationMode); diff --git a/Source/Scene/ModelOutlineLoader.js b/Source/Scene/ModelOutlineLoader.js index 227e8487e57a..bf7061b7368a 100644 --- a/Source/Scene/ModelOutlineLoader.js +++ b/Source/Scene/ModelOutlineLoader.js @@ -8,7 +8,7 @@ import TextureMinificationFilter from "../Renderer/TextureMinificationFilter.js" import TextureWrap from "../Renderer/TextureWrap.js"; import ForEach from "../ThirdParty/GltfPipeline/ForEach.js"; import ModelOutlineGenerator from "./ModelOutlineGenerator.js"; -import OutlineGenerationMode from "./OutlineGenerationMode.js"; +import ModelOutlineGenerationMode from "./ModelOutlineGenerationMode.js"; // glTF does not allow an index value of 65535 because this is the primitive // restart value in some APIs. @@ -36,7 +36,7 @@ ModelOutlineLoader.hasExtension = function (model) { * @private */ ModelOutlineLoader.shouldGenerateOutlines = function (model) { - if (model.outlineGenerationMode === OutlineGenerationMode.ON) { + if (model.outlineGenerationMode === ModelOutlineGenerationMode.ON) { return true; } @@ -58,7 +58,7 @@ ModelOutlineLoader.shouldGenerateOutlines = function (model) { }); } if ( - model.generateOutlines === OutlineGenerationMode.USE_GLTF_SETTINGS && + model.generateOutlines === ModelOutlineGenerationMode.USE_GLTF_SETTINGS && outlineGenerationRequestedInGltf ) { return true; diff --git a/Source/Scene/ModelUtility.js b/Source/Scene/ModelUtility.js index 9e95a49af748..b8e40d9d9565 100644 --- a/Source/Scene/ModelUtility.js +++ b/Source/Scene/ModelUtility.js @@ -16,7 +16,7 @@ import ForEach from "../ThirdParty/GltfPipeline/ForEach.js"; import hasExtension from "../ThirdParty/GltfPipeline/hasExtension.js"; import AttributeType from "./AttributeType.js"; import Axis from "./Axis.js"; -import OutlineGenerationMode from "./OutlineGenerationMode.js"; +import ModelOutlineGenerationMode from "./ModelOutlineGenerationMode.js"; /** * @private @@ -87,8 +87,8 @@ ModelUtility.splitIncompatibleMaterials = function ( var hasTangents = defined(primitive.attributes.TANGENT); var hasTexCoords = defined(primitive.attributes.TEXCOORD_0); var hasOutline = - outlineGenerationMode === OutlineGenerationMode.ON || - (OutlineGenerationMode.USE_GLTF_SETTINGS && + outlineGenerationMode === ModelOutlineGenerationMode.ON || + (ModelOutlineGenerationMode.USE_GLTF_SETTINGS && defined(primitive.extensions) && defined(primitive.extensions.CESIUM_primitive_outline)); diff --git a/Source/Scene/processModelMaterialsCommon.js b/Source/Scene/processModelMaterialsCommon.js index 226212d2e899..bb59571836d1 100644 --- a/Source/Scene/processModelMaterialsCommon.js +++ b/Source/Scene/processModelMaterialsCommon.js @@ -6,13 +6,13 @@ import addToArray from "../ThirdParty/GltfPipeline/addToArray.js"; import ForEach from "../ThirdParty/GltfPipeline/ForEach.js"; import hasExtension from "../ThirdParty/GltfPipeline/hasExtension.js"; import ModelUtility from "./ModelUtility.js"; -import OutlineGenerationMode from "./OutlineGenerationMode.js"; +import ModelOutlineGenerationMode from "./ModelOutlineGenerationMode.js"; /** * @private */ function processModelMaterialsCommon(gltf, options) { - options = defaultValue(options, {outlineGenerationMode: OutlineGenerationMode.USE_GLTF_SETTINGS}); + options = defaultValue(options, {outlineGenerationMode: ModelOutlineGenerationMode.USE_GLTF_SETTINGS}); if (!defined(gltf)) { return; diff --git a/Source/Scene/processPbrMaterials.js b/Source/Scene/processPbrMaterials.js index ae030df1c50d..a9716e2fea98 100644 --- a/Source/Scene/processPbrMaterials.js +++ b/Source/Scene/processPbrMaterials.js @@ -6,13 +6,13 @@ import addToArray from "../ThirdParty/GltfPipeline/addToArray.js"; import ForEach from "../ThirdParty/GltfPipeline/ForEach.js"; import hasExtension from "../ThirdParty/GltfPipeline/hasExtension.js"; import ModelUtility from "./ModelUtility.js"; -import OutlineGenerationMode from "./OutlineGenerationMode.js"; +import ModelOutlineGenerationMode from "./ModelOutlineGenerationMode.js"; /** * @private */ function processPbrMaterials(gltf, options) { - options = defaultValue(options, {outlineGenerationMode: OutlineGenerationMode.USE_GLTF_SETTINGS}); + options = defaultValue(options, {outlineGenerationMode: ModelOutlineGenerationMode.USE_GLTF_SETTINGS}); // No need to create new techniques if they already exist, // the shader should handle these values From e8c92a148aad5ed7730e50cf320d8ad1ac7a5fd1 Mon Sep 17 00:00:00 2001 From: Emma Krantz Date: Tue, 25 Aug 2020 16:29:00 +0800 Subject: [PATCH 06/17] const and let to var --- Source/Scene/ModelOutlineGenerator.js | 128 ++++++++------------------ 1 file changed, 39 insertions(+), 89 deletions(-) diff --git a/Source/Scene/ModelOutlineGenerator.js b/Source/Scene/ModelOutlineGenerator.js index 075fa24f0134..26b6d1b93e53 100644 --- a/Source/Scene/ModelOutlineGenerator.js +++ b/Source/Scene/ModelOutlineGenerator.js @@ -2,14 +2,13 @@ import ForEach from "../ThirdParty/GltfPipeline/ForEach.js"; import WebGLConstants from "../Core/WebGLConstants.js"; import defined from "../Core/defined.js"; import Cartesian3 from "../Core/Cartesian3.js"; -import ModelUtility from "../Scene/ModelUtility.js"; function ModelOutlineGenerator() {} ModelOutlineGenerator.generateOutlinesForModel = function (model) { var gltf = model.gltf; ForEach.mesh(gltf, function (mesh, meshId) { - ForEach.meshPrimitive(mesh, function (primitive, primitiveId) { + ForEach.meshPrimitive(mesh, function (_primitive, primitiveId) { outlinePrimitive(model, meshId, primitiveId); }); }); @@ -68,8 +67,6 @@ function outlinePrimitive(model, meshId, primitiveId) { triangleIndexAccessorGltf.count ); - var triangleGetter = generateVertexAttributeGetter(triangleIndices, 3); - var halfEdgeMap = new Map(); var vertexPositionGetter = generateVertexAttributeGetter( positions, @@ -84,9 +81,6 @@ function outlinePrimitive(model, meshId, primitiveId) { vertexPositionGetter ); } - // for (let i = 0; i < positionAccessorGltf.count; i += 3) { - // addTriangleToEdgeGraph(halfEdgeMap, i, vertexPositionGetter); - // } var normalBufferView = loadResources.getBuffer(normalBufferViewGltf); var normals = new Float32Array( @@ -113,7 +107,7 @@ function outlinePrimitive(model, meshId, primitiveId) { .outlineWhenAngleBetweenFaceNormalsExceeds; } - let outlineIndexBuffer = findEdgesToOutline( + var outlineIndexBuffer = findEdgesToOutline( halfEdgeMap, vertexNormalGetter, triangleIndices, @@ -133,7 +127,7 @@ function outlinePrimitive(model, meshId, primitiveId) { loadResources.buffers[bufferId] = outlineIndexBuffer; // Add new bufferview - let bufferViewId = + var bufferViewId = bufferViews.push({ buffer: bufferId, byteOffset: 0, @@ -142,7 +136,7 @@ function outlinePrimitive(model, meshId, primitiveId) { }) - 1; // Add new accessor - let accessorId = + var accessorId = accessors.push({ bufferView: bufferViewId, byteOffset: 0, @@ -174,24 +168,24 @@ function addIndexedTriangleToEdgeGraph( triangleIndices, vertexPositionGetter ) { - let vertexIndexA = triangleIndices[triangleStartIndex]; - let vertexIndexB = triangleIndices[triangleStartIndex + 1]; - let vertexIndexC = triangleIndices[triangleStartIndex + 2]; - let first = addHalfEdge( + var vertexIndexA = triangleIndices[triangleStartIndex]; + var vertexIndexB = triangleIndices[triangleStartIndex + 1]; + var vertexIndexC = triangleIndices[triangleStartIndex + 2]; + var first = addHalfEdge( halfEdgeMap, vertexPositionGetter, vertexIndexA, vertexIndexB, triangleStartIndex ); - let second = addHalfEdge( + var second = addHalfEdge( halfEdgeMap, vertexPositionGetter, vertexIndexB, vertexIndexC, triangleStartIndex ); - let last = addHalfEdge( + var last = addHalfEdge( halfEdgeMap, vertexPositionGetter, vertexIndexC, @@ -200,21 +194,21 @@ function addIndexedTriangleToEdgeGraph( ); // and the other direction... - let first2 = addHalfEdge( + var first2 = addHalfEdge( halfEdgeMap, vertexPositionGetter, vertexIndexC, vertexIndexB, triangleStartIndex ); - let second2 = addHalfEdge( + var second2 = addHalfEdge( halfEdgeMap, vertexPositionGetter, vertexIndexB, vertexIndexA, triangleStartIndex ); - let last2 = addHalfEdge( + var last2 = addHalfEdge( halfEdgeMap, vertexPositionGetter, vertexIndexA, @@ -223,50 +217,6 @@ function addIndexedTriangleToEdgeGraph( ); } -// function addTriangleToEdgeGraph( -// halfEdgeMap, -// triangleStartIndex, -// vertexPositionGetter -// ) { -// let first = addHalfEdge( -// halfEdgeMap, -// vertexPositionGetter, -// triangleStartIndex, -// triangleStartIndex + 1, -// undefined -// ); -// let second = addHalfEdge( -// halfEdgeMap, -// vertexPositionGetter, -// triangleStartIndex + 1, -// triangleStartIndex + 2 -// ); -// let last = addHalfEdge( -// halfEdgeMap, -// vertexPositionGetter, -// triangleStartIndex + 2, -// triangleStartIndex -// ); -// let first2 = addHalfEdge( -// halfEdgeMap, -// vertexPositionGetter, -// triangleStartIndex + 2, -// triangleStartIndex + 1 -// ); -// let second2 = addHalfEdge( -// halfEdgeMap, -// vertexPositionGetter, -// triangleStartIndex + 1, -// triangleStartIndex -// ); -// let last2 = addHalfEdge( -// halfEdgeMap, -// vertexPositionGetter, -// triangleStartIndex, -// triangleStartIndex + 2 -// ); -// } - function addHalfEdge( halfEdgeMap, vertexPositionGetter, @@ -274,7 +224,7 @@ function addHalfEdge( destinationVertexIdx, triangleIndex ) { - const halfEdge = { + var halfEdge = { sourceVertex: vertexPositionGetter(sourceVertexIdx), destinationVertex: vertexPositionGetter(destinationVertexIdx), originalIdx: [sourceVertexIdx], @@ -283,11 +233,11 @@ function addHalfEdge( if (defined(triangleIndex)) { halfEdge.triangleStartIndex = [triangleIndex]; } - const mapIdx = generateMapKey( + var mapIdx = generateMapKey( halfEdge.sourceVertex, halfEdge.destinationVertex ); - const halfEdgeFromMap = halfEdgeMap.get(mapIdx); + var halfEdgeFromMap = halfEdgeMap.get(mapIdx); if (halfEdgeFromMap) { halfEdgeFromMap.originalIdx.push(sourceVertexIdx); halfEdgeFromMap.destinationIdx.push(destinationVertexIdx); @@ -314,9 +264,9 @@ function generateMapKey(sourceVertex, destinationVertex) { } function getNeighboringEdge(halfEdgeMap, edge) { - const neighborIdx = generateMapKey(edge.destinationVertex, edge.sourceVertex); - let neighbor = halfEdgeMap.get(neighborIdx); - const tolerance = Number.EPSILON; + var neighborIdx = generateMapKey(edge.destinationVertex, edge.sourceVertex); + var neighbor = halfEdgeMap.get(neighborIdx); + var tolerance = Number.EPSILON; if ( neighbor && (Math.abs(neighbor.destinationVertex[0] - edge.sourceVertex[0]) > @@ -333,14 +283,14 @@ function getNeighboringEdge(halfEdgeMap, edge) { // Returns index of first vertex of triangle function getFirstVertexOfFaces(halfEdge, triangleIndices) { - const faces = []; + var faces = []; if (halfEdge.triangleStartIndex) { - for (let index of halfEdge.triangleStartIndex) { + for (var index of halfEdge.triangleStartIndex) { faces.push(triangleIndices[index]); } } else { - for (let index of halfEdge.originalIdx) { - const triangleStart = index - (index % 3); + for (var index of halfEdge.originalIdx) { + var triangleStart = index - (index % 3); faces.push(triangleStart); } } @@ -354,21 +304,21 @@ function findEdgesToOutline( minimumAngle ) { var outlineThese = []; - const checked = new Set(); - const allEdges = Array.from(halfEdgeMap.values()); - for (let i = 0; i < allEdges.length; i++) { - const edge = allEdges[i]; + var checked = new Set(); + var allEdges = Array.from(halfEdgeMap.values()); + for (var i = 0; i < allEdges.length; i++) { + var edge = allEdges[i]; if ( checked.has(generateMapKey(edge.sourceVertex, edge.destinationVertex)) || checked.has(generateMapKey(edge.destinationVertex, edge.sourceVertex)) ) { continue; } - const neighbor = getNeighboringEdge(halfEdgeMap, edge); + var neighbor = getNeighboringEdge(halfEdgeMap, edge); if (!defined(neighbor)) { continue; } - const numIndicesToCheck = 21; + var numIndicesToCheck = 21; if (edge.originalIdx.length > numIndicesToCheck) { edge.originalIdx = edge.originalIdx.slice(0, numIndicesToCheck); } @@ -379,25 +329,25 @@ function findEdgesToOutline( // there is something wrong with your face logic // why do you need the first vertex of every face? // why not just use the ones attached to the edge? - const primaryEdgeFaces = getFirstVertexOfFaces(edge, triangleIndices); - const neighbourEdgeFaces = getFirstVertexOfFaces(neighbor, triangleIndices); - let highlight = false; - let highlightStartVertex; - let highlightEndVertex; - for (let i = 0; i < primaryEdgeFaces.length; i++) { + var primaryEdgeFaces = getFirstVertexOfFaces(edge, triangleIndices); + var neighbourEdgeFaces = getFirstVertexOfFaces(neighbor, triangleIndices); + var highlight = false; + var highlightStartVertex; + var highlightEndVertex; + for (var i = 0; i < primaryEdgeFaces.length; i++) { if (highlight) { break; } - const faceNormal = vertexNormalGetter(primaryEdgeFaces[i]); - for (let j = 0; j < neighbourEdgeFaces.length; j++) { + var faceNormal = vertexNormalGetter(primaryEdgeFaces[i]); + for (var j = 0; j < neighbourEdgeFaces.length; j++) { if (primaryEdgeFaces[i] === neighbourEdgeFaces[j]) { continue; } - const neighborNormal = vertexNormalGetter(neighbourEdgeFaces[j]); + var neighborNormal = vertexNormalGetter(neighbourEdgeFaces[j]); if (!defined(faceNormal) || !defined(neighborNormal)) { continue; } - let angleBetween; + var angleBetween; try { angleBetween = Cartesian3.angleBetween( Cartesian3.fromArray(faceNormal), From c1a40bbd90c6870125c546ca986e1e63f5a57198 Mon Sep 17 00:00:00 2001 From: Emma Krantz Date: Fri, 28 Aug 2020 13:41:07 +0800 Subject: [PATCH 07/17] cleaned up outline generation --- Source/Scene/ModelOutlineGenerationMode.js | 2 +- Source/Scene/ModelOutlineGenerator.js | 343 +++++++++++---------- Source/Scene/ModelOutlineLoader.js | 109 +++++-- 3 files changed, 274 insertions(+), 180 deletions(-) diff --git a/Source/Scene/ModelOutlineGenerationMode.js b/Source/Scene/ModelOutlineGenerationMode.js index f7047a1a3654..63bdd009649f 100644 --- a/Source/Scene/ModelOutlineGenerationMode.js +++ b/Source/Scene/ModelOutlineGenerationMode.js @@ -1,7 +1,7 @@ /** * Defines different modes for automatically generating outlines for models. * - * USE_MODEL_SETTINGS will follow whatever is set in the glTF underlying the model. + * USE_GLTF_SETTINGS will follow whatever is set in the glTF underlying the model. * OFF forces outlines to not be generated, overriding what is specified in the model. * ON forces outlines to be generated, overriding what is specified in the model. * diff --git a/Source/Scene/ModelOutlineGenerator.js b/Source/Scene/ModelOutlineGenerator.js index 26b6d1b93e53..664824d252ef 100644 --- a/Source/Scene/ModelOutlineGenerator.js +++ b/Source/Scene/ModelOutlineGenerator.js @@ -2,96 +2,151 @@ import ForEach from "../ThirdParty/GltfPipeline/ForEach.js"; import WebGLConstants from "../Core/WebGLConstants.js"; import defined from "../Core/defined.js"; import Cartesian3 from "../Core/Cartesian3.js"; +import { DeveloperError } from "../Core/DeveloperError.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; function ModelOutlineGenerator() {} ModelOutlineGenerator.generateOutlinesForModel = function (model) { var gltf = model.gltf; + var outlineAny = false; ForEach.mesh(gltf, function (mesh, meshId) { ForEach.meshPrimitive(mesh, function (_primitive, primitiveId) { - outlinePrimitive(model, meshId, primitiveId); + outlineAny = outlinePrimitive(model, meshId, primitiveId) || outlineAny; }); }); + return outlineAny; }; function outlinePrimitive(model, meshId, primitiveId) { - // TODO: Currently only works for indexed primitives var gltf = model.gltf; var mesh = gltf.meshes[meshId]; var primitive = mesh.primitives[primitiveId]; var accessors = gltf.accessors; var bufferViews = gltf.bufferViews; - // TODO: handle unindexed tris var triangleIndexAccessorGltf = accessors[primitive.indices]; - var triangleIndexBufferViewGltf = - bufferViews[triangleIndexAccessorGltf.bufferView]; + var triangleIndexBufferViewGltf; + var indexedTriangleMode = false; + if (defined(triangleIndexAccessorGltf)) { + triangleIndexBufferViewGltf = + bufferViews[triangleIndexAccessorGltf.bufferView]; + indexedTriangleMode = true; + } var positionAccessorGltf = accessors[primitive.attributes.POSITION]; var positionBufferViewGltf = bufferViews[positionAccessorGltf.bufferView]; - // TODO: Error handling for no normals var normalAccessorGltf = accessors[primitive.attributes.NORMAL]; - if (!defined(normalAccessorGltf.bufferView)) { - normalAccessorGltf.bufferView = 0; + if (!defined(normalAccessorGltf)) { + // Can't outline this model because it has no normals + return false; } var normalBufferViewGltf = bufferViews[normalAccessorGltf.bufferView]; if (!defined(normalBufferViewGltf.byteStride)) { - normalBufferViewGltf.byteStride = 12; + normalBufferViewGltf.byteStride = Float32Array.BYTES_PER_ELEMENT * 3; } if (!defined(positionBufferViewGltf.byteStride)) { - positionBufferViewGltf.byteStride = 12; + positionBufferViewGltf.byteStride = Float32Array.BYTES_PER_ELEMENT * 3; } var loadResources = model._loadResources; - var triangleIndexBufferView = loadResources.getBuffer( - triangleIndexBufferViewGltf - ); + + var triangleIndexBufferView; + if (indexedTriangleMode) { + triangleIndexBufferView = loadResources.getBuffer( + triangleIndexBufferViewGltf + ); + } + var positionBufferView = loadResources.getBuffer(positionBufferViewGltf); var positions = new Float32Array( positionBufferView.buffer, positionBufferView.byteOffset + positionAccessorGltf.byteOffset, - positionAccessorGltf.count * 3 + positionAccessorGltf.count * 3 //x, y, z + ); + + var normalBufferView = loadResources.getBuffer(normalBufferViewGltf); + var normals = new Float32Array( + normalBufferView.buffer, + normalBufferView.byteOffset + normalAccessorGltf.byteOffset, + normalAccessorGltf.count * 3 //x, y, z + ); + var vertexNormalGetter = generateVertexAttributeGetter( + normals, + normalBufferViewGltf.byteStride / Float32Array.BYTES_PER_ELEMENT ); - var triangleIndices = - triangleIndexAccessorGltf.componentType === WebGLConstants.UNSIGNED_SHORT - ? new Uint16Array( - triangleIndexBufferView.buffer, - triangleIndexBufferView.byteOffset + - triangleIndexAccessorGltf.byteOffset, - triangleIndexAccessorGltf.count - ) - : new Uint32Array( - triangleIndexBufferView.buffer, - triangleIndexBufferView.byteOffset + - triangleIndexAccessorGltf.byteOffset, - triangleIndexAccessorGltf.count - ); + var triangleIndices; + if (indexedTriangleMode) { + triangleIndices = + triangleIndexAccessorGltf.componentType === WebGLConstants.UNSIGNED_SHORT + ? new Uint16Array( + triangleIndexBufferView.buffer, + triangleIndexBufferView.byteOffset + + triangleIndexAccessorGltf.byteOffset, + triangleIndexAccessorGltf.count + ) + : new Uint32Array( + triangleIndexBufferView.buffer, + triangleIndexBufferView.byteOffset + + triangleIndexAccessorGltf.byteOffset, + triangleIndexAccessorGltf.count + ); + } + /* + * To figure out which faces are adjacent in this mesh, we put its edges into a directed half edge map. + * + * The version used here is adapted from the one described in [this paper](https://www.graphics.rwth-aachen.de/media/papers/directed.pdf). + * A + * / ^ + * / \ + * v 1 \ + * B -----> C + * <----- + * \ 2 ^ + * \ / + * v / + * D + * Each face is represented by 3 directed half edges. For example, face 1 is made up of: + * A -> B + * B -> C + * C -> A + * + * Each edge has a neighbor connecting the same vertices but in the opposite direction. In the diagram above, B->C's neighbor is C->B. + * For each of a face's half edges, we can get its' neighbor, and therefore the face that neighbor belongs to. + * + */ var halfEdgeMap = new Map(); var vertexPositionGetter = generateVertexAttributeGetter( positions, positionBufferViewGltf.byteStride / 4 ); - for (let i = 0; i < triangleIndexAccessorGltf.count; i += 3) { - addIndexedTriangleToEdgeGraph( - halfEdgeMap, - i, - triangleIndices, - vertexPositionGetter - ); + // Populate our half edge map + if (indexedTriangleMode) { + for (var i = 0; i < triangleIndexAccessorGltf.count; i += 3) { + addTriangleToEdgeGraph( + halfEdgeMap, + undefined, + i, + triangleIndices, + vertexPositionGetter + ); + } + } else { + for (var j = 0; j < positionAccessorGltf.count; j += 3) { + addTriangleToEdgeGraph( + halfEdgeMap, + j, + undefined, + undefined, + vertexPositionGetter + ); + } } - var normalBufferView = loadResources.getBuffer(normalBufferViewGltf); - var normals = new Float32Array( - normalBufferView.buffer, - normalBufferView.byteOffset + normalAccessorGltf.byteOffset, - normalAccessorGltf.count * (normalBufferViewGltf.byteStride / 4) - ); - var vertexNormalGetter = generateVertexAttributeGetter( - normals, - normalBufferViewGltf.byteStride / 4 - ); var minimumAngle = Math.PI / 20; if ( @@ -114,6 +169,11 @@ function outlinePrimitive(model, meshId, primitiveId) { minimumAngle ); + if (outlineIndexBuffer.length === 0) { + //No edges to outline + return false; + } + // Add new buffer to gltf var bufferId = gltf.buffers.push({ @@ -140,7 +200,10 @@ function outlinePrimitive(model, meshId, primitiveId) { accessors.push({ bufferView: bufferViewId, byteOffset: 0, - componentType: WebGLConstants.UNSIGNED_INT, + componentType: + outlineIndexBuffer instanceof Uint16Array + ? WebGLConstants.UNSIGNED_SHORT + : WebGLConstants.UNSIGNED_INT, count: outlineIndexBuffer.length, // start and end for each line }) - 1; @@ -150,6 +213,8 @@ function outlinePrimitive(model, meshId, primitiveId) { }, }; gltf.extensionsUsed.push("CESIUM_primitive_outline"); + + return true; } function generateVertexAttributeGetter(vertexArray, elementsPerVertex) { @@ -162,59 +227,49 @@ function generateVertexAttributeGetter(vertexArray, elementsPerVertex) { }; } -function addIndexedTriangleToEdgeGraph( +function addTriangleToEdgeGraph( halfEdgeMap, - triangleStartIndex, + firstVertexIndex, + triangleStartIndex, // in indexedTriangle mode, this is an index into the index buffer. otherwise it's an index to the vertex positions triangleIndices, vertexPositionGetter ) { - var vertexIndexA = triangleIndices[triangleStartIndex]; - var vertexIndexB = triangleIndices[triangleStartIndex + 1]; - var vertexIndexC = triangleIndices[triangleStartIndex + 2]; - var first = addHalfEdge( - halfEdgeMap, - vertexPositionGetter, - vertexIndexA, - vertexIndexB, - triangleStartIndex - ); - var second = addHalfEdge( - halfEdgeMap, - vertexPositionGetter, - vertexIndexB, - vertexIndexC, - triangleStartIndex - ); - var last = addHalfEdge( - halfEdgeMap, - vertexPositionGetter, - vertexIndexC, - vertexIndexA, - triangleStartIndex - ); + var vertexIndexA, vertexIndexB, vertexIndexC; + // Each vertex in the triangle + if (defined(triangleStartIndex) && defined(triangleIndices)) { + vertexIndexA = triangleIndices[triangleStartIndex]; + vertexIndexB = triangleIndices[triangleStartIndex + 1]; + vertexIndexC = triangleIndices[triangleStartIndex + 2]; + } else if (defined(firstVertexIndex)) { + vertexIndexA = firstVertexIndex; + vertexIndexB = firstVertexIndex + 1; + vertexIndexC = firstVertexIndex + 2; + } else { + throw new DeveloperError( + "Either firstVertexIndex, or triangleStartIndex and triangleIndices, must be provided." + ); + } - // and the other direction... - var first2 = addHalfEdge( - halfEdgeMap, - vertexPositionGetter, - vertexIndexC, - vertexIndexB, - triangleStartIndex - ); - var second2 = addHalfEdge( - halfEdgeMap, - vertexPositionGetter, - vertexIndexB, - vertexIndexA, - triangleStartIndex - ); - var last2 = addHalfEdge( - halfEdgeMap, - vertexPositionGetter, - vertexIndexA, - vertexIndexC, - triangleStartIndex - ); + // Each half edge in the triangle (one in each direction) + var edgePairs = [ + [vertexIndexA, vertexIndexB], + [vertexIndexB, vertexIndexC], + [vertexIndexC, vertexIndexA], + [vertexIndexC, vertexIndexB], + [vertexIndexB, vertexIndexA], + [vertexIndexA, vertexIndexC], + ]; + + for (var i = 0; i < edgePairs.length; i++) { + var pair = edgePairs[i]; + addHalfEdge( + halfEdgeMap, + vertexPositionGetter, + pair[0], + pair[1], + triangleStartIndex + ); + } } function addHalfEdge( @@ -265,32 +320,21 @@ function generateMapKey(sourceVertex, destinationVertex) { function getNeighboringEdge(halfEdgeMap, edge) { var neighborIdx = generateMapKey(edge.destinationVertex, edge.sourceVertex); - var neighbor = halfEdgeMap.get(neighborIdx); - var tolerance = Number.EPSILON; - if ( - neighbor && - (Math.abs(neighbor.destinationVertex[0] - edge.sourceVertex[0]) > - tolerance || - Math.abs(neighbor.destinationVertex[1] - edge.sourceVertex[1]) > - tolerance || - Math.abs(neighbor.destinationVertex[2] - edge.sourceVertex[2]) > - tolerance) - ) { - return undefined; - } - return neighbor; + return halfEdgeMap.get(neighborIdx); } // Returns index of first vertex of triangle function getFirstVertexOfFaces(halfEdge, triangleIndices) { var faces = []; if (halfEdge.triangleStartIndex) { - for (var index of halfEdge.triangleStartIndex) { - faces.push(triangleIndices[index]); + // Indexed triangle mode + for (var i of halfEdge.triangleStartIndex) { + faces.push(triangleIndices[i]); } } else { - for (var index of halfEdge.originalIdx) { - var triangleStart = index - (index % 3); + for (var j of halfEdge.originalIdx) { + // Unindexed triangle mode + var triangleStart = j - (j % 3); faces.push(triangleStart); } } @@ -306,8 +350,9 @@ function findEdgesToOutline( var outlineThese = []; var checked = new Set(); var allEdges = Array.from(halfEdgeMap.values()); - for (var i = 0; i < allEdges.length; i++) { - var edge = allEdges[i]; + var maxIndex = 0; + for (var edgeIdx = 0; edgeIdx < allEdges.length; edgeIdx++) { + var edge = allEdges[edgeIdx]; if ( checked.has(generateMapKey(edge.sourceVertex, edge.destinationVertex)) || checked.has(generateMapKey(edge.destinationVertex, edge.sourceVertex)) @@ -325,25 +370,23 @@ function findEdgesToOutline( if (neighbor.originalIdx.length > numIndicesToCheck) { neighbor.originalIdx = neighbor.originalIdx.slice(0, numIndicesToCheck); } - // FIXME - // there is something wrong with your face logic - // why do you need the first vertex of every face? - // why not just use the ones attached to the edge? + + // Get all the faces that share this edge var primaryEdgeFaces = getFirstVertexOfFaces(edge, triangleIndices); - var neighbourEdgeFaces = getFirstVertexOfFaces(neighbor, triangleIndices); - var highlight = false; - var highlightStartVertex; - var highlightEndVertex; + var neighborEdgeFaces = getFirstVertexOfFaces(neighbor, triangleIndices); + var outline = false; + var startVertex; + var endVertex; for (var i = 0; i < primaryEdgeFaces.length; i++) { - if (highlight) { + if (outline) { break; } var faceNormal = vertexNormalGetter(primaryEdgeFaces[i]); - for (var j = 0; j < neighbourEdgeFaces.length; j++) { - if (primaryEdgeFaces[i] === neighbourEdgeFaces[j]) { + for (var j = 0; j < neighborEdgeFaces.length; j++) { + if (primaryEdgeFaces[i] === neighborEdgeFaces[j]) { continue; } - var neighborNormal = vertexNormalGetter(neighbourEdgeFaces[j]); + var neighborNormal = vertexNormalGetter(neighborEdgeFaces[j]); if (!defined(faceNormal) || !defined(neighborNormal)) { continue; } @@ -360,46 +403,32 @@ function findEdgesToOutline( ); continue; } - if (angleBetween > minimumAngle && angleBetween < Math.PI - 0.01) { - highlight = true; - // TODO: make this work for unindexed triangles - - // highlightStartVertex = edge.originalIdx[0]; - // let allVerticesInTriangle = [ - // triangleIndices[edge.triangleStartIndex[i]], - // triangleIndices[edge.triangleStartIndex[i] + 1], - // triangleIndices[edge.triangleStartIndex[i] + 2], - // ]; - // let orderInTriangle = allVerticesInTriangle.indexOf( - // highlightStartVertex - // ); - // let destOrderInTriangle = (orderInTriangle + 1) % 3; - // highlightEndVertex = allVerticesInTriangle[destOrderInTriangle]; - - highlightStartVertex = edge.originalIdx[0]; - highlightEndVertex = edge.destinationIdx[0]; - outlineThese.push(highlightStartVertex); - outlineThese.push(highlightEndVertex); - highlight = true; + if (angleBetween > minimumAngle && angleBetween < Math.PI) { + // Outline this edge + startVertex = edge.originalIdx[0]; + endVertex = edge.destinationIdx[0]; + outlineThese.push(startVertex); + outlineThese.push(endVertex); + maxIndex = Math.max(maxIndex, startVertex, endVertex); + outline = true; break; - // highlightStartVertex = neighbor.originalIdx[0]; - // highlightEndVertex = neighbor.destinationIdx[0]; - // outlineThese.push(highlightStartVertex); - // outlineThese.push(highlightEndVertex); + // We don't need to check any other faces that share this edge, + // we already know we need to outline it } } } - // if (highlight) { - // outlineThese.push(highlightStartVertex); - // outlineThese.push(highlightEndVertex); - // } + checked.add(generateMapKey(edge.sourceVertex, edge.destinationVertex)); checked.add( generateMapKey(neighbor.sourceVertex, neighbor.destinationVertex) ); } - // TODO: check how big the indices are, and if they can fit into a Uint16, use one - return new Uint32Array(outlineThese); + + if (maxIndex > MAX_GLTF_UINT16_INDEX) { + // The largest index won't fit in a Uint16, so use a Uint32 + return new Uint32Array(outlineThese); + } + return new Uint16Array(outlineThese); } export default ModelOutlineGenerator; diff --git a/Source/Scene/ModelOutlineLoader.js b/Source/Scene/ModelOutlineLoader.js index bf7061b7368a..28b3378fdf40 100644 --- a/Source/Scene/ModelOutlineLoader.js +++ b/Source/Scene/ModelOutlineLoader.js @@ -9,6 +9,7 @@ import TextureWrap from "../Renderer/TextureWrap.js"; import ForEach from "../ThirdParty/GltfPipeline/ForEach.js"; import ModelOutlineGenerator from "./ModelOutlineGenerator.js"; import ModelOutlineGenerationMode from "./ModelOutlineGenerationMode.js"; +import WebGLConstants from "../Core/WebGLConstants.js"; // glTF does not allow an index value of 65535 because this is the primitive // restart value in some APIs. @@ -36,6 +37,10 @@ ModelOutlineLoader.hasExtension = function (model) { * @private */ ModelOutlineLoader.shouldGenerateOutlines = function (model) { + if (model.outlineGenerationMode === ModelOutlineGenerationMode.OFF) { + return false; + } + if (model.outlineGenerationMode === ModelOutlineGenerationMode.ON) { return true; } @@ -83,7 +88,10 @@ ModelOutlineLoader.outlinePrimitives = function (model) { } if (ModelOutlineLoader.shouldGenerateOutlines(model)) { - ModelOutlineGenerator.generateOutlinesForModel(model); + if (!ModelOutlineGenerator.generateOutlinesForModel(model)) { + // This model doesn't need outlines loaded for it + return; + } } var gltf = model.gltf; @@ -222,33 +230,90 @@ function addOutline( } var triangleIndexAccessorGltf = accessors[primitive.indices]; - var triangleIndexBufferViewGltf = - bufferViews[triangleIndexAccessorGltf.bufferView]; + var needToCreateIndices = !defined(triangleIndexAccessorGltf); + + var triangleIndexBufferViewGltf = needToCreateIndices + ? {} + : bufferViews[triangleIndexAccessorGltf.bufferView]; + var edgeIndexAccessorGltf = accessors[edgeIndicesAccessorId]; var edgeIndexBufferViewGltf = bufferViews[edgeIndexAccessorGltf.bufferView]; var loadResources = model._loadResources; - var triangleIndexBufferView = loadResources.getBuffer( - triangleIndexBufferViewGltf - ); + + var triangleIndexBufferView; + if (!needToCreateIndices) { + // Generate an index buffer, and use that to create the accessor and bufferview + 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 triangleIndices; + if (needToCreateIndices) { + triangleIndices = + numVertices <= MAX_GLTF_UINT16_INDEX + ? new Uint16Array(numVertices) + : new Uint32Array(numVertices); + for (let i = 0; i < triangleIndices.length; ++i) { + triangleIndices[i] = i; + } + + // Update the model to include this new buffer + var bufferId = + gltf.buffers.push({ + byteLength: triangleIndices.byteLength, + extras: { + _pipeline: { + source: triangleIndices.buffer, + }, + }, + }) - 1; + + triangleIndexBufferViewGltf = { + buffer: bufferId, + byteLength: triangleIndices.byteLength, + byteOffset: 0, + target: WebGLConstants.ELEMENT_ARRAY_BUFFER, + }; + var bufferViewId = gltf.bufferViews.push(triangleIndexBufferViewGltf) - 1; + + triangleIndexAccessorGltf = { + bufferView: bufferViewId, + componentType: + numVertices <= MAX_GLTF_UINT16_INDEX + ? WebGLConstants.UNSIGNED_SHORT + : WebGLConstants.UNSIGNED_INT, + count: triangleIndices.length, + max: numVertices, + min: 0, + type: "SCALAR", + }; + gltf.accessors.push(triangleIndexAccessorGltf); + + loadResources.buffers[bufferId] = triangleIndices; + triangleIndexBufferView = triangleIndices; + } else { + triangleIndices = + triangleIndexAccessorGltf.componentType === WebGLConstants.UNSIGNED_SHORT + ? 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 + edgeIndexAccessorGltf.componentType === WebGLConstants.UNSIGNED_SHORT ? new Uint16Array( edgeIndexBufferView.buffer, edgeIndexBufferView.byteOffset + edgeIndexAccessorGltf.byteOffset, @@ -328,7 +393,7 @@ function addOutline( ) { // We outgrew a 16-bit index buffer, switch to 32-bit. triangleIndices = new Uint32Array(triangleIndices); - triangleIndexAccessorGltf.componentType = 5125; // UNSIGNED_INT + triangleIndexAccessorGltf.componentType = WebGLConstants.UNSIGNED_INT; triangleIndexBufferViewGltf.buffer = gltf.buffers.push({ byteLength: triangleIndices.byteLength, From 4c40b2b8d138580a8c4a9de93c1b099aa456a9de Mon Sep 17 00:00:00 2001 From: Emma Krantz Date: Fri, 28 Aug 2020 13:55:38 +0800 Subject: [PATCH 08/17] better comments --- Source/Scene/ModelOutlineGenerator.js | 35 +++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/Source/Scene/ModelOutlineGenerator.js b/Source/Scene/ModelOutlineGenerator.js index 664824d252ef..ff27be12fb6b 100644 --- a/Source/Scene/ModelOutlineGenerator.js +++ b/Source/Scene/ModelOutlineGenerator.js @@ -9,7 +9,25 @@ import { DeveloperError } from "../Core/DeveloperError.js"; var MAX_GLTF_UINT16_INDEX = 65534; function ModelOutlineGenerator() {} +/** + * Determines which edges in a model should be outlined. + * It does this by adding the index buffer expected by CESIUM_primitive_outline + * extension that determines which edges to outline. + * + * Note that this is reasonably performance expensive, and not recommended for + * use on large meshes. + * @returns true if there are edges to outline, false otherwise. + * @private + */ ModelOutlineGenerator.generateOutlinesForModel = function (model) { + if ( + defined(model.extensionsRequired.KHR_draco_mesh_compression) || + defined(model.extensionsUsed.KHR_draco_mesh_compression) + ) { + // Draco compressed meshes are not supported + return false; + } + var gltf = model.gltf; var outlineAny = false; ForEach.mesh(gltf, function (mesh, meshId) { @@ -217,6 +235,11 @@ function outlinePrimitive(model, meshId, primitiveId) { return true; } +/** + * Generates a function for getting the attributes of a vertex with a particular + * index from a glTF vertex array + * @private + */ function generateVertexAttributeGetter(vertexArray, elementsPerVertex) { return function (index) { return [ @@ -227,6 +250,14 @@ function generateVertexAttributeGetter(vertexArray, elementsPerVertex) { }; } +/** + * Adds a single triangle to the directed half edge map. + * @param {*} halfEdgeMap + * @param {*} firstVertexIndex + * @param {*} triangleStartIndex + * @param {*} triangleIndices + * @param {*} vertexPositionGetter + */ function addTriangleToEdgeGraph( halfEdgeMap, firstVertexIndex, @@ -341,6 +372,10 @@ function getFirstVertexOfFaces(halfEdge, triangleIndices) { return faces; } +/** + * From a directed half edge map, determines which edges should be outlined. + * @private + */ function findEdgesToOutline( halfEdgeMap, vertexNormalGetter, From a44a0a9a6b4f435d6913aa09f74277346aef0ea1 Mon Sep 17 00:00:00 2001 From: Emma Krantz Date: Fri, 28 Aug 2020 16:46:01 +0800 Subject: [PATCH 09/17] configurable outline angle --- Source/Scene/Batched3DModel3DTileContent.js | 4 ++- Source/Scene/Cesium3DTileset.js | 33 ++++++++++++++++++++- Source/Scene/Model.js | 28 ++++++++++++++++- Source/Scene/ModelOutlineGenerator.js | 8 +++-- 4 files changed, 68 insertions(+), 5 deletions(-) diff --git a/Source/Scene/Batched3DModel3DTileContent.js b/Source/Scene/Batched3DModel3DTileContent.js index 7d3b736e1747..ad716c085a52 100644 --- a/Source/Scene/Batched3DModel3DTileContent.js +++ b/Source/Scene/Batched3DModel3DTileContent.js @@ -433,7 +433,8 @@ function initialize(content, arrayBuffer, byteOffset) { sphericalHarmonicCoefficients: tileset.sphericalHarmonicCoefficients, specularEnvironmentMaps: tileset.specularEnvironmentMaps, backFaceCulling: tileset.backFaceCulling, - outlineGenerationMode: tileset.outlineGenerationMode + outlineGenerationMode: tileset.outlineGenerationMode, + outlineGenerationMinimumAngle: tileset.outlineGenerationMinimumAngle }); content._model.readyPromise.then(function (model) { model.activeAnimations.addAll({ @@ -543,6 +544,7 @@ Batched3DModel3DTileContent.prototype.update = function (tileset, frameState) { this._model.specularEnvironmentMaps = this._tileset.specularEnvironmentMaps; this._model.backFaceCulling = this._tileset.backFaceCulling; this._model.outlineGenerationMode = this._tileset.outlineGenerationMode; + this._model.outlineGenerationMinimumAngle = this._tileset.outlineGenerationMinimumAngle; this._model.debugWireframe = this._tileset.debugWireframe; // Update clipping planes diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index 27cfbc9338af..8b88b6f25ca3 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -91,7 +91,8 @@ import ModelOutlineGenerationMode from "./ModelOutlineGenerationMode.js"; * @param {Cartesian3[]} [options.sphericalHarmonicCoefficients] The third order spherical harmonic coefficients used for the diffuse color of image-based lighting. * @param {String} [options.specularEnvironmentMaps] A URL to a KTX file that contains a cube map of the specular lighting and the convoluted specular mipmaps. * @param {Boolean} [options.backFaceCulling=true] Whether to cull back-facing geometry. When true, back face culling is determined by the glTF material's doubleSided property; when false, back face culling is disabled. - * @param {ModelOutlineGenerationMode} [options.outlineGenerationMode=ModelOutlineGenerationMode.USE_GLTF_SETTINGS] + * @param {ModelOutlineGenerationMode} [options.outlineGenerationMode] Determines whether outlines should be generated for this model. + * @param {Number} [options.outlineGenerationMinimumAngle] If generating outlines for this model, determines what the minimum angle between the normals of two faces has to be for the edge between them to receive an outline. * @param {String} [options.debugHeatmapTilePropertyName] The tile variable to colorize as a heatmap. All rendered tiles will be colorized relative to each other's specified variable value. * @param {Boolean} [options.debugFreezeFrame=false] For debugging only. Determines if only the tiles from last frame should be used for rendering. * @param {Boolean} [options.debugColorizeTiles=false] For debugging only. When true, assigns a random color to each tile. @@ -762,11 +763,41 @@ function Cesium3DTileset(options) { */ this.backFaceCulling = defaultValue(options.backFaceCulling, true); + /** + * Determines whether outlines should be generated for this tileset. + * + * @type {ModelOutlineGenerationMode} + * + * @default ModelOutlineGenerationMode.USE_GLTF_SETTINGS + * + * @see ModelOutlineGenerator + */ this.outlineGenerationMode = defaultValue( options.outlineGenerationMode, ModelOutlineGenerationMode.USE_GLTF_SETTINGS ); + /** + * If generating outlines for this model, determines what the minimum angle + * between the normals of two faces has to be for the edge between them to + * receive an outline. + * + * This follows @see Cesium3DTileset.outlineGenerationMode — if outlineGenerationMode is + * OFF or USE_GLTF_SETTINGS, this value will be ignored. If undefined, it will + * use the value from the glTF if it exists, or otherwise the default value + * specified by ModelOutlineGenerator. + * + * @type {number} + * + * @default undefined + * + * @see ModelOutlineGenerator + */ + this.outlineGenerationMinimumAngle = defaultValue( + options.outlineGenerationMinimumAngle, + undefined + ); + /** * This property is for debugging only; it is not optimized for production use. *

diff --git a/Source/Scene/Model.js b/Source/Scene/Model.js index 07ca2f74fa88..6c65b93b4211 100644 --- a/Source/Scene/Model.js +++ b/Source/Scene/Model.js @@ -210,6 +210,8 @@ var uriToGuid = {}; * @param {Number} [options.colorBlendAmount=0.5] Value used to determine the color strength when the colorBlendMode is MIX. A value of 0.0 results in the model's rendered color while a value of 1.0 results in a solid color, with any value in-between resulting in a mix of the two. * @param {Color} [options.silhouetteColor=Color.RED] The silhouette color. If more than 256 models have silhouettes enabled, there is a small chance that overlapping models will have minor artifacts. * @param {Number} [options.silhouetteSize=0.0] The size of the silhouette in pixels. + * @param {ModelOutlineGenerationMode} [options.outlineGenerationMode] Determines whether outlines should be generated for this model. + * @param {Number} [options.outlineGenerationMinimumAngle] If generating outlines for this model, determines what the minimum angle between the normals of two faces has to be for the edge between them to receive an outline. * @param {ClippingPlaneCollection} [options.clippingPlanes] The {@link ClippingPlaneCollection} used to selectively disable rendering the model. * @param {Boolean} [options.dequantizeInShader=true] Determines if a {@link https://github.com/google/draco|Draco} encoded model is dequantized on the GPU. This decreases total memory usage for encoded models. * @param {Cartesian2} [options.imageBasedLightingFactor=Cartesian2(1.0, 1.0)] Scales diffuse and specular image-based lighting from the earth, sky, atmosphere and star skybox. @@ -327,7 +329,31 @@ function Model(options) { * * @see ModelOutlineGenerator */ - this.outlineGenerationMode = defaultValue(options.outlineGenerationMode, ModelOutlineGenerationMode.USE_GLTF_SETTINGS); + this.outlineGenerationMode = defaultValue( + options.outlineGenerationMode, + ModelOutlineGenerationMode.USE_GLTF_SETTINGS + ); + + /** + * If generating outlines for this model, determines what the minimum angle + * between the normals of two faces has to be for the edge between them to + * receive an outline. + * + * This follows @see Model.outlineGenerationMode — if outlineGenerationMode is + * OFF or USE_GLTF_SETTINGS, this value will be ignored. If undefined, it will + * use the value from the glTF if it exists, or otherwise the default value + * specified by ModelOutlineGenerator. + * + * @type {number} + * + * @default undefined + * + * @see ModelOutlineGenerator + */ + this.outlineGenerationMinimumAngle = defaultValue( + options.outlineGenerationMinimumAngle, + undefined + ); /** * The 4x4 transformation matrix that transforms the model from model to world coordinates. diff --git a/Source/Scene/ModelOutlineGenerator.js b/Source/Scene/ModelOutlineGenerator.js index ff27be12fb6b..bddbf9b631e0 100644 --- a/Source/Scene/ModelOutlineGenerator.js +++ b/Source/Scene/ModelOutlineGenerator.js @@ -3,6 +3,7 @@ import WebGLConstants from "../Core/WebGLConstants.js"; import defined from "../Core/defined.js"; import Cartesian3 from "../Core/Cartesian3.js"; import { DeveloperError } from "../Core/DeveloperError.js"; +import ModelOutlineGenerationMode from "../Scene/ModelOutlineGenerationMode.js"; // glTF does not allow an index value of 65535 because this is the primitive // restart value in some APIs. @@ -165,7 +166,9 @@ function outlinePrimitive(model, meshId, primitiveId) { } } - var minimumAngle = Math.PI / 20; + var minimumAngle = defined(model.outlineGenerationMinimumAngle) + ? model.outlineGenerationMinimumAngle + : Math.PI / 20; if ( defined(mesh.primitives[primitiveId].extensions) && @@ -173,7 +176,8 @@ function outlinePrimitive(model, meshId, primitiveId) { defined( mesh.primitives[primitiveId].extensions.CESIUM_primitive_outline .outlineWhenAngleBetweenFaceNormalsExceeds - ) + ) && + model.outlineGenerationMode === ModelOutlineGenerationMode.USE_GLTF_SETTINGS ) { minimumAngle = mesh.primitives[primitiveId].extensions.CESIUM_primitive_outline From b0d507b7687c9d01d1a42075550ef5064a00cd55 Mon Sep 17 00:00:00 2001 From: Emma Krantz Date: Fri, 4 Sep 2020 09:58:15 +0800 Subject: [PATCH 10/17] feedback from PR --- Source/Scene/ModelOutlineGenerator.js | 11 ++++++++--- Source/Scene/ModelOutlineLoader.js | 5 +++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/Source/Scene/ModelOutlineGenerator.js b/Source/Scene/ModelOutlineGenerator.js index bddbf9b631e0..6e2dce64772c 100644 --- a/Source/Scene/ModelOutlineGenerator.js +++ b/Source/Scene/ModelOutlineGenerator.js @@ -140,7 +140,7 @@ function outlinePrimitive(model, meshId, primitiveId) { var halfEdgeMap = new Map(); var vertexPositionGetter = generateVertexAttributeGetter( positions, - positionBufferViewGltf.byteStride / 4 + positionBufferViewGltf.byteStride / Float32Array.BYTES_PER_ELEMENT ); // Populate our half edge map @@ -265,7 +265,9 @@ function generateVertexAttributeGetter(vertexArray, elementsPerVertex) { function addTriangleToEdgeGraph( halfEdgeMap, firstVertexIndex, - triangleStartIndex, // in indexedTriangle mode, this is an index into the index buffer. otherwise it's an index to the vertex positions + triangleStartIndex, // in indexedTriangle mode, this is an index into the + // index buffer. otherwise it's an index to the vertex + // positions triangleIndices, vertexPositionGetter ) { @@ -285,7 +287,10 @@ function addTriangleToEdgeGraph( ); } - // Each half edge in the triangle (one in each direction) + // For topologically "well behaved" meshes, adding half edges in both + // directions isn't necessary-- every half-edge's twin exists in another face. + // But in non-2-manifold meshes (where more than 2 faces share an edge), or + // meshes with unconnected faces, this assumption doesn't hold. var edgePairs = [ [vertexIndexA, vertexIndexB], [vertexIndexB, vertexIndexC], diff --git a/Source/Scene/ModelOutlineLoader.js b/Source/Scene/ModelOutlineLoader.js index 28b3378fdf40..c0f34c19b644 100644 --- a/Source/Scene/ModelOutlineLoader.js +++ b/Source/Scene/ModelOutlineLoader.js @@ -48,6 +48,10 @@ ModelOutlineLoader.shouldGenerateOutlines = function (model) { var outlineGenerationRequestedInGltf = false; if (ModelOutlineLoader.hasExtension(model)) { ForEach.mesh(model.gltf, function (mesh, _meshId) { + if (outlineGenerationRequestedInGltf) { + return true; // break + } + ForEach.meshPrimitive(mesh, function (primitive, _primitiveId) { if ( defined(primitive.extensions) && @@ -58,6 +62,7 @@ ModelOutlineLoader.shouldGenerateOutlines = function (model) { ) ) { outlineGenerationRequestedInGltf = true; + return true; // break } }); }); From 164b68bf8e4edb83ef5afe2448b07dbacbd89265 Mon Sep 17 00:00:00 2001 From: Emma Krantz Date: Fri, 4 Sep 2020 10:03:54 +0800 Subject: [PATCH 11/17] added comment about duplicate vertices --- Source/Scene/ModelOutlineGenerator.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Source/Scene/ModelOutlineGenerator.js b/Source/Scene/ModelOutlineGenerator.js index 6e2dce64772c..4b0687f40a47 100644 --- a/Source/Scene/ModelOutlineGenerator.js +++ b/Source/Scene/ModelOutlineGenerator.js @@ -407,6 +407,10 @@ function findEdgesToOutline( if (!defined(neighbor)) { continue; } + // For performance reasons we want to cap the number of vertices we check + // for each edge. Some meshes can have a lot of duplicate vertices in the + // same spot, so we don't want to outline those many, many times. + // This number is arbitrary. var numIndicesToCheck = 21; if (edge.originalIdx.length > numIndicesToCheck) { edge.originalIdx = edge.originalIdx.slice(0, numIndicesToCheck); From e43bdfdf4d6a71ac0f746c80df220eb2d18db2ba Mon Sep 17 00:00:00 2001 From: Emma Krantz Date: Wed, 4 Nov 2020 17:02:57 +0800 Subject: [PATCH 12/17] bumped version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 086d03415807..e07618ce94c1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "terriajs-cesium", - "version": "1.73.1", + "version": "1.73.1-outlines", "description": "Cesium for TerriaJS.", "homepage": "http://cesium.com/cesiumjs/", "license": "Apache-2.0", From 2f1cd2aa75872486efcbc0b7e241dbea2e614b9c Mon Sep 17 00:00:00 2001 From: Emma Krantz Date: Wed, 4 Nov 2020 17:20:51 +0800 Subject: [PATCH 13/17] removed DeveloperError from Source/Scene/ModelOutlineGenerator.js because it was causing minify errors --- Source/Scene/ModelOutlineGenerator.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Source/Scene/ModelOutlineGenerator.js b/Source/Scene/ModelOutlineGenerator.js index 4b0687f40a47..d097db1d6aad 100644 --- a/Source/Scene/ModelOutlineGenerator.js +++ b/Source/Scene/ModelOutlineGenerator.js @@ -2,7 +2,6 @@ import ForEach from "../ThirdParty/GltfPipeline/ForEach.js"; import WebGLConstants from "../Core/WebGLConstants.js"; import defined from "../Core/defined.js"; import Cartesian3 from "../Core/Cartesian3.js"; -import { DeveloperError } from "../Core/DeveloperError.js"; import ModelOutlineGenerationMode from "../Scene/ModelOutlineGenerationMode.js"; // glTF does not allow an index value of 65535 because this is the primitive @@ -282,9 +281,9 @@ function addTriangleToEdgeGraph( vertexIndexB = firstVertexIndex + 1; vertexIndexC = firstVertexIndex + 2; } else { - throw new DeveloperError( - "Either firstVertexIndex, or triangleStartIndex and triangleIndices, must be provided." - ); + // throw new DeveloperError( + // "Either firstVertexIndex, or triangleStartIndex and triangleIndices, must be provided." + // ); } // For topologically "well behaved" meshes, adding half edges in both @@ -408,7 +407,7 @@ function findEdgesToOutline( continue; } // For performance reasons we want to cap the number of vertices we check - // for each edge. Some meshes can have a lot of duplicate vertices in the + // for each edge. Some meshes can have a lot of duplicate vertices in the // same spot, so we don't want to outline those many, many times. // This number is arbitrary. var numIndicesToCheck = 21; From cd3bd40e8cdcc31f82bc38dc392a004df5219107 Mon Sep 17 00:00:00 2001 From: Emma Krantz Date: Wed, 4 Nov 2020 17:24:01 +0800 Subject: [PATCH 14/17] removed stray for of loops --- Source/Scene/ModelOutlineGenerator.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Scene/ModelOutlineGenerator.js b/Source/Scene/ModelOutlineGenerator.js index d097db1d6aad..c9ed55bcdbd8 100644 --- a/Source/Scene/ModelOutlineGenerator.js +++ b/Source/Scene/ModelOutlineGenerator.js @@ -367,11 +367,11 @@ function getFirstVertexOfFaces(halfEdge, triangleIndices) { var faces = []; if (halfEdge.triangleStartIndex) { // Indexed triangle mode - for (var i of halfEdge.triangleStartIndex) { + for (var i = 0; i < halfEdge.triangleStartIndex.length; i++) { faces.push(triangleIndices[i]); } } else { - for (var j of halfEdge.originalIdx) { + for (var j = 0; j < halfEdge.originalIdx.length; j++) { // Unindexed triangle mode var triangleStart = j - (j % 3); faces.push(triangleStart); From d46ee137858d5d5639bbce4ead94862efa1bf42b Mon Sep 17 00:00:00 2001 From: Emma Krantz Date: Wed, 4 Nov 2020 17:33:34 +0800 Subject: [PATCH 15/17] remove stray let --- Source/Scene/ModelOutlineLoader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Scene/ModelOutlineLoader.js b/Source/Scene/ModelOutlineLoader.js index c0f34c19b644..219664690091 100644 --- a/Source/Scene/ModelOutlineLoader.js +++ b/Source/Scene/ModelOutlineLoader.js @@ -262,7 +262,7 @@ function addOutline( numVertices <= MAX_GLTF_UINT16_INDEX ? new Uint16Array(numVertices) : new Uint32Array(numVertices); - for (let i = 0; i < triangleIndices.length; ++i) { + for (var i = 0; i < triangleIndices.length; ++i) { triangleIndices[i] = i; } From c355a0fdfaf55f1b8ab9fc2bf2cd51ea39d1d25a Mon Sep 17 00:00:00 2001 From: Emma Krantz Date: Thu, 26 Nov 2020 11:35:25 +1100 Subject: [PATCH 16/17] fix type errors --- Source/Scene/ModelOutlineGenerator.js | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Scene/ModelOutlineGenerator.js b/Source/Scene/ModelOutlineGenerator.js index c9ed55bcdbd8..ff96233362b5 100644 --- a/Source/Scene/ModelOutlineGenerator.js +++ b/Source/Scene/ModelOutlineGenerator.js @@ -260,6 +260,7 @@ function generateVertexAttributeGetter(vertexArray, elementsPerVertex) { * @param {*} triangleStartIndex * @param {*} triangleIndices * @param {*} vertexPositionGetter + * @private */ function addTriangleToEdgeGraph( halfEdgeMap, From 98155be5facb92527f635caaf97dad046233550d Mon Sep 17 00:00:00 2001 From: Emma Krantz Date: Thu, 3 Dec 2020 12:30:54 +1100 Subject: [PATCH 17/17] added missing config options for outlines in modelgraphics and modelvisulaizer, fixed outline generation bugs --- Source/DataSources/ModelGraphics.js | 30 +++++++++++++++++++++++++++ Source/DataSources/ModelVisualizer.js | 16 ++++++++++++++ Source/Scene/Model.js | 4 ++-- Source/Scene/ModelOutlineGenerator.js | 29 +++++++++++++------------- package.json | 2 +- 5 files changed, 64 insertions(+), 17 deletions(-) diff --git a/Source/DataSources/ModelGraphics.js b/Source/DataSources/ModelGraphics.js index 1b0c2e07bdbd..2687602ad981 100644 --- a/Source/DataSources/ModelGraphics.js +++ b/Source/DataSources/ModelGraphics.js @@ -44,6 +44,8 @@ function createArticulationStagePropertyBag(value) { * @property {PropertyBag | Object.} [nodeTransformations] An object, where keys are names of nodes, and values are {@link TranslationRotationScale} Properties describing the transformation to apply to that node. The transformation is applied after the node's existing transformation as specified in the glTF, and does not replace the node's existing transformation. * @property {PropertyBag | Object.} [articulations] An object, where keys are composed of an articulation name, a single space, and a stage name, and the values are numeric properties. * @property {Property | ClippingPlaneCollection} [clippingPlanes] A property specifying the {@link ClippingPlaneCollection} used to selectively disable rendering the model. + * @property {Property | ModelOutlineGenerationMode} [options.outlineGenerationMode] A property that determines whether outlines should be generated for this model. + * @property {Property | Number} [options.outlineGenerationMinimumAngle] A property that if generating outlines for this model, determines what the minimum angle between the normals of two faces has to be for the edge between them to receive an outline. */ /** @@ -108,6 +110,8 @@ function ModelGraphics(options) { this._articulationsSubscription = undefined; this._clippingPlanes = undefined; this._clippingPlanesSubscription = undefined; + this._outlineGenerationMode = undefined; + this._outlineGenerationMinimumAngle = undefined; this.merge(defaultValue(options, defaultValue.EMPTY_OBJECT)); } @@ -312,6 +316,22 @@ Object.defineProperties(ModelGraphics.prototype, { */ clippingPlanes: createPropertyDescriptor("clippingPlanes"), + /** + * A property that determines whether outlines should be generated for this model. + * @memberof ModelGraphics.prototype + * @type {Property|undefined} + */ + outlineGenerationMode: createPropertyDescriptor("outlineGenerationMode"), + + /** + * A property that if generating outlines for this model, determines what the minimum angle between the normals of two faces has to be for the edge between them to receive an outline. + * @memberof ModelGraphics.prototype + * @type {Property|undefined} + */ + outlineGenerationMinimumAngle: createPropertyDescriptor( + "outlineGenerationMinimumAngle" + ), + /** * A property specifying the {@link Axis} up axis of the model. * @memberOf ModelGraphics.prototype @@ -357,6 +377,8 @@ ModelGraphics.prototype.clone = function (result) { result.nodeTransformations = this.nodeTransformations; result.articulations = this.articulations; result.clippingPlanes = this.clippingPlanes; + result.outlineGenerationMode = this.outlineGenerationMode; + result.outlineGenerationMinimumAngle = this.outlineGenerationMinimumAngle; return result; }; @@ -427,6 +449,14 @@ ModelGraphics.prototype.merge = function (source) { this.clippingPlanes, source.clippingPlanes ); + this.outlineGenerationMode = defaultValue( + this.outlineGenerationMode, + source.outlineGenerationMode + ); + this.outlineGenerationMinimumAngle = defaultValue( + this.outlineGenerationMinimumAngle, + source.outlineGenerationMinimumAngle + ); var sourceNodeTransformations = source.nodeTransformations; if (defined(sourceNodeTransformations)) { diff --git a/Source/DataSources/ModelVisualizer.js b/Source/DataSources/ModelVisualizer.js index 142e723d0d60..18327d63fd71 100644 --- a/Source/DataSources/ModelVisualizer.js +++ b/Source/DataSources/ModelVisualizer.js @@ -123,6 +123,14 @@ ModelVisualizer.prototype.update = function (time) { defaultIncrementallyLoadTextures ), scene: this._scene, + outlineGenerationMode: Property.getValueOrDefault( + modelGraphics._outlineGenerationMode, + time + ), + outlineGenerationMinimumAngle: Property.getValueOrDefault( + modelGraphics._outlineGenerationMinimumAngle, + time + ), }); model.id = entity; primitives.add(model); @@ -224,6 +232,14 @@ ModelVisualizer.prototype.update = function (time) { modelGraphics._forwardAxis, time ); + model._outlineGenerationMode = Property.getValueOrUndefined( + modelGraphics._outlineGenerationMode, + time + ); + model._outlineGenerationMinimumAngle = Property.getValueOrUndefined( + modelGraphics._outlineGenerationMinimumAngle, + time + ); if (model.ready) { var runAnimations = Property.getValueOrDefault( diff --git a/Source/Scene/Model.js b/Source/Scene/Model.js index 627b6eb4456a..be3b5897d317 100644 --- a/Source/Scene/Model.js +++ b/Source/Scene/Model.js @@ -210,8 +210,6 @@ var uriToGuid = {}; * @param {Number} [options.colorBlendAmount=0.5] Value used to determine the color strength when the colorBlendMode is MIX. A value of 0.0 results in the model's rendered color while a value of 1.0 results in a solid color, with any value in-between resulting in a mix of the two. * @param {Color} [options.silhouetteColor=Color.RED] The silhouette color. If more than 256 models have silhouettes enabled, there is a small chance that overlapping models will have minor artifacts. * @param {Number} [options.silhouetteSize=0.0] The size of the silhouette in pixels. - * @param {ModelOutlineGenerationMode} [options.outlineGenerationMode] Determines whether outlines should be generated for this model. - * @param {Number} [options.outlineGenerationMinimumAngle] If generating outlines for this model, determines what the minimum angle between the normals of two faces has to be for the edge between them to receive an outline. * @param {ClippingPlaneCollection} [options.clippingPlanes] The {@link ClippingPlaneCollection} used to selectively disable rendering the model. * @param {Boolean} [options.dequantizeInShader=true] Determines if a {@link https://github.com/google/draco|Draco} encoded model is dequantized on the GPU. This decreases total memory usage for encoded models. * @param {Cartesian2} [options.imageBasedLightingFactor=Cartesian2(1.0, 1.0)] Scales diffuse and specular image-based lighting from the earth, sky, atmosphere and star skybox. @@ -1423,6 +1421,8 @@ function containsGltfMagic(uint8Array) { * @param {Boolean} [options.dequantizeInShader=true] Determines if a {@link https://github.com/google/draco|Draco} encoded model is dequantized on the GPU. This decreases total memory usage for encoded models. * @param {Credit|String} [options.credit] A credit for the model, which is displayed on the canvas. * @param {Boolean} [options.backFaceCulling=true] Whether to cull back-facing geometry. When true, back face culling is determined by the material's doubleSided property; when false, back face culling is disabled. Back faces are not culled if {@link Model#color} is translucent or {@link Model#silhouetteSize} is greater than 0.0. + * @param {ModelOutlineGenerationMode} [options.outlineGenerationMode] Determines whether outlines should be generated for this model. + * @param {Number} [options.outlineGenerationMinimumAngle] If generating outlines for this model, determines what the minimum angle between the normals of two faces has to be for the edge between them to receive an outline. * * @returns {Model} The newly created model. * diff --git a/Source/Scene/ModelOutlineGenerator.js b/Source/Scene/ModelOutlineGenerator.js index ff96233362b5..dc0d400b5d18 100644 --- a/Source/Scene/ModelOutlineGenerator.js +++ b/Source/Scene/ModelOutlineGenerator.js @@ -81,14 +81,14 @@ function outlinePrimitive(model, meshId, primitiveId) { var positions = new Float32Array( positionBufferView.buffer, positionBufferView.byteOffset + positionAccessorGltf.byteOffset, - positionAccessorGltf.count * 3 //x, y, z + positionBufferViewGltf.length ); var normalBufferView = loadResources.getBuffer(normalBufferViewGltf); var normals = new Float32Array( normalBufferView.buffer, normalBufferView.byteOffset + normalAccessorGltf.byteOffset, - normalAccessorGltf.count * 3 //x, y, z + normalBufferViewGltf.length ); var vertexNormalGetter = generateVertexAttributeGetter( normals, @@ -245,6 +245,9 @@ function outlinePrimitive(model, meshId, primitiveId) { */ function generateVertexAttributeGetter(vertexArray, elementsPerVertex) { return function (index) { + if (elementsPerVertex * index > vertexArray.length) { + console.log("whoops"); + } return [ vertexArray[elementsPerVertex * index], vertexArray[elementsPerVertex * index + 1], @@ -266,8 +269,7 @@ function addTriangleToEdgeGraph( halfEdgeMap, firstVertexIndex, triangleStartIndex, // in indexedTriangle mode, this is an index into the - // index buffer. otherwise it's an index to the vertex - // positions + // index buffer. triangleIndices, vertexPositionGetter ) { @@ -307,7 +309,7 @@ function addTriangleToEdgeGraph( vertexPositionGetter, pair[0], pair[1], - triangleStartIndex + defined(triangleIndices) ? triangleIndices[triangleStartIndex] : undefined ); } } @@ -317,7 +319,7 @@ function addHalfEdge( vertexPositionGetter, sourceVertexIdx, destinationVertexIdx, - triangleIndex + triangleStartIndex ) { var halfEdge = { sourceVertex: vertexPositionGetter(sourceVertexIdx), @@ -325,8 +327,8 @@ function addHalfEdge( originalIdx: [sourceVertexIdx], destinationIdx: [destinationVertexIdx], }; - if (defined(triangleIndex)) { - halfEdge.triangleStartIndex = [triangleIndex]; + if (defined(triangleStartIndex)) { + halfEdge.triangleStartIndex = [triangleStartIndex]; } var mapIdx = generateMapKey( halfEdge.sourceVertex, @@ -336,8 +338,8 @@ function addHalfEdge( if (halfEdgeFromMap) { halfEdgeFromMap.originalIdx.push(sourceVertexIdx); halfEdgeFromMap.destinationIdx.push(destinationVertexIdx); - if (defined(triangleIndex)) { - halfEdgeFromMap.triangleStartIndex.push(triangleIndex); + if (defined(triangleStartIndex)) { + halfEdgeFromMap.triangleStartIndex.push(triangleStartIndex); } } else { halfEdgeMap.set(mapIdx, halfEdge); @@ -368,13 +370,12 @@ function getFirstVertexOfFaces(halfEdge, triangleIndices) { var faces = []; if (halfEdge.triangleStartIndex) { // Indexed triangle mode - for (var i = 0; i < halfEdge.triangleStartIndex.length; i++) { - faces.push(triangleIndices[i]); - } + faces.push(...halfEdge.triangleStartIndex); } else { for (var j = 0; j < halfEdge.originalIdx.length; j++) { // Unindexed triangle mode - var triangleStart = j - (j % 3); + var triangleStart = + halfEdge.originalIdx[j] - (halfEdge.originalIdx[j] % 3); faces.push(triangleStart); } } diff --git a/package.json b/package.json index e07618ce94c1..87c0e20e6e1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "terriajs-cesium", - "version": "1.73.1-outlines", + "version": "1.73.1-outlines-1", "description": "Cesium for TerriaJS.", "homepage": "http://cesium.com/cesiumjs/", "license": "Apache-2.0",