UNPKG

@xeokit/xeokit-convert

Version:

JavaScript utilities to create .XKT files

665 lines (523 loc) 19.9 kB
import {earcut} from './../lib/earcut.js'; import {math} from "./../lib/math.js"; const tempVec2a = math.vec2(); const tempVec3a = math.vec3(); const tempVec3b = math.vec3(); const tempVec3c = math.vec3(); /** * @desc Parses a CityJSON model into an {@link XKTModel}. * * [CityJSON](https://www.cityjson.org) is a JSON-based encoding for a subset of the CityGML data model (version 2.0.0), * which is an open standardised data model and exchange format to store digital 3D models of cities and * landscapes. CityGML is an official standard of the [Open Geospatial Consortium](https://www.ogc.org/). * * This converter function supports most of the [CityJSON 1.0.2 Specification](https://www.cityjson.org/specs/1.0.2), * with the following limitations: * * * Does not (yet) support CityJSON semantics for geometry primitives. * * Does not (yet) support textured geometries. * * Does not (yet) support geometry templates. * * When the CityJSON file provides multiple *themes* for a geometry, then we parse only the first of the provided themes for that geometry. * * ## Usage * * In the example below we'll create an {@link XKTModel}, then load a CityJSON model into it. * * ````javascript * utils.loadJSON("./models/cityjson/DenHaag.json", async (data) => { * * const xktModel = new XKTModel(); * * parseCityJSONIntoXKTModel({ * data, * xktModel, * log: (msg) => { console.log(msg); } * }).then(()=>{ * xktModel.finalize(); * }, * (msg) => { * console.error(msg); * }); * }); * ```` * * @param {Object} params Parsing params. * @param {Object} params.data CityJSON data. * @param {XKTModel} params.xktModel XKTModel to parse into. * @param {boolean} [params.center=false] Set true to center the CityJSON vertex positions to [0,0,0]. This is applied before the transformation matrix, if specified. * @param {Boolean} [params.transform] 4x4 transformation matrix to transform CityJSON vertex positions. Use this to rotate, translate and scale them if neccessary. * @param {Object} [params.stats] Collects statistics. * @param {function} [params.log] Logging callback. @returns {Promise} Resolves when CityJSON has been parsed. */ function parseCityJSONIntoXKTModel({ data, xktModel, center = false, transform = null, stats = {}, log }) { return new Promise(function (resolve, reject) { if (!data) { reject("Argument expected: data"); return; } if (data.type !== "CityJSON") { reject("Invalid argument: data is not a CityJSON file"); return; } if (!xktModel) { reject("Argument expected: xktModel"); return; } let vertices; log("Using parser: parseCityJSONIntoXKTModel"); log(`center: ${center}`); if (transform) { log(`transform: [${transform}]`); } if (data.transform || center || transform) { vertices = copyVertices(data.vertices); if (data.transform) { transformVertices(vertices, data.transform) } if (center) { centerVertices(vertices); } if (transform) { customTransformVertices(vertices, transform); } } else { vertices = data.vertices; } stats.sourceFormat = data.type || ""; stats.schemaVersion = data.version || ""; stats.title = ""; stats.author = ""; stats.created = ""; stats.numMetaObjects = 0; stats.numPropertySets = 0; stats.numTriangles = 0; stats.numVertices = 0; stats.numObjects = 0; stats.numGeometries = 0; const rootMetaObjectId = math.createUUID(); xktModel.createMetaObject({ metaObjectId: rootMetaObjectId, metaObjectType: "Model", metaObjectName: "Model" }); stats.numMetaObjects++; const modelMetaObjectId = math.createUUID(); xktModel.createMetaObject({ metaObjectId: modelMetaObjectId, metaObjectType: "CityJSON", metaObjectName: "CityJSON", parentMetaObjectId: rootMetaObjectId }); stats.numMetaObjects++; const ctx = { data, vertices, xktModel, rootMetaObjectId: modelMetaObjectId, log: (log || function (msg) { }), nextId: 0, stats }; ctx.xktModel.schema = data.type + " " + data.version; ctx.log("Converting " + ctx.xktModel.schema); parseCityJSON(ctx); resolve(); }); } function copyVertices(vertices) { const vertices2 = []; for (let i = 0, j = 0; i < vertices.length; i++, j += 3) { const x = vertices[i][0]; const y = vertices[i][1]; const z = vertices[i][2]; vertices2.push([x, y, z]); } return vertices2; } function transformVertices(vertices, cityJSONTransform) { const scale = cityJSONTransform.scale || math.vec3([1, 1, 1]); const translate = cityJSONTransform.translate || math.vec3([0, 0, 0]); for (let i = 0; i < vertices.length; i++) { const vertex = vertices[i]; vertex[0] = (vertex[0] * scale[0]) + translate[0]; vertex[1] = (vertex[1] * scale[1]) + translate[1]; vertex[2] = (vertex[2] * scale[2]) + translate[2]; } } function centerVertices(vertices) { if (center) { const centerPos = math.vec3(); const numPoints = vertices.length; for (let i = 0, len = vertices.length; i < len; i++) { const vertex = vertices[i]; centerPos[0] += vertex[0]; centerPos[1] += vertex[1]; centerPos[2] += vertex[2]; } centerPos[0] /= numPoints; centerPos[1] /= numPoints; centerPos[2] /= numPoints; for (let i = 0, len = vertices.length; i < len; i++) { const vertex = vertices[i]; vertex[0] -= centerPos[0]; vertex[1] -= centerPos[1]; vertex[2] -= centerPos[2]; } } } function customTransformVertices(vertices, transform) { if (transform) { const mat = math.mat4(transform); for (let i = 0, len = vertices.length; i < len; i++) { const vertex = vertices[i]; math.transformPoint3(mat, vertex, vertex); } } } function parseCityJSON(ctx) { const data = ctx.data; const cityObjects = data.CityObjects; for (const objectId in cityObjects) { if (cityObjects.hasOwnProperty(objectId)) { const cityObject = cityObjects[objectId]; parseCityObject(ctx, cityObject, objectId); } } } function parseCityObject(ctx, cityObject, objectId) { const xktModel = ctx.xktModel; const data = ctx.data; const metaObjectId = objectId; const metaObjectType = cityObject.type; const metaObjectName = metaObjectType + " : " + objectId; const parentMetaObjectId = cityObject.parents ? cityObject.parents[0] : ctx.rootMetaObjectId; xktModel.createMetaObject({ metaObjectId, metaObjectName, metaObjectType, parentMetaObjectId }); ctx.stats.numMetaObjects++; if (!(cityObject.geometry && cityObject.geometry.length > 0)) { return; } const meshIds = []; for (let i = 0, len = cityObject.geometry.length; i < len; i++) { const geometry = cityObject.geometry[i]; let objectMaterial; let surfaceMaterials; const appearance = data.appearance; if (appearance) { const materials = appearance.materials; if (materials) { const geometryMaterial = geometry.material; if (geometryMaterial) { const themeIds = Object.keys(geometryMaterial); if (themeIds.length > 0) { const themeId = themeIds[0]; const theme = geometryMaterial[themeId]; if (theme.value !== undefined) { objectMaterial = materials[theme.value]; } else { const values = theme.values; if (values) { surfaceMaterials = []; for (let j = 0, lenj = values.length; j < lenj; j++) { const value = values[i]; const surfaceMaterial = materials[value]; surfaceMaterials.push(surfaceMaterial); } } } } } } } if (surfaceMaterials) { parseGeometrySurfacesWithOwnMaterials(ctx, geometry, surfaceMaterials, meshIds); } else { parseGeometrySurfacesWithSharedMaterial(ctx, geometry, objectMaterial, meshIds); } } if (meshIds.length > 0) { xktModel.createEntity({ entityId: objectId, meshIds: meshIds }); ctx.stats.numObjects++; } } function parseGeometrySurfacesWithOwnMaterials(ctx, geometry, surfaceMaterials, meshIds) { const geomType = geometry.type; switch (geomType) { case "MultiPoint": break; case "MultiLineString": break; case "MultiSurface": case "CompositeSurface": const surfaces = geometry.boundaries; parseSurfacesWithOwnMaterials(ctx, surfaceMaterials, surfaces, meshIds); break; case "Solid": const shells = geometry.boundaries; for (let j = 0; j < shells.length; j++) { const surfaces = shells[j]; parseSurfacesWithOwnMaterials(ctx, surfaceMaterials, surfaces, meshIds); } break; case "MultiSolid": case "CompositeSolid": const solids = geometry.boundaries; for (let j = 0; j < solids.length; j++) { for (let k = 0; k < solids[j].length; k++) { const surfaces = solids[j][k]; parseSurfacesWithOwnMaterials(ctx, surfaceMaterials, surfaces, meshIds); } } break; case "GeometryInstance": break; } } function parseSurfacesWithOwnMaterials(ctx, surfaceMaterials, surfaces, meshIds) { const vertices = ctx.vertices; const xktModel = ctx.xktModel; for (let i = 0; i < surfaces.length; i++) { const surface = surfaces[i]; const surfaceMaterial = surfaceMaterials[i] || {diffuseColor: [0.8, 0.8, 0.8], transparency: 1.0}; const face = []; const holes = []; const sharedIndices = []; const geometryCfg = { positions: [], indices: [] }; for (let j = 0; j < surface.length; j++) { if (face.length > 0) { holes.push(face.length); } const newFace = extractLocalIndices(ctx, surface[j], sharedIndices, geometryCfg); face.push(...newFace); } if (face.length === 3) { // Triangle geometryCfg.indices.push(face[0]); geometryCfg.indices.push(face[1]); geometryCfg.indices.push(face[2]); } else if (face.length > 3) { // Polygon // Prepare to triangulate const pList = []; for (let k = 0; k < face.length; k++) { pList.push({ x: vertices[sharedIndices[face[k]]][0], y: vertices[sharedIndices[face[k]]][1], z: vertices[sharedIndices[face[k]]][2] }); } const normal = getNormalOfPositions(pList, math.vec3()); // Convert to 2D let pv = []; for (let k = 0; k < pList.length; k++) { to2D(pList[k], normal, tempVec2a); pv.unshift(tempVec2a[0]); pv.unshift(tempVec2a[1]); } // Triangulate const tr = earcut(pv, holes, 2); // Create triangles for (let k = 0; k < tr.length; k += 3) { geometryCfg.indices.unshift(face[tr[k]]); geometryCfg.indices.unshift(face[tr[k + 1]]); geometryCfg.indices.unshift(face[tr[k + 2]]); } } const geometryId = "" + ctx.nextId++; const meshId = "" + ctx.nextId++; xktModel.createGeometry({ geometryId: geometryId, primitiveType: "triangles", positions: geometryCfg.positions, indices: geometryCfg.indices }); xktModel.createMesh({ meshId: meshId, geometryId: geometryId, color: (surfaceMaterial && surfaceMaterial.diffuseColor) ? surfaceMaterial.diffuseColor : [0.8, 0.8, 0.8], opacity: 1.0 //opacity: (surfaceMaterial && surfaceMaterial.transparency !== undefined) ? (1.0 - surfaceMaterial.transparency) : 1.0 }); meshIds.push(meshId); ctx.stats.numGeometries++; ctx.stats.numVertices += geometryCfg.positions.length / 3; ctx.stats.numTriangles += geometryCfg.indices.length / 3; } } function parseGeometrySurfacesWithSharedMaterial(ctx, geometry, objectMaterial, meshIds) { const xktModel = ctx.xktModel; const sharedIndices = []; const geometryCfg = { positions: [], indices: [] }; const geomType = geometry.type; switch (geomType) { case "MultiPoint": break; case "MultiLineString": break; case "MultiSurface": case "CompositeSurface": const surfaces = geometry.boundaries; parseSurfacesWithSharedMaterial(ctx, surfaces, sharedIndices, geometryCfg); break; case "Solid": const shells = geometry.boundaries; for (let j = 0; j < shells.length; j++) { const surfaces = shells[j]; parseSurfacesWithSharedMaterial(ctx, surfaces, sharedIndices, geometryCfg); } break; case "MultiSolid": case "CompositeSolid": const solids = geometry.boundaries; for (let j = 0; j < solids.length; j++) { for (let k = 0; k < solids[j].length; k++) { const surfaces = solids[j][k]; parseSurfacesWithSharedMaterial(ctx, surfaces, sharedIndices, geometryCfg); } } break; case "GeometryInstance": break; } const geometryId = "" + ctx.nextId++; const meshId = "" + ctx.nextId++; xktModel.createGeometry({ geometryId: geometryId, primitiveType: "triangles", positions: geometryCfg.positions, indices: geometryCfg.indices }); xktModel.createMesh({ meshId: meshId, geometryId: geometryId, color: (objectMaterial && objectMaterial.diffuseColor) ? objectMaterial.diffuseColor : [0.8, 0.8, 0.8], opacity: 1.0 //opacity: (objectMaterial && objectMaterial.transparency !== undefined) ? (1.0 - objectMaterial.transparency) : 1.0 }); meshIds.push(meshId); ctx.stats.numGeometries++; ctx.stats.numVertices += geometryCfg.positions.length / 3; ctx.stats.numTriangles += geometryCfg.indices.length / 3; } function parseSurfacesWithSharedMaterial(ctx, surfaces, sharedIndices, primitiveCfg) { const vertices = ctx.vertices; for (let i = 0; i < surfaces.length; i++) { let boundary = []; let holes = []; for (let j = 0; j < surfaces[i].length; j++) { if (boundary.length > 0) { holes.push(boundary.length); } const newBoundary = extractLocalIndices(ctx, surfaces[i][j], sharedIndices, primitiveCfg); boundary.push(...newBoundary); } if (boundary.length === 3) { // Triangle primitiveCfg.indices.push(boundary[0]); primitiveCfg.indices.push(boundary[1]); primitiveCfg.indices.push(boundary[2]); } else if (boundary.length > 3) { // Polygon let pList = []; for (let k = 0; k < boundary.length; k++) { pList.push({ x: vertices[sharedIndices[boundary[k]]][0], y: vertices[sharedIndices[boundary[k]]][1], z: vertices[sharedIndices[boundary[k]]][2] }); } const normal = getNormalOfPositions(pList, math.vec3()); let pv = []; for (let k = 0; k < pList.length; k++) { to2D(pList[k], normal, tempVec2a); pv.unshift(tempVec2a[0]); pv.unshift(tempVec2a[1]); } const tr = earcut(pv, holes, 2); for (let k = 0; k < tr.length; k += 3) { primitiveCfg.indices.unshift(boundary[tr[k]]); primitiveCfg.indices.unshift(boundary[tr[k + 1]]); primitiveCfg.indices.unshift(boundary[tr[k + 2]]); } } } } function extractLocalIndices(ctx, boundary, sharedIndices, geometryCfg) { const vertices = ctx.vertices; const newBoundary = [] for (let i = 0, len = boundary.length; i < len; i++) { const index = boundary[i]; if (sharedIndices.includes(index)) { const vertexIndex = sharedIndices.indexOf(index); newBoundary.push(vertexIndex); } else { geometryCfg.positions.push(vertices[index][0]); geometryCfg.positions.push(vertices[index][1]); geometryCfg.positions.push(vertices[index][2]); newBoundary.push(sharedIndices.length); sharedIndices.push(index); } } return newBoundary } function getNormalOfPositions(positions, normal) { for (let i = 0; i < positions.length; i++) { let nexti = i + 1; if (nexti === positions.length) { nexti = 0; } normal[0] += ((positions[i].y - positions[nexti].y) * (positions[i].z + positions[nexti].z)); normal[1] += ((positions[i].z - positions[nexti].z) * (positions[i].x + positions[nexti].x)); normal[2] += ((positions[i].x - positions[nexti].x) * (positions[i].y + positions[nexti].y)); } return math.normalizeVec3(normal); } function to2D(_p, _n, re) { const p = tempVec3a; const n = tempVec3b; const x3 = tempVec3c; p[0] = _p.x; p[1] = _p.y; p[2] = _p.z; n[0] = _n.x; n[1] = _n.y; n[2] = _n.z; x3[0] = 1.1; x3[1] = 1.1; x3[2] = 1.1; const dist = math.lenVec3(math.subVec3(x3, n)); if (dist < 0.01) { x3[0] += 1.0; x3[1] += 2.0; x3[2] += 3.0; } const dot = math.dotVec3(x3, n); const tmp2 = math.mulVec3Scalar(n, dot, math.vec3()); x3[0] -= tmp2[0]; x3[1] -= tmp2[1]; x3[2] -= tmp2[2]; math.normalizeVec3(x3); const y3 = math.cross3Vec3(n, x3, math.vec3()); const x = math.dotVec3(p, x3); const y = math.dotVec3(p, y3); re[0] = x; re[1] = y; } export {parseCityJSONIntoXKTModel};