UNPKG

@babylonjs/core

Version:

Getting started? Play directly with the Babylon.js API using our [playground](https://playground.babylonjs.com/). It also contains a lot of samples to learn how to use it.

1,103 lines 61.3 kB
/** This file must only contain pure code and pure imports */ import { __decorate } from "../../../tslib.es6.js"; import { editableInPropertyPage } from "../../../Decorators/nodeDecorator.js"; import { Vector3 } from "../../../Maths/math.vector.pure.js"; import { VertexData, VertexDataMaterialInfo } from "../../mesh.vertexData.js"; import { NodeGeometryBlockConnectionPointTypes } from "../Enums/nodeGeometryConnectionPointTypes.js"; import { NodeGeometryBlock } from "../nodeGeometryBlock.js"; import { RegisterClass } from "../../../Misc/typeStore.js"; const PositionEpsilon = 1e-5; const OutputPositionEpsilon = 1e-4; const NormalEpsilon = 1e-8; const AngleEpsilon = 1e-7; const TriangleAreaEpsilon = PositionEpsilon * PositionEpsilon * PositionEpsilon * PositionEpsilon; function _Quantize(value) { const quantized = Math.round(value / PositionEpsilon); return quantized === 0 ? 0 : quantized; } function _PositionKey(x, y, z) { return `${_Quantize(x)}:${_Quantize(y)}:${_Quantize(z)}`; } function _VectorKey(position) { return _PositionKey(position.x, position.y, position.z); } function _OutputQuantize(value) { const quantized = Math.round(value / OutputPositionEpsilon); return quantized === 0 ? 0 : quantized; } function _OutputPositionKey(x, y, z) { return `${x}:${y}:${z}`; } function _EdgeKey(v0, v1) { return v0 < v1 ? `${v0}:${v1}` : `${v1}:${v0}`; } function _CloneVertexData(vertexData) { const clone = vertexData.clone(); if (!clone.normals && clone.positions && clone.indices) { const normals = []; VertexData.ComputeNormals(clone.positions, clone.indices, normals); clone.normals = normals; } return clone; } function _NormalizeNormalOrFallback(normal, fallback) { return normal.lengthSquared() > NormalEpsilon ? normal.normalizeToNew() : fallback.normalizeToNew(); } function _BuildAttributeDescriptors(vertexData, vertexCount) { const descriptors = []; const addDescriptor = (name, stride) => { const source = vertexData[name]; if (!source || source.length < vertexCount * stride) { return; } descriptors.push({ name, source, stride, offset: descriptors.reduce((sum, descriptor) => sum + descriptor.stride, 0), output: [], }); }; addDescriptor("tangents", 4); addDescriptor("uvs", 2); addDescriptor("uvs2", 2); addDescriptor("uvs3", 2); addDescriptor("uvs4", 2); addDescriptor("uvs5", 2); addDescriptor("uvs6", 2); if (vertexData.colors) { addDescriptor("colors", vertexData.colors.length === vertexData.positions.length ? 3 : 4); } addDescriptor("matricesIndices", 4); addDescriptor("matricesWeights", 4); addDescriptor("matricesIndicesExtra", 4); addDescriptor("matricesWeightsExtra", 4); return descriptors; } function _GetAttributeLength(descriptors) { return descriptors.reduce((sum, descriptor) => sum + descriptor.stride, 0); } function _GetVertexAttributes(descriptors, vertexIndex) { const attributes = []; for (const descriptor of descriptors) { const sourceOffset = vertexIndex * descriptor.stride; for (let index = 0; index < descriptor.stride; index++) { attributes.push(descriptor.source[sourceOffset + index]); } } return attributes; } function _InterpolateAttributes(start, end, amount) { if (!start.length) { return start; } return start.map((value, index) => value + (end[index] - value) * amount); } function _AverageAttributes(attributes, length) { if (!length || !attributes.length) { return []; } const result = new Array(length).fill(0); for (const attribute of attributes) { for (let index = 0; index < length; index++) { result[index] += attribute[index] ?? 0; } } for (let index = 0; index < length; index++) { result[index] /= attributes.length; } return result; } function _AttributesMatch(left, right) { if (left.length !== right.length) { return false; } for (let index = 0; index < left.length; index++) { if (Math.abs(left[index] - right[index]) > OutputPositionEpsilon) { return false; } } return true; } function _AssignAttributeOutputs(result, descriptors, vertexAttributes) { for (const descriptor of descriptors) { descriptor.output.length = 0; for (const attributes of vertexAttributes) { for (let index = 0; index < descriptor.stride; index++) { descriptor.output.push(attributes[descriptor.offset + index] ?? 0); } } switch (descriptor.name) { case "tangents": result.tangents = descriptor.output; break; case "uvs": result.uvs = descriptor.output; break; case "uvs2": result.uvs2 = descriptor.output; break; case "uvs3": result.uvs3 = descriptor.output; break; case "uvs4": result.uvs4 = descriptor.output; break; case "uvs5": result.uvs5 = descriptor.output; break; case "uvs6": result.uvs6 = descriptor.output; break; case "colors": result.colors = descriptor.output; break; case "matricesIndices": result.matricesIndices = descriptor.output; break; case "matricesWeights": result.matricesWeights = descriptor.output; break; case "matricesIndicesExtra": result.matricesIndicesExtra = descriptor.output; break; case "matricesWeightsExtra": result.matricesWeightsExtra = descriptor.output; break; } } } function _GetMaterialIndex(vertexData, indexStart) { if (!vertexData.materialInfos) { return 0; } for (const materialInfo of vertexData.materialInfos) { if (indexStart >= materialInfo.indexStart && indexStart < materialInfo.indexStart + materialInfo.indexCount) { return materialInfo.materialIndex; } } return 0; } function _BuildMaterialInfoResult(vertexData, indices, materialIndices, vertexCount) { if (!vertexData.materialInfos?.length) { return { indices, materialInfos: null }; } const materialOrder = vertexData.materialInfos.map((materialInfo) => materialInfo.materialIndex); const groups = new Map(); for (let triangleIndex = 0; triangleIndex < materialIndices.length; triangleIndex++) { const materialIndex = materialIndices[triangleIndex]; let group = groups.get(materialIndex); if (!group) { group = []; groups.set(materialIndex, group); if (!materialOrder.includes(materialIndex)) { materialOrder.push(materialIndex); } } const indexOffset = triangleIndex * 3; group.push(indices[indexOffset], indices[indexOffset + 1], indices[indexOffset + 2]); } const groupedIndices = []; const materialInfos = []; for (const materialIndex of materialOrder) { const group = groups.get(materialIndex); if (!group?.length) { continue; } const materialInfo = new VertexDataMaterialInfo(); materialInfo.materialIndex = materialIndex; materialInfo.indexStart = groupedIndices.length; materialInfo.indexCount = group.length; materialInfo.verticesStart = 0; materialInfo.verticesCount = vertexCount; groupedIndices.push(...group); materialInfos.push(materialInfo); } return { indices: groupedIndices, materialInfos }; } function _IsFlatFace(face) { return face.cornerNormals.every((normal) => Vector3.Dot(normal, face.normal) > 1 - PositionEpsilon); } function _IsBevelPolygonPoint(point) { return point.position !== undefined; } function _GetCapPointPosition(point) { return _IsBevelPolygonPoint(point) ? point.position : point; } function _GetCapPointNormal(point, fallback) { return _IsBevelPolygonPoint(point) ? point.normal : fallback; } function _GetCapPointAttributes(point) { return _IsBevelPolygonPoint(point) ? point.attributes : []; } function _GetCapPointMaterialIndex(point) { return _IsBevelPolygonPoint(point) ? point.materialIndex : 0; } function _BuildTopology(vertexData) { const positions = vertexData.positions; const normals = vertexData.normals; if (!positions || positions.length < 9) { return null; } const vertexCount = positions.length / 3; const indices = vertexData.indices && vertexData.indices.length ? Array.from(vertexData.indices) : Array.from({ length: vertexCount }, (_, index) => index); const weldedPositionMap = new Map(); const originalToWelded = []; const weldedPositions = []; for (let index = 0; index < vertexCount; index++) { const x = positions[index * 3]; const y = positions[index * 3 + 1]; const z = positions[index * 3 + 2]; const key = _PositionKey(x, y, z); let weldedIndex = weldedPositionMap.get(key); if (weldedIndex === undefined) { weldedIndex = weldedPositions.length; weldedPositionMap.set(key, weldedIndex); weldedPositions.push(new Vector3(x, y, z)); } originalToWelded[index] = weldedIndex; } const faces = []; const edges = new Map(); const vertexFaces = new Map(); const edge0 = new Vector3(); const edge1 = new Vector3(); const normal = new Vector3(); for (let index = 0; index < indices.length; index += 3) { let originalIndices = [indices[index], indices[index + 1], indices[index + 2]]; const i0 = originalToWelded[originalIndices[0]]; const i1 = originalToWelded[originalIndices[1]]; const i2 = originalToWelded[originalIndices[2]]; if (i0 === i1 || i1 === i2 || i2 === i0) { continue; } let faceIndices = [i0, i1, i2]; const p0 = weldedPositions[faceIndices[0]]; const p1 = weldedPositions[faceIndices[1]]; const p2 = weldedPositions[faceIndices[2]]; p1.subtractToRef(p0, edge0); p2.subtractToRef(p0, edge1); Vector3.CrossToRef(edge0, edge1, normal); if (normal.lengthSquared() < NormalEpsilon) { continue; } let cornerNormals = normals ? [ _NormalizeNormalOrFallback(Vector3.FromArray(normals, originalIndices[0] * 3), normal), _NormalizeNormalOrFallback(Vector3.FromArray(normals, originalIndices[1] * 3), normal), _NormalizeNormalOrFallback(Vector3.FromArray(normals, originalIndices[2] * 3), normal), ] : [normal.normalizeToNew(), normal.normalizeToNew(), normal.normalizeToNew()]; const averageCornerNormal = _NormalizeNormalOrFallback(cornerNormals[0].add(cornerNormals[1]).addInPlace(cornerNormals[2]), normal); if (normals && Vector3.Dot(normal, averageCornerNormal) < 0) { faceIndices = [i0, i2, i1]; originalIndices = [originalIndices[0], originalIndices[2], originalIndices[1]]; cornerNormals = [cornerNormals[0], cornerNormals[2], cornerNormals[1]]; normal.scaleInPlace(-1); } const faceNormal = normal.normalizeToNew(); if (!normals) { cornerNormals = [faceNormal.clone(), faceNormal.clone(), faceNormal.clone()]; } const faceIndex = faces.length; faces.push({ indices: faceIndices, originalIndices, normal: faceNormal, cornerNormals, materialIndex: _GetMaterialIndex(vertexData, index), }); for (const vertexIndex of faceIndices) { let faceList = vertexFaces.get(vertexIndex); if (!faceList) { faceList = []; vertexFaces.set(vertexIndex, faceList); } faceList.push(faceIndex); } for (let edgeIndex = 0; edgeIndex < 3; edgeIndex++) { const v0 = faceIndices[edgeIndex]; const v1 = faceIndices[(edgeIndex + 1) % 3]; const key = _EdgeKey(v0, v1); let edge = edges.get(key); if (!edge) { edge = { key, v0: Math.min(v0, v1), v1: Math.max(v0, v1), faces: [], }; edges.set(key, edge); } edge.faces.push({ faceIndex }); } } if (!faces.length) { return null; } return { positions: weldedPositions, faces, edges, vertexFaces, }; } function _ClonePolygonPoint(point) { return { position: point.position.clone(), normal: point.normal.clone(), attributes: point.attributes.slice(), materialIndex: point.materialIndex, }; } function _InterpolatePolygonPoint(start, end, amount) { return { position: Vector3.Lerp(start.position, end.position, amount), normal: _NormalizeNormalOrFallback(Vector3.Lerp(start.normal, end.normal, amount), start.normal), attributes: _InterpolateAttributes(start.attributes, end.attributes, amount), materialIndex: start.materialIndex, }; } function _InterpolateSegmentNormal(segment, t) { const denominator = segment.tMax - segment.tMin; if (denominator <= PositionEpsilon) { return segment.minNormal.clone(); } const amount = Math.min(1, Math.max(0, (t - segment.tMin) / denominator)); return _NormalizeNormalOrFallback(Vector3.Lerp(segment.minNormal, segment.maxNormal, amount), segment.minNormal); } function _InterpolateSegmentPoint(segment, t) { if (t <= segment.tMin + PositionEpsilon) { return segment.minPoint; } if (t >= segment.tMax - PositionEpsilon) { return segment.maxPoint; } return Vector3.Lerp(segment.minPoint, segment.maxPoint, (t - segment.tMin) / (segment.tMax - segment.tMin)); } function _InterpolateSegmentAttributes(segment, t) { if (t <= segment.tMin + PositionEpsilon) { return segment.minAttributes; } if (t >= segment.tMax - PositionEpsilon) { return segment.maxAttributes; } return _InterpolateAttributes(segment.minAttributes, segment.maxAttributes, (t - segment.tMin) / (segment.tMax - segment.tMin)); } function _ClipPolygonAgainstEdge(polygon, edgeStart, inward, amount) { if (!polygon.length) { return polygon; } const output = []; let previous = polygon[polygon.length - 1]; let previousDistance = Vector3.Dot(previous.position.subtract(edgeStart), inward) - amount; let previousInside = previousDistance >= -PositionEpsilon; for (const current of polygon) { const currentDistance = Vector3.Dot(current.position.subtract(edgeStart), inward) - amount; const currentInside = currentDistance >= -PositionEpsilon; if (currentInside !== previousInside) { const denominator = previousDistance - currentDistance; if (Math.abs(denominator) > NormalEpsilon) { const t = previousDistance / denominator; output.push(_InterpolatePolygonPoint(previous, current, t)); } } if (currentInside) { output.push(_ClonePolygonPoint(current)); } previous = current; previousDistance = currentDistance; previousInside = currentInside; } return output; } function _SlerpDirections(start, end, amount) { const dot = Math.min(1, Math.max(-1, Vector3.Dot(start, end))); if (dot > 1 - PositionEpsilon) { return Vector3.Lerp(start, end, amount).normalize(); } const theta = Math.acos(dot); const sinTheta = Math.sin(theta); if (Math.abs(sinTheta) < NormalEpsilon) { return Vector3.Lerp(start, end, amount).normalize(); } const startScale = Math.sin((1 - amount) * theta) / sinTheta; const endScale = Math.sin(amount * theta) / sinTheta; const result = start.scale(startScale).addInPlace(end.scale(endScale)); if (result.lengthSquared() < NormalEpsilon) { return Vector3.Lerp(start, end, amount).normalize(); } return result.normalize(); } function _AddUniquePoint(points, point, normal, attributes, materialIndex) { const key = _VectorKey(point); const normalizedNormal = _NormalizeNormalOrFallback(normal, normal); for (const existing of points) { if (_VectorKey(_GetCapPointPosition(existing)) === key) { if (_IsBevelPolygonPoint(existing)) { existing.normal.addInPlace(normalizedNormal).normalize(); existing.attributes = _AverageAttributes([existing.attributes, attributes], attributes.length); } return; } } points.push({ position: point.clone(), normal: normalizedNormal, attributes: attributes.slice(), materialIndex, }); } function _AddUniqueNormal(normals, normal) { for (const existing of normals) { if (Vector3.Dot(existing, normal) > 1 - PositionEpsilon) { return; } } normals.push(normal.clone()); } function _SolveThreePlaneIntersection(normals, distances) { const cross12 = Vector3.Cross(normals[1], normals[2]); const denominator = Vector3.Dot(normals[0], cross12); if (Math.abs(denominator) < NormalEpsilon) { return null; } const result = cross12.scale(distances[0]); result.addInPlace(Vector3.Cross(normals[2], normals[0]).scale(distances[1])); result.addInPlace(Vector3.Cross(normals[0], normals[1]).scale(distances[2])); result.scaleInPlace(1 / denominator); return result; } function _BuildCoplanarFaceClipEdges(topology, selectedEdges) { const result = new Map(); const visitedFaces = new Set(); for (let startFaceIndex = 0; startFaceIndex < topology.faces.length; startFaceIndex++) { if (visitedFaces.has(startFaceIndex)) { continue; } const group = []; const stack = [startFaceIndex]; const groupNormal = topology.faces[startFaceIndex].normal; visitedFaces.add(startFaceIndex); while (stack.length) { const faceIndex = stack.pop(); const face = topology.faces[faceIndex]; group.push(faceIndex); for (let edgeIndex = 0; edgeIndex < 3; edgeIndex++) { const key = _EdgeKey(face.indices[edgeIndex], face.indices[(edgeIndex + 1) % 3]); const edge = topology.edges.get(key); if (!edge) { continue; } for (const edgeFace of edge.faces) { if (visitedFaces.has(edgeFace.faceIndex)) { continue; } if (Vector3.Dot(groupNormal, topology.faces[edgeFace.faceIndex].normal) < 1 - PositionEpsilon) { continue; } visitedFaces.add(edgeFace.faceIndex); stack.push(edgeFace.faceIndex); } } } const clipEdges = []; const addedClipEdges = new Set(); for (const faceIndex of group) { const face = topology.faces[faceIndex]; for (let edgeIndex = 0; edgeIndex < 3; edgeIndex++) { const start = face.indices[edgeIndex]; const end = face.indices[(edgeIndex + 1) % 3]; const key = _EdgeKey(start, end); if (!selectedEdges.has(key) || addedClipEdges.has(key)) { continue; } const edgeStart = topology.positions[start]; const edgeEnd = topology.positions[end]; const edgeDirection = edgeEnd.subtract(edgeStart).normalize(); const inward = Vector3.Cross(face.normal, edgeDirection).normalize(); clipEdges.push({ key, start, end, inward }); addedClipEdges.add(key); } } for (const faceIndex of group) { result.set(faceIndex, clipEdges); } } return result; } function _InsertPointOnPolygonBoundary(polygon, point) { for (const existing of polygon) { if (existing.position.subtract(point).lengthSquared() <= PositionEpsilon * PositionEpsilon) { return; } } let bestEdgeIndex = -1; let bestDistanceSquared = Number.MAX_VALUE; let bestProjection = 0; for (let index = 0; index < polygon.length; index++) { const start = polygon[index].position; const end = polygon[(index + 1) % polygon.length].position; const edge = end.subtract(start); const edgeLengthSquared = edge.lengthSquared(); if (edgeLengthSquared < PositionEpsilon * PositionEpsilon) { continue; } const projection = Vector3.Dot(point.subtract(start), edge) / edgeLengthSquared; if (projection < -PositionEpsilon || projection > 1 + PositionEpsilon) { continue; } const closest = start.add(edge.scale(projection)); const distanceSquared = closest.subtract(point).lengthSquared(); if (distanceSquared < bestDistanceSquared) { bestDistanceSquared = distanceSquared; bestEdgeIndex = index; bestProjection = projection; } } if (bestEdgeIndex !== -1 && bestDistanceSquared <= OutputPositionEpsilon * OutputPositionEpsilon) { const insertedPoint = _InterpolatePolygonPoint(polygon[bestEdgeIndex], polygon[(bestEdgeIndex + 1) % polygon.length], bestProjection); insertedPoint.position = point.clone(); polygon.splice(bestEdgeIndex + 1, 0, insertedPoint); } } function _BuildMergedBoundaryPolygon(polygons) { const points = new Map(); const edgeUseCounts = new Map(); for (const polygon of polygons) { for (const point of polygon) { const key = _VectorKey(point.position); const existing = points.get(key); if (existing) { existing.normal.addInPlace(point.normal).normalize(); } else { points.set(key, _ClonePolygonPoint(point)); } } for (let index = 0; index < polygon.length; index++) { const key0 = _VectorKey(polygon[index].position); const key1 = _VectorKey(polygon[(index + 1) % polygon.length].position); if (key0 === key1) { continue; } const edgeKey = key0 < key1 ? `${key0}|${key1}` : `${key1}|${key0}`; const edgeUseCount = edgeUseCounts.get(edgeKey); if (edgeUseCount) { edgeUseCount.count++; } else { edgeUseCounts.set(edgeKey, { count: 1, key0, key1 }); } } } const adjacency = new Map(); for (const edge of Array.from(edgeUseCounts.values())) { if (edge.count !== 1) { continue; } let adjacency0 = adjacency.get(edge.key0); if (!adjacency0) { adjacency0 = []; adjacency.set(edge.key0, adjacency0); } adjacency0.push(edge.key1); let adjacency1 = adjacency.get(edge.key1); if (!adjacency1) { adjacency1 = []; adjacency.set(edge.key1, adjacency1); } adjacency1.push(edge.key0); } if (!adjacency.size || Array.from(adjacency.values()).some((neighbors) => neighbors.length !== 2)) { return null; } const startKey = Array.from(adjacency.keys()).sort()[0]; const orderedKeys = []; let previousKey = ""; let currentKey = startKey; do { orderedKeys.push(currentKey); const neighbors = adjacency.get(currentKey); const nextKey = neighbors[0] === previousKey ? neighbors[1] : neighbors[0]; previousKey = currentKey; currentKey = nextKey; if (orderedKeys.length > adjacency.size) { return null; } } while (currentKey !== startKey); if (orderedKeys.length !== adjacency.size) { return null; } return orderedKeys.map((key) => points.get(key)); } function _BevelVertexData(vertexData, amount, segments, angle) { const topology = _BuildTopology(vertexData); if (!topology || amount <= PositionEpsilon || segments <= 0) { return _CloneVertexData(vertexData); } const selectedEdges = new Set(); const selectedVertices = new Set(); const selectedEdgeCountPerVertex = new Map(); const threshold = Math.cos(angle + AngleEpsilon); let shortestSelectedEdgeLength = Number.MAX_VALUE; for (const edge of Array.from(topology.edges.values())) { if (edge.faces.length !== 2) { continue; } const face0 = topology.faces[edge.faces[0].faceIndex]; const face1 = topology.faces[edge.faces[1].faceIndex]; if (Vector3.Dot(face0.normal, face1.normal) < threshold) { selectedEdges.add(edge.key); selectedVertices.add(edge.v0); selectedVertices.add(edge.v1); selectedEdgeCountPerVertex.set(edge.v0, (selectedEdgeCountPerVertex.get(edge.v0) ?? 0) + 1); selectedEdgeCountPerVertex.set(edge.v1, (selectedEdgeCountPerVertex.get(edge.v1) ?? 0) + 1); shortestSelectedEdgeLength = Math.min(shortestSelectedEdgeLength, Vector3.Distance(topology.positions[edge.v0], topology.positions[edge.v1])); } } if (!selectedEdges.size) { return _CloneVertexData(vertexData); } const bevelAmount = Math.min(amount, shortestSelectedEdgeLength * 0.5); if (bevelAmount <= PositionEpsilon) { return _CloneVertexData(vertexData); } const faceClipEdges = _BuildCoplanarFaceClipEdges(topology, selectedEdges); const attributeDescriptors = _BuildAttributeDescriptors(vertexData, vertexData.positions.length / 3); const attributeLength = _GetAttributeLength(attributeDescriptors); const outputPositions = []; const outputNormals = []; const outputNormalContributions = []; const outputVertexAttributes = []; const outputIndices = []; const outputTriangleMaterialIndices = []; const outputVertexBuckets = new Map(); const outputVertexGroups = []; const faceEdgeSegments = new Map(); const capPoints = new Map(); const preparedFaces = []; const edgeIntervals = new Map(); const getOrCreateVertex = (position, normal, smoothingGroup, attributes) => { const normalizedNormal = normal.normalizeToNew(); const vertexAttributes = attributeLength ? (attributes.length === attributeLength ? attributes : new Array(attributeLength).fill(0)) : []; const qx = _OutputQuantize(position.x); const qy = _OutputQuantize(position.y); const qz = _OutputQuantize(position.z); let canonicalIndex = -1; for (let dz = -1; dz <= 1; dz++) { for (let dy = -1; dy <= 1; dy++) { for (let dx = -1; dx <= 1; dx++) { const bucket = outputVertexBuckets.get(_OutputPositionKey(qx + dx, qy + dy, qz + dz)); if (!bucket) { continue; } for (const index of bucket) { const outputIndex = index * 3; const deltaX = outputPositions[outputIndex] - position.x; const deltaY = outputPositions[outputIndex + 1] - position.y; const deltaZ = outputPositions[outputIndex + 2] - position.z; if (deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ <= OutputPositionEpsilon * OutputPositionEpsilon) { if (canonicalIndex === -1) { canonicalIndex = index; } if (outputVertexGroups[index] !== smoothingGroup) { continue; } if (!_AttributesMatch(outputVertexAttributes[index], vertexAttributes)) { continue; } const normalContributions = outputNormalContributions[index]; if (!normalContributions.some((normal) => Vector3.Dot(normal, normalizedNormal) > 1 - PositionEpsilon)) { normalContributions.push(normalizedNormal); outputNormals[index].addInPlace(normalizedNormal); } return index; } } } } } const index = outputPositions.length / 3; const outputX = canonicalIndex === -1 ? position.x : outputPositions[canonicalIndex * 3]; const outputY = canonicalIndex === -1 ? position.y : outputPositions[canonicalIndex * 3 + 1]; const outputZ = canonicalIndex === -1 ? position.z : outputPositions[canonicalIndex * 3 + 2]; const key = _OutputPositionKey(_OutputQuantize(outputX), _OutputQuantize(outputY), _OutputQuantize(outputZ)); let bucket = outputVertexBuckets.get(key); if (!bucket) { bucket = []; outputVertexBuckets.set(key, bucket); } bucket.push(index); outputPositions.push(outputX, outputY, outputZ); outputNormals.push(normalizedNormal); outputNormalContributions.push([normalizedNormal.clone()]); outputVertexAttributes.push(vertexAttributes.slice()); outputVertexGroups.push(smoothingGroup); return index; }; const addCapPoint = (vertexIndex, point, normal, attributes, materialIndex) => { let points = capPoints.get(vertexIndex); if (!points) { points = []; capPoints.set(vertexIndex, points); } _AddUniquePoint(points, point, normal, attributes, materialIndex); }; const addTriangle = (p0, p1, p2, targetNormal, n0 = targetNormal, n1 = targetNormal, n2 = targetNormal, smoothingGroup = "smooth", p0Attributes = [], p1Attributes = [], p2Attributes = [], materialIndex = 0) => { const edge0 = p1.subtract(p0); const edge1 = p2.subtract(p0); const normal = Vector3.Cross(edge0, edge1); if (normal.lengthSquared() < TriangleAreaEpsilon) { return; } let v1 = p1; let v2 = p2; let vn1 = n1; let vn2 = n2; let va1 = p1Attributes; let va2 = p2Attributes; if (Vector3.Dot(normal, targetNormal) > 0) { v1 = p2; v2 = p1; vn1 = n2; vn2 = n1; va1 = p2Attributes; va2 = p1Attributes; } const i0 = getOrCreateVertex(p0, n0, smoothingGroup, p0Attributes); const i1 = getOrCreateVertex(v1, vn1, smoothingGroup, va1); const i2 = getOrCreateVertex(v2, vn2, smoothingGroup, va2); if (i0 === i1 || i1 === i2 || i2 === i0) { return; } outputIndices.push(i0, i1, i2); outputTriangleMaterialIndices.push(materialIndex); }; const addFacePolygon = (polygon, normal, smoothingGroup, useFlatNormals) => { const center = new Vector3(); const centerAttributes = _AverageAttributes(polygon.map((point) => point.attributes), attributeLength); const materialIndex = polygon[0]?.materialIndex ?? 0; const centerNormal = useFlatNormals ? normal : _NormalizeNormalOrFallback(polygon.reduce((accumulator, point) => accumulator.addInPlace(point.normal), new Vector3()), normal); for (const point of polygon) { center.addInPlace(point.position); } center.scaleInPlace(1 / polygon.length); for (let index = 0; index < polygon.length; index++) { const nextIndex = (index + 1) % polygon.length; addTriangle(center, polygon[index].position, polygon[nextIndex].position, normal, centerNormal, useFlatNormals ? normal : polygon[index].normal, useFlatNormals ? normal : polygon[nextIndex].normal, smoothingGroup, centerAttributes, polygon[index].attributes, polygon[nextIndex].attributes, materialIndex); } }; for (let faceIndex = 0; faceIndex < topology.faces.length; faceIndex++) { const face = topology.faces[faceIndex]; const isFlat = _IsFlatFace(face); let polygon = face.indices.map((index, cornerIndex) => ({ position: topology.positions[index].clone(), normal: face.cornerNormals[cornerIndex].clone(), attributes: _GetVertexAttributes(attributeDescriptors, face.originalIndices[cornerIndex]), materialIndex: face.materialIndex, })); const selectedFaceEdges = []; for (let edgeIndex = 0; edgeIndex < 3; edgeIndex++) { const start = face.indices[edgeIndex]; const end = face.indices[(edgeIndex + 1) % 3]; const key = _EdgeKey(start, end); if (!selectedEdges.has(key)) { continue; } const edgeStart = topology.positions[start]; const edgeEnd = topology.positions[end]; const edgeDirection = edgeEnd.subtract(edgeStart).normalize(); const inward = Vector3.Cross(face.normal, edgeDirection).normalize(); selectedFaceEdges.push({ key, start, end, inward }); } const clipEdges = faceClipEdges.get(faceIndex) ?? selectedFaceEdges; for (const clipEdge of clipEdges) { polygon = _ClipPolygonAgainstEdge(polygon, topology.positions[clipEdge.start], clipEdge.inward, bevelAmount); } if (isFlat) { for (const point of polygon) { point.normal = face.normal.clone(); } } preparedFaces.push({ faceIndex, polygon, selectedFaceEdges, isFlat }); for (const selectedFaceEdge of selectedFaceEdges) { const edge = topology.edges.get(selectedFaceEdge.key); const edgeStart = topology.positions[edge.v0]; const edgeEnd = topology.positions[edge.v1]; const axis = edgeEnd.subtract(edgeStart); const edgeLength = axis.length(); if (edgeLength < PositionEpsilon) { continue; } axis.normalize(); const pointsOnLine = []; const orientedEdgeStart = topology.positions[selectedFaceEdge.start]; for (const point of polygon) { const distance = Vector3.Dot(point.position.subtract(orientedEdgeStart), selectedFaceEdge.inward); if (Math.abs(distance - bevelAmount) < PositionEpsilon * 10) { const t = Vector3.Dot(point.position.subtract(edgeStart), axis); if (t >= -PositionEpsilon && t <= edgeLength + PositionEpsilon) { pointsOnLine.push({ point, t: Math.min(edgeLength, Math.max(0, t)) }); } } } if (pointsOnLine.length < 2) { continue; } pointsOnLine.sort((a, b) => a.t - b.t); const minPoint = pointsOnLine[0].point.position; const maxPoint = pointsOnLine[pointsOnLine.length - 1].point.position; const minNormal = pointsOnLine[0].point.normal; const maxNormal = pointsOnLine[pointsOnLine.length - 1].point.normal; const minAttributes = pointsOnLine[0].point.attributes; const maxAttributes = pointsOnLine[pointsOnLine.length - 1].point.attributes; const segment = { edgeKey: selectedFaceEdge.key, faceIndex, inward: selectedFaceEdge.inward, tMin: pointsOnLine[0].t, tMax: pointsOnLine[pointsOnLine.length - 1].t, minPoint, maxPoint, minNormal, maxNormal, minAttributes, maxAttributes, materialIndex: face.materialIndex, }; faceEdgeSegments.set(`${faceIndex}|${selectedFaceEdge.key}`, segment); addCapPoint(edge.v0, minPoint, pointsOnLine[0].point.normal, minAttributes, face.materialIndex); addCapPoint(edge.v1, maxPoint, pointsOnLine[pointsOnLine.length - 1].point.normal, maxAttributes, face.materialIndex); } } for (const edgeKey of Array.from(selectedEdges)) { const edge = topology.edges.get(edgeKey); const face0 = edge.faces[0].faceIndex; const face1 = edge.faces[1].faceIndex; const segment0 = faceEdgeSegments.get(`${face0}|${edgeKey}`); const segment1 = faceEdgeSegments.get(`${face1}|${edgeKey}`); if (!segment0 || !segment1) { continue; } const edgeStart = topology.positions[edge.v0]; const edgeEnd = topology.positions[edge.v1]; const axis = edgeEnd.subtract(edgeStart); const edgeLength = axis.length(); if (edgeLength < PositionEpsilon) { continue; } axis.normalize(); const tStart = Math.max(segment0.tMin, segment1.tMin); const tEnd = Math.min(segment0.tMax, segment1.tMax); if (tEnd - tStart <= PositionEpsilon) { continue; } edgeIntervals.set(edgeKey, { tStart, tEnd }); } for (const preparedFace of preparedFaces) { for (const selectedFaceEdge of preparedFace.selectedFaceEdges) { const edge = topology.edges.get(selectedFaceEdge.key); const interval = edgeIntervals.get(selectedFaceEdge.key); const segment = faceEdgeSegments.get(`${preparedFace.faceIndex}|${selectedFaceEdge.key}`); if (!interval || !segment) { continue; } const edgeStart = topology.positions[edge.v0]; const edgeEnd = topology.positions[edge.v1]; const axis = edgeEnd.subtract(edgeStart); const edgeLength = axis.length(); if (edgeLength < PositionEpsilon) { continue; } axis.normalize(); for (const t of [interval.tStart, interval.tEnd]) { if (t <= segment.tMin + PositionEpsilon || t >= segment.tMax - PositionEpsilon) { continue; } _InsertPointOnPolygonBoundary(preparedFace.polygon, edgeStart.add(axis.scale(t)).addInPlace(segment.inward.scale(bevelAmount))); } } } const emittedMergedFaces = new Set(); const preparedFacesByPlane = new Map(); for (const preparedFace of preparedFaces) { if (!preparedFace.selectedFaceEdges.length || !preparedFace.isFlat) { continue; } const face = topology.faces[preparedFace.faceIndex]; const distance = Vector3.Dot(topology.positions[face.indices[0]], face.normal); const planeKey = `${_PositionKey(face.normal.x, face.normal.y, face.normal.z)}:${_Quantize(distance)}`; let group = preparedFacesByPlane.get(planeKey); if (!group) { group = []; preparedFacesByPlane.set(planeKey, group); } group.push(preparedFace); } for (const group of Array.from(preparedFacesByPlane.values())) { if (group.length < 2) { continue; } const mergedPolygon = _BuildMergedBoundaryPolygon(group.map((preparedFace) => preparedFace.polygon)); if (!mergedPolygon || mergedPolygon.length < 3) { continue; } const face = topology.faces[group[0].faceIndex]; addFacePolygon(mergedPolygon, face.normal, `face-flat:${_PositionKey(face.normal.x, face.normal.y, face.normal.z)}`, true); for (const preparedFace of group) { emittedMergedFaces.add(preparedFace.faceIndex); } } for (const preparedFace of preparedFaces) { if (emittedMergedFaces.has(preparedFace.faceIndex)) { continue; } const face = topology.faces[preparedFace.faceIndex]; const smoothingGroup = preparedFace.selectedFaceEdges.length && !preparedFace.isFlat ? "smooth" : `face-flat:${_PositionKey(face.normal.x, face.normal.y, face.normal.z)}`; if (preparedFace.polygon.length >= 3) { if (smoothingGroup === "smooth") { addFacePolygon(preparedFace.polygon, face.normal, smoothingGroup, false); } else { for (let index = 1; index < preparedFace.polygon.length - 1; index++) { addTriangle(preparedFace.polygon[0].position, preparedFace.polygon[index].position, preparedFace.polygon[index + 1].position, face.normal, face.normal, face.normal, face.normal, smoothingGroup, preparedFace.polygon[0].attributes, preparedFace.polygon[index].attributes, preparedFace.polygon[index + 1].attributes, face.materialIndex); } } } } for (const edgeKey of Array.from(selectedEdges)) { const edge = topology.edges.get(edgeKey); const face0 = edge.faces[0].faceIndex; const face1 = edge.faces[1].faceIndex; const segment0 = faceEdgeSegments.get(`${face0}|${edgeKey}`); const segment1 = faceEdgeSegments.get(`${face1}|${edgeKey}`); const interval = edgeIntervals.get(edgeKey); if (!segment0 || !segment1 || !interval) { continue; } const edgeStart = topology.positions[edge.v0]; const edgeEnd = topology.positions[edge.v1]; const axis = edgeEnd.subtract(edgeStart); const edgeLength = axis.length(); if (edgeLength < PositionEpsilon) { continue; } axis.normalize(); const { tStart, tEnd } = interval; const faceNormal0 = topology.faces[segment0.faceIndex].normal; const faceNormal1 = topology.faces[segment1.faceIndex].normal; const segment0Start = _InterpolateSegmentPoint(segment0, tStart); const segment1Start = _InterpolateSegmentPoint(segment1, tStart); const segment0End = _InterpolateSegmentPoint(segment0, tEnd); const segment1End = _InterpolateSegmentPoint(segment1, tEnd); const segment0StartNormal = _InterpolateSegmentNormal(segment0, tStart); const segment1StartNormal = _InterpolateSegmentNormal(segment1, tStart); const segment0EndNormal = _InterpolateSegmentNormal(segment0, tEnd); const segment1EndNormal = _InterpolateSegmentNormal(segment1, tEnd); const segment0StartAttributes = _InterpolateSegmentAttributes(segment0, tStart); const segment1StartAttributes = _InterpolateSegmentAttributes(segment1, tStart); const segment0EndAttributes = _InterpolateSegmentAttributes(segment0, tEnd); const segment1EndAttributes = _InterpolateSegmentAttributes(segment1, tEnd); const centerStart = segment0Start .subtract(faceNormal0.scale(bevelAmount)) .addInPlace(segment1Start.subtract(faceNormal1.scale(bevelAmount))) .scaleInPlace(0.5); const centerEnd = segment0End .subtract(faceNormal0.scale(bevelAmount)) .addInPlace(segment1End.subtract(faceNormal1.scale(bevelAmount))) .scaleInPlace(0.5); const startProfilePoints = []; const endProfilePoints = []; const startProfileNormals = []; const endProfileNormals = []; const startProfileAttributes = []; const endProfileAttributes = []; for (let segmentIndex = 0; segmentIndex <= segments; segmentIndex++) { const profileAmount = segmentIndex / segments; const profileNormal = _SlerpDirections(faceNormal0, faceNormal1, profileAmount); const startNormal = _SlerpDirections(segment0StartNormal, segment1StartNormal, profileAmount); const endNormal = _SlerpDirections(segment0EndNormal, segment1EndNormal, profileAmount); const startAttributes = _InterpolateAttributes(segment0StartAttributes, segment1StartAttributes, profileAmount); const endAttributes = _InterpolateAttributes(segment0EndAttributes, segment1EndAttributes, profileAmount); let startPoint = centerStart.add(profileNormal.scale(bevelAmount)); let endPoint = centerEnd.add(profileNormal.scale(bevelAmount)); if (segmentIndex === 0) { startPoint = segment0Start; endPoint = segment0End; } else if (segmentIndex === segments) { startPoint = segment1Start; endPoint = segment1End; } startProfilePoints.push(startPoint); endProfilePoints.push(endPoint); startProfileNormals.push(startNormal); endProfileNormals.push(endNormal); startProfileAttributes.push(startAttributes); endProfileAttributes.push(endAttributes); addCapPoint(edge.v0, startPoint, startNormal, startAttributes, segment0.materialIndex); addCapPoint(edge.v1, endPoint, endNormal, endAttributes, segment0.materialIndex); } for (let segmentIndex = 0; segmentIndex < segments; segmentIndex++) { const normalTarget = _SlerpDirections(faceNormal0, faceNormal1, (segmentIndex + 0.5) / segments); addTriangle(startProfilePoints[segmentIndex], endProfilePoints[segmentIndex], endProfilePoints[segmentIndex + 1], normalTarget, startProfileNormals[segmentIndex], endProfileNormals[segmentIndex], endProfileNormals[segmentIndex + 1], "smooth", startProfileAttributes[segmentIndex], endProfileAttributes[segmentIndex], endProfileAttributes[segmentIndex + 1], segment0.materialIndex); addTriangle(startProfilePoints[segmentIndex], endProfilePoints[segmentIndex + 1], startProfilePoints[segmentIndex + 1], normalTarget, startProfileNormals[segmentIndex], endProfileNormals[segmentIndex + 1], startProfileNormals[segmentIndex + 1], "smooth", startProfileAttributes[segmentIndex], endProfileAttributes[segmentIndex + 1], startProfileAttributes[segmentIndex + 1], segment0.materialIndex); } } const addSphericalCornerPatch = (vertexIndex) => { if ((selectedEdgeCountPerVertex.get(vertexIndex) ?? 0) < 3) { return false; } const incidentNormals = []; const vertexPosition = topology.positions[vertexIndex]; const cornerCapPoints = capPoints.get(vertexIndex) ?? []; const cornerAttributes = _AverageAttributes(cornerCapPoints.map((point) => _GetCapPointAttributes(point)), attributeLength); const materialIndex = cornerCapPoints.length ? _GetCapPointMaterialIndex(cornerCapPoints[0]) : 0; for (const edgeKey of Array.from(selectedEdges)) { const edge = topology.edges.get(edgeKey); if (edge.v0 !== vertexIndex && edge.v1 !== vertexIndex) { continue; } for (const edgeFace of edge.faces) { _AddUniqueNormal(incidentNormals, topology.faces[edgeFace.faceIndex].normal); } } if (incidentNormals.length !== 3) { return false; } const averageNormal = incidentNormals[0].add(incidentNormals[1]).addInPlace(incidentNormals[2]).normalize(); const tangent = incidentNormals[0].subtract(averageNormal.scale(Vector3.Dot(incidentNormals[0], averageNormal))).normalize(); const bitangent = Vector3.Cross(averageNormal, tangent).normalize(); incidentNormals.sort((a, b) => Math.atan2(Vector3.Dot(a, bitangent), Vector3.Dot(a, tangent)) - Math.atan2(Vector3.Dot(b, bitangent), Vector3.Dot(b, tangent))); const distances = incidentNormals.map((normal) => Vector3.Dot(vertexPosition, normal) - bevelAmount); const center = _SolveThreePlaneIntersection(incidentNormals, distances); if (!center) { return false; } const rows = []; for (let rowIndex = 0; rowIndex <= segments; rowIndex++) { const rowAmount = rowIndex / segments; const left = _SlerpDirections(incidentNormals[0], incidentNormals[1], rowAmount); const right = _SlerpDirections(incidentNormals[0], incidentNormals[2], rowAmount); const row = []; for (let columnIndex = 0; columnIndex <= rowIndex; columnIndex++) { const columnAmount = rowIndex === 0 ? 0 : columnIndex / rowIndex; const direction = rowIndex === 0 ? incidentNormals[0].clone() : _SlerpDirections(left, right, columnAmount); row.push({ position: center.add(direction.scale(bevelAmount)), normal: direction, attributes: cornerAttributes, materialIndex, }); } rows.push(row); } for (let rowIndex = 0; rowIndex < segments; rowIndex++) { const row = rows[rowIndex]; const nextRow = rows[rowIndex + 1]; for (let columnIndex = 0; columnIndex < row.length; columnIndex++) { const targetNormal0 = row[columnIndex].position.subtract(center).normalize(); const targetNormal1 = nextRow[columnIndex].po