replate-frontend-viewer
Version:
1,046 lines (904 loc) • 36 kB
JavaScript
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;
}
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} 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} 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);