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.

400 lines 15.7 kB
import { Vector3 } from "../Maths/math.vector.js"; import { VertexData } from "./mesh.vertexData.js"; import { Scalar } from "../Maths/math.scalar.js"; const PositionShift = Math.pow(10, 4); /** * Rounds a number (simulate integer rounding) * @internal */ function Round(x) { return (x + (x > 0 ? 0.5 : -0.5)) << 0; } /** * Generates a hash string from a number * @internal */ function HashFromNumber(num, shift = PositionShift) { let roundedNumber = Round(num * shift); if (roundedNumber === 0) { roundedNumber = 0; // prevent -0 } return `${roundedNumber}`; } /** * Generates a hash string from a Vector3 * @internal */ function HashFromVector(v, shift = PositionShift) { return `${HashFromNumber(v.x, shift)},${HashFromNumber(v.y, shift)},${HashFromNumber(v.z, shift)}`; } /** * Gathers attribute names from a VertexData object * @internal */ function GatherAttributes(vertexData) { const desired = ["positions", "normals", "uvs"]; const available = Object.keys(vertexData).filter((k) => Array.isArray(vertexData[k])); return Array.from(new Set([...desired, ...available])); } /** * Sets triangle data into an attribute array * @internal */ function SetTriangle(arr, index, itemSize, vec0, vec1, vec2) { for (let i = 0; i < itemSize; i++) { arr[index + i] = vec0[i]; arr[index + itemSize + i] = vec1[i]; arr[index + 2 * itemSize + i] = vec2[i]; } } /** * Converts indexed VertexData to a non-indexed form * @internal */ function ToNonIndexed(vertexData) { if (!vertexData.indices || vertexData.indices.length === 0) { return vertexData; // already non-indexed } const newPositions = []; const newNormals = []; const newUVs = []; const indices = vertexData.indices; const pos = vertexData.positions; const norm = vertexData.normals; const uv = vertexData.uvs; for (let i = 0; i < indices.length; i++) { const idx = indices[i]; newPositions.push(pos[3 * idx], pos[3 * idx + 1], pos[3 * idx + 2]); if (norm) { newNormals.push(norm[3 * idx], norm[3 * idx + 1], norm[3 * idx + 2]); } if (uv) { newUVs.push(uv[2 * idx], uv[2 * idx + 1]); } } const newVertexData = new VertexData(); newVertexData.positions = newPositions; if (newNormals.length) { newVertexData.normals = newNormals; } if (newUVs.length) { newVertexData.uvs = newUVs; } return newVertexData; } /** Helper to read a Vector3 from an attribute array * @internal */ function ReadVector(destination, attribute, index, itemSize) { if (itemSize === 3) { destination.fromArray(attribute, index * 3); return; } // For uvs (itemSize 2), return a Vector3 with z = 0. destination.set(attribute[index * 2], attribute[index * 2 + 1], 0); } function ProcessFlatAttribute(source, vertexCount, output) { const v0 = new Vector3(); const v1 = new Vector3(); const v2 = new Vector3(); const m01 = new Vector3(); const m12 = new Vector3(); const m20 = new Vector3(); for (let i = 0; i < vertexCount; i += 3) { const j = i * 3; v0.set(source[j], source[j + 1], source[j + 2]); v1.set(source[j + 3], source[j + 4], source[j + 5]); v2.set(source[j + 6], source[j + 7], source[j + 8]); v0.addToRef(v1, m01); m01.scaleInPlace(0.5); v1.addToRef(v2, m12); m12.scaleInPlace(0.5); v2.addToRef(v0, m20); m20.scaleInPlace(0.5); // Positions output.push(v0.x, v0.y, v0.z, m01.x, m01.y, m01.z, m20.x, m20.y, m20.z); output.push(v1.x, v1.y, v1.z, m12.x, m12.y, m12.z, m01.x, m01.y, m01.z); output.push(v2.x, v2.y, v2.z, m20.x, m20.y, m20.z, m12.x, m12.y, m12.z); output.push(m01.x, m01.y, m01.z, m12.x, m12.y, m12.z, m20.x, m20.y, m20.z); } } /** * Applies one iteration of flat subdivision (each triangle becomes 4). * @internal */ function Flat(vertexData) { const data = ToNonIndexed(vertexData); const positions = data.positions; const normals = data.normals; const uvs = data.uvs; const vertexCount = positions.length / 3; const newPositions = []; const newNormals = []; const newUVs = []; ProcessFlatAttribute(positions, vertexCount, newPositions); if (normals && normals.length) { ProcessFlatAttribute(normals, vertexCount, newNormals); } if (uvs && uvs.length) { for (let i = 0; i < vertexCount; i += 3) { const j = i * 2; const uv0 = [uvs[j], uvs[j + 1]]; const uv1 = [uvs[j + 2], uvs[j + 3]]; const uv2 = [uvs[j + 4], uvs[j + 5]]; const uv01 = [(uv0[0] + uv1[0]) / 2, (uv0[1] + uv1[1]) / 2]; const uv12 = [(uv1[0] + uv2[0]) / 2, (uv1[1] + uv2[1]) / 2]; const uv20 = [(uv2[0] + uv0[0]) / 2, (uv2[1] + uv0[1]) / 2]; newUVs.push(...uv0, ...uv01, ...uv20); newUVs.push(...uv1, ...uv12, ...uv01); newUVs.push(...uv2, ...uv20, ...uv12); newUVs.push(...uv01, ...uv12, ...uv20); } } const newVertexCount = newPositions.length / 3; const newIndices = []; for (let i = 0; i < newVertexCount; i++) { newIndices.push(i); } const newVertexData = new VertexData(); newVertexData.positions = newPositions; if (newNormals.length) { newVertexData.normals = newNormals; } if (newUVs.length) { newVertexData.uvs = newUVs; } newVertexData.indices = newIndices; return newVertexData; } /** * Applies one iteration of smooth subdivision with vertex averaging. * This function uses the subdivideAttribute routine to adjust vertex data. * @internal */ function Smooth(vertexData, options) { // Convert to non-indexed and apply flat subdivision first. const sourceData = ToNonIndexed(vertexData); const flatData = Flat(sourceData); const attributeList = GatherAttributes(sourceData); const origPositions = sourceData.positions; const flatPositions = flatData.positions; const vertexCount = origPositions.length / 3; // Build connectivity maps from the original geometry. const hashToIndex = {}; const existingNeighbors = {}; const flatOpposites = {}; const existingEdges = {}; function addNeighbor(posHash, neighborHash, index) { if (!existingNeighbors[posHash]) { existingNeighbors[posHash] = {}; } if (!existingNeighbors[posHash][neighborHash]) { existingNeighbors[posHash][neighborHash] = []; } existingNeighbors[posHash][neighborHash].push(index); } function addOpposite(posHash, index) { if (!flatOpposites[posHash]) { flatOpposites[posHash] = []; } flatOpposites[posHash].push(index); } function addEdgePoint(posHash, edgeHash) { if (!existingEdges[posHash]) { existingEdges[posHash] = new Set(); } existingEdges[posHash].add(edgeHash); } const temp = new Vector3(); const v0 = new Vector3(); const v1 = new Vector3(); const v2 = new Vector3(); const m01 = new Vector3(); const m12 = new Vector3(); const m20 = new Vector3(); // Process original positions for (let i = 0; i < vertexCount; i += 3) { ReadVector(v0, origPositions, i, 3); ReadVector(v1, origPositions, i + 1, 3); ReadVector(v2, origPositions, i + 2, 3); const h0 = HashFromVector(v0); const h1 = HashFromVector(v1); const h2 = HashFromVector(v2); addNeighbor(h0, h1, i + 1); addNeighbor(h0, h2, i + 2); addNeighbor(h1, h0, i); addNeighbor(h1, h2, i + 2); addNeighbor(h2, h0, i); addNeighbor(h2, h1, i + 1); // Opposites from flat subdivision: calculate midpoints. v0.addToRef(v1, m01); m01.scaleInPlace(0.5); v1.addToRef(v2, m12); m12.scaleInPlace(0.5); v2.addToRef(v0, m20); m20.scaleInPlace(0.5); addOpposite(HashFromVector(m01), i + 2); addOpposite(HashFromVector(m12), i); addOpposite(HashFromVector(m20), i + 1); // Track edges for preserveEdges. addEdgePoint(h0, HashFromVector(m01)); addEdgePoint(h0, HashFromVector(m20)); addEdgePoint(h1, HashFromVector(m01)); addEdgePoint(h1, HashFromVector(m12)); addEdgePoint(h2, HashFromVector(m12)); addEdgePoint(h2, HashFromVector(m20)); } // Build map from flat positions to indices. for (let i = 0; i < flatPositions.length / 3; i++) { ReadVector(temp, flatPositions, i, 3); const h = HashFromVector(temp); if (!hashToIndex[h]) { hashToIndex[h] = []; } hashToIndex[h].push(i); } // Prepare temporary vectors for subdivideAttribute. const _vertex = [new Vector3(), new Vector3(), new Vector3()]; const _position = [new Vector3(), new Vector3(), new Vector3()]; const _average = new Vector3(); const _temp = new Vector3(); // subdivideAttribute: adjusts vertex attributes using Loop’s averaging rules. function subdivideAttribute(attributeName, existingAttribute, flattenedAttribute) { const itemSize = attributeName === "uvs" ? 2 : 3; const flatVertexCount = flatPositions.length / 3; const floatArray = new Array(flatVertexCount * itemSize); let index = 0; for (let i = 0; i < flatVertexCount; i += 3) { for (let v = 0; v < 3; v++) { if (attributeName === "uvs" && !options.uvSmooth) { // Simply copy UVs. ReadVector(_vertex[v], flattenedAttribute, i + v, 2); } else if (attributeName === "normals") { ReadVector(_position[v], flatPositions, i + v, 3); const positionHash = HashFromVector(_position[v]); const positionsArr = hashToIndex[positionHash] || []; const k = positionsArr.length; const beta = 0.75 / k; const startWeight = 1.0 - beta * k; ReadVector(_vertex[v], flattenedAttribute, i + v, 3); _vertex[v].scaleInPlace(startWeight); for (const positionIndex of positionsArr) { ReadVector(_average, flattenedAttribute, positionIndex, 3); _average.scaleInPlace(beta); _vertex[v].addInPlace(_average); } } else { // 'positions', 'colors', etc. ReadVector(_vertex[v], flattenedAttribute, i + v, itemSize); ReadVector(_position[v], flatPositions, i + v, 3); const positionHash = HashFromVector(_position[v]); const neighbors = existingNeighbors[positionHash]; const opposites = flatOpposites[positionHash]; if (neighbors) { if (options.preserveEdges) { const edgeSet = existingEdges[positionHash]; let hasPair = true; edgeSet.forEach((edgeHash) => { if (flatOpposites[edgeHash] && flatOpposites[edgeHash].length % 2 !== 0) { hasPair = false; } }); if (!hasPair) { // If edges aren't paired, skip adjustment. continue; } } const neighborKeys = Object.keys(neighbors); const k = neighborKeys.length; const beta = (1 / k) * (5 / 8 - Math.pow(3 / 8 + (1 / 4) * Math.cos((2 * Math.PI) / k), 2)); const heavy = 1 / k / k; const weight = Scalar.Lerp(heavy, beta, options.weight); const startWeight = 1.0 - weight * k; _vertex[v].scaleInPlace(startWeight); for (const neighborHash in neighbors) { const neighborIndices = neighbors[neighborHash]; _average.set(0, 0, 0); for (const neighborIndex of neighborIndices) { ReadVector(_temp, existingAttribute, neighborIndex, itemSize); _average.addInPlace(_temp); } _average.scaleInPlace(1 / neighborIndices.length); _average.scaleInPlace(weight); _vertex[v].addInPlace(_average); } } else if (opposites && opposites.length === 2) { const k = opposites.length; const beta = 0.125; // 1/8 const startWeight = 1.0 - beta * k; _vertex[v].scaleInPlace(startWeight); for (const oppositeIndex of opposites) { ReadVector(_average, existingAttribute, oppositeIndex, itemSize); _average.scaleInPlace(beta); _vertex[v].addInPlace(_average); } } } } // Write out new triangle vertices. SetTriangle(floatArray, index, itemSize, _vertex[0].asArray(), _vertex[1].asArray(), _vertex[2].asArray()); index += itemSize * 3; } return floatArray; } // Build new attributes for the smoothed geometry. const smoothData = new VertexData(); for (const attributeName of attributeList) { if (attributeName === "indices") { continue; } const existingAttribute = sourceData[attributeName]; const flattenedAttribute = flatData[attributeName]; if (!existingAttribute || !flattenedAttribute) { continue; } const newArray = subdivideAttribute(attributeName, existingAttribute, flattenedAttribute); smoothData[attributeName] = newArray; } // Rebuild indices sequentially. const newPositions = smoothData.positions; const newIndices = []; for (let i = 0; i < newPositions.length / 3; i++) { newIndices.push(i); } smoothData.indices = newIndices; return smoothData; } /** * Subdivide a vertexData using Loop algorithm * @param vertexData The vertexData to subdivide * @param level The number of times to subdivide * @param options The options to use when subdividing * @returns The subdivided vertexData */ export function Subdivide(vertexData, level, options) { options = { flatOnly: false, uvSmooth: false, preserveEdges: false, weight: 1, ...options, }; if (!vertexData.positions || vertexData.positions.length === 0 || level <= 0) { return vertexData; } // Clone the input let modified = vertexData.clone(); for (let i = 0; i < level; i++) { if (options.flatOnly) { modified = Flat(modified); } else { modified = Smooth(modified, options); } } return modified; } //# sourceMappingURL=mesh.vertexData.subdivide.js.map