@sauskylark/potree
Version:
WebGL point cloud viewer
491 lines (387 loc) • 13.6 kB
JavaScript
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;
}
}