@sauskylark/potree
Version:
WebGL point cloud viewer
530 lines (368 loc) • 12.6 kB
JavaScript
import * as THREE from "../../../libs/three.js/build/three.module.js";
import { EventDispatcher } from "../../EventDispatcher.js";
import { Utils } from "../../utils.js";
import {Line2} from "../../../libs/three.js/lines/Line2.js";
import {LineGeometry} from "../../../libs/three.js/lines/LineGeometry.js";
import {LineMaterial} from "../../../libs/three.js/lines/LineMaterial.js";
class ControlPoint{
constructor(){
this.position = new THREE.Vector3(0, 0, 0);
this.target = new THREE.Vector3(0, 0, 0);
this.positionHandle = null;
this.targetHandle = null;
}
};
export class CameraAnimation extends EventDispatcher{
constructor(viewer){
super();
this.viewer = viewer;
this.selectedElement = null;
this.controlPoints = [];
this.uuid = THREE.Math.generateUUID();
this.node = new THREE.Object3D();
this.node.name = "camera animation";
this.viewer.scene.scene.add(this.node);
this.frustum = this.createFrustum();
this.node.add(this.frustum);
this.name = "Camera Animation";
this.duration = 5;
this.t = 0;
// "centripetal", "chordal", "catmullrom"
this.curveType = "centripetal"
this.visible = true;
this.createUpdateHook();
this.createPath();
}
static defaultFromView(viewer){
const animation = new CameraAnimation(viewer);
const camera = viewer.scene.getActiveCamera();
const target = viewer.scene.view.getPivot();
const cpCenter = new THREE.Vector3(
0.3 * camera.position.x + 0.7 * target.x,
0.3 * camera.position.y + 0.7 * target.y,
0.3 * camera.position.z + 0.7 * target.z,
);
const targetCenter = new THREE.Vector3(
0.05 * camera.position.x + 0.95 * target.x,
0.05 * camera.position.y + 0.95 * target.y,
0.05 * camera.position.z + 0.95 * target.z,
);
const r = camera.position.distanceTo(target) * 0.3;
//const dir = target.clone().sub(camera.position).normalize();
const angle = Utils.computeAzimuth(camera.position, target);
const n = 5;
for(let i = 0; i < n; i++){
let u = 1.5 * Math.PI * (i / n) + angle;
const dx = r * Math.cos(u);
const dy = r * Math.sin(u);
const cpPos = [
cpCenter.x + dx,
cpCenter.y + dy,
cpCenter.z,
];
const targetPos = [
targetCenter.x + dx * 0.1,
targetCenter.y + dy * 0.1,
targetCenter.z,
];
const cp = animation.createControlPoint();
cp.position.set(...cpPos);
cp.target.set(...targetPos);
}
return animation;
}
createUpdateHook(){
const viewer = this.viewer;
viewer.addEventListener("update", () => {
const camera = viewer.scene.getActiveCamera();
const {width, height} = viewer.renderer.getSize(new THREE.Vector2());
this.node.visible = this.visible;
for(const cp of this.controlPoints){
{ // position
const projected = cp.position.clone().project(camera);
const visible = this.visible && (projected.z < 1 && projected.z > -1);
if(visible){
const x = width * (projected.x * 0.5 + 0.5);
const y = height - height * (projected.y * 0.5 + 0.5);
cp.positionHandle.svg.style.left = x - cp.positionHandle.svg.clientWidth / 2;
cp.positionHandle.svg.style.top = y - cp.positionHandle.svg.clientHeight / 2;
cp.positionHandle.svg.style.display = "";
}else{
cp.positionHandle.svg.style.display = "none";
}
}
{ // target
const projected = cp.target.clone().project(camera);
const visible = this.visible && (projected.z < 1 && projected.z > -1);
if(visible){
const x = width * (projected.x * 0.5 + 0.5);
const y = height - height * (projected.y * 0.5 + 0.5);
cp.targetHandle.svg.style.left = x - cp.targetHandle.svg.clientWidth / 2;
cp.targetHandle.svg.style.top = y - cp.targetHandle.svg.clientHeight / 2;
cp.targetHandle.svg.style.display = "";
}else{
cp.targetHandle.svg.style.display = "none";
}
}
}
this.line.material.resolution.set(width, height);
this.updatePath();
{ // frustum
const frame = this.at(this.t);
const frustum = this.frustum;
frustum.position.copy(frame.position);
frustum.lookAt(...frame.target.toArray());
frustum.scale.set(20, 20, 20);
frustum.material.resolution.set(width, height);
}
});
}
createControlPoint(index){
if(index === undefined){
index = this.controlPoints.length;
}
const cp = new ControlPoint();
if(this.controlPoints.length >= 2 && index === 0){
const cp1 = this.controlPoints[0];
const cp2 = this.controlPoints[1];
const dir = cp1.position.clone().sub(cp2.position).multiplyScalar(0.5);
cp.position.copy(cp1.position).add(dir);
const tDir = cp1.target.clone().sub(cp2.target).multiplyScalar(0.5);
cp.target.copy(cp1.target).add(tDir);
}else if(this.controlPoints.length >= 2 && index === this.controlPoints.length){
const cp1 = this.controlPoints[this.controlPoints.length - 2];
const cp2 = this.controlPoints[this.controlPoints.length - 1];
const dir = cp2.position.clone().sub(cp1.position).multiplyScalar(0.5);
cp.position.copy(cp1.position).add(dir);
const tDir = cp2.target.clone().sub(cp1.target).multiplyScalar(0.5);
cp.target.copy(cp2.target).add(tDir);
}else if(this.controlPoints.length >= 2){
const cp1 = this.controlPoints[index - 1];
const cp2 = this.controlPoints[index];
cp.position.copy(cp1.position.clone().add(cp2.position).multiplyScalar(0.5));
cp.target.copy(cp1.target.clone().add(cp2.target).multiplyScalar(0.5));
}
// cp.position.copy(viewer.scene.view.position);
// cp.target.copy(viewer.scene.view.getPivot());
cp.positionHandle = this.createHandle(cp.position);
cp.targetHandle = this.createHandle(cp.target);
this.controlPoints.splice(index, 0, cp);
this.dispatchEvent({
type: "controlpoint_added",
controlpoint: cp,
});
return cp;
}
removeControlPoint(cp){
this.controlPoints = this.controlPoints.filter(_cp => _cp !== cp);
this.dispatchEvent({
type: "controlpoint_removed",
controlpoint: cp,
});
cp.positionHandle.svg.remove();
cp.targetHandle.svg.remove();
// TODO destroy cp
}
createPath(){
{ // position
const geometry = new LineGeometry();
let material = new LineMaterial({
color: 0x00ff00,
dashSize: 5,
gapSize: 2,
linewidth: 2,
resolution: new THREE.Vector2(1000, 1000),
});
const line = new Line2(geometry, material);
this.line = line;
this.node.add(line);
}
{ // target
const geometry = new LineGeometry();
let material = new LineMaterial({
color: 0x0000ff,
dashSize: 5,
gapSize: 2,
linewidth: 2,
resolution: new THREE.Vector2(1000, 1000),
});
const line = new Line2(geometry, material);
this.targetLine = line;
this.node.add(line);
}
}
createFrustum(){
const f = 0.3;
const positions = [
0, 0, 0,
-f, -f, +1,
0, 0, 0,
f, -f, +1,
0, 0, 0,
f, f, +1,
0, 0, 0,
-f, f, +1,
-f, -f, +1,
f, -f, +1,
f, -f, +1,
f, f, +1,
f, f, +1,
-f, f, +1,
-f, f, +1,
-f, -f, +1,
];
const geometry = new LineGeometry();
geometry.setPositions(positions);
geometry.verticesNeedUpdate = true;
geometry.computeBoundingSphere();
let material = new LineMaterial({
color: 0xff0000,
linewidth: 2,
resolution: new THREE.Vector2(1000, 1000),
});
const line = new Line2(geometry, material);
line.computeLineDistances();
return line;
}
updatePath(){
{ // positions
const positions = this.controlPoints.map(cp => cp.position);
const first = positions[0];
const curve = new THREE.CatmullRomCurve3(positions);
curve.curveType = this.curveType;
const n = 100;
const curvePositions = [];
for(let k = 0; k <= n; k++){
const t = k / n;
const position = curve.getPoint(t).sub(first);
curvePositions.push(position.x, position.y, position.z);
}
this.line.geometry.setPositions(curvePositions);
this.line.geometry.verticesNeedUpdate = true;
this.line.geometry.computeBoundingSphere();
this.line.position.copy(first);
this.line.computeLineDistances();
this.cameraCurve = curve;
}
{ // targets
const positions = this.controlPoints.map(cp => cp.target);
const first = positions[0];
const curve = new THREE.CatmullRomCurve3(positions);
curve.curveType = this.curveType;
const n = 100;
const curvePositions = [];
for(let k = 0; k <= n; k++){
const t = k / n;
const position = curve.getPoint(t).sub(first);
curvePositions.push(position.x, position.y, position.z);
}
this.targetLine.geometry.setPositions(curvePositions);
this.targetLine.geometry.verticesNeedUpdate = true;
this.targetLine.geometry.computeBoundingSphere();
this.targetLine.position.copy(first);
this.targetLine.computeLineDistances();
this.targetCurve = curve;
}
}
at(t){
if(t > 1){
t = 1;
}else if(t < 0){
t = 0;
}
const camPos = this.cameraCurve.getPointAt(t);
const target = this.targetCurve.getPointAt(t);
const frame = {
position: camPos,
target: target,
};
return frame;
}
set(t){
this.t = t;
}
createHandle(vector){
const svgns = "http://www.w3.org/2000/svg";
const svg = document.createElementNS(svgns, "svg");
svg.setAttribute("width", "2em");
svg.setAttribute("height", "2em");
svg.setAttribute("position", "absolute");
svg.style.left = "50px";
svg.style.top = "50px";
svg.style.position = "absolute";
svg.style.zIndex = "10000";
const circle = document.createElementNS(svgns, 'circle');
circle.setAttributeNS(null, 'cx', "1em");
circle.setAttributeNS(null, 'cy', "1em");
circle.setAttributeNS(null, 'r', "0.5em");
circle.setAttributeNS(null, 'style', 'fill: red; stroke: black; stroke-width: 0.2em;' );
svg.appendChild(circle);
const element = this.viewer.renderer.domElement.parentElement;
element.appendChild(svg);
const startDrag = (evt) => {
this.selectedElement = svg;
document.addEventListener("mousemove", drag);
};
const endDrag = (evt) => {
this.selectedElement = null;
document.removeEventListener("mousemove", drag);
};
const drag = (evt) => {
if (this.selectedElement) {
evt.preventDefault();
const rect = viewer.renderer.domElement.getBoundingClientRect();
const x = evt.clientX - rect.x;
const y = evt.clientY - rect.y;
const {width, height} = this.viewer.renderer.getSize(new THREE.Vector2());
const camera = this.viewer.scene.getActiveCamera();
//const cp = this.controlPoints.find(cp => cp.handle.svg === svg);
const projected = vector.clone().project(camera);
projected.x = ((x / width) - 0.5) / 0.5;
projected.y = (-(y - height) / height - 0.5) / 0.5;
const unprojected = projected.clone().unproject(camera);
vector.set(unprojected.x, unprojected.y, unprojected.z);
}
};
svg.addEventListener('mousedown', startDrag);
svg.addEventListener('mouseup', endDrag);
const handle = {
svg: svg,
};
return handle;
}
setVisible(visible){
this.node.visible = visible;
const display = visible ? "" : "none";
for(const cp of this.controlPoints){
cp.positionHandle.svg.style.display = display;
cp.targetHandle.svg.style.display = display;
}
this.visible = visible;
}
setDuration(duration){
this.duration = duration;
}
getDuration(duration){
return this.duration;
}
play(){
const tStart = performance.now();
const duration = this.duration;
const originalyVisible = this.visible;
this.setVisible(false);
const onUpdate = (delta) => {
let tNow = performance.now();
let elapsed = (tNow - tStart) / 1000;
let t = elapsed / duration;
this.set(t);
const frame = this.at(t);
viewer.scene.view.position.copy(frame.position);
viewer.scene.view.lookAt(frame.target);
if(t > 1){
this.setVisible(originalyVisible);
this.viewer.removeEventListener("update", onUpdate);
}
};
this.viewer.addEventListener("update", onUpdate);
}
}