|
| 1 | +/* MD |
| 2 | + ## 📄 Fast Model Picking with Color Coding |
| 3 | + --- |
| 4 | + This tutorial demonstrates how to use the FastModelPicker component to quickly identify which fragment model is under the mouse cursor. Unlike raycasting, this component uses color coding for extremely fast model identification, making it ideal for scenarios where you need to quickly determine which model the user is hovering over or clicking on. |
| 5 | +
|
| 6 | + ### 🖖 Importing our Libraries |
| 7 | + First things first, let's install all necessary dependencies to make this example work: |
| 8 | +*/ |
| 9 | + |
| 10 | +import * as THREE from "three"; |
| 11 | +import Stats from "stats.js"; |
| 12 | +// You have to import * as OBC from "@thatopen/components" |
| 13 | +import * as OBC from "../.."; |
| 14 | + |
| 15 | +/* MD |
| 16 | + ### 🌎 Setting up a Simple Scene |
| 17 | + To get started, let's set up a basic ThreeJS scene. This will serve as the foundation for our application and allow us to visualize the 3D models effectively: |
| 18 | +*/ |
| 19 | + |
| 20 | +const components = new OBC.Components(); |
| 21 | + |
| 22 | +const worlds = components.get(OBC.Worlds); |
| 23 | +const world = worlds.create< |
| 24 | + OBC.SimpleScene, |
| 25 | + OBC.OrthoPerspectiveCamera, |
| 26 | + OBC.SimpleRenderer |
| 27 | +>(); |
| 28 | + |
| 29 | +world.scene = new OBC.SimpleScene(components); |
| 30 | +world.scene.setup(); |
| 31 | +world.scene.three.background = null; |
| 32 | + |
| 33 | +const container = document.getElementById("container")!; |
| 34 | +world.renderer = new OBC.SimpleRenderer(components, container); |
| 35 | +world.camera = new OBC.OrthoPerspectiveCamera(components); |
| 36 | +await world.camera.controls.setLookAt(68, 23, -8.5, 21.5, -5.5, 23); |
| 37 | + |
| 38 | +components.init(); |
| 39 | + |
| 40 | +/* MD |
| 41 | + ### 🛠️ Setting Up Fragments |
| 42 | + Now, let's configure the FragmentsManager. This will allow us to load models effortlessly and start manipulating them with ease: |
| 43 | +*/ |
| 44 | + |
| 45 | +const workerUrl = |
| 46 | + "/node_modules/@thatopen/fragments/dist/Worker/worker.mjs"; |
| 47 | +const fragments = components.get(OBC.FragmentsManager); |
| 48 | +fragments.init(workerUrl); |
| 49 | + |
| 50 | +world.camera.controls.addEventListener("rest", () => |
| 51 | + fragments.core.update(true), |
| 52 | +); |
| 53 | + |
| 54 | +world.onCameraChanged.add((camera) => { |
| 55 | + for (const [, model] of fragments.list) { |
| 56 | + model.useCamera(camera.three); |
| 57 | + } |
| 58 | + fragments.core.update(true); |
| 59 | +}); |
| 60 | + |
| 61 | +fragments.list.onItemSet.add(({ value: model }) => { |
| 62 | + model.useCamera(world.camera.three); |
| 63 | + world.scene.three.add(model.object); |
| 64 | + fragments.core.update(true); |
| 65 | +}); |
| 66 | + |
| 67 | +/* MD |
| 68 | + ### 📂 Loading Fragments Models |
| 69 | + With the core setup complete, it's time to load Fragments models into our scene. We'll load multiple models to demonstrate the picker's ability to distinguish between them: |
| 70 | +*/ |
| 71 | + |
| 72 | +const fragPaths = [ |
| 73 | + "/resources/frags/school_arq.frag", |
| 74 | + "/resources/frags/school_str.frag", |
| 75 | +]; |
| 76 | + |
| 77 | +await Promise.all( |
| 78 | + fragPaths.map(async (path) => { |
| 79 | + const modelId = path.split("/").pop()?.split(".").shift(); |
| 80 | + if (!modelId) return null; |
| 81 | + const file = await fetch(path); |
| 82 | + const buffer = await file.arrayBuffer(); |
| 83 | + return fragments.core.load(buffer, { modelId }); |
| 84 | + }), |
| 85 | +); |
| 86 | + |
| 87 | +/* MD |
| 88 | + ### ✨ Using The FastModelPicker Component |
| 89 | + Now let's set up the FastModelPicker. This component uses color coding to quickly identify which model is under the mouse cursor: |
| 90 | +*/ |
| 91 | + |
| 92 | +const pickers = components.get(OBC.FastModelPickers); |
| 93 | +const picker = pickers.get(world); |
| 94 | + |
| 95 | +/* MD |
| 96 | + ### 🎨 Enabling Debug Mode |
| 97 | + Debug mode shows a small canvas in the top-right corner displaying the color-coded render. This is useful for understanding how the component works: |
| 98 | +*/ |
| 99 | + |
| 100 | +picker.setDebugMode(true); |
| 101 | + |
| 102 | +/* MD |
| 103 | + ### 🖱️ Picking Models on Mouse Move |
| 104 | + Let's add an event listener to detect which model is under the mouse cursor as you move it: |
| 105 | +*/ |
| 106 | + |
| 107 | +const infoPanel = document.createElement("div"); |
| 108 | +infoPanel.style.position = "fixed"; |
| 109 | +infoPanel.style.top = "10px"; |
| 110 | +infoPanel.style.left = "10px"; |
| 111 | +infoPanel.style.backgroundColor = "rgba(0, 0, 0, 0.8)"; |
| 112 | +infoPanel.style.color = "white"; |
| 113 | +infoPanel.style.padding = "10px"; |
| 114 | +infoPanel.style.borderRadius = "5px"; |
| 115 | +infoPanel.style.fontFamily = "monospace"; |
| 116 | +infoPanel.style.zIndex = "10000"; |
| 117 | +infoPanel.innerHTML = "Move your mouse over the models..."; |
| 118 | +document.body.appendChild(infoPanel); |
| 119 | + |
| 120 | +container.addEventListener("pointermove", async (event) => { |
| 121 | + // Get mouse position in normalized coordinates |
| 122 | + const bounds = container.getBoundingClientRect(); |
| 123 | + const x = ((event.clientX - bounds.left) / bounds.width) * 2 - 1; |
| 124 | + const y = -((event.clientY - bounds.top) / bounds.height) * 2 + 1; |
| 125 | + const position = new THREE.Vector2(x, y); |
| 126 | + |
| 127 | + // Get model ID at mouse position |
| 128 | + const modelId = await picker.getModelAt(position); |
| 129 | + |
| 130 | + if (modelId) { |
| 131 | + infoPanel.innerHTML = `Model ID: <strong>${modelId}</strong>`; |
| 132 | + infoPanel.style.color = "lime"; |
| 133 | + } else { |
| 134 | + infoPanel.innerHTML = "No model detected"; |
| 135 | + infoPanel.style.color = "white"; |
| 136 | + } |
| 137 | +}); |
| 138 | + |
| 139 | +/* MD |
| 140 | + ### 🎯 Picking Models on Click |
| 141 | + You can also pick models on click events: |
| 142 | +*/ |
| 143 | + |
| 144 | +container.addEventListener("click", async (event) => { |
| 145 | + const bounds = container.getBoundingClientRect(); |
| 146 | + const x = ((event.clientX - bounds.left) / bounds.width) * 2 - 1; |
| 147 | + const y = -((event.clientY - bounds.top) / bounds.height) * 2 + 1; |
| 148 | + const position = new THREE.Vector2(x, y); |
| 149 | + |
| 150 | + const modelId = await picker.getModelAt(position); |
| 151 | + if (modelId) { |
| 152 | + console.log("Clicked on model:", modelId); |
| 153 | + } |
| 154 | +}); |
| 155 | + |
| 156 | +/* MD |
| 157 | + ### 📊 Performance Stats |
| 158 | + Let's add performance monitoring to see how fast the picker is: |
| 159 | +*/ |
| 160 | + |
| 161 | +const stats = new Stats(); |
| 162 | +stats.showPanel(2); |
| 163 | +document.body.append(stats.dom); |
| 164 | +stats.dom.style.left = "0px"; |
| 165 | +stats.dom.style.zIndex = "unset"; |
| 166 | + |
| 167 | +world.renderer.onBeforeUpdate.add(() => stats.begin()); |
| 168 | +world.renderer.onAfterUpdate.add(() => stats.end()); |
| 169 | + |
| 170 | +/* MD |
| 171 | + ### 💡 Tips |
| 172 | + - The FastModelPicker is much faster than raycasting for simple model identification |
| 173 | + - Debug mode is useful for understanding how the color coding works |
| 174 | + - The component automatically handles multiple models and assigns unique colors to each |
| 175 | + - You can disable debug mode by calling `picker.setDebugMode(false)` |
| 176 | +*/ |
| 177 | + |
0 commit comments