Skip to content

Commit c436dda

Browse files
committed
portal demo
1 parent 409b7f7 commit c436dda

File tree

16 files changed

+323
-0
lines changed

16 files changed

+323
-0
lines changed

.github/workflows/static.yml

+2
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ jobs:
5050
mkdir -p public/examples/editor
5151
mkdir -p public/examples/hit-testing
5252
mkdir -p public/examples/uikit
53+
mkdir -p public/examples/portal
5354
cp -r ./examples/minecraft/dist/* ./public/examples/minecraft
5455
cp -r ./examples/pingpong/dist/* ./public/examples/pingpong
5556
cp -r ./examples/rag-doll/dist/* ./public/examples/rag-doll
@@ -61,6 +62,7 @@ jobs:
6162
cp -r ./examples/editor/dist/* ./public/examples/editor
6263
cp -r ./examples/hit-testing/dist/* ./public/examples/hit-testing
6364
cp -r ./examples/uikit/dist/* ./public/examples/uikit
65+
cp -r ./examples/portal/dist/* ./public/examples/portal
6466
6567
- name: Upload Artifact
6668
uses: actions/upload-artifact@v4

docs/getting-started/examples.md

+3
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,7 @@ nav: 3
3939
<li>
4040
[![Screenshot from the uikit + handle demo](./uikit.gif)](https://pmndrs.github.io/xr/examples/uikit/)
4141
</li>
42+
<li>
43+
[![Screenshot from the portal demo](./portal.gif)](https://pmndrs.github.io/xr/examples/portal/)
44+
</li>
4245
</Grid>

docs/getting-started/portal.gif

10.4 MB
Loading

examples/portal/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dist

examples/portal/README.md

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
## Attributions
2+
3+
Music by <a href="https://pixabay.com/users/rockot-1947599/?utm_source=link-attribution&utm_medium=referral&utm_campaign=music&utm_content=245050">Rockot</a> from <a href="https://pixabay.com//?utm_source=link-attribution&utm_medium=referral&utm_campaign=music&utm_content=245050">Pixabay</a>
4+
5+
Lever 3D Model by [Amine.Elouneg](https://sketchfab.com/Amine.Elouneg) from Sketchfab
6+
7+
Lever Sound Effect by <a href="https://pixabay.com/users/freesound_community-46691455/?utm_source=link-attribution&utm_medium=referral&utm_campaign=music&utm_content=100823">freesound_community</a> from <a href="https://pixabay.com/sound-effects//?utm_source=link-attribution&utm_medium=referral&utm_campaign=music&utm_content=100823">Pixabay</a>
8+
9+
Sound Effect by <a href="https://pixabay.com/users/freesound_community-46691455/?utm_source=link-attribution&utm_medium=referral&utm_campaign=music&utm_content=47323">freesound_community</a> from <a href="https://pixabay.com//?utm_source=link-attribution&utm_medium=referral&utm_campaign=music&utm_content=47323">Pixabay</a>

examples/portal/app.tsx

+202
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import { Canvas, useFrame } from '@react-three/fiber'
2+
import { useHover, createXRStore, XR, noEvents, PointerEvents, IfInSessionMode } from '@react-three/xr'
3+
import { Fragment, MutableRefObject, useRef, useState } from 'react'
4+
import { BackSide, ExtrudeGeometry, Float32BufferAttribute, Mesh, RepeatWrapping, Shape, Vector2 } from 'three'
5+
import { OrbitHandles } from '@react-three/handle'
6+
import { Environment, Gltf, Mask, Texture, useMask, useTexture } from '@react-three/drei'
7+
import { Lever } from './lever.js'
8+
import { lerp } from 'three/src/math/MathUtils.js'
9+
10+
// Create a cube geometry with open top and bottom faces
11+
const shape = new Shape()
12+
shape.moveTo(-1, -1)
13+
shape.lineTo(1, -1)
14+
shape.lineTo(1, 1)
15+
shape.lineTo(-1, 1)
16+
shape.lineTo(-1, -1)
17+
18+
const extrudeSettings = {
19+
steps: 1,
20+
depth: 2,
21+
bevelEnabled: false,
22+
}
23+
24+
// Create the geometry by extruding a square shape with open top and bottom
25+
const geometry = new ExtrudeGeometry(shape, extrudeSettings)
26+
27+
// Create a custom BufferGeometry with only the side faces
28+
const positions = []
29+
const normals = []
30+
const uvs = []
31+
32+
// Extract only the side faces from the extruded geometry
33+
const positionAttr = geometry.attributes.position
34+
const normalAttr = geometry.attributes.normal
35+
const uvAttr = geometry.attributes.uv
36+
37+
// Loop through the vertices and only keep the ones for the side faces
38+
for (let i = 0; i < positionAttr.count; i++) {
39+
const normal = new Vector2(normalAttr.getX(i), normalAttr.getY(i)).length()
40+
// Only keep vertices where the normal is not pointing up or down
41+
if (Math.abs(normalAttr.getZ(i)) < 0.9) {
42+
positions.push(positionAttr.getX(i), positionAttr.getY(i), positionAttr.getZ(i))
43+
normals.push(normalAttr.getX(i), normalAttr.getY(i), normalAttr.getZ(i))
44+
uvs.push(uvAttr.getX(i), uvAttr.getY(i))
45+
}
46+
}
47+
48+
// Update the geometry with only the side faces
49+
geometry.setAttribute('position', new Float32BufferAttribute(positions, 3))
50+
geometry.setAttribute('normal', new Float32BufferAttribute(normals, 3))
51+
geometry.setAttribute('uv', new Float32BufferAttribute(uvs, 2))
52+
geometry.computeVertexNormals()
53+
54+
// Translate the geometry so that it's centered at the origin
55+
geometry.translate(0, 0, -1)
56+
geometry.rotateX(Math.PI / 2)
57+
58+
const store = createXRStore({
59+
foveation: 0,
60+
hand: {
61+
model: {
62+
renderOrder: -1,
63+
colorWrite: false,
64+
},
65+
},
66+
})
67+
68+
export function App() {
69+
const stencil = useMask(1)
70+
const openRef = useRef(false)
71+
return (
72+
<>
73+
<div
74+
style={{
75+
display: 'flex',
76+
flexDirection: 'row',
77+
gap: '1rem',
78+
position: 'absolute',
79+
zIndex: 10000,
80+
bottom: '1rem',
81+
left: '50%',
82+
transform: 'translate(-50%, 0)',
83+
}}
84+
>
85+
<button
86+
style={{
87+
background: 'white',
88+
borderRadius: '0.5rem',
89+
border: 'none',
90+
fontWeight: 'bold',
91+
color: 'black',
92+
padding: '1rem 2rem',
93+
cursor: 'pointer',
94+
fontSize: '1.5rem',
95+
boxShadow: '0px 0px 20px rgba(0,0,0,1)',
96+
}}
97+
onClick={() => store.enterAR()}
98+
>
99+
Enter AR
100+
</button>
101+
<button
102+
style={{
103+
background: 'white',
104+
borderRadius: '0.5rem',
105+
border: 'none',
106+
fontWeight: 'bold',
107+
color: 'black',
108+
padding: '1rem 2rem',
109+
cursor: 'pointer',
110+
fontSize: '1.5rem',
111+
boxShadow: '0px 0px 20px rgba(0,0,0,1)',
112+
}}
113+
onClick={() => store.enterVR()}
114+
>
115+
Enter VR
116+
</button>
117+
</div>
118+
<Canvas
119+
gl={{ stencil: true }}
120+
camera={{ position: [-1, 1, 1] }}
121+
events={noEvents}
122+
style={{ width: '100%', flexGrow: 1 }}
123+
>
124+
<XR store={store}>
125+
<PointerEvents />
126+
<OrbitHandles />
127+
<Environment preset="park" />
128+
<group position-z={-1.3}>
129+
<Lever openRef={openRef} position-z={1} scale={0.01} />
130+
<Door openRef={openRef} />
131+
<Mask id={1} scale={2 * 0.6} rotation-x={-Math.PI / 2}>
132+
<planeGeometry />
133+
</Mask>
134+
<group scale={0.6} position-y={0.6 * -(-1 + 0.41 + 0.42 * 7)}>
135+
{new Array(8).fill(undefined).map((_, i) => (
136+
<Fragment key={i}>
137+
<mesh position-y={-1 + 0.2 + 0.42 * i} scale-y={0.2} geometry={geometry}>
138+
<meshPhysicalMaterial {...stencil} metalness={0.8} roughness={0.1} side={BackSide} color="black" />
139+
</mesh>
140+
<mesh position-y={-1 + 0.41 + 0.42 * i} scale-y={0.01} geometry={geometry}>
141+
<meshPhysicalMaterial {...stencil} metalness={0.8} roughness={0.1} side={BackSide} color="white" />
142+
</mesh>
143+
</Fragment>
144+
))}
145+
<Img stencil={stencil} />
146+
</group>
147+
</group>
148+
</XR>
149+
</Canvas>
150+
</>
151+
)
152+
}
153+
154+
function Door({ openRef }: { openRef: MutableRefObject<boolean> }) {
155+
const leftRef = useRef<Mesh>(null)
156+
const rightRef = useRef<Mesh>(null)
157+
158+
useFrame((_, delta) => {
159+
if (leftRef.current == null || rightRef.current == null) {
160+
return
161+
}
162+
const leftGoalX = openRef.current ? -1 : -0.3
163+
const rightGoalX = openRef.current ? 1 : 0.3
164+
leftRef.current.position.x = lerp(leftRef.current.position.x, leftGoalX, delta)
165+
rightRef.current.position.x = lerp(rightRef.current.position.x, rightGoalX, delta)
166+
})
167+
168+
return (
169+
<>
170+
<mesh
171+
ref={leftRef}
172+
position-x={-0.3}
173+
position-y={0.01}
174+
scale={[1.05 * 0.6, 2.5 * 0.6, 2 * 0.6]}
175+
rotation-x={-Math.PI / 2}
176+
>
177+
<planeGeometry />
178+
<meshBasicMaterial colorWrite={false} />
179+
</mesh>
180+
<mesh
181+
ref={rightRef}
182+
position-x={0.3}
183+
position-y={0.01}
184+
scale={[1.05 * 0.6, 2.5 * 0.6, 2 * 0.6]}
185+
rotation-x={-Math.PI / 2}
186+
>
187+
<planeGeometry />
188+
<meshBasicMaterial colorWrite={false} />
189+
</mesh>
190+
</>
191+
)
192+
}
193+
194+
function Img({ stencil }: { stencil: ReturnType<typeof useMask> }) {
195+
const texture = useTexture('img.jpg')
196+
return (
197+
<mesh position-y={-1} scale={2} rotation-x={-Math.PI / 2}>
198+
<planeGeometry />
199+
<meshBasicMaterial {...stencil} toneMapped={false} map={texture} />
200+
</mesh>
201+
)
202+
}

examples/portal/index.html

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Document</title>
7+
<script async type="module" src="./index.tsx"></script>
8+
</head>
9+
<body style="touch-action: none; margin: 0; position: relative; width: 100dvw; height: 100dvh; overflow: hidden;">
10+
<div id="root" style="position: absolute; inset: 0; display: flex; flex-direction: column;"></div>
11+
</body>
12+
</html>

examples/portal/index.tsx

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { createRoot } from 'react-dom/client'
2+
import { App } from './app.js'
3+
import { StrictMode } from 'react'
4+
5+
createRoot(document.getElementById('root')!).render(
6+
<StrictMode>
7+
<App />
8+
</StrictMode>,
9+
)

examples/portal/lever.tsx

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
Auto-generated by: https://github.com/pmndrs/gltfjsx
3+
*/
4+
5+
import * as THREE from 'three'
6+
import React, { MutableRefObject, useRef } from 'react'
7+
import { useGLTF } from '@react-three/drei'
8+
import { GLTF } from 'three/examples/jsm/loaders/GLTFLoader.js'
9+
import { JSX } from 'react/jsx-runtime'
10+
import { defaultApply, Handle } from '@react-three/handle'
11+
import { degToRad } from 'three/src/math/MathUtils.js'
12+
13+
type GLTFResult = GLTF & {
14+
nodes: {
15+
body: THREE.Mesh
16+
lever: THREE.Mesh
17+
}
18+
materials: {
19+
lver: THREE.MeshStandardMaterial
20+
}
21+
}
22+
23+
export function Lever({ openRef, ...props }: JSX.IntrinsicElements['group'] & { openRef: MutableRefObject<boolean> }) {
24+
const { nodes, materials } = useGLTF('lever.glb') as any as GLTFResult
25+
return (
26+
<group {...props} dispose={null}>
27+
<mesh
28+
geometry={nodes.body.geometry}
29+
material={materials.lver}
30+
position={[-5.082, 6.427, 0]}
31+
rotation={[0, 0, Math.PI / 2]}
32+
/>
33+
<group position={[-0.092, 4.208, 0.135]} rotation={[0, 0, Math.PI / 2]}>
34+
<Handle
35+
translate="as-rotate"
36+
apply={(state, target) => {
37+
if (state.current.rotation.z > degToRad(-20)) {
38+
openRef.current = false
39+
}
40+
if (state.current.rotation.z < degToRad(-115)) {
41+
openRef.current = true
42+
}
43+
defaultApply(state, target)
44+
}}
45+
scale={false}
46+
multitouch={false}
47+
rotate={{ x: false, y: false, z: [degToRad(-135), 0] }}
48+
>
49+
<mesh geometry={nodes.lever.geometry} material={materials.lver} />
50+
</Handle>
51+
</group>
52+
</group>
53+
)
54+
}
55+
56+
useGLTF.preload('lever.glb')

examples/portal/package.json

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"dependencies": {
3+
"@react-three/xr": "workspace:~",
4+
"@react-three/handle": "workspace:~"
5+
},
6+
"scripts": {
7+
"dev": "vite --host"
8+
}
9+
}

examples/portal/public/img.jpg

359 KB
Loading
179 KB
Binary file not shown.

examples/portal/public/lever.glb

3.45 MB
Binary file not shown.

examples/portal/public/normal.jpg

38.5 KB
Loading
148 KB
Binary file not shown.

examples/portal/vite.config.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { defineConfig } from 'vite'
2+
import path from 'path'
3+
import react from '@vitejs/plugin-react'
4+
import basicSsl from '@vitejs/plugin-basic-ssl'
5+
6+
// https://vitejs.dev/config/
7+
export default defineConfig({
8+
plugins: [react(), basicSsl()],
9+
base: '/xr/examples/portal/',
10+
resolve: {
11+
alias: [
12+
{ find: '@react-three/xr', replacement: path.resolve(__dirname, '../../packages/react/xr/src/index.ts') },
13+
{
14+
find: '@pmndrs/pointer-events',
15+
replacement: path.resolve(__dirname, '../../packages/pointer-events/src/index.ts'),
16+
},
17+
],
18+
dedupe: ['@react-three/fiber', 'three'],
19+
},
20+
})

0 commit comments

Comments
 (0)