diff --git a/package.json b/package.json index c0c2a51a..b2f8fb64 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "spacesvr", - "version": "2.6.5", + "version": "2.7.0", "private": true, "description": "A standardized reality for future of the 3D Web", "keywords": [ diff --git a/src/ideas/mediated/LostFloor.tsx b/src/ideas/mediated/LostFloor.tsx index b3f81a20..4ea53091 100644 --- a/src/ideas/mediated/LostFloor.tsx +++ b/src/ideas/mediated/LostFloor.tsx @@ -15,7 +15,7 @@ export function LostFloor() { fragHead + shader.fragmentShader.replace( "#include ", - "#include \n" + fragColorFragment + "#include \n " + fragColorFragment ); }; return m; @@ -199,5 +199,5 @@ const fragHead = ` const fragColorFragment = ` diffuseColor.rgb -= 0.2 * (snoise(vPos) + 1.) / 2.; - diffuseColor.r -= 0.1 * (snoise(-vPos) + 1.) / 2.; + diffuseColor.r -= 0.025 * (snoise(-vPos) + 1.) / 2.; `; diff --git a/src/ideas/modifiers/Collidable/components/TrimeshCollider.tsx b/src/ideas/modifiers/Collidable/components/TrimeshCollider.tsx index 6fb3e52d..9422b0f8 100644 --- a/src/ideas/modifiers/Collidable/components/TrimeshCollider.tsx +++ b/src/ideas/modifiers/Collidable/components/TrimeshCollider.tsx @@ -31,13 +31,9 @@ export default function TrimeshCollider(props: TrimeshColliderProps) { return g; }, [geo, curScale]); - const [, api] = useTrimeshCollision(geometry); - - // there's some state update that causes the api not to receive an out of sync position - // i think it's whenever the api gets recreated. for now, just re-apply transforms every 2 seconds - const needsUpdate = useRef(false); - useLimitedFrame(1 / 2, () => { - needsUpdate.current = true; + const [, api] = useTrimeshCollision(geometry, { + pos: pos.toArray(), + rot: [euler.x, euler.y, euler.z], }); const lastUpdatedMatrix = useRef(new Matrix4()); @@ -49,16 +45,12 @@ export default function TrimeshCollider(props: TrimeshColliderProps) { group.current.matrixWorld.decompose(pos, quat, scale); // no need to update if nothing changed - if ( - lastUpdatedMatrix.current.equals(group.current.matrixWorld) && - !needsUpdate.current - ) { + if (lastUpdatedMatrix.current.equals(group.current.matrixWorld)) { return; } // update last values lastUpdatedMatrix.current.copy(group.current.matrixWorld); - needsUpdate.current = false; // if a change was found, update collider api.position.copy(pos); diff --git a/src/ideas/modifiers/Collidable/utils/trimesh.ts b/src/ideas/modifiers/Collidable/utils/trimesh.ts index 086da6ec..35ed331e 100644 --- a/src/ideas/modifiers/Collidable/utils/trimesh.ts +++ b/src/ideas/modifiers/Collidable/utils/trimesh.ts @@ -1,11 +1,14 @@ -import { useTrimesh } from "@react-three/cannon"; +import { Triplet, useTrimesh } from "@react-three/cannon"; import { BufferAttribute, BufferGeometry, InterleavedBufferAttribute, } from "three"; -export const useTrimeshCollision = (geometry: BufferGeometry) => { +export const useTrimeshCollision = ( + geometry: BufferGeometry, + trans?: { pos?: Triplet; rot?: Triplet } +) => { const indices = (geometry.index as BufferAttribute).array as number[]; const isInterleaved = @@ -30,6 +33,8 @@ export const useTrimeshCollision = (geometry: BufferGeometry) => { () => ({ type: "Static", args: [vertices, indices], + position: trans?.pos, + rotation: trans?.rot, }), undefined, [geometry.uuid] diff --git a/src/ideas/ui/Button.tsx b/src/ideas/ui/Button.tsx index a570c1ba..04216f31 100644 --- a/src/ideas/ui/Button.tsx +++ b/src/ideas/ui/Button.tsx @@ -28,7 +28,7 @@ export function Button(props: ButtonProps) { font = "https://d27rt3a60hh1lx.cloudfront.net/fonts/Quicksand_Bold.otf", fontSize = 0.05, width, - maxWidth = 0.25, + maxWidth, textColor = "black", color = "#fff", outline = true, @@ -85,7 +85,11 @@ export function Button(props: ButtonProps) { }, []); const PADDING = fontSize * 0.9; - const MAX_WIDTH = width ? Math.min(width, maxWidth) : maxWidth; + const MAX_WIDTH = !maxWidth + ? Infinity + : width + ? Math.max(width, maxWidth) + : maxWidth; const WIDTH = (width || dims[0]) + PADDING * 2; const HEIGHT = dims[1] + PADDING; const DEPTH = fontSize * 1.1; @@ -123,7 +127,7 @@ export function Button(props: ButtonProps) { {/* @ts-ignore */} diff --git a/src/ideas/ui/TextInput/index.tsx b/src/ideas/ui/TextInput/index.tsx index dd8dd739..8d2c5b48 100644 --- a/src/ideas/ui/TextInput/index.tsx +++ b/src/ideas/ui/TextInput/index.tsx @@ -348,7 +348,7 @@ export function TextInput(props: TextProps) { @@ -356,7 +356,7 @@ export function TextInput(props: TextProps) { {/* @ts-ignore */} diff --git a/src/layers/Environment/logic/canvas.ts b/src/layers/Environment/logic/canvas.ts index ce6fdedc..90817f73 100644 --- a/src/layers/Environment/logic/canvas.ts +++ b/src/layers/Environment/logic/canvas.ts @@ -1,5 +1,6 @@ import { Props as ContainerProps } from "@react-three/fiber/dist/declarations/src/web/Canvas"; import { ResizeObserver } from "@juggle/resize-observer"; +import { NoToneMapping } from "three"; export const defaultCanvasProps: Partial = { gl: { @@ -8,11 +9,13 @@ export const defaultCanvasProps: Partial = { depth: true, alpha: false, stencil: false, + physicallyCorrectLights: true, + toneMapping: NoToneMapping, }, shadows: false, - camera: { position: [0, 2, 0], near: 0.01, far: 150 }, + camera: { position: [0, 2, 0], near: 0.01, far: 300 }, resize: { polyfill: ResizeObserver }, dpr: 1, - raycaster: { far: 2 }, + raycaster: { far: 3 }, events: undefined, }; diff --git a/src/layers/Environment/ui/PauseMenu/index.tsx b/src/layers/Environment/ui/PauseMenu/index.tsx index 508920a7..cbcc919c 100644 --- a/src/layers/Environment/ui/PauseMenu/index.tsx +++ b/src/layers/Environment/ui/PauseMenu/index.tsx @@ -48,7 +48,7 @@ export default function PauseMenu(props: PauseMenuProps) { const PAUSE_ITEMS: PauseItem[] = [ ...pauseMenuItems, { - text: "v2.6.5", + text: "v2.7.0", link: "https://www.npmjs.com/package/spacesvr", }, ...menuItems, diff --git a/src/layers/Player/index.tsx b/src/layers/Player/index.tsx index ff1107be..4f186ae3 100644 --- a/src/layers/Player/index.tsx +++ b/src/layers/Player/index.tsx @@ -94,7 +94,7 @@ export function Player(props: PlayerLayer) { const velocity = useRef(new Vector3()); const lockControls = useRef(false); const raycaster = useMemo( - () => new Raycaster(new Vector3(), new Vector3(), 0, 2), + () => new Raycaster(new Vector3(), new Vector3(), 0, 5), [] ); diff --git a/src/layers/Toolbelt/ideas/Lights.tsx b/src/layers/Toolbelt/ideas/Lights.tsx index d6988283..f967bbd5 100644 --- a/src/layers/Toolbelt/ideas/Lights.tsx +++ b/src/layers/Toolbelt/ideas/Lights.tsx @@ -1,8 +1,8 @@ export default function Lights() { return ( - - + + ); } diff --git a/src/tools/Camera/components/Instruction.tsx b/src/tools/Camera/components/Instruction.tsx new file mode 100644 index 00000000..9245b06c --- /dev/null +++ b/src/tools/Camera/components/Instruction.tsx @@ -0,0 +1,66 @@ +import { a, useSpring } from "@react-spring/three"; +import { Text } from "@react-three/drei"; +import { Floating, Key } from "../../../ideas"; +import { useEnvironment } from "../../../layers"; + +const FONT_FILE = + "https://d27rt3a60hh1lx.cloudfront.net/fonts/Quicksand_Bold.otf"; + +type InstructionProps = { + open: boolean; + setOpen: (open: boolean) => void; +}; + +export default function Instruction(props: InstructionProps) { + const { open, setOpen } = props; + + const { device } = useEnvironment(); + + const CLOSED_SCALE = device.mobile ? 0.5 : 0.5; + + const { scale } = useSpring({ scale: open ? 0 : CLOSED_SCALE }); + + const FONT_SIZE = 0.055; + + const DESKTOP_TEXT = `Press to ${open ? "close" : "open"}`; + const MOBILE_TEXT = "tap to open"; + + return ( + + + + {device.mobile ? MOBILE_TEXT : DESKTOP_TEXT} + + + {device.desktop && ( + + setOpen(!open)} + /> + + )} + + ); +} diff --git a/src/tools/Camera/index.tsx b/src/tools/Camera/index.tsx index dfd87e37..75dd6168 100644 --- a/src/tools/Camera/index.tsx +++ b/src/tools/Camera/index.tsx @@ -1,24 +1,23 @@ -import { useEffect, useMemo, useRef, Suspense, useState } from "react"; -import { - Group, - Quaternion, - Vector3, - Mesh, - PerspectiveCamera as ThrPerspectiveCamera, -} from "three"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { Group, Mesh, PerspectiveCamera as ThrPerspectiveCamera } from "three"; import { createPortal, MeshProps, useThree } from "@react-three/fiber"; import { PerspectiveCamera, Text } from "@react-three/drei"; -import CameraModel from "./models/Camera"; -import { usePhotography } from "./utils/photo"; +import { usePhotography } from "./logic/photo"; import { useEnvironment } from "../../layers/Environment"; import { config, useSpring, animated } from "@react-spring/three"; import { Tool } from "../../ideas/modifiers/Tool"; import { useToolbelt } from "../../layers/Toolbelt"; -import { Interactable } from "../../ideas"; -import { useLimitedFrame } from "../../logic"; +import { Button, Interactable, Model } from "../../ideas"; +import { isTyping, useKeypress } from "../../logic"; +import { useRendering } from "./logic/rendering"; +import Instruction from "./components/Instruction"; const AUDIO_URL = "https://d27rt3a60hh1lx.cloudfront.net/tools/camera/shutter-sound.mp3"; +const CAMERA_MODEL_URL = + "https://d1htv66kutdwsl.cloudfront.net/6a38d41a-9fd4-43aa-bd7a-e2c6e16f6f20/ec577637-0ab6-4101-b077-d84f6a69e062.glb"; +const FONT_URL = + "https://d27rt3a60hh1lx.cloudfront.net/fonts/Quicksand_Bold.otf"; type CameraProps = { onCapture?: () => void }; @@ -32,94 +31,114 @@ export function Camera(props: CameraProps) { const cam = useRef(); const group = useRef(null); const mesh = useRef(null); - const photo = usePhotography(cam); - const [pressShutter, setPressShutter] = useState(false); + const [open, setOpen] = useState(false); + const [pressShutter, setPressShutter] = useState(false); const ENABLED = toolbelt.activeTool?.name === "Camera"; + const photo = usePhotography(cam, open); - const { shutterY } = useSpring({ - shutterY: pressShutter ? 0 : 1, + const { shutterY, rotX, rotY, scale } = useSpring({ + shutterY: pressShutter || !open ? 0 : 1, + rotX: open ? 0 : 0.3, + rotY: open ? 0 : device.mobile ? Math.PI - 0.5 : -0.1, + scale: open ? (device.mobile ? 0.15 : 0.2) : device.mobile ? 0.1 : 0.25, config: config.stiff, }); - const dummy = useMemo(() => new Vector3(), []); - const qummy = useMemo(() => new Quaternion(), []); - useLimitedFrame(24, (state) => { - if (!cam.current || !mesh.current || !group.current || !ENABLED) return; - - // move mesh to camera's position - mesh.current.getWorldPosition(dummy); - cam.current.position.set(0, 0, 0); - cam.current.position.add(dummy); - mesh.current.getWorldQuaternion(qummy); - cam.current.rotation.setFromQuaternion(qummy); - - // render to camera viewfinder - state.gl.autoClear = true; - state.gl.setRenderTarget(photo.target); - state.gl.render(scene, cam.current); - state.gl.setRenderTarget(null); - }); + useRendering(ENABLED, cam, group, mesh, photo.target); - const onClick = () => { + const onClick = useCallback(() => { setPressShutter(true); const audio = new Audio(AUDIO_URL); audio.play(); photo.takePicture(); if (onCapture) onCapture(); - }; + }, [onCapture, photo]); useEffect(() => { - if (!ENABLED || paused || pressShutter || device.mobile) return; + if (!ENABLED || paused || pressShutter || !open) return; document.addEventListener("click", onClick); return () => document.removeEventListener("click", onClick); - }, [ENABLED, paused, photo, pressShutter, device, onClick]); + }, [ENABLED, paused, photo, pressShutter, device, open, onCapture]); useEffect(() => { if (pressShutter) setTimeout(() => setPressShutter(false), 750); }, [pressShutter]); + useKeypress( + "c", + () => { + if (isTyping() || !ENABLED) return; + setOpen(!open); + }, + [ENABLED, open] + ); + const BoxApproximation = (props: MeshProps) => ( - - - + + ); + const POS: [number, number] = open + ? [0, 0] + : device.mobile + ? [0.9, 0.9] + : [0.8, -0.8]; + + const INFO_TEXT = device.mobile + ? "tap camera to take a picture" + : "click to take a picture\nscroll to zoom\nc to close"; + return ( - - - - }> - - + + + + + {device.mobile && ( - + setOpen(true) : onClick}> )} - + - + - {`${device.mobile ? "tap" : "click"} to take a picture`} + {INFO_TEXT} + {device.mobile && open && ( + + )} - + {createPortal( - , + , scene )} diff --git a/src/tools/Camera/logic/photo.ts b/src/tools/Camera/logic/photo.ts new file mode 100644 index 00000000..c4e7a217 --- /dev/null +++ b/src/tools/Camera/logic/photo.ts @@ -0,0 +1,106 @@ +import { MutableRefObject, useEffect, useMemo, useState } from "react"; +import { + MathUtils, + NearestFilter, + NoToneMapping, + PerspectiveCamera, + RGBAFormat, + sRGBEncoding, + Vector2, + WebGLRenderer, + WebGLRenderTarget, +} from "three"; +import { useThree } from "@react-three/fiber"; +import { useEnvironment } from "../../../layers/Environment"; + +type Photography = { + resolution: Vector2; + aspect: Vector2; + takePicture: () => void; + target: WebGLRenderTarget; + fov: number; +}; + +export const usePhotography = ( + cam: MutableRefObject, + open: boolean +): Photography => { + const { device } = useEnvironment(); + const { scene } = useThree(); + + const [fov, setFov] = useState(50); + useEffect(() => setFov(50), [open]); + const resolution = useMemo( + () => new Vector2(3, 2).normalize().multiplyScalar(2186), + [] + ); + const aspect = useMemo(() => resolution.clone().normalize(), [resolution]); + const target = useMemo( + () => + new WebGLRenderTarget(resolution.x, resolution.y, { + stencilBuffer: true, // text boxes look strange without this idk man + minFilter: NearestFilter, + magFilter: NearestFilter, + format: RGBAFormat, + }), + [resolution] + ); + const renderer = useMemo(() => { + const r = new WebGLRenderer({ + preserveDrawingBuffer: true, + precision: "highp", + antialias: true, + }); + r.physicallyCorrectLights = true; + r.toneMapping = NoToneMapping; + r.outputEncoding = sRGBEncoding; + return r; + }, []); + + useEffect(() => { + renderer.setPixelRatio(device.desktop ? 2 : 1); // could be 3, just really fat + renderer.setSize(target.width, target.height); + }, [device.desktop, target.width, target.height, renderer]); + + useEffect(() => { + // increase/decrease fov on scroll + const onScroll = (e: WheelEvent) => { + if (!cam.current) return; + const fov = MathUtils.clamp(cam.current.fov + e.deltaY * 0.05, 10, 75); + setFov(fov); + }; + window.addEventListener("wheel", onScroll); + return () => window.removeEventListener("wheel", onScroll); + }, [cam]); + + const takePicture = () => { + if (!cam.current) return; + + document.body.append(renderer.domElement); + cam.current.aspect = aspect.x / aspect.y; + + renderer.render(scene, cam.current); + + const link = document.createElement("a"); + const today = new Date(); + const name = + document.title + + " - www.muse.place" + + window.location.pathname + + " - " + + today.toLocaleDateString("en-US") + + " " + + today.getHours() + + ":" + + today.getMinutes(); + + link.download = `${name}.jpg`; + link.href = renderer.domElement.toDataURL("image/jpeg"); + link.click(); + + link.remove(); + document.body.removeChild(renderer.domElement); + }; + + return { resolution, aspect, takePicture, target, fov }; +}; diff --git a/src/tools/Camera/logic/rendering.ts b/src/tools/Camera/logic/rendering.ts new file mode 100644 index 00000000..d0febee3 --- /dev/null +++ b/src/tools/Camera/logic/rendering.ts @@ -0,0 +1,59 @@ +import { MutableRefObject, RefObject, useEffect, useMemo, useRef } from "react"; +import { useLimitedFrame } from "../../../logic"; +import { + Group, + Mesh, + PerspectiveCamera, + Quaternion, + Vector3, + WebGLRenderTarget, +} from "three"; +import { useEnvironment } from "../../../layers/Environment"; +import { useThree } from "@react-three/fiber"; + +export const useRendering = ( + ENABLED: boolean, + cam: MutableRefObject, + group: RefObject, + mesh: RefObject, + target: WebGLRenderTarget +) => { + const { paused } = useEnvironment(); + const { scene } = useThree(); + + const dummy = useMemo(() => new Vector3(), []); + const qummy = useMemo(() => new Quaternion(), []); + + // prep render the camera until first pause to compile materials in advance rather than first time tool is enabled + const prepRendering = useRef(true); + useEffect(() => { + if (!paused) prepRendering.current = false; + }, [paused]); + useLimitedFrame(1 / 4, (state) => { + if (ENABLED || !prepRendering.current || !cam.current) return; // don't double render + state.gl.autoClear = true; + state.gl.setRenderTarget(target); + state.gl.render(scene, cam.current); + state.gl.setRenderTarget(null); + state.gl.autoClear = false; + }); + + useLimitedFrame(24, (state) => { + if (!cam.current || !mesh.current || !group.current || !ENABLED || !open) + return; + + // move mesh to camera's position + mesh.current.getWorldPosition(dummy); + mesh.current.getWorldQuaternion(qummy); + cam.current.position.set(0, 0, 0.5).applyQuaternion(qummy); // move back 0.5m + cam.current.position.add(dummy); + cam.current.rotation.setFromQuaternion(qummy); + + // render to camera viewfinder + state.gl.autoClear = true; + state.gl.setRenderTarget(target); + state.gl.render(scene, cam.current); + state.gl.setRenderTarget(null); + state.gl.autoClear = false; + }); +}; diff --git a/src/tools/Camera/models/Camera.tsx b/src/tools/Camera/models/Camera.tsx deleted file mode 100644 index 5aa722ed..00000000 --- a/src/tools/Camera/models/Camera.tsx +++ /dev/null @@ -1,48 +0,0 @@ -/* -Auto-generated by: https://github.com/pmndrs/gltfjsx -*/ - -import * as THREE from "three"; -import React, { useRef } from "react"; -import { useGLTF } from "@react-three/drei"; -import { GLTF } from "three-stdlib"; -import { Group, MeshStandardMaterial } from "three"; - -type GLTFResult = GLTF & { - nodes: { - camera_1: THREE.Mesh; - camera_2: THREE.Mesh; - }; - materials: { - camera_body: THREE.MeshStandardMaterial; - }; -}; - -export const CAMERA_FILE_URL = - "https://d27rt3a60hh1lx.cloudfront.net/models/Camera-1652915410/camera_02_cleaned.glb.gz"; - -export default function Model(props: JSX.IntrinsicElements["group"]) { - const group = useRef(null); - const { nodes } = useGLTF(CAMERA_FILE_URL) as GLTFResult; - - (nodes.camera_1.material as MeshStandardMaterial).metalness = 0.3; - - return ( - - - - - - - - - ); -} diff --git a/src/tools/Camera/utils/photo.ts b/src/tools/Camera/utils/photo.ts deleted file mode 100644 index 4e5955dc..00000000 --- a/src/tools/Camera/utils/photo.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { MutableRefObject, useMemo } from "react"; -import { - ACESFilmicToneMapping, - NearestFilter, - PerspectiveCamera, - RGBAFormat, - sRGBEncoding, - Vector2, - WebGLRenderer, - WebGLRenderTarget, -} from "three"; -import { useThree } from "@react-three/fiber"; - -type Photography = { - resolution: Vector2; - aspect: Vector2; - takePicture: () => void; - target: WebGLRenderTarget; -}; - -export const usePhotography = ( - cam: MutableRefObject -): Photography => { - const { scene } = useThree(); - - const resolution = useMemo( - () => new Vector2(3, 2).normalize().multiplyScalar(2186), - [] - ); - - const aspect = useMemo(() => resolution.clone().normalize(), [resolution]); - - const target = useMemo( - () => - new WebGLRenderTarget(resolution.x, resolution.y, { - stencilBuffer: true, - minFilter: NearestFilter, - magFilter: NearestFilter, - format: RGBAFormat, - }), - [resolution] - ); - - const takePicture = () => { - if (!cam.current) return; - - const r = new WebGLRenderer({ - preserveDrawingBuffer: true, - precision: "highp", - antialias: true, - }); - r.physicallyCorrectLights = false; - r.setPixelRatio(2); // could be 3, just really fat - r.setSize(target.width, target.height); - r.outputEncoding = sRGBEncoding; - r.toneMapping = ACESFilmicToneMapping; - - document.body.append(r.domElement); - cam.current.aspect = aspect.x / aspect.y; - - r.render(scene, cam.current); - - const link = document.createElement("a"); - const today = new Date(); - const name = - document.title + - " - www.muse.place" + - window.location.pathname + - " - " + - today.toLocaleDateString("en-US") + - " " + - today.getHours() + - ":" + - today.getMinutes(); - - link.download = `${name}.png`; - link.href = r.domElement.toDataURL("image/png"); - link.click(); - - link.remove(); - document.body.removeChild(r.domElement); - r.dispose(); - }; - - return { resolution, aspect, takePicture, target }; -}; diff --git a/src/worlds/Lost.tsx b/src/worlds/Lost.tsx index 382dcac8..d8a00f32 100644 --- a/src/worlds/Lost.tsx +++ b/src/worlds/Lost.tsx @@ -6,8 +6,8 @@ export function LostWorld() { return ( - - + +