Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
c42ffdf
docs: Got started on the tutorial update
taeuscherpferd Oct 28, 2025
d7c8dd8
feat: Added an example project for working with guards
taeuscherpferd Nov 1, 2025
d5c50de
chore: continued work on the guards tutorial
taeuscherpferd Nov 3, 2025
cd45e1a
chore: Saving today's progress
taeuscherpferd Nov 4, 2025
46f9bed
chore: Added the beginning of a section for `IfFacingCamera`
taeuscherpferd Nov 6, 2025
ae0a7fb
chore: Just chipping away at progress a little more at a time
taeuscherpferd Nov 6, 2025
e3abfb8
docs: Documented another hook
taeuscherpferd Nov 7, 2025
b9f8aae
feat: Added a new section in the tutorial for useXRSessionFeatureEnabled
taeuscherpferd Nov 13, 2025
682a1a7
docs: Finished the first draft of the new tutorial
taeuscherpferd Nov 13, 2025
fa43754
docs: Final touch ups and optimizations before submitting a PR
Nov 14, 2025
b5a9626
docs: Fixed a final formatting issue with the tutorial
taeuscherpferd Nov 14, 2025
ae2fdb2
chore: PR feedback cleanup. Didn't mean to commit these
taeuscherpferd Nov 14, 2025
6462290
chore: Added the example to the workflow
taeuscherpferd Nov 14, 2025
cd1556f
chore: Adjusted path, added helpful links
taeuscherpferd Nov 14, 2025
ff78bc9
Merge branch 'main' into kokeefe/guards-and-checks-tutorial-update
taeuscherpferd Nov 14, 2025
4943208
chore: Restoring pnpm-lock
taeuscherpferd Nov 14, 2025
da01578
chore: ugh
taeuscherpferd Nov 14, 2025
bfe8aba
docs: Updating the type doc and some minor wording tweaks
taeuscherpferd Nov 14, 2025
f620bc6
docs: Fixed a bug in the example and made the very last final tweaks
taeuscherpferd Nov 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/static.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ jobs:
mkdir -p public/examples/hit-testing
mkdir -p public/examples/uikit
mkdir -p public/examples/portal
mkdir -p public/examples/guards
cp -r ./examples/minecraft/dist/* ./public/examples/minecraft
cp -r ./examples/pingpong/dist/* ./public/examples/pingpong
cp -r ./examples/rag-doll/dist/* ./public/examples/rag-doll
Expand All @@ -144,6 +145,7 @@ jobs:
cp -r ./examples/hit-testing/dist/* ./public/examples/hit-testing
cp -r ./examples/uikit/dist/* ./public/examples/uikit
cp -r ./examples/portal/dist/* ./public/examples/portal
cp -r ./examples/guards/dist/* ./public/examples/guards

- name: Upload Artifact
uses: actions/upload-artifact@v4
Expand Down
472 changes: 462 additions & 10 deletions docs/tutorials/guards.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions examples/guards/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist
25 changes: 25 additions & 0 deletions examples/guards/ColorChangingBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Box } from '@react-three/drei'
import { useXRSessionVisibilityState } from '@react-three/xr'
import { useEffect, useState } from 'react'
import * as THREE from 'three'

interface ColorChangingBoxProps {
position?: [number, number, number]
}

export const ColorChangingBox = ({ position }: ColorChangingBoxProps) => {
const [color, setColor] = useState(new THREE.Color('blue'))
const visState = useXRSessionVisibilityState()

useEffect(() => {
if (visState !== 'visible') {
setColor(new THREE.Color(Math.random() * 0xffffff))
}
}, [visState])

return (
<Box position={position} args={[0.5, 0.5, 0.5]}>
<meshBasicMaterial color={color} />
</Box>
)
}
16 changes: 16 additions & 0 deletions examples/guards/Message.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Container, Text } from '@react-three/uikit'

interface MessageProps {
message: string
}

export const Message = ({ message }: MessageProps) => {
console.log('But I am still rendered no matter what!')
return (
<group position={[-2, 2, -3]}>
<Container borderRadius={50} backgroundColor={'black'} padding={5}>
<Text color={'white'}>{message}</Text>
</Container>
</group>
)
}
20 changes: 20 additions & 0 deletions examples/guards/ShyBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Box } from '@react-three/drei'
import { IfSessionVisible } from '@react-three/xr'
import { useRef } from 'react'
import * as THREE from 'three'

interface ShyBoxProps {
position?: [number, number, number]
}

export const ShyBox = ({ position }: ShyBoxProps) => {
const boxRef = useRef<THREE.Mesh>(null)

return (
<IfSessionVisible>
<Box ref={boxRef} args={[0.5, 0.5, 0.5]} position={position}>
<meshBasicMaterial color="#268594" />
</Box>
</IfSessionVisible>
)
}
32 changes: 32 additions & 0 deletions examples/guards/SpinningBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Box } from '@react-three/drei'
import { useFrame } from '@react-three/fiber'
import { IfFacingCamera } from '@react-three/xr'
import { useRef } from 'react'
import * as THREE from 'three'

interface SpinningBoxProps {
position?: [number, number, number]
}

const cameraDirectionHelper = new THREE.Vector3(0, 0, -1)

export const SpinningBox = ({ position }: SpinningBoxProps) => {
const boxRef = useRef<THREE.Mesh>(null)

useFrame((state, delta) => {
const box = boxRef.current
if (box) {
box.rotation.y += delta
}
})

return (
<>
<IfFacingCamera direction={cameraDirectionHelper} angle={Math.PI}>
<Box ref={boxRef} args={[0.5, 0.5, 0.5]} position={position}>
<meshBasicMaterial color="orange" />
</Box>
</IfFacingCamera>
</>
)
}
53 changes: 53 additions & 0 deletions examples/guards/SupportedFeaturesPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Container, Text } from '@react-three/uikit'
import { useXRSessionFeatureEnabled } from '@react-three/xr'

interface SupportedFeaturesPanelProps {
position?: [number, number, number]
}

export const SupportedFeaturesPanel = ({ position }: SupportedFeaturesPanelProps) => {
const canUseAnchors = useXRSessionFeatureEnabled('anchors')
const canUseBoundedFloor = useXRSessionFeatureEnabled('bounded-floor')
const canUseDepthSensing = useXRSessionFeatureEnabled('depth-sensing')
const canUseDomOverlay = useXRSessionFeatureEnabled('dom-overlay')
const canUseHandTracking = useXRSessionFeatureEnabled('hand-tracking')
const canUseHitTest = useXRSessionFeatureEnabled('hit-test')
const canUseLayers = useXRSessionFeatureEnabled('layers')
const canUseLightEstimation = useXRSessionFeatureEnabled('light-estimation')
const canUseLocal = useXRSessionFeatureEnabled('local')
const canUseLocalFloor = useXRSessionFeatureEnabled('local-floor')
const canUseSecondaryViews = useXRSessionFeatureEnabled('secondary-views')
const canUseUnbounded = useXRSessionFeatureEnabled('unbounded')
const canUseViewer = useXRSessionFeatureEnabled('viewer')

const getTextColor = (enabled: boolean) => (enabled ? 'lightgreen' : 'red')

return (
<group position={position}>
<Container
width={200}
height={280}
padding={5}
backgroundColor={'#222222'}
borderRadius={0.5}
display={'flex'}
flexDirection={'column'}
>
<Text color={'white'}>{'Supported Features:'}</Text>
<Text color={getTextColor(canUseAnchors)}>{`Anchors: ${canUseAnchors}`}</Text>
<Text color={getTextColor(canUseBoundedFloor)}>{`Bounded Floor: ${canUseBoundedFloor}`}</Text>
<Text color={getTextColor(canUseDepthSensing)}>{`Depth Sensing: ${canUseDepthSensing}`}</Text>
<Text color={getTextColor(canUseDomOverlay)}>{`DOM Overlay: ${canUseDomOverlay}`}</Text>
<Text color={getTextColor(canUseHandTracking)}>{`Hand Tracking: ${canUseHandTracking}`}</Text>
<Text color={getTextColor(canUseHitTest)}>{`Hit Test: ${canUseHitTest}`}</Text>
<Text color={getTextColor(canUseLayers)}>{`Layers: ${canUseLayers}`}</Text>
<Text color={getTextColor(canUseLightEstimation)}>{`Light Estimation: ${canUseLightEstimation}`}</Text>
<Text color={getTextColor(canUseLocal)}>{`Local: ${canUseLocal}`}</Text>
<Text color={getTextColor(canUseLocalFloor)}>{`Local Floor: ${canUseLocalFloor}`}</Text>
<Text color={getTextColor(canUseSecondaryViews)}>{`Secondary Views: ${canUseSecondaryViews}`}</Text>
<Text color={getTextColor(canUseUnbounded)}>{`Unbounded: ${canUseUnbounded}`}</Text>
<Text color={getTextColor(canUseViewer)}>{`Viewer: ${canUseViewer}`}</Text>
</Container>
</group>
)
}
33 changes: 33 additions & 0 deletions examples/guards/SupportedSessionModesPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Container, Text } from '@react-three/uikit'
import { useXRSessionModeSupported } from '@react-three/xr'

interface SupportedSessionModesPanelProps {
position?: [number, number, number]
}

export const SupportedSessionModesPanel = ({ position }: SupportedSessionModesPanelProps) => {
const supportsImmersiveVR = useXRSessionModeSupported('immersive-vr')
const supportsImmersiveAR = useXRSessionModeSupported('immersive-ar')
const supportsInline = useXRSessionModeSupported('inline')

const getTextColor = (supported?: boolean) => (supported ? 'lightgreen' : 'red')

return (
<group position={position}>
<Container
width={200}
height={120}
padding={5}
backgroundColor={'#222222'}
borderRadius={0.5}
display={'flex'}
flexDirection={'column'}
>
<Text color={'white'}>{'Supported Session Modes:'}</Text>
<Text color={getTextColor(supportsImmersiveVR)}>{`Immersive VR: ${supportsImmersiveVR}`}</Text>
<Text color={getTextColor(supportsImmersiveAR)}>{`Immersive AR: ${supportsImmersiveAR}`}</Text>
<Text color={getTextColor(supportsInline)}>{`Inline: ${supportsInline}`}</Text>
</Container>
</group>
)
}
53 changes: 53 additions & 0 deletions examples/guards/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { OrbitControls, Plane } from '@react-three/drei'
import { Canvas } from '@react-three/fiber'
import { createXRStore, IfInSessionMode, IfSessionModeSupported, ShowIfSessionModeSupported, XR } from '@react-three/xr'
import * as THREE from 'three'
import { ColorChangingBox } from './ColorChangingBox.js'
import { Message } from './Message.js'
import { ShyBox } from './ShyBox.js'
import { SpinningBox } from './SpinningBox.js'
import './styles.css'
import { SupportedFeaturesPanel } from './SupportedFeaturesPanel.js'
import { SupportedSessionModesPanel } from './SupportedSessionModesPanel.js'

const store = createXRStore({ offerSession: false, emulate: false })

const axisColor = new THREE.Color('#9d3d4a')
const gridColor = new THREE.Color('#4f4f4f')

export function App() {
return (
<div className="App">
<Canvas camera={{ position: [5, 3, 5] }}>
<color attach={'background'} args={['#3f3f3f']} />
<gridHelper args={[50, 50, axisColor, gridColor]} />
<XR store={store}>
<Plane args={[10, 10]} rotation={[-Math.PI / 2, 0, 0]}>
<meshBasicMaterial color={'darkgreen'} />
</Plane>
<ShyBox position={[-2, 1, 0]} />
<SpinningBox position={[2, 1, 0]} />
<SupportedFeaturesPanel position={[2, 3, -3]} />
<SupportedSessionModesPanel position={[-2, 3.7, -3]} />
<ColorChangingBox position={[1.5, 1, -2]} />
<IfInSessionMode deny={['immersive-ar', 'immersive-vr']}>
<OrbitControls />
</IfInSessionMode>
</XR>
<ShowIfSessionModeSupported mode="immersive-vr">
<Message message="This text is only visible when VR sessions are supported" />
</ShowIfSessionModeSupported>
</Canvas>
<IfSessionModeSupported mode="immersive-vr">
<button className="enterVRButton" onClick={() => store.enterVR()}>
{'Enter VR'}
</button>
</IfSessionModeSupported>
<IfSessionModeSupported mode="immersive-ar">
<button className="enterARButton" onClick={() => store.enterAR()}>
{'Enter AR'}
</button>
</IfSessionModeSupported>
</div>
)
}
12 changes: 12 additions & 0 deletions examples/guards/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Guards</title>
<script async type="module" src="./index.tsx"></script>
</head>
<body style="touch-action: none; margin: 0; position: relative; width: 100dvw; height: 100dvh; overflow: hidden;">
<div id="root" style="position: absolute; inset: 0; display: flex; flex-direction: column;"></div>
</body>
</html>
9 changes: 9 additions & 0 deletions examples/guards/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { App } from './App.js'

createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
12 changes: 12 additions & 0 deletions examples/guards/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"dependencies": {
"@react-three/uikit": "^1.0.56",
"@react-three/xr": "workspace:~"
},
"scripts": {
"dev": "vite --host",
"build": "vite build",
"check:eslint": "eslint \"*.{ts,tsx}\"",
"fix:eslint": "eslint \"*.{ts,tsx}\" --fix"
}
}
57 changes: 57 additions & 0 deletions examples/guards/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
html {
box-sizing: border-box;
margin: 0;
padding: 0;
}

body {
margin: 0;
}

.App {
font-family: sans-serif;
text-align: center;
width: 100vw;
height: 100vh;
}

.desktopMessage {
color: white;
background-color: black;
width: 30rem;
height: 5rem;
border-radius: 1rem;
position: absolute;
display: flex;
top: 2rem;
left: 2rem;
font-size: 2rem;
font-weight: bold;
padding-left: 1rem;
padding-top: 1rem;
}

button {
position: absolute;
background: black;
border-radius: 0.5rem;
border: none;
font-weight: bold;
color: white;
padding: 1rem 2rem;
cursor: pointer;
font-size: 1.5rem;
box-shadow: 0px 0px 20px rgba(0, 0, 0, 1);
}

.enterVRButton {
bottom: 1rem;
transform: translate(-50%, 0);
left: 40%;
}

.enterARButton {
bottom: 1rem;
left: 60%;
transform: translate(-50%, 0);
}
12 changes: 12 additions & 0 deletions examples/guards/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import basicSsl from '@vitejs/plugin-basic-ssl'
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'

// https://vitejs.dev/config/
export default defineConfig({
base: '/xr/examples/guards/',
plugins: [react(), basicSsl()],
resolve: {
dedupe: ['@react-three/fiber', 'three'],
},
})
Loading