aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLibravatar Silas Bartha <silas@exvacuum.dev>2025-02-10 01:29:10 -0500
committerLibravatar Silas Bartha <silas@exvacuum.dev>2025-02-10 01:29:10 -0500
commit55a0a3de6550f0142b79ab40645c20b465ddded8 (patch)
treecc767e6442ad71d7be782b0890726528107a41f0
parent848d2b02064e2454edb85cd016e7b6840ab77e36 (diff)
first person controls
-rw-r--r--assets/terrain.glbbin0 -> 39846888 bytes
-rw-r--r--package.json3
-rw-r--r--public/vite.svg1
-rw-r--r--src/App.jsx64
-rw-r--r--src/components/chatbubble.jsx42
-rw-r--r--src/components/ground.jsx17
-rw-r--r--src/components/notes.jsx14
-rw-r--r--src/components/player.jsx78
-rw-r--r--src/main.jsx1
-rw-r--r--src/style.scss41
-rw-r--r--yarn.lock51
11 files changed, 284 insertions, 28 deletions
diff --git a/assets/terrain.glb b/assets/terrain.glb
new file mode 100644
index 0000000..4ffde3c
--- /dev/null
+++ b/assets/terrain.glb
Binary files differ
diff --git a/package.json b/package.json
index 3c4945c..94e77cb 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,10 @@
"dependencies": {
"@react-three/drei": "^9.121.4",
"@react-three/fiber": "^9.0.0-rc.7",
+ "@react-three/rapier": "^1.5.0",
"@types/three": "^0.173.0",
+ "color": "^4.2.3",
+ "lodash.debounce": "^4.0.8",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"sass": "^1.84.0",
diff --git a/public/vite.svg b/public/vite.svg
deleted file mode 100644
index e7b8dfb..0000000
--- a/public/vite.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg> \ No newline at end of file
diff --git a/src/App.jsx b/src/App.jsx
index ebd5d36..25fe834 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,15 +1,63 @@
import './style.scss'
-import { Canvas } from '@react-three/fiber';
-import ChatBubble from './components/chatbubble';
+import { Canvas, useFrame } from '@react-three/fiber';
+import Notes from './components/notes';
+import Player from './components/player';
+import Ground from './components/ground';
+import React, { Suspense, useContext, useEffect, useState } from 'react';
+import { Physics } from '@react-three/rapier';
+
+export const AppContext = React.createContext(null);
+
+function KeyPressedClearer() {
+ const { setKeysPressed } = useContext(AppContext);
+ useFrame((_s, _d) => setKeysPressed([]));
+ return <></>
+}
function App() {
+ const [keys, setKeys] = useState([]);
+ const [keysPressed, setKeysPressed] = useState([]);
+ const playerKeyDown = (event) => {
+ setKeys(keys => {
+ if(!keys.includes(event.code)) {
+ setKeysPressed(keysPressed => [...keysPressed, event.code]);
+ }
+ return [...keys, event.code]
+ });
+ }
+ const playerKeyUp = (event) => {
+ setKeys(keys => keys.filter(k => k != event.code));
+ }
+ useEffect(() => {
+ window.addEventListener('keydown', playerKeyDown);
+ window.addEventListener('keyup', playerKeyUp);
+ return () => {
+ window.removeEventListener('keydown', playerKeyDown);
+ window.removeEventListener('keyup', playerKeyUp);
+ };
+ }, []);
return (
- <Canvas>
- <ambientLight intensity={Math.PI / 2} />
- <spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} decay={0} intensity={Math.PI} />
- <pointLight position={[-10, -10, -10]} decay={0} intensity={Math.PI} />
- <ChatBubble position={[0, 0, 0]} />
- </Canvas>
+ <AppContext.Provider value={{ keys: keys, keysPressed: keysPressed, setKeysPressed: setKeysPressed }}>
+ <Canvas shadows>
+ <KeyPressedClearer />
+ <Suspense>
+ <Physics timeStep={1/60}>
+ <directionalLight
+ castShadow
+ position={[100, 100, 100]}
+ lookAt={[0, 0, 0]}
+ intensity={Math.PI / 2}
+ shadow-mapSize-height={2048}
+ shadow-mapSize-width={2048}
+ />
+ <ambientLight intensity={Math.PI / 4} />
+ <Notes />
+ <Player />
+ <Ground />
+ </Physics>
+ </Suspense>
+ </Canvas>
+ </AppContext.Provider>
)
}
diff --git a/src/components/chatbubble.jsx b/src/components/chatbubble.jsx
index b3f4ad5..a1ef114 100644
--- a/src/components/chatbubble.jsx
+++ b/src/components/chatbubble.jsx
@@ -1,25 +1,51 @@
-import { useRef, useState } from 'react'
+import { useContext, useRef, useState } from 'react'
import * as everforest from '../_everforest.module.scss'
import { useGLTF } from '@react-three/drei';
-import { useFrame } from '@react-three/fiber';
+import { useFrame, useThree } from '@react-three/fiber';
+import { Html } from '@react-three/drei';
+import { AppContext } from '../App';
+import Color from 'color';
+import { Vector3 } from 'three';
-export default function ChatBubble(props) {
+export default function ChatBubble({ position, text }) {
const meshRef = useRef();
const [hovered, setHovered] = useState(false);
+ const [activatable, setActivatable] = useState(false);
const [active, setActive] = useState(false);
- useFrame((_, delta) => (meshRef.current.rotation.y += delta));
- const {nodes} = useGLTF('../assets/message-bubble.glb');
+ const { keysPressed } = useContext(AppContext);
+ const { camera } = useThree();
+ useFrame((_, delta) => {
+ if (active) {
+ meshRef.current.rotation.y += delta;
+ }
+ if(hovered) {
+ let cameraPos = new Vector3();
+ camera.getWorldPosition(cameraPos);
+ setActivatable(cameraPos.distanceToSquared(meshRef.current.position) < 9);
+ } else {
+ setActivatable(false);
+ }
+ if (keysPressed.includes('KeyE') && activatable) {
+ setActive(!active);
+ }
+ });
+ const { nodes } = useGLTF('../assets/message-bubble.glb');
+ let color = Color(active ? everforest.blue : everforest.orange);
+ if (activatable) {
+ color = color.lighten(.1);
+ }
return (
<mesh
- {...props}
+ position={position}
ref={meshRef}
scale={active ? 3 : 2}
- onClick={(_) => setActive(!active)}
geometry={nodes.Curve.geometry}
+ castShadow
onPointerOver={(_) => setHovered(true)}
onPointerOut={(_) => setHovered(false)}>
- <meshStandardMaterial color={active ? everforest.blue : everforest.orange} />
+ <meshStandardMaterial color={color.toString()} />
+ {active && <Html center position={[0, .5, 0]} className='unselectable textPopup'><span>{text}</span></Html>}
</mesh>
)
}
diff --git a/src/components/ground.jsx b/src/components/ground.jsx
new file mode 100644
index 0000000..4042538
--- /dev/null
+++ b/src/components/ground.jsx
@@ -0,0 +1,17 @@
+import { useRef } from 'react';
+import * as everforest from '../_everforest.module.scss'
+import { RigidBody } from '@react-three/rapier';
+import { DoubleSide } from 'three';
+import { useGLTF } from '@react-three/drei';
+
+export default function Ground() {
+ const meshRef = useRef();
+ const { nodes } = useGLTF('../assets/terrain.glb');
+ return (
+ <RigidBody type='fixed' colliders="trimesh">
+ <mesh ref={meshRef} position={[0, 0, 0]} rotation={[-Math.PI / 2, 0, 0]} receiveShadow geometry={nodes.Plane.geometry}>
+ <meshStandardMaterial color={everforest.yellow} side={DoubleSide}/>
+ </mesh>
+ </RigidBody>
+ );
+}
diff --git a/src/components/notes.jsx b/src/components/notes.jsx
new file mode 100644
index 0000000..0dd267a
--- /dev/null
+++ b/src/components/notes.jsx
@@ -0,0 +1,14 @@
+import ChatBubble from "./chatbubble";
+
+const chatbubbles = [
+ {position: [0,0,-3], text: "ugh. really struggling with double bleeds in my ankles. makes it hard to do very much of anything, let alone focus for my hobbies"},
+ {position: [-1,0,-5], text: "reddit is everywhere on google and i am sick of it... why can't there be a good forum site?"},
+];
+
+export default function Notes() {
+ return (<>
+ {chatbubbles.map((chatbubble, index) =>
+ <ChatBubble key={index} position={chatbubble.position} text={chatbubble.text}/>
+ )}
+ </>);
+}
diff --git a/src/components/player.jsx b/src/components/player.jsx
new file mode 100644
index 0000000..bd8e392
--- /dev/null
+++ b/src/components/player.jsx
@@ -0,0 +1,78 @@
+import { Capsule, PerspectiveCamera, PointerLockControls } from "@react-three/drei";
+import { useFrame } from "@react-three/fiber";
+import { useContext, useEffect, useRef } from "react";
+import { AppContext } from "../App";
+import { CapsuleCollider, RapierCollider, RapierRigidBody, RigidBody, useRapier, vec3 } from "@react-three/rapier";
+import { quat } from "@react-three/rapier";
+import { Euler, Object3D, Vector3 } from "three";
+import { useBeforePhysicsStep } from "@react-three/rapier";
+
+const _movespeed = 3.0;
+
+export default function Player() {
+ const controlsRef = useRef();
+ const { keys } = useContext(AppContext);
+ const rapier = useRapier();
+ const controller = useRef();
+ const collider = useRef();
+ const rigidbody = useRef();
+ const camera = useRef();
+
+ const refState = useRef({
+ grounded: false,
+ jumping: false,
+ velocity: vec3(),
+ });
+
+ useEffect(() => {
+ const c = rapier.world.createCharacterController(0.1);
+ c.setApplyImpulsesToDynamicBodies(true);
+ c.setCharacterMass(0.2);
+ controller.current = c;
+ }, [rapier]);
+
+ useBeforePhysicsStep((world) => {
+ if (controller.current && rigidbody.current && collider.current) {
+ const move_axis_x = +(keys.includes('KeyD')) - +(keys.includes('KeyA'));
+ const move_axis_z = +(keys.includes('KeyW')) - +(keys.includes('KeyS'));
+
+ const { velocity } = refState.current;
+ const position = vec3(rigidbody.current.translation());
+ const movement = vec3();
+
+ const forward = new Vector3();
+ camera.current.getWorldDirection(forward);
+ const left = new Vector3().crossVectors(forward, camera.current.up);
+
+ movement.x += move_axis_z * world.timestep * _movespeed * forward.x;
+ movement.z += move_axis_z * world.timestep * _movespeed * forward.z;
+ movement.x += move_axis_x * world.timestep * _movespeed * left.x;
+ movement.z += move_axis_x * world.timestep * _movespeed * left.z;
+
+ if (refState.current.grounded) {
+ velocity.y = 0;
+ } else {
+ velocity.y -= 9.81 * world.timestep * world.timestep;
+ }
+
+ movement.add(velocity);
+
+ controller.current.computeColliderMovement(collider.current, movement);
+ refState.current.grounded = controller.current.computedGrounded();
+
+ let correctedMovement = controller.current.computedMovement();
+ position.add(vec3(correctedMovement));
+
+ rigidbody.current.setNextKinematicTranslation(position);
+ }
+ });
+
+ return (
+ <RigidBody type="kinematicPosition" colliders={false} ref={rigidbody} position={[0, 2, 0]}>
+ <PerspectiveCamera makeDefault position={[0, .9, 0]} fov={90} ref={camera} />
+ <PointerLockControls ref={controlsRef} />
+ <CapsuleCollider ref={collider} args={[1, 0.5]} />
+ </RigidBody>
+ );
+}
+
diff --git a/src/main.jsx b/src/main.jsx
index 3d9da8a..97bc548 100644
--- a/src/main.jsx
+++ b/src/main.jsx
@@ -5,5 +5,6 @@ import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
+ <div className='dot'/>
</StrictMode>,
)
diff --git a/src/style.scss b/src/style.scss
index 1bc4d97..23c3679 100644
--- a/src/style.scss
+++ b/src/style.scss
@@ -23,6 +23,17 @@
/// }}}
+.dot {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 5px;
+ height: 5px;
+ border-radius: 50%;
+ transform: translate3d(-50%, -50%, 0);
+ border: 2px solid white;
+}
+
html,
body,
#root {
@@ -39,15 +50,6 @@ html {
height: 100%;
}
-/* body { */
-/* color: everforest.$fg; */
-/* background-color: everforest.$bg1; */
-/* margin: 0 auto; */
-/* max-width: 800px; */
-/* padding: 10px; */
-/* min-height: 100%; */
-/* } */
-
a {
color: everforest.$blue;
}
@@ -93,3 +95,24 @@ pre table {
color: everforest.$orange;
}
}
+
+.textPopup {
+ /* text-align: center; */
+ pointer-events: none;
+ background-color: everforest.$bg1;
+ color: everforest.$fg;
+ border-radius: 5px;
+ font-size: 15px;
+ width: 200px;
+ max-width: 50vw;
+ padding: 5px;
+}
+
+.unselectable {
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
diff --git a/yarn.lock b/yarn.lock
index 8ca67f3..da6f679 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -171,6 +171,11 @@
"@babel/helper-string-parser" "^7.25.9"
"@babel/helper-validator-identifier" "^7.25.9"
+"@dimforge/rapier3d-compat@0.14.0":
+ version "0.14.0"
+ resolved "https://registry.yarnpkg.com/@dimforge/rapier3d-compat/-/rapier3d-compat-0.14.0.tgz#c6148f743aa99de320231527c466c1a48ada4c9f"
+ integrity sha512-/uHrUzS+CRQ+NQrrJCEDUkhwHlNsAAexbNXgbN9sHY+GwR+SFFAFrxRr8Llf5/AJZzqiLANdQIfJ63Cw4gJVqw==
+
"@esbuild/aix-ppc64@0.24.2":
version "0.24.2"
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz#38848d3e25afe842a7943643cbcd387cc6e13461"
@@ -615,6 +620,15 @@
suspend-react "^0.1.3"
zustand "^4.1.2"
+"@react-three/rapier@^1.5.0":
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/@react-three/rapier/-/rapier-1.5.0.tgz#f6af5b1dd6895a73df0d09e15576eed1f0398379"
+ integrity sha512-gylk2KyCer9EoymFyTyc+g2IqyAq4mTbZgaHoSJi6gHoXlJsC2LVeN4jedvegvjUsXPExdE60wHjCPa+DS4iXw==
+ dependencies:
+ "@dimforge/rapier3d-compat" "0.14.0"
+ suspend-react "^0.1.3"
+ three-stdlib "^2.29.4"
+
"@rollup/rollup-android-arm-eabi@4.34.6":
version "4.34.6"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.6.tgz#9b726b4dcafb9332991e9ca49d54bafc71d9d87f"
@@ -1073,11 +1087,27 @@ color-convert@^2.0.1:
dependencies:
color-name "~1.1.4"
-color-name@~1.1.4:
+color-name@^1.0.0, color-name@~1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+color-string@^1.9.0:
+ version "1.9.1"
+ resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4"
+ integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==
+ dependencies:
+ color-name "^1.0.0"
+ simple-swizzle "^0.2.2"
+
+color@^4.2.3:
+ version "4.2.3"
+ resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a"
+ integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==
+ dependencies:
+ color-convert "^2.0.1"
+ color-string "^1.9.0"
+
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@@ -1763,6 +1793,11 @@ is-array-buffer@^3.0.4, is-array-buffer@^3.0.5:
call-bound "^1.0.3"
get-intrinsic "^1.2.6"
+is-arrayish@^0.3.1:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
+ integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
+
is-async-function@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.1.1.tgz#3e69018c8e04e73b738793d020bfe884b9fd3523"
@@ -2041,6 +2076,11 @@ locate-path@^6.0.0:
dependencies:
p-locate "^5.0.0"
+lodash.debounce@^4.0.8:
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
+ integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
+
lodash.merge@^4.6.2:
version "4.6.2"
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
@@ -2545,6 +2585,13 @@ side-channel@^1.1.0:
side-channel-map "^1.0.1"
side-channel-weakmap "^1.0.2"
+simple-swizzle@^0.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
+ integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==
+ dependencies:
+ is-arrayish "^0.3.1"
+
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
@@ -2649,7 +2696,7 @@ three-mesh-bvh@^0.7.8:
resolved "https://registry.yarnpkg.com/three-mesh-bvh/-/three-mesh-bvh-0.7.8.tgz#83156e4d3945734db076de1c94809331481b3fdd"
integrity sha512-BGEZTOIC14U0XIRw3tO4jY7IjP7n7v24nv9JXS1CyeVRWOCkcOMhRnmENUjuV39gktAw4Ofhr0OvIAiTspQrrw==
-three-stdlib@^2.35.6:
+three-stdlib@^2.29.4, three-stdlib@^2.35.6:
version "2.35.13"
resolved "https://registry.yarnpkg.com/three-stdlib/-/three-stdlib-2.35.13.tgz#477fc5ffdef8a8923395ae2d7f26b0aeb6d60689"
integrity sha512-AbXVObkM0OFCKX0r4VmHguGTdebiUQA+Yl+4VNta1wC158gwY86tCkjp2LFfmABtjYJhdK6aP13wlLtxZyLMAA==