UNPKG

vicowa-web-components

Version:
1,085 lines (975 loc) 48.2 kB
/* eslint new-cap: ["off"] */ /* switching this off for this file because babylon uses almost all uppercase starting function names */ import '/third_party/earcut/dist/earcut.dev.js'; import '/third_party/babylonjs/babylon.max.js'; import '/third_party/babylonjs-loaders/babylonjs.loaders.js'; import { WebComponentBaseClass } from '/third_party/web-component-base-class/src/web-component-base-class.js'; import '../vicowa-resize-detector/vicowa-resize-detector.js'; import { CAMERA_TYPES, CAP_TYPES } from './vicowa-webgl-definitions.js'; import { intersectRayAndTriangle, toDegrees, toRadians, vectorLength } from '../utilities/mathHelpers.js'; import debug from '../utilities/debug.js'; const meshContainer = Symbol('meshContainer'); const wrapReference = Symbol('wrapReference'); const originalDispose = Symbol('originalDispose'); function unWrap(wrapped) { return wrapped[meshContainer]; } const babylon = window.BABYLON; let nameID = 0; // validate the name or create a name when none is specified function ensureName(name) { debug.assert(!name || !/--/.test(name), "names containing a double dash '--' are reserved for internal use only"); return (!name) ? `objectID--${++nameID}` : name; } function multiplyObject(obj, multiplier, keysToMultiply) { Object.keys(obj).filter((key) => keysToMultiply.includes(key)).forEach((key) => { obj[key] = (obj[key] || 0) * multiplier; }); return obj; } function multiplyVector(vector, multiplier) { return multiplyObject(vector, multiplier, ['x', 'y', 'z']); } function convertToVectorObject(vector) { return (Array.isArray(vector)) ? { x: vector[0], y: vector[1], z: vector[2] } : vector; } function convertToVector3(vector) { return new babylon.Vector3(vector.x, vector.y, vector.z); } function flattenMeshes(mesh) { return [mesh].concat(mesh.getChildMeshes(false)); } function getMeshObject(controlData, meshNamePathOrObject) { let mesh = null; if (Array.isArray(meshNamePathOrObject)) { // this is a path to the mesh, maybe because we clicked a child mesh mesh = controlData.meshes[meshNamePathOrObject[0]] || controlData.instances[meshNamePathOrObject[0]]; meshNamePathOrObject.slice(1).forEach((name) => { if (mesh) { mesh = mesh.getChildMeshes(true, (childMesh) => childMesh.name === name)[0]; } }); } else if (typeof meshNamePathOrObject === 'string') { mesh = controlData.meshes[meshNamePathOrObject] || controlData.instances[meshNamePathOrObject]; } else { mesh = unWrap(meshNamePathOrObject); } return mesh; } function getTopLevelMeshObject(controlData, meshNamePathOrObject) { let mesh = getMeshObject(controlData, meshNamePathOrObject); while (mesh && controlData.meshes[mesh.name] !== mesh) { mesh = mesh.parent; } return mesh; } function applyRecursive(mesh, propertyName, propertyValue) { mesh[propertyName] = propertyValue; mesh.getChildMeshes(false).forEach((child) => { child[propertyName] = propertyValue; }); } function createShadowGenerator(light) { const shadowGenerator = new babylon.ShadowGenerator(1024, light); shadowGenerator.getShadowMap().refreshRate = babylon.RenderTargetTexture.REFRESHRATE_RENDER_ONCE; shadowGenerator.bias = 0.0001; shadowGenerator.forceBackFacesOnly = true; shadowGenerator.normalBias = 0.02; light.shadowMaxZ = 1000; light.shadowMinZ = -1000; shadowGenerator.useContactHardeningShadow = true; shadowGenerator.contactHardeningLightSizeUVRatio = 0.05; shadowGenerator.setDarkness(0.2); return shadowGenerator; } function addShadowCaster(controlData, meshObject) { Object.keys(controlData.lights).forEach((lightKey) => { const light = controlData.lights[lightKey]; if (light.shadowGenerator) { light.shadowGenerator.addShadowCaster(meshObject); } }); } function applyAllMeshes(meshes, excludedNames, propertyName, propertyValue) { Object.keys(meshes).forEach((key) => { if (!excludedNames.includes(key)) { applyRecursive(meshes[key], propertyName, propertyValue); } }); } function setPosition(mesh, position, unitMultiplier) { Object.assign(mesh.position, multiplyVector(position, unitMultiplier)); } function setRotation(mesh, rotation) { mesh.rotation.x = (rotation.x !== undefined) ? toRadians(rotation.x) : mesh.rotation.x; mesh.rotation.y = (rotation.y !== undefined) ? toRadians(rotation.y) : mesh.rotation.y; mesh.rotation.z = (rotation.z !== undefined) ? toRadians(rotation.z) : mesh.rotation.z; } function setScale(mesh, scale) { Object.assign(mesh.scaling, scale); } function addMaterial(controlData, materialName, settings) { const material = controlData.materials[materialName] || new babylon.StandardMaterial(materialName, controlData.scene); if (settings.diffuse) { Object.assign(material.diffuseColor, settings.diffuse); } if (settings.specular) { Object.assign(material.specularColor, settings.specular); } if (settings.emissive) { Object.assign(material.emissiveColor, settings.emissive); } if (settings.ambient) { Object.assign(material.ambientColor, settings.ambient); } if (settings.alpha !== undefined) { material.alpha = settings.alpha; } if (settings.texture && settings.texture.src) { material.invertNormalMapY = material.invertNormalMapX = settings.texture.invertBump || false; if (settings.texture.src) { material.diffuseTexture = new babylon.Texture(settings.texture.src, controlData.scene); material.diffuseTexture.hasAlpha = settings.texture.alpha || false; } if (settings.texture.bumpSrc) { material.bumpTexture = new babylon.Texture(settings.texture.bumpSrc, controlData.scene); } if (settings.texture.opacitySrc) { material.opacityTexture = new babylon.Texture(settings.texture.opacitySrc, controlData.scene); } if (settings.texture.xScale) { if (material.diffuseTexture) { material.diffuseTexture.uScale = settings.texture.xScale; } if (material.bumpTexture) { material.bumpTexture.uScale = settings.texture.xScale; } if (material.opacityTexture) { material.opacityTexture.uScale = settings.texture.xScale; } } if (settings.texture.yScale) { if (material.diffuseTexture) { material.diffuseTexture.vScale = settings.texture.yScale; } if (material.bumpTexture) { material.bumpTexture.vScale = settings.texture.yScale; } if (material.opacityTexture) { material.opacityTexture.vScale = settings.texture.yScale; } } } controlData.materials[materialName] = material; return material; } function applySettings(controlData, meshObject, settings) { if (meshObject && settings) { if (settings.position) { setPosition(meshObject, settings.position, controlData.multiplier); } if (settings.rotation) { setRotation(meshObject, settings.rotation); } if (settings.scale) { setScale(meshObject, settings.scale); } if (settings.visible !== undefined) { applyRecursive(meshObject, 'isVisible', settings.visible); } if (settings.collisions !== undefined) { applyRecursive(meshObject, 'checkCollisions', settings.collisions); } if (settings.material) { if (typeof settings.material === 'string') { const material = controlData.materials[settings.material]; if (material) { meshObject.material = material; } } else { if (settings.material.name) { const material = addMaterial(controlData, settings.material.name, settings.material); if (material) { meshObject.material = material; } } } } meshObject.renderingGroupId = (settings.renderingGroupId === undefined) ? meshObject.renderingGroupId || 1 : settings.renderingGroupId; meshObject.outline = (settings.outline === undefined) ? meshObject.outline : settings.outline; // do shadows if (settings.shadows !== undefined) { if (settings.shadows) { addShadowCaster(controlData, meshObject); meshObject.receiveShadows = true; } } } return meshObject; } function createMeshInstance(mesh, name) { const instance = mesh.createInstance(name); mesh.getChildren().forEach((child) => instance.addChild(createMeshInstance(child, `${name}-${child.name}`))); return instance; } function createSphere(controlData, settings, name) { return applySettings(controlData, babylon.MeshBuilder.CreateSphere(ensureName(name), { segments: settings.segments || 16, diameter: (settings.diameter || 1) * controlData.multiplier, diameterX: (settings.diameterX || settings.diameter || 1) * controlData.multiplier, diameterY: (settings.diameterY || settings.diameter || 1) * controlData.multiplier, diameterZ: (settings.diameterZ || settings.diameter || 1) * controlData.multiplier, arc: settings.arc || 1, slice: settings.slice || 1, sideOrientation: settings.sideOrientation }, controlData.scene), settings); } function createBox(controlData, settings, name) { return applySettings(controlData, babylon.MeshBuilder.CreateBox(ensureName(name), { width: (settings.width || 1) * controlData.multiplier, height: (settings.height || 1) * controlData.multiplier, depth: (settings.depth || 1) * controlData.multiplier, sideOrientation: settings.sideOrientation }, controlData.scene), settings); } function createPlane(controlData, settings, name) { return applySettings(controlData, babylon.MeshBuilder.CreatePlane(ensureName(name), { width: (settings.width || 1) * controlData.multiplier, height: (settings.height || 1) * controlData.multiplier, sideOrientation: settings.sideOrientation }, controlData.scene), settings); } function createPolyLine(controlData, settings, name) { const linePoints = settings.points.map((point) => new babylon.Vector3(point.x * controlData.multiplier, point.y * controlData.multiplier, point.z * controlData.multiplier)); const polyLine = (linePoints.length > 1) ? babylon.MeshBuilder.CreateLines(ensureName(name), { points: linePoints }, controlData.scene) : null; return (polyLine) ? applySettings(controlData, polyLine, settings) : null; } function createExtrudedPolygon(controlData, settings, name) { const shape = (settings.outline || []).map((point) => convertToVector3(multiplyVector(convertToVectorObject(point), controlData.multiplier))); const holes = (settings.holes || []).map((hole) => hole.map((point) => convertToVector3(multiplyVector(convertToVectorObject(point), controlData.multiplier)))); return (shape.length) ? applySettings(controlData, babylon.MeshBuilder.ExtrudePolygon(ensureName(name), { shape, depth: (settings.depth || 0) * controlData.multiplier, holes, }, controlData.scene), settings) : null; } function createCylinder(controlData, settings, name) { return applySettings(controlData, babylon.MeshBuilder.CreateCylinder(ensureName(name), { height: (settings.height || 1) * controlData.multiplier, diameter: (settings.diameter === undefined) ? settings.diameter : (settings.diameter || 1) * controlData.multiplier, diameterTop: ((settings.diameterTop === undefined) ? (settings.diameter || 1) : settings.diameterTop) * controlData.multiplier, diameterBottom: ((settings.diameterBottom === undefined) ? (settings.diameter || 1) : settings.diameterBottom) * controlData.multiplier, tessellation: settings.segments || 16, arc: settings.arc || 1, enclose: settings.enclose || false }, controlData.scene), settings); } const capConvert = {}; capConvert[CAP_TYPES.NO_CAP] = babylon.Mesh.NO_CAP; capConvert[CAP_TYPES.CAP_START] = babylon.Mesh.CAP_START; capConvert[CAP_TYPES.CAP_END] = babylon.Mesh.CAP_END; capConvert[CAP_TYPES.CAP_ALL] = babylon.Mesh.CAP_ALL; function createTube(controlData, settings, name) { const path = (settings.path || []).map((point) => convertToVector3(multiplyVector(convertToVectorObject(point), controlData.multiplier))); return (path.length) ? applySettings(controlData, babylon.MeshBuilder.CreateTube(ensureName(name), { path, radius: (settings.radius || 1) * controlData.multiplier, tessellation: settings.segments || 16, arc: settings.arc || 1, cap: capConvert[settings.cap] || babylon.Mesh.NO_CAP, }, controlData.scene), settings) : null; } function createGround(controlData, newSettings, name) { const settings = Object.assign({}, newSettings, { shadows: false, collisions: false }); const ground = applySettings(controlData, babylon.MeshBuilder.CreateGround(ensureName(name), { width: (settings.width || 1) * controlData.multiplier, height: (settings.height || 1) * controlData.multiplier, subdivisions: settings.subdivisions || 1 }, controlData.scene), settings); controlData.meshes[ground.name] = ground; ground.receiveShadows = true; ground.checkCollisions = controlData.allObjectCollisions; } function wrap(controlData, mesh) { let wrapped = mesh ? mesh[wrapReference] : null; if (!wrapped && mesh) { wrapped = { get visible() { return this[meshContainer].isVisible; }, set visible(visible) { applyRecursive(this[meshContainer], 'isVisible', visible); }, get parent() { return wrap(controlData, this[meshContainer].parent); }, get center() { const bounding = this[meshContainer].getBoundingInfo(); const center = (bounding && bounding.boundingBox) ? bounding.boundingBox.centerWorld : null; return { x: center.x / controlData.multiplier, y: center.y / controlData.multiplier, z: center.z / controlData.multiplier }; }, get maximum() { const bounding = this[meshContainer].getBoundingInfo(); const maximum = (bounding && bounding.boundingBox) ? bounding.boundingBox.maximumWorld : null; return { x: maximum.x / controlData.multiplier, y: maximum.y / controlData.multiplier, z: maximum.z / controlData.multiplier }; }, get minimum() { const bounding = this[meshContainer].getBoundingInfo(); const minimum = (bounding && bounding.boundingBox) ? bounding.boundingBox.minimumWorld : null; return { x: minimum.x / controlData.multiplier, y: minimum.y / controlData.multiplier, z: minimum.z / controlData.multiplier }; }, get boundingVectors() { const bounding = this[meshContainer].getBoundingInfo(); return (bounding && bounding.boundingBox) ? bounding.boundingBox.vectorsWorld.map((vector) => ({ x: vector.x / controlData.multiplier, y: vector.y / controlData.multiplier, z: vector.z / controlData.multiplier })) : null; }, get name() { return this[meshContainer].name; }, get position() { return { x: this[meshContainer].position.x / controlData.multiplier, y: this[meshContainer].position.y / controlData.multiplier, z: this[meshContainer].position.z / controlData.multiplier }; }, set position(position) { setPosition(this[meshContainer], position, controlData.multiplier); }, get scale() { return { x: this[meshContainer].scaling.x, y: this[meshContainer].scaling.y, z: this[meshContainer].scaling.z }; }, set scale(scaling) { setScale(this[meshContainer], scaling); }, get rotation() { return { x: toDegrees(this[meshContainer].rotation.x), y: toDegrees(this[meshContainer].rotation.y), z: toDegrees(this[meshContainer].rotation.z) }; }, set rotation(rotation) { setRotation(this[meshContainer], rotation); }, clone(name) { return wrap(controlData, this[meshContainer].clone(ensureName(name))); }, updateCoordinates() { this[meshContainer].computeWorldMatrix(true); }, remove() { this[meshContainer].dispose(); }, applySettings(settings) { applySettings(controlData, this[meshContainer], settings); }, offset(offset) { this[meshContainer].position.addInPlace({ x: offset.x * controlData.multiplier, y: offset.y * controlData.multiplier, z: offset.z * controlData.multiplier }); }, isRemoved() { return this[meshContainer] === undefined; }, }; wrapped[meshContainer] = mesh; mesh[wrapReference] = wrapped; mesh[originalDispose] = mesh.dispose; mesh.dispose = function() { if (this[wrapReference].onRemove) { this[wrapReference].onRemove(this[wrapReference]); } delete controlData.meshes[this.name]; delete controlData.instances[this.name]; delete this[wrapReference][meshContainer]; delete this[wrapReference]; this.dispose = this[originalDispose]; delete this[originalDispose]; this.dispose(); }; } return wrapped; } function wrapGroup(controlData, mesh) { const wrapped = wrap(controlData, mesh); wrapped.addChild = function(child) { this[meshContainer].addChild(unWrap(child)); }; wrapped.removeChild = function(child) { this[meshContainer].removeChild(unWrap(child)); }; wrapped.addChildren = function(children) { const tempMesh = this[meshContainer]; children.forEach((child) => tempMesh.addChild(unWrap(child))); }; wrapped.getChildren = function(filter, recursive) { return this[meshContainer].getChildMeshes(!recursive, (childMesh) => !filter || filter(wrap(controlData, childMesh))).map((child) => wrap(controlData, child)); }; wrapped.clone = function(name) { return wrapGroup(controlData, this[meshContainer].clone(ensureName(name))); }; return wrapped; } function wrapEvent(controlData, event) { return { event: event.event, hitObject: (event.pickInfo.hit) ? wrap(controlData, event.pickInfo.pickedMesh) : null, }; } function addPointerDownListener(controlData, callback) { controlData.scene.onPointerObservable.add((event) => { callback(wrapEvent(controlData, event)); }, babylon.PointerEventTypes.POINTERDOWN); } function addPointerUpListener(controlData, callback) { controlData.scene.onPointerObservable.add((event) => { callback(wrapEvent(controlData, event)); }, babylon.PointerEventTypes.POINTERUP); } function addPointerMoveListener(controlData, callback) { controlData.scene.onPointerObservable.add((event) => { callback(wrapEvent(controlData, event)); }, babylon.PointerEventTypes.POINTERMOVE); } function addPointerClickListener(controlData, callback) { controlData.scene.onPointerObservable.add((event) => { callback(wrapEvent(controlData, event)); }, babylon.PointerEventTypes.POINTERPICK); } function screenToObjectPoint(controlData, position, mesh, boundingBoxCheck) { const childMeshes = flattenMeshes(mesh); const pickInfo = controlData.scene.pick(position.x, position.y, (testMesh) => childMeshes.includes(testMesh), boundingBoxCheck || false); return (pickInfo.hit) ? { x: pickInfo.pickedPoint.x / controlData.multiplier, y: pickInfo.pickedPoint.y / controlData.multiplier, z: pickInfo.pickedPoint.z / controlData.multiplier } : null; } function pointToScreenPoint(controlData, point) { return babylon.Vector3.Project(new babylon.Vector3(point.x * controlData.multiplier, point.y * controlData.multiplier, point.z * controlData.multiplier), babylon.Matrix.Identity(), controlData.scene.getTransformMatrix(), controlData.camera.viewport.toGlobal(controlData.engine)); } function screenPointToBoundingProjection(controlData, point, mesh) { const ray = controlData.scene.createPickingRay(point.x, point.y, babylon.Matrix.Identity(), controlData.camera, false); const boxVectors = mesh.getBoundingInfo().boundingBox.vectorsWorld; const triangleIndexes = [[0, 3, 5], [0, 5, 2], [0, 4, 6], [0, 6, 3], [0, 2, 7], [0, 7, 4], [1, 7, 2], [1, 2, 5], [1, 6, 3], [1, 3, 5], [1, 6, 4], [1, 4, 7]]; let distance = -1; let intersection = null; triangleIndexes.forEach((triangleIndexSet) => { const is = intersectRayAndTriangle(ray.origin, ray.direction, boxVectors[triangleIndexSet[0]], boxVectors[triangleIndexSet[1]], boxVectors[triangleIndexSet[2]]); if (is) { const dist = vectorLength(is, ray.origin); if (distance < 0 || distance > dist) { distance = dist; intersection = is; } } }); return (intersection) ? { start: { x: ray.origin.x / controlData.multiplier, y: ray.origin.y / controlData.multiplier, z: ray.origin.z / controlData.multiplier }, intersection: { x: intersection.x / controlData.multiplier, y: intersection.y / controlData.multiplier, z: intersection.z / controlData.multiplier } } : null; } /** * Class to represent the vicowa-webgl custom element * @extends WebComponentBaseClass * @property {boolean} loadingScreen Set to true to show a loading screen or false for no loading screen * @property {boolean} selectionBoundingBox Set to true to show a bounding box when an object is selected or false to not show this box */ export class VicowaWebgl extends WebComponentBaseClass { #privateData; constructor() { super(); this.#privateData = { lights: {}, meshes: {}, instances: {}, materials: {}, allObjectCollisions: true, // by default, all objects will do collision checking defaultShadows: true, // by default all objects cast shadows, this can be changed either per object or as a global setting multiplier: 10, assetsManager: null, scene: null, engine: null, extensions: [], }; } static get properties() { return { loadingScreen: { type: Boolean, value: false, reflectToAttribute: true, observer: (control) => control.#handleLoadingScreenChange(), }, selectionBoundingBox: { type: Boolean, value: false, reflectToAttribute: true, observer: (control) => control.#handleSelectionBoundingBoxChange(), }, }; } addExtension(extensionObject) { const controlData = this.#privateData; if (controlData.extensions.indexOf(extensionObject) === -1) { controlData.extensions.push(extensionObject); // make sure the scene has been created before attaching this if (controlData.scene) { this.#attachExtension(extensionObject); } } } async addObjectResource(name, objectName, fileName, settings) { const controlData = this.#privateData; const meshTask = controlData.assetsManager.addMeshTask(name, objectName, `${fileName.split('/').slice(0, -1).join('/')}/`, fileName.split('/').slice(-1)[0]); return await new Promise((resolve, reject) => { meshTask.onSuccess = (task) => { let newMesh = null; if (!settings.position) { settings.position = { x: 0, y: 0, z: 0 }; } if (task.loadedMeshes.length === 1) { newMesh = this.#addMesh(applySettings(controlData, task.loadedMeshes[0], settings)); } else { const mesh = new babylon.Mesh(name, controlData.scene); task.loadedMeshes.forEach((loadedMesh) => { loadedMesh.renderingGroupId = 1; loadedMesh.addChild(loadedMesh); }); newMesh = this.#addMesh(applySettings(controlData, mesh, settings)); } resolve(wrap(controlData, newMesh)); }; meshTask.onError = (task, message, pException) => { reject({ message, exception: pException }); }; controlData.assetsManager.load(); }); } set unitMultiplier(multiplier) { this.#privateData.multiplier = multiplier; } get unitMultiplier() { return this.#privateData.multiplier; } setBackgroundColor(red, green, blue) { this.#privateData.scene.clearColor = new babylon.Color3(red, green, blue); } setAmbientColor(red, green, blue) { this.#privateData.scene.ambientColor = new babylon.Color3(red, green, blue); } createSkyBox(skyBoxImageDirectory) { const controlData = this.#privateData; // using the "old" way of creating a sky box, because the helper function makes it very very slow const skyBox = babylon.MeshBuilder.CreateBox('skyBox', { size: 10000.0, sideOrientation: babylon.Mesh.BACKSIDE }, controlData.scene); const skyBoxMaterial = new babylon.StandardMaterial('skyBox', controlData.scene); skyBoxMaterial.backFaceCulling = true; skyBoxMaterial.reflectionTexture = new babylon.CubeTexture(skyBoxImageDirectory, controlData.scene); skyBoxMaterial.reflectionTexture.coordinatesMode = babylon.Texture.SKYBOX_MODE; skyBoxMaterial.diffuseColor = new babylon.Color3(0, 0, 0); skyBoxMaterial.specularColor = new babylon.Color3(0, 0, 0); skyBoxMaterial.disableLighting = true; skyBox.material = skyBoxMaterial; skyBox.infiniteDistance = true; skyBox.renderingGroupId = 0; } // basic objects addSphere(settings, name) { return wrap(this.#privateData, this.#addMesh(createSphere(this.#privateData, this.#addDefaultSettings(settings), name))); } addBox(settings, name) { return wrap(this.#privateData, this.#addMesh(createBox(this.#privateData, this.#addDefaultSettings(settings), name))); } addPlane(settings, name) { return wrap(this.#privateData, this.#addMesh(createPlane(this.#privateData, this.#addDefaultSettings(settings), name))); } addPolyLine(settings, name) { return wrap(this.#privateData, this.#addMesh(createPolyLine(this.#privateData, this.#addDefaultSettings(settings), name))); } addExtrudedPolygon(settings, name) { return wrap(this.#privateData, this.#addMesh(createExtrudedPolygon(this.#privateData, this.#addDefaultSettings(settings), name))); } addCylinder(settings, name) { return wrap(this.#privateData, this.#addMesh(createCylinder(this.#privateData, this.#addDefaultSettings(settings), name))); } addTube(settings, name) { return wrap(this.#privateData, this.#addMesh(createTube(this.#privateData, this.#addDefaultSettings(settings), name))); } // special objects addGround(settings, name) { return wrap(this.#privateData, createGround(this.#privateData, settings, name)); } // retrieval getObjectByNameOrPath(objectNameOrPath) { return wrap(this.#privateData, getMeshObject(this.#privateData, objectNameOrPath)); } createObjectInstance(objectNamePrefix, sourceName, copies, settings) { const controlData = this.#privateData; const mesh = controlData.meshes[sourceName]; if (mesh) { copies = copies || 1; for (let index = 0; index < copies; index++) { const cloneName = objectNamePrefix + index; this.#addInstance(cloneName, createMeshInstance(mesh.createInstance, cloneName), settings); } } return (mesh) ? wrap(controlData, mesh) : null; } startRendering() { const controlData = this.#privateData; controlData.engine.runRenderLoop(() => { controlData.scene.render(); }); } stopRendering() { this.#privateData.engine.stopRenderLoop(); } addEnvironmentalLight(settings, name) { const controlData = this.#privateData; const light = new babylon.HemisphericLight(ensureName(name), new babylon.Vector3(settings.x || 0, settings.y || 0, settings.z || 0), controlData.scene); controlData.lights[light.name] = { light }; this.setEnvironmentalLightColor(settings.color.ground, light.name); this.setLightColors(settings.color, light.name); } setEnvironmentalLightColor(color, name) { const light = this.#privateData.lights[name]; if (light && light instanceof babylon.HemisphericLight) { Object.assign(light.light.groundColor, color); } } addDirectionalLight(settings, name) { const controlData = this.#privateData; const light = new babylon.DirectionalLight(ensureName(name), new babylon.Vector3(settings.x || 0, settings.y || 0, settings.z || 0), controlData.scene); controlData.lights[light.name] = { light }; this.setLightColors(settings.color, light.name); if (settings.generateShadows) { controlData.lights[light.name].shadowGenerator = createShadowGenerator(light); } } addPointLight(settings, name) { const controlData = this.#privateData; const light = new babylon.PointLight(ensureName(name), new babylon.Vector3(settings.x * this.unitMultiplier || 0, settings.y * this.unitMultiplier || 0, settings.z * this.unitMultiplier || 0), controlData.scene); controlData.lights[light.name] = { light }; this.setLightColors(settings.color, light.name); if (settings.generateShadows) { controlData.lights[light.name].shadowGenerator = createShadowGenerator(light); } } addSpotLight(settings, name) { const controlData = this.#privateData; const light = new babylon.SpotLight(ensureName(name), new babylon.Vector3(settings.x * this.unitMultiplier || 0, settings.y * this.unitMultiplier || 0, settings.z * this.unitMultiplier || 0), new babylon.Vector3(settings.direction.x || 0, settings.direction.y || 0, settings.direction.z || 0), settings.angle || 0, (settings.reach || 100) * this.unitMultiplier, controlData.scene); controlData.lights[light.name] = { light }; this.setLightColors(settings.color, light.name); if (settings.generateShadows) { controlData.lights[light.name].shadowGenerator = createShadowGenerator(light); } } removeLight(name) { const controlData = this.#privateData; controlData.lights[name].light.dispose(); delete controlData.lights[name]; } setLightColors(colors, name) { const light = this.#privateData.lights[name]; if (light) { if (colors.diffuse) { Object.assign(light.light.diffuse, colors.diffuse); } if (colors.specular) { Object.assign(light.light.specular, colors.specular); } } } setObjectPosition(object, position) { const mesh = getMeshObject(this.#privateData, object); if (mesh) { setPosition(mesh, position, this.unitMultiplier); } } setObjectRotation(object, rotation) { const mesh = getMeshObject(this.#privateData, object); if (mesh) { setRotation(mesh, rotation); } } setObjectScale(object, scale) { const mesh = getMeshObject(this.#privateData, object); if (mesh) { setScale(mesh, scale); } } setCameraTarget(object) { const controlData = this.#privateData; const mesh = getMeshObject(controlData, object); if (mesh) { controlData.camera.lockedTarget = mesh; } } addMaterial(materialName, settings) { return addMaterial(this.#privateData, materialName, settings); } setObjectMaterial(objects, objMaterial) { const controlData = this.#privateData; if (typeof objMaterial === 'string') { const meshNames = Array.isArray(objects) ? objects : [objects]; meshNames.forEach((meshName) => { const mesh = getMeshObject(controlData, meshName); const material = controlData.materials[objMaterial]; if (mesh && material) { mesh.material = material; } }); } } setGravity(settings) { Object.assign(this.#privateData.scene.gravity, settings); } set defaultShadows(shadows) { this.#privateData.defaultShadows = shadows; } get defaultShadows() { return this.#privateData.defaultShadows; } set cameraGravity(gravity) { this.#privateData.camera.applyGravity = gravity; } get cameraGravity() { return this.#privateData.camera.applyGravity; } set cameraCollisions(collisions) { this.#privateData.camera.checkCollisions = collisions; } get cameraCollisions() { return this.#privateData.camera.checkCollisions; } setObjectVisibility(object, visible) { const mesh = getMeshObject(this.#privateData, object); if (mesh) { applyRecursive(mesh, 'isVisible', visible || false); } } isObjectVisible(obj) { const controlData = this.#privateData; const mesh = getMeshObject(controlData, obj); return (mesh) ? mesh.isVisible : undefined; } setVirtualBody(settings) { const controlData = this.#privateData; if (controlData.camera) { if (controlData.camera instanceof babylon.ArcRotateCamera) { Object.assign(controlData.camera.collisionRadius, multiplyVector(settings.bodySize || {}, this.unitMultiplier)); } else { Object.assign(controlData.camera.ellipsoid, multiplyVector(settings.bodySize || {}, this.unitMultiplier)); Object.assign(controlData.camera.ellipsoidOffset, multiplyVector(settings.eyeOffset || {}, this.unitMultiplier)); } } else { throw new Error('make sure to set a camera before calling this function'); } } enableAllObjectCollisions(excluded) { const controlData = this.#privateData; controlData.allObjectCollisions = true; applyAllMeshes(controlData.meshes, excluded || [], 'checkCollisions', true); applyAllMeshes(controlData.instances, excluded || [], 'checkCollisions', true); } disableAllObjectCollisions(excluded) { const controlData = this.#privateData; controlData.allObjectCollisions = false; applyAllMeshes(controlData.meshes, excluded || [], 'checkCollisions', false); applyAllMeshes(controlData.instances, excluded || [], 'checkCollisions', false); } setCheckCollisionForObject(obj, enabled) { const controlData = this.#privateData; const mesh = getMeshObject(controlData, obj); if (mesh) { applyRecursive(mesh, 'checkCollisions', enabled); } } getCheckCollisionForObject(object) { const controlData = this.#privateData; const mesh = getMeshObject(controlData, object); return (mesh) ? mesh.checkCollisions : undefined; } setFog(enabled, settings) { const controlData = this.#privateData; // fog controlData.scene.fogEnabled = enabled; controlData.scene.fogMode = babylon.Scene.FOGMODE_EXP; if (settings.density) { controlData.scene.fogDensity = settings.density; } if (settings.color) { controlData.scene.fogColor = new babylon.Color3(settings.color.r, settings.color.g, settings.color.b); } } /** * Set the camera to use * @param {CAMERA_TYPES} type The type of camera to create * @param {object} settings The settings for creating the camera */ setCamera(type, settings) { const controlData = this.#privateData; controlData.preventDefault = settings.preventDefault; settings = settings || {}; if (controlData.camera) { controlData.camera.dispose(); } switch (type) { case CAMERA_TYPES.ORBITAL: { // if no positions are specified, the camera will be positioned at a distance of 10 a longitude of 0 and a latitude of 45 degrees and point at 0, 0, 0 const position = Object.assign({}, { longitude: 0, latitude: 45, distance: 10 }, settings.position); const target = Object.assign({}, { x: 0, y: 0, z: 0 }, settings.target || {}); controlData.camera = new babylon[(settings.vrEnabled) ? 'VRDeviceOrientationArcRotateCamera' : 'ArcRotateCamera']('camera', toRadians(position.longitude), toRadians(position.latitude), position.distance * this.unitMultiplier, new babylon.Vector3(target.x * this.unitMultiplier, target.y * this.unitMultiplier, target.z * this.unitMultiplier), controlData.scene); controlData.camera.attachControl(this.$.canvas, !(controlData.preventDefault || false)); if (settings.minLongitude) { controlData.camera.lowerAlphaLimit = toRadians(settings.minLongitude); } if (settings.maxLongitude) { controlData.camera.upperAlphaLimit = toRadians(settings.maxLongitude); } if (settings.minLatitude) { controlData.camera.lowerBetaLimit = toRadians(settings.minLatitude); } if (settings.maxLatitude) { controlData.camera.upperBetaLimit = toRadians(settings.maxLatitude); } if (settings.minDistance) { controlData.camera.lowerRadiusLimit = settings.minDistance * this.unitMultiplier; } if (settings.maxDistance) { controlData.camera.upperRadiusLimit = settings.maxDistance * this.unitMultiplier; } if (settings.targetMesh) { controlData.camera.lockedTarget = settings.targetMesh; } break; } case CAMERA_TYPES.FREE: { // if no positions are specified, the camera will be positioned at 0 0 -10 and will be pointing at 0, 0, 0 const position = Object.assign({}, { x: 0, y: 0, z: -10 }, settings.position || {}); const target = Object.assign({}, { x: 0, y: 0, z: 0 }, settings.target || {}); controlData.camera = new babylon[(settings.vrEnabled) ? ((settings.mobile) ? 'VRDeviceOrientationFreeCamera' : 'WebVRFreeCamera') : 'UniversalCamera']('camera', new babylon.Vector3(position.x * this.unitMultiplier, position.y * this.unitMultiplier, position.z * this.unitMultiplier), controlData.scene, false); controlData.camera.attachControl(this.$.canvas, !(controlData.preventDefault || false)); controlData.camera.setTarget(new babylon.Vector3(target.x * this.unitMultiplier, target.y * this.unitMultiplier, target.z * this.unitMultiplier)); break; } case CAMERA_TYPES.FOLLOW: { const position = Object.assign({}, { x: 0, y: 0, z: -10 }, settings.position || {}); // if no positions are specified, the camera will be positioned at 0 0 -10 and will be pointing at 0, 0, 0 controlData.camera = new babylon.FollowCamera('camera', new babylon.Vector3(position.x * this.unitMultiplier, position.y * this.unitMultiplier, position.z * this.unitMultiplier), controlData.scene); controlData.camera.attachControl(this.$.canvas, !(controlData.preventDefault || false)); if (settings.targetMesh) { controlData.camera.lockedTarget = settings.targetMesh; } const follow = Object.assign({}, { radius: controlData.camera.radius, heightAbove: controlData.camera.heightOffset, rotation: controlData.camera.rotationOffset, acceleration: controlData.camera.cameraAcceleration, maxSpeed: controlData.camera.maxCameraSpeed, }, multiplyObject(settings.follow, this.unitMultiplier, ['radius', 'heightAbove'])); controlData.camera.radius = follow.radius; controlData.camera.heightOffset = follow.heightAbove; controlData.camera.rotationOffset = toRadians(follow.rotation); controlData.camera.cameraAcceleration = follow.acceleration; controlData.camera.maxCameraSpeed = follow.maxSpeed; break; } default: throw new Error(`Unknown camera type ${type}`); } } removeObject(obj) { const mesh = getMeshObject(this.#privateData, obj); if (mesh) { mesh.dispose(); } } unGroupObject(obj) { const controlData = this.#privateData; const mesh = (typeof obj === 'string') ? controlData.meshes[obj] : unWrap(obj); const newObjects = []; const childMeshes = (mesh) ? mesh.getChildMeshes(true) : []; childMeshes.forEach((childMesh) => { mesh.removeChild(childMesh); newObjects.push(childMesh.name); controlData.meshes[childMesh.name] = childMesh; }); return newObjects; } groupObjects(objectNames, newName) { const controlData = this.#privateData; const meshes = []; let newMesh = null; const refreshBoundingInfo = (mesh) => { const children = mesh.getChildren(); let boundingInfo = children[0].getBoundingInfo(); let min = boundingInfo.boundingBox.minimumWorld; let max = boundingInfo.boundingBox.maximumWorld; for (let index = 1; index < children.length; index++) { boundingInfo = children[index].getBoundingInfo(); min = babylon.Vector3.Minimize(min, boundingInfo.boundingBox.minimumWorld); max = babylon.Vector3.Maximize(max, boundingInfo.boundingBox.maximumWorld); } mesh.setBoundingInfo(new babylon.BoundingInfo(min, max)); }; objectNames.forEach((name) => { const mesh = controlData.meshes[name]; if (mesh) { meshes.push(mesh); } }); if (meshes.length) { newMesh = new babylon.Mesh(newName, controlData.scene); meshes.forEach((newChild) => { newMesh.addChild(newChild); delete controlData.meshes[newChild.name]; }); controlData.meshes[newName] = newMesh; newMesh.computeWorldMatrix(true); refreshBoundingInfo(newMesh); } return (newMesh) ? newMesh.name : null; } selectObject(obj) { const controlData = this.#privateData; const mesh = getTopLevelMeshObject(controlData, obj); if (mesh) { const allMeshes = flattenMeshes(mesh); allMeshes.forEach((someMesh) => { someMesh.selected = true; someMesh.showBoundingBox = this.selectionBoundingBox; someMesh.notSelectMaterial = someMesh.material; if (someMesh.material && someMesh.material.hasOwnProperty('diffuseColor')) { someMesh.material = someMesh.material.clone('temp'); someMesh.material.diffuseColor = controlData.materials['selected'].diffuseColor; someMesh.material.alpha = Math.max(0.5, someMesh.material.alpha); } else { someMesh.material = controlData.materials['selected'].clone(); } }); } } unSelectObject(objectName) { const controlData = this.#privateData; const mesh = getTopLevelMeshObject(controlData, objectName); if (mesh) { const allMeshes = flattenMeshes(mesh); allMeshes.forEach((someMesh) => { if (someMesh.selected) { someMesh.selected = false; someMesh.showBoundingBox = false; someMesh.material.dispose(); someMesh.material = someMesh.notSelectMaterial; delete someMesh.notSelectMaterial; } }); } } getSelectedObjects() { const controlData = this.#privateData; return Object.keys(controlData.meshes).filter((key) => getMeshObject(controlData, key).selected).concat(Object.keys(controlData.instances).filter((key) => getMeshObject(controlData, key).selected)); } unSelectAll() { const controlData = this.#privateData; Object.keys(controlData.meshes).forEach((key) => this.unSelectObject(key)); Object.keys(controlData.instances).forEach((key) => this.unSelectObject(key)); } isObjectSelected(objectName) { const mesh = getMeshObject(this.#privateData, objectName); return mesh && mesh.selected; } set selectColor(color) { Object.assign(this.#privateData.materials['selected'].diffuseColor, color); } get selectColor() { const color = (this.#privateData.materials['selected'] || {}).diffuseColor; return { r: color.r || 1, g: color.g || 0, b: color.b || 0 }; } getDataUrl(type, quality) { return this.$.canvas.toDataURL(type || 'image/png', quality || undefined); } getBlob(callback, type, quality) { this.$.canvas.toBlob(callback, type || 'image/png', quality || undefined); } attached() { const controlData = this.#privateData; // create the engine controlData.engine = new babylon.Engine(this.$.canvas, true, { preserveDrawingBuffer: true, stencil: true }); const createScene = () => { // Create a Scene object controlData.scene = new babylon.Scene(controlData.engine); controlData.scene.collisionsEnabled = true; controlData.scene.workerCollisions = true; // use web workers for collisions this.addMaterial('selected', { diffuse: { r: 1, g: 0, b: 0 } }); controlData.assetsManager = new babylon.AssetsManager(controlData.scene); controlData.assetsManager.useDefaultLoadingScreen = this.loadingScreen; const options = new babylon.SceneOptimizerOptions(); options.addOptimization(new babylon.HardwareScalingOptimization(0, 1)); // Optimizer this.optimizer = new babylon.SceneOptimizer(controlData.scene, options); controlData.assetsManager.onFinish = () => { this.stopRendering(); this.startRendering(); }; }; createScene(); controlData.scene.onPointerObservable.add((event) => { if (event.pickInfo.hit) { if (this.onObjectClicked) { const path = [event.pickInfo.pickedMesh.name]; let parent = event.pickInfo.pickedMesh.parent; while (parent) { path.unshift(parent.name); parent = parent.parent; } this.onObjectClicked({ distance: event.pickInfo.distance / this.unitMultiplier, object: event.pickInfo.pickedMesh.name, mainObject: path[0], parent: path[path.length - 2] || null, path, location: { x: event.pickInfo.pickedPoint.x / this.unitMultiplier, y: event.pickInfo.pickedPoint.y / this.unitMultiplier, z: event.pickInfo.pickedPoint.z / this.unitMultiplier }, }); } } }, babylon.PointerEventTypes.POINTERPICK); const resizeDetector = this.$.resizeDetector; resizeDetector.addObserver(() => { controlData.engine.resize(); }, this); // attach all extensions now controlData.extensions.forEach((extensionObject) => this.#attachExtension(extensionObject)); } detached() { const controlData = this.#privateData; this.stopRendering(); controlData.scene.dispose(); controlData.engine.dispose(); controlData.lights = {}; controlData.meshes = {}; controlData.instances = {}; controlData.materials = {}; controlData.assetsManager = null; controlData.engine = null; controlData.scene = null; controlData.extensions = []; } #handleLoadingScreenChange() { const controlData = this.#privateData; if (controlData.assetsManager) { controlData.assetsManager.useDefaultLoadingScreen = this.loadingScreen; } } #handleSelectionBoundingBoxChange() { const controlData = this.#privateData; const updateBoundingBox = (key) => { const mesh = getMeshObject(controlData, key); if (mesh.selected) { mesh.showBoundingBox = this.selectionBoundingBox; } }; Object.keys(controlData.meshes).forEach(updateBoundingBox); Object.keys(controlData.instances).forEach(updateBoundingBox); } #attachExtension(extensionObject) { const controlData = this.#privateData; extensionObject.attach(this, { // get a mesh object from the main store getObject(objectNameOrPath) { return wrap(controlData, getTopLevelMeshObject(controlData, objectNameOrPath)); }, // object creation createGroup(name) { return wrapGroup(controlData, new babylon.Mesh(ensureName(name), controlData.scene)); }, createSphere(settings, name) { return wrap(controlData, createSphere(controlData, settings, name)); }, createBox(settings, name) { return wrap(controlData, createBox(controlData, settings, name)); }, createPlane(settings, name) { return wrap(controlData, createPlane(controlData, settings, name)); }, createPolyLine(settings, name) { return wrap(controlData, createPolyLine(controlData, settings, name)); }, createExtrudedPolygon(settings, name) { return wrap(controlData, createExtrudedPolygon(controlData, settings, name)); }, createCylinder(settings, name) { return wrap(controlData, createCylinder(controlData, settings, name)); }, createTube(settings, name) { return wrap(controlData, createTube(controlData, settings, name)); }, // settings applySettings(settings, obj) { applySettings(controlData, unWrap(obj), settings); }, // event handling addPointerDownListener(callback) { addPointerDownListener(controlData, callback); }, addPointerUpListener(callback) { addPointerUpListener(controlData, callback); }, addPointerMoveListener(callback) { addPointerMoveListener(controlData, callback); }, addPointerClickListener(callback) { addPointerClickListener(controlData, callback); }, // utility screenToObjectPoint(screenPoint, wrapped) { return screenToObjectPoint(controlData, screenPoint, unWrap(wrapped)); }, pointToScreenPoint(point) { const tmpPoint = pointToScreenPoint(controlData, point); return { x: tmpPoint.x, y: tmpPoint.y }; }, screenPointToBoundingProjection(screenPoint, wrapped) { return screenPointToBoundingProjection(controlData, screenPoint, unWrap(wrapped)); }, get pointerPos() { return { x: controlData.scene.pointerX, y: controlData.scene.pointerY }; }, // camera detachCameraControl: () => { controlData.camera.detachControl(this.$.canvas); }, attachCameraControl: () => { controlData.camera.attachControl(this.$.canvas, !(controlData.preventDefault || false)); }, }); } #addDefaultSettings(settings) { const controlData = this.#privateData; return Object.assign({ collisions: controlData.allObjectCollisions, shadows: this.defaultShadows, }, settings); } #addMesh(meshObject) { if (meshObject) { const controlData = this.#privateData; controlData.meshes[meshObject.name] = meshObject; Object.keys(controlData.lights).forEach((key) => { if (controlData.lights[key].shadowGenerator) { controlData.lights[key].shadowGenerator.recreateShadowMap(); } }); } return meshObject; } #addInstance(name, meshObject, settings) { const controlData = this.#privateData; settings = settings || {}; if (meshObject) { if (settings.position) { setPosition(meshObject, settings.position, this.unitMultiplier); } if (settings.rotation) { setRotation(meshObject, settings.rotation); } if (settings.scale) { setScale(meshObject, settings.scale); } if (settings.visible !== undefined) { meshObject.isVisible = settings.visible; } // do shadows either if explicit shadows option is set to true in the settings or when defaultShadows is set to true and the shadows property is undefined if (settings.shadows || (this.defaultShadows && settings.shadows === undefined)) { addShadowCaster(controlData, meshObject); } applyRecursive(meshObject, 'checkCollisions', (settings.collisions === undefined) ? controlData.allObjectCollisions : settings.collisions); controlData.instances[name] = meshObject; } } static get template() { return ` <script src="/third_party/earcut/dist/earcut.dev.js"></script> <script src="/third_party/babylonjs/babylon.max.js"></script> <script src="/third_party/babylonjs-loaders/babylonjs.loaders.js"></script> <style> :host { display: block; position: relative; } #main, #canvas { position: relative; width: 100%; height: 100%; } </style> <div id="main"> <vicowa-resize-detector id="resize-detector"></vicowa-resize-detector> <canvas id="canvas"></canvas> </div> `; } } window.customElements.define('vicowa-webgl', VicowaWebgl);