port pointcloud to webcomponent

This commit is contained in:
Tom 2025-01-23 10:57:36 +00:00
parent f3a97e5b04
commit 2e05be2a70
6 changed files with 356 additions and 170 deletions

View File

@ -2,7 +2,6 @@
title: Replacing an image colour with transparency title: Replacing an image colour with transparency
layout: post layout: post
excerpt: What happens if you convert an RGB image to RGBA by pretending it was sitting on a white background? excerpt: What happens if you convert an RGB image to RGBA by pretending it was sitting on a white background?
draft: true
images: /assets/blog/alpha_test images: /assets/blog/alpha_test
thumbnail: /assets/blog/alpha_test/thumbnail.png thumbnail: /assets/blog/alpha_test/thumbnail.png

View File

@ -101,94 +101,75 @@ Click and drag to spin me around. It didn't really capture my nose very well, I
<figure> <figure>
<img class="no-wc" src="{{page.assets}}/rear_stereo/point_cloud_preview.png"> <img class="no-wc" src="{{page.assets}}/rear_stereo/point_cloud_preview.png">
<canvas style ="width: 100%;" id="canvas-id-1"></canvas> <point-cloud-viewer model="/assets/blog/heic_depth_map/rear_stereo/pointcloud.pcd" camera = '{"type":"perspective","position":[-3.598,-0.4154,1.971],"rotation":[0.2078,-1.06,0.1819],"zoom":1,"target":[0,0,0]}'>
</point-cloud-viewer>
<figcaption class="no-wc">If you have JS enabled this is interactive.</figcaption> <figcaption class="no-wc">If you have JS enabled this is interactive.</figcaption>
<figcaption class="has-wc">An interactive point cloud view.</figcaption>
</figure> </figure>
<script type="module"> <script type="module">
import * as THREE from "three"; // import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js"; // import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { DragControls } from "three/addons/controls/DragControls.js"; // import { DragControls } from "three/addons/controls/DragControls.js";
import { PCDLoader } from 'three/addons/loaders/PCDLoader.js'; // import { PCDLoader } from 'three/addons/loaders/PCDLoader.js';
import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; // import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
init('canvas-id-1', '{{page.assets}}/rear_stereo/pointcloud.pcd'); // init('canvas-id-1', '{{page.assets}}/rear_stereo/pointcloud.pcd');
init('canvas-id-2', '{{page.assets}}/front_facing/pointcloud.pcd'); // init('canvas-id-2', '{{page.assets}}/front_facing/pointcloud.pcd');
function init(canvas_id, url) { // function init(canvas_id, url) {
let render, gui, orbitControls; // let render, gui, orbitControls;
let canvas = document.getElementById(canvas_id); // let canvas = document.getElementById(canvas_id);
const loader = new PCDLoader(); // const loader = new PCDLoader();
let scene = new THREE.Scene(); // let scene = new THREE.Scene();
scene.add( new THREE.AxesHelper( 1 ) ); // scene.add( new THREE.AxesHelper( 1 ) );
loader.load(url, function ( points ) { // loader.load(url, function ( points ) {
points.geometry.center(); // points.geometry.center();
points.geometry.rotateZ( -Math.PI/2 ); // points.geometry.rotateZ( -Math.PI/2 );
points.name = 'depth_map'; // points.name = 'depth_map';
scene.add( points ); // scene.add( points );
points.material.color = new THREE.Color(0x999999); // points.material.color = new THREE.Color(0x999999);
points.material.size = 0.001 // points.material.size = 0.001
render(); // render();
} ); // } );
// --- Scene --- // // --- Scene ---
const aspect = canvas.clientWidth / canvas.clientHeight; // const aspect = canvas.clientWidth / canvas.clientHeight;
let camera = new THREE.PerspectiveCamera( 30, aspect, 0.01, 40 ); // let camera = new THREE.PerspectiveCamera( 30, aspect, 0.01, 40 );
camera.position.set( -2, 2, 3); // camera.position.set( -2, 2, 3);
camera.lookAt(0, 0, 0); // camera.lookAt(0, 0, 0);
// --- Renderer (use the existing canvas) --- // // --- Renderer (use the existing canvas) ---
let renderer = new THREE.WebGLRenderer({ alpha: true, canvas: canvas, antialias: true }); // let renderer = new THREE.WebGLRenderer({ alpha: true, canvas: canvas, antialias: true });
renderer.setSize(canvas.clientWidth, canvas.clientHeight,); // renderer.setSize(canvas.clientWidth, canvas.clientHeight,);
render = () => renderer.render(scene, camera); // render = () => renderer.render(scene, camera);
// --- OrbitControls --- // // --- OrbitControls ---
orbitControls = new OrbitControls(camera, renderer.domElement); // orbitControls = new OrbitControls(camera, renderer.domElement);
orbitControls.addEventListener( 'change', render); // orbitControls.addEventListener( 'change', render);
const ambientLight = new THREE.AmbientLight(0xffffff, 0.7); // const ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
scene.add(ambientLight); // scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.7); // const dirLight = new THREE.DirectionalLight(0xffffff, 0.7);
dirLight.position.set(5, 5, 10); // dirLight.position.set(5, 5, 10);
scene.add(dirLight); // scene.add(dirLight);
window.addEventListener('resize', onWindowResize, false); // window.addEventListener('resize', onWindowResize, false);
function onWindowResize() { // function onWindowResize() {
camera.aspect = canvas.clientWidth / canvas.clientHeight; // camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix(); // camera.updateProjectionMatrix();
renderer.setSize(canvas.clientWidth, canvas.clientHeight); // renderer.setSize(canvas.clientWidth, canvas.clientHeight);
} // }
// const elem = document.querySelector('#screenshot'); // }
// elem.addEventListener('click', () => {
// render();
// canvas.toBlob((blob) => {
// saveBlob(blob, `screencapture-${canvas.width}x${canvas.height}.png`);
// });
// });
// const saveBlob = (function() {
// const a = document.createElement('a');
// document.body.appendChild(a);
// a.style.display = 'none';
// return function saveData(blob, fileName) {
// const url = window.URL.createObjectURL(blob);
// console.log(url);
// a.href = url;
// a.download = fileName;
// a.click();
// };
// }());
}
</script> </script>
## Update ## Update
@ -207,6 +188,8 @@ The depth information, while lower resolution, is much better. My nose really po
<figure> <figure>
<img class="no-wc" src="{{page.assets}}/front_facing/point_cloud_preview.png"> <img class="no-wc" src="{{page.assets}}/front_facing/point_cloud_preview.png">
<canvas style ="width: 100%;" id="canvas-id-2"></canvas> <point-cloud-viewer model="/assets/blog/heic_depth_map/front_facing/pointcloud.pcd" camera = '{"type":"perspective","position":[-3.682,0.3606,1.82],"rotation":[-0.1955,-1.104,-0.1751],"zoom":1,"target":[0,0,0]}'>
</point-cloud-viewer>
<figcaption class="no-wc">If you have JS enabled this is interactive.</figcaption> <figcaption class="no-wc">If you have JS enabled this is interactive.</figcaption>
<figcaption class="has-wc">An interactive point cloud view.</figcaption>
</figure> </figure>

View File

@ -4,7 +4,8 @@
height: 100%; height: 100%;
} }
outline-model-viewer { outline-model-viewer,
point-cloud-viewer {
width: 100%; width: 100%;
min-height: 300px; min-height: 300px;
display: flex; display: flex;

View File

@ -0,0 +1,85 @@
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { PCDLoader } from "three/addons/loaders/PCDLoader.js";
import { Timer } from "three/addons/Addons.js";
import {
componentHTML,
setupThreeJS,
deserialiseCamera,
deserialiseControls,
} from "./helpers.js";
export class PointCloudViewer extends HTMLElement {
constructor() {
super();
this.isVisible = true;
this.shadow = this.attachShadow({ mode: "open" });
}
connectedCallback() {
const { container, canvas, scene, renderer, gui } = setupThreeJS(this);
const loader = new PCDLoader();
scene.add(new THREE.AxesHelper(1));
const model = this.getAttribute("model");
const render = () => renderer.render(scene, this.camera);
this.render = render;
loader.load(model, function (points) {
points.material.size = 0.05;
gui
.add(points.material, "size", 0.01, 0.2)
.name("Point Size")
.onChange(render);
points.geometry.center();
points.geometry.rotateZ(-Math.PI / 2);
points.name = "depth_map";
scene.add(points);
points.material.color = new THREE.Color(0x999999);
render();
console.log("Model Loaded.");
});
// --- OrbitControls ---
this.controls = new OrbitControls(this.camera, renderer.domElement);
this.controls.addEventListener("change", render);
this.controls.enableDamping = true;
this.controls.dampingFactor = 0.25;
this.controls.autoRotate = true;
deserialiseControls(this);
canvas.addEventListener("click", () => {
this.controls.autoRotate = false;
});
const ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.7);
dirLight.position.set(5, 5, 10);
scene.add(dirLight);
window.addEventListener("resize", onWindowResize, false);
function onWindowResize() {
this.camera.aspect = canvas.clientWidth / canvas.clientHeight;
this.camera.updateProjectionMatrix();
renderer.setSize(canvas.clientWidth, canvas.clientHeight);
}
const timer = new Timer();
const update = () => {
if (this.isVisible) {
timer.update();
const delta = timer.getDelta();
requestAnimationFrame(update);
this.controls.update(delta);
this.render();
}
};
update();
}
}

View File

@ -0,0 +1,210 @@
import * as THREE from "three";
import GUI from "lil-gui";
export function serialiseCamera(component) {
const { camera, controls } = component;
const position = Object.values(camera.position);
const extractXYZ = ({ _x, _y, _z }) => [_x, _y, _z];
const rotation = extractXYZ(camera.rotation);
const fixed = (l) => l.map((x) => parseFloat(x.toPrecision(4)));
return JSON.stringify({
type: "perspective",
fov: camera.fov,
near: camera.near,
far: camera.far,
position: fixed(position),
rotation: fixed(rotation),
zoom: camera.zoom,
target: fixed(Object.values(controls.target)),
});
}
// Todo alllow isometric camera
export function deserialiseCamera(component) {
const { canvas, initial_camera_state } = component;
const aspect = canvas.clientWidth / canvas.clientHeight;
const camera = new THREE.PerspectiveCamera(30, aspect, 0.01, 40);
if (!initial_camera_state) return;
if (initial_camera_state.type !== "perspective") return;
if (initial_camera_state.fov) camera.fov = initial_camera_state.fov;
if (initial_camera_state.near) camera.near = initial_camera_state.near;
if (initial_camera_state.far) camera.far = initial_camera_state.far;
if (initial_camera_state.zoom) camera.zoom = initial_camera_state.zoom;
if (initial_camera_state.position)
camera.position.set(...initial_camera_state.position);
if (initial_camera_state.rotation)
camera.rotation.set(...initial_camera_state.rotation);
camera.updateProjectionMatrix();
return camera;
}
export function deserialiseControls(component) {
const { controls, initial_camera_state } = component;
if (initial_camera_state.target && controls.target)
controls.target.set(...initial_camera_state.target);
}
const saveBlob = (function () {
const a = document.createElement("a");
document.body.appendChild(a);
a.style.display = "none";
return function saveData(blob, fileName) {
const url = window.URL.createObjectURL(blob);
console.log(url);
a.href = url;
a.download = fileName;
a.click();
};
})();
function takeScreenshot(component) {
const { canvas, render } = component;
render();
canvas.toBlob((blob) => {
saveBlob(blob, `screencapture-${canvas.width}x${canvas.height}.png`);
});
}
function componentHTML(component_rect) {
const { height } = component_rect;
console.log("Height:", height);
return `
<div id="container">
<span id = "clicked-item"></span>
<!-- <button id="fullscreen-btn"></button> --!>
<canvas class = "object-viewer"></canvas>
</div>
<link rel="stylesheet" href="/node_modules/lil-gui/dist/lil-gui.min.css">
<style>
#container {
position: relative;
width: 100%;
display: flex;
flex-direction: column;
border-radius: inherit;
}
#clicked-item {
position: absolute;
top: 10px;
left: 10px;
z-index: 10;
font-size: 0.7em;
background: none;
border: none;
color: var(--theme-text-color);
opacity: 50%;
}
#fullscreen-btn {
position: absolute;
top: 10px;
right: 10px;
z-index: 10;
font-size: 24px;
background: none;
border: none;
cursor: pointer;
color: var(--theme-text-color);
}
#fullscreen-btn:hover {
color: var(--theme-subtle-outline);
}
.lil-gui .title {height: 2em;}
.lil-gui.root {
margin-top: calc(${height}px - 2em);
width: 100%;
z-index: 1;
--background-color: none;
--text-color: var(--theme-text-color);
--title-background-color: none;
--title-text-color: var(--theme-text-color);
--widget-color: var(--theme-subtle-outline);
--hover-color: lightgrey;
--focus-color: lightgrey;
--number-color: #2cc9ff;
--string-color: #a2db3c;
}
.lil-gui button {
border: var(--theme-subtle-outline) 1px solid;
}
.lil-gui .controller.string input {
background-color: var(--theme-subtle-outline);
color: var(--theme-text-color);
}
canvas {
position: absolute;
width: 100%;
height: ${height}px;
border-radius: inherit;
}
</style>
`;
}
// Usage const { container, canvas, scene, gui } = setupThreeJS(this);
function setupThreeJS(component) {
const component_rect = component.getBoundingClientRect();
// Create the component HTML
component.shadow.innerHTML = componentHTML(component_rect);
component.container = component.shadow.querySelector("div#container");
component.canvas = component.shadow.querySelector("canvas");
const canvas_rect = component.canvas.getBoundingClientRect();
if (component.getAttribute("camera")) {
component.initial_camera_state = JSON.parse(
component.getAttribute("camera")
);
component.removeAttribute("camera");
}
component.camera = deserialiseCamera(component);
component.scene = new THREE.Scene();
component.renderer = new THREE.WebGLRenderer({
canvas: component.canvas,
alpha: true,
});
component.renderer.setPixelRatio(window.devicePixelRatio);
component.renderer.setSize(canvas_rect.width, canvas_rect.height, false);
component.gui = new GUI({
title: "Settings",
container: component.container,
injectStyles: true,
closeFolders: true,
});
if ((component.getAttribute("debug") || "closed") !== "open")
component.gui.close();
const params = {
printCamera: () => console.log(serialiseCamera(component)),
screenshot: () => takeScreenshot(component),
resetCamera: () => {
deserialiseCamera(component);
component.render();
},
};
component.gui.add(params, "printCamera").name("Print Viewport State");
component.gui.add(params, "screenshot").name("Take Screenshot");
component.gui.add(params, "resetCamera").name("Reset Viewport");
return component;
}
export { componentHTML, setupThreeJS };

View File

@ -12,9 +12,14 @@ import { FXAAShader } from "three/addons/shaders/FXAAShader.js";
import { Timer } from "three/addons/Addons.js"; import { Timer } from "three/addons/Addons.js";
import GUI from "lil-gui"; import GUI from "lil-gui";
import { componentHTML, setupThreeJS, serialiseCamera } from "./helpers.js";
import { CustomOutlinePass } from "./CustomOutlinePass.js"; import { CustomOutlinePass } from "./CustomOutlinePass.js";
import FindSurfaces from "./FindSurfaces.js"; import FindSurfaces from "./FindSurfaces.js";
import { PointCloudViewer } from "./PointCloudViewer.js";
customElements.define("point-cloud-viewer", PointCloudViewer);
// Todo: // Todo:
// Swap in the version of this code that has a debug GUI behind a flag // Swap in the version of this code that has a debug GUI behind a flag
// Consider support for transparent objects by rendering them as a wireframe in the color and excluding them from the edge pass. // Consider support for transparent objects by rendering them as a wireframe in the color and excluding them from the edge pass.
@ -59,26 +64,11 @@ function printGLTFScene(scene, maxDepth = 3, depth = 0, indent = 0) {
}); });
} }
const serialiseCamera = (camera, controls) => {
const position = Object.values(camera.position);
const extractXYZ = ({ _x, _y, _z }) => [_x, _y, _z];
const rotation = extractXYZ(camera.rotation);
const fixed = (l) => l.map((x) => parseFloat(x.toPrecision(4)));
return JSON.stringify({
position: fixed(position),
rotation: fixed(rotation),
zoom: camera.zoom,
target: fixed(Object.values(controls.target)),
});
};
export class OutlineModelViewer extends HTMLElement { export class OutlineModelViewer extends HTMLElement {
constructor() { constructor() {
super(); super();
this.isVisible = true; // Track visibility this.isVisible = true;
this.shadow = this.attachShadow({ mode: "open" }); this.shadow = this.attachShadow({ mode: "open" });
// Mouse and raycaster
this.raycaster = new THREE.Raycaster(); this.raycaster = new THREE.Raycaster();
this.mouse = new THREE.Vector2(); this.mouse = new THREE.Vector2();
this.intersectedObject = null; // Store currently intersected object this.intersectedObject = null; // Store currently intersected object
@ -95,8 +85,7 @@ export class OutlineModelViewer extends HTMLElement {
const mul = 2; const mul = 2;
let component_rect = this.getBoundingClientRect(); let component_rect = this.getBoundingClientRect();
this.shadow.innerHTML = componentHTML(component_rect);
this.render(component_rect.height);
const model_path = this.getAttribute("model"); const model_path = this.getAttribute("model");
const spin = (this.getAttribute("spin") || "true") === "true"; const spin = (this.getAttribute("spin") || "true") === "true";
@ -492,87 +481,6 @@ export class OutlineModelViewer extends HTMLElement {
} }
document.addEventListener("fullscreenchange", onFullScreenChange); document.addEventListener("fullscreenchange", onFullScreenChange);
} }
render(height) {
this.shadow.innerHTML = `
<div id="container">
<span id = "clicked-item"></span>
<!-- <button id="fullscreen-btn"></button> --!>
<canvas class = "object-viewer"></canvas>
</div>
<link rel="stylesheet" href="/node_modules/lil-gui/dist/lil-gui.min.css">
<style>
#container {
position: relative;
width: 100%;
display: flex;
flex-direction: column;
border-radius: inherit;
}
#clicked-item {
position: absolute;
top: 10px;
left: 10px;
z-index: 10;
font-size: 0.7em;
background: none;
border: none;
color: var(--theme-text-color);
opacity: 50%;
}
#fullscreen-btn {
position: absolute;
top: 10px;
right: 10px;
z-index: 10;
font-size: 24px;
background: none;
border: none;
cursor: pointer;
color: var(--theme-text-color);
}
#fullscreen-btn:hover {
color: var(--theme-subtle-outline);
}
.lil-gui .title {height: 2em;}
.lil-gui.root {
margin-top: calc(${height}px - 2em);
width: 100%;
z-index: 1;
--background-color: none;
--text-color: var(--theme-text-color);
--title-background-color: none;
--title-text-color: var(--theme-text-color);
--widget-color: var(--theme-subtle-outline);
--hover-color: lightgrey;
--focus-color: lightgrey;
--number-color: #2cc9ff;
--string-color: #a2db3c;
}
.lil-gui button {
border: var(--theme-subtle-outline) 1px solid;
}
.lil-gui .controller.string input {
background-color: var(--theme-subtle-outline);
color: var(--theme-text-color);
}
canvas {
position: absolute;
width: 100%;
height: ${height}px;
border-radius: inherit;
}
</style>
`;
}
} }
customElements.define("outline-model-viewer", OutlineModelViewer); customElements.define("outline-model-viewer", OutlineModelViewer);