UNPKG

replate-frontend-viewer

Version:

1,046 lines (904 loc) 36 kB
import * as GaussianSplats3D from "./build/gaussian-splats-3d.module.js" import * as THREE from 'three' import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js" import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js" import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js" class ReplateViewer extends HTMLElement{ static get observedAttributes() { return ['background', 'auto-rotate', 'quality']; } constructor() { super() this.framesThisSecond = 0; this.lastFpsUpdate = 0; this.intervalId = [] this.isDeviceMobile = this.isMobile() this.deviceScore = this.calculateDeviceScore() this.urlParams = new URLSearchParams(window.location.search); this.showingLoader = false this.currentAsset = { url: null, type: null, method: null }; const mode = this.urlParams.get("mode") ? this.urlParams.get("mode") : "view" this.clock = new THREE.Clock(); this.shouldAnimate = false; this.config = { background: "#000000", debug: false, animation: false, animationType: null, mode: mode, sphere: { themeChosen: 0, themes: [{ colors: ["rgba(0, 0, 0, 1)", "rgba(8, 8, 8, 1)", "rgba(0, 0, 0, 1)"], stops: [0, 0.48, 1] }] }, plane: { texture: "https://static.vecteezy.com/system/resources/previews/009/229/194/large_2x/brown-wooden-table-texture-for-background-or-wallpaper-use-free-photo.jpg", width: 4, height: 0.2, depth: 4 }, controls: { enableDamping: true, // Smoothly animate the orbit dampingFactor: 0.040, // Adjust damping factor for smoothness autoRotate: mode == "screenshot" ? false : true, autoRotateSpeed: 2.4, enableZoom: true, // Enable zooming zoomSpeed: 0.3, // Adjust zoom sensitivity maxZoom: 0.5, // Adjust zoom sensitivity minZoom: 1, // Adjust zoom sensitivity enablePan: false, // Enable panning panSpeed: 0.3, rotateSpeed: 1.5, // Adjust rotation sensitivity maxPolarAngle: Math.PI/1, smoothZoom: true, zoomDampingFactor: 10, // Add damping factor for zooming maxDistance: 20, // Set maximum distance the camera can be zoomed in minDistance: 2 // Set maximum distance the camera can be zoomed in*/ }, defaultCameraGlb: { fov: 70, near: 2, far: 4, aspect: this.width / this.height }, defaultCameraSplat: { fov: 70, near: 1, far: 5, aspect: 0.65 }, defaultCameraReplate: { fov: 70, near: 1, far: 20, aspect: 0.65 }, glbParams: { maxSize: null, desiredSize: null, pixelRatio: null, scale: null }, splatParams: { position: null, rotation: null, scale: null, pixelRatio: null }, replateParams: { position: null, rotation: null, scale: null, pixelRatio: null } } this.config.glbParams = this.createGlbParams(this.config.mode) this.config.splatParams = this.createSplatParams(this.config.mode) this.config.replateParams = this.createReplateParams(this.config.mode) if(this.hasAttribute('debug')){ this.config.debug = this.getAttribute("debug") == "true" ? true : false } if(this.hasAttribute("quality")){ changeQuality(this.getAttribute("quality")) } if(this.hasAttribute("background")){ this.config.background = this.getAttribute("background") } if(this.hasAttribute("auto-rotate")){ this.config.controls.autoRotate = this.getAttribute("auto-rotate") } const computedStyle = window.getComputedStyle(this); this.width = parseInt(computedStyle.width); this.height = parseInt(computedStyle.height); if(!this.loader){ this.loader = new GLTFLoader(); this.dracoLoader = new DRACOLoader(); this.dracoLoader.setDecoderPath(`https://raw.githubusercontent.com/google/draco/main/javascript/`); this.loader.setCrossOrigin('anonymous'); this.loader.setDRACOLoader(this.dracoLoader); } this.attachShadow({ mode: 'open' }); this.shadowRoot.innerHTML = ` <style> :host { display: block; width: ${this.width}px; height: ${this.height}px; border: 1px solid black; background-color: ${this.config.background}; } #replate-viewer-debug { width: ${window.getComputedStyle(this).width}; margin: 20px; margin-top: 40px; opacity: 100%; z-index: 9999999; position: absolute; display: block; bottom: ${window.getComputedStyle(this).bottom}; left: ${window.getComputedStyle(this).left}; pointer-events: none; } .replate-loader-container { position: fixed; top: 0; left: 0; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; background-color: rgba(0, 0, 0, 0.7); z-index: 1000; visibility: visible; } .replate-loader { border: 10px solid #3a3b3c; border-top: 10px solid #ffffff; border-radius: 50%; width: 60px; height: 60px; animation: spin 2s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } svg { height: 10%; width: 10%; } </style> <div id="replate-viewer-container"> </div> <div id="replate-loader-container" class="replate-loader-container" style="visibility: hidden;"> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="etR3gyBNQqo1" viewBox="-45 -100 500 400" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" project-id="932b25233e764f728efce6575f02403d" export-id="cf995b158c114759adc8a7562cb9dd78" cached="false"> <style><![CDATA[ #etR3gyBNQqo2_to {animation: etR3gyBNQqo2_to__to 2500ms linear infinite normal forwards}@keyframes etR3gyBNQqo2_to__to { 0% {transform: translate(92.5px,100px);animation-timing-function: cubic-bezier(0.175,0.885,0.32,1.275)} 16.666667% {transform: translate(92.5px,60px);animation-timing-function: cubic-bezier(0.175,0.885,0.32,1.275)} 33.333333% {transform: translate(92.5px,140px);animation-timing-function: cubic-bezier(0.175,0.885,0.32,1.275)} 50% {transform: translate(92.5px,100px);animation-timing-function: cubic-bezier(0.175,0.885,0.32,1.275)} 83.333333% {transform: translate(205px,100px);animation-timing-function: cubic-bezier(0.175,0.885,0.32,1.275)} 100% {transform: translate(92.5px,100px)}} #etR3gyBNQqo3_to {animation: etR3gyBNQqo3_to__to 2500ms linear infinite normal forwards}@keyframes etR3gyBNQqo3_to__to { 0% {transform: translate(317.5px,100px);animation-timing-function: cubic-bezier(0.175,0.885,0.32,1.275)} 16.666667% {transform: translate(317.5px,140px);animation-timing-function: cubic-bezier(0.175,0.885,0.32,1.275)} 33.333333% {transform: translate(317.5px,60px);animation-timing-function: cubic-bezier(0.175,0.885,0.32,1.275)} 50% {transform: translate(317.5px,100px);animation-timing-function: cubic-bezier(0.175,0.885,0.32,1.275)} 83.333333% {transform: translate(205px,100px);animation-timing-function: cubic-bezier(0.175,0.885,0.32,1.275)} 100% {transform: translate(317.5px,100px)}} ]]></style> <g id="etR3gyBNQqo2_to" transform="translate(92.5,100)"><circle r="92.5" transform="translate(0,0)" fill="#fff"/></g><g id="etR3gyBNQqo3_to" transform="translate(317.5,100)"><circle r="92.5" transform="translate(0,0)" fill="#fff"/></g></svg> </div> `; this.replateContainer = this.shadowRoot.getElementById("replate-viewer-container") this.replateLoader = this.shadowRoot.getElementById("replate-loader-container") this.scene = new THREE.Scene(); this.renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true }); this.renderer.setClearColor(this.config.background, 1) this.renderer.setSize(this.width, this.height) this.renderer.xr.enabled = true; this.canvas = this.renderer.domElement; this.canvas.addEventListener('click', (event) => { this.killAnimation() }) this.replateContainer.appendChild(this.renderer.domElement); //this.addSphereBackground() this.lastAnimationFrame = requestAnimationFrame(this.updateFrame); window.addEventListener('resize', this.onWindowResize, false); } changeQuality = (quality) => { console.log("Quality found: " + quality) switch(quality){ case "very-low": this.config.splatParams.pixelRatio = "0.2" this.config.replateParams.pixelRatio = "0.2" break; case "low": this.config.splatParams.pixelRatio = "0.8" this.config.replateParams.pixelRatio = "0.8" break; case "medium": this.config.splatParams.pixelRatio = "1" this.config.replateParams.pixelRatio = "1" break; case "high": this.config.splatParams.pixelRatio = "1.3" this.config.replateParams.pixelRatio = "1.3" break; case "very-high": this.config.splatParams.pixelRatio = "1.7" this.config.replateParams.pixelRatio = "1.7" break; default: this.config.splatParams.pixelRatio = "1.3" this.config.replateParams.pixelRatio = "1.3" break; } } showLoader = () => { console.log("Showing loader...") if(this.config.mode == "viewApp" || this.config.mode == "editorApp") return if(this.showingLoader) return this.showingLoader = true this.replateLoader.style.visibility = 'visible'; } hideLoader = () => { console.log("Hiding loader...") if(this.config.mode == "viewApp" || this.config.mode == "editorApp") return if(this.showingLoader){ setTimeout(() => { this.replateLoader.style.visibility = 'hidden'; this.showingLoader = false }, 500) } } createGlbParams(mode){ if(mode == "screenshot"){ return { maxSize: 13, desiredSize: 4, pixelRatio: 2, scale: [4, 4, 4] } } return { scale: [3, 3, 3], desiredSize: 3.5, pixelRatio: 3, maxSize: 10 } } createSplatParams(mode){ if(mode == "screenshot"){ return { position: [0, 0, 0], rotation: [1, 0, 0, 0], scale: [2.5, 2.5, 2.5], pixelRatio: 0.8, } } if(mode == "editorApp"){ return { position: [0, 0.3, 0], rotation: [1, 0, 0, 0], scale: [1.4, 1.4, 1.4], pixelRatio: window.devicePixelRatio > 2 ? 1.3 : 1 } } return { position: [0, 0, 0], rotation: [1, 0, 0, 0], scale: [2, 2, 2], pixelRatio: window.devicePixelRatio > 2 ? 1.3 : 1 } } createReplateParams(mode){ if(mode == "screenshot"){ return { position: [0, 0.6, 0], rotation: [-1, 0, 0, 0], scale: [3.5, 3.5, 3.5], pixelRatio: 0.8, } } if(mode == "editorApp"){ return { position: [0, 0.6, 0], rotation: [-1, 0, 0, 0], scale: [2.2, 2.2, 2.2], pixelRatio: window.devicePixelRatio > 2 ? 1.3 : 1 } } return { position: [0, 0.6, 0], rotation: [-1, 0, 0, 0], scale: [2, 2, 2], pixelRatio: window.devicePixelRatio > 2 ? 1.3 : 1 } } attributeChangedCallback(name, oldValue, newValue) { console.log(`Changed attribute ${name}, old value: ${oldValue}, new value: ${newValue}`) if (name === 'background') { this.updateBackground(newValue); } else if(name === 'quality'){ this.changeQuality(newValue) } } updateBackground = (val) => { this.config.background = val this.renderer.setClearColor(val, 1) } isMobile = () => { return /mobile|android|kindle|silk|midp|phone/.test(navigator.userAgent.toLowerCase()); } calculateDeviceScore = () => { let score = 0; score += (screen.width * screen.height) / 100000; if (window.navigator.deviceMemory) score += navigator.deviceMemory * 5; if (navigator.hardwareConcurrency) score += navigator.hardwareConcurrency * 3; if (navigator.userAgent.includes('Safari') && !navigator.userAgent.includes('Chrome')) score *= 1.1; return score.toFixed(2); } onWindowResize = () => { try{ const computedStyle = window.getComputedStyle(this); this.width = parseInt(computedStyle.width); this.height = parseInt(computedStyle.height); this.camera.aspect = this.width / this.height; this.camera.updateProjectionMatrix(); this.renderer.setSize(this.width, this.height); }catch(err){ console.log("Camera not yet defined") } } get src() { return this.getAttribute('src'); } set src(newUrl) { this.setAttribute('src', newUrl); if(newUrl == this.currentAsset.url) return this.assetSelection(newUrl) } clearModelsFromScene = () => { if(!this.scene) return for (let i = this.scene.children.length - 1; i >= 0; i--) { let child = this.scene.children[i]; if (child.isGroup == true) { this.scene.remove(child); if (child.geometry) child.geometry.dispose(); if (child.material) { if (Array.isArray(child.material)) { child.material.forEach(material => material.dispose()); } else { child.material.dispose(); } } } else { if(child.name != "SPHERE"){ this.scene.remove(child) } } } } getFieldOfView = () => { return this.camera.fov } getCameraOrbit = () => { this.controls.update(); return { azimuthalAngle: this.controls.getAzimuthalAngle(), polarAngle: this.controls.getPolarAngle() }; } updateRendererExposure = (exposure) => { console.log("Updating exposure...") if (this.renderer) this.renderer.toneMappingExposure = exposure; } addOpacityOnInterval = () => { if (parseFloat(this.replateContainer.style.opacity) <= 1) { this.replateContainer.style.opacity = parseFloat(this.replateContainer.style.opacity) + 0.05; } else { for (const interval of this.intervalId) { clearInterval(interval); } } } updateCameraOrbit = (orbitValue) => { console.log("Updating camera orbit...") const [azimuth, polar, distance] = orbitValue.split(' ').map(v => { return v.endsWith('deg') ? THREE.MathUtils.degToRad(parseFloat(v)) : parseFloat(v); }); const position = new THREE.Vector3(); position.x = distance * Math.sin(polar) * Math.sin(azimuth); position.y = distance * Math.cos(polar); position.z = distance * Math.sin(polar) * Math.cos(azimuth); this.camera.position.copy(position); this.updateCameraTarget(this.getAttribute('camera-target')); // Update to ensure the target is correct after orbit change } updateMaxCameraOrbit = (value) => { const [azimuthStart, polarAngle, azimuthEnd] = value.split(' '); const maxPolar = polarAngle.endsWith('deg') ? THREE.MathUtils.degToRad(parseFloat(polarAngle)) : Math.PI; this.controls.maxPolarAngle = maxPolar; } updateCameraTarget = (targetValue) => { console.log("Updating camera target...") const [x, y, z] = targetValue.split(' ').map(parseFloat); const target = new THREE.Vector3(x, y, z); this.camera.lookAt(target); } triggerEvent = (event) => { this.dispatchEvent(new CustomEvent(event)) window.postMessage(event); if(window.ReactNativeWebView){ window.ReactNativeWebView.postMessage(event) } window.dispatchEvent(new CustomEvent(event)) } addSphereBackground = () => { const sphereGeometry = new THREE.SphereGeometry(15, 2048, 1024); // A large radius to act as the background const canvas = document.createElement('canvas'); canvas.style.top = 0; canvas.width = 8192; // Texture width canvas.height = 8192; // Texture height document.body.appendChild(canvas) const context = canvas.getContext('2d'); const gradientStartY = 0; // Start the gradient a bit lower const gradientEndY = canvas.height; // End at the bottom of the canvas const gradient = context.createLinearGradient(0, gradientStartY, 0, gradientEndY); this.config.sphere.themes[this.config.sphere.themeChosen].colors.forEach((color, index) => { const stop = this.config.sphere.themes[this.config.sphere.themeChosen].stops[index] gradient.addColorStop(stop, color); // Adjusted stop position }); context.fillStyle = gradient; context.fillRect(0, 0, canvas.width, canvas.height); const gradientTexture = new THREE.CanvasTexture(canvas); /*gradientTexture.wrapS = THREE.ClampToEdgeWrapping; gradientTexture.wrapT = THREE.ClampToEdgeWrapping;*/ gradientTexture.needsUpdate = true; gradientTexture.minFilter = THREE.LinearFilter; gradientTexture.magFilter = THREE.LinearFilter; gradientTexture.generateMipmaps = false; const sphereMaterial = new THREE.MeshBasicMaterial({ map: gradientTexture, side: THREE.BackSide }); // Render the inside of the sphere //sphereMaterial.roughness = 0.1 const backgroundSphere = new THREE.Mesh(sphereGeometry, sphereMaterial); backgroundSphere.name = "SPHERE" this.scene.add(backgroundSphere); } addPlaneToScene = () => { this.loader.load("https://frame.replatealpha.it/assets/glb/plane.glb", (gltf) => { const model = gltf.scene; model.position.set(0, 0, 0) this.scene.add(model) }) } loadReplate = () => { console.log("Loading replate model with new viewer...") this.showLoader() //this.scene.fog = new THREE.Fog(0x000000, 1, 4); /*if(this.config.mode == "screenshot"){ this.replateContainer.style.opacity = 1 }else{ this.replateContainer.style.opacity = 0 }*/ this.viewer.addSplatScene(this.currentAsset.url, { 'splatAlphaRemovalThreshold': 1, 'streamView': false, 'showLoadingUI': false, 'progressiveLoad': true, 'position': this.config.replateParams.position, 'rotation': this.config.replateParams.rotation, 'scale': this.config.replateParams.scale }).then(async () => { if(this.config.mode != "screenshot") this.intervalId.push(setInterval(this.addOpacityOnInterval, 10)) this.triggerEvent("viewerLoad") this.hideLoader() this.replateContainer.style.opacity = 1 if(this.config.animation == true) { this.resetAnimation() this.loadAnimation() } }).catch((err) => { console.log(err) if(!this.currentAsset.fallback){ console.log("Loading fallback...") this.currentAsset = { url: `https://models.${process.env.DOMAIN}/nomodel.glb`, type: "glb", method: 0, fallback: true } this.loadGlb() } this.hideLoader() this.triggerEvent("viewerError") }); } loadSplat = () => { console.log("Loading splat model...") this.showLoader() this.scene.fog = new THREE.Fog(0x000000, 1, 4); if(this.config.mode == "screenshot"){ this.replateContainer.style.opacity = 1 }else{ this.replateContainer.style.opacity = 0 } this.viewer.addSplatScene(this.currentAsset.url, { 'splatAlphaRemovalThreshold': 1, 'streamView': false, 'showLoadingUI': false, 'progressiveLoad': true, 'position': this.config.splatParams.position, 'rotation': this.config.splatParams.rotation, 'scale': this.config.splatParams.scale }).then(async () => { if(this.config.mode != "screenshot") this.intervalId.push(setInterval(this.addOpacityOnInterval, 10)) this.triggerEvent("viewerLoad") this.hideLoader() this.replateContainer.style.opacity = 1 if(this.config.animation == true) { this.resetAnimation() this.loadAnimation() } //this.addPlaneToScene() /*const bounds = this.viewer.splatMesh.computeBoundingBox(true); console.log("SCALE") const boxHelper = new THREE.Box3Helper(bounds, 0xff0000); // 0xff0000 is the color (red) this.scene.add(boxHelper);*/ }).catch((err) => { console.log(err) if(!this.currentAsset.fallback){ console.log("Loading fallback...") this.currentAsset = { url: `https://models.${process.env.DOMAIN}/nomodel.glb`, type: "glb", method: 0, fallback: true } this.loadGlb() } this.hideLoader() this.triggerEvent("viewerError") }); } loadGlb = () => { console.log("Loading glb model...") this.showLoader() if(this.config.mode == "screenshot"){ this.replateContainer.style.opacity = 1 }else{ this.replateContainer.style.opacity = 0 } this.loader.load(this.currentAsset.url, (gltf) => { const model = gltf.scene; if(!this.currentAsset.url.includes("nomodel.glb")){ var bbox = new THREE.Box3().setFromObject(model); var cent = bbox.getCenter(new THREE.Vector3()); var size = bbox.getSize(new THREE.Vector3()); // Calculate the maximum axis size var maxAxis = Math.max(size.x, size.y, size.z); var scaleFactor = this.config.glbParams.desiredSize / maxAxis; if (scaleFactor > this.config.glbParams.maxSize) { scaleFactor = this.config.glbParams.maxSize; } model.scale.multiplyScalar(scaleFactor); bbox.setFromObject(model); bbox.getCenter(cent); bbox.getSize(size); model.position.copy(cent).multiplyScalar(-1); const boundingBox = new THREE.Box3().setFromObject(model); const newSize = new THREE.Vector3(); boundingBox.getSize(newSize); }else{ model.scale.set(5, 5, 5); } if(this.config.mode == "editorApp"){ model.position.y += 0.7 } this.scene.add(model); if(this.config.mode != "screenshot") this.intervalId.push(setInterval(this.addOpacityOnInterval, 10)) this.triggerEvent("viewerLoad") this.hideLoader() this.replateContainer.style.opacity = 1 if(this.config.animation == true) { this.resetAnimation() this.loadAnimation() } }, (progress) => { //console.log(progress) }, (err) => { console.log(err) if(!this.currentAsset.fallback){ this.currentAsset = { url: `https://models.${process.env.DOMAIN}/nomodel.glb`, type: "glb", method: 0, fallback: true } this.loadGlb() } this.hideLoader() this.triggerEvent("viewerError") }) } resetAnimationFrame = () => { this.lastAnimationFrame = null; // -> Reset animation frame } setupPixelRatio = () => { if(this.currentAsset.method == 0){ this.renderer.setPixelRatio(this.config.glbParams.pixelRatio); } else if(this.currentAsset.method == 1) { this.renderer.setPixelRatio(this.config.splatParams.pixelRatio); } } disposeSceneAndViewer = () => { if(this.viewer){ this.viewer.dispose() this.viewer = null } this.clearModelsFromScene() } reinitializeRenderer = () => { console.log("Reinitialize renderer...") this.renderer.toneMapping = THREE.ACESFilmicToneMapping; this.renderer.toneMappingExposure = 1; this.renderer.setSize(this.width, this.height); this.setupPixelRatio() } setupGlb = () => { if(this.camera){ this.camera.fov = 20 } this.disposeSceneAndViewer() this.reinitializeCamera() this.reinitializeControls() this.resetAnimationFrame() this.reinitializeRenderer() this.scene.add(new THREE.AmbientLight(0xffffff, 1.5)); this.pointLight = new THREE.PointLight( 0xffffff, 50, 100 ); this.pointLight.position.set( 0, 5, 0 ); this.scene.add( this.pointLight ); this.loadGlb() } setupReplate = () => { if(this.camera){ this.camera.fov = 20 } this.disposeSceneAndViewer() this.reinitializeCamera() this.reinitializeControls() this.resetAnimationFrame() this.reinitializeRenderer() this.viewer = new GaussianSplats3D.Viewer({ 'sharedMemoryForWorkers': false, 'selfDrivenMode': true, 'renderer': this.renderer, 'camera': this.camera, 'threeScene': this.scene, 'useBuiltInControls': false, 'antialiased': true, 'ignoreDevicePixelRatio': false, 'gpuAcceleratedSort': false, 'halfPrecisionCovariancesOnGPU': false, 'sharedMemoryForWorkers': false, 'integerBasedSort': false, 'webXRMode': GaussianSplats3D.WebXRMode.None, 'renderMode': GaussianSplats3D.RenderMode.OnChange, 'sceneRevealMode': GaussianSplats3D.SceneRevealMode.Instant, 'focalAdjustment': 2 }) this.loadReplate() } setupSplat = () => { if(this.camera){ this.camera.fov = 20 } this.disposeSceneAndViewer() this.reinitializeCamera() this.reinitializeControls() this.resetAnimationFrame() this.reinitializeRenderer() this.viewer = new GaussianSplats3D.Viewer({ 'selfDrivenMode': false, 'renderer': this.renderer, 'camera': this.camera, 'threeScene': this.scene, 'useBuiltInControls': false, 'antialiased': true, 'ignoreDevicePixelRatio': false, 'gpuAcceleratedSort': false, 'halfPrecisionCovariancesOnGPU': false, 'sharedMemoryForWorkers': false, 'integerBasedSort': false, 'webXRMode': GaussianSplats3D.WebXRMode.None, 'renderMode': GaussianSplats3D.RenderMode.OnChange, 'sceneRevealMode': GaussianSplats3D.SceneRevealMode.Instant, 'focalAdjustment': 2 }) this.loadSplat() } assetSelection = async (url) => { this.lastAsset = this.currentAsset try{ if(this.lastAsset.method == 1 && this.viewer){ console.log("Deleting old splat from scene...") await this.viewer.removeSplatScene(0, false) } }catch(err){ console.log(err) } if(!url.includes(".splat") && !url.includes(".ply") && !url.includes(".ksplat") && !url.includes(".gltf") && !url.includes(".glb") && !url.includes(".replate")){ if(1 == 1){ this.currentAsset = { url: `https://models.${process.env.DOMAIN}/nomodel.glb`, type: `https://models.${process.env.DOMAIN}/nomodel.glb`.split(".").pop(), method: 0, fallback: true } this.setupGlb() } }else if(url.includes(".splat") || url.includes(".ply") || url.includes(".ksplat")){ this.currentAsset = { url, type: url.split(".").pop(), method: 1 } this.setupSplat() }else if(url.includes(".gltf") || url.includes(".glb")){ this.currentAsset = { url, type: url.split(".").pop(), method: 0 } this.setupGlb() }else if(url.includes(".replate")){ this.currentAsset = { url, type: url.split(".").pop(), method: 2, fallback: true } this.setupReplate() } } reinitializeControls = () => { console.log("Initializing orbit controls...") this.controls = new OrbitControls(this.camera, this.renderer.domElement); this.controls.enableDamping = true; // Smoothly animate the orbit this.controls.dampingFactor = 0.040; // Adjust damping factor for smoothness this.controls.autoRotate = this.config.controls.autoRotate; this.controls.autoRotateSpeed = 1.4; this.controls.enableZoom = true; // Enable zooming this.controls.zoomSpeed = 0.3; // Adjust zoom sensitivity this.controls.maxZoom = 0.5; // Adjust zoom sensitivity this.controls.minZoom = 1; // Adjust zoom sensitivity this.controls.enablePan = false; // Enable panning this.controls.panSpeed = 0.3; this.controls.rotateSpeed = 1.5; // Adjust rotation sensitivity this.controls.maxPolarAngle = Math.PI/2.7; this.controls.smoothZoom = true; this.controls.enableZoom = true; // Enable zooming this.controls.zoomSpeed = 0.4; // Adjust zoom sensitivity this.controls.zoomDampingFactor = 10; // Add damping factor for zooming this.controls.maxDistance = 20; // Set maximum distance the camera can be zoomed in this.controls.minDistance = -20; // Set maximum distance the camera can be zoomed in this.controls.update(); console.log("Appena reinizializzati i controlli mi salvo la posizione finale...") console.log(this.camera.position) console.log("Setting again real camera position...") this.realCameraPosition = this.camera.position } reinitializeCamera = () => { console.log("Initializing camera for splat...") this.camera = new THREE.PerspectiveCamera(16, this.width / this.height, 2, 80); this.camera.lookAt(new THREE.Vector3().fromArray([0, 0, 0])); this.camera.up = new THREE.Vector3().fromArray([0, 1, 0]).normalize(); this.camera.position.copy(new THREE.Vector3().fromArray([0, 1, 20])); } updateFrame = (time) => { try{ this.lastAnimationFrame = requestAnimationFrame(this.updateFrame); this.renderer.render(this.scene, this.camera); this.controls.update(); if(this.config.animation){ this.runAnimation() } if((this.currentAsset.method == 1 || this.currentAsset.method == 2) && this.viewer){ this.viewer.update(); this.viewer.render(); } if(this.debug) this.updateDebugInfo(time) }catch(err){ console.log(err) } } updateDebugInfo = (time) => { if (time > this.lastFpsUpdate + 1000) { // update every second this.debugElement.innerHTML = ` Frames per second: ${this.framesThisSecond}<br> Device Pixel Ratio: ${window.devicePixelRatio}<br> Device Family: ${this.isDeviceMobile}<br> Device Score: ${this.deviceScore}<br> Asset Url: ${this.currentAsset.url}<br> Asset Type: ${this.currentAsset.type}<br> Asset Fallback: ${this.currentAsset.fallback ? this.currentAsset.fallback : false}<br> Camera Position: x=${this.camera.position.x.toFixed(2)}, y=${this.camera.position.y.toFixed(2)}, z=${this.camera.position.z.toFixed(2)}<br> Camera Up Vector: x=${this.camera.up.x.toFixed(2)}, y=${this.camera.up.y.toFixed(2)}, z=${this.camera.up.z.toFixed(2)}<br> Camera FOV: ${this.camera.fov.toFixed(2)} degrees<br> Camera Aspect Ratio: ${this.camera.aspect.toFixed(2)}<br> Camera Near Clip: ${this.camera.near.toFixed(2)}, Camera Far Clip: ${this.camera.far.toFixed(2)}<br> Control Target: x=${this.controls.target.x.toFixed(2)}, y=${this.controls.target.y.toFixed(2)}, z=${this.controls.target.z.toFixed(2)}<br> Zoom Level: ${this.controls.object.zoom.toFixed(2)}<br> Controls Enabled: ${this.controls.enabled}<br> Azimuthal Angle: ${this.controls.getAzimuthalAngle().toFixed(2)} rad, Polar Angle: ${this.controls.getPolarAngle().toFixed(2)} rad<br> Auto Rotate: ${this.controls.autoRotate}, Auto Rotate Speed: ${this.controls.autoRotateSpeed.toFixed(2)}<br> Pan Speed: ${this.controls.panSpeed.toFixed(2)}, Rotate Speed: ${this.controls.rotateSpeed.toFixed(2)}<br> Zoom Speed: ${this.controls.zoomSpeed.toFixed(2)}, Zoom Damping Factor: ${this.controls.zoomDampingFactor}<br> Damping Factor: ${this.controls.enableDamping ? this.controls.dampingFactor.toFixed(2) : "Not enabled"}<br> Max Polar Angle: ${this.controls.maxPolarAngle.toFixed(2)} rad<br>` this.lastFpsUpdate = time; this.framesThisSecond = 0; } this.framesThisSecond++; } resetAnimation = () => { this.clock.start(); // Restart the clock this.clock.elapsedTime = 0; // Reset the elapsed time this.shouldAnimate = true; } runAnimation = () => { //console.log("Running animation...") if(!this.shouldAnimate) return // Se l'animazione è diversa runnare quella scelta, se no vado in default this.animationRender() } loadAnimation = () => { //console.log("Loading animation...") // Se l'animazione è diversa configurare quella scelta, se no vado in default this.animationPreset() } killAnimation = () => { if(this.shouldAnimate){ const { x, y, z } = this.realCameraPosition console.log("Killing animatiom, setting: " + x + " " + y + " " + z) this.controls.autoRotate = this.config.controls.autoRotate; this.controls.update() this.camera.position.copy(new THREE.Vector3().fromArray([x, y, z])); this.shouldAnimate = false } } animationPreset = () => { const { x, y, z } = this.realCameraPosition this.keyFrames = [ { time: 0, position: new THREE.Vector3( 0, y + 12, 0) }, { time: 0.5, position: new THREE.Vector3(0, y + 10, 0) }, { time: 1, position: new THREE.Vector3(0, y + 8, 0) }, { time: 1.5, position: new THREE.Vector3(0, y + 6, 0) }, { time: 2.5, position: new THREE.Vector3(x - 5, y, z) }, { time: 3.5, position: new THREE.Vector3(x, y, z) } ] } animationRender = () => { //console.log("Rendering animation...") let time = this.clock.getElapsedTime(); let currentPos; for (let i = 0; i < (this.keyFrames.length) - 1; i++) { const startKeyframe =this.keyFrames[i]; const endKeyframe = this.keyFrames[i + 1]; if (time >= startKeyframe.time && time <= endKeyframe.time) { const factor = (time - startKeyframe.time) / (endKeyframe.time - startKeyframe.time); currentPos = new THREE.Vector3().lerpVectors(startKeyframe.position, endKeyframe.position, factor); break; } } if(time > this.keyFrames[this.keyFrames.length - 1].time){ console.log("Finita l'animazione, autoRotate a true e si riparte...") this.controls.autoRotate = this.config.controls.autoRotate; this.controls.update() this.shouldAnimate = false } if (!currentPos) { currentPos = this.keyFrames[this.keyFrames.length - 1].position; }else{ this.camera.up = new THREE.Vector3().fromArray([0, 1, 0]).normalize(); this.camera.position.copy(currentPos); this.camera.lookAt(0, 0, 0); } } } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } customElements.define('replate-viewer', ReplateViewer);