From ef77cf6181a57fa39ceae3a9f9b85b90f028b112 Mon Sep 17 00:00:00 2001 From: Bela Bohlender Date: Tue, 25 Jun 2024 18:41:14 +0200 Subject: [PATCH] feat: spherecast for xr interactions --- examples/vanilla/index.ts | 38 ++++++++-- packages/uikit/src/components/content.ts | 4 +- packages/uikit/src/components/custom.ts | 10 +-- packages/uikit/src/components/icon.ts | 4 +- packages/uikit/src/components/image.ts | 5 +- packages/uikit/src/components/svg.ts | 4 +- .../uikit/src/panel/instanced-panel-mesh.ts | 13 +++- .../uikit/src/panel/interaction-panel-mesh.ts | 72 +++++++++++++++++-- 8 files changed, 118 insertions(+), 32 deletions(-) diff --git a/examples/vanilla/index.ts b/examples/vanilla/index.ts index fc4c9a0a..9f608293 100644 --- a/examples/vanilla/index.ts +++ b/examples/vanilla/index.ts @@ -1,12 +1,23 @@ -import { AmbientLight, PerspectiveCamera, Scene, WebGLRenderer } from 'three' -import { reversePainterSortStable, Container, Fullscreen, Image, Text, Svg, Content } from '@pmndrs/uikit' +import { + AmbientLight, + Intersection, + Mesh, + PerspectiveCamera, + Scene, + Sphere, + SphereGeometry, + Vector3, + WebGLRenderer, +} from 'three' +import { reversePainterSortStable, Container, Fullscreen, Image, Text, Svg, Content, Root } from '@pmndrs/uikit' import { Delete } from '@pmndrs/uikit-lucide' import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js' +import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' // init const camera = new PerspectiveCamera(70, 1, 0.01, 100) -camera.position.z = 10 +camera.position.z = 5 const scene = new Scene() scene.add(new AmbientLight(undefined, 2)) @@ -14,10 +25,18 @@ scene.add(camera) const canvas = document.getElementById('root') as HTMLCanvasElement +const controls = new OrbitControls(camera, canvas) + const renderer = new WebGLRenderer({ antialias: true, canvas }) +const position = new Vector3(0, 0, 0.199) +const sphere = new Sphere(position, 0.2) +const sphereMesh = new Mesh(new SphereGeometry(0.2)) +sphereMesh.position.copy(position) +scene.add(sphereMesh) + //UI -const root = new Fullscreen(renderer, undefined, { +const root = new Root(camera, renderer, { flexDirection: 'row', gap: 30, width: 1000, @@ -27,7 +46,14 @@ const root = new Fullscreen(renderer, undefined, { backgroundColor: 'red', overflow: 'scroll', }) -camera.add(root) +scene.add(root) + +setTimeout(() => { + const intersections: Array = [] + root.internals.interactionPanel.spherecast?.(sphere, intersections) + console.log(intersections) +}, 1000) + const c = new Content({ flexShrink: 0, height: 100, backgroundColor: 'black' }) const loader = new GLTFLoader() loader.load('example.glb', (gltf) => c.add(gltf.scene)) @@ -66,7 +92,6 @@ function updateSize() { renderer.setPixelRatio(window.devicePixelRatio) camera.aspect = window.innerWidth / window.innerHeight camera.updateProjectionMatrix() - root.updateSize() } updateSize() @@ -79,6 +104,7 @@ function animation(time: number) { const delta = prev == null ? 0 : time - prev prev = time + controls.update(delta) root.update(delta) renderer.render(scene, camera) diff --git a/packages/uikit/src/components/content.ts b/packages/uikit/src/components/content.ts index 043ffd08..0eb22700 100644 --- a/packages/uikit/src/components/content.ts +++ b/packages/uikit/src/components/content.ts @@ -27,7 +27,7 @@ import { PanelGroupProperties, computedPanelGroupDependencies } from '../panel/i import { createInteractionPanel } from '../panel/instanced-panel-mesh.js' import { Box3, Material, Mesh, Object3D, Vector3 } from 'three' import { darkPropertyTransformers } from '../dark.js' -import { getDefaultPanelMaterialConfig, makeClippedRaycast } from '../panel/index.js' +import { getDefaultPanelMaterialConfig, makeClippedCast } from '../panel/index.js' import { MergedProperties, computedInheritableProperty } from '../properties/index.js' import { KeepAspectRatioProperties } from './image.js' @@ -215,7 +215,7 @@ function createMeasureContent( setupRenderOrder(object, root, orderInfo) object.material.clippingPlanes = clippingPlanes object.material.needsUpdate = true - object.raycast = makeClippedRaycast(object, object.raycast, root.object, parentClippingRect, orderInfo) + object.raycast = makeClippedCast(object, object.raycast, root.object, parentClippingRect, orderInfo) } }) const parent = content.parent diff --git a/packages/uikit/src/components/custom.ts b/packages/uikit/src/components/custom.ts index 78e4354d..59b01d3c 100644 --- a/packages/uikit/src/components/custom.ts +++ b/packages/uikit/src/components/custom.ts @@ -24,7 +24,7 @@ import { Listeners, setupLayoutListeners, setupClippedListeners } from '../liste import { Object3DRef, ParentContext } from '../context.js' import { FrontSide, Material, Mesh } from 'three' import { darkPropertyTransformers } from '../dark.js' -import { ShadowProperties, makeClippedRaycast } from '../panel/index.js' +import { ShadowProperties, makeClippedCast } from '../panel/index.js' export type InheritableCustomContainerProperties = WithClasses< WithConditionals< @@ -106,13 +106,7 @@ export function createCustomContainer( }), ) } - mesh.raycast = makeClippedRaycast( - mesh, - mesh.raycast, - parentContext.root.object, - parentContext.clippingRect, - orderInfo, - ) + mesh.raycast = makeClippedCast(mesh, mesh.raycast, parentContext.root.object, parentContext.clippingRect, orderInfo) setupRenderOrder(mesh, parentContext.root, orderInfo) subscriptions.push( effect(() => { diff --git a/packages/uikit/src/components/icon.ts b/packages/uikit/src/components/icon.ts index a5792a05..f6f40e9f 100644 --- a/packages/uikit/src/components/icon.ts +++ b/packages/uikit/src/components/icon.ts @@ -21,7 +21,7 @@ import { keepAspectRatioPropertyTransformer, } from './utils.js' import { Initializers, Subscriptions, fitNormalizedContentInside } from '../utils.js' -import { makeClippedRaycast } from '../panel/interaction-panel-mesh.js' +import { makeClippedCast } from '../panel/interaction-panel-mesh.js' import { computedIsClipped, createGlobalClippingPlanes } from '../clipping.js' import { setupLayoutListeners, setupClippedListeners } from '../listeners.js' import { createActivePropertyTransfomers } from '../active.js' @@ -187,7 +187,7 @@ function createIconGroup( geometry.computeBoundingBox() const mesh = new Mesh(geometry, material) mesh.matrixAutoUpdate = false - mesh.raycast = makeClippedRaycast( + mesh.raycast = makeClippedCast( mesh, mesh.raycast, parentContext.root.object, diff --git a/packages/uikit/src/components/image.ts b/packages/uikit/src/components/image.ts index cd1b405b..d5549fc9 100644 --- a/packages/uikit/src/components/image.ts +++ b/packages/uikit/src/components/image.ts @@ -50,7 +50,7 @@ import { import { MergedProperties } from '../properties/merged.js' import { Initializers, readReactive, unsubscribeSubscriptions } from '../utils.js' import { setupImmediateProperties } from '../properties/immediate.js' -import { makeClippedRaycast, makePanelRaycast } from '../panel/interaction-panel-mesh.js' +import { makeClippedCast, makePanelRaycast, makePanelSpherecast } from '../panel/interaction-panel-mesh.js' import { computedIsClipped, computedClippingRect, createGlobalClippingPlanes } from '../clipping.js' import { setupLayoutListeners, setupClippedListeners } from '../listeners.js' import { computedInheritableProperty } from '../properties/utils.js' @@ -261,7 +261,8 @@ function createImageMesh( root, initializers, ) - mesh.raycast = makeClippedRaycast(mesh, makePanelRaycast(mesh), root.object, parentContext.clippingRect, orderInfo) + mesh.raycast = makeClippedCast(mesh, makePanelRaycast(mesh), root.object, parentContext.clippingRect, orderInfo) + mesh.spherecast = makeClippedCast(mesh, makePanelSpherecast(mesh), root.object, parentContext.clippingRect, orderInfo) setupRenderOrder(mesh, root, orderInfo) const objectFit = computedInheritableProperty(propertiesSignal, 'objectFit', defaultImageFit) initializers.push(() => diff --git a/packages/uikit/src/components/svg.ts b/packages/uikit/src/components/svg.ts index d7673a66..739680bb 100644 --- a/packages/uikit/src/components/svg.ts +++ b/packages/uikit/src/components/svg.ts @@ -30,7 +30,7 @@ import { loadResourceWithParams, } from './utils.js' import { ColorRepresentation, Initializers, fitNormalizedContentInside, readReactive } from '../utils.js' -import { makeClippedRaycast } from '../panel/interaction-panel-mesh.js' +import { makeClippedCast } from '../panel/interaction-panel-mesh.js' import { computedIsClipped, computedClippingRect, ClippingRect, createGlobalClippingPlanes } from '../clipping.js' import { setupLayoutListeners, setupClippedListeners } from '../listeners.js' import { createActivePropertyTransfomers } from '../active.js' @@ -299,7 +299,7 @@ async function loadSvg( box3Helper.union(geometry.boundingBox!) const mesh = new Mesh(geometry, material) mesh.matrixAutoUpdate = false - mesh.raycast = makeClippedRaycast(mesh, mesh.raycast, root.object, clippedRect, orderInfo) + mesh.raycast = makeClippedCast(mesh, mesh.raycast, root.object, clippedRect, orderInfo) setupRenderOrder(mesh, root, orderInfo) mesh.userData.color = path.color mesh.scale.y = -1 diff --git a/packages/uikit/src/panel/instanced-panel-mesh.ts b/packages/uikit/src/panel/instanced-panel-mesh.ts index f5c896c0..48a475fa 100644 --- a/packages/uikit/src/panel/instanced-panel-mesh.ts +++ b/packages/uikit/src/panel/instanced-panel-mesh.ts @@ -2,8 +2,8 @@ import { Box3, InstancedBufferAttribute, Mesh, Object3DEventMap, Sphere, Vector2 import { createPanelGeometry, panelGeometry } from './utils.js' import { instancedPanelDepthMaterial, instancedPanelDistanceMaterial } from './panel-material.js' import { Signal, effect } from '@preact/signals-core' -import { Initializers, Subscriptions } from '../utils.js' -import { makeClippedRaycast, makePanelRaycast } from './interaction-panel-mesh.js' +import { Initializers } from '../utils.js' +import { makeClippedCast, makePanelRaycast, makePanelSpherecast } from './interaction-panel-mesh.js' import { OrderInfo } from '../order.js' import { ClippingRect } from '../clipping.js' import { RootContext } from '../context.js' @@ -17,7 +17,14 @@ export function createInteractionPanel( ): Mesh { const panel = new Mesh(panelGeometry) panel.matrixAutoUpdate = false - panel.raycast = makeClippedRaycast(panel, makePanelRaycast(panel), rootContext.object, parentClippingRect, orderInfo) + panel.raycast = makeClippedCast(panel, makePanelRaycast(panel), rootContext.object, parentClippingRect, orderInfo) + panel.spherecast = makeClippedCast( + panel, + makePanelSpherecast(panel), + rootContext.object, + parentClippingRect, + orderInfo, + ) panel.visible = false initializers.push(() => effect(() => { diff --git a/packages/uikit/src/panel/interaction-panel-mesh.ts b/packages/uikit/src/panel/interaction-panel-mesh.ts index 62ec4ab5..d1a7f53f 100644 --- a/packages/uikit/src/panel/interaction-panel-mesh.ts +++ b/packages/uikit/src/panel/interaction-panel-mesh.ts @@ -1,4 +1,4 @@ -import { Intersection, Mesh, Object3D, Object3DEventMap, Plane, Raycaster, Vector2, Vector3 } from 'three' +import { Intersection, Mesh, Object3D, Object3DEventMap, Plane, Raycaster, Sphere, Vector2, Vector3 } from 'three' import { ClippingRect } from '../clipping.js' import { Signal } from '@preact/signals-core' import { OrderInfo } from '../order.js' @@ -20,8 +20,66 @@ const sides: Array = [ const distancesHelper = [0, 0, 0, 0] +declare module 'three' { + interface Object3D { + spherecast?(sphere: Sphere, intersects: Array): void + } +} + +export function makePanelSpherecast(mesh: Mesh): Exclude { + return (sphere, intersects) => { + const matrixWorld = mesh.matrixWorld + planeHelper.constant = 0 + planeHelper.normal.set(0, 0, 1) + planeHelper.applyMatrix4(matrixWorld) + + planeHelper.projectPoint(sphere.center, vectorHelper) + + if (vectorHelper.distanceToSquared(sphere.center) > sphere.radius * sphere.radius) { + return + } + + const normal = planeHelper.normal.clone() + + for (let i = 0; i < 4; i++) { + const side = sides[i] + planeHelper.copy(side).applyMatrix4(matrixWorld) + + let distance = planeHelper.distanceToPoint(vectorHelper) + if (distance < 0) { + if (Math.abs(distance) > sphere.radius) { + return + } + //clamp point + planeHelper.projectPoint(vectorHelper, vectorHelper) + distance = 0 + } + distancesHelper[i] = distance + } + + const distance = sphere.center.distanceTo(vectorHelper) + + if (distance > sphere.radius) { + return + } + + console.log(distancesHelper) + + intersects.push({ + distance, + object: mesh, + point: vectorHelper.clone(), + uv: new Vector2( + distancesHelper[0] / (distancesHelper[0] + distancesHelper[1]), + distancesHelper[3] / (distancesHelper[2] + distancesHelper[3]), + ), + normal, + }) + } +} + export function makePanelRaycast(mesh: Mesh): Mesh['raycast'] { - return (raycaster: Raycaster, intersects: Array>>) => { + return (raycaster, intersects) => { const matrixWorld = mesh.matrixWorld planeHelper.constant = 0 planeHelper.normal.set(0, 0, 1) @@ -56,21 +114,21 @@ export function makePanelRaycast(mesh: Mesh): Mesh['raycast'] { } } -export function makeClippedRaycast( +export function makeClippedCast>( mesh: Mesh, - fn: Mesh['raycast'], + fn: T, rootObject: Object3DRef, clippingRect: Signal | undefined, orderInfo: Signal, -): Mesh['raycast'] { - return (raycaster: Raycaster, intersects: Intersection>[]) => { +) { + return (raycaster: Parameters[0], intersects: Parameters[1]) => { const obj = rootObject instanceof Object3D ? rootObject : rootObject.current if (obj == null || orderInfo.value == null) { return } const { majorIndex, minorIndex, elementType } = orderInfo.value const oldLength = intersects.length - fn.call(mesh, raycaster, intersects) + ;(fn as any).call(mesh, raycaster, intersects) const clippingPlanes = clippingRect?.value?.planes const outerMatrixWorld = obj.matrixWorld outer: for (let i = intersects.length - 1; i >= oldLength; i--) {