UNPKG

@sauskylark/potree

Version:

WebGL point cloud viewer

491 lines (387 loc) 13.6 kB
import * as THREE from "../../../libs/three.js/build/three.module.js"; import {OrientedImageControls} from "./OrientedImageControls.js"; import { EventDispatcher } from "../../EventDispatcher.js"; // https://support.pix4d.com/hc/en-us/articles/205675256-How-are-yaw-pitch-roll-defined // https://support.pix4d.com/hc/en-us/articles/202558969-How-are-omega-phi-kappa-defined function createMaterial(){ let vertexShader = ` uniform float uNear; varying vec2 vUV; varying vec4 vDebug; void main(){ vDebug = vec4(0.0, 1.0, 0.0, 1.0); vec4 modelViewPosition = modelViewMatrix * vec4(position, 1.0); // make sure that this mesh is at least in front of the near plane modelViewPosition.xyz += normalize(modelViewPosition.xyz) * uNear; gl_Position = projectionMatrix * modelViewPosition; vUV = uv; } `; let fragmentShader = ` uniform sampler2D tColor; uniform float uOpacity; varying vec2 vUV; varying vec4 vDebug; void main(){ vec4 color = texture2D(tColor, vUV); gl_FragColor = color; gl_FragColor.a = uOpacity; } `; const material = new THREE.ShaderMaterial( { uniforms: { // time: { value: 1.0 }, // resolution: { value: new THREE.Vector2() } tColor: {value: new THREE.Texture() }, uNear: {value: 0.0}, uOpacity: {value: 1.0}, }, vertexShader: vertexShader, fragmentShader: fragmentShader, side: THREE.DoubleSide, } ); material.side = THREE.DoubleSide; return material; } const planeGeometry = new THREE.PlaneGeometry(1, 1); const lineGeometry = new THREE.Geometry(); lineGeometry.vertices.push( new THREE.Vector3(-0.5, -0.5, 0), new THREE.Vector3( 0.5, -0.5, 0), new THREE.Vector3( 0.5, 0.5, 0), new THREE.Vector3(-0.5, 0.5, 0), new THREE.Vector3(-0.5, -0.5, 0), ); export class OrientedImage{ constructor(id){ this.id = id; this.fov = 1.0; this.position = new THREE.Vector3(); this.rotation = new THREE.Vector3(); this.width = 0; this.height = 0; this.fov = 1.0; const material = createMaterial(); const lineMaterial = new THREE.LineBasicMaterial( { color: 0x00ff00 } ); this.mesh = new THREE.Mesh(planeGeometry, material); this.line = new THREE.Line(lineGeometry, lineMaterial); this.texture = null; this.mesh.orientedImage = this; } set(position, rotation, dimension, fov){ let radians = rotation.map(THREE.Math.degToRad); this.position.set(...position); this.mesh.position.set(...position); this.rotation.set(...radians); this.mesh.rotation.set(...radians); [this.width, this.height] = dimension; this.mesh.scale.set(this.width / this.height, 1, 1); this.fov = fov; this.updateTransform(); } updateTransform(){ let {mesh, line, fov} = this; mesh.updateMatrixWorld(); const dir = mesh.getWorldDirection(); const alpha = THREE.Math.degToRad(fov / 2); const d = -0.5 / Math.tan(alpha); const move = dir.clone().multiplyScalar(d); mesh.position.add(move); line.position.copy(mesh.position); line.scale.copy(mesh.scale); line.rotation.copy(mesh.rotation); } }; export class OrientedImages extends EventDispatcher{ constructor(){ super(); this.node = null; this.cameraParams = null; this.imageParams = null; this.images = null; this._visible = true; } set visible(visible){ if(this._visible === visible){ return; } for(const image of this.images){ image.mesh.visible = visible; image.line.visible = visible; } this._visible = visible; this.dispatchEvent({ type: "visibility_changed", images: this, }); } get visible(){ return this._visible; } }; export class OrientedImageLoader{ static async loadCameraParams(path){ const res = await fetch(path); const text = await res.text(); const parser = new DOMParser(); const doc = parser.parseFromString(text, "application/xml"); const width = parseInt(doc.getElementsByTagName("width")[0].textContent); const height = parseInt(doc.getElementsByTagName("height")[0].textContent); const f = parseFloat(doc.getElementsByTagName("f")[0].textContent); let a = (height / 2) / f; let fov = 2 * THREE.Math.radToDeg(Math.atan(a)); const params = { path: path, width: width, height: height, f: f, fov: fov, }; return params; } static async loadImageParams(path){ const response = await fetch(path); if(!response.ok){ console.error(`failed to load ${path}`); return; } const content = await response.text(); const lines = content.split(/\r?\n/); const imageParams = []; for(let i = 1; i < lines.length; i++){ const line = lines[i]; if(line.startsWith("#")){ continue; } const tokens = line.split(/\s+/); if(tokens.length < 6){ continue; } const params = { id: tokens[0], x: Number.parseFloat(tokens[1]), y: Number.parseFloat(tokens[2]), z: Number.parseFloat(tokens[3]), omega: Number.parseFloat(tokens[4]), phi: Number.parseFloat(tokens[5]), kappa: Number.parseFloat(tokens[6]), }; // const whitelist = ["47518.jpg"]; // if(whitelist.includes(params.id)){ // imageParams.push(params); // } imageParams.push(params); } // debug //return [imageParams[50]]; return imageParams; } static async load(cameraParamsPath, imageParamsPath, viewer){ const tStart = performance.now(); const [cameraParams, imageParams] = await Promise.all([ OrientedImageLoader.loadCameraParams(cameraParamsPath), OrientedImageLoader.loadImageParams(imageParamsPath), ]); const orientedImageControls = new OrientedImageControls(viewer); const raycaster = new THREE.Raycaster(); const tEnd = performance.now(); console.log(tEnd - tStart); // const sp = new THREE.PlaneGeometry(1, 1); // const lg = new THREE.Geometry(); // lg.vertices.push( // new THREE.Vector3(-0.5, -0.5, 0), // new THREE.Vector3( 0.5, -0.5, 0), // new THREE.Vector3( 0.5, 0.5, 0), // new THREE.Vector3(-0.5, 0.5, 0), // new THREE.Vector3(-0.5, -0.5, 0), // ); const {width, height} = cameraParams; const orientedImages = []; const sceneNode = new THREE.Object3D(); sceneNode.name = "oriented_images"; for(const params of imageParams){ // const material = createMaterial(); // const lm = new THREE.LineBasicMaterial( { color: 0x00ff00 } ); // const mesh = new THREE.Mesh(sp, material); const {x, y, z, omega, phi, kappa} = params; // const [rx, ry, rz] = [omega, phi, kappa] // .map(THREE.Math.degToRad); // mesh.position.set(x, y, z); // mesh.scale.set(width / height, 1, 1); // mesh.rotation.set(rx, ry, rz); // { // mesh.updateMatrixWorld(); // const dir = mesh.getWorldDirection(); // const alpha = THREE.Math.degToRad(cameraParams.fov / 2); // const d = -0.5 / Math.tan(alpha); // const move = dir.clone().multiplyScalar(d); // mesh.position.add(move); // } // sceneNode.add(mesh); // const line = new THREE.Line(lg, lm); // line.position.copy(mesh.position); // line.scale.copy(mesh.scale); // line.rotation.copy(mesh.rotation); // sceneNode.add(line); let orientedImage = new OrientedImage(params.id); // orientedImage.setPosition(x, y, z); // orientedImage.setRotation(omega, phi, kappa); // orientedImage.setDimension(width, height); let position = [x, y, z]; let rotation = [omega, phi, kappa]; let dimension = [width, height]; orientedImage.set(position, rotation, dimension, cameraParams.fov); sceneNode.add(orientedImage.mesh); sceneNode.add(orientedImage.line); orientedImages.push(orientedImage); } let hoveredElement = null; let clipVolume = null; const onMouseMove = (evt) => { const tStart = performance.now(); if(hoveredElement){ hoveredElement.line.material.color.setRGB(0, 1, 0); } evt.preventDefault(); //var array = getMousePosition( container, evt.clientX, evt.clientY ); const rect = viewer.renderer.domElement.getBoundingClientRect(); const [x, y] = [evt.clientX, evt.clientY]; const array = [ ( x - rect.left ) / rect.width, ( y - rect.top ) / rect.height ]; const onClickPosition = new THREE.Vector2(...array); //const intersects = getIntersects(onClickPosition, scene.children); const camera = viewer.scene.getActiveCamera(); const mouse = new THREE.Vector3( + ( onClickPosition.x * 2 ) - 1, - ( onClickPosition.y * 2 ) + 1 ); const objects = orientedImages.map(i => i.mesh); raycaster.setFromCamera( mouse, camera ); const intersects = raycaster.intersectObjects( objects ); let selectionChanged = false; if ( intersects.length > 0){ //console.log(intersects); const intersection = intersects[0]; const orientedImage = intersection.object.orientedImage; orientedImage.line.material.color.setRGB(1, 0, 0); selectionChanged = hoveredElement !== orientedImage; hoveredElement = orientedImage; }else{ hoveredElement = null; } let shouldRemoveClipVolume = clipVolume !== null && hoveredElement === null; let shouldAddClipVolume = clipVolume === null && hoveredElement !== null; if(clipVolume !== null && (hoveredElement === null || selectionChanged)){ // remove existing viewer.scene.removePolygonClipVolume(clipVolume); clipVolume = null; } if(shouldAddClipVolume || selectionChanged){ const img = hoveredElement; const fov = cameraParams.fov; const aspect = cameraParams.width / cameraParams.height; const near = 1.0; const far = 1000 * 1000; const camera = new THREE.PerspectiveCamera(fov, aspect, near, far); camera.rotation.order = viewer.scene.getActiveCamera().rotation.order; camera.rotation.copy(img.mesh.rotation); { const mesh = img.mesh; const dir = mesh.getWorldDirection(); const pos = mesh.position; const alpha = THREE.Math.degToRad(fov / 2); const d = 0.5 / Math.tan(alpha); const newCamPos = pos.clone().add(dir.clone().multiplyScalar(d)); const newCamDir = pos.clone().sub(newCamPos); const newCamTarget = new THREE.Vector3().addVectors( newCamPos, newCamDir.clone().multiplyScalar(viewer.getMoveSpeed())); camera.position.copy(newCamPos); } let volume = new Potree.PolygonClipVolume(camera); let m0 = new THREE.Mesh(); let m1 = new THREE.Mesh(); let m2 = new THREE.Mesh(); let m3 = new THREE.Mesh(); m0.position.set(-1, -1, 0); m1.position.set( 1, -1, 0); m2.position.set( 1, 1, 0); m3.position.set(-1, 1, 0); volume.markers.push(m0, m1, m2, m3); volume.initialized = true; viewer.scene.addPolygonClipVolume(volume); clipVolume = volume; } const tEnd = performance.now(); //console.log(tEnd - tStart); }; const moveToImage = (image) => { console.log("move to image " + image.id); const mesh = image.mesh; const newCamPos = image.position.clone(); const newCamTarget = mesh.position.clone(); viewer.scene.view.setView(newCamPos, newCamTarget, 500, () => { orientedImageControls.capture(image); }); if(image.texture === null){ const target = image; const tmpImagePath = `${Potree.resourcePath}/images/loading.jpg`; new THREE.TextureLoader().load(tmpImagePath, (texture) => { if(target.texture === null){ target.texture = texture; target.mesh.material.uniforms.tColor.value = texture; mesh.material.needsUpdate = true; } } ); const imagePath = `${imageParamsPath}/../${target.id}`; new THREE.TextureLoader().load(imagePath, (texture) => { target.texture = texture; target.mesh.material.uniforms.tColor.value = texture; mesh.material.needsUpdate = true; } ); } }; const onMouseClick = (evt) => { if(orientedImageControls.hasSomethingCaptured()){ return; } if(hoveredElement){ moveToImage(hoveredElement); } }; viewer.renderer.domElement.addEventListener( 'mousemove', onMouseMove, false ); viewer.renderer.domElement.addEventListener( 'mousedown', onMouseClick, false ); viewer.addEventListener("update", () => { for(const image of orientedImages){ const world = image.mesh.matrixWorld; const {width, height} = image; const aspect = width / height; const camera = viewer.scene.getActiveCamera(); const imgPos = image.mesh.getWorldPosition(new THREE.Vector3()); const camPos = camera.position; const d = camPos.distanceTo(imgPos); const minSize = 1; // in degrees of fov const a = THREE.Math.degToRad(minSize); let r = d * Math.tan(a); r = Math.max(r, 1); image.mesh.scale.set(r * aspect, r, 1); image.line.scale.set(r * aspect, r, 1); image.mesh.material.uniforms.uNear.value = camera.near; } }); const images = new OrientedImages(); images.node = sceneNode; images.cameraParamsPath = cameraParamsPath; images.imageParamsPath = imageParamsPath; images.cameraParams = cameraParams; images.imageParams = imageParams; images.images = orientedImages; Potree.debug.moveToImage = moveToImage; return images; } }