Skip to content

Commit

Permalink
feat: spherecast for xr interactions
Browse files Browse the repository at this point in the history
  • Loading branch information
bbohlender committed Jun 25, 2024
1 parent bcdaf7c commit ef77cf6
Show file tree
Hide file tree
Showing 8 changed files with 118 additions and 32 deletions.
38 changes: 32 additions & 6 deletions examples/vanilla/index.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,42 @@
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))
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,
Expand All @@ -27,7 +46,14 @@ const root = new Fullscreen(renderer, undefined, {
backgroundColor: 'red',
overflow: 'scroll',
})
camera.add(root)
scene.add(root)

setTimeout(() => {
const intersections: Array<Intersection> = []
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))
Expand Down Expand Up @@ -66,7 +92,6 @@ function updateSize() {
renderer.setPixelRatio(window.devicePixelRatio)
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
root.updateSize()
}

updateSize()
Expand All @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions packages/uikit/src/components/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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
Expand Down
10 changes: 2 additions & 8 deletions packages/uikit/src/components/custom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<
Expand Down Expand Up @@ -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(() => {
Expand Down
4 changes: 2 additions & 2 deletions packages/uikit/src/components/icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 3 additions & 2 deletions packages/uikit/src/components/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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(() =>
Expand Down
4 changes: 2 additions & 2 deletions packages/uikit/src/components/svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down
13 changes: 10 additions & 3 deletions packages/uikit/src/panel/instanced-panel-mesh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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(() => {
Expand Down
72 changes: 65 additions & 7 deletions packages/uikit/src/panel/interaction-panel-mesh.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -20,8 +20,66 @@ const sides: Array<Plane> = [

const distancesHelper = [0, 0, 0, 0]

declare module 'three' {
interface Object3D {
spherecast?(sphere: Sphere, intersects: Array<Intersection>): void
}
}

export function makePanelSpherecast(mesh: Mesh): Exclude<Mesh['spherecast'], undefined> {
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<Intersection<Object3D<Object3DEventMap>>>) => {
return (raycaster, intersects) => {
const matrixWorld = mesh.matrixWorld
planeHelper.constant = 0
planeHelper.normal.set(0, 0, 1)
Expand Down Expand Up @@ -56,21 +114,21 @@ export function makePanelRaycast(mesh: Mesh): Mesh['raycast'] {
}
}

export function makeClippedRaycast(
export function makeClippedCast<T extends Mesh['raycast'] | Exclude<Mesh['spherecast'], undefined>>(
mesh: Mesh,
fn: Mesh['raycast'],
fn: T,
rootObject: Object3DRef,
clippingRect: Signal<ClippingRect | undefined> | undefined,
orderInfo: Signal<OrderInfo | undefined>,
): Mesh['raycast'] {
return (raycaster: Raycaster, intersects: Intersection<Object3D<Object3DEventMap>>[]) => {
) {
return (raycaster: Parameters<T>[0], intersects: Parameters<T>[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--) {
Expand Down

0 comments on commit ef77cf6

Please sign in to comment.