2025-04-07 20:09:47 +02:00

288 lines
9.2 KiB
JavaScript

import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { DRACOLoader } from "three/addons/loaders/DRACOLoader.js";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js";
import { FXAAShader } from "three/addons/shaders/FXAAShader.js";
import { Timer } from "three/addons/Addons.js";
import { setupThreeJS, serialiseCamera } from "./helpers.js";
import { CustomOutlinePass } from "./CustomOutlinePass.js";
import FindSurfaces from "./FindSurfaces.js";
import { load_gltf } from "./LoadGLTF.js";
export class OutlineModelViewer extends HTMLElement {
constructor() {
super();
this.isVisible = true;
this.shadow = this.attachShadow({ mode: "open" });
this.raycaster = new THREE.Raycaster();
this.mouse = new THREE.Vector2();
this.intersectedObject = null; // Store currently intersected object
}
// Handle mouse movement and update mouse coordinates
onMouseMove(event, canvas) {
const rect = canvas.getBoundingClientRect();
this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
}
hideUI() {
console.log("Hiding UI");
this.component.hideUI();
}
updatePixelRatio(r) {
this.pixelRatio = r;
this.component.renderer.setPixelRatio(r);
this.customOutline.updateEdgeThickness(
this.pixelRatio * this.edgeThickness
);
}
updateEdgeThickness(t) {
this.edgeThickness = t;
this.customOutline.updateEdgeThickness(
this.pixelRatio * this.edgeThickness
);
}
connectedCallback() {
let element = this;
let component = setupThreeJS(this);
this.component = component;
const { canvas, camera, scene, renderer, gui } = component;
const model_path = this.getAttribute("model");
const spin = (this.getAttribute("spin") || "true") === "true";
// determine the outline and bg colors
const body = document.getElementsByTagName("body")[0];
const style = window.getComputedStyle(body);
const outline_color = style.getPropertyValue("--theme-model-line-color");
const model_color = style.getPropertyValue("--theme-model-bg-color");
const directionalLight = new THREE.DirectionalLight(
0xffffff,
this.getAttribute("directional-light") || 2
);
scene.add(directionalLight);
directionalLight.position.set(1.7, 1, -1);
const ambientLight = new THREE.AmbientLight(
0xffffff,
this.getAttribute("ambient-light") || 0.5
);
scene.add(ambientLight);
// Set up post processing
// Create a render target that holds a depthTexture so we can use it in the outline pass
// See: https://threejs.org/docs/index.html#api/en/renderers/WebGLRenderTarget.depthBuffer
const depthTexture = new THREE.DepthTexture();
const renderTarget = new THREE.WebGLRenderTarget(
canvas.width,
canvas.height,
{
depthTexture: depthTexture,
depthBuffer: true,
}
);
// Initial render pass.
const composer = new EffectComposer(renderer, renderTarget);
component.composer = composer;
const pass = new RenderPass(scene, camera);
composer.addPass(pass);
// Outline pass.
const customOutline = new CustomOutlinePass(
new THREE.Vector2(canvas.width, canvas.height),
scene,
camera,
outline_color,
this.edgeThickness * this.pixelRatio
);
composer.addPass(customOutline);
this.customOutline = customOutline;
// Antialias pass.
const effectFXAA = new ShaderPass(FXAAShader);
effectFXAA.uniforms["resolution"].value.set(
1.0 / canvas.width,
1.0 / canvas.height
);
// composer.addPass(effectFXAA);
// Set over sampling ratio
this.updateEdgeThickness(1);
this.updatePixelRatio(window.devicePixelRatio);
renderer.setSize(canvas.clientWidth, canvas.clientHeight, false);
component.render = composer.render;
// Load model
const loader = new GLTFLoader();
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderConfig({ type: "js" });
dracoLoader.setDecoderPath("https://www.gstatic.com/draco/v1/decoders/");
loader.setDRACOLoader(dracoLoader);
const surfaceFinder = new FindSurfaces();
loader.load(model_path, (gltf) =>
load_gltf(this, scene, surfaceFinder, model_color, customOutline, gltf)
);
// Set up orbital camera controls.
let controls = new OrbitControls(camera, renderer.domElement);
component.controls = controls;
controls.autoRotate = spin;
controls.update();
// Event listener for mouse movement
canvas.addEventListener("mousemove", (event) =>
this.onMouseMove(event, canvas)
);
let intersects = [];
const doRayCast = () => {
// Perform raycasting for a click
this.raycaster.setFromCamera(this.mouse, camera);
intersects.length = 0;
this.raycaster.intersectObjects(scene.children, true, intersects);
if (intersects.length > 0) {
const object = intersects[0].object;
// If the intersected object has changed
if (this.intersectedObject !== object) {
if (this.intersectedObject) {
// Reset the color of the previously hovered object
this.intersectedObject.material.emissive.setHex(
this.intersectedObject.currentHex
);
}
this.shadow.querySelector(
"#clicked-item"
).innerText = `${object.name}`;
}
} else if (this.intersectedObject) {
this.intersectedObject = null;
}
if (intersects.length === 0) {
this.shadow.querySelector("#clicked-item").innerText = "";
}
};
window.addEventListener("click", doRayCast);
// Render loop
this.render_loop = true;
const timer = new Timer();
const update = () => {
if (this.isVisible && this.render_loop) {
timer.update();
const delta = timer.getDelta();
requestAnimationFrame(update);
controls.update(delta);
composer.render();
}
};
update();
// Pausing/resuming the render loop when element visibility changes
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
console.log("Model Viewer Element is visible. Resuming rendering...");
this.isVisible = true;
update(); // Resume the loop
} else {
console.log(
"Model Viewer Element is not visible. Pausing rendering..."
);
this.isVisible = false; // Pauses rendering
}
});
});
// Observe this element for visibility changes
observer.observe(this);
function onWindowResize() {
// Update the internal dimensions of the canvas
canvas.width = canvas.clientWidth * element.pixelRatio;
canvas.height = canvas.clientHeight * element.pixelRatio;
// Recompute the camera matrix
camera.aspect = canvas.width / canvas.height;
camera.updateProjectionMatrix();
// Resive the various render targets
renderer.setSize(canvas.width, canvas.height, false);
composer.setSize(canvas.width, canvas.height);
effectFXAA.setSize(canvas.width, canvas.height);
customOutline.setSize(canvas.width, canvas.height);
//
effectFXAA.uniforms["resolution"].value.set(
1.0 / canvas.width,
1.0 / canvas.height
);
}
this.onWindowResize = onWindowResize;
onWindowResize();
const uniforms = customOutline.fsQuad.material.uniforms;
uniforms.debugVisualize.value = parseInt(this.getAttribute("mode")) || 0;
const params = {
spin: controls.autoRotate,
ambientLight: parseFloat(ambientLight.intensity),
directionalLight: parseFloat(directionalLight.intensity),
mode: { Mode: uniforms.debugVisualize.value },
depthBias: uniforms.multiplierParameters.value.x,
depthMult: uniforms.multiplierParameters.value.y,
lerp: uniforms.multiplierParameters.value.z,
edgeThickness: this.edgeThickness,
pixelRatio: this.pixelRatio,
};
gui.add(params, "spin").onChange((value) => {
controls.autoRotate = value;
});
gui
.add(params.mode, "Mode", {
"Outlines + Shaded (default)": 0,
"Just Outlines": 5,
"Only outer outlines + shading": 1,
"Only shading": 2,
"(Debug) SurfaceID buffer": 4,
"(Debug) Depth buffer": 3,
"(Debug) Depth Difference (external edges / outline)": 6,
"(Debug) SurfaceID Difference (internal edges)": 7,
})
.onChange(function (value) {
uniforms.debugVisualize.value = value;
});
gui.add(params, "edgeThickness", 1, 10).onChange(function (value) {
element.updateEdgeThickness(value);
});
gui.add(params, "pixelRatio", 1, 8, 1).onChange(function (value) {
element.updatePixelRatio(value);
element.onWindowResize();
});
window.addEventListener("resize", onWindowResize, false);
}
}
export default OutlineModelViewer;