From 2e05be2a709765d539e428327f2c2983c27a2e59 Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 23 Jan 2025 10:57:36 +0000 Subject: [PATCH] port pointcloud to webcomponent --- _posts/2025-01-17-colour-to-alpha.md | 1 - _posts/2025-01-18-heic-depth.md | 121 +++++----- _sass/model_viewer.scss | 3 +- .../outline-model-viewer/PointCloudViewer.js | 85 +++++++ assets/js/outline-model-viewer/helpers.js | 210 ++++++++++++++++++ assets/js/outline-model-viewer/index.js | 106 +-------- 6 files changed, 356 insertions(+), 170 deletions(-) create mode 100644 assets/js/outline-model-viewer/PointCloudViewer.js create mode 100644 assets/js/outline-model-viewer/helpers.js diff --git a/_posts/2025-01-17-colour-to-alpha.md b/_posts/2025-01-17-colour-to-alpha.md index 228c695..bb46df0 100644 --- a/_posts/2025-01-17-colour-to-alpha.md +++ b/_posts/2025-01-17-colour-to-alpha.md @@ -2,7 +2,6 @@ title: Replacing an image colour with transparency layout: post 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 thumbnail: /assets/blog/alpha_test/thumbnail.png diff --git a/_posts/2025-01-18-heic-depth.md b/_posts/2025-01-18-heic-depth.md index 201ccc9..4c7a997 100644 --- a/_posts/2025-01-18-heic-depth.md +++ b/_posts/2025-01-18-heic-depth.md @@ -101,94 +101,75 @@ Click and drag to spin me around. It didn't really capture my nose very well, I
- + +
If you have JS enabled this is interactive.
+
An interactive point cloud view.
## Update @@ -207,6 +188,8 @@ The depth information, while lower resolution, is much better. My nose really po
- + +
If you have JS enabled this is interactive.
+
An interactive point cloud view.
\ No newline at end of file diff --git a/_sass/model_viewer.scss b/_sass/model_viewer.scss index fbc1cb1..14be4d1 100644 --- a/_sass/model_viewer.scss +++ b/_sass/model_viewer.scss @@ -4,7 +4,8 @@ height: 100%; } -outline-model-viewer { +outline-model-viewer, +point-cloud-viewer { width: 100%; min-height: 300px; display: flex; diff --git a/assets/js/outline-model-viewer/PointCloudViewer.js b/assets/js/outline-model-viewer/PointCloudViewer.js new file mode 100644 index 0000000..50a9ee8 --- /dev/null +++ b/assets/js/outline-model-viewer/PointCloudViewer.js @@ -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(); + } +} diff --git a/assets/js/outline-model-viewer/helpers.js b/assets/js/outline-model-viewer/helpers.js new file mode 100644 index 0000000..333916b --- /dev/null +++ b/assets/js/outline-model-viewer/helpers.js @@ -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 ` +
+ + + +
+ + + `; +} + +// 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 }; diff --git a/assets/js/outline-model-viewer/index.js b/assets/js/outline-model-viewer/index.js index a27b3be..a4e51d7 100644 --- a/assets/js/outline-model-viewer/index.js +++ b/assets/js/outline-model-viewer/index.js @@ -12,9 +12,14 @@ import { FXAAShader } from "three/addons/shaders/FXAAShader.js"; import { Timer } from "three/addons/Addons.js"; import GUI from "lil-gui"; +import { componentHTML, setupThreeJS, serialiseCamera } from "./helpers.js"; import { CustomOutlinePass } from "./CustomOutlinePass.js"; import FindSurfaces from "./FindSurfaces.js"; +import { PointCloudViewer } from "./PointCloudViewer.js"; + +customElements.define("point-cloud-viewer", PointCloudViewer); + // Todo: // 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. @@ -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 { constructor() { super(); - this.isVisible = true; // Track visibility + this.isVisible = true; this.shadow = this.attachShadow({ mode: "open" }); - - // Mouse and raycaster this.raycaster = new THREE.Raycaster(); this.mouse = new THREE.Vector2(); this.intersectedObject = null; // Store currently intersected object @@ -95,8 +85,7 @@ export class OutlineModelViewer extends HTMLElement { const mul = 2; let component_rect = this.getBoundingClientRect(); - - this.render(component_rect.height); + this.shadow.innerHTML = componentHTML(component_rect); const model_path = this.getAttribute("model"); const spin = (this.getAttribute("spin") || "true") === "true"; @@ -492,87 +481,6 @@ export class OutlineModelViewer extends HTMLElement { } document.addEventListener("fullscreenchange", onFullScreenChange); } - - render(height) { - this.shadow.innerHTML = ` -
- - - -
- - - `; - } } customElements.define("outline-model-viewer", OutlineModelViewer);