|
| 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 | +} |
0 commit comments