+
+
-
- { props.showUI && props.messages && props.messages.length > 0 && props.messages.map((message, index) => (
- {message}
+
+ {props.showUI && props.messages && props.messages.length > 0 && props.messages.map((message, index) => (
+ {message}
))}
-
+
{/* {props.messages.map((message, index) => (
{message}
))} */}
-
-
- )}
- >
- );
- } else {
- return (
- <>
-
-
-
-
-
- { props.showUI && props.messages && props.messages.length > 0 && props.messages.map((message, index) => (
- {message}
- ))}
-
-
-
-
- {/* {props.messages.map((message, index) => (
-
{message}
- ))} */}
-
-
-
- >
- );
- }
- }
-
-/**
- * Represents a participant in a virtual reality scene.
- *
- * @param {Object} participant - The props for the participant.
- *
- * @return {JSX.Element} The participant.
- */
-function Participant(participant) {
- // Participant VRM.
- const fallbackURL = threeObjectPlugin + defaultVRM;
- const playerURL = userData.vrm ? userData.vrm : fallbackURL;
-
- const someSceneState = useLoader(GLTFLoader, playerURL, (loader) => {
- loader.register((parser) => {
- return new VRMLoaderPlugin(parser);
- });
- });
-
- if (someSceneState?.userData?.gltfExtensions?.VRM) {
- const playerController = someSceneState.userData.vrm;
- VRMUtils.rotateVRM0(playerController);
- const rotationVRM = playerController.scene.rotation.y;
- playerController.scene.rotation.set(0, rotationVRM, 0);
- playerController.scene.scale.set(1, 1, 1);
-
- const theScene = useThree();
-
- useEffect(() => {
- participant.p2pcf.on("msg", (peer, data) => {
- // console.log(peer, data);
- const finalData = new TextDecoder("utf-8").decode(data);
- const participantData = JSON.parse(finalData);
- const participantObject = theScene.scene.getObjectByName(
- peer.client_id
- );
- if (participantObject) {
- // const loadedProfile = useLoader(
- // TextureLoader,
- // participantData[peer.client_id][2].profileImage
- // );
- // if (loadedProfile) {
- // participantObject.traverse((obj) => {
- // if (
- // obj.name === "profile" &&
- // obj.material.map === null
- // ) {
- // const newMat = obj.material.clone();
- // newMat.map = loadedProfile;
- // obj.material = newMat;
- // obj.material.map.needsUpdate = true;
- // }
- // });
- // }
- participantObject.position.set(
- participantData[peer.client_id][0].position[0],
- participantData[peer.client_id][0].position[1],
- participantData[peer.client_id][0].position[2]
- );
- participantObject.rotation.set(
- participantData[peer.client_id][1].rotation[0],
- participantData[peer.client_id][1].rotation[1],
- participantData[peer.client_id][1].rotation[2]
- );
- }
- });
- }, []);
-
- // participant.p2pcf.on('peerclose', peer => {
- // const participantObject = theScene.scene.getObjectByName(peer.client_id);
- // // theScene.scene.remove(participantObject.name);
- // theScene.scene.remove(...participantObject.children);
- // // removePeerUi(peer.id)
- // })
-
- const modelClone = SkeletonUtils.clone(playerController.scene);
- // set modelClone visible to true
- modelClone.visible = true;
-
+ )}
+ >
+ );
+ } else {
return (
<>
- {playerController && (
-
- )}
+
+
+
+
+
+ {props.showUI && props.messages && props.messages.length > 0 && props.messages.map((message, index) => (
+ {message}
+ ))}
+
+
+
+
+ {/* {props.messages.map((message, index) => (
+
{message}
+ ))} */}
+
+
+
+
>
);
}
}
-function Participants(props) {
-
- useEffect(() => {
- const p2pcf = window.p2pcf;
- if (p2pcf) {
- p2pcf.on("peerconnect", (peer) => {
- // console.log("connected peer", peer);
- // add peer.client_id to participants
- props.setParticipant([...props.participants, peer.client_id]);
- });
- }
- }, []);
-
- return (
- <>
- {props.participants &&
- props.participants.map((item, index) => {
- return (
- <>
-
- >
- );
- })}
- >
- );
-
-}
-
/**
* Represents a saved object in a virtual reality world.
*
@@ -427,6 +390,17 @@ function Participants(props) {
* @return {JSX.Element} The saved object.
*/
function SavedObject(props) {
+ useEffect(() => {
+ // Once the component is ready, dispatch an event to notify the parent
+ const event = new Event('mainComponentReady');
+ document.dispatchEvent(event);
+ }, []);
+
+
+ useThree(({ camera, scene }) => {
+ window.scene = scene;
+ window.camera = camera;
+ });
const meshRef = useRef();
const [url, set] = useState(props.url);
@@ -441,11 +415,11 @@ function SavedObject(props) {
useThree(({ camera }) => {
camera.add(listener);
});
-
+
const gltf = useLoader(GLTFLoader, url, (loader) => {
const dracoLoader = new DRACOLoader();
- dracoLoader.setDecoderPath( threeObjectPluginRoot + "/inc/utils/draco/");
- dracoLoader.setDecoderConfig({type: 'js'});
+ dracoLoader.setDecoderPath(threeObjectPluginRoot + "/inc/utils/draco/");
+ dracoLoader.setDecoderConfig({ type: 'js' });
loader.setDRACOLoader(dracoLoader);
loader.register(
@@ -585,12 +559,12 @@ function SavedObject(props) {
})}
{colliders &&
colliders.map((item, index) => {
- const pos = new THREE.Vector3(); // create once an reuse it
- const quat = new THREE.Quaternion(); // create once an reuse it
+ const pos = new THREE.Vector3();
+ const quat = new THREE.Quaternion();
const rotation = new THREE.Euler();
const quaternion = item[0].getWorldQuaternion(quat);
const finalRotation =
- rotation.setFromQuaternion(quaternion);
+ rotation.setFromQuaternion(quaternion);
const worldPosition = item[0].getWorldPosition(pos);
if (item[1].type === "mesh") {
return (
@@ -642,46 +616,176 @@ function SavedObject(props) {
}
export default function EnvironmentFront(props) {
- const [participants, setParticipant] = useState([]);
+
+ const [loadedAudios, setLoadedAudios] = useState([]);
+ const [allAudiosLoaded, setAllAudiosLoaded] = useState(false);
+
+
const [showUI, setShowUI] = useState(true);
+ const [displayName, setDisplayName] = useState(props.userData.inWorldName);
+ const [playerAvatar, setPlayerAvatar] = useState(props.userData.playerVRM);
const canvasRef = useRef(null);
+ const r3fCanvasRef = useRef(null);
- // let string = '{\"spell\":\"complexQuery\",\"outputs\":{\"Output\":\"{\\\"message\\\": \\\" Hi there! How can I help you?\\\",\\\"tone\\\": \\\"friendly\\\"}\"},\"state\":{}}';
- // let string = 'Hello! Welcome to this 3OV world! Feel free to ask me anything. I am especially versed in the 3OV metaverse plugin for WordPress.'
const [mobileControls, setMobileControls] = useState(null);
- const [mobileRotControls, setMobileRotControls] = useState(null);
+ const [mobileRotControls, setMobileRotControls] = useState(null);
const movement = useKeyboardControls();
-
+
const [messages, setMessages] = useState();
const [messageHistory, setMessageHistory] = useState();
const [loaded, setLoaded] = useState(false);
- const [spawnPoints, setSpawnPoints] = useState([0,0,0]);
- const [messageObject, setMessageObject] = useState({"tone": "happy", "message": "hello!"});
+ const [spawnPoints, setSpawnPoints] = useState([0, 0, 0]);
+ const [messageObject, setMessageObject] = useState({ "tone": "happy", "message": "hello!" });
const [objectsInRoom, setObjectsInRoom] = useState([]);
- const [url, setURL] = useState(props.threeUrl ? props.threeUrl : (threeObjectPlugin + defaultEnvironment));
+ const [url, setURL] = useState(props.threeUrl ? props.threeUrl : (defaultEnvironment));
+ const [loadingWorld, setLoadingWorld] = useState(true);
+ const avatarHeightOffset = useRef(0);
+
+ const mirror = new Reflector(
+ new THREE.PlaneGeometry(Number(100), Number(100)),
+ {
+ color: new THREE.Color(0x7f7f7f),
+ textureWidth: 1440,
+ textureHeight: 1440
+ }
+ );
+ useEffect(() => {
+ if (loadedAudios.length === props.audiosToAdd.length && !allAudiosLoaded) {
+ setAllAudiosLoaded(true);
+ loadedAudios.forEach(audio => {
+ if (audio.userData.autoPlay === "1") {
+ audio.play();
+ }
+ });
+ }
+ }, [loadedAudios, props.audiosToAdd, allAudiosLoaded]);
+
+ useEffect(() => {
+ const handleReady = () => {
+ setTimeout(() => {
+ const event = new Event("loaderIsGone");
+ window.dispatchEvent(event);
+ setLoadingWorld(false);
+ }, 3000);
+ };
+ // Listen for the ready event
+ document.addEventListener('mainComponentReady', handleReady);
+
+ return () => {
+ document.removeEventListener('mainComponentReady', handleReady);
+ };
+ }, []);
+ const [dpr, setDpr] = useState(2);
+
+ useEffect(() => {
+ const handleKeyDown = (event) => {
+ // Bypass the default behavior of the spacebar and other keys if needed.
+ if ((event.key === ' ' || event.key === 'Spacebar') && document.pointerLockElement === r3fCanvasRef.current) {
+ event.preventDefault(); // Prevent scrolling when spacebar is pressed
+ }
+ };
+
+ window.addEventListener('keydown', handleKeyDown);
+
+ return () => {
+ window.removeEventListener('keydown', handleKeyDown);
+ };
+ }, []);
+
+ // xrDevice debugger
+ //
+ // useEffect(() => {
+ // const xrDevice = new XRDevice(metaQuest3);
+ // xrDevice.ipd = 0;
+ // xrDevice.fovy = Math.PI / 3;
+ // xrDevice.installRuntime();
+ // window.xrDevice = xrDevice;
+ // const handleKeyDown = (event) => {
+ // if (event.shiftKey) {
+ // switch (event.key) {
+ // case "ArrowLeft":
+ // xrDevice.controllers.right.position.x -= 0.1;
+ // break;
+ // case "ArrowRight":
+ // xrDevice.controllers.right.position.x += 0.1;
+ // break;
+ // case "ArrowUp":
+ // xrDevice.controllers.right.position.y += 0.1;
+ // break;
+ // case "ArrowDown":
+ // xrDevice.controllers.right.position.y -= 0.1;
+ // break;
+ // case "?":
+ // xrDevice.controllers.right.position.z -= 0.1;
+ // break;
+ // case ">":
+ // xrDevice.controllers.right.position.z += 0.1;
+ // break;
+ // }
+ // } else {
+ // switch (event.key) {
+ // case "ArrowLeft":
+ // xrDevice.controllers.left.position.x -= 0.1;
+ // break;
+ // case "ArrowRight":
+ // xrDevice.controllers.left.position.x += 0.1;
+ // break;
+ // case "ArrowUp":
+ // xrDevice.controllers.left.position.y += 0.1;
+ // break;
+ // case "ArrowDown":
+ // xrDevice.controllers.left.position.y -= 0.1;
+ // break;
+ // case ".":
+ // xrDevice.controllers.left.position.z -= 0.1;
+ // break;
+ // case "/":
+ // xrDevice.controllers.left.position.z += 0.1;
+ // break;
+ // }
+ // };
+ // };
+ // document.addEventListener("keydown", handleKeyDown);
+ // return () => {
+ // document.removeEventListener("keydown", handleKeyDown);
+ // };
+ // }, []);
if (loaded === true) {
- const elements = document.body.getElementsByTagName('*');
- const webXRNotAvail = Array.from(elements).find((el) => el.textContent === 'WEBXR NOT AVAILABLE');
- if (webXRNotAvail) {
- webXRNotAvail.style.display = "none";
- }
+ // emit javascript event "loaded"
+ const loadedEvent = new Event("loaded");
+ window.dispatchEvent(loadedEvent);
+ // const elements = document.body.getElementsByTagName('*');
+ // const webXRNotAvail = Array.from(elements).find((el) => el.textContent === 'WEBXR NOT AVAILABLE');
+ // if (webXRNotAvail) {
+ // webXRNotAvail.style.display = "none";
+ // }
+ props.userData.inWorldName = displayName;
+ window.userData = props.userData;
+ props.userData.playerVRM = playerAvatar;
if (props.deviceTarget === "vr") {
return (
<>
-
}
+
{
+ e.target.requestPointerLock();
+ }}
+ className="threeov-main-canvas"
+ dpr={dpr}
+ mode="concurrent"
style={{
backgroundColor: props.backgroundColor,
margin: "0",
@@ -692,1351 +796,1490 @@ export default function EnvironmentFront(props) {
zIndex: 1
}}
>
+
+
+ setDpr(1)} factor={1} onChange={({ factor }) => setDpr(Math.floor(0.5 + 1.5 * factor, 1))} />
+
- { isVRCompatible() && }
- {/* */}
- {/* */}
-
-
- }>
- {props.hdr &&
-
- }
-
- */}
+
+
+
+ {props.hdr &&
+
+ }
+
+ {/*
- {/* */}
- {/* Debug physics */}
- {url && (
- <>
-
-
+ */}
+
+
+ {/* */}
+ {/* Debug physics */}
+ {url && loaded && (
+ <>
+
-
-
- {Object.values(props.sky).map(
- (item, index) => {
+ avatarHeightOffset={avatarHeightOffset}
+ useNormal={false}
+ >
+ {(props.networkingBlock.length > 0) && (
+
+ )}
+
+ {Object.values(props.sky).map(
+ (item, index) => {
+ return (
+ <>
+
+ >
+ );
+ }
+ )}
+ {Object.values(
+ props.imagesToAdd
+ ).map((item, index) => {
+ let imagePosX, imagePosY, imagePosZ, imageScaleX, imageScaleY, imageScaleZ;
+ let imageRotationX, imageRotationY, imageRotationZ, imageUrl, aspectHeight, aspectWidth;
+ let transparent;
+ if (item.tagName.toLowerCase() === 'three-image-block') {
+ imagePosX = item.getAttribute('positionX') || '';
+ imagePosY = item.getAttribute('positionY') || '';
+ imagePosZ = item.getAttribute('positionZ') || '';
+ imageScaleX = item.getAttribute('scaleX') || '';
+ imageScaleY = item.getAttribute('scaleY') || '';
+ imageScaleZ = item.getAttribute('scaleZ') || '';
+ imageRotationX = item.getAttribute('rotationX') || '';
+ imageRotationY = item.getAttribute('rotationY') || '';
+ imageRotationZ = item.getAttribute('rotationZ') || '';
+ imageUrl = item.getAttribute('imageUrl') || '';
+ aspectHeight = item.getAttribute('aspectHeight') || '';
+ aspectWidth = item.getAttribute('aspectWidth') || '';
+ transparent = item.getAttribute('transparent') || false;
+ } else {
+ imagePosX =
+ item.querySelector(
+ "p.image-block-positionX"
+ )
+ ? item.querySelector(
+ "p.image-block-positionX"
+ ).innerText
+ : "";
+
+ imagePosY =
+ item.querySelector(
+ "p.image-block-positionY"
+ )
+ ? item.querySelector(
+ "p.image-block-positionY"
+ ).innerText
+ : "";
+
+ imagePosZ =
+ item.querySelector(
+ "p.image-block-positionZ"
+ )
+ ? item.querySelector(
+ "p.image-block-positionZ"
+ ).innerText
+ : "";
+
+ imageScaleX =
+ item.querySelector(
+ "p.image-block-scaleX"
+ )
+ ? item.querySelector(
+ "p.image-block-scaleX"
+ ).innerText
+ : "";
+
+ imageScaleY =
+ item.querySelector(
+ "p.image-block-scaleY"
+ )
+ ? item.querySelector(
+ "p.image-block-scaleY"
+ ).innerText
+ : "";
+
+ imageScaleZ =
+ item.querySelector(
+ "p.image-block-scaleZ"
+ )
+ ? item.querySelector(
+ "p.image-block-scaleZ"
+ ).innerText
+ : "";
+
+ imageRotationX =
+ item.querySelector(
+ "p.image-block-rotationX"
+ )
+ ? item.querySelector(
+ "p.image-block-rotationX"
+ ).innerText
+ : "";
+
+ imageRotationY =
+ item.querySelector(
+ "p.image-block-rotationY"
+ )
+ ? item.querySelector(
+ "p.image-block-rotationY"
+ ).innerText
+ : "";
+
+ imageRotationZ =
+ item.querySelector(
+ "p.image-block-rotationZ"
+ )
+ ? item.querySelector(
+ "p.image-block-rotationZ"
+ ).innerText
+ : "";
+
+ imageUrl =
+ item.querySelector(
+ "p.image-block-url"
+ )
+ ? item.querySelector(
+ "p.image-block-url"
+ ).innerText
+ : "";
+
+ aspectHeight =
+ item.querySelector(
+ "p.image-block-aspect-height"
+ )
+ ? item.querySelector(
+ "p.image-block-aspect-height"
+ ).innerText
+ : "";
+
+ aspectWidth =
+ item.querySelector(
+ "p.image-block-aspect-width"
+ )
+ ? item.querySelector(
+ "p.image-block-aspect-width"
+ ).innerText
+ : "";
+
+ transparent =
+ item.querySelector(
+ "p.image-block-transparent"
+ )
+ ? item.querySelector(
+ "p.image-block-transparent"
+ ).innerText
+ : false;
+ }
return (
- <>
-
- >
+
);
- }
- )}
- {Object.values(
- props.imagesToAdd
- ).map((item, index) => {
- const imagePosX =
- item.querySelector(
- "p.image-block-positionX"
- )
- ? item.querySelector(
- "p.image-block-positionX"
- ).innerText
- : "";
-
- const imagePosY =
- item.querySelector(
- "p.image-block-positionY"
- )
- ? item.querySelector(
- "p.image-block-positionY"
- ).innerText
- : "";
-
- const imagePosZ =
- item.querySelector(
- "p.image-block-positionZ"
- )
- ? item.querySelector(
- "p.image-block-positionZ"
- ).innerText
- : "";
-
- const imageScaleX =
- item.querySelector(
- "p.image-block-scaleX"
- )
- ? item.querySelector(
- "p.image-block-scaleX"
- ).innerText
- : "";
-
- const imageScaleY =
- item.querySelector(
- "p.image-block-scaleY"
- )
- ? item.querySelector(
- "p.image-block-scaleY"
- ).innerText
- : "";
-
- const imageScaleZ =
- item.querySelector(
- "p.image-block-scaleZ"
- )
- ? item.querySelector(
- "p.image-block-scaleZ"
- ).innerText
- : "";
-
- const imageRotationX =
- item.querySelector(
- "p.image-block-rotationX"
- )
- ? item.querySelector(
- "p.image-block-rotationX"
- ).innerText
- : "";
-
- const imageRotationY =
- item.querySelector(
- "p.image-block-rotationY"
- )
- ? item.querySelector(
- "p.image-block-rotationY"
- ).innerText
- : "";
-
- const imageRotationZ =
- item.querySelector(
- "p.image-block-rotationZ"
- )
- ? item.querySelector(
- "p.image-block-rotationZ"
- ).innerText
- : "";
-
- const imageUrl =
- item.querySelector(
- "p.image-block-url"
- )
- ? item.querySelector(
- "p.image-block-url"
- ).innerText
- : "";
-
- const aspectHeight =
- item.querySelector(
- "p.image-block-aspect-height"
- )
- ? item.querySelector(
- "p.image-block-aspect-height"
- ).innerText
- : "";
-
- const aspectWidth =
- item.querySelector(
- "p.image-block-aspect-width"
- )
- ? item.querySelector(
- "p.image-block-aspect-width"
- ).innerText
- : "";
-
- const transparent =
- item.querySelector(
- "p.image-block-transparent"
- )
- ? item.querySelector(
- "p.image-block-transparent"
- ).innerText
- : false;
- return (
-
- );
- })}
- {Object.values(
- props.videosToAdd
- ).map((item, index) => {
- const videoPosX =
- item.querySelector(
- "p.video-block-positionX"
- )
- ? item.querySelector(
- "p.video-block-positionX"
- ).innerText
- : "";
-
- const videoPosY =
- item.querySelector(
- "p.video-block-positionY"
- )
- ? item.querySelector(
- "p.video-block-positionY"
- ).innerText
- : "";
-
- const videoPosZ =
- item.querySelector(
- "p.video-block-positionZ"
- )
- ? item.querySelector(
- "p.video-block-positionZ"
- ).innerText
- : "";
-
- const videoScaleX =
- item.querySelector(
- "p.video-block-scaleX"
- )
- ? item.querySelector(
- "p.video-block-scaleX"
- ).innerText
- : "";
-
- const videoScaleY =
- item.querySelector(
- "p.video-block-scaleY"
- )
- ? item.querySelector(
- "p.video-block-scaleY"
- ).innerText
- : "";
-
- const videoScaleZ =
- item.querySelector(
- "p.video-block-scaleZ"
- )
- ? item.querySelector(
- "p.video-block-scaleZ"
- ).innerText
- : "";
-
- const videoRotationX =
- item.querySelector(
- "p.video-block-rotationX"
- )
- ? item.querySelector(
- "p.video-block-rotationX"
- ).innerText
- : "";
-
- const videoRotationY =
- item.querySelector(
- "p.video-block-rotationY"
- )
- ? item.querySelector(
- "p.video-block-rotationY"
- ).innerText
- : "";
-
- const videoRotationZ =
- item.querySelector(
- "p.video-block-rotationZ"
- )
- ? item.querySelector(
- "p.video-block-rotationZ"
- ).innerText
- : "";
-
- const videoUrl =
- item.querySelector(
- "div.video-block-url"
- )
- ? item.querySelector(
- "div.video-block-url"
- ).innerText
- : "";
-
- const aspectHeight =
- item.querySelector(
- "p.video-block-aspect-height"
- )
- ? item.querySelector(
- "p.video-block-aspect-height"
- ).innerText
- : "";
-
- const aspectWidth =
- item.querySelector(
- "p.video-block-aspect-width"
- )
- ? item.querySelector(
- "p.video-block-aspect-width"
- ).innerText
- : "";
-
- const autoPlay =
- item.querySelector(
- "p.video-block-autoplay"
- )
- ? item.querySelector(
+ })}
+ {Object.values(props.videosToAdd).map((item, index) => {
+ let videoPosX, videoPosY, videoPosZ, videoScaleX, videoScaleY, videoScaleZ;
+ let videoRotationX, videoRotationY, videoRotationZ, videoUrl, aspectHeight, aspectWidth;
+ let autoPlay, customModel, videoModelUrl, videoControlsEnabled;
+
+ if (item.tagName.toLowerCase() === 'three-video-block') {
+ videoPosX = item.getAttribute('positionX') || '';
+ videoPosY = item.getAttribute('positionY') || '';
+ videoPosZ = item.getAttribute('positionZ') || '';
+ videoScaleX = item.getAttribute('scaleX') || '';
+ videoScaleY = item.getAttribute('scaleY') || '';
+ videoScaleZ = item.getAttribute('scaleZ') || '';
+ videoRotationX = item.getAttribute('rotationX') || '';
+ videoRotationY = item.getAttribute('rotationY') || '';
+ videoRotationZ = item.getAttribute('rotationZ') || '';
+ videoUrl = item.getAttribute('videoUrl') || '';
+ aspectHeight = item.getAttribute('aspectHeight') || '';
+ aspectWidth = item.getAttribute('aspectWidth') || '';
+ autoPlay = item.hasAttribute('autoplay') ? "1" : false;
+ customModel = item.getAttribute('customModel') ? item.getAttribute('customModel') : false;
+ videoModelUrl = item.getAttribute('modelUrl') || '';
+ videoControlsEnabled = item.getAttribute('videoControlsEnabled') === "1" ? true : false;
+ } else {
+ videoPosX =
+ item.querySelector(
+ "p.video-block-positionX"
+ )
+ ? item.querySelector(
+ "p.video-block-positionX"
+ ).innerText
+ : "";
+
+ videoPosY =
+ item.querySelector(
+ "p.video-block-positionY"
+ )
+ ? item.querySelector(
+ "p.video-block-positionY"
+ ).innerText
+ : "";
+
+ videoPosZ =
+ item.querySelector(
+ "p.video-block-positionZ"
+ )
+ ? item.querySelector(
+ "p.video-block-positionZ"
+ ).innerText
+ : "";
+
+ videoScaleX =
+ item.querySelector(
+ "p.video-block-scaleX"
+ )
+ ? item.querySelector(
+ "p.video-block-scaleX"
+ ).innerText
+ : "";
+
+ videoScaleY =
+ item.querySelector(
+ "p.video-block-scaleY"
+ )
+ ? item.querySelector(
+ "p.video-block-scaleY"
+ ).innerText
+ : "";
+
+ videoScaleZ =
+ item.querySelector(
+ "p.video-block-scaleZ"
+ )
+ ? item.querySelector(
+ "p.video-block-scaleZ"
+ ).innerText
+ : "";
+
+ videoRotationX =
+ item.querySelector(
+ "p.video-block-rotationX"
+ )
+ ? item.querySelector(
+ "p.video-block-rotationX"
+ ).innerText
+ : "";
+
+ videoRotationY =
+ item.querySelector(
+ "p.video-block-rotationY"
+ )
+ ? item.querySelector(
+ "p.video-block-rotationY"
+ ).innerText
+ : "";
+
+ videoRotationZ =
+ item.querySelector(
+ "p.video-block-rotationZ"
+ )
+ ? item.querySelector(
+ "p.video-block-rotationZ"
+ ).innerText
+ : "";
+
+ videoUrl =
+ item.querySelector(
+ "div.video-block-url"
+ )
+ ? item.querySelector(
+ "div.video-block-url"
+ ).innerText
+ : "";
+
+ aspectHeight =
+ item.querySelector(
+ "p.video-block-aspect-height"
+ )
+ ? item.querySelector(
+ "p.video-block-aspect-height"
+ ).innerText
+ : "";
+
+ aspectWidth =
+ item.querySelector(
+ "p.video-block-aspect-width"
+ )
+ ? item.querySelector(
+ "p.video-block-aspect-width"
+ ).innerText
+ : "";
+
+ autoPlay =
+ item.querySelector(
"p.video-block-autoplay"
- ).innerText
- : false;
-
- const customModel =
- item.querySelector(
- "p.video-block-custom-model"
- )
- ? item.querySelector(
- "p.video-block-custom-model"
- ).innerText
- : false;
- const videoModelUrl =
- item.querySelector(
- "div.video-block-model-url"
- )
- ? item.querySelector(
- "div.video-block-model-url"
- ).innerText
- : "";
-
- return (
-
- );
- })}
- {Object.values(props.audiosToAdd).map((item, index) => {
- const audioPosX = item.querySelector("p.audio-block-positionX")
- ? item.querySelector("p.audio-block-positionX").innerText
- : "";
-
- const audioPosY = item.querySelector("p.audio-block-positionY")
- ? item.querySelector("p.audio-block-positionY").innerText
- : "";
-
- const audioPosZ = item.querySelector("p.audio-block-positionZ")
- ? item.querySelector("p.audio-block-positionZ").innerText
- : "";
-
- const audioScaleX = item.querySelector("p.audio-block-scaleX")
- ? item.querySelector("p.audio-block-scaleX").innerText
- : "";
-
- const audioScaleY = item.querySelector("p.audio-block-scaleY")
- ? item.querySelector("p.audio-block-scaleY").innerText
- : "";
-
- const audioScaleZ = item.querySelector("p.audio-block-scaleZ")
- ? item.querySelector("p.audio-block-scaleZ").innerText
- : "";
-
- const audioRotationX = item.querySelector("p.audio-block-rotationX")
- ? item.querySelector("p.audio-block-rotationX").innerText
- : "";
-
- const audioRotationY = item.querySelector("p.audio-block-rotationY")
- ? item.querySelector("p.audio-block-rotationY").innerText
- : "";
-
- const audioRotationZ = item.querySelector("p.audio-block-rotationZ")
- ? item.querySelector("p.audio-block-rotationZ").innerText
- : "";
-
- const audioUrl = item.querySelector("p.audio-block-url")
- ? item.querySelector("p.audio-block-url").innerText
- : "";
-
- const autoPlay = item.querySelector("p.audio-block-autoPlay")
- ? item.querySelector("p.audio-block-autoPlay").innerText === "1"
- : false;
-
- const loop = item.querySelector("p.audio-block-loop")
- ? item.querySelector("p.audio-block-loop").innerText === "1"
- : false;
-
- const volume = item.querySelector("p.audio-block-volume")
- ? Number(item.querySelector("p.audio-block-volume").innerText)
- : 1;
-
- const positional = item.querySelector("p.audio-block-positional")
- ? item.querySelector("p.audio-block-positional").innerText === "1"
- : false;
-
- const coneInnerAngle = item.querySelector("p.audio-block-coneInnerAngle")
- ? Number(item.querySelector("p.audio-block-coneInnerAngle").innerText)
- : 1;
-
- const coneOuterAngle = item.querySelector("p.audio-block-coneOuterAngle")
- ? Number(item.querySelector("p.audio-block-coneOuterAngle").innerText)
- : 1;
-
- const coneOuterGain = item.querySelector("p.audio-block-coneOuterGain")
- ? Number(item.querySelector("p.audio-block-coneOuterGain").innerText)
- : 1;
-
- const distanceModel = item.querySelector("p.audio-block-distanceModel")
- ? item.querySelector("p.audio-block-distanceModel").innerText
- : "inverse";
-
- const maxDistance = item.querySelector("p.audio-block-maxDistance")
- ? Number(item.querySelector("p.audio-block-maxDistance").innerText)
- : 1;
-
- const refDistance = item.querySelector("p.audio-block-refDistance")
- ? Number(item.querySelector("p.audio-block-refDistance").innerText)
- : 1;
-
- const rolloffFactor = item.querySelector("p.audio-block-rolloffFactor")
- ? Number(item.querySelector("p.audio-block-rolloffFactor").innerText)
- : 1;
-
- return (
-
- );
- })}
- {props.lightsToAdd.length < 1 && (
- <>
-
-
- >
- )}
- {Object.values(props.lightsToAdd).map((item, index) => {
- const lightPosX = item.querySelector("p.light-block-positionX")
- ? item.querySelector("p.light-block-positionX").innerText
- : "";
-
- const lightPosY = item.querySelector("p.light-block-positionY")
- ? item.querySelector("p.light-block-positionY").innerText
- : "";
-
- const lightPosZ = item.querySelector("p.light-block-positionZ")
- ? item.querySelector("p.light-block-positionZ").innerText
- : "";
-
- const lightRotationX = item.querySelector("p.light-block-rotationX")
- ? item.querySelector("p.light-block-rotationX").innerText
- : "";
-
- const lightRotationY = item.querySelector("p.light-block-rotationY")
- ? item.querySelector("p.light-block-rotationY").innerText
- : "";
-
- const lightRotationZ = item.querySelector("p.light-block-rotationZ")
- ? item.querySelector("p.light-block-rotationZ").innerText
- : "";
-
- const lightType = item.querySelector("p.light-block-type")
- ? item.querySelector("p.light-block-type").innerText
- : "ambient";
-
- const lightColor = item.querySelector("p.light-block-color")
- ? item.querySelector("p.light-block-color").innerText
- : "";
-
- const lightItensity = item.querySelector("p.light-block-intensity")
- ? item.querySelector("p.light-block-intensity").innerText
- : "";
-
- const lightDistance = item.querySelector("p.light-block-distance")
- ? item.querySelector("p.light-block-distance").innerText
- : "";
-
- const lightDecay = item.querySelector("p.light-block-decay")
- ? item.querySelector("p.light-block-decay").innerText
- : "";
-
- const lightAngle = item.querySelector("p.light-block-angle")
- ? item.querySelector("p.light-block-angle").innerText
- : "";
-
- const lightPenumbra = item.querySelector("p.light-block-penumbra")
- ? item.querySelector("p.light-block-penumbra").innerText
- : "";
-
- return (
-
- );
- })}
-
- {Object.values(
- props.npcsToAdd
- ).map((npc, index) => {
- const modelPosX =
- npc.querySelector(
- "p.npc-block-position-x"
- )
- ? npc.querySelector(
- "p.npc-block-position-x"
- ).innerText
- : "";
-
- const modelPosY =
- npc.querySelector(
- "p.npc-block-position-y"
- )
- ? npc.querySelector(
- "p.npc-block-position-y"
- ).innerText
- : "";
-
- const modelPosZ =
- npc.querySelector(
- "p.npc-block-position-z"
- )
- ? npc.querySelector(
- "p.npc-block-position-z"
- ).innerText
- : "";
-
- const modelRotationX =
- npc.querySelector(
- "p.npc-block-rotation-x"
- )
- ? npc.querySelector(
- "p.npc-block-rotation-x"
- ).innerText
- : "";
-
- const modelRotationY =
- npc.querySelector(
- "p.npc-block-rotation-y"
- )
- ? npc.querySelector(
- "p.npc-block-rotation-y"
- ).innerText
- : "";
-
- const modelRotationZ =
- npc.querySelector(
- "p.npc-block-rotation-z"
- )
- ? npc.querySelector(
- "p.npc-block-rotation-z"
- ).innerText
- : "";
-
- const url = npc.querySelector(
- "p.npc-block-url"
- )
- ? npc.querySelector(
- "p.npc-block-url"
- ).innerText
- : "";
-
- const alt = npc.querySelector(
- "p.npc-block-alt"
- )
- ? npc.querySelector(
- "p.npc-block-alt"
- ).innerText
- : "";
-
- const personality = npc.querySelector(
- "p.npc-block-personality"
- )
- ? npc.querySelector(
- "p.npc-block-personality"
- ).innerText
- : "";
-
- const defaultMessage = npc.querySelector(
- "p.npc-block-default-message"
- )
- ? npc.querySelector(
- "p.npc-block-default-message"
- ).innerText
- : "";
-
- const name = npc.querySelector(
- "p.npc-block-name"
- )
- ? npc.querySelector(
- "p.npc-block-name"
- ).innerText
- : "";
-
- const objectAwareness =
- npc.querySelector(
- "p.npc-block-object-awareness"
- )
- ? npc.querySelector(
- "p.npc-block-object-awareness"
- ).innerText
- : false;
-
- return (
-
- );
- })}
- {Object.values(
- props.modelsToAdd
- ).map((model, index) => {
- const modelPosX =
- model.querySelector(
- "p.model-block-position-x"
- )
- ? model.querySelector(
- "p.model-block-position-x"
- ).innerText
- : "";
-
- const modelPosY =
- model.querySelector(
- "p.model-block-position-y"
- )
- ? model.querySelector(
- "p.model-block-position-y"
- ).innerText
- : "";
-
- const modelPosZ =
- model.querySelector(
- "p.model-block-position-z"
- )
- ? model.querySelector(
- "p.model-block-position-z"
- ).innerText
- : "";
-
- const modelScaleX =
- model.querySelector(
- "p.model-block-scale-x"
- )
- ? model.querySelector(
- "p.model-block-scale-x"
- ).innerText
- : "";
-
- const modelScaleY =
- model.querySelector(
- "p.model-block-scale-y"
- )
- ? model.querySelector(
- "p.model-block-scale-y"
- ).innerText
- : "";
-
- const modelScaleZ =
- model.querySelector(
- "p.model-block-scale-z"
- )
- ? model.querySelector(
- "p.model-block-scale-z"
- ).innerText
- : "";
-
- const modelRotationX =
- model.querySelector(
- "p.model-block-rotation-x"
- )
- ? model.querySelector(
- "p.model-block-rotation-x"
- ).innerText
- : "";
-
- const modelRotationY =
- model.querySelector(
- "p.model-block-rotation-y"
- )
- ? model.querySelector(
- "p.model-block-rotation-y"
- ).innerText
- : "";
-
- const modelRotationZ =
- model.querySelector(
- "p.model-block-rotation-z"
- )
- ? model.querySelector(
- "p.model-block-rotation-z"
- ).innerText
- : "";
-
- const url = model.querySelector(
- "p.model-block-url"
- )
- ? model.querySelector(
- "p.model-block-url"
- ).innerText
- : "";
-
- const animations =
- model.querySelector(
- "p.model-block-animations"
- )
- ? model.querySelector(
- "p.model-block-animations"
- ).innerText
- : "";
-
- const alt = model.querySelector(
- "p.model-block-alt"
- )
- ? model.querySelector(
- "p.model-block-alt"
- ).innerText
- : "";
-
- if (!objectsInRoom.includes(alt)) {
- setObjectsInRoom([...objectsInRoom, alt]);
+ )
+ ? item.querySelector(
+ "p.video-block-autoplay"
+ ).innerText
+ : false;
+
+ customModel =
+ item.querySelector(
+ "p.video-block-custom-model"
+ )
+ ? item.querySelector(
+ "p.video-block-custom-model"
+ ).innerText
+ : false;
+ videoModelUrl =
+ item.querySelector(
+ "div.video-block-model-url"
+ )
+ ? item.querySelector(
+ "div.video-block-model-url"
+ ).innerText
+ : "";
+ videoControlsEnabled = true;
}
-
- const collidable =
- model.querySelector(
- "p.model-block-collidable"
- )
- ? model.querySelector(
- "p.model-block-collidable"
- ).innerText
- : false;
- return (
-
- );
- })}
- {Object.values(props.htmlToAdd).map(
- (model, index) => {
- const textContent =
- model.querySelector(
- "p.three-text-content"
- )
- ? model.querySelector(
- "p.three-text-content"
- ).innerText
+ return (
+
+ );
+ })}
+ {Object.values(props.audiosToAdd).map((item, index) => {
+ let audioPosX, audioPosY, audioPosZ, audioRotationX, audioRotationY, audioRotationZ;
+ let audioUrl, autoPlay, loop, volume, positional, coneInnerAngle, coneOuterAngle, coneOuterGain, distanceModel, maxDistance, refDistance, rolloffFactor;
+
+ if (item.tagName.toLowerCase() === 'three-audio-block') {
+ audioPosX = item.getAttribute('positionX') || '';
+ audioPosY = item.getAttribute('positionY') || '';
+ audioPosZ = item.getAttribute('positionZ') || '';
+ audioRotationX = item.getAttribute('rotationX') || '';
+ audioRotationY = item.getAttribute('rotationY') || '';
+ audioRotationZ = item.getAttribute('rotationZ') || '';
+ audioUrl = item.getAttribute('audioUrl') || '';
+ autoPlay = item.hasAttribute('autoplay') ? "1" : "0";
+ loop = item.hasAttribute('loop') ? "1" : "0";
+ volume = item.getAttribute('volume') || '';
+ positional = item.hasAttribute('positional') ? "1" : "0";
+ coneInnerAngle = item.getAttribute('coneInnerAngle') || '';
+ coneOuterAngle = item.getAttribute('coneOuterAngle') || '';
+ coneOuterGain = item.getAttribute('coneOuterGain') || '';
+ distanceModel = item.getAttribute('distanceModel') || '';
+ maxDistance = item.getAttribute('maxDistance') || '';
+ refDistance = item.getAttribute('refDistance') || '';
+ rolloffFactor = item.getAttribute('rolloffFactor') || '';
+ } else {
+ audioPosX = item.querySelector("p.audio-block-positionX")?.innerText || "";
+ audioPosY = item.querySelector("p.audio-block-positionY")?.innerText || "";
+ audioPosZ = item.querySelector("p.audio-block-positionZ")?.innerText || "";
+ audioRotationX = item.querySelector("p.audio-block-rotationX")?.innerText || "";
+ audioRotationY = item.querySelector("p.audio-block-rotationY")?.innerText || "";
+ audioRotationZ = item.querySelector("p.audio-block-rotationZ")?.innerText || "";
+ audioUrl = item.querySelector("p.audio-block-url")?.innerText || "";
+ autoPlay = item.querySelector("p.audio-block-autoPlay")?.innerText === "1" ? "1" : "0";
+ loop = item.querySelector("p.audio-block-loop")?.innerText === "1" ? "1" : "0";
+ volume = item.querySelector("p.audio-block-volume")?.innerText || "";
+ positional = item.querySelector("p.audio-block-positional")?.innerText === "1" ? "1" : "0";
+ coneInnerAngle = item.querySelector("p.audio-block-coneInnerAngle")?.innerText || "";
+ coneOuterAngle = item.querySelector("p.audio-block-coneOuterAngle")?.innerText || "";
+ coneOuterGain = item.querySelector("p.audio-block-coneOuterGain")?.innerText || "";
+ distanceModel = item.querySelector("p.audio-block-distanceModel")?.innerText || "";
+ maxDistance = item.querySelector("p.audio-block-maxDistance")?.innerText || "";
+ refDistance = item.querySelector("p.audio-block-refDistance")?.innerText || "";
+ rolloffFactor = item.querySelector("p.audio-block-rolloffFactor")?.innerText || "";
+ }
+
+ return (
+ {
+ setLoadedAudios(prev => [...prev, loadedAudio]);
+ }}
+ />
+ );
+ })}
+
+
+ {props.lightsToAdd.length < 1 && (
+ <>
+
+
+ >
+ )}
+ {Object.values(props.lightsToAdd).map((item, index) => {
+ let lightPosX, lightPosY, lightPosZ, lightRotationX, lightRotationY, lightRotationZ;
+ let lightType, lightColor, lightItensity, lightDistance, lightDecay, lightAngle, lightPenumbra;
+ let targetX, targetY, targetZ = 0;
+ if (item.tagName.toLowerCase() === 'three-light-block') {
+ lightPosX = item.getAttribute('positionX') || '';
+ lightPosY = item.getAttribute('positionY') || '';
+ lightPosZ = item.getAttribute('positionZ') || '';
+ lightRotationX = item.getAttribute('rotationX') || '';
+ lightRotationY = item.getAttribute('rotationY') || '';
+ lightRotationZ = item.getAttribute('rotationZ') || '';
+ lightType = item.getAttribute('type') || 'ambient';
+ lightColor = item.getAttribute('color') || '';
+ lightItensity = item.getAttribute('intensity') || '';
+ lightDistance = item.getAttribute('distance') || '';
+ lightDecay = item.getAttribute('decay') || '';
+ lightAngle = item.getAttribute('angle') || '';
+ lightPenumbra = item.getAttribute('penumbra') || '';
+ } else {
+ lightPosX = item.querySelector("p.light-block-positionX")
+ ? item.querySelector("p.light-block-positionX").innerText
: "";
- const rotationX =
- model.querySelector(
- "p.three-text-rotationX"
- )
- ? model.querySelector(
- "p.three-text-rotationX"
- ).innerText
+
+ lightPosY = item.querySelector("p.light-block-positionY")
+ ? item.querySelector("p.light-block-positionY").innerText
: "";
- const rotationY =
- model.querySelector(
- "p.three-text-rotationY"
- )
- ? model.querySelector(
- "p.three-text-rotationY"
- ).innerText
+
+ lightPosZ = item.querySelector("p.light-block-positionZ")
+ ? item.querySelector("p.light-block-positionZ").innerText
: "";
- const rotationZ =
- model.querySelector(
- "p.three-text-rotationZ"
- )
- ? model.querySelector(
- "p.three-text-rotationZ"
- ).innerText
+
+ lightRotationX = item.querySelector("p.light-block-rotationX")
+ ? item.querySelector("p.light-block-rotationX").innerText
: "";
- const positionX =
- model.querySelector(
- "p.three-text-positionX"
+
+ lightRotationY = item.querySelector("p.light-block-rotationY")
+ ? item.querySelector("p.light-block-rotationY").innerText
+ : "";
+
+ lightRotationZ = item.querySelector("p.light-block-rotationZ")
+ ? item.querySelector("p.light-block-rotationZ").innerText
+ : "";
+
+ lightType = item.querySelector("p.light-block-type")
+ ? item.querySelector("p.light-block-type").innerText
+ : "ambient";
+
+ lightColor = item.querySelector("p.light-block-color")
+ ? item.querySelector("p.light-block-color").innerText
+ : "";
+
+ lightItensity = item.querySelector("p.light-block-intensity")
+ ? item.querySelector("p.light-block-intensity").innerText
+ : "";
+
+ lightDistance = item.querySelector("p.light-block-distance")
+ ? item.querySelector("p.light-block-distance").innerText
+ : "";
+
+ lightDecay = item.querySelector("p.light-block-decay")
+ ? item.querySelector("p.light-block-decay").innerText
+ : "";
+
+ lightAngle = item.querySelector("p.light-block-angle")
+ ? item.querySelector("p.light-block-angle").innerText
+ : "";
+
+ lightPenumbra = item.querySelector("p.light-block-penumbra")
+ ? item.querySelector("p.light-block-penumbra").innerText
+ : "";
+ }
+
+ return (
+
+ );
+ })}
+
+ {Object.values(
+ props.npcsToAdd
+ ).map((npc, index) => {
+ let url, modelPosX, modelPosY, modelPosZ, modelRotationX, modelRotationY, modelRotationZ, name, alt, defaultMessage, personality, objectAwareness;
+ if (npc.tagName.toLowerCase() === 'three-npc-block') {
+ url = npc.getAttribute('threeObjectUrl') || '';
+ modelPosX = npc.getAttribute('positionX') || '';
+ modelPosY = npc.getAttribute('positionY') || '';
+ modelPosZ = npc.getAttribute('positionZ') || '';
+ modelRotationX = npc.getAttribute('rotationX') || '';
+ modelRotationY = npc.getAttribute('rotationY') || '';
+ modelRotationZ = npc.getAttribute('rotationZ') || '';
+ name = npc.getAttribute('name') || '';
+ defaultMessage = npc.getAttribute('defaultMessage') || '';
+ personality = npc.getAttribute('personality') || '';
+ objectAwareness = npc.getAttribute('objectAwareness') || false;
+ } else {
+ modelPosX =
+ npc.querySelector(
+ "p.npc-block-position-x"
+ )
+ ? npc.querySelector(
+ "p.npc-block-position-x"
+ ).innerText
+ : "";
+
+ modelPosY =
+ npc.querySelector(
+ "p.npc-block-position-y"
+ )
+ ? npc.querySelector(
+ "p.npc-block-position-y"
+ ).innerText
+ : "";
+
+ modelPosZ =
+ npc.querySelector(
+ "p.npc-block-position-z"
+ )
+ ? npc.querySelector(
+ "p.npc-block-position-z"
+ ).innerText
+ : "";
+
+ modelRotationX =
+ npc.querySelector(
+ "p.npc-block-rotation-x"
+ )
+ ? npc.querySelector(
+ "p.npc-block-rotation-x"
+ ).innerText
+ : "";
+
+ modelRotationY =
+ npc.querySelector(
+ "p.npc-block-rotation-y"
+ )
+ ? npc.querySelector(
+ "p.npc-block-rotation-y"
+ ).innerText
+ : "";
+
+ modelRotationZ =
+ npc.querySelector(
+ "p.npc-block-rotation-z"
+ )
+ ? npc.querySelector(
+ "p.npc-block-rotation-z"
+ ).innerText
+ : "";
+
+ url = npc.querySelector(
+ "p.npc-block-url"
)
- ? model.querySelector(
- "p.three-text-positionX"
+ ? npc.querySelector(
+ "p.npc-block-url"
).innerText
: "";
- const positionY =
- model.querySelector(
- "p.three-text-positionY"
+
+ alt = npc.querySelector(
+ "p.npc-block-alt"
)
- ? model.querySelector(
- "p.three-text-positionY"
+ ? npc.querySelector(
+ "p.npc-block-alt"
).innerText
: "";
- const positionZ =
- model.querySelector(
- "p.three-text-positionZ"
+
+ personality = npc.querySelector(
+ "p.npc-block-personality"
)
- ? model.querySelector(
- "p.three-text-positionZ"
+ ? npc.querySelector(
+ "p.npc-block-personality"
).innerText
: "";
- const scaleX =
- model.querySelector(
- "p.three-text-scaleX"
+
+ defaultMessage = npc.querySelector(
+ "p.npc-block-default-message"
)
- ? model.querySelector(
- "p.three-text-scaleX"
+ ? npc.querySelector(
+ "p.npc-block-default-message"
).innerText
: "";
- const scaleY =
- model.querySelector(
- "p.three-text-scaleY"
+
+ name = npc.querySelector(
+ "p.npc-block-name"
)
- ? model.querySelector(
- "p.three-text-scaleY"
+ ? npc.querySelector(
+ "p.npc-block-name"
).innerText
: "";
- const scaleZ =
- model.querySelector(
- "p.three-text-scaleZ"
+
+ objectAwareness =
+ npc.querySelector(
+ "p.npc-block-object-awareness"
+ )
+ ? npc.querySelector(
+ "p.npc-block-object-awareness"
+ ).innerText
+ : false;
+ }
+
+ return (
+
+ );
+ })}
+ {Object.values(
+ props.modelsToAdd
+ ).map((model, index) => {
+ let modelPosX, modelPosY, modelPosZ, modelScaleX, modelScaleY, modelScaleZ;
+ let modelRotationX, modelRotationY, modelRotationZ, url, animations, alt, collidable;
+ if (model.tagName.toLowerCase() === 'three-model-block') {
+ modelPosX = model.getAttribute('positionX') || '';
+ modelPosY = model.getAttribute('positionY') || '';
+ modelPosZ = model.getAttribute('positionZ') || '';
+ modelScaleX = model.getAttribute('scaleX') || '';
+ modelScaleY = model.getAttribute('scaleY') || '';
+ modelScaleZ = model.getAttribute('scaleZ') || '';
+ modelRotationX = model.getAttribute('rotationX') || '';
+ modelRotationY = model.getAttribute('rotationY') || '';
+ modelRotationZ = model.getAttribute('rotationZ') || '';
+ url = model.getAttribute('threeObjectUrl') || '';
+ animations = model.getAttribute('animations') || '';
+ alt = model.getAttribute('alt') || '';
+ if (!objectsInRoom.includes(alt)) {
+ setObjectsInRoom([...objectsInRoom, alt]);
+ }
+ collidable = model.getAttribute('collidable');
+ } else {
+ modelPosX =
+ model.querySelector(
+ "p.model-block-position-x"
+ )
+ ? model.querySelector(
+ "p.model-block-position-x"
+ ).innerText
+ : "";
+
+ modelPosY =
+ model.querySelector(
+ "p.model-block-position-y"
+ )
+ ? model.querySelector(
+ "p.model-block-position-y"
+ ).innerText
+ : "";
+
+ modelPosZ =
+ model.querySelector(
+ "p.model-block-position-z"
+ )
+ ? model.querySelector(
+ "p.model-block-position-z"
+ ).innerText
+ : "";
+
+ modelScaleX =
+ model.querySelector(
+ "p.model-block-scale-x"
+ )
+ ? model.querySelector(
+ "p.model-block-scale-x"
+ ).innerText
+ : "";
+
+ modelScaleY =
+ model.querySelector(
+ "p.model-block-scale-y"
+ )
+ ? model.querySelector(
+ "p.model-block-scale-y"
+ ).innerText
+ : "";
+
+ modelScaleZ =
+ model.querySelector(
+ "p.model-block-scale-z"
+ )
+ ? model.querySelector(
+ "p.model-block-scale-z"
+ ).innerText
+ : "";
+
+ modelRotationX =
+ model.querySelector(
+ "p.model-block-rotation-x"
+ )
+ ? model.querySelector(
+ "p.model-block-rotation-x"
+ ).innerText
+ : "";
+
+ modelRotationY =
+ model.querySelector(
+ "p.model-block-rotation-y"
+ )
+ ? model.querySelector(
+ "p.model-block-rotation-y"
+ ).innerText
+ : "";
+
+ modelRotationZ =
+ model.querySelector(
+ "p.model-block-rotation-z"
+ )
+ ? model.querySelector(
+ "p.model-block-rotation-z"
+ ).innerText
+ : "";
+
+ url = model.querySelector(
+ "p.model-block-url"
)
? model.querySelector(
- "p.three-text-scaleZ"
+ "p.model-block-url"
).innerText
: "";
- const textColor =
- model.querySelector(
- "p.three-text-color"
+ animations =
+ model.querySelector(
+ "p.model-block-animations"
+ )
+ ? model.querySelector(
+ "p.model-block-animations"
+ ).innerText
+ : "";
+
+ alt = model.querySelector(
+ "p.model-block-alt"
)
? model.querySelector(
- "p.three-text-color"
+ "p.model-block-alt"
).innerText
: "";
+ if (!objectsInRoom.includes(alt)) {
+ setObjectsInRoom([...objectsInRoom, alt]);
+ }
+
+ collidable =
+ model.querySelector(
+ "p.model-block-collidable"
+ )
+ ? model.querySelector(
+ "p.model-block-collidable"
+ ).innerText
+ : false;
+ }
+ // log all of the vars above
return (
-
+ );
+ })}
+ {Object.values(props.textToAdd).map(
+ (model, index) => {
+ let textContent, rotationX, rotationY, rotationZ, positionX, positionY, positionZ, scaleX, scaleY, scaleZ, textColor;
+
+ if (model.tagName.toLowerCase() === 'three-text-block') {
+ textContent = model.getAttribute('textContent') || '';
+ rotationX = model.getAttribute('rotationX') || '';
+ rotationY = model.getAttribute('rotationY') || '';
+ rotationZ = model.getAttribute('rotationZ') || '';
+ positionX = model.getAttribute('positionX') || '';
+ positionY = model.getAttribute('positionY') || '';
+ positionZ = model.getAttribute('positionZ') || '';
+ scaleX = model.getAttribute('scaleX') || '';
+ scaleY = model.getAttribute('scaleY') || '';
+ scaleZ = model.getAttribute('scaleZ') || '';
+ textColor = model.getAttribute('textColor') || '';
+ } else {
+ textContent =
+ model.querySelector(
+ "p.three-text-content"
+ )
+ ? model.querySelector(
+ "p.three-text-content"
+ ).innerText
+ : "";
+ rotationX =
+ model.querySelector(
+ "p.three-text-rotationX"
+ )
+ ? model.querySelector(
+ "p.three-text-rotationX"
+ ).innerText
+ : "";
+ rotationY =
+ model.querySelector(
+ "p.three-text-rotationY"
+ )
+ ? model.querySelector(
+ "p.three-text-rotationY"
+ ).innerText
+ : "";
+ rotationZ =
+ model.querySelector(
+ "p.three-text-rotationZ"
+ )
+ ? model.querySelector(
+ "p.three-text-rotationZ"
+ ).innerText
+ : "";
+ positionX =
+ model.querySelector(
+ "p.three-text-positionX"
+ )
+ ? model.querySelector(
+ "p.three-text-positionX"
+ ).innerText
+ : "";
+ positionY =
+ model.querySelector(
+ "p.three-text-positionY"
+ )
+ ? model.querySelector(
+ "p.three-text-positionY"
+ ).innerText
+ : "";
+ positionZ =
+ model.querySelector(
+ "p.three-text-positionZ"
+ )
+ ? model.querySelector(
+ "p.three-text-positionZ"
+ ).innerText
+ : "";
+ scaleX =
+ model.querySelector(
+ "p.three-text-scaleX"
+ )
+ ? model.querySelector(
+ "p.three-text-scaleX"
+ ).innerText
+ : "";
+ scaleY =
+ model.querySelector(
+ "p.three-text-scaleY"
+ )
+ ? model.querySelector(
+ "p.three-text-scaleY"
+ ).innerText
+ : "";
+ scaleZ =
+ model.querySelector(
+ "p.three-text-scaleZ"
+ )
+ ? model.querySelector(
+ "p.three-text-scaleZ"
+ ).innerText
+ : "";
+
+ textColor =
+ model.querySelector(
+ "p.three-text-color"
+ )
+ ? model.querySelector(
+ "p.three-text-color"
+ ).innerText
+ : "";
+ }
+
+ return (
+
+ );
+ }
+ )}
+ {Object.values(
+ props.portalsToAdd
+ ).map((model, index) => {
+ let modelPosX, modelPosY, modelPosZ, modelScaleX, modelScaleY, modelScaleZ;
+ let modelRotationX, modelRotationY, modelRotationZ, url, destinationUrl, animations, label, labelOffsetX, labelOffsetY, labelOffsetZ, labelTextColor;
+ if (model.tagName.toLowerCase() === 'three-portal-block') {
+ modelPosX = model.getAttribute('positionX') || '';
+ modelPosY = model.getAttribute('positionY') || '';
+ modelPosZ = model.getAttribute('positionZ') || '';
+ modelScaleX = model.getAttribute('scaleX') || '';
+ modelScaleY = model.getAttribute('scaleY') || '';
+ modelScaleZ = model.getAttribute('scaleZ') || '';
+ modelRotationX = model.getAttribute('rotationX') || '';
+ modelRotationY = model.getAttribute('rotationY') || '';
+ modelRotationZ = model.getAttribute('rotationZ') || '';
+ url = model.getAttribute('threeObjectUrl') || '';
+ destinationUrl = model.getAttribute('destinationUrl') || '';
+ animations = model.getAttribute('animations') || '';
+ label = model.getAttribute('label') || '';
+ labelOffsetX = model.getAttribute('labelOffsetX') || '';
+ labelOffsetY = model.getAttribute('labelOffsetY') || '';
+ labelOffsetZ = model.getAttribute('labelOffsetZ') || '';
+ labelTextColor = model.getAttribute('labelTextColor') || '';
+ } else {
+ modelPosX =
+ model.querySelector(
+ "p.three-portal-block-position-x"
+ )
+ ? model.querySelector(
+ "p.three-portal-block-position-x"
+ ).innerText
+ : "";
+
+ modelPosY =
+ model.querySelector(
+ "p.three-portal-block-position-y"
+ )
+ ? model.querySelector(
+ "p.three-portal-block-position-y"
+ ).innerText
+ : "";
+
+ modelPosZ =
+ model.querySelector(
+ "p.three-portal-block-position-z"
+ )
+ ? model.querySelector(
+ "p.three-portal-block-position-z"
+ ).innerText
+ : "";
+
+ modelScaleX =
+ model.querySelector(
+ "p.three-portal-block-scale-x"
+ )
+ ? model.querySelector(
+ "p.three-portal-block-scale-x"
+ ).innerText
+ : "";
+
+ modelScaleY =
+ model.querySelector(
+ "p.three-portal-block-scale-y"
+ )
+ ? model.querySelector(
+ "p.three-portal-block-scale-y"
+ ).innerText
+ : "";
+
+ modelScaleZ =
+ model.querySelector(
+ "p.three-portal-block-scale-z"
+ )
+ ? model.querySelector(
+ "p.three-portal-block-scale-z"
+ ).innerText
+ : "";
+
+ modelRotationX =
+ model.querySelector(
+ "p.three-portal-block-rotation-x"
+ )
+ ? model.querySelector(
+ "p.three-portal-block-rotation-x"
+ ).innerText
+ : "";
+
+ modelRotationY =
+ model.querySelector(
+ "p.three-portal-block-rotation-y"
+ )
+ ? model.querySelector(
+ "p.three-portal-block-rotation-y"
+ ).innerText
+ : "";
+
+ modelRotationZ =
+ model.querySelector(
+ "p.three-portal-block-rotation-z"
+ )
+ ? model.querySelector(
+ "p.three-portal-block-rotation-z"
+ ).innerText
+ : "";
+
+ url = model.querySelector(
+ "p.three-portal-block-url"
+ )
+ ? model.querySelector(
+ "p.three-portal-block-url"
+ ).innerText
+ : "";
+
+ destinationUrl =
+ model.querySelector(
+ "p.three-portal-block-destination-url"
+ )
+ ? model.querySelector(
+ "p.three-portal-block-destination-url"
+ ).innerText
+ : "";
+
+ animations =
+ model.querySelector(
+ "p.three-portal-block-animations"
+ )
+ ? model.querySelector(
+ "p.three-portal-block-animations"
+ ).innerText
+ : "";
+
+ label =
+ model.querySelector(
+ "p.three-portal-block-label"
+ )
+ ? model.querySelector(
+ "p.three-portal-block-label"
+ ).innerText
+ : "";
+
+ labelOffsetX =
+ model.querySelector(
+ "p.three-portal-block-label-offset-x"
+ )
+ ? model.querySelector(
+ "p.three-portal-block-label-offset-x"
+ ).innerText
+ : "";
+
+ labelOffsetY =
+ model.querySelector(
+ "p.three-portal-block-label-offset-y"
+ )
+ ? model.querySelector(
+ "p.three-portal-block-label-offset-y"
+ ).innerText
+ : "";
+
+ labelOffsetZ =
+ model.querySelector(
+ "p.three-portal-block-label-offset-z"
+ )
+ ? model.querySelector(
+ "p.three-portal-block-label-offset-z"
+ ).innerText
+ : "";
+ labelTextColor =
+ model.querySelector(
+ "p.three-portal-block-label-text-color"
+ )
+ ? model.querySelector(
+ "p.three-portal-block-label-text-color"
+ ).innerText
+ : "";
+ }
+
+ return (
+
);
- }
- )}
- {Object.values(
- props.portalsToAdd
- ).map((model, index) => {
- const modelPosX =
- model.querySelector(
- "p.three-portal-block-position-x"
- )
- ? model.querySelector(
- "p.three-portal-block-position-x"
- ).innerText
- : "";
-
- const modelPosY =
- model.querySelector(
- "p.three-portal-block-position-y"
- )
- ? model.querySelector(
- "p.three-portal-block-position-y"
- ).innerText
- : "";
-
- const modelPosZ =
- model.querySelector(
- "p.three-portal-block-position-z"
- )
- ? model.querySelector(
- "p.three-portal-block-position-z"
- ).innerText
- : "";
-
- const modelScaleX =
- model.querySelector(
- "p.three-portal-block-scale-x"
- )
- ? model.querySelector(
- "p.three-portal-block-scale-x"
- ).innerText
- : "";
-
- const modelScaleY =
- model.querySelector(
- "p.three-portal-block-scale-y"
- )
- ? model.querySelector(
- "p.three-portal-block-scale-y"
- ).innerText
- : "";
-
- const modelScaleZ =
- model.querySelector(
- "p.three-portal-block-scale-z"
- )
- ? model.querySelector(
- "p.three-portal-block-scale-z"
- ).innerText
- : "";
-
- const modelRotationX =
- model.querySelector(
- "p.three-portal-block-rotation-x"
- )
- ? model.querySelector(
- "p.three-portal-block-rotation-x"
- ).innerText
- : "";
-
- const modelRotationY =
- model.querySelector(
- "p.three-portal-block-rotation-y"
- )
- ? model.querySelector(
- "p.three-portal-block-rotation-y"
- ).innerText
- : "";
-
- const modelRotationZ =
- model.querySelector(
- "p.three-portal-block-rotation-z"
- )
- ? model.querySelector(
- "p.three-portal-block-rotation-z"
- ).innerText
- : "";
-
- const url = model.querySelector(
- "p.three-portal-block-url"
- )
- ? model.querySelector(
- "p.three-portal-block-url"
- ).innerText
- : "";
-
- const destinationUrl =
- model.querySelector(
- "p.three-portal-block-destination-url"
- )
- ? model.querySelector(
- "p.three-portal-block-destination-url"
- ).innerText
- : "";
-
- const animations =
- model.querySelector(
- "p.three-portal-block-animations"
- )
- ? model.querySelector(
- "p.three-portal-block-animations"
- ).innerText
- : "";
-
- const label =
- model.querySelector(
- "p.three-portal-block-label"
- )
- ? model.querySelector(
- "p.three-portal-block-label"
- ).innerText
- : "";
-
- const labelOffsetX =
- model.querySelector(
- "p.three-portal-block-label-offset-x"
- )
- ? model.querySelector(
- "p.three-portal-block-label-offset-x"
- ).innerText
- : "";
-
- const labelOffsetY =
- model.querySelector(
- "p.three-portal-block-label-offset-y"
- )
- ? model.querySelector(
- "p.three-portal-block-label-offset-y"
- ).innerText
- : "";
-
- const labelOffsetZ =
- model.querySelector(
- "p.three-portal-block-label-offset-z"
- )
- ? model.querySelector(
- "p.three-portal-block-label-offset-z"
- ).innerText
- : "";
- const labelTextColor =
- model.querySelector(
- "p.three-portal-block-label-text-color"
- )
- ? model.querySelector(
- "p.three-portal-block-label-text-color"
- ).innerText
- : "";
-
- return (
-
- );
- })}
-
- >
- )}
-
-
- {/*
+ >
+ )}
+
+
+ {/* */}
-
+
+
{Object.values(
props.npcsToAdd
).map((npc, index) => {
-
- const personality = npc.querySelector(
- "p.npc-block-personality"
- )
- ? npc.querySelector(
- "p.npc-block-personality"
- ).innerText
- : "";
- const defaultMessage = npc.querySelector(
- "p.npc-block-default-message"
- )
- ? npc.querySelector(
- "p.npc-block-default-message"
- ).innerText
- : "";
-
- const objectAwareness = npc.querySelector(
- "p.npc-block-object-awareness"
- )
- ? npc.querySelector(
- "p.npc-block-object-awareness"
- ).innerText
- : "";
-
- const name = npc.querySelector(
- "p.npc-block-name"
- )
- ? npc.querySelector(
- "p.npc-block-name"
- ).innerText
- : "";
-
- return (
-
- )
+ )
})}
- <>
- { isMobile() && (
-
{
- if(data.force > 1.5){
- movement.current.shift = true;
- } else {
- movement.current.shift = false;
- }
- if(data.direction && data.direction.angle){
- if(data.direction.angle === "up" && ! movement.current.forward){
- movement.current.forward = true;
- movement.current.backward = false;
- movement.current.left = false;
- movement.current.right = false;
- } else if(data.direction.angle === "down" && ! movement.current.backward){
- movement.current.forward = false;
- movement.current.backward = true;
- movement.current.left = false;
- movement.current.right = false;
- } else if(data.direction.angle === "left" && ! movement.current.left){
- movement.current.forward = false;
- movement.current.backward = false;
- movement.current.left = true;
- movement.current.right = false;
- } else if(data.direction.angle === "right" && ! movement.current.right){
- movement.current.forward = false;
- movement.current.backward = false;
- movement.current.left = false;
- movement.current.right = true;
- }
- }
- }}
- onEnd={( evt, data ) => {
- movement.current.forward = false;
- movement.current.backward = false;
- movement.current.left = false;
- movement.current.right = false;
- }}
- />
- /* {
- console.log(data.direction.angle);
- }}
- // onEnd={(evt, data) => setMobileRotControls(null)}
- /> */
- ) }
+ <>
+ {isMobile() && (
+
+ )}
>
>
);
@@ -2049,24 +2292,68 @@ export default function EnvironmentFront(props) {
backgroundColor: props.backgroundColor,
backgroundImage: `url(${props.previewImage})`,
backgroundPosition: "center",
+ backgroundSize: "cover",
margin: "0",
- height: "900px",
+ height: "100vh",
width: "100%",
padding: "0",
alignItems: "center",
- justifyContent: "center"
+ justifyContent: "center",
+ display: "flex",
}}
>
+
+
+ {/*
Display Name */}
+ {(props.networkingBlock.length > 0) ? (
+ <>
+
setDisplayName(e.target.value)} />
+ {(props.networkingBlock[0].attributes.customAvatars && props.networkingBlock[0].attributes.customAvatars.value === "1") && (
+
+ VRM or Sprite URL
+ {
+ e.preventDefault();
+ setPlayerAvatar(e.dataTransfer.getData('text'));
+ }}
+ value={playerAvatar}
+ onChange={(e) => setPlayerAvatar(e.target.value)}
+ />
+
+ )}
+
{
+ goToPrivateRoom();
+ canvasRef.current.scrollIntoView({ behavior: 'smooth' });
+ setLoaded(true);
+ }}
+ style={{
+ padding: "10px"
+ }}
+ >
+ {" "}
+ {"Join Private"}
+
+ >
+ ) : (
+
+ VRM or Sprite URL
+ setPlayerAvatar(e.target.value)} />
+
+ )}
+
{
@@ -2074,13 +2361,17 @@ export default function EnvironmentFront(props) {
setLoaded(true);
}}
style={{
- margin: "0 auto",
padding: "10px"
}}
>
{" "}
- Load World{" "}
+ {props.networkingBlock.length > 0 ? "Join Public" : "Load World"}
+ {(props.networkingBlock.length > 0) && (
+
+
After entering, use the "Join Voice" button to select your microphone.
+
+ )}
);
diff --git a/blocks/environment/components/Networking.js b/blocks/environment/components/Networking.js
index abde1d4..bfec508 100644
--- a/blocks/environment/components/Networking.js
+++ b/blocks/environment/components/Networking.js
@@ -1,127 +1,744 @@
import P2PCF from "./p2pcf/p2pcf.js";
+import React, { useEffect, useMemo, useState } from "react";
+import audioIcon from '../../../inc/assets/mic_icon.png';
+import audioIconMute from '../../../inc/assets/mic_icon_mute.png';
+import participants from '../../../inc/assets/participants.png';
+import worldIcon from '../../../inc/assets/world_icon.png';
+import cornerAccent from '../../../inc/assets/corner_accent.png';
+import settingsIcon from '../../../inc/assets/settings_icon.png';
+import { color } from "@wordpress/icons";
+import { XRButton } from "@react-three/xr";
-const Networking = (props) => {
- if (!document.location.hash) {
- document.location =
- document.location.toString() + `#xpp-${props.postSlug}`;
+const DEFAULT_TURN_ICE = [
+ {
+ urls: "turn:openrelay.metered.ca:80",
+ username: "openrelayproject",
+ credential: "openrelayproject"
+ },
+ {
+ urls: "turn:openrelay.metered.ca:443",
+ username: "openrelayproject",
+ credential: "openrelayproject"
+ },
+ {
+ urls: "turn:openrelay.metered.ca:443?transport=tcp",
+ username: "openrelayproject",
+ credential: "openrelayproject"
}
+];
+
+const generateRoomId = (sharedRoomID) => {
+ const domainName = window.location.hostname;
+ return `${domainName}-${sharedRoomID}`;
+ };
+
+
+ async function fetchTURNcredentials() {
+ const endpoint = turnCredentials['apiUrl'];
+ const nonce = turnCredentials['nonce'];
+
+ try {
+ // Fetch TURN credentials from the WordPress endpoint
+ const response = await fetch(endpoint, {
+ method: 'GET',
+ headers: {
+ 'X-WP-Nonce': nonce,
+ 'Content-Type': 'application/json'
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch TURN credentials');
+ }
+
+ const data = await response.json();
+ const turnUrls = data;
+
+ return turnUrls;
+ } catch (error) {
+ console.error('Failed to fetch TURN credentials. Using defaults.', error);
+ return DEFAULT_TURN_ICE;
+ }
+}
+
+const Networking = (props) => {
+ let isNetworkActivated = props.networkingBlock.length > 0;
+ let isOffline = false;
+ let isMuted = false; // Initial state of the microphone
+ let localStream = null; // To hold the local media stream
+ const [p2pcf, setP2pcf] = useState(null);
+ const returnXRButton = (props) => {
+ return
;
+ };
+
+ const RoomDropdownContent = () => {
+ let dropdown = document.getElementById("room-dropdown");
+ // empty the contents of the dropdown
+ dropdown.innerHTML = "";
+ dropdown.innerText = "Room: " + window.p2pcf.roomId;
+
+ // create a paragraph element to be added after the dropdown
+ let roomParagraph = document.createElement("p");
+ roomParagraph.innerText = "Description: ";
+ roomParagraph.style.marginTop = "10px";
+ roomParagraph.style.marginBottom = "10px";
+ roomParagraph.style.textAlign = "left";
+
+ dropdown.appendChild(roomParagraph);
+
+ dropdown.style.display = dropdown.style.display === "none" ? "block" : "none";
+ };
+
+ const AudioDropdownContent = async (button) => {
+ let dropdown = document.getElementById("room-dropdown");
+ // empty the contents of the dropdown
+ dropdown.innerHTML = "";
+ // add a h3 heading that says "Select Microphone"
+ let heading = document.createElement("h4");
+ heading.innerText = "Select Microphone";
+ heading.style.marginTop = "10px";
+ heading.style.marginBottom = "10px";
+ heading.style.textAlign = "left";
+ heading.style.fontSize = "0.7em";
+ heading.style.textAlign = "left";
+ heading.style.fontWeight = "600";
+ heading.style.color = "white";
+ heading.style.paddingLeft = "5px";
+ heading.style.fontWeight = "600";
+ heading.style.fontFamily = "Arial";
+ dropdown.appendChild(heading);
+ // make the inner text of the dropdown a list of the users in the room
+ // loop with index for each peer
+ // add a select toggle and a button to change the microphone device
+ let select = document.createElement("select");
+ select.id = "audio-select";
+ select.style.marginTop = "10px";
+ select.style.marginBottom = "10px";
+ select.style.textAlign = "left";
+ select.style.fontSize = "0.6em";
+ select.style.width = "100%";
+ select.style.height = "30px";
+ select.style.borderRadius = "5px";
+ select.style.cursor = "pointer";
+ select.style.backgroundColor = "white";
+ select.style.color = "black";
+ select.style.padding = "5px";
+ select.style.marginBottom = "10px";
+ select.style.marginTop = "10px";
+ select.style.marginLeft = "0px";
+ select.style.marginRight = "0px";
+ select.style.border = "solid 1px #959595";
+ select.style.boxSizing = "border-box";
+ // populate the select with the available audio devices
+ navigator.mediaDevices.enumerateDevices().then(function(devices) {
+ devices.forEach(function(device) {
+ if (device.kind === 'audioinput') {
+ let option = document.createElement("option");
+ option.value = device.deviceId;
+ option.text = device.label;
+ select.appendChild(option);
+ }
+ });
+ });
+ dropdown.appendChild(select);
+ // create a button for submitting the audio device change
+ let submit = document.createElement("button");
+ submit.innerText = "Join";
+ submit.style.marginTop = "10px";
+ submit.style.marginBottom = "10px";
+ submit.style.textAlign = "center";
+ submit.style.fontSize = "0.6em";
+ submit.style.fontWeight = "600";
+ submit.style.height = "35px";
+ submit.style.borderRadius = "15px";
+ submit.style.backgroundColor = "white";
+ submit.style.color = "black";
+ submit.style.width = "55px";
+ submit.style.padding = "10px";
+ submit.style.marginBottom = "10px";
+ submit.style.marginTop = "10px";
+ submit.style.marginLeft = "0px";
+ submit.style.marginRight = "0px";
+ submit.style.border = "solid 1px #959595";
+ submit.style.cursor = "pointer";
+ submit.style.boxSizing = "border-box";
+ submit.addEventListener("click", async (event) => {
+ // get the selected audio device
+ let audioSelect = document.getElementById("audio-select");
+ let audioDevice = audioSelect.options[audioSelect.selectedIndex].value;
+ // set the audio device
+ navigator.mediaDevices.getUserMedia({ audio: { deviceId: audioDevice } }).then(function(stream) {
+ // set the local stream to the new stream
+ localStream = stream;
+ // set a window variable for the local stream
+ window.localStream = stream;
+ // loop through the peers and set their streams to the new stream
+ for (const peer of window.p2pcf.peers.values()) {
+ peer.addStream(stream);
+ }
+ });
+ // getUserMedia(); // Initialize media stream
+
+ stream = await navigator.mediaDevices.getUserMedia({
+ audio: true
+ });
+
+ // for (const peer of p2pcf.peers.values()) {
+ // peer.addStream(stream);
+ // }
+ var audioJoin = button.target.parentNode;
+ audioJoin.style.display = "none";
+ dropdown.style.display = dropdown.style.display === "none" ? "block" : "none";
+ var muteIcon = document.createElement("button");
+ muteIcon.style.backgroundImage = `url(${audioIcon})`;
+ muteIcon.style.backgroundSize = "cover";
+ muteIcon.id = "mute-icon";
+ muteIcon.style.width = "40px";
+ muteIcon.style.height = "40px";
+ muteIcon.style.padding = "10px";
+ muteIcon.style.marginTop = "3px";
+ muteIcon.style.marginRight = "5px";
+ muteIcon.style.boxSizing = "border-box";
+ muteIcon.style.borderRadius = "50%";
+ muteIcon.style.backgroundPosition = "center";
+ muteIcon.style.backgroundRepeat = "no-repeat";
+ muteIcon.style.backgroundColor = "#FFFFFF";
+ muteIcon.style.border = "solid 1px #959595";
+ muteIcon.style.backgroundSize = "30px";
+ muteIcon.addEventListener("click", (event) => {
+ // console.log("mute", event);
+ // stream.getAudioTracks()[0].enabled = !stream.getAudioTracks()[0]
+ // .enabled;
+ onMuteButtonPressed(stream);
+ });
+ var settingsIconElement = document.createElement("button");
+ settingsIconElement.style.backgroundImage = `url(${settingsIcon})`;
+ settingsIconElement.style.backgroundSize = "cover";
+ settingsIconElement.id = "mute-icon";
+ settingsIconElement.style.width = "40px";
+ settingsIconElement.style.height = "40px";
+ settingsIconElement.style.padding = "10px";
+ settingsIconElement.style.marginTop = "3px";
+ settingsIconElement.style.marginRight = "5px";
+ settingsIconElement.style.boxSizing = "border-box";
+ settingsIconElement.style.borderRadius = "50%";
+ settingsIconElement.style.backgroundPosition = "center";
+ settingsIconElement.style.backgroundRepeat = "no-repeat";
+ settingsIconElement.style.backgroundColor = "#FFFFFF";
+ settingsIconElement.style.border = "solid 1px #959595";
+ settingsIconElement.style.backgroundSize = "30px";
+ settingsIconElement.addEventListener("click", (event) => {
+ // console.log("mute", event);
+ // stream.getAudioTracks()[0].enabled = !stream.getAudioTracks()[0]
+ // .enabled;
+ SettingsDopdownContent()
+ });
+
+ //append to the audio button
+ if(audioJoin.parentNode){
+ audioJoin.parentNode.appendChild(muteIcon);
+ audioJoin.parentNode.appendChild(settingsIconElement);
+ // remove the audioJoin button
+ //audioJoin.remove();
+ toggleMute(stream);
+ }
+
+ });
+ dropdown.appendChild(submit);
+
+ dropdown.style.display = dropdown.style.display === "none" ? "block" : "none";
+
+ };
- const userProfileName =
+ const SettingsDopdownContent = async (button) => {
+
+ let dropdown = document.getElementById("room-dropdown");
+ // empty the contents of the dropdown
+ dropdown.innerHTML = "";
+ // add a h3 heading that says "Select Microphone"
+ let heading = document.createElement("h4");
+ heading.innerText = "Select Microphone";
+ heading.style.marginTop = "10px";
+ heading.style.marginBottom = "10px";
+ heading.style.textAlign = "left";
+ heading.style.fontSize = "0.7em";
+ heading.style.textAlign = "left";
+ heading.style.fontWeight = "600";
+ heading.style.color = "white";
+ heading.style.paddingLeft = "5px";
+ heading.style.fontWeight = "600";
+ heading.style.fontFamily = "Arial";
+ dropdown.appendChild(heading);
+ // make the inner text of the dropdown a list of the users in the room
+ // loop with index for each peer
+ // add a select toggle and a button to change the microphone device
+ let select = document.createElement("select");
+ select.id = "audio-select";
+ select.style.marginTop = "10px";
+ select.style.marginBottom = "10px";
+ select.style.textAlign = "left";
+ select.style.fontSize = "0.6em";
+ select.style.width = "100%";
+ select.style.height = "30px";
+ select.style.borderRadius = "5px";
+ select.style.cursor = "pointer";
+ select.style.backgroundColor = "white";
+ select.style.color = "black";
+ select.style.padding = "5px";
+ select.style.marginBottom = "10px";
+ select.style.marginTop = "10px";
+ select.style.marginLeft = "0px";
+ select.style.marginRight = "0px";
+ select.style.border = "solid 1px #959595";
+ select.style.boxSizing = "border-box";
+ // populate the select with the available audio devices
+ navigator.mediaDevices.enumerateDevices().then(function(devices) {
+ devices.forEach(function(device) {
+ if (device.kind === 'audioinput') {
+ let option = document.createElement("option");
+ option.value = device.deviceId;
+ option.text = device.label;
+ select.appendChild(option);
+ }
+ });
+ });
+ dropdown.appendChild(select);
+ // create a button for submitting the audio device change
+ let submit = document.createElement("button");
+ submit.innerText = "Switch";
+ submit.style.marginTop = "10px";
+ submit.style.marginBottom = "10px";
+ submit.style.textAlign = "center";
+ submit.style.fontSize = "0.6em";
+ submit.style.fontWeight = "600";
+ submit.style.height = "35px";
+ submit.style.borderRadius = "15px";
+ submit.style.backgroundColor = "white";
+ submit.style.color = "black";
+ submit.style.width = "55px";
+ submit.style.padding = "10px";
+ submit.style.marginBottom = "10px";
+ submit.style.marginTop = "10px";
+ submit.style.marginLeft = "0px";
+ submit.style.marginRight = "0px";
+ submit.style.border = "solid 1px #959595";
+ submit.style.cursor = "pointer";
+ submit.style.boxSizing = "border-box";
+ submit.addEventListener("click", async (event) => {
+ // get the selected audio device
+ let audioSelect = document.getElementById("audio-select");
+ let audioDevice = audioSelect.options[audioSelect.selectedIndex].value;
+ // set the audio device
+ navigator.mediaDevices.getUserMedia({ audio: { deviceId: audioDevice } }).then(function(stream) {
+ // loop through the peers and set their streams to the new stream
+ for (const peer of p2pcf.peers.values()) {
+ peer.removeStream(localStream);
+ peer.addStream(stream);
+ }
+ // set the local stream to the new stream
+ localStream = stream;
+ });
+ getUserMedia(); // Initialize media stream
+
+ stream = await navigator.mediaDevices.getUserMedia({
+ audio: true
+ });
+ for (const peer of p2pcf.peers.values()) {
+ peer.addStream(stream);
+ }
+ });
+ dropdown.appendChild(submit);
+
+ dropdown.style.display = dropdown.style.display === "none" ? "block" : "none";
+
+ };
+
+
+ const PeerDropdownContent = () => {
+ const userProfileName =
userData.userId === ""
? Math.floor(Math.random() * 100000)
: userData.userId;
- const p2pcf = new P2PCF(
- "user-" + userProfileName,
- document.location.hash.substring(1),
- {
- workerUrl: "https://p2pcf.sxpdigital.workers.dev/",
- slowPollingRateMs: 5000,
- fastPollingRateMs: 1500
+
+ window.participants[window.p2pcf.sessionId] = window.userData.inWorldName ? window.userData.inWorldName : "User-" + userProfileName;
+
+ let dropdown = document.getElementById("room-dropdown");
+ // empty the contents of the dropdown
+ dropdown.innerHTML = "";
+ // make the inner text of the dropdown a list of the users in the room
+ // loop with index for each peer
+ let index = 1;
+ if( index === 1 ){
+ let playerParagraph = document.createElement("p");
+ if(window.participants[window.p2pcf.sessionId]){
+ playerParagraph.innerHTML = '
' + index + ": " + window.participants[window.p2pcf.sessionId];
+ } else {
+ playerParagraph.innerHTML = '
' + index + ": " + peer.client_id;
+ }
+ playerParagraph.style.marginTop = "10px";
+ playerParagraph.style.marginBottom = "10px";
+ playerParagraph.style.textAlign = "left";
+ playerParagraph.style.fontSize = "0.6em";
+ dropdown.appendChild(playerParagraph);
+ index++;
}
- );
- window.p2pcf = p2pcf;
- console.log("client id:", p2pcf.clientId);
- const removePeerUi = (clientId) => {
- document.getElementById(clientId)?.remove();
- document.getElementById(`${clientId}-video`)?.remove();
- };
+ for (const peer of window.p2pcf.peers.values()) {
+ let peerParagraph = document.createElement("p");
- const addPeerUi = (sessionId) => {
- if (document.getElementById(sessionId)) return;
+ if(window.participants[peer.id]){
+ peerParagraph.innerHTML = '
' + index + ": " + window.participants[peer.id];
+ } else {
+ peerParagraph.innerHTML = '
' + index + ": " + peer.client_id;
+ }
+ // peerParagraph.innerHTML = '
' + index + ": " + window.participants[peer.id].inWorldName;
+ peerParagraph.style.marginTop = "10px";
+ peerParagraph.style.marginBottom = "10px";
+ peerParagraph.style.textAlign = "left";
+ peerParagraph.style.fontSize = "0.6em";
+ dropdown.appendChild(peerParagraph);
+ index++;
+ }
- const peerEl = document.createElement("div");
- peerEl.style = "display: flex;";
+ dropdown.style.display = dropdown.style.display === "none" ? "block" : "none";
+ };
+ // Function to toggle mute on the local audio stream
+ const toggleMute = async (stream) => {
+ if (stream) {
+ var muteIcon = document.getElementById("mute-icon");
- const name = document.createElement("div");
- name.innerText = sessionId.substring(0, 5);
+ isMuted = !isMuted;
+ // mute the local stream microphone
+ // localStream.getAudioTracks()[0].enabled = !isMuted;
+ for (let i = 0; i < window.localStream.getAudioTracks().length; i++) {
+ window.localStream.getAudioTracks()[i].enabled = !isMuted;
+ }
- peerEl.id = sessionId;
- peerEl.appendChild(name);
+ if(muteIcon){
+ if (localStream.getAudioTracks()[0].enabled) {
+ muteIcon.style.backgroundImage = `url(${audioIcon})`;
+ } else {
+ muteIcon.style.backgroundImage = `url(${audioIconMute})`;
+ }
+ }
- document.getElementById("peers").appendChild(peerEl);
+ }
};
- const addMessage = (message) => {
- const messageEl = document.createElement("div");
- messageEl.innerText = message;
- document.getElementById("messages").appendChild(messageEl);
+ // Function to get the user's media
+ const getUserMedia = async () => {
+ try {
+ localStream = await navigator.mediaDevices.getUserMedia({ audio: true });
+ // Do something with the stream like sending it to other peers
+ } catch (error) {
+ console.error('Error accessing the microphone', error);
+ }
+ };
+ // Call this function when you want to toggle mute (e.g., when a button is pressed)
+ const onMuteButtonPressed = (stream) => {
+ toggleMute(stream);
};
- let stream;
- p2pcf.on("peerconnect", (peer) => {
- console.log("Peer connect", peer.id, peer);
- console.log(peer.client_id);
- if (stream) {
- peer.addStream(stream);
- }
- peer.on("track", (track, stream) => {
- console.log("got track", track);
- const video = document.createElement("audio");
- video.id = `${peer.id}-audio`;
- video.srcObject = stream;
- video.setAttribute("playsinline", true);
- document.getElementById("videos").appendChild(video);
- video.play();
- });
- addPeerUi(peer.id);
- });
-
- p2pcf.on("peerclose", (peer) => {
- console.log("Peer close", peer.id, peer);
- removePeerUi(peer.id);
- });
-
- p2pcf.on("msg", (peer, data) => {
- addMessage(
- peer.id.substring(0, 5) +
- ": " +
- new TextDecoder("utf-8").decode(data)
- );
- });
+ useEffect(() => {
+ const mainContainer = document.getElementById("networking");
+ // set container background to accent image
+ mainContainer.style.backgroundImage = `url(${cornerAccent})`;
+ mainContainer.style.backgroundSize = "cover";
+ }, []);
const go = () => {
- document.getElementById("session-id").innerText =
- p2pcf.sessionId.substring(0, 5) + "@" + p2pcf.roomId + ":";
-
+ // document.getElementById("session-id").innerText = "Room: " + p2pcf.roomId;
+
// document.getElementById('send-button').addEventListener('click', () => {
// const box = document.getElementById('send-box');
// addMessage(p2pcf.sessionId.substring(0, 5) + ': ' + box.value);
// p2pcf.broadcast(new TextEncoder().encode(box.value));
// box.value = '';
// })
+ const mainContainer = document.getElementById("networking");
+
+ // set container background to accent image
+ mainContainer.style.backgroundImage = `url(${cornerAccent})`;
+ mainContainer.style.backgroundSize = "cover";
+ mainContainer.style.display = "block";
+
+ const audioButton = document.getElementById("audio-button");
+ if (audioButton) {
+ audioButton.addEventListener("click", async (button) => {
+ // request permissions for microphone devices then do audioDropdownContent
+ navigator.mediaDevices.getUserMedia({ audio: true }).then(function (stream) {
+ AudioDropdownContent(button);
+ });
+ });
+ }
+
+ if (isNetworkActivated && !window.p2pcf && !isOffline ) {
+ const userProfileName = Math.floor(Math.random() * 100000);
+ const domainName = window.location.hostname.replace(/\./g, '-');
+ const roomIdentifier = `3ov-${props.postSlug}`;
+ const roomId = `${domainName}-${roomIdentifier}`;
+
+ fetchTURNcredentials().then(iceServers => {
+ if (!iceServers) {
+ console.error('Could not fetch TURN credentials. P2P functionality may be limited.');
+ }
+
+ const p2pcf = new P2PCF(
+ "user-" + userProfileName,
+ roomId,
+ {
+ workerUrl: multiplayerWorker,
+ slowPollingRateMs: 5000,
+ fastPollingRateMs: 1500,
+ participantLimit: props.networkingBlock[0].attributes.participantLimit.value,
+ // Use fetched TURN credentials if available
+ turnIceServers: iceServers,
+ }
+ );
+
+ setupP2PCF(p2pcf);
+ setP2pcf(p2pcf);
+ window.p2pcf = p2pcf;
+ window.participants = [];
+
+ });
+ }
+
+ // if the room id is different from the url/#hash then reinitialize the p2pcf
+ if (window.p2pcf && window.p2pcf.roomId !== window.location.hash.substring(1)) {
+ console.log("should not be hitting here");
+ // remove the window.p2pcf object from the window
+ delete window.p2pcf;
+ // Reinitialize P2PCF with the new room ID
+ const userProfileName = Math.floor(Math.random() * 100000);
+ let p2pcf = new P2PCF(
+ "user-" + userProfileName,
+ window.location.hash.substring(1),
+ {
+ workerUrl: multiplayerWorker,
+ slowPollingRateMs: 5000,
+ fastPollingRateMs: 1500,
+ participantLimit: props.networkingBlock[0].attributes.participantLimit.value,
+ }
+ );
+ setupP2PCF(p2pcf);
+ p2pcf.start({ playerVRM: userData.playerVRM ? userData.playerVRM : defaultAvatar });
+ } else if (window.p2pcf) {
+ window.p2pcf.start({ playerVRM: userData.playerVRM ? userData.playerVRM : defaultAvatar });
+ }
+ addPeerUi();
+ addRoomUi();
+ };
- document
- .getElementById("audio-button")
- .addEventListener("click", async () => {
- stream = await navigator.mediaDevices.getUserMedia({
- audio: true
- });
+ useEffect(() => {
+ if (isNetworkActivated) {
+ const domainName = window.location.hostname.replace(/\./g, '-');
+ const roomIdentifier = `3ov-${props.postSlug}`;
+ const roomId = `${domainName}-${roomIdentifier}`;
+
+ if (!document.location.hash) {
+ document.location = document.location.toString() + `#${roomId}`;
+ }
+ }
+
+ const handleLoaded = (event) => {
+ go();
+ // Remove the event listener after handling the first 'loaded' event
+ window.removeEventListener("loaded", handleLoaded);
+ };
+
+ window.addEventListener("loaded", handleLoaded);
+
+ return () => {
+ window.removeEventListener("loaded", handleLoaded);
+ };
+ }, []);
+
- for (const peer of p2pcf.peers.values()) {
+ useEffect(() => {
+ if(isNetworkActivated && window.p2pcf ){
+ window.p2pcf.on("roomfullrefresh", (peer) => {
+ console.log("So sorry, Room full refresh", peer);
+ const userProfileName = Math.floor(Math.random() * 100000);
+
+ // Wait for the URL hash to update to ensure room ID is new
+ setTimeout(() => {
+ // remove the window.p2pcf object from the window
+ delete window.p2pcf;
+ // Reinitialize P2PCF with the new room ID
+ const p2pcf = new P2PCF(
+ "user-" + userProfileName,
+ window.location.hash.substring(1),
+ {
+ workerUrl: multiplayerWorker,
+ slowPollingRateMs: 5000,
+ fastPollingRateMs: 1500,
+ participantLimit: props.networkingBlock[0].attributes.participantLimit.value,
+ }
+ );
+ setupP2PCF(p2pcf);
+
+ }, 500);
+ });
+ }
+
+ if( isNetworkActivated && window.p2pcf ){
+ window.p2pcf.on("peerconnect", (peer) => {
+ if (stream) {
peer.addStream(stream);
}
+ peer.on("track", (track, stream) => {
+ const video = document.createElement("audio");
+ video.id = `${peer.id}-audio`;
+ video.srcObject = stream;
+ video.setAttribute("playsinline", true);
+ document.getElementById("videos").appendChild(video);
+ video.play();
+ });
+ });
+
+ window.p2pcf.on("peerclose", (peer) => {
+ removePeerUi(peer.id);
});
+
+ window.p2pcf.on("msg", (peer, data) => {
+ addMessage(
+ peer.id.substring(0, 5) +
+ ": " +
+ new TextDecoder("utf-8").decode(data)
+ );
+ });
+ }
+ }, [p2pcf]);
+
+ // if( isNetworkActivated ){
+ // if ( ! document.location.hash ) {
+ // document.location = document.location.toString() + `#3ov-${props.postSlug}`;
+ // }
+ // const userProfileName = Math.floor( Math.random() * 100000 );
+ // let p2pcf = new P2PCF(
+ // "user-" + userProfileName,
+ // document.location.hash.substring(1),
+ // {
+ // workerUrl: multiplayerWorker,
+ // slowPollingRateMs: 5000,
+ // fastPollingRateMs: 1500,
+ // participantLimit: props.networkingBlock[0].attributes.participantLimit.value,
+ // turnCredentials: turnCredentials,
+ // turnIceServers: turnIceServers,
+ // }
+ // );
+
+ // window.p2pcf = p2pcf;
+ // window.participants = [];
+ //}
- p2pcf.start();
+ const removePeerUi = (clientId) => {
+ document.getElementById(clientId)?.remove();
+ document.getElementById(`${clientId}-video`)?.remove();
};
- if (
- document.readyState === "complete" ||
- document.readyState === "interactive"
- ) {
- document
- .getElementById("join-button")
- .addEventListener("click", async () => {
- window.addEventListener("DOMContentLoaded", audio, {
- once: true
- });
- // window.addEventListener('DOMContentLoaded', go, { once: true })
- });
- } else {
- window.addEventListener("DOMContentLoaded", go, { once: true });
- }
+
+ const setupP2PCF = (p2pcfInstance) => {
+ // Start the P2PCF instance with any necessary configurations
+ p2pcfInstance.start({ playerVRM: userData.playerVRM ? userData.playerVRM : defaultAvatar });
+ window.p2pcf = p2pcfInstance;
+ };
+
+
+ const addPeerUi = (sessionId) => {
+ // if (document.getElementById(sessionId)) return;
+ var peerIcon = document.createElement("button");
+ peerIcon.style.backgroundImage = `url(${participants})`;
+ peerIcon.style.backgroundSize = "cover";
+ peerIcon.style.width = "40px";
+ peerIcon.style.height = "40px";
+ peerIcon.style.padding = "10px";
+ peerIcon.style.boxSizing = "border-box";
+ peerIcon.style.borderRadius = "50%";
+ peerIcon.style.backgroundPosition = "center";
+ peerIcon.style.backgroundRepeat = "no-repeat";
+ peerIcon.style.backgroundColor = "#FFFFFF";
+ peerIcon.style.border = "solid 1px #959595";
+ peerIcon.style.backgroundSize = "30px";
+ peerIcon.style.marginRight = "5px";
+ peerIcon.style.marginTop = "3px";
+ peerIcon.style.cursor = "pointer";
+
+ // const peerEl = document.createElement("div");
+ // peerEl.style = "display: flex;";
+
+ // const name = document.createElement("div");
+ // name.innerText = sessionId.substring(0, 5);
+
+ // peerEl.id = sessionId;
+ // peerEl.appendChild(name);
+
+ // add click listener
+ peerIcon.addEventListener("click", (event) => {
+ PeerDropdownContent();
+ // Position the dropdown near the roomIcon
+ let dropdown = document.getElementById("room-dropdown");
+ });
+
+ document.getElementById("network-ui-container").prepend(peerIcon);
+ };
+ const addRoomUi = (sessionId) => {
+ // if (document.getElementById(sessionId)) return;
+ var roomIcon = document.createElement("button");
+ roomIcon.style.backgroundImage = `url(${worldIcon})`;
+ roomIcon.style.backgroundSize = "cover";
+ roomIcon.style.width = "40px";
+ roomIcon.style.height = "40px";
+ roomIcon.style.padding = "10px";
+ roomIcon.style.marginTop = "3px";
+ roomIcon.style.marginRight = "5px";
+ roomIcon.style.marginLeft = "5px";
+ roomIcon.style.boxSizing = "border-box";
+ roomIcon.style.borderRadius = "50%";
+ roomIcon.style.backgroundPosition = "center";
+ roomIcon.style.backgroundRepeat = "no-repeat";
+ roomIcon.style.backgroundColor = "#FFFFFF";
+ roomIcon.style.border = "solid 1px #959595";
+ roomIcon.style.cursor = "pointer";
+ roomIcon.style.backgroundSize = "30px";
+ roomIcon.addEventListener("click", (event) => {
+ RoomDropdownContent();
+ // Position the dropdown near the roomIcon
+ let dropdown = document.getElementById("room-dropdown");
+ dropdown.style.left = roomIcon.offsetLeft + "px";
+ dropdown.style.top = roomIcon.offsetTop + roomIcon.offsetHeight + "px";
+ });
+
+ let dropdown = document.getElementById("room-dropdown");
+ dropdown.style.display = "none";
+ dropdown.style.position = "absolute";
+ dropdown.style.backgroundColor = "#000000cc";
+ dropdown.style.color = "white";
+ dropdown.style.padding = "10px";
+ dropdown.style.width = "200px";
+ dropdown.style.height = "150px";
+ dropdown.style.borderRadius = "15px";
+ // add corner accent
+ dropdown.style.backgroundImage = `url(${cornerAccent})`;
+ dropdown.style.backgroundSize = "auto";
+ dropdown.style.backgroundPosition = "top left"
+ dropdown.style.backgroundRepeat = "no-repeat";
+
+
+ document.getElementById("network-ui-container").prepend(roomIcon);
+ // prepend the returnXRButton to the network-ui-container
+ };
+
+ const addMessage = (message) => {
+ const messageEl = document.createElement("div");
+ messageEl.innerText = message;
+
+ document.getElementById("messages").appendChild(messageEl);
+ };
+ let stream;
+
return <>>;
};
-export default Networking;
\ No newline at end of file
+export default Networking;
diff --git a/blocks/environment/components/Player.js b/blocks/environment/components/Player.js
index 79f2b65..22b618c 100644
--- a/blocks/environment/components/Player.js
+++ b/blocks/environment/components/Player.js
@@ -1,701 +1,1207 @@
-import { Mesh, Raycaster, DoubleSide, MeshBasicMaterial, RingGeometry, AudioListener, Group, Quaternion, Matrix4, VectorKeyframeTrack, QuaternionKeyframeTrack, LoopPingPong, AnimationClip, NumberKeyframeTrack, AnimationMixer, Vector3, Vector2, BufferGeometry, CircleGeometry, sRGBEncoding, MathUtils } from "three";
+import { Box3,
+ Mesh,
+ Raycaster,
+ PerspectiveCamera,
+ ArrowHelper,
+ Euler, MathUtils, NearestFilter, LoopOnce, DoubleSide, MeshBasicMaterial, RingGeometry, BoxGeometry, AudioListener, Color, Group, Quaternion, Matrix4, VectorKeyframeTrack, QuaternionKeyframeTrack, LoopPingPong, AnimationClip, NumberKeyframeTrack, AnimationMixer, Vector3, Vector2, BufferGeometry, CircleGeometry, sRGBEncoding } from "three";
import { TextureLoader } from "three/src/loaders/TextureLoader";
import { useFrame, useLoader, useThree, Interactive } from "@react-three/fiber";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
+import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader';
+import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";
import { FBXLoader } from "three/examples/jsm/loaders/FBXLoader";
-import { OrbitControls } from '@react-three/drei';
-import { useKeyboardControls } from "./Controls"
+import { OrbitControls, SpriteAnimator, KeyboardControls } from '@react-three/drei';
+import * as SkeletonUtils from "three/examples/jsm/utils/SkeletonUtils.js";
import { useRef, useState, useEffect } from "react";
+import { useXR, useController } from '@react-three/xr';
import { RigidBody, CapsuleCollider, useRapier, vec3, interactionGroups, CuboidCollider } from "@react-three/rapier";
import defaultVRM from "../../../inc/avatars/3ov_default_avatar.vrm";
-import { VRMUtils, VRMSchema, VRMLoaderPlugin, VRMExpressionPresetName, VRMHumanBoneName } from "@pixiv/three-vrm";
-import { useXR } from "@react-three/xr";
+import blankVRM from "../../../inc/avatars/blank_avatar.vrm";
+import { VRMUtils, VRMHumanBones, VRMSchema, VRMLoaderPlugin, VRMSpringBoneManager, VRMExpressionPresetName, VRMHumanBoneName, VRM } from "@pixiv/three-vrm";
+
import idle from "../../../inc/avatars/friendly.fbx";
import walk from "../../../inc/avatars/walking.fbx";
import run from "../../../inc/avatars/running.fbx";
+import jump from "../../../inc/avatars/Jump.fbx";
+import fall from "../../../inc/avatars/falling.fbx";
+import { getMixamoRig } from "../utils/rigMap";
+import ShapePointsMesh from "../utils/ShapePointsMesh";
+import DynLineMesh from "../utils/DynLineMesh";
+import Ecctrl, { EcctrlAnimation, useGame, useFollowCam, useJoystickControls } from "ecctrl";
+// import avatar from ./avatar/index.js
+import { ExokitAvatar } from "./avatar";
-function Reticle() {
- const { camera } = useThree();
- var reticle = new Mesh(
- new RingGeometry( 0.85 * 5, 5, 32),
- new MeshBasicMaterial( {color: 0xffffff, side: DoubleSide })
- );
- reticle.scale.set(1.3, 1.3, 1.3);
- reticle.position.z = -1000;
- reticle.name = "reticle";
- reticle.frustumCulled = false;
- reticle.renderOrder = 1000;
- reticle.lookAt(camera.position)
- reticle.material.depthTest = false;
- reticle.material.depthWrite = false;
- reticle.material.opacity = 0.025;
-
- return reticle;
-}
-/**
- * A map from Mixamo rig name to VRM Humanoid bone name
- */
-const mixamoVRMRigMap = {
- mixamorigHips: 'hips',
- mixamorigSpine: 'spine',
- mixamorigSpine1: 'chest',
- mixamorigSpine2: 'upperChest',
- mixamorigNeck: 'neck',
- mixamorigHead: 'head',
- mixamorigLeftShoulder: 'leftShoulder',
- mixamorigLeftArm: 'leftUpperArm',
- mixamorigLeftForeArm: 'leftLowerArm',
- mixamorigLeftHand: 'leftHand',
- mixamorigLeftHandThumb1: 'leftThumbMetacarpal',
- mixamorigLeftHandThumb2: 'leftThumbProximal',
- mixamorigLeftHandThumb3: 'leftThumbDistal',
- mixamorigLeftHandIndex1: 'leftIndexProximal',
- mixamorigLeftHandIndex2: 'leftIndexIntermediate',
- mixamorigLeftHandIndex3: 'leftIndexDistal',
- mixamorigLeftHandMiddle1: 'leftMiddleProximal',
- mixamorigLeftHandMiddle2: 'leftMiddleIntermediate',
- mixamorigLeftHandMiddle3: 'leftMiddleDistal',
- mixamorigLeftHandRing1: 'leftRingProximal',
- mixamorigLeftHandRing2: 'leftRingIntermediate',
- mixamorigLeftHandRing3: 'leftRingDistal',
- mixamorigLeftHandPinky1: 'leftLittleProximal',
- mixamorigLeftHandPinky2: 'leftLittleIntermediate',
- mixamorigLeftHandPinky3: 'leftLittleDistal',
- mixamorigRightShoulder: 'rightShoulder',
- mixamorigRightArm: 'rightUpperArm',
- mixamorigRightForeArm: 'rightLowerArm',
- mixamorigRightHand: 'rightHand',
- mixamorigRightHandPinky1: 'rightLittleProximal',
- mixamorigRightHandPinky2: 'rightLittleIntermediate',
- mixamorigRightHandPinky3: 'rightLittleDistal',
- mixamorigRightHandRing1: 'rightRingProximal',
- mixamorigRightHandRing2: 'rightRingIntermediate',
- mixamorigRightHandRing3: 'rightRingDistal',
- mixamorigRightHandMiddle1: 'rightMiddleProximal',
- mixamorigRightHandMiddle2: 'rightMiddleIntermediate',
- mixamorigRightHandMiddle3: 'rightMiddleDistal',
- mixamorigRightHandIndex1: 'rightIndexProximal',
- mixamorigRightHandIndex2: 'rightIndexIntermediate',
- mixamorigRightHandIndex3: 'rightIndexDistal',
- mixamorigRightHandThumb1: 'rightThumbMetacarpal',
- mixamorigRightHandThumb2: 'rightThumbProximal',
- mixamorigRightHandThumb3: 'rightThumbDistal',
- mixamorigLeftUpLeg: 'leftUpperLeg',
- mixamorigLeftLeg: 'leftLowerLeg',
- mixamorigLeftFoot: 'leftFoot',
- mixamorigLeftToeBase: 'leftToes',
- mixamorigRightUpLeg: 'rightUpperLeg',
- mixamorigRightLeg: 'rightLowerLeg',
- mixamorigRightFoot: 'rightFoot',
- mixamorigRightToeBase: 'rightToes',
-};
+import {
+ Armature,
+ Pose,
+ BipedRig,
+ IKChain,
+ HipSolver,
+ SpineSolver,
+ LimbSolver,
+ FootSolver,
+ SwingTwistSolver,
+ SwingTwistEndsSolver,
+ ZSolver
+} from 'ossos';
+
+const DamperTimeS = 0.15;
+
+const __rot = new Quaternion();
+const __shoulderWPos = new Vector3();
+const __originWPos = new Vector3();
+const __originWDir = new Vector3();
+const __offset = new Vector3();
+
+const mixamoVRMRigMap = getMixamoRig();
-/**
- * Download Mixamo animation, convert it for usage with three-vrm, and return the converted animation.
- *
- * @param {string} url - The URL of Mixamo animation data
- * @param {VRM} vrm - The target VRM
- * @returns {Promise
} - The adapted AnimationClip
- */
+function addHandRotationControls() {
+ const container = document.createElement('div');
+ container.style.position = 'fixed';
+ container.style.top = '10px';
+ container.style.right = '10px';
+ container.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
+ container.style.padding = '10px';
+ container.style.borderRadius = '5px';
+ container.style.color = 'white';
+ container.style.fontFamily = 'Arial, sans-serif';
+ container.style.zIndex = '10000';
+ container.style.maxHeight = '80vh';
+ container.style.overflowY = 'auto';
+
+ const createSlider = (name, min, max, step, defaultValue) => {
+ const label = document.createElement('label');
+ label.textContent = `${name}: `;
+ label.style.display = 'block';
+ label.style.marginBottom = '5px';
+
+ const slider = document.createElement('input');
+ slider.type = 'range';
+ slider.min = min;
+ slider.max = max;
+ slider.step = step;
+ slider.value = defaultValue;
+ slider.style.width = '100%';
+
+ const valueDisplay = document.createElement('span');
+ valueDisplay.textContent = defaultValue;
+ valueDisplay.style.marginLeft = '5px';
+
+ slider.addEventListener('input', () => {
+ valueDisplay.textContent = slider.value;
+ window.handRotationControls[name] = parseFloat(slider.value);
+ });
+
+ label.appendChild(slider);
+ label.appendChild(valueDisplay);
+ return label;
+ };
+
+ const createCheckbox = (name) => {
+ const label = document.createElement('label');
+ label.style.display = 'block';
+ label.style.marginBottom = '5px';
+
+ const checkbox = document.createElement('input');
+ checkbox.type = 'checkbox';
+ checkbox.style.marginRight = '5px';
+
+ checkbox.addEventListener('change', () => {
+ window.handRotationControls[name] = checkbox.checked;
+ });
+
+ label.appendChild(checkbox);
+ label.appendChild(document.createTextNode(name));
+ return label;
+ };
+
+ window.handRotationControls = {
+ rightHandRotationOffsetX: 0,
+ rightHandRotationOffsetY: 0,
+ rightHandRotationOffsetZ: -90,
+ leftHandRotationOffsetX: 0,
+ leftHandRotationOffsetY: 0,
+ leftHandRotationOffsetZ: 90,
+ flipRightHandX: false,
+ flipRightHandY: false,
+ flipRightHandZ: false,
+ flipLeftHandX: false,
+ flipLeftHandY: false,
+ flipLeftHandZ: false,
+ rightArmPoleX: 0,
+ rightArmPoleY: 0,
+ rightArmPoleZ: -1,
+ leftArmPoleX: 0,
+ leftArmPoleY: 0,
+ leftArmPoleZ: -1
+ };
+
+ container.appendChild(createSlider('rightHandRotationOffsetX', -180, 180, 1, 0));
+ container.appendChild(createSlider('rightHandRotationOffsetY', -180, 180, 1, 0));
+ container.appendChild(createSlider('rightHandRotationOffsetZ', -180, 180, 1, 0));
+ container.appendChild(createSlider('leftHandRotationOffsetX', -180, 180, 1, 0));
+ container.appendChild(createSlider('leftHandRotationOffsetY', -180, 180, 1, 0));
+ container.appendChild(createSlider('leftHandRotationOffsetZ', -180, 180, 1, 0));
+ container.appendChild(createCheckbox('flipRightHandX'));
+ container.appendChild(createCheckbox('flipRightHandY'));
+ container.appendChild(createCheckbox('flipRightHandZ'));
+ container.appendChild(createCheckbox('flipLeftHandX'));
+ container.appendChild(createCheckbox('flipLeftHandY'));
+ container.appendChild(createCheckbox('flipLeftHandZ'));
+
+ // Add arm pole target sliders
+ container.appendChild(document.createElement('hr'));
+ container.appendChild(document.createTextNode('Arm Pole Targets:'));
+ container.appendChild(createSlider('rightArmPoleX', -1, 1, 0.1, 0));
+ container.appendChild(createSlider('rightArmPoleY', -1, 1, 0.1, 0));
+ container.appendChild(createSlider('rightArmPoleZ', -1, 1, 0.1, -1));
+ container.appendChild(createSlider('leftArmPoleX', -1, 1, 0.1, 0));
+ container.appendChild(createSlider('leftArmPoleY', -1, 1, 0.1, 0));
+ container.appendChild(createSlider('leftArmPoleZ', -1, 1, 0.1, -1));
+
+ // document.body.appendChild(container);
+ }
+
function loadMixamoAnimation(url, vrm) {
- let loader;
- if (url.endsWith('.fbx')) {
- loader = new FBXLoader(); // Use an FBX loader
- } else {
- loader = new GLTFLoader(); // Use a GLTF loader
+let loader;
+if (url.endsWith('.fbx')) {
+ loader = new FBXLoader();
+} else {
+ loader = new GLTFLoader();
+}
+return loader.loadAsync(url).then((resource) => {
+ const clip = resource.animations[0];
+
+ if (url.endsWith('.glb')) {
+ resource = resource.scene;
}
- return loader.loadAsync(url).then((resource) => {
- const clip = resource.animations[0]; // Extract the AnimationClip
- // if resource is GLB, get the scene
- if (url.endsWith('.glb')) {
- resource = resource.scene;
- }
+ let tracks = [];
- let tracks = []; // KeyframeTracks compatible with VRM to be stored here
-
- let restRotationInverse = new Quaternion();
- let parentRestWorldRotation = new Quaternion();
- let _quatA = new Quaternion();
- let _vec3 = new Vector3();
-
- // Adjust according to the height of the hips.
- let mixamoHips = resource.getObjectByName('mixamorigHips');
- let regularHips = resource.getObjectByName('hips');
- let mainHip;
- if (mixamoHips) {
- mainHip = mixamoHips.position.y;
- } else if (regularHips) {
- mainHip = regularHips.position.y;
- }
- const vrmHipsY = vrm.humanoid?.getNormalizedBoneNode('hips').getWorldPosition(_vec3).y;
- const vrmRootY = vrm.scene.getWorldPosition(_vec3).y;
- const vrmHipsHeight = Math.abs(vrmHipsY - vrmRootY);
- const hipsPositionScale = vrmHipsHeight / mainHip;
+ let restRotationInverse = new Quaternion();
+ let parentRestWorldRotation = new Quaternion();
+ let _quatA = new Quaternion();
+ let _vec3 = new Vector3();
- clip.tracks.forEach((track) => {
- // Convert each track for VRM usage, and push to `tracks`
- let trackSplitted = track.name.split('.');
- let mixamoRigName = trackSplitted[0];
- let vrmBoneName = mixamoVRMRigMap[mixamoRigName];
- let vrmNodeName = vrm.humanoid?.getNormalizedBoneNode(vrmBoneName)?.name;
- let mixamoRigNode = resource.getObjectByName(mixamoRigName);
+ let mixamoHips = resource.getObjectByName('mixamorigHips');
+ let regularHips = resource.getObjectByName('hips');
+ let mainHip;
+ if (mixamoHips) {
+ mainHip = mixamoHips.position.y;
+ } else if (regularHips) {
+ mainHip = regularHips.position.y;
+ }
+ VRMUtils.rotateVRM0(vrm);
+ VRMUtils.removeUnnecessaryVertices( vrm.scene );
+ VRMUtils.removeUnnecessaryJoints( vrm.scene );
+ const vrmHipsY = vrm.humanoid?.getNormalizedBoneNode('hips').getWorldPosition(_vec3).y;
+ const vrmRootY = vrm.scene.getWorldPosition(_vec3).y;
+ const vrmHipsHeight = Math.abs(vrmHipsY - vrmRootY);
+ const hipsPositionScale = vrmHipsHeight / mainHip;
- if (vrmNodeName != null) {
+ clip.tracks.forEach((track) => {
+ let trackSplitted = track.name.split('.');
+ let mixamoRigName = trackSplitted[0];
+ let vrmBoneName = mixamoVRMRigMap[mixamoRigName];
+ let vrmNodeName = vrm.humanoid?.getNormalizedBoneNode(vrmBoneName)?.name;
+ let mixamoRigNode = resource.getObjectByName(mixamoRigName);
- let propertyName = trackSplitted[1];
+ if (vrmNodeName != null) {
+ let propertyName = trackSplitted[1];
- // Store rotations of rest-pose.
- mixamoRigNode.getWorldQuaternion(restRotationInverse).invert();
- mixamoRigNode.parent.getWorldQuaternion(parentRestWorldRotation);
+ mixamoRigNode.getWorldQuaternion(restRotationInverse).invert();
+ mixamoRigNode.parent.getWorldQuaternion(parentRestWorldRotation);
- if (track instanceof QuaternionKeyframeTrack) {
+ if (track instanceof QuaternionKeyframeTrack) {
+ for (let i = 0; i < track.values.length; i += 4) {
+ let flatQuaternion = track.values.slice(i, i + 4);
- // Retarget rotation of mixamoRig to NormalizedBone.
- for (let i = 0; i < track.values.length; i += 4) {
+ _quatA.fromArray(flatQuaternion);
- let flatQuaternion = track.values.slice(i, i + 4);
+ _quatA
+ .premultiply(parentRestWorldRotation)
+ .multiply(restRotationInverse);
- _quatA.fromArray(flatQuaternion);
+ _quatA.toArray(flatQuaternion);
- _quatA
- .premultiply(parentRestWorldRotation)
- .multiply(restRotationInverse);
+ flatQuaternion.forEach((v, index) => {
+ track.values[index + i] = v;
+ });
+ }
- _quatA.toArray(flatQuaternion);
+ tracks.push(
+ new QuaternionKeyframeTrack(
+ `${vrmNodeName}.${propertyName}`,
+ track.times,
+ track.values.map((v, i) => (vrm.meta?.metaVersion === '0' && i % 2 === 0 ? -v : v)),
+ ),
+ );
+ } else if (track instanceof VectorKeyframeTrack) {
+ let value = track.values.map((v, i) => (vrm.meta?.metaVersion === '0' && i % 3 !== 1 ? -v : v) * hipsPositionScale);
+ tracks.push(new VectorKeyframeTrack(`${vrmNodeName}.${propertyName}`, track.times, value));
+ }
+ }
+ });
- flatQuaternion.forEach((v, index) => {
+ return new AnimationClip('vrmAnimation', clip.duration, tracks);
+});
+}
- track.values[index + i] = v;
+function addResetButton(props) {
+const button = document.createElement('button');
+button.innerHTML = 'Respawn';
+button.onclick = () => {
+ props.movement.current.respawn = true;
+ setTimeout(() => {
+ props.movement.current.respawn = false;
+ }, 100);
+};
- });
+button.style.position = 'fixed';
+button.style.bottom = '190px';
+button.style.left = '10px';
+button.style.zIndex = '1000';
+button.style.padding = '10px';
+button.style.border = 'none';
+button.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
+button.style.color = 'white';
+button.style.cursor = 'pointer';
+button.style.borderRadius = '5px';
+button.style.fontFamily = 'Arial';
+button.style.fontSize = '16px';
+button.style.fontWeight = 'bold';
+document.body.appendChild(button);
+}
- }
+class XrHead {
+constructor(context) {
+ this.context = context;
+ this.position = new Vector3();
+ this.quaternion = new Quaternion();
+ this.worldUp = new Vector3();
+ this.forward = new Vector3();
+ this.up = new Vector3();
+ this.right = new Vector3();
+}
- tracks.push(
- new QuaternionKeyframeTrack(
- `${vrmNodeName}.${propertyName}`,
- track.times,
- track.values.map((v, i) => (vrm.meta?.metaVersion === '0' && i % 2 === 0 ? - v : v)),
- ),
- );
-
- } else if (track instanceof VectorKeyframeTrack) {
- let value = track.values.map((v, i) => (vrm.meta?.metaVersion === '0' && i % 3 !== 1 ? - v : v) * hipsPositionScale);
- tracks.push(new VectorKeyframeTrack(`${vrmNodeName}.${propertyName}`, track.times, value));
- }
+update() {
+ this.context.camera.getWorldPosition(this.position);
+ this.context.camera.getWorldQuaternion(this.quaternion);
+ this.worldUp.set(0, 1, 0);
+ this.up.set(0, 1, 0).applyQuaternion(this.quaternion);
+ this.forward.set(0, 0, -1).applyQuaternion(this.quaternion);
+ this.right.set(1, 0, 0).applyQuaternion(this.quaternion);
+}
+}
- }
+class Vector3Damper {
+constructor(period) {
+ this.period = period || 0.15;
+ this._samples = [];
+ this._total = new Vector3();
+ this._average = new Vector3();
+}
- });
+add(time, sample) {
+ const removeSamplesBefore = time - this.period;
+ while (this._samples.length && this._samples[0].time < removeSamplesBefore) {
+ const s = this._samples.shift();
+ this._total.x -= s.x;
+ this._total.y -= s.y;
+ this._total.z -= s.z;
+ }
+ this._total.x += sample.x;
+ this._total.y += sample.y;
+ this._total.z += sample.z;
+ this._samples.push({ time: time, x: sample.x, y: sample.y, z: sample.z });
+ const count = this._samples.length;
+ this._average.set(this._total.x / count, this._total.y / count, this._total.z / count);
+ return this._average;
+}
- return new AnimationClip('vrmAnimation', clip.duration, tracks);
+get average() {
+ return this._average;
+}
- });
+clear() {
+ this._samples = [];
+ this._total.setScalar(0);
+ this._average.setScalar(0);
}
+}
export default function Player(props) {
- const canMoveRef = useRef(true);
- const falling = useRef(true);
- const animationsRef = useRef();
- const orbitRef = useRef();
- const rigidRef = useRef();
- const castRef = useRef();
-
- const idleFile = threeObjectPlugin + idle;
- const walkingFile = threeObjectPlugin + walk;
- const runningFile = threeObjectPlugin + run;
- // const [walkFile, setWalkFile] = useState(model.threeObjectPlugin + walk);
- const spawnPoint = props.spawnPoint? props.spawnPoint.map(Number) : [0,0,0]; // convert spawnPoint to numbers
- const { controllers } = useXR();
- const { camera, scene, clock } = useThree();
- const { world, rapier } = useRapier();
- const participantObject = scene.getObjectByName("playerOne");
- const mouse = new Vector2();
-
- // if (!scene.getObjectByName("reticle")){
- // camera.add(Reticle());
- // }
+const [isModelLoaded, setIsModelLoaded] = useState(false);
+const currentPlayerAvatarRef = useRef(null);
+const playerControllerRef = useRef(null);
+const playerMixerRef = useRef(null);
+const { camera, gl } = useThree();
+const { isPresenting } = useXR();
+const [presentingState, setPresentingState] = useState(false);
+const prevPositionRef = useRef(null);
+const { controllers } = useXR();
+const rightController = useController('right');
+const leftController = useController('left');
+const head = useRef(new XrHead(useThree()));
+const pointerOriginDamper = useRef(new Vector3Damper(DamperTimeS));
+const pointerDirectionDamper = useRef(new Vector3Damper(DamperTimeS));
- if ( controllers.length > 0 ) {
- scene.remove(scene.getObjectByName("reticle"));
- }
+const characterRef = useRef(null);
+
+const [frameName, setFrameName] = useState();
+
+const canMoveRef = useRef(true);
+const spriteRef = useRef();
+const animationsRef = useRef();
+const playerModelRef = useRef();
+
+const orbitRef = useRef();
+const rigidRef = useRef();
+const castRef = useRef();
+const [loaderIsGone, setLoaderIsGone] = useState(false);
+const [avatarIsSprite, setAvatarIsSprite] = useState(false);
+
+const curAnimation = useGame((state) => state.curAnimation);
+const initializeAnimationSet = useGame(
+ (state) => state.initializeAnimationSet
+);
+const idleAnimation = useGame((state) => state.idle);
+const walkAnimation = useGame((state) => state.walk);
+const runAnimation = useGame((state) => state.run);
+const action1Animation = useGame((state) => state.action1);
+const action2Animation = useGame((state) => state.action2);
+const action3Animation = useGame((state) => state.action3);
+const action4Animation = useGame((state) => state.action4);
+const resetAnimation = useGame((state) => state.reset);
+const [open, setOpen] = useState(false);
+const HEAD_LAYER = 1;
+
+
+const animationSet = {
+ idle: "idle",
+ walk: "walking",
+ run: "running",
+ jump: "jump",
+};
+useEffect(() => {
+ initializeAnimationSet(animationSet);
+}, []);
- // useFrame(() => {
- // if (participantObject) {
- // const posY = participantObject.parent.position.y;
- // camera.position.setY(posY + 0.23);
- // }
- // });
+useEffect(() => {
+ const handleReady = () => {
+ setLoaderIsGone(true);
+ removeEventListener('loaderIsGone', handleReady);
+ };
+ window.addEventListener('loaderIsGone', handleReady);
+ addResetButton(props);
+}, []);
- // Participant VRM.
- const fallbackURL = threeObjectPlugin + defaultVRM;
- const defaultAvatarURL = props.defaultAvatar;
- let playerURL = userData.vrm ? userData.vrm : fallbackURL;
- if(defaultAvatarURL){
- playerURL = defaultAvatarURL;
+const idleFile = idle;
+const walkingFile = walk;
+const runningFile = run;
+const jumpFile = jump;
+const fallingFile = fall;
+const spawnPoint = props.spawnPoint ? props.spawnPoint.map(Number) : [0, 0, 0];
+const { scene, clock } = useThree();
+const { world, rapier } = useRapier();
+const participantObject = scene.getObjectByName("playerOne");
+let debug = {};
+
+useEffect(() => {
+ if (userData.playerVRM.endsWith('.png')) {
+ setAvatarIsSprite(true);
}
- const someSceneState = useLoader(GLTFLoader, playerURL, (loader) => {
- loader.register((parser) => {
- return new VRMLoaderPlugin(parser);
+}, []);
+
+let animationFiles = [idleFile, walkingFile, runningFile, jumpFile];
+// Participant VRM.
+const fallbackURL = defaultVRM;
+const defaultAvatarURL = props.defaultPlayerAvatar;
+let playerURL;
+if(defaultAvatarURL){
+ playerURL = defaultAvatarURL;
+}
+playerURL = userData.playerVRM ? userData.playerVRM : fallbackURL;
+if( playerURL.endsWith( '.png' ) ){
+ playerURL = blankVRM;
+}
+
+// if the playerURL ends in .png
+useEffect(() => {
+ if( userData.playerVRM.endsWith( '.png' ) ){
+ setAvatarIsSprite(true);
+ }
+}, []);
+
+useEffect(() => {
+ if (!currentPlayerAvatarRef.current) {
+ const loader = new GLTFLoader();
+ const ktx2Loader = new KTX2Loader();
+ ktx2Loader.setTranscoderPath(threeObjectPluginRoot + "/inc/utils/basis/");
+ ktx2Loader.detectSupport(gl);
+ loader.setKTX2Loader(ktx2Loader);
+ // const helperRoot = new Group();
+ // helperRoot.renderOrder = 10000;
+ // scene.add(helperRoot);
+ // debug.pnt = new ShapePointsMesh();
+ // debug.ln = new DynLineMesh();
+ // scene.add(debug.pnt);
+ // scene.add(debug.ln);
+
+ // loader.register( parser => new VRMLoaderPlugin( parser, { helperRoot } ) );
+ loader.register( parser => new VRMLoaderPlugin( parser ) );
+
+ loader.load(playerURL, (gltf) => {
+ currentPlayerAvatarRef.current = gltf;
+ playerControllerRef.current = gltf.userData.vrm;
+
+ // Calculate the avatar's height offset
+ const headBone = gltf.userData.vrm.humanoid.getNormalizedBoneNode(VRMHumanBoneName.Head);
+ headBone.layers.set(HEAD_LAYER);
+ headBone.visible = false;
+ // traverse the head bone to hide the mesh
+ headBone.traverse((child) => {
+ if(child.isMesh){
+ child.visible = false;
+ }
});
+ const headWorldPosition = new Vector3();
+ headBone.getWorldPosition(headWorldPosition);
+
+ const avatarWorldPosition = new Vector3();
+ gltf.scene.getWorldPosition(avatarWorldPosition);
+
+ props.avatarHeightOffset.current = headWorldPosition.y - avatarWorldPosition.y;
+
+ setIsModelLoaded(true);
+ }, undefined, error => {
+ console.error('An error happened during the loading of the model:', error);
});
+ }
+}, [playerURL, gl]);
- if (someSceneState?.userData?.gltfExtensions?.VRM) {
- const playerController = someSceneState.userData.vrm;
- // Check if the avatar is reachable with a 200 response code.
- // Check if the avatar is reachable with a 200 response code.
- const fetchProfile = async () => {
- try {
- const response = await fetch(userData.profileImage);
- if (response.status === 200) {
- const loadedProfile = useLoader(TextureLoader, userData.profileImage);
-
- playerController.scene.traverse((obj) => {
- obj.frustumCulled = false;
-
- if (obj.name === "profile") {
- const newMat = obj.material.clone();
- newMat.map = loadedProfile;
- obj.material = newMat;
- obj.material.map.needsUpdate = true;
- }
- });
+useEffect(() => {
+ if (isModelLoaded && playerControllerRef.current) {
+
+ const avatarOptions = {
+ fingers: true,
+ hair: true,
+ decapitate: false,
+ visemes: true,
+ microphoneMediaStream: null,
+ muted: true,
+ debug: false,
+ };
+
+ const playerController = playerControllerRef.current;
+ playerControllerRef.current.avatar = new ExokitAvatar(playerControllerRef.current, avatarOptions);
+ const animationsMixer = new AnimationMixer(playerController.scene);
+ playerMixerRef.current = animationsMixer;
+ let animationsPromises = animationFiles.map(file => loadMixamoAnimation(file, playerController));
+ playerController.scene.visible = false;
+
+ Promise.all(animationsPromises)
+ .then(animations => {
+ const idleAction = animationsMixer.clipAction(animations[0]);
+ const walkingAction = animationsMixer.clipAction(animations[1]);
+ const runningAction = animationsMixer.clipAction(animations[2]);
+ const jumpingAction = animationsMixer.clipAction(animations[3]);
+ idleAction.timeScale = 1;
+ walkingAction.timeScale = 0;
+ runningAction.timeScale = 0;
+ jumpingAction.timeScale = 0;
+ animationsRef.current = { idle: idleAction, walking: walkingAction, running: runningAction, jump: jumpingAction };
+ idleAction.play();
+ playerController.scene.visible = true;
+ });
+ }
+}, [isModelLoaded]);
+
+useEffect(() => {
+ addHandRotationControls();
+ }, []);
+
+useEffect(() => {
+ if( isPresenting ){
+ console.log( 'Presenting' );
+ // kill all animations
+ // if (playerMixerRef.current) {
+ // playerMixerRef.current.stopAllAction();
+ // }
+ // stop idle
+
+ if (animationsRef.current) {
+ const { idle, walking, running, jump, falling } = animationsRef.current;
+ if(idle){
+ idle.stop();
}
- return response;
- } catch (err) {
- // Handle the error properly or rethrow it to be caught elsewhere.
- // console.error("Error fetching profile:", err);
- // throw err;
+ if(walking){
+ walking.stop();
}
- };
+ if(running){
+ running.stop();
+ }
+ if(jump){
+ jump.stop();
+ }
+ }
+ }
+}, [isPresenting]);
+
+let lastUpdateTime = 0;
+let blinkTimer = 0;
+let blinkInterval = getRandomBlinkInterval();
+
+function getRandomBlinkInterval() {
+ return 5 + Math.random() * 10;
+}
+
+function handleBlinking(delta) {
+ blinkTimer += delta;
+ if (blinkTimer > blinkInterval && playerControllerRef.current) {
+ performBlink(playerControllerRef.current);
+ blinkTimer = 0;
+ blinkInterval = getRandomBlinkInterval();
+ }
+}
+
+function performBlink(vrm) {
+ const blinkDuration = 0.05 + Math.random() * 0.1;
+ const steps = Math.round(blinkDuration / 0.01);
+
+ for (let i = 0; i <= steps; i++) {
+ const s = i / steps;
+ setTimeout(() => {
+ vrm.expressionManager.setValue('blinkLeft', s);
+ vrm.expressionManager.setValue('blinkRight', s);
+ }, s * blinkDuration * 1000);
+ }
+
+ setTimeout(() => {
+ for (let i = 0; i <= steps; i++) {
+ const s = 1 - i / steps;
setTimeout(() => {
- fetchProfile()
- .then((response) => {
- // handle the response here if needed
- })
- .catch((err) => {
- // Handle the error here if needed
- });
- }, 1000);
- // VRMUtils.rotateVRM0(playerController);
- const currentVrm = playerController;
- const currentMixer = new AnimationMixer(currentVrm.scene);
+ vrm.expressionManager.setValue('blinkLeft', s);
+ vrm.expressionManager.setValue('blinkRight', s);
+ }, (1 - s) * blinkDuration * 1000);
+ }
+ }, blinkDuration * 1000 + 200);
+}
-
- // need to dynamically do this on scroll
- // playerController.firstPerson.humanoid.humanBones.head.node.scale.set([
- // 0, 0, 0
- // ]);
-
- // const movement = useKeyboardControls();
- const velocity = useRef(spawnPoint); // Use a ref instead of state for velocity
- let lastUpdateTime = 0;
- let blinkTimer = 0; // Initialize the blinkTimer outside the useFrame loop.
- let blinkInterval = 5 + Math.random() * 10; // Blink roughly every 2 to 6 seconds
-
- // frame loop
- useFrame((state, delta) => {
- let isMoving = false;
- const currentTime = state.clock.elapsedTime;
- const timeSinceLastUpdate = currentTime - lastUpdateTime;
- let rigidBodyPosition = [0, 0, 0]
- if(rigidRef.current?.translation()){
- rigidBodyPosition = rigidRef.current.translation();
- }
- const forward = new Vector3();
- camera.getWorldDirection(forward);
- forward.negate(); // In Three.js camera looks towards negative Z, so we negate the vector
- forward.normalize();
- const right = new Vector3();
- right.crossVectors(camera.up, forward);
- right.normalize();
- // initialize the moving state to false
- const raycaster = state.raycaster;
- if (timeSinceLastUpdate >= 0.1) {
- lastUpdateTime = currentTime;
+const movementTimeoutRef = useRef(null);
+const updateRate = 1000 / 5;
+const lastNetworkUpdateTimeRef = useRef(0);
+let countHangtime = 0;
+let isMoving;
+let lastKeyPressTime = 0;
+let wasJumping = false;
+
+useEffect(() => {
+ isMoving = false;
+}, []);
+let isJumping = false;
+const getJoystickValues = useJoystickControls(
+ (state) => state.getJoystickValues
+);
+const playerForward = new Vector3(0, 0, 1);
+
+ useFrame((state, delta) => {
+ const joystickValues = getJoystickValues();
+ let forward = props.movement.current.forward;
+ let backward = props.movement.current.backward;
+ let left = props.movement.current.left;
+ let right = props.movement.current.right;
+ let shift = props.movement.current.shift;
+ let space = props.movement.current.space;
+
+ if (joystickValues) {
+ if (joystickValues.joystickAng > 0) {
+ if (joystickValues.joystickDis > 60) {
+ shift = true;
}
- if (currentVrm) {
- currentVrm.update(delta);
+ forward = true;
+ }
+ if (joystickValues.button1Pressed === true) {
+ space = true;
+ }
+ }
+
+ if (playerControllerRef.current) {
+ playerControllerRef.current.update(delta);
+ }
+
+ if (playerMixerRef.current) {
+ playerMixerRef.current.update(delta);
+ }
+
+ const now = state.clock.elapsedTime * 1000;
+
+ if (backward || forward || left || right) {
+ if (characterRef.current.userData.canJump) {
+ isMoving = true;
+
+ if (now - lastKeyPressTime > 100) {
+ if (window.p2pcf) {
+ const participantObject = scene.getObjectByName("playerOne");
+
+ var target = new Vector3();
+ var worldPosition = participantObject.getWorldPosition(target);
+ const position = [
+ worldPosition.x,
+ worldPosition.y,
+ worldPosition.z
+ ];
+
+ const rotation = [
+ participantObject.parent.parent.rotation.x,
+ participantObject.parent.parent.rotation.y,
+ participantObject.parent.parent.rotation.z
+ ];
+
+ const currentAction = !characterRef.current.userData.canJump ? "jumping" : "walking";
+ const messageObject = {
+ [window.p2pcf.clientId]: {
+ position: position,
+ rotation: rotation,
+ profileImage: userData.profileImage,
+ playerVRM: userData.playerVRM,
+ vrm: userData.vrm,
+ inWorldName: window.userData.inWorldName ? window.userData.inWorldName : userData.inWorldName,
+ isMoving: {
+ action: currentAction,
+ instance: 'update',
+ hangtime: countHangtime
+ }
}
- if (currentMixer) {
- currentMixer.update(delta);
+ };
+
+ if (shift && characterRef.current.userData.canJump) {
+ messageObject[window.p2pcf.clientId].isMoving.action = "running";
}
- blinkTimer += delta; // Increment timer
-
- //blink
- if (blinkTimer > blinkInterval) {
- if (currentVrm) {
- // Randomize the duration of the blink between 0.05 and 0.15 seconds
- const blinkDuration = 0.05 + Math.random() * 0.1;
- const steps = Math.round(blinkDuration / 0.01); // We want each step to be roughly 0.01 seconds
-
- // Close both eyes over the course of the blink duration
- for(let i = 0; i <= steps; i++) {
- const s = i / steps;
- setTimeout(() => {
- currentVrm.expressionManager.setValue('blinkLeft', s);
- currentVrm.expressionManager.setValue('blinkRight', s);
- }, s * blinkDuration * 1000);
- }
-
- // Open both eyes over the course of the blink duration, after a small delay
- setTimeout(() => {
- for(let i = 0; i <= steps; i++) {
- const s = 1 - i / steps;
- setTimeout(() => {
- currentVrm.expressionManager.setValue('blinkLeft', s);
- currentVrm.expressionManager.setValue('blinkRight', s);
- }, (1 - s) * blinkDuration * 1000);
- }
- }, blinkDuration * 1000 + 200); // Add a small delay before opening the eyes
-
- blinkTimer = 0; // Reset the timer
- blinkInterval = 5 + Math.random() * 10; // Blink roughly every 10 to 25 seconds
+
+ const message = JSON.stringify(messageObject);
+ window.p2pcf.broadcast(new TextEncoder().encode(message)), window.p2pcf;
+ lastKeyPressTime = now;
+ lastNetworkUpdateTimeRef.current = now;
+ }
+ }
+
+ if (now - lastNetworkUpdateTimeRef.current > updateRate) {
+ if (window.p2pcf) {
+ const participantObject = scene.getObjectByName("playerOne");
+
+ var target = new Vector3();
+ var worldPosition = participantObject.getWorldPosition(target);
+ const position = [
+ worldPosition.x,
+ worldPosition.y,
+ worldPosition.z
+ ];
+
+ const rotation = [
+ participantObject.parent.parent.rotation.x,
+ participantObject.parent.parent.rotation.y,
+ participantObject.parent.parent.rotation.z
+ ];
+
+ const currentAction = !characterRef.current.userData.canJump ? "jumping" : "walking";
+
+ const messageObject = {
+ [window.p2pcf.clientId]: {
+ position: position,
+ rotation: rotation,
+ profileImage: userData.profileImage,
+ playerVRM: userData.playerVRM,
+ vrm: userData.vrm,
+ inWorldName: window.userData.inWorldName ? window.userData.inWorldName : userData.inWorldName,
+ isMoving: {
+ action: currentAction,
+ instance: 'update',
+ hangtime: countHangtime
}
}
- let speedPerSecondFB = 3.6; // This is equivalent to 0.06 per frame at 60 FPS
- let speedPerSecondLR = 1.8; // This is equivalent to 0.03 per frame at 60 FPS
-
- if (props.movement.current.shift) {
- speedPerSecondFB = 7.2; // This is equivalent to 0.12 per frame at 60 FPS
- speedPerSecondLR = 4.2; // This is equivalent to 0.07 per frame at 60 FPS
+ };
+
+ if (shift && characterRef.current.userData.canJump) {
+ messageObject[window.p2pcf.clientId].isMoving.action = "running";
}
- let newVelocity = [...velocity.current];
- let newPosition = null;
-
- if (props.movement.current.backward && canMoveRef.current) {
- let speed = speedPerSecondFB * delta;
- newVelocity[0] += speed * forward.x;
- newVelocity[2] += speed * forward.z;
- isMoving = true;
- } else if (props.movement.current.forward && canMoveRef.current) {
- let speed = speedPerSecondFB * delta;
- newVelocity[0] -= speed * forward.x;
- newVelocity[2] -= speed * forward.z;
- isMoving = true;
- } else if (props.movement.current.left && canMoveRef.current) {
- let speed = speedPerSecondLR * delta;
- newVelocity[0] -= speed * right.x;
- newVelocity[2] -= speed * right.z;
- isMoving = true;
- } else if (props.movement.current.right && canMoveRef.current) {
- let speed = speedPerSecondLR * delta;
- newVelocity[0] += speed * right.x;
- newVelocity[2] += speed * right.z;
- isMoving = true;
+ const message = JSON.stringify(messageObject);
+ window.p2pcf.broadcast(new TextEncoder().encode(message)), window.p2pcf;
+ lastNetworkUpdateTimeRef.current = now;
+ }
+ }
+
+ clearTimeout(movementTimeoutRef.current);
+ movementTimeoutRef.current = setTimeout(() => {
+ isMoving = false;
+ }, 500);
+ }
+ } else {
+ if (isMoving) {
+ isMoving = false;
+ clearTimeout(movementTimeoutRef.current);
+ if (window.p2pcf?.clientId) {
+ const participantObject = scene.getObjectByName("playerOne");
+ var target = new Vector3();
+ var worldPosition = participantObject.getWorldPosition(target);
+ const position = [
+ worldPosition.x,
+ worldPosition.y,
+ worldPosition.z
+ ];
+
+ const messageStopObject = {
+ [window.p2pcf.clientId]: {
+ isMoving: false,
+ position: position
+ }
+ };
+ const messageStop = JSON.stringify(messageStopObject);
+ window.p2pcf.broadcast(new TextEncoder().encode(messageStop));
+ lastNetworkUpdateTimeRef.current = now;
+ }
+ }
+ }
+
+ if (isPresenting && !presentingState) {
+ const newCamera = new PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
+ const participantObject = scene.getObjectByName("playerOne");
+ // set the rotation to 0
+ participantObject.parent.parent.rotation.set(0, 0, 0);
+ participantObject.rotation.set(0, 0, 0);
+ const xrCamera = gl.xr.getCamera(newCamera);
+ gl.xr.enabled = true;
+ state.camera = xrCamera;
+ setPresentingState(true);
+ } else if (!isPresenting && presentingState) {
+ setPresentingState(false);
+ }
+
+ handleBlinking(delta);
+
+ if (animationsRef.current) {
+ if (playerControllerRef.current && participantObject) {
+ const cameraWorldQuaternion = new Quaternion();
+ camera.getWorldQuaternion(cameraWorldQuaternion);
+ const cameraForward = new Vector3(0, 0, -1).applyQuaternion(cameraWorldQuaternion);
+
+ const characterWorldQuaternion = new Quaternion();
+ participantObject.parent.getWorldQuaternion(characterWorldQuaternion);
+ const characterForward = new Vector3(0, 0, 1).applyQuaternion(characterWorldQuaternion);
+ const neutralRotation = new Euler(0, 0, 0);
+
+ const characterToCamera = new Vector3().subVectors(camera.position, participantObject.getWorldPosition(new Vector3())).normalize();
+
+ const dotProduct = characterForward.dot(cameraForward);
+ const azimuthalAngle = Math.acos(Math.min(Math.max(dotProduct, -1), 1));
+
+ const angleThreshold = Math.PI / 2;
+ if (azimuthalAngle < angleThreshold) {
+ if (avatarIsSprite) {
+ if (isMoving && frameName !== 'WalkForward') {
+ setFrameName('WalkForward');
}
- if(props.movement.current.respawn === true){
- newPosition = spawnPoint;
- newVelocity = spawnPoint;
- velocity.current = spawnPoint;
+ if (isMoving === false) {
+ if (frameName !== 'ForwardIdle') {
+ setFrameName('ForwardIdle');
}
-
- // if shift is pressed, run by setting speed to 0.1
- if(canMoveRef.current){
- velocity.current = newVelocity;
}
+ }
+ } else {
+ if (avatarIsSprite && isMoving && frameName !== 'WalkBackward') {
+ setFrameName('WalkBackward');
+ }
+ if (avatarIsSprite && isMoving === false && frameName !== 'BackwardIdle') {
+ setFrameName('BackwardIdle');
+ }
+ }
+ }
- const rotationSpeed = 0.5;
- if (props.movement.current.backward) {
- orbitRef.current.minPolarAngle = Math.PI / 1.8;
- orbitRef.current.maxPolarAngle = Math.PI / 1.25;
- orbitRef.current.maxDistance = 2;
- orbitRef.current.minDistance = 2;
- } else {
- // Reset properties to default values
- if(isMoving){
- orbitRef.current.minPolarAngle = Math.PI / 1.5;
- } else {
- orbitRef.current.minPolarAngle = Math.PI / 1.8;
+ const { idle, walking, running, jump, falling } = animationsRef.current;
+
+ if (props.movement.current.respawn) {
+ characterRef.current.setBodyType(rapier.RigidBodyType.Fixed, 1);
+ characterRef.current.setTranslation(new Vector3(Number(spawnPoint[0]), Number(spawnPoint[1]), Number(spawnPoint[2])), true);
+ } else if (!props.movement.current.respawn && characterRef.current.bodyType() === 1) {
+ characterRef.current.setBodyType(rapier.RigidBodyType.Dynamic, 0);
+ }
+
+ if (isMoving && characterRef.current.userData.canJump) {
+ jump.clampWhenFinished = false;
+ jump.reset();
+ jump.setEffectiveTimeScale(0);
+ jump.setEffectiveWeight(0);
+ } else if (!isMoving && characterRef.current.userData.canJump) {
+ jump.clampWhenFinished = false;
+ jump.reset();
+ jump.setEffectiveTimeScale(0);
+ jump.setEffectiveWeight(0);
+ idle.setEffectiveTimeScale(1);
+ idle.setEffectiveWeight(1);
+ }
+
+ if (!characterRef.current.userData.canJump) {
+ if (window.p2pcf) {
+ const participantObject = scene.getObjectByName("playerOne");
+
+ var target = new Vector3();
+ var worldPosition = participantObject.getWorldPosition(target);
+ const position = [
+ worldPosition.x,
+ worldPosition.y,
+ worldPosition.z
+ ];
+
+ const rotation = [
+ participantObject.parent.parent.rotation.x,
+ participantObject.parent.parent.rotation.y,
+ participantObject.parent.parent.rotation.z
+ ];
+ if (!prevPositionRef.current || Math.abs(position[1] - prevPositionRef.current[1]) > 0.01) {
+ const messageObject = {
+ [window.p2pcf.clientId]: {
+ position: position,
+ rotation: rotation,
+ profileImage: userData.profileImage,
+ playerVRM: userData.playerVRM,
+ vrm: userData.vrm,
+ inWorldName: window.userData.inWorldName ? window.userData.inWorldName : userData.inWorldName,
+ isMoving: {
+ action: "jumping",
+ instance: 'first',
+ hangtime: countHangtime
+ }
}
- orbitRef.current.maxPolarAngle = Math.PI / 1.2;
- orbitRef.current.maxDistance = 2;
- orbitRef.current.minDistance = 1.3;
+ };
+ const message = JSON.stringify(messageObject);
+ if ((now - lastNetworkUpdateTimeRef.current > updateRate) && (lastNetworkUpdateTimeRef.current !== 0)) {
+ window.p2pcf.broadcast(new TextEncoder().encode(message)), window.p2pcf;
+ lastNetworkUpdateTimeRef.current = now;
}
-
- // send a raycast from the orbit camera and check if there is an obstacle in the way
-
- // We compute the direction from the camera to the player
- let direction = camera.getWorldDirection(new Vector3());
- // normalize the direction to not be looking up or downward
- direction.normalize();
- direction.y = 0;
-
- // Adjust the direction based on the movement direction
- if(props.movement.current.backward) {
- direction.negate(); // for backward movement, we want to reverse the direction
- } else if (props.movement.current.right && !props.movement.current.left && !props.movement.current.forward && !props.movement.current.backward) {
- direction.applyAxisAngle(new Vector3(0, 1, 0), -Math.PI / 2); // for right movement, rotate the direction 90 degrees counterclockwise
- } else if (props.movement.current.left && !props.movement.current.right && !props.movement.current.forward && !props.movement.current.backward) {
- direction.applyAxisAngle(new Vector3(0, 1, 0), Math.PI / 2); // for left movement, rotate the direction 90 degrees clockwise
}
+ prevPositionRef.current = position;
+ }
- // Define the desired rotation matrix
- let matrix = new Matrix4();
- matrix.lookAt(new Vector3(0,0,0), direction, new Vector3(0,1,0));
-
- // Create a quaternion from the rotation matrix
- let desiredQuaternion = new Quaternion();
- desiredQuaternion.setFromRotationMatrix(matrix);
-
- if ( props.movement.current.forward === true ||
- props.movement.current.backward === true ||
- props.movement.current.left === true ||
- props.movement.current.right === true
- ) {
- // Apply slerp to the player's current quaternion, gradually aligning it with the desired quaternion
- playerController.scene.quaternion.slerp(desiredQuaternion, rotationSpeed);
- }
- if(castRef.current){
- castRef.current.setRotation(desiredQuaternion);
+ countHangtime++;
+
+ if (jump.getEffectiveTimeScale() === 0) {
+ if (countHangtime > 3) {
+ jump.setEffectiveTimeScale(1);
+ jump.setEffectiveWeight(1);
+ jump.clampWhenFinished = true;
+ jump.time = jump._clip.duration;
+ jump.play();
+ running.setEffectiveTimeScale(0);
+ running.setEffectiveWeight(0);
+ walking.setEffectiveTimeScale(0);
+ walking.setEffectiveWeight(0);
}
+ }
- if (isMoving && canMoveRef.current) {
- newPosition = [
- velocity.current[0],
- rigidBodyPosition.y,
- velocity.current[2]
+ wasJumping = true;
+ } else {
+ if (wasJumping) {
+ if (window.p2pcf) {
+ const participantObject = scene.getObjectByName("playerOne");
+ setTimeout(() => {
+ var target = new Vector3();
+ var worldPosition = participantObject.getWorldPosition(target);
+ const position = [
+ worldPosition.x,
+ worldPosition.y,
+ worldPosition.z
];
- participantObject.parent.position.set(...newPosition);
- castRef.current.setTranslation({x: newPosition[0], y: newPosition[1], z: newPosition[2]});
- }
- // animation logic
- if (animationsRef.current) {
- const { idle, walking, running } = animationsRef.current;
-
- if (isMoving) {
- // If moving, but idle animation is playing, stop it and play walking animation
- // if (idle.isRunning()) {
- // blend from idle to walking
- if(props.movement.current.shift) {
- if (walking.isRunning()) {
- walking.crossFadeTo(running, 1);
- } else {
- idle.crossFadeTo(running, 1);
- }
- running.enabled = true;
- running.setEffectiveTimeScale(1);
- running.setEffectiveWeight(1);
- idle.enabled = true;
- idle.setEffectiveTimeScale(1);
- idle.setEffectiveWeight(0);
- walking.enabled = true;
- walking.setEffectiveTimeScale(1);
- walking.setEffectiveWeight(0);
- running.play();
- } else {
- if (running.isRunning()) {
- running.crossFadeTo(walking, 1);
- } else {
- idle.crossFadeTo(walking, 1);
- }
- walking.enabled = true;
- walking.setEffectiveTimeScale(1);
- walking.setEffectiveWeight(1);
- running.enabled = true;
- running.setEffectiveTimeScale(1);
- running.setEffectiveWeight(0);
- idle.enabled = true;
- idle.setEffectiveTimeScale(1);
- idle.setEffectiveWeight(0);
- walking.play();
- }
- // }
- } else {
- // If not moving, but walking animation is playing, stop it and play idle animation
- if (walking.isRunning()) {
- // blend from walking to idle
- walking.crossFadeTo(idle, 1);
- // set the walking animation to lower weight so it blends into the idle animation
- walking.enabled = true;
- walking.setEffectiveTimeScale(1);
- walking.setEffectiveWeight(0);
- running.setEffectiveTimeScale(1);
- running.setEffectiveWeight(0);
- idle.enabled = true;
- idle.setEffectiveTimeScale(1);
- idle.setEffectiveWeight(1);
- idle.play();
- } else if (running.isRunning()) {
- // blend from running to idle
- running.crossFadeTo(idle, 1);
- // set the running animation to lower weight so it blends into the idle animation
- running.enabled = true;
- running.setEffectiveTimeScale(1);
- running.setEffectiveWeight(0);
- walking.setEffectiveTimeScale(1);
- walking.setEffectiveWeight(0);
- idle.enabled = true;
- idle.setEffectiveTimeScale(1);
- idle.setEffectiveWeight(1);
- idle.play();
+
+ const rotation = [
+ participantObject.parent.parent.rotation.x,
+ participantObject.parent.parent.rotation.y,
+ participantObject.parent.parent.rotation.z
+ ];
+
+ if ((countHangtime > 0) && lastNetworkUpdateTimeRef.current !== 0) {
+ const messageStopObject = {
+ [window.p2pcf.clientId]: {
+ isMoving: {
+ action: "jumpStop",
+ hangtime: countHangtime
+ },
+ position: position,
+ rotation: rotation
}
+ };
+ const messageStop = JSON.stringify(messageStopObject);
+ window.p2pcf.broadcast(new TextEncoder().encode(messageStop));
+ countHangtime = 0;
+ lastNetworkUpdateTimeRef.current = now;
}
+ }, 100);
}
- if (participantObject) {
- camera.lookAt(
- participantObject.parent.position.x,
- playerController.firstPerson.humanoid.humanBones.head.node.getWorldPosition(new Vector3()).y,
- participantObject.parent.position.z
- );
-
- if (orbitRef.current){
- let newTarget = new Vector3(
- participantObject.parent.position.x,
- playerController.firstPerson.humanoid.humanBones.head.node.getWorldPosition(new Vector3()).y + 1.5,
- participantObject.parent.position.z
- );
- // lerpVectors() orbitRef from current position target to newTarget
- orbitRef.current.target.lerpVectors(orbitRef.current.target, newTarget, 0.5);
- }
- }
-
- // update rigidBody's position
- if (rigidRef.current && participantObject?.parent?.position?.x) {
- // // match the rigidBody's position to the participantObject's position.
- // set the rigidbody type to one that can be moved by setTranslation
- if(props.movement.current.backward || props.movement.current.forward || props.movement.current.left || props.movement.current.right || falling.current === true) {
- rigidRef.current.setBodyType(rapier.RigidBodyType.Dynamic, 1);
- // rigidRef.current.setFriction(1); // Set the friction to 1 so the player doesn't slide
- } else {
- rigidRef.current.setBodyType(rapier.RigidBodyType.Fixed, 1);
- }
+ wasJumping = false;
+ }
+ }
- // set a const of the rigidBody's current position
- rigidRef.current.setTranslation({ x: participantObject.parent.position.x, y: rigidBodyPosition.y, z: participantObject.parent.position.z});
+ if (isMoving && characterRef.current.userData.canJump) {
+ countHangtime = 0;
+
+ if (shift) {
+ if (walking.isRunning()) {
+ walking.crossFadeTo(running, 1.1);
+ } else {
+ idle.crossFadeTo(running, 1.1);
}
- if(props.movement.current.respawn === true){
-
- const x = Number(props.spawnPoint[0]);
- const y = Number(props.spawnPoint[1]);
- const z = Number(props.spawnPoint[2]);
- if (props.spawnPointsToAdd) {
- let finalPoints = [];
- props.spawnPointsToAdd.forEach((point) => {
- finalPoints.push([Number(point.position.x), Number(point.position.y), Number(point.position.z)]);
- });
- finalPoints.push([x, y, z]);
- //pick a random point
- let randomPoint = finalPoints[Math.floor(Math.random() * finalPoints.length)];
- if([x, y, z] !== [camera.position.x, camera.position.y, camera.position.z]){
- // Check if the converted values are valid and finite
- // Set the camera's position
- // orbitRef.position.set(randomPoint[0], randomPoint[1], randomPoint[2]);
- castRef.current.setTranslation({
- x: randomPoint[0],
- y: randomPoint[1],
- z: randomPoint[2]
- });
- participantObject.parent.position.set(randomPoint[0], randomPoint[1], randomPoint[2]);
- // move the rigidRef to the new position
- rigidRef.current.setTranslation({
- x: randomPoint[0],
- y: randomPoint[1],
- z: randomPoint[2]
- });
- }
-
- } else {
- // Check if the converted values are valid and finite
- // Set the camera's position
- camera.position.set(x, y, z);
-
- castRef.current.setTranslation({
- x: x,
- y: y,
- z: z
- });
- }
+ running.enabled = true;
+ running.setEffectiveTimeScale(1);
+ running.setEffectiveWeight(1);
+ idle.enabled = true;
+ idle.setEffectiveTimeScale(1);
+ idle.setEffectiveWeight(0);
+ walking.enabled = true;
+ walking.setEffectiveTimeScale(1);
+ walking.setEffectiveWeight(0);
+ running.play();
+ } else {
+ if (running.isRunning()) {
+ running.crossFadeTo(walking, 1);
+ } else {
+ idle.crossFadeTo(walking, 1);
}
- });
+ walking.enabled = true;
+ walking.setEffectiveTimeScale(1);
+ walking.setEffectiveWeight(1);
+ running.enabled = true;
+ running.setEffectiveTimeScale(1);
+ running.setEffectiveWeight(0);
+ idle.enabled = true;
+ idle.setEffectiveTimeScale(1);
+ idle.setEffectiveWeight(0);
+ walking.play();
+ }
+ } else {
+ if (characterRef.current.userData.canJump) {
+ isJumping = false;
+ if (walking.isRunning()) {
+ walking.crossFadeTo(idle, 1);
+ walking.enabled = true;
+ walking.setEffectiveTimeScale(1);
+ walking.setEffectiveWeight(0);
+ running.setEffectiveTimeScale(1);
+ running.setEffectiveWeight(0);
+ idle.enabled = true;
+ idle.setEffectiveTimeScale(1);
+ idle.setEffectiveWeight(1);
+ idle.play();
+ } else if (running.isRunning()) {
+ running.crossFadeTo(idle, 1);
+ running.enabled = true;
+ running.setEffectiveTimeScale(1);
+ running.setEffectiveWeight(0);
+ walking.setEffectiveTimeScale(1);
+ walking.setEffectiveWeight(0);
+ idle.enabled = true;
+ idle.setEffectiveTimeScale(1);
+ idle.setEffectiveWeight(1);
+ idle.play();
+ }
+ }
+ }
+ if (space) {
+ if (characterRef.current.userData.canJump) {
+ isJumping = true;
+ countHangtime = 0;
+ jump.setEffectiveTimeScale(1);
+ jump.setEffectiveWeight(1);
+ idle.setEffectiveTimeScale(0);
+ walking.setEffectiveTimeScale(0);
+ running.setEffectiveTimeScale(0);
+ idle.setEffectiveWeight(0);
+ walking.setEffectiveWeight(0);
+ running.setEffectiveWeight(0);
+ jump.setLoop(LoopOnce, 1);
+ jump.reset();
+ jump.clampWhenFinished = true;
+ jump.play();
+ }
+ }
+ }
+ });
- let animationFiles = [idleFile, walkingFile, runningFile];
- let animationsPromises = animationFiles.map(file => loadMixamoAnimation(file, currentVrm));
-
- Promise.all(animationsPromises)
- .then(animations => {
- const idleAction = currentMixer.clipAction(animations[0]);
- const walkingAction = currentMixer.clipAction(animations[1]);
- const runningAction = currentMixer.clipAction(animations[2]);
- idleAction.timeScale = 1;
- walkingAction.timeScale = 1;
- runningAction.timeScale = 1;
+ // Add this function at the top of your component or in a utility file
+ function debugVector3(name, vector) {
+ console.log(`${name}: x: ${vector.x.toFixed(2)}, y: ${vector.y.toFixed(2)}, z: ${vector.z.toFixed(2)}`);
+ }
+
+
+ function applySimpleIK(spineBone, boneChain, targetPosition, iterations = 10, elbowWeight = 0.8, wristWeight = 0.2, shoulderWeight = 0.2) {
+ const endEffector = boneChain[boneChain.length - 1];
- animationsRef.current = { idle: idleAction, walking: walkingAction, running: runningAction };
- idleAction.play();
- });
+ for (let i = 0; i < iterations; i++) {
+ for (let j = boneChain.length - 2; j >= 0; j--) {
+ const bone = boneChain[j];
+ const nextBone = boneChain[j + 1];
+
+ const toTarget = targetPosition.clone().sub(bone.getWorldPosition(new Vector3()));
+ const toNextBone = nextBone.getWorldPosition(new Vector3()).sub(bone.getWorldPosition(new Vector3()));
+
+ const quaternion = new Quaternion().setFromUnitVectors(toNextBone.normalize(), toTarget.normalize());
+
+ let weight;
+ if (j === 0) {
+ weight = shoulderWeight;
+ } else if (j === 1) {
+ const spineWorldPosition = new Vector3();
+ spineBone.getWorldPosition(spineWorldPosition);
+ const distanceToBody = targetPosition.distanceTo(spineWorldPosition);
+ const elbowBendThreshold = 0.3;
- return (
+ if (distanceToBody < elbowBendThreshold) {
+ const elbowBendAngle = Math.PI / 16;
+ const elbowBendAxis = new Vector3(0, 0, 1);
+ const elbowBendQuaternion = new Quaternion().setFromAxisAngle(elbowBendAxis, elbowBendAngle);
+ quaternion.multiply(elbowBendQuaternion);
+ }
+ weight = elbowWeight;
+ } else {
+ weight = wristWeight;
+ }
+ // limit weight to reasonable values
+ weight = MathUtils.clamp(weight, 0, 1);
+
+ bone.quaternion.slerp(quaternion, weight);
+ }
+ }
+ }
+
+ let frameCounter = 0;
+ const logFrequency = 60; // Log every 60 frames, adjust this value as needed
+ function conditionalLog(message, ...optionalParams) {
+ frameCounter++;
+ if (frameCounter % logFrequency === 0) {
+ console.log(message, ...optionalParams);
+ }
+ }
+ const HAND_VERTICAL_OFFSET = -1.0;
+ const ARM_FORWARD = new Vector3(1, 0, 0);
+ const UP = new Vector3(0, 1, 0);
+
+ const armRef = useRef(null);
+ const rigRef = useRef(null);
+// Add these as component-level variables
+const debugArrows = {
+ leftForward: null,
+ leftUp: null,
+ rightForward: null,
+ rightUp: null
+ };
+
+ useFrame((state, delta) => {
+ if (isPresenting && playerControllerRef.current && playerControllerRef.current.avatar) {
+ const avatar = playerControllerRef.current.avatar;
+ const avatarParent = avatar.model.parent.parent;
+
+ // Update avatar inputs
+ avatar.inputs.hmd.position.copy(camera.position);
+
+ // Update left and right hand positions and rotations
+ const leftHandPosition = leftController.controller.position.clone();
+ const leftHandQuaternion = leftController.controller.quaternion.clone();
+ avatarParent.worldToLocal(leftHandPosition);
+ avatar.inputs.leftGamepad.position.copy(leftHandPosition);
+ avatar.inputs.leftGamepad.quaternion.copy(leftHandQuaternion);
+
+ const rightHandPosition = rightController.controller.position.clone();
+ const rightHandQuaternion = rightController.controller.quaternion.clone();
+ avatarParent.worldToLocal(rightHandPosition);
+ avatar.inputs.rightGamepad.position.copy(rightHandPosition);
+ avatar.inputs.rightGamepad.quaternion.copy(rightHandQuaternion);
+
+ avatar.setFloorHeight(0);
+ avatar.update(delta);
+ }
+ });
+
+ const keyboardMap = [
+ { name: "forward", keys: ["ArrowUp", "KeyW"] },
+ { name: "backward", keys: ["ArrowDown", "KeyS"] },
+ { name: "leftward", keys: ["ArrowLeft", "KeyA"] },
+ { name: "rightward", keys: ["ArrowRight", "KeyD"] },
+ { name: "jump", keys: ["Space"] },
+ { name: "run", keys: ["Shift"] },
+ // Optional animation key map
+ { name: "action1", keys: ["1"] },
+ { name: "action2", keys: ["2"] },
+ { name: "action3", keys: ["3"] },
+ { name: "action4", keys: ["KeyF"] },
+ ];
+
+ const canvas = document.querySelector('div.threeov-main-canvas');
+
+ return (
+ <>
+
+
+ {isModelLoaded && playerControllerRef.current && (
<>
- {playerController && (
- <>
-
- {/* */}
-
-
-
-
- {
- canMoveRef.current = false;
- }}
- onIntersectionExit={({ manifold, target }) => {
- canMoveRef.current = true;
- }}
- angularVelocity={[0, 0, 0]}
- linearVelocity={[0, 0, 0]}
- >
- console.log("enter")}
- position={[0, 1.4, -0.5]}
- args={[0.03, 0.03, 0.03]}
- />
-
- >
- )}
+ {avatarIsSprite && (
+
+ )}
>
- );
- }
+ )}
+
+
+ >
+ );
}
diff --git a/blocks/environment/components/TeleportTravel.js b/blocks/environment/components/TeleportTravel.js
index 6bc8ef9..9d67e92 100644
--- a/blocks/environment/components/TeleportTravel.js
+++ b/blocks/environment/components/TeleportTravel.js
@@ -1,14 +1,18 @@
-import { Raycaster, Vector3 } from "three";
-import { useXR, Interactive } from "@react-three/xr";
+import { Raycaster, Vector3, Mesh, MeshBasicMaterial, BoxGeometry } from "three";
+import { useXR, Interactive, useController, useTeleportation } from "@react-three/xr";
import { useFrame, useThree } from "@react-three/fiber";
import { useCallback, useRef, useState, useEffect } from "react";
import { useRapier, useRigidBody, RigidBody } from "@react-three/rapier";
+import {
+ Text,
+} from "@react-three/drei";
export function TeleportIndicator(props) {
+
return (
<>
-
+
>
@@ -24,6 +28,88 @@ export function ClickIndicatorObject(props) {
>
);
}
+function Button({ onClick, position, color, hoverColor }) {
+ const mesh = React.useRef();
+
+ const [hovered, setHovered] = React.useState(false);
+ const currentColor = hovered ? hoverColor : color;
+
+ return (
+ setHovered(true)}
+ onBlur={() => setHovered(false)}
+ >
+
+
+
+
+
+ );
+}
+
+function Menu() {
+ const menuRef = useRef();
+ const { camera } = useThree();
+ const { player, controllers } = useXR();
+ const [ muted, setMuted ] = useState(false);
+ const handleButtonClick = () => {
+ console.log("Button clicked");
+ if(window.localStream){
+ // loop through all audio tracks and mute them
+ for (let i = 0; i < window.localStream.getAudioTracks().length; i++) {
+ window.localStream.getAudioTracks()[i].enabled = !window.localStream.getAudioTracks()[i].enabled;
+ }
+ // if(muted){
+ // window.localStream.getAudioTracks()[0].enabled = false;
+ // } else {
+ // window.localStream.getAudioTracks()[0].enabled = true;
+ // }
+ // console.log("window.localStream", window.localStream.getAudioTracks()[0]);
+ }
+ setMuted(!muted);
+ };
+
+ // when the player is available, add the menu to the player
+ // useEffect(() => {
+ // console.log("hit the player ref", player)
+ // if (player && menuRef.current) {
+ // //menuRef.current.visible = true;
+ // player.add(menuRef.current);
+ // menuRef.current.position.set(0, 2, -0.8);
+ // }
+ // console.log(window.localStream);
+ // }, [player]);
+
+
+ // useFrame(() => {
+ // if (menuRef.current) {
+
+ // }
+ // });
+
+ return (
+
+
+
+ { muted ? `Mute` : `Unmute`}
+
+ {/* Additional menu items */}
+
+ );
+}
+
export default function TeleportTravel(props) {
const { scene } = useThree();
@@ -38,6 +124,8 @@ export default function TeleportTravel(props) {
const [canInteract, setCanInteract] = useState(false);
const [spawnPos, setSpawnPos] = useState(props.spawnPoint);
const [intersectionPoint, setIntersectionPoint] = useState();
+ const [currentPosition, setCurrentPosition] = useState(new Vector3());
+
const target = useRef();
const targetLoc = useRef();
const ray = useRef(new Raycaster());
@@ -56,9 +144,15 @@ export default function TeleportTravel(props) {
const z = Number(spawnPos[2]);
if (isPresenting) {
- player.position.x = x
- player.position.y = y
- player.position.z = z
+ const participantObject = scene.getObjectByName("playerOne");
+ console.log("participantObject", participantObject);
+ if (participantObject) {
+ player.position.x = participantObject.parent.parent.position.x;
+ player.position.y = participantObject.parent.parent.position.y;
+ player.position.z = participantObject.parent.parent.position.z;
+ } else {
+ player.position.set(x, y, z);
+ }
}
}, [isPresenting])
@@ -68,24 +162,36 @@ export default function TeleportTravel(props) {
// Remove the reticle when the controllers are registered.
const reticle = scene.getObjectByName("reticle");
const participantObject = scene.getObjectByName("playerOne");
- if (controllers.length > 0 && reticle) {
+ if (controllers?.length > 0 && reticle) {
// console.log("participantObject", participantObject);
// set participantObject to invisible
participantObject.visible = false;
reticle.visible = false;
- }
+ }
}, [controllers]);
+ const movementTimeoutRef = useRef(null);
+ const teleport = useTeleportation();
+ let dominantController = useController('right');
+ // const rightController = useController('right')
+
+ const updateRate = 1000 / 5; // 5Hz update rate
+ const lastNetworkUpdateTimeRef = useRef(0);
+
+
+ useFrame((state, delta) => {
+ const now = state.clock.elapsedTime * 1000;
- useFrame(() => {
if (
isHovered &&
controllers.length > 0 &&
ray.current &&
target.current &&
- targetLoc.current
+ targetLoc.current &&
+ dominantController &&
+ dominantController.controller
) {
- controllers[0].controller.getWorldDirection(rayDir.current.dir);
- controllers[0].controller.getWorldPosition(rayDir.current.pos);
+ dominantController.controller.getWorldDirection(rayDir.current.dir);
+ dominantController.controller.getWorldPosition(rayDir.current.pos);
// ray.far = 0.05;
// ray.near = 0.01;
rayDir.current.dir.multiplyScalar(-1);
@@ -109,6 +215,7 @@ export default function TeleportTravel(props) {
}
});
if (containsInteractiveObject) {
+ // console.log("set teleport false in contains interactive object");
setCanInteract(true);
setCanTeleport(false);
} else {
@@ -128,67 +235,118 @@ export default function TeleportTravel(props) {
// new Vector3(1, 0, 0),
// Math.PI / 2
// );
- targetLoc.current.position.copy(p);
+ targetLoc.current.position.copy(p);
} else {
targetLoc.current.position.copy(intersection.point);
}
+ setCurrentPosition(intersection.point);
+ }
+ if (now - lastNetworkUpdateTimeRef.current > updateRate) {
+ const p2pcf = window.p2pcf;
+ if (p2pcf) {
+ const rotation = [
+ player.rotation.x,
+ player.rotation.y,
+ player.rotation.z,
+ ];
+ const messageObject = {
+ [p2pcf.clientId]: {
+ rotation: rotation,
+ profileImage: userData.profileImage,
+ vrm: userData.vrm,
+ inWorldName: userData.inWorldName,
+ },
+ };
+ const message = JSON.stringify(messageObject);
+ p2pcf.broadcast(new TextEncoder().encode(message)), p2pcf;
+ lastNetworkUpdateTimeRef.current = now;
+ }
}
}
});
const click = useCallback(() => {
+ console.log("clicking", player);
if (isHovered && !canInteract) {
- targetLoc.current.position.set(
- targetLoc.current.position.x,
- targetLoc.current.position.y + 0.1,
- targetLoc.current.position.z
- );
- if (canTeleport) {
- player.position.copy(targetLoc.current.position);
+ targetLoc.current.position.set(
+ targetLoc.current.position.x,
+ targetLoc.current.position.y + (props.avatarHeightOffset.current? (props.avatarHeightOffset.current -0.8) : 0.4),
+ targetLoc.current.position.z
+ );
+ if (canTeleport) {
+ console.log("teleporting to", targetLoc.current.position);
+ player.position.copy(targetLoc.current.position);
+ const p2pcf = window.p2pcf;
+ const participantObject = scene.getObjectByName("playerOne");
+ if (participantObject) {
+ console.log("participantObject", participantObject);
+ if (p2pcf) {
+ var target = new Vector3();
+ var worldPosition = participantObject.getWorldPosition(target);
+ const position = [
+ targetLoc.current.position.x,
+ targetLoc.current.position.y,
+ targetLoc.current.position.z,
+ ];
+ const rotation = [
+ player.rotation.x,
+ player.rotation.y,
+ player.rotation.z,
+ ];
+ const messageObject = {
+ [p2pcf.clientId]: {
+ position: position,
+ rotation: rotation,
+ profileImage: userData.profileImage,
+ vrm: userData.vrm,
+ inWorldName: userData.inWorldName,
+ isMoving: "walking",
+ },
+ };
+ console.log("sending message", messageObject);
+ clearTimeout(movementTimeoutRef.current);
+ movementTimeoutRef.current = setTimeout(() => {
+ const messageStopObject = {
+ [p2pcf.clientId]: {
+ isMoving: false,
+ },
+ };
+ const messageStop = JSON.stringify(messageStopObject);
+ p2pcf.broadcast(new TextEncoder().encode(messageStop));
+ }, 100);
+ const message = JSON.stringify(messageObject);
+ p2pcf.broadcast(new TextEncoder().encode(message)), p2pcf;
+ }
}
+ }
}
if (isHovered && canInteract) {
- if (controllers.length > 0) {
- const rigidBodyDesc = new rapier.RigidBodyDesc(
- rapier.RigidBodyType.Static
- )
- // The rigid body translation.
- // Default: zero vector.
- .setTranslation(
- targetLoc.current.position.x,
- targetLoc.current.position.y,
- targetLoc.current.position.z - 0.008
- )
- .setLinvel(0, 0, 0)
- // The linear velocity of this body.
- // .setLinvel(targetLoc.current.position.x, targetLoc.current.position.y - 1.1, targetLoc.current.position.z)
- // Default: zero vector.
- .setGravityScale(1)
- // Default: zero velocity.
- .setCanSleep(false)
- // Whether or not CCD is enabled for this rigid-body.
- // Default: false
- .setCcdEnabled(true);
- const rigidBody = world.createRigidBody(rigidBodyDesc);
-
- const collider = world.createCollider(
- rapier.ColliderDesc.cuboid(0.05, 0.05, 0.05),
- rigidBody
- // rapier.ColliderDesc.capsule(0.5, 0.5), rigidBody
- );
-
- collider.setFriction(0.1);
- collider.setRestitution(0);
- // collider.setSensor(true);
- // collider.setTranslation(intersects[0].point);
- setTimeout(() => {
- world.removeCollider(collider);
- world.removeRigidBody(rigidBody);
- }, 200);
- }
+ if (controllers.length > 0) {
+ const rigidBodyDesc = new rapier.RigidBodyDesc(rapier.RigidBodyType.Static)
+ .setTranslation(
+ targetLoc.current.position.x,
+ targetLoc.current.position.y,
+ targetLoc.current.position.z - 0.008
+ )
+ .setLinvel(0, 0, 0)
+ .setGravityScale(1)
+ .setCanSleep(false)
+ .setCcdEnabled(true);
+ const rigidBody = world.createRigidBody(rigidBodyDesc);
+ const collider = world.createCollider(
+ rapier.ColliderDesc.cuboid(0.05, 0.05, 0.05),
+ rigidBody
+ );
+ collider.setFriction(0.1);
+ collider.setRestitution(0);
+ setTimeout(() => {
+ world.removeCollider(collider);
+ world.removeRigidBody(rigidBody);
+ }, 200);
+ }
}
- }, [isHovered, canTeleport, canInteract]);
-
+ }, [isHovered, canTeleport, canInteract]);
+
return (
<>
{isHovered && canTeleport && (
@@ -201,8 +359,12 @@ export default function TeleportTravel(props) {
)}
+
{
+
+ click();
+ }}
onHover={(e) => {
setIsHovered(true);
}}
diff --git a/blocks/environment/components/ThreeObjectEdit.js b/blocks/environment/components/ThreeObjectEdit.js
index 86f9658..391f345 100644
--- a/blocks/environment/components/ThreeObjectEdit.js
+++ b/blocks/environment/components/ThreeObjectEdit.js
@@ -1,6 +1,19 @@
-import * as THREE from "three";
+import {
+ VideoTexture,
+ Vector3,
+ TextureLoader,
+ Euler,
+ Color,
+ MeshBasicMaterial,
+ DoubleSide,
+ sRGBEncoding,
+ AudioListener,
+ BoxGeometry,
+ Mesh
+ } from "three";
+
import React, { Suspense, useRef, useState, useEffect, useMemo } from "react";
-import { Canvas, useLoader, useFrame, useThree } from "@react-three/fiber";
+import { Canvas, useLoader, useFrame, useThree, extend } from "@react-three/fiber";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";
import {
@@ -21,13 +34,16 @@ import { VRMUtils, VRMLoaderPlugin } from "@pixiv/three-vrm";
import { GLTFAudioEmitterExtension } from "three-omi";
import { Icon, moveTo, rotateLeft, resizeCornerNE } from "@wordpress/icons";
// import { A11y } from "@react-three/a11y";
-import { Perf } from "r3f-perf";
+// import { Perf } from "r3f-perf";
// import EditControls from "./EditControls";
import { Resizable } from "re-resizable";
import defaultFont from "../../../inc/fonts/roboto.woff";
import audioIcon from "../../../inc/assets/audio_icon.png";
import lightIcon from "../../../inc/assets/light_icon.png";
import { EditorPluginProvider, useEditorPlugins, EditorPluginContext } from './EditorPluginProvider'; // Import the PluginProvider
+import { LumaSplatsThree } from "@lumaai/luma-web";
+// Make LumaSplatsThree available to R3F
+extend( { LumaSplats: LumaSplatsThree } );
const { registerStore } = wp.data;
@@ -42,7 +58,7 @@ function TextObject(text) {
useEffect(() => {
if( text.focusID === text.htmlobjectId ) {
- const someFocus = new THREE.Vector3(Number(text.positionX), Number(text.positionY), Number(text.positionZ));
+ const someFocus = new Vector3(Number(text.positionX), Number(text.positionY), Number(text.positionZ));
text.changeFocusPoint(someFocus);
}
}, [text.focusID]);
@@ -66,7 +82,7 @@ function TextObject(text) {
object={textObj}
size={0.5}
onObjectChange={(e) => {
- const rot = new THREE.Euler(0, 0, 0, "XYZ");
+ const rot = new Euler(0, 0, 0, "XYZ");
const scale = e?.target.worldScale;
rot.setFromQuaternion(
e?.target.worldQuaternion
@@ -106,7 +122,7 @@ function TextObject(text) {
scale={[text.scaleX, text.scaleY, text.scaleZ]}
>
@@ -123,7 +139,7 @@ function TextObject(text) {
function ThreeSky(sky) {
const skyUrl = sky.src.skyUrl;
if (skyUrl) {
- const texture_1 = useLoader(THREE.TextureLoader, skyUrl);
+ const texture_1 = useLoader(TextureLoader, skyUrl);
return (
-
+
);
} else {
@@ -150,15 +166,24 @@ function ThreeSky(sky) {
function Spawn(spawn) {
const spawnObj = useRef();
const [isSelected, setIsSelected] = useState();
- const spawnBlockAttributes = wp.data
- .select("core/block-editor")
- .getBlockAttributes(spawn.spawnpointID);
+ const [spawnBlockAttributes, setSpawnPointAttributes] = useState(
+ wp.data
+ .select("core/block-editor")
+ .getBlockAttributes(spawn.spawnpointID)
+ );
+ useEffect(() => {
+ const attributes = wp.data
+ .select("core/block-editor")
+ .getBlockAttributes(spawn.spawnpointID);
+ setSpawnPointAttributes(attributes);
+ }, [spawn.spawnpointID]);
+
const TransformController = ({ condition, wrap, children }) =>
condition ? wrap(children) : children;
useEffect(() => {
if( spawn.focusID === spawn.spawnpointID ) {
- const someFocus = new THREE.Vector3(Number(spawn.positionX), Number(spawn.positionY), Number(spawn.positionZ));
+ const someFocus = new Vector3(Number(spawn.positionX), Number(spawn.positionY), Number(spawn.positionZ));
spawn.changeFocusPoint(someFocus);
}
}, [spawn.focusID]);
@@ -181,8 +206,8 @@ function Spawn(spawn) {
enabled={spawn.focusID === spawn.spawnpointID}
object={spawnObj}
size={0.5}
- onObjectChange={(e) => {
- const rot = new THREE.Euler(0, 0, 0, "XYZ");
+ onMouseUp={(e) => {
+ const rot = new Euler(0, 0, 0, "XYZ");
const scale = e?.target.worldScale;
rot.setFromQuaternion(
e?.target.worldQuaternion
@@ -200,31 +225,37 @@ function Spawn(spawn) {
scaleY: scale.y,
scaleZ: scale.z
});
+ setSpawnPointAttributes(
+ wp.data
+ .select("core/block-editor")
+ .getBlockAttributes(spawn.spawnpointID)
+ );
}}
>
{children}
)}
>
- {spawnBlockAttributes && (
+
- )}
+
);
@@ -232,7 +263,7 @@ function Spawn(spawn) {
}
function ImageObject(threeImage) {
- const texture2 = useLoader(THREE.TextureLoader, threeImage.url);
+ const texture2 = useLoader(TextureLoader, threeImage.url);
const imgObj = useRef();
const [isSelected, setIsSelected] = useState();
const threeImageBlockAttributes = wp.data
@@ -243,7 +274,7 @@ function ImageObject(threeImage) {
useEffect(() => {
if( threeImage.focusID === threeImage.imageID ) {
- const someFocus = new THREE.Vector3(Number(threeImage.positionX), Number(threeImage.positionY), Number(threeImage.positionZ));
+ const someFocus = new Vector3(Number(threeImage.positionX), Number(threeImage.positionY), Number(threeImage.positionZ));
threeImage.changeFocusPoint(someFocus);
}
}, [threeImage.focusID]);
@@ -266,7 +297,7 @@ function ImageObject(threeImage) {
object={imgObj}
size={0.5}
onObjectChange={(e) => {
- const rot = new THREE.Euler(0, 0, 0, "XYZ");
+ const rot = new Euler(0, 0, 0, "XYZ");
const scale = e?.target.worldScale;
rot.setFromQuaternion(e?.target.worldQuaternion);
wp.data
@@ -317,12 +348,12 @@ function ImageObject(threeImage) {
{threeImageBlockAttributes.transparent ? (
) : (
)}
@@ -334,7 +365,7 @@ function ImageObject(threeImage) {
}
function AudioObject(threeAudio) {
- const texture2 = useLoader(THREE.TextureLoader, (threeObjectPlugin + audioIcon));
+ const texture2 = useLoader(TextureLoader, (audioIcon));
const [threeAudioBlockAttributes, setThreeAudioBlockAttributes] = useState(
wp.data
@@ -360,7 +391,7 @@ function AudioObject(threeAudio) {
useEffect(() => {
if( threeAudio.focusID === threeAudio.audioID ) {
- const someFocus = new THREE.Vector3(Number(threeAudio.positionX), Number(threeAudio.positionY), Number(threeAudio.positionZ));
+ const someFocus = new Vector3(Number(threeAudio.positionX), Number(threeAudio.positionY), Number(threeAudio.positionZ));
threeAudio.changeFocusPoint(someFocus);
}
}, [threeAudio.focusID]);
@@ -387,7 +418,7 @@ function AudioObject(threeAudio) {
object={audioObj}
size={0.5}
onMouseUp={(e) => {
- const rot = new THREE.Euler(0, 0, 0, "XYZ");
+ const rot = new Euler(0, 0, 0, "XYZ");
const scale = e?.target.worldScale;
rot.setFromQuaternion(e?.target.worldQuaternion);
wp.data
@@ -427,7 +458,7 @@ function AudioObject(threeAudio) {
{
if( threeLight.focusID === threeLight.lightID ) {
- const someFocus = new THREE.Vector3(Number(threeLight.positionX), Number(threeLight.positionY), Number(threeLight.positionZ));
+ const someFocus = new Vector3(Number(threeLight.positionX), Number(threeLight.positionY), Number(threeLight.positionZ));
threeLight.changeFocusPoint(someFocus);
}
}, [threeLight.focusID]);
@@ -568,7 +599,7 @@ function LightObject(threeLight) {
object={lightObj}
size={0.5}
onMouseUp={(e) => {
- const rot = new THREE.Euler(0, 0, 0, "XYZ");
+ const rot = new Euler(0, 0, 0, "XYZ");
const scale = e?.target.worldScale;
rot.setFromQuaternion(e?.target.worldQuaternion);
wp.data
@@ -609,7 +640,7 @@ function LightObject(threeLight) {
{
if( threeVideo.focusID === threeVideo.videoID ) {
- const someFocus = new THREE.Vector3(Number(threeVideo.positionX), Number(threeVideo.positionY), Number(threeVideo.positionZ));
+ const someFocus = new Vector3(Number(threeVideo.positionX), Number(threeVideo.positionY), Number(threeVideo.positionZ));
threeVideo.changeFocusPoint(someFocus);
}
}, [threeVideo.focusID]);
@@ -737,7 +768,7 @@ function VideoObject(threeVideo) {
object={videoObj}
size={0.5}
onMouseUp={(e) => {
- const rot = new THREE.Euler(0, 0, 0, "XYZ");
+ const rot = new Euler(0, 0, 0, "XYZ");
const scale = e?.target.worldScale;
rot.setFromQuaternion(e?.target.worldQuaternion);
wp.data
@@ -790,7 +821,7 @@ function VideoObject(threeVideo) {
{
setTimeout(() => set(props.url), 2000);
}, []);
- const [listener] = useState(() => new THREE.AudioListener());
+ const [listener] = useState(() => new AudioListener());
useThree(({ camera }) => {
camera.add(listener);
});
const { camera } = useThree();
- const gltf = useLoader(GLTFLoader, props.url, (loader) => {
+ let gltf;
+ try {
+ gltf = useLoader(GLTFLoader, props.url, (loader) => {
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath( threeObjectPluginRoot + "/inc/utils/draco/");
- dracoLoader.setDecoderConfig({type: 'js'}); // (Optional) Override detection of WASM support.
+ dracoLoader.setDecoderConfig({type: 'js'});
loader.setDRACOLoader(dracoLoader);
-
+
if(listener){
- loader.register(
- (parser) => new GLTFAudioEmitterExtension(parser, listener)
- );
+ loader.register(
+ (parser) => new GLTFAudioEmitterExtension(parser, listener)
+ );
}
loader.register((parser) => {
- return new VRMLoaderPlugin(parser);
+ return new VRMLoaderPlugin(parser);
});
- });
+ });
+ } catch (error) {
+ console.error("Failed to load GLTF file: ", error);
+ // Set gltf to a fallback Three.js object
+ const geometry = new BoxGeometry();
+ const material = new MeshBasicMaterial({color: 0x00ff00});
+ gltf = new Mesh(geometry, material);
+ }
- const { actions } = useAnimations(gltf.animations, gltf.scene);
+ const { actions } = useAnimations(gltf?.animations, gltf?.scene);
const animationList = props.animations ? props.animations.split(",") : "";
useEffect(() => {
@@ -859,7 +899,7 @@ function ModelObject(props) {
// update id if active
useEffect(() => {
if( props.focusID === props.modelID ) {
- const someFocus = new THREE.Vector3(Number(props.positionX), Number(props.positionY), Number(props.positionZ));
+ const someFocus = new Vector3(Number(props.positionX), Number(props.positionY), Number(props.positionZ));
props.changeFocusPoint(someFocus);
}
}, [props.focusID]);
@@ -891,7 +931,7 @@ function ModelObject(props) {
object={obj}
size={0.5}
onMouseUp={(e) => {
- const rot = new THREE.Euler(0, 0, 0, "XYZ");
+ const rot = new Euler(0, 0, 0, "XYZ");
const scale = e?.target.worldScale;
rot.setFromQuaternion(
e?.target.worldQuaternion
@@ -947,7 +987,10 @@ function ModelObject(props) {
>
);
}
- gltf.scene.rotation.set(0, 0, 0);
+
+ if ( gltf.scene ) {
+ gltf.scene.rotation.set(0, 0, 0);
+ }
// const copyGltf = useMemo(() => gltf.scene.clone(), [gltf.scene]);
return (
@@ -973,7 +1016,7 @@ function ModelObject(props) {
object={obj}
size={0.5}
onMouseUp={(e) => {
- const rot = new THREE.Euler(0, 0, 0, "XYZ");
+ const rot = new Euler(0, 0, 0, "XYZ");
const scale = e?.target.worldScale;
rot.setFromQuaternion(
e?.target.worldQuaternion
@@ -1002,7 +1045,7 @@ function ModelObject(props) {
)}
>
- {modelBlockAttributes && (
+ {modelBlockAttributes && gltf.scene && (
{
if( props.focusID === props.modelID ) {
- const someFocus = new THREE.Vector3(Number(props.positionX), Number(props.positionY), Number(props.positionZ));
+ const someFocus = new Vector3(Number(props.positionX), Number(props.positionY), Number(props.positionZ));
props.changeFocusPoint(someFocus);
}
}, [props.focusID]);
@@ -1064,10 +1107,10 @@ function NPCObject(props) {
const vrm = gltf.userData.vrm;
VRMUtils.rotateVRM0(vrm);
const rotationVRM = vrm.scene.rotation.y + parseFloat(0);
- let defaultColor = "0x000000";
+ let defaultColor = "#000000";
var colorValue = parseInt ( defaultColor.replace("#","0x"), 16 );
- const color = new THREE.Color( colorValue );
+ const color = new Color( colorValue );
return (
<>
@@ -1092,7 +1135,7 @@ function NPCObject(props) {
object={obj}
size={0.5}
onMouseUp={(e) => {
- const rot = new THREE.Euler(0, 0, 0, "XYZ");
+ const rot = new Euler(0, 0, 0, "XYZ");
const scale = e?.target.worldScale;
rot.setFromQuaternion(
e?.target.worldQuaternion
@@ -1170,7 +1213,7 @@ function NPCObject(props) {
object={obj}
size={0.5}
onMouseUp={(e) => {
- const rot = new THREE.Euler(0, 0, 0, "XYZ");
+ const rot = new Euler(0, 0, 0, "XYZ");
const scale = e?.target.worldScale;
rot.setFromQuaternion(
e?.target.worldQuaternion
@@ -1231,7 +1274,7 @@ function PortalObject(model) {
useEffect(() => {
if( model.focusID === model.portalID ) {
- const someFocus = new THREE.Vector3(Number(model.positionX), Number(model.positionY), Number(model.positionZ));
+ const someFocus = new Vector3(Number(model.positionX), Number(model.positionY), Number(model.positionZ));
model.changeFocusPoint(someFocus);
}
}, [model.focusID]);
@@ -1243,7 +1286,7 @@ function PortalObject(model) {
useEffect(() => {
setTimeout(() => set(model.url), 2000);
}, []);
- const [listener] = useState(() => new THREE.AudioListener());
+ const [listener] = useState(() => new AudioListener());
useThree(({ camera }) => {
camera.add(listener);
@@ -1317,7 +1360,7 @@ function PortalObject(model) {
object={obj}
size={0.5}
onMouseUp={(e) => {
- const rot = new THREE.Euler(0, 0, 0, "XYZ");
+ const rot = new Euler(0, 0, 0, "XYZ");
const scale = e?.target.worldScale;
rot.setFromQuaternion(
e?.target.worldQuaternion
@@ -1366,12 +1409,13 @@ function PortalObject(model) {
]}
>
{
setTimeout(() => set(props.url), 2000);
}, [props.url]);
- const [listener] = useState(() => new THREE.AudioListener());
+ const [listener] = useState(() => new AudioListener());
useThree(({ camera }) => {
camera.add(listener);
@@ -1563,7 +1619,7 @@ function ThreeObject(props) {
const gltf = useLoader(GLTFLoader, url, (loader) => {
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath( threeObjectPluginRoot + "/inc/utils/draco/");
- dracoLoader.setDecoderConfig({type: 'js'}); // (Optional) Override detection of WASM support.
+ dracoLoader.setDecoderConfig({type: 'js'});
loader.setDRACOLoader(dracoLoader);
loader.register(
@@ -1622,7 +1678,7 @@ function ThreeObject(props) {
size={0.5}
position={ [ blockPosition.positionX, blockPosition.positionY, blockPosition.positionZ ] }
onObjectChange={(e) => {
- const rot = new THREE.Euler(0, 0, 0, "XYZ");
+ const rot = new Euler(0, 0, 0, "XYZ");
const scale = e?.target.worldScale;
rot.setFromQuaternion(
e?.target.worldQuaternion
@@ -1671,7 +1727,6 @@ function ThreeObject(props) {
positionZ={spawnpoint.positionZ}
transformMode={props.transformMode}
changeFocusPoint={props.changeFocusPoint}
- // setFocusPosition={props.setFocusPosition}
shouldFocus={props.shouldFocus}
/>
)}
@@ -1938,9 +1993,10 @@ export default function ThreeObjectEdit(props) {
diff --git a/blocks/environment/components/ThreeObjectFront.js b/blocks/environment/components/ThreeObjectFront.js
new file mode 100644
index 0000000..c0275d1
--- /dev/null
+++ b/blocks/environment/components/ThreeObjectFront.js
@@ -0,0 +1,572 @@
+import * as THREE from 'three';
+import { AudioListener, Group, Quaternion, VectorKeyframeTrack, QuaternionKeyframeTrack, LoopPingPong, AnimationClip, NumberKeyframeTrack, AnimationMixer, Vector3, BufferGeometry, MeshBasicMaterial, DoubleSide, Mesh, CircleGeometry, sRGBEncoding } from "three";
+import React, { Suspense, useRef, useState, useEffect } from 'react';
+import { Canvas, useLoader, useFrame, useThree } from '@react-three/fiber';
+import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
+import { FBXLoader } from "three/examples/jsm/loaders/FBXLoader";
+import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader';
+
+import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";
+import { Physics, RigidBody } from "@react-three/rapier";
+import { USDZLoader } from 'three/examples/jsm/loaders/USDZLoader';
+import { GLTFGoogleTiltBrushMaterialExtension } from "three-icosa";
+import idle from "../../../inc/avatars/friendly.fbx";
+
+import {
+ OrthographicCamera,
+ OrbitControls,
+ useAnimations,
+} from '@react-three/drei';
+import { GLTFAudioEmitterExtension } from 'three-omi';
+import {
+ VRCanvas,
+ ARCanvas,
+ DefaultXRControllers,
+ Hands,
+} from '@react-three/xr';
+import { VRMUtils, VRMSchema, VRMLoaderPlugin, VRMExpressionPresetName, VRMHumanBoneName } from "@pixiv/three-vrm";
+import TeleportTravel from './ThreeObjectTeleport';
+import { PlaneGeometry } from 'three';
+
+/**
+ * A map from Mixamo rig name to VRM Humanoid bone name
+ */
+const mixamoVRMRigMap = {
+ mixamorigHips: 'hips',
+ mixamorigSpine: 'spine',
+ mixamorigSpine1: 'chest',
+ mixamorigSpine2: 'upperChest',
+ mixamorigNeck: 'neck',
+ mixamorigHead: 'head',
+ mixamorigLeftShoulder: 'leftShoulder',
+ mixamorigLeftArm: 'leftUpperArm',
+ mixamorigLeftForeArm: 'leftLowerArm',
+ mixamorigLeftHand: 'leftHand',
+ mixamorigLeftHandThumb1: 'leftThumbMetacarpal',
+ mixamorigLeftHandThumb2: 'leftThumbProximal',
+ mixamorigLeftHandThumb3: 'leftThumbDistal',
+ mixamorigLeftHandIndex1: 'leftIndexProximal',
+ mixamorigLeftHandIndex2: 'leftIndexIntermediate',
+ mixamorigLeftHandIndex3: 'leftIndexDistal',
+ mixamorigLeftHandMiddle1: 'leftMiddleProximal',
+ mixamorigLeftHandMiddle2: 'leftMiddleIntermediate',
+ mixamorigLeftHandMiddle3: 'leftMiddleDistal',
+ mixamorigLeftHandRing1: 'leftRingProximal',
+ mixamorigLeftHandRing2: 'leftRingIntermediate',
+ mixamorigLeftHandRing3: 'leftRingDistal',
+ mixamorigLeftHandPinky1: 'leftLittleProximal',
+ mixamorigLeftHandPinky2: 'leftLittleIntermediate',
+ mixamorigLeftHandPinky3: 'leftLittleDistal',
+ mixamorigRightShoulder: 'rightShoulder',
+ mixamorigRightArm: 'rightUpperArm',
+ mixamorigRightForeArm: 'rightLowerArm',
+ mixamorigRightHand: 'rightHand',
+ mixamorigRightHandPinky1: 'rightLittleProximal',
+ mixamorigRightHandPinky2: 'rightLittleIntermediate',
+ mixamorigRightHandPinky3: 'rightLittleDistal',
+ mixamorigRightHandRing1: 'rightRingProximal',
+ mixamorigRightHandRing2: 'rightRingIntermediate',
+ mixamorigRightHandRing3: 'rightRingDistal',
+ mixamorigRightHandMiddle1: 'rightMiddleProximal',
+ mixamorigRightHandMiddle2: 'rightMiddleIntermediate',
+ mixamorigRightHandMiddle3: 'rightMiddleDistal',
+ mixamorigRightHandIndex1: 'rightIndexProximal',
+ mixamorigRightHandIndex2: 'rightIndexIntermediate',
+ mixamorigRightHandIndex3: 'rightIndexDistal',
+ mixamorigRightHandThumb1: 'rightThumbMetacarpal',
+ mixamorigRightHandThumb2: 'rightThumbProximal',
+ mixamorigRightHandThumb3: 'rightThumbDistal',
+ mixamorigLeftUpLeg: 'leftUpperLeg',
+ mixamorigLeftLeg: 'leftLowerLeg',
+ mixamorigLeftFoot: 'leftFoot',
+ mixamorigLeftToeBase: 'leftToes',
+ mixamorigRightUpLeg: 'rightUpperLeg',
+ mixamorigRightLeg: 'rightLowerLeg',
+ mixamorigRightFoot: 'rightFoot',
+ mixamorigRightToeBase: 'rightToes',
+};
+
+
+/**
+ * Load Mixamo animation, convert for three-vrm use, and return it.
+ *
+ * @param {string} url A url of mixamo animation data
+ * @param {VRM} vrm A target VRM
+ * @returns {Promise} The converted AnimationClip
+ */
+function loadMixamoAnimation(url, vrm, positionY, positionX, positionZ, scaleX, scaleY, scaleZ, rotationX, rotationY, rotationZ, rotationW) {
+ const loader = new FBXLoader();
+ return loader.loadAsync(url).then((asset) => {
+ const clip = AnimationClip.findByName(asset.animations, 'mixamo.com');
+ const tracks = [];
+
+ const restRotationInverse = new Quaternion();
+ const parentRestWorldRotation = new Quaternion();
+ const _quatA = new Quaternion();
+ const _vec3 = new Vector3();
+
+ // Adjust with reference to hips height.
+ const motionHipsHeight = asset.getObjectByName('mixamorigHips').position.y;
+ const vrmHipsY = vrm.humanoid?.getNormalizedBoneNode('hips').getWorldPosition(_vec3).y;
+ const vrmRootY = vrm.scene.getWorldPosition(_vec3).y;
+ const vrmHipsHeight = Math.abs(vrmHipsY - vrmRootY);
+ const hipsPositionScale = vrmHipsHeight / motionHipsHeight;
+
+ clip.tracks.forEach((track) => {
+ // Convert each tracks for VRM use, and push to `tracks`
+ const trackSplitted = track.name.split('.');
+ const mixamoRigName = trackSplitted[0];
+ const vrmBoneName = mixamoVRMRigMap[mixamoRigName];
+ const vrmNodeName = vrm.humanoid?.getNormalizedBoneNode(vrmBoneName)?.name;
+ const mixamoRigNode = asset.getObjectByName(mixamoRigName);
+
+ if (vrmNodeName != null) {
+
+ const propertyName = trackSplitted[1];
+
+ // Store rotations of rest-pose.
+ mixamoRigNode.getWorldQuaternion(restRotationInverse).invert();
+ mixamoRigNode.parent.getWorldQuaternion(parentRestWorldRotation);
+
+ if (track instanceof QuaternionKeyframeTrack) {
+
+ // Retarget rotation of mixamoRig to NormalizedBone.
+ for (let i = 0; i < track.values.length; i += 4) {
+
+ const flatQuaternion = track.values.slice(i, i + 4);
+
+ _quatA.fromArray(flatQuaternion);
+
+ _quatA
+ .premultiply(parentRestWorldRotation)
+ .multiply(restRotationInverse);
+
+ _quatA.toArray(flatQuaternion);
+
+ flatQuaternion.forEach((v, index) => {
+
+ track.values[index + i] = v;
+
+ });
+
+ }
+
+ tracks.push(
+ new QuaternionKeyframeTrack(
+ `${vrmNodeName}.${propertyName}`,
+ track.times,
+ track.values.map((v, i) => (vrm.meta?.metaVersion === '0' && i % 2 === 0 ? - v : v)),
+ ),
+ );
+
+ } else if (track instanceof VectorKeyframeTrack) {
+ const value = track.values.map((v, i) => (vrm.meta?.metaVersion === '0' && i % 3 !== 1 ? - v : v) * hipsPositionScale);
+ tracks.push(new VectorKeyframeTrack(`${vrmNodeName}.${propertyName}`, track.times, value));
+ }
+
+ }
+
+ });
+
+ return new AnimationClip('vrmAnimation', clip.duration, tracks);
+
+ });
+
+}
+
+function SavedObject( props ) {
+ const [idleFile, setIdleFile] = useState(idle);
+ const [ url, set ] = useState( props.url );
+ useEffect( () => {
+ setTimeout( () => set( props.url ), 2000 );
+ }, [] );
+ const [ listener ] = useState( () => new THREE.AudioListener() );
+ const { clock, camera, gl } = useThree();
+ useThree( ( { camera } ) => {
+ camera.add( listener );
+ } );
+
+ // USDZ loader.
+ if(props.url.split(/[#?]/)[0].split('.').pop().trim() === "usdz") {
+
+ const usdz = useLoader( USDZLoader, url);
+
+ return ;
+ }
+
+ const gltf = useLoader( GLTFLoader, url, ( loader ) => {
+ if(openbrushEnabled === true){
+ loader.register(
+ (parser) =>
+ new GLTFGoogleTiltBrushMaterialExtension(parser, openbrushDirectory)
+ );
+ }
+ const ktx2Loader = new KTX2Loader();
+ ktx2Loader.setTranscoderPath(threeObjectPluginRoot + "/inc/utils/basis/");
+ ktx2Loader.detectSupport(gl);
+ loader.setKTX2Loader(ktx2Loader);
+ const dracoLoader = new DRACOLoader();
+ dracoLoader.setDecoderPath( threeObjectPluginRoot + "/inc/utils/draco/");
+ dracoLoader.setDecoderConfig({type: 'js'});
+ loader.setDRACOLoader(dracoLoader);
+ loader.register(
+ ( parser ) => new GLTFAudioEmitterExtension( parser, listener )
+ );
+ loader.register( ( parser ) => {
+
+ return new VRMLoaderPlugin( parser );
+ } );
+ } );
+
+ const { actions } = useAnimations( gltf.animations, gltf.scene );
+
+ const animationList = props.animations ? props.animations.split( ',' ) : '';
+ useEffect( () => {
+ if ( animationList ) {
+ animationList.forEach( ( name ) => {
+ if ( Object.keys( actions ).includes( name ) ) {
+ actions[ name ].play();
+ }
+ } );
+ }
+ }, [] );
+
+ const generator = gltf.asset.generator;
+ if (String(generator).includes("Tilt Brush")) {
+ gltf.scene.position.set( 0, props.positionY, 0 );
+ gltf.scene.rotation.set( 0, props.rotationY, 0 );
+ gltf.scene.scale.set( props.scale, props.scale, props.scale );
+
+ return (
+
+ );
+ }
+
+ if(gltf?.userData?.gltfExtensions?.VRM){
+ // make the VRM invisible while setting up animations
+ gltf.scene.visible = false;
+ const vrm = gltf.userData.vrm;
+ VRMUtils.rotateVRM0(vrm);
+ // Disable frustum culling
+ vrm.scene.traverse((obj) => {
+ obj.frustumCulled = false;
+ });
+
+ // scene.add(vrm.scene);
+
+ const currentVrm = vrm;
+ const currentMixer = new AnimationMixer(currentVrm.scene);
+ // Load animation
+ if (currentVrm) {
+ if (currentVrm.humanoid) {
+ let head = currentVrm.humanoid.getRawBoneNode(VRMHumanBoneName.Head);
+ if (head) {
+ const headPos = new Vector3();
+ head.getWorldPosition(headPos);
+ let offsetPositionY = headPos.y - props.positionY;
+ const newTarget = new Vector3( headPos.x, offsetPositionY , headPos.z);
+ const newPos = new Vector3( camera.position.x, offsetPositionY , camera.position.z);
+ props.orbitRef.current.target = newTarget;
+ props.orbitRef.current.maxPolarAngle = Math.PI / 2;
+ props.orbitRef.current.minPolarAngle = Math.PI / 2;
+ }
+ }
+ }
+
+ useFrame((state, delta) => {
+
+ // keep the camera looking at the head bone
+ if (currentVrm) {
+ // currentVrm.expressionManager.setValue( VRMExpressionPresetName.Neutral, 0 );
+ // currentVrm.expressionManager.setValue( VRMExpressionPresetName.Relaxed, 0.8 );
+ currentVrm.update(delta);
+ }
+ if (currentMixer) {
+ currentMixer.update(delta);
+ }
+ });
+
+ // retarget the animations from mixamo to the current vrm
+ useEffect(() => {
+ let isActive = true;
+
+ const updateVRMVisibility = async () => {
+ if (currentVrm) {
+ // make the VRM invisible while setting up animations
+ currentVrm.scene.visible = false;
+ if ( props.defaultAvatarAnimation ) {
+ loadMixamoAnimation(props.defaultAvatarAnimation, currentVrm, 0, props.positionY, props.positionZ, props.scale, props.scale, props.scale).then((clip) => {
+ currentMixer.clipAction(clip).play();
+ currentMixer.update(clock.getDelta());
+ // make the VRM visible again
+ currentVrm.scene.visible = true;
+ if (currentVrm) {
+ if (currentVrm.humanoid) {
+ let head = currentVrm.humanoid.getRawBoneNode(VRMHumanBoneName.Head);
+ if (head) {
+ const headPos = new Vector3();
+ head.getWorldPosition(headPos);
+ let offsetPositionY = headPos.y - props.positionY;
+ const newTarget = new Vector3( headPos.x, offsetPositionY , headPos.z);
+ const newPos = new Vector3( camera.position.x, offsetPositionY , camera.position.z);
+ props.orbitRef.current.target = newTarget;
+ props.orbitRef.current.maxPolarAngle = Math.PI / 2;
+ props.orbitRef.current.minPolarAngle = Math.PI / 2;
+ }
+ }
+ }
+ });
+ } else {
+ loadMixamoAnimation(idleFile, currentVrm, 0, props.positionY, props.positionZ, props.scale, props.scale, props.scale).then((clip) => {
+ currentMixer.clipAction(clip).play();
+ currentMixer.update(clock.getDelta());
+ // make the VRM visible again
+ currentVrm.scene.visible = true;
+ if (currentVrm) {
+ if (currentVrm.humanoid) {
+ let head = currentVrm.humanoid.getRawBoneNode(VRMHumanBoneName.Head);
+ if (head) {
+ const headPos = new Vector3();
+ head.getWorldPosition(headPos);
+ let offsetPositionY = headPos.y - props.positionY;
+ const newTarget = new Vector3( headPos.x, offsetPositionY , headPos.z);
+ const newPos = new Vector3( camera.position.x, offsetPositionY , camera.position.z);
+ props.orbitRef.current.target = newTarget;
+ props.orbitRef.current.maxPolarAngle = Math.PI / 2;
+ props.orbitRef.current.minPolarAngle = Math.PI / 2;
+ }
+ }
+ }
+ });
+ }
+ }
+ };
+
+ updateVRMVisibility();
+ return () => {
+ isActive = false;
+ };
+
+ }, []);
+
+ return (
+
+
+
+ );
+ }
+ gltf.scene.position.set( 0, props.positionY, 0 );
+ gltf.scene.rotation.set( 0, props.rotationY, 0 );
+ gltf.scene.scale.set( props.scale, props.scale, props.scale );
+ return ;
+}
+
+function Floor( props ) {
+ return (
+
+
+
+
+ );
+}
+
+export default function ThreeObjectFront( props ) {
+ const orbitRef = useRef();
+
+ if ( props.deviceTarget === 'vr' ) {
+ return (
+ <>
+
+
+
+
+
+
+
+ { props.threeUrl && (
+ <>
+
+
+
+
+
+
+
+
+ >
+ ) }
+
+
+
+
+ { props.hasTip === '1' ? (
+ Click and drag ^
+ ) : (
+
+ ) }
+ >
+ );
+ }
+ if ( props.deviceTarget === 'ar' ) {
+ return (
+ <>
+
+
+
+
+ { props.threeUrl && (
+
+ ) }
+
+
+
+ { props.hasTip === '1' ? (
+ Click and drag ^
+ ) : (
+
+ ) }
+ >
+ );
+ }
+ if ( props.deviceTarget === '2d' ) {
+ return (
+ <>
+
+
+
+
+ { props.threeUrl && (
+
+ ) }
+
+
+
+ { props.hasTip === '1' ? (
+ Click and drag ^
+ ) : (
+
+ ) }
+ >
+ );
+ }
+}
diff --git a/blocks/environment/components/ThreeObjectTeleport.js b/blocks/environment/components/ThreeObjectTeleport.js
new file mode 100644
index 0000000..784173f
--- /dev/null
+++ b/blocks/environment/components/ThreeObjectTeleport.js
@@ -0,0 +1,95 @@
+import { Raycaster, Vector3 } from "three";
+import { useXR, Interactive } from "@react-three/xr";
+import { useFrame } from "@react-three/fiber";
+import { useCallback, useRef, useState } from "react";
+
+export function TeleportIndicator(props) {
+ return (
+ <>
+
+
+
+
+
+ >
+ );
+}
+
+export default function TeleportTravel(props) {
+ const {
+ centerOnTeleport,
+ Indicator = TeleportIndicator,
+ useNormal = true
+ } = props;
+ const [isHovered, setIsHovered] = useState(false);
+ const target = useRef();
+ const targetLoc = useRef();
+ const ray = useRef(new Raycaster());
+
+ const rayDir = useRef({
+ pos: new Vector3(),
+ dir: new Vector3()
+ });
+
+ const { controllers, player } = useXR();
+
+ useFrame(() => {
+ if (
+ isHovered &&
+ controllers.length > 0 &&
+ ray.current &&
+ target.current &&
+ targetLoc.current
+ ) {
+ controllers[0].controller.getWorldDirection(rayDir.current.dir);
+ controllers[0].controller.getWorldPosition(rayDir.current.pos);
+ rayDir.current.dir.multiplyScalar(-1);
+ ray.current.set(rayDir.current.pos, rayDir.current.dir);
+
+ const [intersection] = ray.current.intersectObject(target.current);
+
+ if (intersection) {
+ if (useNormal) {
+ const p = intersection.point;
+
+ targetLoc.current.position.set(0, 0, 0);
+
+ const n = intersection.face.normal.clone();
+ n.transformDirection(intersection.object.matrixWorld);
+
+ targetLoc.current.lookAt(n);
+ targetLoc.current.rotateOnAxis(
+ new Vector3(1, 0, 0),
+ Math.PI / 2
+ );
+ targetLoc.current.position.copy(p);
+ } else {
+ targetLoc.current.position.copy(intersection.point);
+ }
+ }
+ }
+ });
+
+ const click = useCallback(() => {
+ if (isHovered) {
+ player.position.copy(targetLoc.current.position);
+ }
+ }, [centerOnTeleport, isHovered, useNormal]);
+
+ return (
+ <>
+ {isHovered && (
+
+
+
+ )}
+ setIsHovered(true)}
+ onBlur={() => setIsHovered(false)}
+ >
+ {props.children}
+
+ >
+ );
+}
diff --git a/blocks/environment/components/avatar/index.js b/blocks/environment/components/avatar/index.js
new file mode 100644
index 0000000..6273e1c
--- /dev/null
+++ b/blocks/environment/components/avatar/index.js
@@ -0,0 +1,422 @@
+import * as THREE from 'three';
+import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
+import { VRMLLoader } from 'three/examples/jsm/loaders/VRMLLoader';
+import { VRMUtils, VRMSchema, VRMHumanBoneName } from '@pixiv/three-vrm';
+
+class ExokitAvatar {
+ constructor(model, options = {}) {
+ this.vrm = model;
+ this.model = model.scene;
+
+ this.options = {
+ fingers: true,
+ hair: true,
+ decapitate: false,
+ visemes: true,
+ microphoneMediaStream: null,
+ muted: true,
+ debug: false,
+ ...options,
+ };
+
+ this.inputs = {
+ hmd: {
+ position: new THREE.Vector3(),
+ quaternion: new THREE.Quaternion(),
+ },
+ leftGamepad: {
+ position: new THREE.Vector3(),
+ quaternion: new THREE.Quaternion(),
+ pointer: 0,
+ grip: 0,
+ },
+ rightGamepad: {
+ position: new THREE.Vector3(),
+ quaternion: new THREE.Quaternion(),
+ pointer: 0,
+ grip: 0,
+ },
+ };
+
+ this.floorHeight = 0;
+ this.microphoneVolume = 0;
+
+ this.init();
+ }
+
+ async init() {
+ this.applyOptions();
+ this.setUpAnimations();
+ this.setUpAudio();
+ }
+
+
+ applyOptions() {
+ const { vrm } = this.vrm;
+
+ if (vrm) {
+ const invisibleMeshes = [];
+
+ if (!this.options.fingers) {
+ invisibleMeshes.push(
+ vrm.humanoid.getRawBoneNode(VRMHumanBoneName.LeftIndexDistal),
+ vrm.humanoid.getRawBoneNode(VRMHumanBoneName.LeftIndexIntermediate),
+ vrm.humanoid.getRawBoneNode(VRMHumanBoneName.LeftIndexProximal),
+ vrm.humanoid.getRawBoneNode(VRMHumanBoneName.LeftLittleDistal),
+ vrm.humanoid.getRawBoneNode(VRMHumanBoneName.LeftLittleIntermediate),
+ vrm.humanoid.getRawBoneNode(VRMHumanBoneName.LeftLittleProximal),
+ vrm.humanoid.getRawBoneNode(VRMHumanBoneName.LeftMiddleDistal),
+ vrm.humanoid.getRawBoneNode(VRMHumanBoneName.LeftMiddleIntermediate),
+ vrm.humanoid.getRawBoneNode(VRMHumanBoneName.LeftMiddleProximal),
+ vrm.humanoid.getRawBoneNode(VRMHumanBoneName.LeftRingDistal),
+ vrm.humanoid.getRawBoneNode(VRMHumanBoneName.LeftRingIntermediate),
+ vrm.humanoid.getRawBoneNode(VRMHumanBoneName.LeftRingProximal),
+ vrm.humanoid.getRawBoneNode(VRMHumanBoneName.LeftThumbDistal),
+ vrm.humanoid.getRawBoneNode(VRMHumanBoneName.LeftThumbIntermediate),
+ vrm.humanoid.getRawBoneNode(VRMHumanBoneName.LeftThumbProximal),
+ vrm.humanoid.getRawBoneNode(VRMHumanBoneName.RightIndexDistal),
+ vrm.humanoid.getRawBoneNode(VRMHumanBoneName.RightIndexIntermediate),
+ vrm.humanoid.getRawBoneNode(VRMHumanBoneName.RightIndexProximal),
+ vrm.humanoid.getRawBoneNode(VRMHumanBoneName.RightLittleDistal),
+ vrm.humanoid.getRawBoneNode(VRMHumanBoneName.RightLittleIntermediate),
+ vrm.humanoid.getRawBoneNode(VRMHumanBoneName.RightLittleProximal),
+ vrm.humanoid.getRawBoneNode(VRMHumanBoneName.RightMiddleDistal),
+ vrm.humanoid.getRawBoneNode(VRMHumanBoneName.RightMiddleIntermediate),
+ vrm.humanoid.getRawBoneNode(VRMHumanBoneName.RightMiddleProximal),
+ vrm.humanoid.getRawBoneNode(VRMHumanBoneName.RightRingDistal),
+ vrm.humanoid.getRawBoneNode(VRMHumanBoneName.RightRingIntermediate),
+ vrm.humanoid.getRawBoneNode(VRMHumanBoneName.RightRingProximal),
+ vrm.humanoid.getRawBoneNode(VRMHumanBoneName.RightThumbDistal),
+ vrm.humanoid.getRawBoneNode(VRMHumanBoneName.RightThumbIntermediate),
+ vrm.humanoid.getRawBoneNode(VRMHumanBoneName.RightThumbProximal),
+ );
+ }
+
+ if (!this.options.hair) {
+ invisibleMeshes.push(
+ ...vrm.secondaryAnimation._colliderGroups._colliders
+ .map(collider => collider.node)
+ .filter(node => node !== undefined),
+ );
+ }
+
+ if (this.options.decapitate) {
+ invisibleMeshes.push(vrm.humanoid.getRawBoneNode(VRMHumanBoneName.Head));
+ }
+
+ invisibleMeshes.forEach(mesh => {
+ if (mesh) {
+ mesh.visible = false;
+ }
+ });
+
+ if (!this.options.visemes || this.options.muted) {
+ vrm.blendShapeProxy.setValue(VRMSchema.BlendShapePresetName.Aa, 0);
+ vrm.blendShapeProxy.setValue(VRMSchema.BlendShapePresetName.Ih, 0);
+ vrm.blendShapeProxy.setValue(VRMSchema.BlendShapePresetName.Ou, 0);
+ vrm.blendShapeProxy.setValue(VRMSchema.BlendShapePresetName.Ee, 0);
+ vrm.blendShapeProxy.setValue(VRMSchema.BlendShapePresetName.Oh, 0);
+ }
+
+ if (this.options.debug) {
+ vrm.scene.add(new THREE.SkeletonHelper(vrm.scene));
+ }
+ }
+ }
+
+ setUpAnimations() {
+ this.animations = {};
+
+ if (this.vrm) {
+ console.log("bones", this.vrm.humanoid.humanBones);
+
+ console.log("bones", this.vrm.humanoid.humanBones);
+
+ const boneMap = {
+ [VRMHumanBoneName.Hips]: 0,
+ [VRMHumanBoneName.Spine]: 1,
+ [VRMHumanBoneName.Chest]: 2,
+ [VRMHumanBoneName.UpperChest]: 3,
+ [VRMHumanBoneName.Neck]: 4,
+ [VRMHumanBoneName.Head]: 5,
+ [VRMHumanBoneName.LeftShoulder]: 6,
+ [VRMHumanBoneName.LeftUpperArm]: 7,
+ [VRMHumanBoneName.LeftLowerArm]: 8,
+ [VRMHumanBoneName.LeftHand]: 9,
+ [VRMHumanBoneName.RightShoulder]: 10,
+ [VRMHumanBoneName.RightUpperArm]: 11,
+ [VRMHumanBoneName.RightLowerArm]: 12,
+ [VRMHumanBoneName.RightHand]: 13,
+ };
+
+ console.log("All bones:", this.vrm.humanoid.humanBones);
+
+ Object.entries(this.vrm.humanoid.humanBones).forEach(([boneName, boneData]) => {
+ console.log(`Processing bone: ${boneName}`);
+ if (boneData && boneData.node) {
+ console.log(`Bone ${boneName} found`);
+ this.animations[boneName] = {
+ position: boneData.node.position.clone(),
+ quaternion: boneData.node.quaternion.clone(),
+ originalPosition: boneData.node.position.clone(),
+ originalQuaternion: boneData.node.quaternion.clone()
+ };
+ } else {
+ console.warn(`Bone ${boneName} not found or doesn't have a node`);
+ }
+ });
+
+ console.log("Animations object:", this.animations);
+ }
+ }
+
+ setUpAudio() {
+ if (this.options.microphoneMediaStream) {
+ const audioContext = THREE.AudioContext.getContext();
+ const source = audioContext.createMediaStreamSource(this.options.microphoneMediaStream);
+ this.analyser = audioContext.createAnalyser();
+ source.connect(this.analyser);
+ }
+ }
+
+ setHeadPose(position, quaternion) {
+ this.inputs.hmd.position.copy(position);
+ this.inputs.hmd.quaternion.copy(quaternion);
+ }
+
+ setLeftHandPose(position, quaternion, pointer, grip) {
+ this.inputs.leftGamepad.position.copy(position);
+ this.inputs.leftGamepad.quaternion.copy(quaternion);
+ this.inputs.leftGamepad.pointer = pointer;
+ this.inputs.leftGamepad.grip = grip;
+ }
+
+ setRightHandPose(position, quaternion, pointer, grip) {
+ this.inputs.rightGamepad.position.copy(position);
+ this.inputs.rightGamepad.quaternion.copy(quaternion);
+ this.inputs.rightGamepad.pointer = pointer;
+ this.inputs.rightGamepad.grip = grip;
+ }
+
+ setFloorHeight(floorHeight) {
+ this.floorHeight = floorHeight;
+ }
+
+ update(delta) {
+ this.updateAnimations(delta);
+ this.updateVisemes();
+ this.model.updateMatrixWorld(true);
+ }
+
+
+ updateAnimations() {
+ console.log("updateAnimations called");
+
+ if (!this.vrm) {
+ console.log("VRM model not available");
+ return;
+ }
+ console.log("VRM loaded:", this.vrm);
+ console.log("VRM Humanoid:", this.vrm.humanoid);
+
+ const { hmd, leftGamepad, rightGamepad } = this.inputs;
+
+ // Update the hips position and rotation based on the HMD
+ const hipsNode = this.vrm.humanoid.getRawBoneNode(VRMHumanBoneName.Hips);
+ if (hipsNode) {
+ hipsNode.position.copy(hmd.position);
+ hipsNode.quaternion.copy(hmd.quaternion);
+ }
+
+ // Update the chest and spine rotations based on the HMD
+ const chestNode = this.vrm.humanoid.getRawBoneNode(VRMHumanBoneName.Chest);
+ const upperChestNode = this.vrm.humanoid.getRawBoneNode(VRMHumanBoneName.UpperChest);
+ const neckNode = this.vrm.humanoid.getRawBoneNode(VRMHumanBoneName.Neck);
+ if (chestNode && upperChestNode && neckNode) {
+ const chestRotation = new THREE.Quaternion();
+ const upperChestRotation = new THREE.Quaternion();
+ const neckRotation = new THREE.Quaternion();
+ // Calculate the chest, upper chest, and neck rotations based on the HMD rotation
+ // ...
+ chestNode.quaternion.copy(chestRotation);
+ upperChestNode.quaternion.copy(upperChestRotation);
+ neckNode.quaternion.copy(neckRotation);
+ }
+
+ // Update the head rotation based on the HMD
+ const headNode = this.vrm.humanoid.getRawBoneNode(VRMHumanBoneName.Head);
+ if (headNode) {
+ headNode.quaternion.copy(hmd.quaternion);
+ }
+
+ // Update the shoulder positions based on the shoulder width
+ const shoulderWidth = 0.3;
+ const leftShoulderPosition = new THREE.Vector3(-shoulderWidth / 2, 0, 0);
+ const rightShoulderPosition = new THREE.Vector3(shoulderWidth / 2, 0, 0);
+ const leftShoulderNode = this.vrm.humanoid.getRawBoneNode(VRMHumanBoneName.LeftShoulder);
+ const rightShoulderNode = this.vrm.humanoid.getRawBoneNode(VRMHumanBoneName.RightShoulder);
+ if (leftShoulderNode) {
+ leftShoulderNode.position.copy(leftShoulderPosition);
+ }
+ if (rightShoulderNode) {
+ rightShoulderNode.position.copy(rightShoulderPosition);
+ }
+
+ // Update the upper arm rotations based on the gamepads
+ const leftUpperArmNode = this.vrm.humanoid.getRawBoneNode(VRMHumanBoneName.LeftUpperArm);
+ const rightUpperArmNode = this.vrm.humanoid.getRawBoneNode(VRMHumanBoneName.RightUpperArm);
+ if (leftUpperArmNode) {
+ leftUpperArmNode.quaternion.copy(leftGamepad.quaternion);
+ }
+ if (rightUpperArmNode) {
+ rightUpperArmNode.quaternion.copy(rightGamepad.quaternion);
+ }
+
+ // Update the lower arm rotations based on the gamepads
+ const leftLowerArmNode = this.vrm.humanoid.getRawBoneNode(VRMHumanBoneName.LeftLowerArm);
+ const rightLowerArmNode = this.vrm.humanoid.getRawBoneNode(VRMHumanBoneName.RightLowerArm);
+ if (leftLowerArmNode) {
+ leftLowerArmNode.quaternion.copy(leftGamepad.quaternion);
+ }
+ if (rightLowerArmNode) {
+ rightLowerArmNode.quaternion.copy(rightGamepad.quaternion);
+ }
+
+ // Update the hand positions and rotations based on the gamepads
+ const leftHandNode = this.vrm.humanoid.getRawBoneNode(VRMHumanBoneName.LeftHand);
+ const rightHandNode = this.vrm.humanoid.getRawBoneNode(VRMHumanBoneName.RightHand);
+
+ if (leftHandNode) {
+ const leftHandWorldPosition = new THREE.Vector3().copy(leftGamepad.position);
+ const leftHandLocalPosition = leftHandNode.parent.worldToLocal(leftHandWorldPosition);
+ leftHandNode.position.copy(leftHandLocalPosition);
+ leftHandNode.quaternion.copy(leftGamepad.quaternion);
+ }
+
+ if (rightHandNode) {
+ const rightHandWorldPosition = new THREE.Vector3().copy(rightGamepad.position);
+ const rightHandLocalPosition = rightHandNode.parent.worldToLocal(rightHandWorldPosition);
+ rightHandNode.position.copy(rightHandLocalPosition);
+ rightHandNode.quaternion.copy(rightGamepad.quaternion);
+ }
+
+ // Update the finger rotations based on the gamepad inputs
+ if (this.options.fingers) {
+ this.updateFingers('left', leftGamepad);
+ this.updateFingers('right', rightGamepad);
+ }
+
+ // Update the upper leg rotations based on the IK solver
+ const leftUpperLegNode = this.vrm.humanoid.getRawBoneNode(VRMHumanBoneName.LeftUpperLeg);
+ const rightUpperLegNode = this.vrm.humanoid.getRawBoneNode(VRMHumanBoneName.RightUpperLeg);
+ if (leftUpperLegNode && rightUpperLegNode) {
+ const leftUpperLegRotation = new THREE.Quaternion();
+ const rightUpperLegRotation = new THREE.Quaternion();
+ // Calculate the upper leg rotations based on the IK solver
+ // ...
+ leftUpperLegNode.quaternion.copy(leftUpperLegRotation);
+ rightUpperLegNode.quaternion.copy(rightUpperLegRotation);
+ }
+
+ // Update the lower leg rotations based on the IK solver
+ const leftLowerLegNode = this.vrm.humanoid.getRawBoneNode(VRMHumanBoneName.LeftLowerLeg);
+ const rightLowerLegNode = this.vrm.humanoid.getRawBoneNode(VRMHumanBoneName.RightLowerLeg);
+ if (leftLowerLegNode && rightLowerLegNode) {
+ const leftLowerLegRotation = new THREE.Quaternion();
+ const rightLowerLegRotation = new THREE.Quaternion();
+ // Calculate the lower leg rotations based on the IK solver
+ // ...
+ leftLowerLegNode.quaternion.copy(leftLowerLegRotation);
+ rightLowerLegNode.quaternion.copy(rightLowerLegRotation);
+ }
+
+ // Update the foot positions and rotations based on the IK solver
+ const leftFootNode = this.vrm.humanoid.getRawBoneNode(VRMHumanBoneName.LeftFoot);
+ const rightFootNode = this.vrm.humanoid.getRawBoneNode(VRMHumanBoneName.RightFoot);
+ if (leftFootNode && rightFootNode) {
+ const leftFootPosition = new THREE.Vector3();
+ const rightFootPosition = new THREE.Vector3();
+ const leftFootRotation = new THREE.Quaternion();
+ const rightFootRotation = new THREE.Quaternion();
+ // Calculate the foot positions and rotations based on the IK solver
+ // ...
+ leftFootNode.position.copy(leftFootPosition);
+ leftFootNode.quaternion.copy(leftFootRotation);
+ rightFootNode.position.copy(rightFootPosition);
+ rightFootNode.quaternion.copy(rightFootRotation);
+ }
+ }
+
+ updateFingers(side, gamepad) {
+ const fingerBones = [
+ VRMHumanBoneName.ThumbProximal,
+ VRMHumanBoneName.ThumbIntermediate,
+ VRMHumanBoneName.ThumbDistal,
+ VRMHumanBoneName.IndexProximal,
+ VRMHumanBoneName.IndexIntermediate,
+ VRMHumanBoneName.IndexDistal,
+ VRMHumanBoneName.MiddleProximal,
+ VRMHumanBoneName.MiddleIntermediate,
+ VRMHumanBoneName.MiddleDistal,
+ VRMHumanBoneName.RingProximal,
+ VRMHumanBoneName.RingIntermediate,
+ VRMHumanBoneName.RingDistal,
+ VRMHumanBoneName.LittleProximal,
+ VRMHumanBoneName.LittleIntermediate,
+ VRMHumanBoneName.LittleDistal,
+ ];
+
+ fingerBones.forEach(boneName => {
+ const bone = this.vrm.humanoid.getRawBoneNode(
+ side === 'left' ? `Left${boneName}` : `Right${boneName}`,
+ );
+
+ if (!bone) return;
+
+ const t = side === 'left' ? gamepad.pointer : gamepad.grip;
+ const euler = new THREE.Euler(
+ -t * Math.PI * 0.5,
+ 0,
+ 0,
+ 'YXZ',
+ );
+ bone.quaternion.setFromEuler(euler);
+ });
+ }
+
+ updateVisemes() {
+ if (!this.options.visemes || !this.analyser) return;
+
+ const frequencies = new Uint8Array(this.analyser.frequencyBinCount);
+ this.analyser.getByteFrequencyData(frequencies);
+
+ const volume = frequencies.reduce((sum, f) => sum + f, 0) / frequencies.length;
+
+ if (this.options.muted) {
+ this.microphoneVolume = 0;
+ } else {
+ this.microphoneVolume = volume / 255;
+ }
+
+ const blendShapes = {
+ [VRMSchema.BlendShapePresetName.Aa]: 0,
+ [VRMSchema.BlendShapePresetName.Ih]: 0,
+ [VRMSchema.BlendShapePresetName.Ou]: 0,
+ [VRMSchema.BlendShapePresetName.Ee]: 0,
+ [VRMSchema.BlendShapePresetName.Oh]: 0,
+ };
+
+ if (this.microphoneVolume > 0.2) {
+ const vowels = ['Aa', 'Ih', 'Ou', 'Ee', 'Oh'];
+ const vowelIndex = Math.floor(Math.random() * vowels.length);
+ const vowel = vowels[vowelIndex];
+ blendShapes[VRMSchema.BlendShapePresetName[vowel]] = this.microphoneVolume;
+ }
+
+ Object.entries(blendShapes).forEach(([blendShape, value]) => {
+ this.vrm.blendShapeProxy.setValue(blendShape, value);
+ });
+ }
+}
+
+export { ExokitAvatar };
diff --git a/blocks/environment/components/core/front/ModelObject.js b/blocks/environment/components/core/front/ModelObject.js
index 9159132..e7242f2 100644
--- a/blocks/environment/components/core/front/ModelObject.js
+++ b/blocks/environment/components/core/front/ModelObject.js
@@ -3,7 +3,25 @@ import { useFrame, useLoader, useThree } from "@react-three/fiber";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { FBXLoader } from "three/examples/jsm/loaders/FBXLoader";
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";
-import { AudioListener, Group, Quaternion, VectorKeyframeTrack, QuaternionKeyframeTrack, LoopPingPong, AnimationClip, NumberKeyframeTrack, AnimationMixer, Vector3, BufferGeometry, MeshBasicMaterial, DoubleSide, Mesh, CircleGeometry, sRGBEncoding } from "three";
+import {
+ AudioListener,
+ Group,
+ Quaternion,
+ VectorKeyframeTrack,
+ QuaternionKeyframeTrack,
+ LoopPingPong,
+ AnimationClip,
+ NumberKeyframeTrack,
+ AnimationMixer,
+ Vector3,
+ BufferGeometry,
+ MeshBasicMaterial,
+ DoubleSide,
+ Mesh,
+ CircleGeometry,
+ sRGBEncoding,
+ BoxGeometry
+} from "three";
import { RigidBody } from "@react-three/rapier";
import {
useAnimations,
@@ -13,64 +31,12 @@ import { GLTFAudioEmitterExtension } from "three-omi";
import { GLTFGoogleTiltBrushMaterialExtension } from "three-icosa";
import { VRMUtils, VRMSchema, VRMLoaderPlugin, VRMExpressionPresetName } from "@pixiv/three-vrm";
import idle from "../../../../../inc/avatars/friendly.fbx";
+import { getMixamoRig } from "../../../utils/rigMap";
/**
* A map from Mixamo rig name to VRM Humanoid bone name
*/
-const mixamoVRMRigMap = {
- mixamorigHips: 'hips',
- mixamorigSpine: 'spine',
- mixamorigSpine1: 'chest',
- mixamorigSpine2: 'upperChest',
- mixamorigNeck: 'neck',
- mixamorigHead: 'head',
- mixamorigLeftShoulder: 'leftShoulder',
- mixamorigLeftArm: 'leftUpperArm',
- mixamorigLeftForeArm: 'leftLowerArm',
- mixamorigLeftHand: 'leftHand',
- mixamorigLeftHandThumb1: 'leftThumbMetacarpal',
- mixamorigLeftHandThumb2: 'leftThumbProximal',
- mixamorigLeftHandThumb3: 'leftThumbDistal',
- mixamorigLeftHandIndex1: 'leftIndexProximal',
- mixamorigLeftHandIndex2: 'leftIndexIntermediate',
- mixamorigLeftHandIndex3: 'leftIndexDistal',
- mixamorigLeftHandMiddle1: 'leftMiddleProximal',
- mixamorigLeftHandMiddle2: 'leftMiddleIntermediate',
- mixamorigLeftHandMiddle3: 'leftMiddleDistal',
- mixamorigLeftHandRing1: 'leftRingProximal',
- mixamorigLeftHandRing2: 'leftRingIntermediate',
- mixamorigLeftHandRing3: 'leftRingDistal',
- mixamorigLeftHandPinky1: 'leftLittleProximal',
- mixamorigLeftHandPinky2: 'leftLittleIntermediate',
- mixamorigLeftHandPinky3: 'leftLittleDistal',
- mixamorigRightShoulder: 'rightShoulder',
- mixamorigRightArm: 'rightUpperArm',
- mixamorigRightForeArm: 'rightLowerArm',
- mixamorigRightHand: 'rightHand',
- mixamorigRightHandPinky1: 'rightLittleProximal',
- mixamorigRightHandPinky2: 'rightLittleIntermediate',
- mixamorigRightHandPinky3: 'rightLittleDistal',
- mixamorigRightHandRing1: 'rightRingProximal',
- mixamorigRightHandRing2: 'rightRingIntermediate',
- mixamorigRightHandRing3: 'rightRingDistal',
- mixamorigRightHandMiddle1: 'rightMiddleProximal',
- mixamorigRightHandMiddle2: 'rightMiddleIntermediate',
- mixamorigRightHandMiddle3: 'rightMiddleDistal',
- mixamorigRightHandIndex1: 'rightIndexProximal',
- mixamorigRightHandIndex2: 'rightIndexIntermediate',
- mixamorigRightHandIndex3: 'rightIndexDistal',
- mixamorigRightHandThumb1: 'rightThumbMetacarpal',
- mixamorigRightHandThumb2: 'rightThumbProximal',
- mixamorigRightHandThumb3: 'rightThumbDistal',
- mixamorigLeftUpLeg: 'leftUpperLeg',
- mixamorigLeftLeg: 'leftLowerLeg',
- mixamorigLeftFoot: 'leftFoot',
- mixamorigLeftToeBase: 'leftToes',
- mixamorigRightUpLeg: 'rightUpperLeg',
- mixamorigRightLeg: 'rightLowerLeg',
- mixamorigRightFoot: 'rightFoot',
- mixamorigRightToeBase: 'rightToes',
-};
+const mixamoVRMRigMap = getMixamoRig();
/* global THREE, mixamoVRMRigMap */
@@ -82,10 +48,10 @@ const mixamoVRMRigMap = {
* @returns {Promise} The converted AnimationClip
*/
function loadMixamoAnimation(url, vrm, positionY, positionX, positionZ, scaleX, scaleY, scaleZ, rotationX, rotationY, rotationZ, rotationW) {
- const loader = new FBXLoader(); // A loader which loads FBX
+ const loader = new FBXLoader();
return loader.loadAsync(url).then((asset) => {
- const clip = AnimationClip.findByName(asset.animations, 'mixamo.com'); // extract the AnimationClip
- const tracks = []; // KeyframeTracks compatible with VRM will be added here
+ const clip = AnimationClip.findByName(asset.animations, 'mixamo.com');
+ const tracks = [];
const restRotationInverse = new Quaternion();
const parentRestWorldRotation = new Quaternion();
@@ -169,7 +135,7 @@ function loadMixamoAnimation(url, vrm, positionY, positionX, positionZ, scaleX,
* @return {JSX.Element} The model object.
*/
export function ModelObject(model) {
- const [idleFile, setIdleFile] = useState(model.threeObjectPlugin + idle);
+ const [idleFile, setIdleFile] = useState(idle);
const [clicked, setClickEvent] = useState();
const [url, set] = useState(model.url);
useEffect(() => {
@@ -182,30 +148,41 @@ export function ModelObject(model) {
camera.add(listener);
});
- const gltf = useLoader(GLTFLoader, url, (loader) => {
- const dracoLoader = new DRACOLoader();
- dracoLoader.setDecoderPath( model.threeObjectPluginRoot + "/inc/utils/draco/");
- dracoLoader.setDecoderConfig({type: 'js'}); // (Optional) Override detection of WASM support.
- loader.setDRACOLoader(dracoLoader);
+ let gltf;
- loader.register(
- (parser) => new GLTFAudioEmitterExtension(parser, listener)
- );
- if (openbrushEnabled === true) {
+ try{
+ gltf = useLoader(GLTFLoader, url, (loader) => {
+ const dracoLoader = new DRACOLoader();
+ dracoLoader.setDecoderPath( model.threeObjectPluginRoot + "/inc/utils/draco/");
+ dracoLoader.setDecoderConfig({type: 'js'});
+ loader.setDRACOLoader(dracoLoader);
+
loader.register(
- (parser) =>
- new GLTFGoogleTiltBrushMaterialExtension(
- parser,
- openbrushDirectory
- )
+ (parser) => new GLTFAudioEmitterExtension(parser, listener)
);
- }
- loader.register((parser) => {
- return new VRMLoaderPlugin(parser);
- });
- });
+ if (openbrushEnabled === true) {
+ loader.register(
+ (parser) =>
+ new GLTFGoogleTiltBrushMaterialExtension(
+ parser,
+ openbrushDirectory
+ )
+ );
+ }
+ loader.register((parser) => {
+ return new VRMLoaderPlugin(parser);
+ });
+ });
+ } catch (error) {
+ console.error("Failed to load GLTF file: ", error);
+ // Set gltf to a fallback Three.js object
+ const geometry = new BoxGeometry();
+ const material = new MeshBasicMaterial({color: 0x00ff00});
+ gltf = new Mesh(geometry, material);
+ }
- const audioObject = gltf.scene.getObjectByProperty('type', 'Audio');
+
+ const audioObject = gltf?.scene?.getObjectByProperty('type', 'Audio');
const { actions } = useAnimations(gltf.animations, gltf.scene);
const animationClips = gltf.animations;
@@ -215,13 +192,13 @@ export function ModelObject(model) {
if (animationList) {
animationList.forEach((name) => {
if (Object.keys(actions).includes(name)) {
- console.log(actions[name].play());
+ actions[name].play();
}
});
}
}, []);
- const generator = gltf.asset.generator;
+ const generator = gltf?.asset?.generator;
// return tilt brush if tilt brush
if (String(generator).includes("Tilt Brush")) {
@@ -316,73 +293,58 @@ export function ModelObject(model) {
const circle = new Mesh(geometryCircle, materialCircle);
return circle;
});
-
- if (model.collidable === "1") {
- return (
- {
- setClickEvent(!clicked);
- if (audioObject) {
- if (clicked) {
- audioObject.play();
- triangle.material.visible = false;
- circle.material.visible = false;
- } else {
- audioObject.pause();
- triangle.material.visible = true;
- circle.material.visible = true;
+ if(gltf.scene) {
+ if (model.collidable === "1") {
+ return (
+ {
+ if (audioObject) {
+ setClickEvent(!clicked);
+ if (clicked) {
+ audioObject.play();
+ triangle.material.visible = false;
+ circle.material.visible = false;
+ } else {
+ audioObject.pause();
+ triangle.material.visible = true;
+ circle.material.visible = true;
+ }
}
- }
- }}
- // onCollisionEnter={ ( props ) =>(
- // // window.location.href = model.destinationUrl
- // )
- // }
- >
-
-
+ }}
+ >
+
+
+ );
+ }
+ return (
+ <>
+
+ >
);
}
- return (
- <>
-
- >
- );
}
\ No newline at end of file
diff --git a/blocks/environment/components/core/front/NPCObject.js b/blocks/environment/components/core/front/NPCObject.js
index c551586..b0e547d 100644
--- a/blocks/environment/components/core/front/NPCObject.js
+++ b/blocks/environment/components/core/front/NPCObject.js
@@ -3,7 +3,7 @@ import { useFrame, useLoader, useThree } from "@react-three/fiber";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { FBXLoader } from "three/examples/jsm/loaders/FBXLoader";
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";
-import { AudioListener, Group, Quaternion, VectorKeyframeTrack, QuaternionKeyframeTrack, LoopPingPong, AnimationClip, NumberKeyframeTrack, AnimationMixer, Vector3, BufferGeometry, MeshBasicMaterial, DoubleSide, Mesh, CircleGeometry, sRGBEncoding } from "three";
+import { Color, AudioListener, Group, Quaternion, VectorKeyframeTrack, QuaternionKeyframeTrack, LoopPingPong, AnimationClip, NumberKeyframeTrack, AnimationMixer, Vector3, BufferGeometry, MeshBasicMaterial, DoubleSide, Mesh, CircleGeometry, sRGBEncoding } from "three";
import { RigidBody } from "@react-three/rapier";
import {
useAnimations,
@@ -13,64 +13,12 @@ import { GLTFAudioEmitterExtension } from "three-omi";
import { GLTFGoogleTiltBrushMaterialExtension } from "three-icosa";
import { VRMUtils, VRMSchema, VRMLoaderPlugin, VRMExpressionPresetName, VRMHumanBoneName } from "@pixiv/three-vrm";
import idle from "../../../../../inc/avatars/friendly.fbx";
+import { getMixamoRig } from "../../../utils/rigMap";
/**
* A map from Mixamo rig name to VRM Humanoid bone name
*/
-const mixamoVRMRigMap = {
- mixamorigHips: 'hips',
- mixamorigSpine: 'spine',
- mixamorigSpine1: 'chest',
- mixamorigSpine2: 'upperChest',
- mixamorigNeck: 'neck',
- mixamorigHead: 'head',
- mixamorigLeftShoulder: 'leftShoulder',
- mixamorigLeftArm: 'leftUpperArm',
- mixamorigLeftForeArm: 'leftLowerArm',
- mixamorigLeftHand: 'leftHand',
- mixamorigLeftHandThumb1: 'leftThumbMetacarpal',
- mixamorigLeftHandThumb2: 'leftThumbProximal',
- mixamorigLeftHandThumb3: 'leftThumbDistal',
- mixamorigLeftHandIndex1: 'leftIndexProximal',
- mixamorigLeftHandIndex2: 'leftIndexIntermediate',
- mixamorigLeftHandIndex3: 'leftIndexDistal',
- mixamorigLeftHandMiddle1: 'leftMiddleProximal',
- mixamorigLeftHandMiddle2: 'leftMiddleIntermediate',
- mixamorigLeftHandMiddle3: 'leftMiddleDistal',
- mixamorigLeftHandRing1: 'leftRingProximal',
- mixamorigLeftHandRing2: 'leftRingIntermediate',
- mixamorigLeftHandRing3: 'leftRingDistal',
- mixamorigLeftHandPinky1: 'leftLittleProximal',
- mixamorigLeftHandPinky2: 'leftLittleIntermediate',
- mixamorigLeftHandPinky3: 'leftLittleDistal',
- mixamorigRightShoulder: 'rightShoulder',
- mixamorigRightArm: 'rightUpperArm',
- mixamorigRightForeArm: 'rightLowerArm',
- mixamorigRightHand: 'rightHand',
- mixamorigRightHandPinky1: 'rightLittleProximal',
- mixamorigRightHandPinky2: 'rightLittleIntermediate',
- mixamorigRightHandPinky3: 'rightLittleDistal',
- mixamorigRightHandRing1: 'rightRingProximal',
- mixamorigRightHandRing2: 'rightRingIntermediate',
- mixamorigRightHandRing3: 'rightRingDistal',
- mixamorigRightHandMiddle1: 'rightMiddleProximal',
- mixamorigRightHandMiddle2: 'rightMiddleIntermediate',
- mixamorigRightHandMiddle3: 'rightMiddleDistal',
- mixamorigRightHandIndex1: 'rightIndexProximal',
- mixamorigRightHandIndex2: 'rightIndexIntermediate',
- mixamorigRightHandIndex3: 'rightIndexDistal',
- mixamorigRightHandThumb1: 'rightThumbMetacarpal',
- mixamorigRightHandThumb2: 'rightThumbProximal',
- mixamorigRightHandThumb3: 'rightThumbDistal',
- mixamorigLeftUpLeg: 'leftUpperLeg',
- mixamorigLeftLeg: 'leftLowerLeg',
- mixamorigLeftFoot: 'leftFoot',
- mixamorigLeftToeBase: 'leftToes',
- mixamorigRightUpLeg: 'rightUpperLeg',
- mixamorigRightLeg: 'rightLowerLeg',
- mixamorigRightFoot: 'rightFoot',
- mixamorigRightToeBase: 'rightToes',
-};
+const mixamoVRMRigMap = getMixamoRig();
/* global THREE, mixamoVRMRigMap */
@@ -84,19 +32,19 @@ const mixamoVRMRigMap = {
function loadMixamoAnimation(url, vrm) {
let loader;
if (url.endsWith('.fbx')) {
- loader = new FBXLoader(); // A loader which loads FBX
+ loader = new FBXLoader();
} else {
- loader = new GLTFLoader(); // A loader which loads GLTF
+ loader = new GLTFLoader();
}
return loader.loadAsync(url).then((asset) => {
- const clip = asset.animations[0]; // extract the AnimationClip
+ const clip = asset.animations[0];
// if asset is glb extract the scene
if (url.endsWith('.glb')) {
asset = asset.scene;
}
- const tracks = []; // KeyframeTracks compatible with VRM will be added here
+ const tracks = [];
const restRotationInverse = new Quaternion();
const parentRestWorldRotation = new Quaternion();
@@ -187,7 +135,7 @@ function loadMixamoAnimation(url, vrm) {
* @return {JSX.Element} The model object.
*/
export function NPCObject(model) {
- const [idleFile, setIdleFile] = useState(model.threeObjectPlugin + idle);
+ const [idleFile, setIdleFile] = useState(idle);
const [clicked, setClickEvent] = useState();
const [activeMessage, setActiveMessage] = useState([]);
const headPositionY = useRef([]);
@@ -231,7 +179,7 @@ export function NPCObject(model) {
const gltf = useLoader(GLTFLoader, url, (loader) => {
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath( model.threeObjectPluginRoot + "/inc/utils/draco/");
- dracoLoader.setDecoderConfig({type: 'js'}); // (Optional) Override detection of WASM support.
+ dracoLoader.setDecoderConfig({type: 'js'});
loader.setDRACOLoader(dracoLoader);
loader.register(
@@ -269,7 +217,7 @@ export function NPCObject(model) {
if (animationList) {
animationList.forEach((name) => {
if (Object.keys(actions).includes(name)) {
- console.log(actions[name].play());
+ actions[name].play();
}
});
}
@@ -377,15 +325,20 @@ export function NPCObject(model) {
});
// retarget the animations from mixamo to the current vrm
- if (model.defaultAvatarAnimation){
+ // if model.defaultAvatarAnimation is not empty
+ if (model.defaultAvatarAnimation[0]){
+ // hide the model while we load the animation
+ currentVrm.scene.visible = false;
loadMixamoAnimation(model.defaultAvatarAnimation, currentVrm).then((clip) => {
currentMixer.clipAction(clip).play();
currentMixer.update(clock.getDelta());
+ currentVrm.scene.visible = true;
});
} else {
loadMixamoAnimation(idleFile, currentVrm).then((clip) => {
currentMixer.clipAction(clip).play();
currentMixer.update(clock.getDelta());
+ currentVrm.scene.visible = true;
});
}
@@ -403,18 +356,20 @@ export function NPCObject(model) {
}
let defaultColor = "0xffffff";
let black = "0x000000";
- var colorValue = parseInt ( defaultColor.replace("#","0x"), 16 );
- var blackValue = parseInt ( black.replace("#","0x"), 16 );
+ var colorValue = new Color( parseInt ( defaultColor.replace("#","0x"), 16 ) );
+ var blackValue = new Color( parseInt ( black.replace("#","0x"), 16 ) );
return (
} - The adapted AnimationClip
+ */
+function loadMixamoAnimation(url, vrm) {
+ let loader;
+ if (url.endsWith('.fbx')) {
+ loader = new FBXLoader();
+ } else {
+ loader = new GLTFLoader();
+ }
+ return loader.loadAsync(url).then((resource) => {
+ const clip = resource.animations[0];
+
+ // if resource is GLB, get the scene
+ if (url.endsWith('.glb')) {
+ resource = resource.scene;
+ }
+
+ let tracks = [];
+
+ let restRotationInverse = new THREE.Quaternion();
+ let parentRestWorldRotation = new THREE.Quaternion();
+ let _quatA = new THREE.Quaternion();
+ let _vec3 = new THREE.Vector3();
+
+ // Adjust according to the height of the hips.
+ let mixamoHips = resource.getObjectByName('mixamorigHips');
+ let regularHips = resource.getObjectByName('hips');
+ let mainHip;
+ if (mixamoHips) {
+ mainHip = mixamoHips.position.y;
+ } else if (regularHips) {
+ mainHip = regularHips.position.y;
+ }
+ const vrmHipsY = vrm.humanoid?.getNormalizedBoneNode('hips').getWorldPosition(_vec3).y;
+ const vrmRootY = vrm.scene.getWorldPosition(_vec3).y;
+ const vrmHipsHeight = Math.abs(vrmHipsY - vrmRootY);
+ const hipsPositionScale = vrmHipsHeight / mainHip;
+
+ clip.tracks.forEach((track) => {
+ // Convert each track for VRM usage, and push to `tracks`
+ let trackSplitted = track.name.split('.');
+ let mixamoRigName = trackSplitted[0];
+ let vrmBoneName = mixamoVRMRigMap[mixamoRigName];
+ let vrmNodeName = vrm.humanoid?.getNormalizedBoneNode(vrmBoneName)?.name;
+ let mixamoRigNode = resource.getObjectByName(mixamoRigName);
+
+ if (vrmNodeName != null) {
+
+ let propertyName = trackSplitted[1];
+
+ // Store rotations of rest-pose.
+ mixamoRigNode.getWorldQuaternion(restRotationInverse).invert();
+ mixamoRigNode.parent.getWorldQuaternion(parentRestWorldRotation);
+
+ if (track instanceof THREE.QuaternionKeyframeTrack) {
+
+ // Retarget rotation of mixamoRig to NormalizedBone.
+ for (let i = 0; i < track.values.length; i += 4) {
+
+ let flatQuaternion = track.values.slice(i, i + 4);
+
+ _quatA.fromArray(flatQuaternion);
+
+ _quatA
+ .premultiply(parentRestWorldRotation)
+ .multiply(restRotationInverse);
+
+ _quatA.toArray(flatQuaternion);
+
+ flatQuaternion.forEach((v, index) => {
+
+ track.values[index + i] = v;
+
+ });
+
+ }
+
+ tracks.push(
+ new THREE.QuaternionKeyframeTrack(
+ `${vrmNodeName}.${propertyName}`,
+ track.times,
+ track.values.map((v, i) => (vrm.meta?.metaVersion === '0' && i % 2 === 0 ? - v : v)),
+ ),
+ );
+
+ } else if (track instanceof THREE.VectorKeyframeTrack) {
+ let value = track.values.map((v, i) => (vrm.meta?.metaVersion === '0' && i % 3 !== 1 ? - v : v) * hipsPositionScale);
+ tracks.push(new THREE.VectorKeyframeTrack(`${vrmNodeName}.${propertyName}`, track.times, value));
+ }
+
+ }
+ });
+ return new THREE.AnimationClip('vrmAnimation', clip.duration, tracks);
+
+ });
+}
+
+/**
+ * Represents a participant in a virtual reality scene.
+ *
+ * @param {Object} participant - The props for the participant.
+ *
+ * @return {JSX.Element} The participant.
+ */
+function Participant(participant) {
+ const fallbackURL = defaultVRM;
+ let playerURL = participant.playerVRM;
+ const animationMixerRef = participant.animationMixerRef;
+ const animationsRef = participant.animationsRef;
+ const vrmsRef = participant.vrmsRef;
+ const mixers = participant.mixers;
+ const [someVRM, setSomeVRM] = useState(null);
+ const [frameName, setFrameName] = useState('BackwardIdle');
+ const theScene = useThree();
+ const { gl } = theScene;
+ const displayNameTextRef = useRef(null);
+ const [participantData, setParticipantData] = useState(null);
+ const participantObject = useRef(null);
+ const interpolationDuration = 300; // Adjust this value to control the smoothness
+ const [profileImage, setProfileImage] = useState(null);
+ const lastJumpTimes = useRef({});
+ const height = useRef(1.8);
+
+ useEffect(() => {
+ const textureLoader = new THREE.TextureLoader();
+ textureLoader.crossOrigin = '';
+ textureLoader.load(participant.profileImage, (texture) => {
+ setProfileImage(texture);
+ });
+ }, []);
+
+ // Load the VRM model
+ useEffect(() => {
+ const loader = new GLTFLoader();
+ const ktx2Loader = new KTX2Loader();
+ ktx2Loader.setTranscoderPath(threeObjectPluginRoot + "/inc/utils/basis/");
+ ktx2Loader.detectSupport(gl);
+ loader.setKTX2Loader(ktx2Loader);
+ loader.register(parser => new VRMLoaderPlugin(parser));
+ if (playerURL.endsWith('.png')) {
+ playerURL = blankVRM;
+ }
+
+ loader.load(playerURL, gltf => {
+ setSomeVRM(gltf);
+ });
+ }, [playerURL, gl]);
+
+ useEffect(() => {
+ if (someVRM?.userData?.gltfExtensions?.VRM) {
+ const playerController = someVRM.userData.vrm;
+ vrmsRef.current[participant.playerName] = playerController;
+ playerController.scene.scale.set(1, 1, 1);
+
+ // Animation files
+ const animationFiles = [idle, walk, run, jump];
+ const animationsPromises = animationFiles.map(file => loadMixamoAnimation(file, playerController));
+
+ // Create animation mixer
+ const newMixer = new THREE.AnimationMixer(playerController.scene);
+ animationMixerRef.current[participant.playerName] = newMixer;
+ mixers.current[participant.playerName] = animationMixerRef.current[participant.playerName];
+ participant.profileUserData.current[participant.playerName] = { inWorldName: participant.playerName, pfp: participant.pfp };
+
+ Promise.all(animationsPromises).then(animations => {
+ animationsRef.current[participant.playerName] = animations;
+
+ const idleAction = animationMixerRef.current[participant.playerName].clipAction(animations[0]);
+ const walkAction = animationMixerRef.current[participant.playerName].clipAction(animations[1]);
+ const runAction = animationMixerRef.current[participant.playerName].clipAction(animations[2]);
+ const jumpAction = animationMixerRef.current[participant.playerName].clipAction(animations[3]);
+ walkAction.setEffectiveWeight(0);
+ runAction.setEffectiveWeight(0);
+ jumpAction.setEffectiveWeight(0);
+ idleAction.setEffectiveWeight(1);
+ idleAction.timeScale = 1;
+ idleAction.play();
+ });
+ }
+ }, [someVRM, theScene, participant.p2pcf, participant.playerName, participant.pfp, vrmsRef, animationMixerRef, mixers, animationsRef]);
+
+ useEffect(() => {
+ if (window.p2pcf) {
+ window.p2pcf.on("msg", (peer, data) => {
+ if (!(peer.id in window.participants)) {
+ return;
+ }
+
+ const finalData = new TextDecoder("utf-8").decode(data);
+ const participantData = JSON.parse(finalData);
+ if (participantObject.current) {
+ // Calculate the height of the avatar
+ const box = new THREE.Box3().setFromObject(participantObject.current);
+ height.current = (box.max.y - box.min.y) + (participantData[peer.client_id].isMoving?.action === "jumping" ? 0.5 : 0.1);
+ if (height.current === -Infinity) {
+ height.current = 1.8;
+ }
+
+ if (participantData[peer.client_id]?.position && participantData[peer.client_id]?.rotation) {
+ setParticipantData((prevData) => ({
+ ...prevData,
+ [peer.client_id]: {
+ ...participantData[peer.client_id],
+ position: participantData[peer.client_id].position,
+ rotation: participantData[peer.client_id].rotation,
+ timestamp: Date.now(),
+ },
+ }));
+ }
+ }
+
+ if (animationsRef.current[peer.client_id]) {
+ const idleAction = animationMixerRef.current[peer.client_id].clipAction(animationsRef.current[peer.client_id][0]);
+ const walkAction = animationMixerRef.current[peer.client_id].clipAction(animationsRef.current[peer.client_id][1]);
+ const runAction = animationMixerRef.current[peer.client_id].clipAction(animationsRef.current[peer.client_id][2]);
+ const jumpAction = animationMixerRef.current[peer.client_id].clipAction(animationsRef.current[peer.client_id][3]);
+ if (participantData[peer.client_id].isMoving && participantData[peer.client_id].isMoving.action === "jumping") {
+ if (!jumpAction.isRunning()) {
+ const currentTime = Date.now();
+ const lastJumpTime = lastJumpTimes.current[peer.client_id] || 0;
+ const jumpCooldown = 1000; // Adjust this value to set a cooldown between jumps
+
+ if (currentTime - lastJumpTime >= jumpCooldown) {
+ lastJumpTimes.current[peer.client_id] = currentTime;
+ walkAction.stop();
+ runAction.stop();
+ idleAction.stop();
+ jumpAction.setEffectiveWeight(1);
+ idleAction.setEffectiveWeight(0);
+ runAction.setEffectiveWeight(0);
+ walkAction.setEffectiveWeight(0);
+ jumpAction.reset();
+ jumpAction.setEffectiveTimeScale(1);
+ jumpAction.setLoop(THREE.LoopOnce, 1);
+ jumpAction.clampWhenFinished = true;
+ jumpAction.play();
+ }
+ }
+ } else if (participantData[peer.client_id].isMoving && participantData[peer.client_id].isMoving.action === "walking") {
+ jumpAction.setEffectiveWeight(0);
+ idleAction.setEffectiveWeight(0);
+ runAction.setEffectiveWeight(0);
+ walkAction.setEffectiveWeight(1);
+ jumpAction.stop();
+ walkAction.play();
+ runAction.stop();
+ idleAction.stop();
+ } else if (participantData[peer.client_id].isMoving && participantData[peer.client_id].isMoving.action === "running") {
+ jumpAction.setEffectiveWeight(0);
+ idleAction.setEffectiveWeight(0);
+ runAction.setEffectiveWeight(1);
+ walkAction.setEffectiveWeight(0);
+ walkAction.stop();
+ runAction.play();
+ idleAction.stop();
+ jumpAction.stop();
+ } else {
+ jumpAction.setEffectiveWeight(0);
+ idleAction.setEffectiveWeight(1);
+ runAction.setEffectiveWeight(0);
+ walkAction.setEffectiveWeight(0);
+ idleAction.play();
+ walkAction.stop();
+ runAction.stop();
+ jumpAction.stop();
+ }
+ }
+
+ if (displayNameTextRef.current && participantData[peer.client_id]?.inWorldName) {
+ displayNameTextRef.current.text = participantData[peer.client_id].inWorldName;
+ window.participants[peer.id] = participantData[peer.client_id].inWorldName;
+ } else {
+ window.participants[peer.id] = participantData[peer.client_id].inWorldName;
+ }
+ });
+ }
+ }, [window.p2pcf, animationMixerRef, animationsRef]);
+
+ useFrame((state, delta) => {
+ if (participantObject.current && participantData && participantData[participant.playerName]) {
+ const { position, rotation, timestamp, isMoving } = participantData[participant.playerName];
+ const now = Date.now();
+ const interpolationFactor = isMoving.action === "jumping" ? Math.min((now - timestamp) / 800, 1) : Math.min((now - timestamp) / interpolationDuration, 1);
+
+ if(isMoving?.action === "jumpStop") {
+ console.log("we got a stopper")
+ //statically set the position, no lerp.
+ participantObject.current.parent.position.lerp(new THREE.Vector3(...position), Math.min((now - timestamp) / 100, 1));
+ // set the action to idle
+ setParticipantData((prevData) => ({
+ ...prevData,
+ [participant.playerName]: {
+ ...prevData[participant.playerName],
+ isMoving: false,
+ position: position,
+ },
+ }));
+ } else{
+ participantObject.current.parent.position.lerp(new THREE.Vector3(...position), interpolationFactor);
+ }
+
+ // Convert rotation array to Euler
+ const targetRotation = new THREE.Euler(...rotation);
+ // Create a Quaternion from the target rotation
+ const targetQuaternion = new THREE.Quaternion().setFromEuler(targetRotation);
+ // Slerp the current rotation towards the target rotation
+ participantObject.current.parent.quaternion.slerp(targetQuaternion, interpolationFactor);
+ }
+
+ if (mixers.current[participant.playerName]) {
+ // const idleAction = mixers.current[participant.playerName]._actions.find(action => action._clip.name === 'idle');
+ const idleAction = mixers.current[participant.playerName]._actions[0];
+ const walkAction = mixers.current[participant.playerName]._actions[1];
+ const runAction = mixers.current[participant.playerName]._actions[2];
+ const jumpAction = mixers.current[participant.playerName]._actions[3];
+
+ // if (idleAction && !idleAction.isRunning()) {
+ // idleAction.setEffectiveWeight(1);
+ // jumpAction.setEffectiveWeight(0);
+ // walkAction.setEffectiveWeight(0);
+ // runAction.setEffectiveWeight(0);
+ // idleAction.reset().play();
+ // }
+
+ mixers.current[participant.playerName].update(state.clock.getDelta());
+ }
+ if (mixers.current[participant.playerName]) {
+ mixers.current[participant.playerName].update(delta);
+ }
+
+ if (someVRM?.userData?.vrm) {
+ someVRM.userData.vrm.update(delta);
+ }
+
+ if (someVRM?.userData?.vrm) {
+ someVRM.userData.vrm.update(state.clock.getDelta());
+ }
+
+ });
+
+ if (!someVRM || !someVRM.userData?.gltfExtensions?.VRM) {
+ return null;
+ }
+
+ const playerController = someVRM.userData.vrm;
+ const modelClone = SkeletonUtils.clone(playerController.scene);
+ modelClone.userData.vrm = playerController;
+
+ const displayName = participant.inWorldName ? participant.inWorldName : participant.playerName;
+
+ let planeWidth = 0.25;
+ let fontSize = 0.04;
+ let xPos = 0.045;
+ if (displayName.length > 8) {
+ planeWidth = 0.35;
+ xPos = -0.005;
+ }
+ if (displayName.length >= 16) {
+ planeWidth = 0.35;
+ fontSize = 0.032;
+ xPos = -0.005;
+ }
+
+ const isPng = participant.playerVRM.endsWith('.png');
+ const color = "#000000";
+ const colorValue = new THREE.Color(parseInt(color.replace("#", "0x"), 16));
+
+ return (
+
+
+
+
+
+
+
+
+
+ {displayName}
+
+
+
+ {isPng && (
+
+ )}
+
+ );
+ }
+
+export function Participants(props) {
+ const theScene = useThree();
+ const profileUserData = useRef([]);
+ const animationMixerRef = useRef([]);
+ const animationsRef = useRef([]);
+ const mixers = useRef([]);
+ const vrmsRef = useRef({});
+ const participants = useParticipantsStore(state => state.participants);
+ const addParticipant = useParticipantsStore(state => state.addParticipant);
+ const removeParticipant = useParticipantsStore(state => state.removeParticipant);
+ // const lastJumpTimes = useRef({});
+
+ // useEffect(() => {
+ // if (window.p2pcf) {
+ // window.p2pcf.on("msg", (peer, data) => {
+ // if (!(peer.id in window.participants)) {
+ // return;
+ // }
+
+ // const finalData = new TextDecoder("utf-8").decode(data);
+ // const participantData = JSON.parse(finalData);
+
+ // if (animationsRef.current[peer.client_id]) {
+ // const walkAction = animationMixerRef.current[peer.client_id].clipAction(animationsRef.current[peer.client_id][1]);
+ // const idleAction = animationMixerRef.current[peer.client_id].clipAction(animationsRef.current[peer.client_id][0]);
+ // const runAction = animationMixerRef.current[peer.client_id].clipAction(animationsRef.current[peer.client_id][2]);
+ // const jumpAction = animationMixerRef.current[peer.client_id].clipAction(animationsRef.current[peer.client_id][3]);
+
+ // if (participantData[peer.client_id].isMoving && participantData[peer.client_id].isMoving.action === "jumping") {
+ // walkAction.stop();
+ // runAction.stop();
+ // idleAction.stop();
+
+ // if (!jumpAction.isRunning()) {
+ // const currentTime = Date.now();
+ // const lastJumpTime = lastJumpTimes.current[peer.client_id] || 0;
+ // const jumpCooldown = 1000; // Adjust this value to set a cooldown between jumps
+
+ // if (currentTime - lastJumpTime >= jumpCooldown) {
+ // console.log("Jumping. this should only happen once.");
+ // lastJumpTimes.current[peer.client_id] = currentTime;
+ // jumpAction.reset();
+ // jumpAction.setEffectiveTimeScale(1);
+ // jumpAction.setEffectiveWeight(1);
+ // jumpAction.setLoop(THREE.LoopOnce, 1);
+ // jumpAction.clampWhenFinished = true;
+ // jumpAction.play();
+ // }
+ // }
+ // } else if (participantData[peer.client_id].isMoving && participantData[peer.client_id].isMoving.action === "walking") {
+ // jumpAction.stop();
+ // walkAction.play();
+ // runAction.stop();
+ // idleAction.stop();
+ // } else if (participantData[peer.client_id].isMoving && participantData[peer.client_id].isMoving.action === "running") {
+ // walkAction.stop();
+ // runAction.play();
+ // idleAction.stop();
+ // jumpAction.stop();
+ // } else {
+ // idleAction.play();
+ // walkAction.stop();
+ // runAction.stop();
+ // jumpAction.stop();
+ // }
+ // }
+
+ // if (participantData[peer.client_id]?.inWorldName) {
+ // window.participants[peer.id] = participantData[peer.client_id].inWorldName;
+ // }
+ // });
+ // }
+ // }, [window.p2pcf, animationMixerRef, animationsRef]);
+
+ useEffect(() => {
+ const p2pcf = window.p2pcf;
+ if (p2pcf) {
+ p2pcf.on("peerclose", (peer) => {
+ delete window.participants[peer.id];
+ delete animationMixerRef.current[peer.client_id];
+ delete animationsRef.current[peer.client_id];
+ delete mixers.current[peer.client_id];
+ removeParticipant(peer.client_id);
+ });
+ }
+ }, [removeParticipant, window.p2pcf]);
+
+ useEffect(() => {
+ const p2pcf = window.p2pcf;
+ if (p2pcf) {
+ p2pcf.on("msg", (peer, data) => {
+ if (!(peer.id in window.participants)) {
+ const finalData = new TextDecoder("utf-8").decode(data);
+ const participantData = JSON.parse(finalData);
+ window.participants[peer.id] = "";
+
+ const newParticipant = [peer.client_id, participantData.playerVRM, participantData.inWorldName, participantData.profileImage];
+ addParticipant(newParticipant);
+ }
+ });
+ }
+ }, [window.p2pcf]);
+
+ return (
+ <>
+ {participants && participants.map((item, index) => {
+ const profileImage = item[3];
+ if (profileImage) {
+ return (
+
+ );
+ }
+
+ return null;
+ })}
+ >
+ );
+}
\ No newline at end of file
diff --git a/blocks/environment/components/core/front/Portal.js b/blocks/environment/components/core/front/Portal.js
index 546247c..7dfee11 100644
--- a/blocks/environment/components/core/front/Portal.js
+++ b/blocks/environment/components/core/front/Portal.js
@@ -118,10 +118,11 @@ export function Portal(model) {
lockZ={false} // Lock the rotation on the z axis (default=false)
>
{
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath( model.threeObjectPluginRoot + "/inc/utils/draco/");
- dracoLoader.setDecoderConfig({type: 'js'}); // (Optional) Override detection of WASM support.
+ dracoLoader.setDecoderConfig({type: 'js'});
loader.setDRACOLoader(dracoLoader);
loader.register(
@@ -214,6 +215,7 @@ export function Portal(model) {
scale={[model.scaleX, model.scaleY, model.scaleZ]}
>
{model.textContent}
diff --git a/blocks/environment/components/core/front/ThreeAudio.js b/blocks/environment/components/core/front/ThreeAudio.js
index d4a015e..4ec3f47 100644
--- a/blocks/environment/components/core/front/ThreeAudio.js
+++ b/blocks/environment/components/core/front/ThreeAudio.js
@@ -32,50 +32,50 @@ import {
*
* @returns {JSX.Element} - Returns a JSX element containing a Three.js primitive object (Audio/PositionalAudio).
*/
-export function ThreeAudio(threeAudio) {
- const { camera } = useThree();
- const [audio, setAudio] = useState(null);
-
- useEffect(() => {
- const listener = new AudioListener();
- camera.add(listener);
-
- // Create either a PositionalAudio object or a normal Audio object based on the positional attribute
- const audio = threeAudio.positional === "1" ? new PositionalAudio(listener) : new Audio(listener);
-
- if (threeAudio.audioUrl) {
- const audioLoader = new AudioLoader();
- audioLoader.load(threeAudio.audioUrl, (buffer) => {
- audio.setBuffer(buffer);
- audio.setLoop(threeAudio.loop === "1" ? true : false);
- audio.setVolume(threeAudio.volume);
- if (threeAudio.autoPlay === "1") audio.play();
- });
- }
-
- if (threeAudio.positional === "1") {
- audio.refDistance = threeAudio.refDistance;
- audio.maxDistance = threeAudio.maxDistance;
- audio.rolloffFactor = threeAudio.rolloffFactor;
- audio.coneInnerAngle = threeAudio.coneInnerAngle;
- audio.coneOuterAngle = threeAudio.coneOuterAngle;
- audio.coneOuterGain = threeAudio.coneOuterGain;
- audio.distanceModel = threeAudio.distanceModel;
- audio.position.set(threeAudio.positionX, threeAudio.positionY, threeAudio.positionZ);
- audio.rotation.set(threeAudio.rotationX, threeAudio.rotationY, threeAudio.rotationZ);
- }
-
- setAudio(audio);
-
- return () => {
- audio.stop();
- camera.remove(listener);
- }
- }, []);
-
- return (
- <>
- {audio && }
- >
- );
-}
+export function ThreeAudio({ threeAudio, onLoad }) {
+ const { camera } = useThree();
+ const [audio, setAudio] = useState(null);
+
+ useEffect(() => {
+ console.log("ThreeAudio: Setting up audio", threeAudio);
+ const listener = new AudioListener();
+ camera.add(listener);
+
+ const audio = threeAudio.positional === "1" ? new PositionalAudio(listener) : new Audio(listener);
+
+ if (threeAudio.audioUrl) {
+ console.log("ThreeAudio: Loading audio from URL", threeAudio.audioUrl);
+ const audioLoader = new AudioLoader();
+ audioLoader.load(threeAudio.audioUrl, (buffer) => {
+ console.log("ThreeAudio: Audio loaded", threeAudio.audioUrl);
+ audio.setBuffer(buffer);
+ audio.setLoop(threeAudio.loop === "1");
+ audio.setVolume(threeAudio.volume);
+ audio.userData = { ...threeAudio }; // Store all props in userData
+ onLoad(audio);
+ setAudio(audio);
+ });
+ }
+
+ if (threeAudio.positional === "1") {
+ audio.refDistance = threeAudio.refDistance;
+ audio.maxDistance = threeAudio.maxDistance;
+ audio.rolloffFactor = threeAudio.rolloffFactor;
+ audio.coneInnerAngle = threeAudio.coneInnerAngle;
+ audio.coneOuterAngle = threeAudio.coneOuterAngle;
+ audio.coneOuterGain = threeAudio.coneOuterGain;
+ audio.distanceModel = threeAudio.distanceModel;
+ audio.position.set(threeAudio.positionX, threeAudio.positionY, threeAudio.positionZ);
+ audio.rotation.set(threeAudio.rotationX, threeAudio.rotationY, threeAudio.rotationZ);
+ }
+
+ return () => {
+ if (audio) audio.stop();
+ camera.remove(listener);
+ }
+ }, []);
+
+ return audio ? : null;
+ }
+
+
\ No newline at end of file
diff --git a/blocks/environment/components/core/front/ThreeImage.js b/blocks/environment/components/core/front/ThreeImage.js
index f3bd59c..16f4f1e 100644
--- a/blocks/environment/components/core/front/ThreeImage.js
+++ b/blocks/environment/components/core/front/ThreeImage.js
@@ -13,6 +13,7 @@ export function ThreeImage(threeImage) {
const texture2 = useLoader(TextureLoader, threeImage.url);
return (
+ Object.assign(document.createElement("video"), {
+ src: threeVideo.url,
+ crossOrigin: "Anonymous",
+ loop: true,
+ muted: true
+ })
+);
+const [audio, setAudio] = useState(null);
- const [video] = useState(() =>
- Object.assign(document.createElement("video"), {
- src: threeVideo.url,
- crossOrigin: "Anonymous",
- loop: true,
- muted: true
- })
- );
- const [audio, setAudio] = useState(null);
+const gltf = (threeVideo.customModel === "1") ? useLoader(GLTFLoader, threeVideo.modelUrl, (loader) => {
+ const dracoLoader = new DRACOLoader();
+ dracoLoader.setDecoderPath( threeVideo.threeObjectPluginRoot + "/inc/utils/draco/");
+ dracoLoader.setDecoderConfig({type: 'js'});
+ loader.setDRACOLoader(dracoLoader);
- const gltf = (threeVideo.customModel === "1") ? useLoader(GLTFLoader, threeVideo.modelUrl, (loader) => {
- const dracoLoader = new DRACOLoader();
- dracoLoader.setDecoderPath( threeVideo.threeObjectPluginRoot + "/inc/utils/draco/");
- dracoLoader.setDecoderConfig({type: 'js'});
- loader.setDRACOLoader(dracoLoader);
-
- loader.register((parser) => {
- return new VRMLoaderPlugin(parser);
- });
- }) : null;
+ loader.register((parser) => {
+ return new VRMLoaderPlugin(parser);
+ });
+}) : null;
- useEffect(() => {
- const listener = new AudioListener();
- camera.add(listener);
- const positionalAudio = new PositionalAudio(listener);
+useEffect(() => {
+ const listener = new AudioListener();
+ camera.add(listener);
+ const positionalAudio = new PositionalAudio(listener);
const audioPosition = [
Number(threeVideo.positionX),
Number(threeVideo.positionY),
@@ -63,55 +63,55 @@ export function ThreeVideo(threeVideo) {
Number(threeVideo.rotationY),
Number(threeVideo.rotationZ)
];
- if (threeVideo.url) {
- const audioLoader = new AudioLoader();
- positionalAudio.refDistance = 5;
- positionalAudio.maxDistance = 10000;
- positionalAudio.rolloffFactor = 5;
- positionalAudio.coneInnerAngle = 360;
- positionalAudio.coneOuterAngle = 0;
- positionalAudio.coneOuterGain = 0.8;
- positionalAudio.distanceModel = "inverse";
- audioLoader.load(threeVideo.url, (buffer) => {
- positionalAudio.setBuffer(buffer);
- positionalAudio.setLoop(true);
- if (play) positionalAudio.play();
- });
- }
- setAudio(positionalAudio);
+ if (threeVideo.url) {
+ const audioLoader = new AudioLoader();
+ positionalAudio.refDistance = 5;
+ positionalAudio.maxDistance = 10000;
+ positionalAudio.rolloffFactor = 5;
+ positionalAudio.coneInnerAngle = 360;
+ positionalAudio.coneOuterAngle = 0;
+ positionalAudio.coneOuterGain = 0.8;
+ positionalAudio.distanceModel = "inverse";
+ audioLoader.load(threeVideo.url, (buffer) => {
+ positionalAudio.setBuffer(buffer);
+ positionalAudio.setLoop(true);
+ if (play) positionalAudio.play();
+ });
+ }
+ setAudio(positionalAudio);
- return () => {
- positionalAudio.stop();
- camera.remove(listener);
- }
- }, []);
+ return () => {
+ positionalAudio.stop();
+ camera.remove(listener);
+ }
+}, []);
- useEffect(() => {
- if (threeVideo.customModel === "1" && gltf && audio) {
- if (gltf.scene) {
- let foundScreen;
- gltf.scene.traverse((child) => {
- if (child.name === "screen") {
- foundScreen = child;
- }
- });
- if (foundScreen) {
- setScreen(foundScreen);
- setScreenParent(foundScreen.parent);
- const videoTexture = new VideoTexture(video);
- videoTexture.encoding = sRGBEncoding;
+useEffect(() => {
+ if (threeVideo.customModel === "1" && gltf && audio) {
+ if (gltf.scene) {
+ let foundScreen;
+ gltf.scene.traverse((child) => {
+ if (child.name === "screen") {
+ foundScreen = child;
+ }
+ });
+ if (foundScreen) {
+ setScreen(foundScreen);
+ setScreenParent(foundScreen.parent);
+ const videoTexture = new VideoTexture(video);
+ videoTexture.colorSpace = sRGBEncoding;
- // new mesh standard material with the map texture
- const material = new MeshStandardMaterial({
+ // new mesh standard material with the map texture
+ const material = new MeshStandardMaterial({
map: videoTexture,
side: DoubleSide
- });
- foundScreen.material = material;
- foundScreen.add(audio);
- }
- }
- }
- }, [gltf, audio]);
+ });
+ foundScreen.material = material;
+ foundScreen.add(audio);
+ }
+ }
+ }
+}, [gltf, audio]);
// Add a triangle mesh on top of the video
const [triangle] = useState(() => {
@@ -152,57 +152,62 @@ export function ThreeVideo(threeVideo) {
}, [video, play]);
return (
{
- if (e.length !== 0) {
- setClickEvent(!clicked);
- if (clicked) {
- video.play();
- audio.play();
- triangle.material.visible = false;
- circle.material.visible = false;
- } else {
- video.pause();
- audio.pause();
- triangle.material.visible = true;
- circle.material.visible = true;
- }
+ box
+ onChangePointerUp={(e) => {
+ if(videoControlsEnabled){
+ if (e.length !== 0) {
+ setClickEvent(!clicked);
+ if (clicked) {
+ video.play();
+ audio.play();
+ triangle.material.visible = false;
+ circle.material.visible = false;
+ } else {
+ video.pause();
+ audio.pause();
+ triangle.material.visible = true;
+ circle.material.visible = true;
+ }
+ }
}
- }}
- filter={(items) => items}
+ }}
+ filter={items => items}
>
-
+ >
{audio && }
{threeVideo.customModel === "1" && gltf ? (
-
+
) : (
{
- setClickEvent(!clicked);
- if (clicked) {
- video.play();
- triangle.material.visible = false;
- circle.material.visible = false;
- } else {
- video.pause();
- triangle.material.visible = true;
- circle.material.visible = true;
+ if(videoControlsEnabled){
+ setClickEvent(!clicked);
+ if (clicked) {
+ video.play();
+ triangle.material.visible = false;
+ circle.material.visible = false;
+ } else {
+ video.pause();
+ triangle.material.visible = true;
+ circle.material.visible = true;
+ }
}
}}
>
@@ -223,9 +228,9 @@ export function ThreeVideo(threeVideo) {
)}
-
-
-
-
- );
+
+
+
+
+);
}
diff --git a/blocks/environment/components/core/front/utils/participantsStore.js b/blocks/environment/components/core/front/utils/participantsStore.js
new file mode 100644
index 0000000..13ecb9d
--- /dev/null
+++ b/blocks/environment/components/core/front/utils/participantsStore.js
@@ -0,0 +1,13 @@
+// participantsStore.js
+import create from 'zustand'
+
+const useParticipantsStore = create((set) => ({
+ participants: [],
+ setParticipants: (participants) => set({ participants }),
+ addParticipant: (participant) => set((state) => ({ participants: [...state.participants, participant] })),
+ removeParticipant: (clientId) => set((state) => ({
+ participants: state.participants.filter((item) => item[0] !== clientId)
+ })),
+}))
+
+export default useParticipantsStore;
diff --git a/blocks/environment/components/p2pcf/p2pcf.bak b/blocks/environment/components/p2pcf/p2pcf.bak
new file mode 100644
index 0000000..049f460
--- /dev/null
+++ b/blocks/environment/components/p2pcf/p2pcf.bak
@@ -0,0 +1,1459 @@
+/**
+ * Peer 2 Peer WebRTC connections with Cloudflare Workers as signalling server
+ * Copyright Greg Fodor
+ * Licensed under MIT
+ */
+
+/* global crypto */
+
+import getBrowserRTC from "get-browser-rtc";
+import EventEmitter from "events";
+import Peer from "tiny-simple-peer";
+import {
+ encode as arrayBufferToBase64,
+ decode as base64ToArrayBuffer
+} from "base64-arraybuffer";
+import { hexToBytes } from "convert-hex";
+import arrayBufferToHex from "array-buffer-to-hex";
+import defaultVRM from "../../../../inc/avatars/3ov_default_avatar.vrm";
+
+let validlyInRoom = false;
+// Based on Chrome
+const MAX_MESSAGE_LENGTH_BYTES = 16000;
+
+const CHUNK_HEADER_LENGTH_BYTES = 12;
+const CHUNK_MAGIC_WORD = 8121;
+const CHUNK_MAX_LENGTH_BYTES =
+ MAX_MESSAGE_LENGTH_BYTES - CHUNK_HEADER_LENGTH_BYTES;
+
+// Signalling messages have a 64-bit unique header
+const SIGNAL_MESSAGE_HEADER_WORDS = [0x82ab, 0x81cd, 0x1295, 0xa1cb];
+
+const CANDIDATE_TYPES = {
+ host: 0,
+ srflx: 1,
+ relay: 2
+};
+
+const CANDIDATE_TCP_TYPES = {
+ active: 0,
+ passive: 1,
+ so: 2
+};
+
+const CANDIDATE_IDX = {
+ TYPE: 0,
+ PROTOCOL: 1,
+ IP: 2,
+ PORT: 3,
+ RELATED_IP: 4,
+ RELATED_PORT: 5,
+ TCP_TYPE: 6
+};
+
+const DEFAULT_STUN_ICE = [
+ { urls: "stun:stun1.l.google.com:19302" },
+ { urls: "stun:global.stun.twilio.com:3478" }
+];
+
+const DEFAULT_TURN_ICE = [
+ {
+ urls: "turn:openrelay.metered.ca:80",
+ username: "openrelayproject",
+ credential: "openrelayproject"
+ },
+ {
+ urls: "turn:openrelay.metered.ca:443",
+ username: "openrelayproject",
+ credential: "openrelayproject"
+ },
+ {
+ urls: "turn:openrelay.metered.ca:443?transport=tcp",
+ username: "openrelayproject",
+ credential: "openrelayproject"
+ }
+];
+
+const randomstring = (len) => {
+ const bytes = crypto.getRandomValues(new Uint8Array(len));
+ const str = bytes.reduce((accum, v) => accum + String.fromCharCode(v), "");
+ return btoa(str).replaceAll("=", "");
+};
+
+const textDecoder = new TextDecoder("utf-8");
+const textEncoder = new TextEncoder();
+
+const arrToText = textDecoder.decode.bind(textDecoder);
+const textToArr = textEncoder.encode.bind(textEncoder);
+
+const removeInPlace = (a, condition) => {
+ let i = 0;
+ let j = 0;
+
+ while (i < a.length) {
+ const val = a[i];
+ if (!condition(val, i, a)) a[j++] = val;
+ i++;
+ }
+
+ a.length = j;
+ return a;
+};
+
+const ua = window.navigator.userAgent;
+const iOS = !!ua.match(/iPad/i) || !!ua.match(/iPhone/i);
+const webkit = !!ua.match(/WebKit/i);
+const iOSSafari = !!(iOS && webkit && !ua.match(/CriOS/i));
+const isFirefox = !!(
+ navigator?.userAgent.toLowerCase().indexOf("firefox") > -1
+);
+
+const hexToBase64 = (hex) => arrayBufferToBase64(hexToBytes(hex));
+const base64ToHex = (b64) => arrayBufferToHex(base64ToArrayBuffer(b64));
+
+function createSdp(isOffer, iceUFrag, icePwd, dtlsFingerprintBase64) {
+ const dtlsHex = base64ToHex(dtlsFingerprintBase64);
+ let dtlsFingerprint = "";
+
+ for (let i = 0; i < dtlsHex.length; i += 2) {
+ dtlsFingerprint += `${dtlsHex[i]}${dtlsHex[i + 1]}${
+ i === dtlsHex.length - 2 ? "" : ":"
+ }`.toUpperCase();
+ }
+
+ const sdp = [
+ "v=0",
+ "o=- 5498186869896684180 2 IN IP4 127.0.0.1",
+ "s=-",
+ "t=0 0",
+ "a=msid-semantic: WMS",
+ "m=application 9 UDP/DTLS/SCTP webrtc-datachannel",
+ "c=IN IP4 0.0.0.0",
+ "a=mid:0",
+ "a=sctp-port:5000"
+ ];
+
+ if (isOffer) {
+ sdp.push("a=setup:actpass");
+ } else {
+ sdp.push("a=setup:active");
+ }
+
+ sdp.push(`a=ice-ufrag:${iceUFrag}`);
+ sdp.push(`a=ice-pwd:${icePwd}`);
+ sdp.push(`a=fingerprint:sha-256 ${dtlsFingerprint}`);
+
+ return sdp.join("\r\n") + "\r\n";
+}
+
+// parseCandidate from https://github.com/fippo/sdp
+const parseCandidate = (line) => {
+ let parts;
+
+ // Parse both variants.
+ if (line.indexOf("a=candidate:") === 0) {
+ parts = line.substring(12).split(" ");
+ } else {
+ parts = line.substring(10).split(" ");
+ }
+
+ const candidate = [
+ CANDIDATE_TYPES[parts[7]], // type
+ parts[2].toLowerCase() === "udp" ? 0 : 1, // protocol
+ parts[4], // ip
+ parseInt(parts[5], 10) // port
+ ];
+
+ for (let i = 8; i < parts.length; i += 2) {
+ switch (parts[i]) {
+ case "raddr":
+ while (candidate.length < 5) candidate.push(null);
+ candidate[4] = parts[i + 1];
+ break;
+ case "rport":
+ while (candidate.length < 6) candidate.push(null);
+ candidate[5] = parseInt(parts[i + 1], 10);
+ break;
+ case "tcptype":
+ while (candidate.length < 7) candidate.push(null);
+ candidate[6] = CANDIDATE_TCP_TYPES[parts[i + 1]];
+ break;
+ default:
+ // Unknown extensions are silently ignored.
+ break;
+ }
+ }
+
+ while (candidate.length < 8) candidate.push(null);
+ candidate[7] = parseInt(parts[3], 10);
+
+ return candidate;
+};
+
+export default class P2PCF extends EventEmitter {
+ constructor(clientId = "", roomId = "", options = {}) {
+ super();
+
+ if (!clientId || clientId.length < 4) {
+ throw new Error("Client ID must be at least four characters");
+ }
+
+ if (!roomId || roomId.length < 4) {
+ throw new Error("Room ID must be at least four characters");
+ }
+
+ this._step = this._step.bind(this);
+
+ this.peers = new Map();
+ this.msgChunks = new Map();
+ this.connectedSessions = [];
+ this.clientId = clientId;
+ this.roomId = roomId;
+ this.sessionId = randomstring(20);
+ this.packages = [];
+ this.dataTimestamp = null;
+ this.lastPackages = null;
+ this.lastProcessedReceivedDataTimestamps = new Map();
+ this.packageReceivedFromPeers = new Set();
+ this.startedAtTimestamp = null;
+ this.peerOptions = options.rtcPeerConnectionOptions || {};
+ this.peerProprietaryConstraints =
+ options.rtcPeerConnectionProprietaryConstraints || {};
+ this.peerSdpTransform = options.sdpTransform || ((sdp) => sdp);
+
+ this.workerUrl =
+ options.workerUrl || "https://p2pcf.sxp.digital";
+
+ if (this.workerUrl.endsWith("/")) {
+ this.workerUrl = this.workerUrl.substring(
+ 0,
+ this.workerUrl.length - 1
+ );
+ }
+
+ this.stunIceServers = options.stunIceServers || DEFAULT_STUN_ICE;
+ this.turnIceServers = options.turnIceServers || DEFAULT_TURN_ICE;
+ this.networkChangePollIntervalMs =
+ options.networkChangePollIntervalMs || 15000;
+
+ this.stateExpirationIntervalMs =
+ options.stateExpirationIntervalMs || 2 * 60 * 1000;
+ this.stateHeartbeatWindowMs = options.stateHeartbeatWindowMs || 30000;
+
+ this.fastPollingDurationMs = options.fastPollingDurationMs || 10000;
+ this.fastPollingRateMs = options.fastPollingRateMs || 750;
+ this.slowPollingRateMs = options.slowPollingRateMs || 1500;
+ this.participantLimit = options.participantLimit || 8;
+
+ this.wrtc = getBrowserRTC();
+ this.dtlsCert = null;
+ this.udpEnabled = null;
+ this.isSymmetric = null;
+ this.dtlsFingerprint = null;
+ this.reflexiveIps = new Set();
+
+ // step
+ this.isSending = false;
+ this.finished = false;
+ this.nextStepTime = -1;
+ this.deleteKey = null;
+ this.sentFirstPoll = false;
+ this.stopFastPollingAt = -1;
+
+ // ContextID is maintained across page refreshes
+ if (!window.history.state?._p2pcfContextId) {
+ window.history.replaceState(
+ {
+ ...window.history.state,
+ _p2pcfContextId: randomstring(20)
+ },
+ window.location.href
+ );
+ }
+
+ this.contextId = window.history.state._p2pcfContextId;
+ }
+
+ async _init() {
+ if (this.dtlsCert === null) {
+ this.dtlsCert =
+ await this.wrtc.RTCPeerConnection.generateCertificate({
+ name: "ECDSA",
+ namedCurve: "P-256"
+ });
+ }
+ }
+
+ async _step(finish = false) {
+ const {
+ sessionId,
+ clientId,
+ roomId,
+ contextId,
+ stateExpirationIntervalMs,
+ stateHeartbeatWindowMs,
+ packages,
+ fastPollingDurationMs,
+ fastPollingRateMs,
+ slowPollingRateMs,
+ participantLimit,
+ } = this;
+
+ const now = Date.now();
+
+ if (finish) {
+ if (this.finished) return;
+ if (!this.deleteKey) return;
+ this.finished = true;
+ } else {
+ if (this.nextStepTime > now) return;
+ if (this.isSending) return;
+ if (this.reflexiveIps.length === 0) return;
+ }
+
+ this.isSending = true;
+
+ try {
+ const localDtlsFingerprintBase64 = hexToBase64(
+ this.dtlsFingerprint.replaceAll(":", "")
+ );
+
+ const localPeerInfo = [
+ sessionId,
+ clientId,
+ this.isSymmetric,
+ localDtlsFingerprintBase64,
+ this.startedAtTimestamp,
+ [...this.reflexiveIps],
+ ];
+
+ const payload = { r: roomId, k: contextId };
+
+ if (finish) {
+ payload.dk = this.deleteKey;
+ }
+
+ const expired =
+ this.dataTimestamp === null ||
+ now - this.dataTimestamp >=
+ stateExpirationIntervalMs - stateHeartbeatWindowMs;
+
+ const packagesChanged =
+ this.lastPackages !== JSON.stringify(packages);
+ let includePackages = false;
+
+ if (expired || packagesChanged || finish) {
+ // This will force a write
+ this.dataTimestamp = now;
+
+ // Compact packages, expire any of them sent more than a minute ago.
+ // (ICE will timeout by then, even if other latency fails us.)
+ removeInPlace(packages, (pkg) => {
+ const sentAt = pkg[pkg.length - 2];
+ return now - sentAt > 60 * 1000;
+ });
+
+ includePackages = true;
+ }
+
+ if (finish) {
+ includePackages = false;
+ }
+
+ // The first poll should just be a read, no writes, to build up packages before we do a write
+ // to reduce worker I/O. So don't include the data + packages on the first request.
+ if (this.sentFirstPoll) {
+ payload.d = localPeerInfo;
+ payload.t = this.dataTimestamp;
+ payload.x = this.stateExpirationIntervalMs;
+
+ if (includePackages) {
+ payload.p = packages;
+ this.lastPackages = JSON.stringify(packages);
+ }
+ }
+ const body = JSON.stringify(payload);
+ const headers = { "Content-Type": "application/json " };
+ let keepalive = false;
+
+ if (finish) {
+ headers["X-Worker-Method"] = "DELETE";
+ keepalive = true;
+ }
+
+ const res = await fetch(this.workerUrl, {
+ method: "POST",
+ headers,
+ body,
+ keepalive
+ });
+
+ const {
+ ps: remotePeerDatas,
+ pk: remotePackages,
+ dk
+ } = await res.json();
+
+ if (dk) {
+ this.deleteKey = dk;
+ }
+
+ if (finish) return;
+
+ // Slight optimization: if the peers are empty on the first poll, immediately publish data to reduce
+ // delay before first peers show up.
+ if (remotePeerDatas.length === 0 && !this.sentFirstPoll) {
+ payload.d = localPeerInfo;
+ payload.t = this.dataTimestamp;
+ payload.x = this.stateExpirationIntervalMs;
+ payload.p = packages;
+ this.lastPackages = JSON.stringify(packages);
+ const res = await fetch(this.workerUrl, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(payload)
+ });
+
+ const { dk } = await res.json();
+
+ if (dk) {
+ this.deleteKey = dk;
+ }
+ }
+
+ this.sentFirstPoll = true;
+
+ const previousPeerSessionIds = [...this.peers.keys()];
+
+ this._handleWorkerResponse(
+ localPeerInfo,
+ localDtlsFingerprintBase64,
+ packages,
+ remotePeerDatas,
+ remotePackages,
+ participantLimit
+ );
+
+ const activeSessionIds = remotePeerDatas.map((p) => p[0]);
+
+ const peersChanged =
+ previousPeerSessionIds.length !== activeSessionIds.length ||
+ activeSessionIds.find(
+ (c) => !previousPeerSessionIds.includes(c)
+ ) ||
+ previousPeerSessionIds.find(
+ (c) => !activeSessionIds.includes(c)
+ );
+
+ // Rate limit requests when room is empty, or look for new joins
+ // Go faster when things are changing to avoid ICE timeouts
+ if (peersChanged) {
+ this.stopFastPollingAt = now + fastPollingDurationMs;
+ }
+
+ if (now < this.stopFastPollingAt) {
+ this.nextStepTime = now + fastPollingRateMs;
+ } else {
+ this.nextStepTime = now + slowPollingRateMs;
+ }
+ } catch (e) {
+ console.error(e);
+ this.nextStepTime = now + slowPollingRateMs;
+ } finally {
+ this.isSending = false;
+ }
+ }
+
+ _handleWorkerResponse(
+ localPeerData,
+ localDtlsFingerprintBase64,
+ localPackages,
+ remotePeerDatas,
+ remotePackages,
+ participantLimit
+ ) {
+ const localStartedAtTimestamp = this.startedAtTimestamp;
+
+ const {
+ dtlsCert: localDtlsCert,
+ peers,
+ lastProcessedReceivedDataTimestamps,
+ packageReceivedFromPeers,
+ stunIceServers,
+ turnIceServers
+ } = this;
+ const [localSessionId, , localSymmetric] = localPeerData;
+
+ const now = Date.now();
+
+ for (const remotePeerData of remotePeerDatas) {
+ const [
+ remoteSessionId,
+ remoteClientId,
+ remoteSymmetric,
+ remoteDtlsFingerprintBase64,
+ remoteStartedAtTimestamp,
+ remoteReflexiveIps,
+ remoteDataTimestamp,
+ ] = remotePeerData;
+
+ // Don't process the same messages twice. This covers disconnect cases where stale data re-creates a peer too early.
+ if (
+ lastProcessedReceivedDataTimestamps.get(remoteSessionId) ===
+ remoteDataTimestamp
+ ) {
+ continue;
+ }
+
+ // Peer A is:
+ // - if both not symmetric or both symmetric, whoever has the most recent data is peer A, since we want Peer B created faster,
+ // and latency will be lowest with older data.
+ // - if one is and one isn't, the non symmetric one is the only one who has valid candidates, so the symmetric one is peer A
+ const isPeerA =
+ localSymmetric === remoteSymmetric
+ ? localStartedAtTimestamp === remoteStartedAtTimestamp
+ ? localSessionId > remoteSessionId
+ : localStartedAtTimestamp > remoteStartedAtTimestamp
+ : localSymmetric;
+
+ // If either side is symmetric, use TURN and hope we avoid connecting via relays
+ // We can't just use TURN if both sides are symmetric because one side might be port restricted and hence won't connect without a relay.
+ const iceServers =
+ localSymmetric || remoteSymmetric
+ ? turnIceServers
+ : stunIceServers;
+
+ // Firefox answer side is very aggressive with ICE timeouts, so always delay answer set until second candidates received.
+ const delaySetRemoteUntilReceiveCandidates = isFirefox;
+ const remotePackage = remotePackages.find(
+ (p) => p[1] === remoteSessionId
+ );
+
+ const peerOptions = { ...this.peerOptions, iceServers };
+
+ if (localDtlsCert) {
+ peerOptions.certificates = [localDtlsCert];
+ }
+
+ if (isPeerA) {
+ if (peers.has(remoteSessionId)) continue;
+ if (!remotePackage) continue;
+
+ lastProcessedReceivedDataTimestamps.set(
+ remoteSessionId,
+ remoteDataTimestamp
+ );
+
+ // If we already added the candidates from B, skip. This check is not strictly necessary given the peer will exist.
+ if (packageReceivedFromPeers.has(remoteSessionId)) continue;
+ packageReceivedFromPeers.add(remoteSessionId);
+
+ // - I create PC
+ // - I create an answer SDP, and munge the ufrag
+ // - Set local description with answer
+ // - Set remote description via the received sdp
+ // - Add the ice candidates
+
+ const [
+ ,
+ ,
+ remoteIceUFrag,
+ remoteIcePwd,
+ remoteDtlsFingerprintBase64,
+ localIceUFrag,
+ localIcePwd,
+ ,
+ remoteCandidates
+ ] = remotePackage;
+
+ const peer = new Peer({
+ config: peerOptions,
+ initiator: false,
+ iceCompleteTimeout: 3000,
+ proprietaryConstraints: this.peerProprietaryConstraints,
+ sdpTransform: (sdp) => {
+ const lines = [];
+
+ for (const l of sdp.split("\r\n")) {
+ if (l.startsWith("a=ice-ufrag")) {
+ lines.push(`a=ice-ufrag:${localIceUFrag}`);
+ } else if (l.startsWith("a=ice-pwd")) {
+ lines.push(`a=ice-pwd:${localIcePwd}`);
+ } else {
+ lines.push(l);
+ }
+ }
+
+ return this.peerSdpTransform(lines.join("\r\n"));
+ }
+ });
+
+ peer.id = remoteSessionId;
+ peer.client_id = remoteClientId;
+
+
+ this._wireUpCommonPeerEvents(peer, participantLimit);
+
+ peers.set(peer.id, peer);
+
+ // Special case if both behind sym NAT or other hole punching isn't working: peer A needs to send its candidates as well.
+ const pkg = [
+ remoteSessionId,
+ localSessionId,
+ /* lfrag */ null,
+ /* lpwd */ null,
+ /* ldtls */ null,
+ /* remote ufrag */ null,
+ /* remote Pwd */ null,
+ now,
+ []
+ ];
+
+ const pkgCandidates = pkg[pkg.length - 1];
+
+ const initialCandidateSignalling = (e) => {
+ if (!e.candidate?.candidate) return;
+ pkgCandidates.push(e.candidate.candidate);
+ };
+
+ peer.on("signal", initialCandidateSignalling);
+
+ const finishIce = () => {
+ peer.removeListener("signal", initialCandidateSignalling);
+ if (localPackages.includes(pkg)) return;
+ if (pkgCandidates.length === 0) return;
+
+ localPackages.push(pkg);
+ };
+
+ peer.once("_iceComplete", finishIce);
+
+ const remoteSdp = createSdp(
+ true,
+ remoteIceUFrag,
+ remoteIcePwd,
+ remoteDtlsFingerprintBase64
+ );
+
+ for (const candidate of remoteCandidates) {
+ peer.signal({ candidate: { candidate, sdpMLineIndex: 0 } });
+ }
+
+ peer.signal({ type: "offer", sdp: remoteSdp });
+ } else {
+ // I am peer B, I need to create a peer first if none exists, and send a package.
+ // - Create PC
+ // - Create offer
+ // - Set local description as-is
+ // - Generate ufrag + pwd
+ // - Generate remote SDP using the dtls fingerprint for A, and my generated ufrag + pwd
+ // - Add an srflx candidate for each of the reflexive IPs for A (on a random port) to hole punch
+ // - Set remote description
+ // so peer reflexive candidates for it show up.
+ // - Let trickle run, then once trickle finishes send a package for A to pick up = [my session id, my offer sdp, generated ufrag/pwd, dtls fingerprint, ice candidates]
+ // - keep the icecandidate listener active, and add the pfrlx candidates when they arrive (but don't send another package)
+ if (!peers.has(remoteSessionId)) {
+ lastProcessedReceivedDataTimestamps.set(
+ remoteSessionId,
+ remoteDataTimestamp
+ );
+
+ const remoteUfrag = randomstring(12);
+ const remotePwd = randomstring(32);
+ const peer = new Peer({
+ config: peerOptions,
+ proprietaryConstraints:
+ this.rtcPeerConnectionProprietaryConstraints,
+ iceCompleteTimeout: 3000,
+ initiator: true,
+ sdpTransform: this.peerSdpTransform
+ });
+
+ peer.id = remoteSessionId;
+ peer.client_id = remoteClientId;
+
+ this._wireUpCommonPeerEvents(peer, participantLimit);
+
+ peers.set(peer.id, peer);
+
+ // This is the 'package' sent to peer A that it needs to start ICE
+ const pkg = [
+ remoteSessionId,
+ localSessionId,
+ /* lfrag */ null,
+ /* lpwd */ null,
+ /* ldtls */ null,
+ remoteUfrag,
+ remotePwd,
+ now,
+ []
+ ];
+
+ const pkgCandidates = pkg[pkg.length - 1];
+
+ const initialCandidateSignalling = (e) => {
+ // Push package onto the given package list, so it will be sent in next polling step.
+ if (!e.candidate?.candidate) return;
+ pkgCandidates.push(e.candidate.candidate);
+ };
+
+ peer.on("signal", initialCandidateSignalling);
+
+ const finishIce = () => {
+ peer.removeListener(
+ "signal",
+ initialCandidateSignalling
+ );
+
+ if (localPackages.includes(pkg)) return;
+ if (pkgCandidates.length === 0) return;
+
+ localPackages.push(pkg);
+ };
+
+ peer.once("_iceComplete", finishIce);
+
+ const enqueuePackageFromOffer = (e) => {
+ if (e.type !== "offer") return;
+ peer.removeListener("signal", enqueuePackageFromOffer);
+
+ for (const l of e.sdp.split("\r\n")) {
+ switch (l.split(":")[0]) {
+ case "a=ice-ufrag":
+ pkg[2] = l.substring(12);
+ break;
+ case "a=ice-pwd":
+ pkg[3] = l.substring(10);
+ break;
+ case "a=fingerprint":
+ pkg[4] = hexToBase64(
+ l.substring(22).replaceAll(":", "")
+ );
+ break;
+ }
+ }
+
+ // Peer A posted its reflexive IPs to try to speed up hole punching by B.
+ let remoteSdp = createSdp(
+ false,
+ remoteUfrag,
+ remotePwd,
+ remoteDtlsFingerprintBase64
+ );
+
+ for (let i = 0; i < remoteReflexiveIps.length; i++) {
+ remoteSdp += `a=candidate:0 1 udp ${i + 1} ${
+ remoteReflexiveIps[i]
+ } 30000 typ srflx\r\n`;
+ }
+
+ if (!delaySetRemoteUntilReceiveCandidates) {
+ peer.signal({ type: "answer", sdp: remoteSdp });
+ } else {
+ peer._pendingRemoteSdp = remoteSdp;
+ }
+ };
+
+ peer.once("signal", enqueuePackageFromOffer);
+ }
+
+ if (!remotePackage) continue;
+
+ // Peer B will also receive candidates in the case where hole punch fails.
+ // If we already added the candidates from A, skip
+ const [, , , , , , , , remoteCandidates] = remotePackage;
+ if (packageReceivedFromPeers.has(remoteSessionId)) continue;
+ if (!peers.has(remoteSessionId)) continue;
+
+ const peer = peers.get(remoteSessionId);
+
+ if (
+ delaySetRemoteUntilReceiveCandidates &&
+ !peer._pc.remoteDescription &&
+ peer._pendingRemoteSdp
+ ) {
+ if (!peer.connected) {
+ for (const candidate of remoteCandidates) {
+ peer.signal({
+ candidate: { candidate, sdpMLineIndex: 0 }
+ });
+ }
+ }
+
+ peer.signal({
+ type: "answer",
+ sdp: peer._pendingRemoteSdp
+ });
+ delete peer._pendingRemoteSdp;
+ packageReceivedFromPeers.add(remoteSessionId);
+ }
+
+ if (
+ !delaySetRemoteUntilReceiveCandidates &&
+ peer._pc.remoteDescription &&
+ remoteCandidates.length > 0
+ ) {
+ if (!peer.connected) {
+ for (const candidate of remoteCandidates) {
+ peer.signal({
+ candidate: { candidate, sdpMLineIndex: 0 }
+ });
+ }
+ }
+
+ packageReceivedFromPeers.add(remoteSessionId);
+ }
+ }
+ }
+
+ const remoteSessionIds = remotePeerDatas.map((p) => p[0]);
+
+ // Remove all peers no longer in the peer list.
+ // TODO deal with simple peer
+ for (const [sessionId, peer] of peers.entries()) {
+ if (remoteSessionIds.includes(sessionId)) continue;
+ this._removePeer(peer, true);
+ }
+ }
+
+ /**
+ * Connect to network and start discovering peers
+ */
+ async start(props) {
+ this.startedAtTimestamp = Date.now();
+ const canStart = await this._getCurrentRoomCount(userData.currentPostId);
+ if ( canStart ) {
+ if( userData.heartbeatEnabled ) {
+ // console.log("heartbeatEnabled", userData.heartbeatEnabled);
+
+ const isInRoom = await this._pingIfInRoom();;
+
+ if (isInRoom.body === 'true') {
+ // console.log("User is already in the room, cannot start. Send another beat.");
+ this._sendHeartbeat();
+ } else {
+ // console.log("User is not in the room, starting initial heartbeat.");
+ this._updateRoomCount("add");
+ this._sendHeartbeat();
+ }
+ this.startHeartbeat();
+ }
+ } else {
+ console.warn("Room is full, cannot start.", canStart);
+ return;
+ }
+
+ await this._init();
+
+ const [udpEnabled, isSymmetric, reflexiveIps, dtlsFingerprint] =
+ await this._getNetworkSettings(this.dtlsCert);
+
+ if (this.finished) return;
+
+ this.udpEnabled = udpEnabled;
+ this.isSymmetric = isSymmetric;
+ this.reflexiveIps = reflexiveIps;
+ this.dtlsFingerprint = dtlsFingerprint;
+
+ let guestDefaultAvatar = defaultAvatar === '' ? defaultVRM : defaultAvatar;
+ this.playerVRM = userData.playerVRM ? userData.playerVRM : guestDefaultAvatar;
+
+ this.networkSettingsInterval = setInterval(async () => {
+ const [
+ newUdpEnabled,
+ newIsSymmetric,
+ newReflexiveIps,
+ newDtlsFingerprint
+ ] = await this._getNetworkSettings(this.dtlsCert);
+
+ if (
+ newUdpEnabled !== this.udpEnabled ||
+ newIsSymmetric !== this.isSymmetric ||
+ newDtlsFingerprint !== this.dtlsFingerprint ||
+ !![...newReflexiveIps].find(
+ (ip) => ![...this.reflexiveIps].find((ip2) => ip === ip2)
+ ) ||
+ !![...reflexiveIps].find(
+ (ip) => ![...newReflexiveIps].find((ip2) => ip === ip2)
+ )
+ ) {
+ // Network changed, force pushing new data
+ this.dataTimestamp = null;
+ }
+
+ this.udpEnabled = newUdpEnabled;
+ this.isSymmetric = newIsSymmetric;
+ this.reflexiveIps = newReflexiveIps;
+ this.dtlsFingerprint = newDtlsFingerprint;
+ }, this.networkChangePollIntervalMs);
+
+ this._step = this._step.bind(this);
+ this.stepInterval = setInterval(this._step, 500);
+ this.destroyOnUnload = () => this.destroy();
+
+ for (const ev of iOSSafari
+ ? ["pagehide"]
+ : ["beforeunload", "unload"]) {
+ window.addEventListener(ev, this.destroyOnUnload);
+ }
+ }
+
+ _removePeer(peer, destroy = false) {
+ const { packageReceivedFromPeers, packages, peers } = this;
+ if (!peers.has(peer.id)) return;
+
+ removeInPlace(packages, (pkg) => pkg[0] === peer.id);
+ packageReceivedFromPeers.delete(peer.id);
+
+ peers.delete(peer.id);
+
+ if (destroy) {
+ peer.destroy();
+ }
+
+ this.emit("peerclose", peer);
+ // // update the room count using the endpoint _updateRoomCountInDatabase
+ // let roomCount = p2pcf.peers.size + 1;
+
+ // this._updateRoomCountInDatabase(roomCount);
+
+ }
+
+ /**
+ * Send a msg and get response for it
+ *
+ * @param Peer peer simple-peer object to send msg to
+ * @param string msg Message to send
+ * @param integer msgID ID of message if it's a response to a previous message
+ * @param peer
+ * @param msg
+ */
+ send(peer, msg) {
+ return new Promise((resolve, reject) => {
+ // if leading byte is zero
+ // next two bytes is message id, then remaining bytes
+ // otherwise its just raw
+ let dataArrBuffer = null;
+
+ let messageId = null;
+
+ if (msg instanceof ArrayBuffer) {
+ dataArrBuffer = msg;
+ } else if (msg instanceof Uint8Array) {
+ if (msg.buffer.byteLength === msg.length) {
+ dataArrBuffer = msg.buffer;
+ } else {
+ dataArrBuffer = msg.buffer.slice(
+ msg.byteOffset,
+ msg.byteOffset + msg.byteLength
+ );
+ }
+ } else {
+ throw new Error("Unsupported send data type", msg);
+ }
+
+ // If the magic word happens to be the beginning of this message, chunk it
+ if (
+ dataArrBuffer.byteLength > MAX_MESSAGE_LENGTH_BYTES ||
+ new Uint16Array(dataArrBuffer, 0, 1) === CHUNK_MAGIC_WORD
+ ) {
+ messageId = Math.floor(Math.random() * 256 * 128);
+ }
+
+ if (messageId !== null) {
+ for (
+ let offset = 0, chunkId = 0;
+ offset < dataArrBuffer.byteLength;
+ offset += CHUNK_MAX_LENGTH_BYTES, chunkId++
+ ) {
+ const chunkSize = Math.min(
+ CHUNK_MAX_LENGTH_BYTES,
+ dataArrBuffer.byteLength - offset
+ );
+ let bufSize = CHUNK_HEADER_LENGTH_BYTES + chunkSize;
+
+ while (bufSize % 4 !== 0) {
+ bufSize++;
+ }
+
+ const buf = new ArrayBuffer(bufSize);
+ new Uint8Array(buf, CHUNK_HEADER_LENGTH_BYTES).set(
+ new Uint8Array(dataArrBuffer, offset, chunkSize)
+ );
+ const u16 = new Uint16Array(buf);
+ const u32 = new Uint32Array(buf);
+
+ u16[0] = CHUNK_MAGIC_WORD;
+ u16[1] = messageId;
+ u16[2] = chunkId;
+ u16[3] =
+ offset + CHUNK_MAX_LENGTH_BYTES >=
+ dataArrBuffer.byteLength
+ ? 1
+ : 0;
+ u32[2] = dataArrBuffer.byteLength;
+
+ peer.send(buf);
+ }
+ } else {
+ peer.send(dataArrBuffer);
+ }
+ });
+ }
+
+ broadcast(msg) {
+ const ps = [];
+
+ for (const peer of this.peers.values()) {
+ if (!peer.connected) continue;
+
+ ps.push(this.send(peer, msg));
+ }
+
+ return Promise.all(ps);
+ }
+
+ /**
+ * Destroy object
+ */
+ destroy() {
+ if (this._step) {
+ this._step(true);
+ }
+
+ if (this.networkSettingsInterval) {
+ clearInterval(this.networkSettingsInterval);
+ this.networkSettingsInterval = null;
+ }
+
+ if (this.stepInterval) {
+ clearInterval(this.stepInterval);
+ this.stepInterval = null;
+ }
+
+ if (this.destroyOnUnload) {
+ for (const ev of iOSSafari
+ ? ["pagehide"]
+ : ["beforeunload", "unload"]) {
+ window.removeEventListener(ev, this.destroyOnUnload);
+ }
+
+ this.destroyOnUnload = null;
+ }
+
+ for (const peer of this.peers.values()) {
+ peer.destroy();
+ }
+ // update the room count using the endpoint _updateRoomCountInDatabase
+ // this._updateRoomCount("subtract");
+ this.stopHeartbeat();
+ }
+
+ /**
+ * Handle msg chunks. Returns false until the last chunk is received. Finally returns the entire msg
+ *
+ * @param object data
+ * @param data
+ * @param messageId
+ * @param chunkId
+ */
+ _chunkHandler(data, messageId, chunkId) {
+ let target = null;
+
+ if (!this.msgChunks.has(messageId)) {
+ const totalLength = new Uint32Array(data, 0, 3)[2];
+ target = new Uint8Array(totalLength);
+ this.msgChunks.set(messageId, target);
+ } else {
+ target = this.msgChunks.get(messageId);
+ }
+
+ const offsetToSet = chunkId * CHUNK_MAX_LENGTH_BYTES;
+
+ const numBytesToSet = Math.min(
+ target.byteLength - offsetToSet,
+ CHUNK_MAX_LENGTH_BYTES
+ );
+
+ target.set(
+ new Uint8Array(data, CHUNK_HEADER_LENGTH_BYTES, numBytesToSet),
+ chunkId * CHUNK_MAX_LENGTH_BYTES
+ );
+
+ return target.buffer;
+ }
+
+ _updateConnectedSessions() {
+ this.connectedSessions.length = 0;
+
+ for (const [sessionId, peer] of this.peers) {
+ if (peer.connected) {
+ this.connectedSessions.push(sessionId);
+ continue;
+ }
+ }
+ }
+
+ async _getNetworkSettings() {
+ await this._init();
+
+ let dtlsFingerprint = null;
+ const candidates = [];
+ const reflexiveIps = new Set();
+
+ const peerOptions = { iceServers: this.stunIceServers };
+
+ if (this.dtlsCert) {
+ peerOptions.certificates = [this.dtlsCert];
+ }
+
+ const pc = new this.wrtc.RTCPeerConnection(peerOptions);
+ pc.createDataChannel("x");
+
+ const p = new Promise((resolve) => {
+ setTimeout(() => resolve(), 5000);
+
+ pc.onicecandidate = (e) => {
+ if (!e.candidate) return resolve();
+
+ if (e.candidate.candidate) {
+ candidates.push(parseCandidate(e.candidate.candidate));
+ }
+ };
+ });
+
+ pc.createOffer().then((offer) => {
+ for (const l of offer.sdp.split("\n")) {
+ if (l.indexOf("a=fingerprint") === -1) continue;
+ dtlsFingerprint = l.split(" ")[1].trim();
+ }
+
+ pc.setLocalDescription(offer);
+ });
+
+ await p;
+
+ pc.close();
+
+ let isSymmetric = false;
+ let udpEnabled = false;
+
+ // Network is not symmetric if we can find a srflx candidate that has a unique related port
+ /* eslint-disable no-labels */
+ loop: for (const c of candidates) {
+ /* eslint-enable no-labels */
+ if (c[0] !== CANDIDATE_TYPES.srflx) continue;
+ udpEnabled = true;
+
+ reflexiveIps.add(c[CANDIDATE_IDX.IP]);
+
+ for (const d of candidates) {
+ if (d[0] !== CANDIDATE_TYPES.srflx) continue;
+ if (c === d) continue;
+
+ if (
+ typeof c[CANDIDATE_IDX.RELATED_PORT] === "number" &&
+ typeof d[CANDIDATE_IDX.RELATED_PORT] === "number" &&
+ c[CANDIDATE_IDX.RELATED_PORT] ===
+ d[CANDIDATE_IDX.RELATED_PORT] &&
+ c[CANDIDATE_IDX.PORT] !== d[CANDIDATE_IDX.PORT]
+ ) {
+ // check port and related port
+ // Symmetric, continue
+ isSymmetric = true;
+ break;
+ }
+ }
+ }
+
+ return [udpEnabled, isSymmetric, reflexiveIps, dtlsFingerprint];
+ }
+
+ _handlePeerError(peer, err) {
+ if (
+ err.errorDetail === "sctp-failure" &&
+ err.message.indexOf("User-Initiated Abort") >= 0
+ ) {
+ return;
+ }
+
+ console.error(err);
+ }
+
+ _checkForSignalOrEmitMessage(peer, msg) {
+ if (msg.byteLength < SIGNAL_MESSAGE_HEADER_WORDS.length * 2) {
+ this.emit("msg", peer, msg);
+ return;
+ }
+
+ const u16 = new Uint16Array(msg, 0, SIGNAL_MESSAGE_HEADER_WORDS.length);
+
+ for (let i = 0; i < SIGNAL_MESSAGE_HEADER_WORDS.length; i++) {
+ if (u16[i] !== SIGNAL_MESSAGE_HEADER_WORDS[i]) {
+ this.emit("msg", peer, msg);
+ return;
+ }
+ }
+
+ const u8 = new Uint8Array(msg, SIGNAL_MESSAGE_HEADER_WORDS.length * 2);
+
+ let payload = arrToText(u8);
+
+ // Might have a trailing byte
+ if (payload.endsWith("\0")) {
+ payload = payload.substring(0, payload.length - 1);
+ }
+
+ peer.signal(payload);
+ }
+ _pingIfInRoom(roomId) {
+ // check if the user is already in the room
+ const postId = userData.currentPostId;
+ const apiUrl = `/wp-json/threeov/v1/handle-heart-check/${postId}`;
+
+ // fetch(apiUrl, {
+ // method: 'POST',
+ // headers: {
+ // 'Content-Type': 'application/json',
+ // 'X-WP-Nonce': userData.nonce,
+ // },
+ // body: JSON.stringify({ displayName: p2pcf.clientId }) // send any necessary data
+ // })
+ // .then(response => response.json())
+ // .then(data => console.log('Heartbeat sent:', data))
+ // .catch(error => console.error('Error sending heartbeat:', error));
+
+ let isInRoom = false;
+ // post api endpoint to see if in room
+ return fetch(apiUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-WP-Nonce': userData.nonce,
+ },
+ body: JSON.stringify({ displayName: p2pcf.clientId })
+ })
+ .then(response => response.json())
+ .catch(error => console.error('Error sending heartbeat:', error));
+ }
+
+ _sendHeartbeat() {
+ const postId = userData.currentPostId;
+ const apiUrl = `/wp-json/threeov/v1/send-heartbeat/${postId}`;
+
+ fetch(apiUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-WP-Nonce': userData.nonce,
+ },
+ body: JSON.stringify({ displayName: p2pcf.clientId })
+ })
+ .then(response => response.json())
+ .then(data => console.log('Heartbeat sent:', data))
+ .catch(error => console.error('Error sending heartbeat:', error));
+ }
+
+ // Start sending heartbeats at regular intervals
+ startHeartbeat() {
+ this.heartbeatInterval = setInterval(() => this._sendHeartbeat(), 30000); // send every 30 seconds
+ }
+
+ // Stop sending heartbeats
+ stopHeartbeat() {
+ clearInterval(this.heartbeatInterval);
+ }
+
+ // Function to get the current room count
+ _getCurrentRoomCount(postId) {
+ const apiUrl = `/wp-json/threeov/v1/get-room-count/${postId}`;
+ return fetch(apiUrl, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-WP-Nonce': userData.nonce,
+ }
+ })
+ .then(response => {
+ if (!response.ok) {
+ throw new Error('Network response was not ok');
+ }
+ // if the response from the get-room-count endpoint isnt true then the room is full
+ return response.json();
+ })
+ .catch(error => {
+ console.error('Error fetching room count:', error);
+ throw error;
+ });
+ }
+
+ _updateRoomCount(action) {
+ const postId = userData.currentPostId;
+ let actionNonce = '';
+ let apiUrl = '';
+ if(action === "add") {
+ actionNonce = userData.addNonce;
+ apiUrl = `/wp-json/threeov/v1/add-room-count/${postId}`;
+ } else if(action === "subtract") {
+ actionNonce = userData.subtractNonce;
+ apiUrl = `/wp-json/threeov/v1/subtract-room-count/${postId}`;
+ }
+ const data = { action: action, actionNonce: actionNonce, clientId: p2pcf.clientId};
+
+ fetch(apiUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-WP-Nonce': userData.nonce,
+ },
+ body: JSON.stringify(data)
+ })
+ .then(response => response.json())
+ .then(data => console.log('Room count updated:', data))
+ .catch((error) => {
+ console.error('Error updating room count:', error);
+ });
+ }
+
+ // // Function to update room count in database via WordPress REST API
+ // _updateRoomCountInDatabase(roomCount) {
+ // const postId = userData.currentPostId // Implement this method based on how you determine the post ID
+ // const apiUrl = `/wp-json/threeov/v1/update-room-count/${postId}`;
+ // const data = { count: roomCount };
+
+ // fetch(apiUrl, {
+ // method: 'POST',
+ // headers: {
+ // 'Content-Type': 'application/json',
+ // 'X-WP-Nonce': userData.nonce,
+ // },
+ // body: JSON.stringify(data)
+ // })
+ // .then(response => response.json())
+ // .then(data => console.log('Room count updated:', data))
+ // .catch((error) => {
+ // console.error('Error updating room count:', error);
+ // });
+ // }
+
+ _wireUpCommonPeerEvents(peer, participantLimit) {
+ peer.on("connect", () => {
+ // console.log(p2pcf.peers.size, "peers connected", p2pcf.peers);
+ let roomCount = p2pcf.peers.size;
+ var limit = parseInt(participantLimit);
+ if( roomCount < limit ) {
+ // console.log("clean room entry", p2pcf);
+ this.emit("peerconnect", peer);
+ // after connecting, send the player data to participant
+ const playerData = { playerVRM: this.playerVRM, inWorldName: userData.inWorldName, profileImage: userData.profileImage};
+ peer.send(new TextEncoder().encode(JSON.stringify(playerData)));
+ // set the user as validly in the room
+ validlyInRoom = true;
+ // Remove packages for the peer once connected
+ removeInPlace(this.packages, (pkg) => pkg[0] === peer.id);
+ this._updateConnectedSessions();
+ } else {
+ console.log("participants", "FULLLLLL");
+ if(! validlyInRoom ) {
+ console.log("participants are", window.participants);
+ console.log("room is full", this.roomId, p2pcf);
+ // if there are more than 2 participants, disconnect the peer increment the room id and retry
+ // Remove packages for the peer once connected
+ // removeInPlace(this.packages, (pkg) => pkg[0] === peer.id);
+
+ // peer.destroy();
+
+ let curentRoomHash = window.location.hash;
+ // remove the # from the hash
+ let currentRoomId = curentRoomHash.substring(1);
+ // example string #3ov-room-1 take the value after the last - and cast it to int
+ let newRoomId = parseInt(currentRoomId.substr(currentRoomId.lastIndexOf("-") + 1));
+ // if the room id is NaN, set it to 1
+ if(isNaN(newRoomId)) {
+ newRoomId = 1;
+ }
+ newRoomId = newRoomId + 1;
+ // remove the old room number from the currentRoomId and add the new one
+ currentRoomId = currentRoomId.substring(0, currentRoomId.lastIndexOf("-") + 1);
+ currentRoomId = currentRoomId + newRoomId;
+ // set the #room in the url
+ window.location.hash = currentRoomId;
+
+ roomCount = 0;
+ this.roomId = newRoomId;
+ // fire an event called "room full refresh"
+ this.emit("roomfullrefresh", peer);
+ }
+ }
+ });
+
+ peer.on("data", (data) => {
+ let messageId = null;
+ let u16 = null;
+ if (data.byteLength >= CHUNK_HEADER_LENGTH_BYTES) {
+ u16 = new Uint16Array(data, 0, CHUNK_HEADER_LENGTH_BYTES / 2);
+
+ if (u16[0] === CHUNK_MAGIC_WORD) {
+ messageId = u16[1];
+ }
+ }
+ if (messageId !== null) {
+ try {
+ const chunkId = u16[2];
+ const last = u16[3] !== 0;
+ const msg = this._chunkHandler(
+ data,
+ messageId,
+ chunkId,
+ last
+ );
+ if (last) {
+ this._checkForSignalOrEmitMessage(peer, msg);
+ this.msgChunks.delete(messageId);
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ } else {
+ this._checkForSignalOrEmitMessage(peer, data);
+ }
+ });
+
+ peer.on("error", (err) => {
+ console.warn(err);
+ });
+
+ peer.on("close", () => {
+ this._removePeer(peer);
+ this._updateConnectedSessions();
+ });
+
+ // Once ICE completes, perform subsequent signalling via the datachannel
+ peer.once("_iceComplete", () => {
+ peer.on("signal", (signalData) => {
+ const payloadBytes = textToArr(JSON.stringify(signalData));
+
+ let len =
+ payloadBytes.byteLength +
+ SIGNAL_MESSAGE_HEADER_WORDS.length * 2;
+
+ if (len % 2 !== 0) {
+ len++;
+ }
+
+ // Add signal header
+ const buf = new ArrayBuffer(len);
+ const u8 = new Uint8Array(buf);
+ const u16 = new Uint16Array(buf);
+
+ u8.set(payloadBytes, SIGNAL_MESSAGE_HEADER_WORDS.length * 2);
+
+ for (let i = 0; i < SIGNAL_MESSAGE_HEADER_WORDS.length; i++) {
+ u16[i] = SIGNAL_MESSAGE_HEADER_WORDS[i];
+ }
+
+ this.send(peer, buf);
+ });
+ });
+ }
+}
\ No newline at end of file
diff --git a/blocks/environment/components/p2pcf/p2pcf.js b/blocks/environment/components/p2pcf/p2pcf.js
index 9c47a97..7fe5238 100644
--- a/blocks/environment/components/p2pcf/p2pcf.js
+++ b/blocks/environment/components/p2pcf/p2pcf.js
@@ -7,7 +7,7 @@
/* global crypto */
import getBrowserRTC from "get-browser-rtc";
-import EventEmitter from "events";
+import { EventEmitter } from 'events'
import Peer from "tiny-simple-peer";
import {
encode as arrayBufferToBase64,
@@ -15,11 +15,13 @@ import {
} from "base64-arraybuffer";
import { hexToBytes } from "convert-hex";
import arrayBufferToHex from "array-buffer-to-hex";
+import defaultVRM from "../../../../inc/avatars/3ov_default_avatar.vrm";
+let validlyInRoom = false;
// Based on Chrome
const MAX_MESSAGE_LENGTH_BYTES = 16000;
-const CHUNK_HEADER_LENGTH_BYTES = 12; // 2 magic, 2 msg id, 2 chunk id, 2 for done bit, 4 for length
+const CHUNK_HEADER_LENGTH_BYTES = 12;
const CHUNK_MAGIC_WORD = 8121;
const CHUNK_MAX_LENGTH_BYTES =
MAX_MESSAGE_LENGTH_BYTES - CHUNK_HEADER_LENGTH_BYTES;
@@ -183,7 +185,7 @@ const parseCandidate = (line) => {
}
while (candidate.length < 8) candidate.push(null);
- candidate[7] = parseInt(parts[3], 10); // Priority last
+ candidate[7] = parseInt(parts[3], 10);
return candidate;
};
@@ -218,9 +220,8 @@ export default class P2PCF extends EventEmitter {
this.peerProprietaryConstraints =
options.rtcPeerConnectionProprietaryConstraints || {};
this.peerSdpTransform = options.sdpTransform || ((sdp) => sdp);
-
this.workerUrl =
- options.workerUrl || "https://p2pcf.minddrop.workers.dev";
+ options.workerUrl[0] || "https://p2pcf.sxp.digital";
if (this.workerUrl.endsWith("/")) {
this.workerUrl = this.workerUrl.substring(
@@ -241,6 +242,7 @@ export default class P2PCF extends EventEmitter {
this.fastPollingDurationMs = options.fastPollingDurationMs || 10000;
this.fastPollingRateMs = options.fastPollingRateMs || 750;
this.slowPollingRateMs = options.slowPollingRateMs || 1500;
+ this.participantLimit = options.participantLimit || 8;
this.wrtc = getBrowserRTC();
this.dtlsCert = null;
@@ -292,7 +294,8 @@ export default class P2PCF extends EventEmitter {
packages,
fastPollingDurationMs,
fastPollingRateMs,
- slowPollingRateMs
+ slowPollingRateMs,
+ participantLimit,
} = this;
const now = Date.now();
@@ -320,7 +323,7 @@ export default class P2PCF extends EventEmitter {
this.isSymmetric,
localDtlsFingerprintBase64,
this.startedAtTimestamp,
- [...this.reflexiveIps]
+ [...this.reflexiveIps],
];
const payload = { r: roomId, k: contextId };
@@ -368,7 +371,6 @@ export default class P2PCF extends EventEmitter {
this.lastPackages = JSON.stringify(packages);
}
}
-
const body = JSON.stringify(payload);
const headers = { "Content-Type": "application/json " };
let keepalive = false;
@@ -405,7 +407,6 @@ export default class P2PCF extends EventEmitter {
payload.x = this.stateExpirationIntervalMs;
payload.p = packages;
this.lastPackages = JSON.stringify(packages);
-
const res = await fetch(this.workerUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
@@ -428,7 +429,8 @@ export default class P2PCF extends EventEmitter {
localDtlsFingerprintBase64,
packages,
remotePeerDatas,
- remotePackages
+ remotePackages,
+ participantLimit
);
const activeSessionIds = remotePeerDatas.map((p) => p[0]);
@@ -466,7 +468,8 @@ export default class P2PCF extends EventEmitter {
localDtlsFingerprintBase64,
localPackages,
remotePeerDatas,
- remotePackages
+ remotePackages,
+ participantLimit
) {
const localStartedAtTimestamp = this.startedAtTimestamp;
@@ -490,7 +493,7 @@ export default class P2PCF extends EventEmitter {
remoteDtlsFingerprintBase64,
remoteStartedAtTimestamp,
remoteReflexiveIps,
- remoteDataTimestamp
+ remoteDataTimestamp,
] = remotePeerData;
// Don't process the same messages twice. This covers disconnect cases where stale data re-creates a peer too early.
@@ -587,7 +590,8 @@ export default class P2PCF extends EventEmitter {
peer.id = remoteSessionId;
peer.client_id = remoteClientId;
- this._wireUpCommonPeerEvents(peer);
+
+ this._wireUpCommonPeerEvents(peer, participantLimit);
peers.set(peer.id, peer);
@@ -667,7 +671,7 @@ export default class P2PCF extends EventEmitter {
peer.id = remoteSessionId;
peer.client_id = remoteClientId;
- this._wireUpCommonPeerEvents(peer);
+ this._wireUpCommonPeerEvents(peer, participantLimit);
peers.set(peer.id, peer);
@@ -814,8 +818,30 @@ export default class P2PCF extends EventEmitter {
/**
* Connect to network and start discovering peers
*/
- async start() {
+ async start(props) {
this.startedAtTimestamp = Date.now();
+ // const canStart = await this._getCurrentRoomCount(userData.currentPostId);
+ // if ( canStart ) {
+ // if( userData.heartbeatEnabled ) {
+ // // console.log("heartbeatEnabled", userData.heartbeatEnabled);
+
+ // const isInRoom = await this._pingIfInRoom();;
+
+ // if (isInRoom.body === 'true') {
+ // // console.log("User is already in the room, cannot start. Send another beat.");
+ // this._sendHeartbeat();
+ // } else {
+ // // console.log("User is not in the room, starting initial heartbeat.");
+ // this._updateRoomCount("add");
+ // this._sendHeartbeat();
+ // }
+ // this.startHeartbeat();
+ // }
+ // } else {
+ // console.warn("Room is full, cannot start.", canStart);
+ // return;
+ // }
+
await this._init();
const [udpEnabled, isSymmetric, reflexiveIps, dtlsFingerprint] =
@@ -828,6 +854,9 @@ export default class P2PCF extends EventEmitter {
this.reflexiveIps = reflexiveIps;
this.dtlsFingerprint = dtlsFingerprint;
+ let guestDefaultAvatar = defaultAvatar === '' ? defaultVRM : defaultAvatar;
+ this.playerVRM = userData.playerVRM ? userData.playerVRM : guestDefaultAvatar;
+
this.networkSettingsInterval = setInterval(async () => {
const [
newUdpEnabled,
@@ -882,6 +911,11 @@ export default class P2PCF extends EventEmitter {
}
this.emit("peerclose", peer);
+ // // update the room count using the endpoint _updateRoomCountInDatabase
+ // let roomCount = p2pcf.peers.size + 1;
+
+ // this._updateRoomCountInDatabase(roomCount);
+
}
/**
@@ -1009,6 +1043,9 @@ export default class P2PCF extends EventEmitter {
for (const peer of this.peers.values()) {
peer.destroy();
}
+ // update the room count using the endpoint _updateRoomCountInDatabase
+ // this._updateRoomCount("subtract");
+ // this.stopHeartbeat();
}
/**
@@ -1136,7 +1173,7 @@ export default class P2PCF extends EventEmitter {
err.errorDetail === "sctp-failure" &&
err.message.indexOf("User-Initiated Abort") >= 0
) {
- return; // Benign shutdown
+ return;
}
console.error(err);
@@ -1168,14 +1205,186 @@ export default class P2PCF extends EventEmitter {
peer.signal(payload);
}
+ _pingIfInRoom(roomId) {
+ // check if the user is already in the room
+ const postId = userData.currentPostId;
+ const apiUrl = `/wp-json/threeov/v1/handle-heart-check/${postId}`;
+
+ // fetch(apiUrl, {
+ // method: 'POST',
+ // headers: {
+ // 'Content-Type': 'application/json',
+ // 'X-WP-Nonce': userData.nonce,
+ // },
+ // body: JSON.stringify({ displayName: p2pcf.clientId }) // send any necessary data
+ // })
+ // .then(response => response.json())
+ // .then(data => console.log('Heartbeat sent:', data))
+ // .catch(error => console.error('Error sending heartbeat:', error));
+
+ let isInRoom = false;
+ // post api endpoint to see if in room
+ return fetch(apiUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-WP-Nonce': userData.nonce,
+ },
+ body: JSON.stringify({ displayName: p2pcf.clientId })
+ })
+ .then(response => response.json())
+ .catch(error => console.error('Error sending heartbeat:', error));
+ }
- _wireUpCommonPeerEvents(peer) {
+ // _sendHeartbeat() {
+ // const postId = userData.currentPostId;
+ // const apiUrl = `/wp-json/threeov/v1/send-heartbeat/${postId}`;
+
+ // fetch(apiUrl, {
+ // method: 'POST',
+ // headers: {
+ // 'Content-Type': 'application/json',
+ // 'X-WP-Nonce': userData.nonce,
+ // },
+ // body: JSON.stringify({ displayName: p2pcf.clientId })
+ // })
+ // .then(response => response.json())
+ // .then(data => console.log('Heartbeat sent:', data))
+ // .catch(error => console.error('Error sending heartbeat:', error));
+ // }
+
+ // Start sending heartbeats at regular intervals
+ // startHeartbeat() {
+ // this.heartbeatInterval = setInterval(() => this._sendHeartbeat(), 30000); // send every 30 seconds
+ // }
+
+ // Stop sending heartbeats
+ // stopHeartbeat() {
+ // clearInterval(this.heartbeatInterval);
+ // }
+
+ // Function to get the current room count
+ // _getCurrentRoomCount(postId) {
+ // const apiUrl = `/wp-json/threeov/v1/get-room-count/${postId}`;
+ // return fetch(apiUrl, {
+ // method: 'GET',
+ // headers: {
+ // 'Content-Type': 'application/json',
+ // 'X-WP-Nonce': userData.nonce,
+ // }
+ // })
+ // .then(response => {
+ // if (!response.ok) {
+ // throw new Error('Network response was not ok');
+ // }
+ // // if the response from the get-room-count endpoint isnt true then the room is full
+ // return response.json();
+ // })
+ // .catch(error => {
+ // console.error('Error fetching room count:', error);
+ // throw error;
+ // });
+ // }
+
+ // _updateRoomCount(action) {
+ // const postId = userData.currentPostId;
+ // let actionNonce = '';
+ // let apiUrl = '';
+ // if(action === "add") {
+ // actionNonce = userData.addNonce;
+ // apiUrl = `/wp-json/threeov/v1/add-room-count/${postId}`;
+ // } else if(action === "subtract") {
+ // actionNonce = userData.subtractNonce;
+ // apiUrl = `/wp-json/threeov/v1/subtract-room-count/${postId}`;
+ // }
+ // const data = { action: action, actionNonce: actionNonce, clientId: p2pcf.clientId};
+
+ // fetch(apiUrl, {
+ // method: 'POST',
+ // headers: {
+ // 'Content-Type': 'application/json',
+ // 'X-WP-Nonce': userData.nonce,
+ // },
+ // body: JSON.stringify(data)
+ // })
+ // .then(response => response.json())
+ // .then(data => console.log('Room count updated:', data))
+ // .catch((error) => {
+ // console.error('Error updating room count:', error);
+ // });
+ // }
+
+ // // Function to update room count in database via WordPress REST API
+ // _updateRoomCountInDatabase(roomCount) {
+ // const postId = userData.currentPostId // Implement this method based on how you determine the post ID
+ // const apiUrl = `/wp-json/threeov/v1/update-room-count/${postId}`;
+ // const data = { count: roomCount };
+
+ // fetch(apiUrl, {
+ // method: 'POST',
+ // headers: {
+ // 'Content-Type': 'application/json',
+ // 'X-WP-Nonce': userData.nonce,
+ // },
+ // body: JSON.stringify(data)
+ // })
+ // .then(response => response.json())
+ // .then(data => console.log('Room count updated:', data))
+ // .catch((error) => {
+ // console.error('Error updating room count:', error);
+ // });
+ // }
+
+ _wireUpCommonPeerEvents(peer, participantLimit) {
peer.on("connect", () => {
- this.emit("peerconnect", peer);
-
- // Remove packages for the peer once connected
- removeInPlace(this.packages, (pkg) => pkg[0] === peer.id);
- this._updateConnectedSessions();
+ // console.log(p2pcf.peers.size, "peers connected", p2pcf.peers);
+ let roomCount = p2pcf.peers.size;
+ var limit = parseInt(participantLimit);
+ console.log("roomCount", roomCount, "limit", limit);
+ if( roomCount < limit ) {
+ console.log("clean room entry", p2pcf);
+ this.emit("peerconnect", peer);
+ // after connecting, send the player data to participant
+ const playerData = { playerVRM: this.playerVRM, inWorldName: userData.inWorldName, profileImage: userData.profileImage};
+ peer.send(new TextEncoder().encode(JSON.stringify(playerData)));
+ // set the user as validly in the room
+ validlyInRoom = true;
+ // Remove packages for the peer once connected
+ removeInPlace(this.packages, (pkg) => pkg[0] === peer.id);
+ this._updateConnectedSessions();
+ } else {
+ console.log("participants", "FULLLLLL");
+ if(! validlyInRoom ) {
+ console.log("participants are", window.participants);
+ console.log("room is full", this.roomId, p2pcf);
+ // if there are more than 2 participants, disconnect the peer increment the room id and retry
+ // Remove packages for the peer once connected
+ // removeInPlace(this.packages, (pkg) => pkg[0] === peer.id);
+
+ // peer.destroy();
+
+ let curentRoomHash = window.location;
+ // remove the # from the hash
+ let currentRoomId = curentRoomHash.substring(1);
+ // example string #3ov-room-1 take the value after the last - and cast it to int
+ let newRoomId = parseInt(currentRoomId.substr(currentRoomId.lastIndexOf("-") + 1));
+ // if the room id is NaN, set it to 1
+ if(isNaN(newRoomId)) {
+ newRoomId = 1;
+ }
+ newRoomId = newRoomId + 1;
+ // remove the old room number from the currentRoomId and add the new one
+ currentRoomId = currentRoomId.substring(0, currentRoomId.lastIndexOf("-") + 1);
+ currentRoomId = currentRoomId + newRoomId;
+ // set the #room in the url
+ window.location = currentRoomId;
+
+ roomCount = 0;
+ this.roomId = newRoomId;
+ // fire an event called "room full refresh"
+ this.emit("roomfullrefresh", peer);
+ }
+ }
});
peer.on("data", (data) => {
diff --git a/blocks/environment/editor.scss b/blocks/environment/editor.scss
index a09e4f1..3ab45f2 100644
--- a/blocks/environment/editor.scss
+++ b/blocks/environment/editor.scss
@@ -1,5 +1,5 @@
-
- .wp-block-three-object-block {
+
+.wp-block-three-object-block {
border: 1px dotted #f00;
}
@@ -7,7 +7,6 @@
position: relative;
top: 0;
bottom: 0;
- width: 105%;
height: 100vh;
}
@@ -23,28 +22,104 @@
#VRButton{
position: fixed !important;
- top: 20px !important;
bottom:auto !important;
- background-color: rgba(76, 0, 99, 0.677) !important;
-}
- .glb-preview-container {
- padding: 100px;
- text-align: center;
- align-items: center;
- align-content: center;
- background-color:#f2f2f2;
- height: 773px;
- box-sizing: border-box;
- padding-left: 230px !important;
- }
-
- .glb-preview-container button{
+ background-color: #230d006a;
+ margin-top: -50px !important;
+}
+
+.threeov-networking-controls {
+ //initializes hidden
+ display: none;
+
+ #session-id {
+ font-size: .75em;
+ }
+
+ .button {
+ background-color: #b7ff00;
+ border-radius: 20px;
+ padding: 5px 10px;
+ border: 1px solid #98d400;
+ // add dropshadow
+ // box-shadow: 0px 0px 5px 1px #0000005e;
+ color: white;
+ cursor: pointer;
+ margin-top: 3px !important;
+ color: #451a00;
+ font-weight: 600;
+ float:left;
+ margin-right: 5px;
+ letter-spacing: 0.01em;
+ max-width:75px;
+ }
+
+ .button:hover {
+ background-color: #ccff4c;
+ border: 1px solid #98d400;
+ // add dropshadow
+ box-shadow: 0px 0px 5px 1px #7f7f7f5e;
+ color: white;
+ cursor: pointer;
+ color: #451a00;
+ font-weight: 600;
+ }
+
+ position: absolute;
+ top: 20px !important;
+ left: 20px;
+ background-color: rgb(92 90 90 / 51%) !important;
+ // background-color: rgba(34, 34, 34, 0.677) !important;
+ z-index: 1000;
+ width: 250px;
+ box-sizing: border-box;
+ padding: 10px;
+ border-radius: 8px;
+ color: white;
+}
+
+
+.threeov-entry-flow {
+ background-color: #212121f4;
+ color: white;
+ text-align: center;
+ border-radius: 20px;
+ box-shadow: 0px 0px 17px 5px rgb(15 0 0 / 57%);
+}
+
+.threeov-entry-flow span {
+ font-size: 0.8em;
+}
+
+.threeov-entry-flow input {
+ border-radius: 10px;
+ margin-top: 10px !important;
+ min-height: 30px;
+ padding-left: 10px;
+}
+
+.threeov-entry-flow input:focus {
+ // border: 1px solid #b7ff00;
+ outline: #b7ff00 solid !important;
+}
+
+.glb-preview-container {
+ padding: 100px;
+ text-align: center;
+ align-items: center;
+ align-content: center;
+ background-color:#f2f2f2;
+ height: 773px;
+ box-sizing: border-box;
+ padding-left: 230px !important;
+}
+
+.glb-preview-container button{
padding: 15px;
border-radius: 30px;
padding: 5px !important;
- border-radius: 30px !important;
- padding-left: 20px !important;
- padding-right: 20px !important;
+ border-radius: 30px !important;
+ padding-left: 20px !important;
+ padding-right: 20px !important;
}
.three-object-viewer-button {
@@ -71,34 +146,68 @@
.wp-block-three-object-viewer-three-light-block,
.wp-block-three-object-viewer-three-portal-block,
.wp-block-three-object-viewer-three-video-block,
+.wp-block-three-object-viewer-three-networking-block,
.wp-block-three-object-viewer-spawn-point-block,
.wp-block-three-object-viewer-three-text-block {
display: flex;
float: left;
padding-left:10px !important;
height: auto;
- padding-right: 10px !important;
- // background-color: rgb(42, 42, 42);
+ padding-right: 10px !important;
+ // background-color: rgb(42, 42, 42);
}
.wp-block-three-object-viewer-environment .block-editor-inner-blocks {
}
+.wp-block-three-object-viewer-environment .block-editor-block-list__layout .block-editor-block-list__block:not([contenteditable]):focus {
+ border-radius: 30px;
+}
+
+.wp-block-three-object-viewer-environment .block-editor-block-list__block .is-selected:after {
+ border-radius: 30px;
+}
+
+.wp-block-three-object-viewer-environment .block-editor-block-list__layout div {
+ cursor: pointer;
+}
+// .wp-block-three-object-viewer-environment .block-editor-block-list__layout div:first-child {
+// margin-top: 5px;
+// margin-bottom: 5px;
+// }
+
+// .wp-block-three-object-viewer-environment .editor-styles-wrapper .block-editor-block-list__layout.is-root-container > .alignfull {
+// width: 100%;
+// }
+
+.wp-block-three-object-viewer-environment .block-editor-block-list__layout .block-editor-block-list__block:not([contenteditable]):focus:after {
+ border-radius: 30px;
+ box-shadow: 0 0 0 3px #f5eaff;
+ margin-left: 10px;
+ max-width: 178px;
+ z-index: 2;
+}
+
+.is-root-container.alignfull:has(.wp-block-three-object-viewer-environment) {
+ // calculate 100% width minus the width of the sidebar .components-panel__header
+ // width: calc(100vw - 280px);
+ margin: 0 !important;
+}
+
// .wp-block-three-object-viewer-environment {
// width: 100vw;
// }
.wp-block-three-object-viewer-environment .block-editor-inner-blocks.iframe {
height: 100vh;
- }
-
- .wp-block-three-object-viewer-environment .block-editor-inner-blocks:not(.iframe) {
+}
+
+.wp-block-three-object-viewer-environment .block-editor-inner-blocks:not(.iframe) {
height: 90vh;
- }
-
+}
- .three-object-viewer-edit-panel .wide-slider {
- display: block;
+.three-object-viewer-edit-panel .wide-slider {
+ display: block;
}
.three-object-environment-edit-container .threeov-three-number-settings {
@@ -118,7 +227,7 @@
margin: 0 auto;
}
.three-object-viewer-inner-edit-container .three-object-viewer-model-name {
- font-size: 0.7em;
+ font-size: 0.7em;
}
.three-object-viewer-inner-edit-container {
@@ -128,35 +237,64 @@
.three-object-viewer-inner {
z-index:1;
flex: 1;
- background-color: #3f3f3f;
+ // background-color: #5f00b5ed;
+ background-color:#6b3077e6;
+ background-image:
+ radial-gradient(at 5% 31%, rgb(54 0 255 / 27%) 0, transparent 50%),
+ radial-gradient(at 1% 0, rgba(200, 82, 255, .392) 0, transparent 50%);
+ background-size: 100% 111%;
+ background-position: 0 -2px;
color: white;
box-sizing: border-box;
- margin-bottom: 1px;
- margin-top: 1px;
+ // margin-bottom: 3px;
+ // margin-top: 3px;
max-width: 180px;
width: 180px;
max-height: 40px;
height: 40px;
font-size: 0.8em;
text-align: center;
- border-radius: 8px;
+ border-radius: 30px;
box-shadow:0px 1px 5px 1px #1e1e1e;
display: flex;
padding-left: 10px;
- padding-top: 5px;
+ padding-top: 5px;
+ border: 2px solid #ffffff17;
+}
+
+.threeov-block-list-container {
+ backdrop-filter: blur(12px);
+ background-color:#3a017d80;
+ background-image:
+ radial-gradient(at 5% 61%, hsla(290, 77%, 64%, 0.253) 0px, transparent 50%),
+ radial-gradient(at 12% 22%, rgba(163, 152, 231, 0.235) 0px, transparent 50%),
+ radial-gradient(at 88% 26%, hsla(243,69%,70%,1) 0px, transparent 50%),
+ radial-gradient(at 81% 21%, rgba(158, 112, 250, 0.322) 0px, transparent 50%),
+ radial-gradient(at 26% 69%, rgba(76, 198, 250, 0.388) 0px, transparent 50%),
+ radial-gradient(at 1% 32%, hsla(260,50%,67%,0.17) 0px, transparent 50%),
+ radial-gradient(at 23% 96%, hsla(229,100%,50%,0.19) 0px, transparent 50%);
}
div:has(> .three-object-viewer-component-container, > .three-object-viewer-inner) {
margin: 0px !important;
}
+div:has(>.three-object-viewer-component-container,>.three-object-viewer-inner):first-child {
+ margin-top: 15px !important;
+}
+
+div:has(>.three-object-viewer-component-container,>.three-object-viewer-inner) {
+ margin-top: 3px !important;
+ margin-bottom: 3px !important;
+}
+
.three-object-viewer-inner svg, .three-object-viewer-component-container svg {
height: 20px;
filter: invert(88%) ;
display: flex;
padding-right: 5px;
- padding-top: 5px;
+ padding-top: 3px;
}
.three-object-viewer-inner p {
@@ -167,8 +305,8 @@ div:has(> .three-object-viewer-component-container, > .three-object-viewer-inner
}
.wp-block-three-object-viewer-environment .block-editor-inner-blocks .block-editor-button-block-appender {
- width: 60px;
- height: 60px;
+ width: 50px;
+ height: 50px;
}
.wp-block-three-object-viewer-environment .block-editor-inner-blocks .block-editor-button-block-appender svg{
@@ -179,8 +317,19 @@ div:has(> .three-object-viewer-component-container, > .three-object-viewer-inner
margin: 0px 0px -60px;
right: 0;
position: absolute;
- background-color: #8e00f2;
- color: white;
+ background-color: #b7ff00;
+ color: #000;
+ border-radius: 30px;
+ margin-top: 12px;
+ margin-right: 7px;
+ box-shadow: 0px 0px 7px 1px rgb(15 0 0 / 57%);
+}
+
+.wp-block-three-object-viewer-environment .block-editor-inner-blocks .block-editor-button-block-appender:hover {
+ background-color: #ccff4c;
+}
+.wp-block-three-object-viewer-environment .threeov-main-canvas {
+ outline: none !important;
}
.wp-block-three-object-viewer-environment:has(> .block-editor-inner-blocks) {
@@ -189,7 +338,8 @@ div:has(> .three-object-viewer-component-container, > .three-object-viewer-inner
.wp-block-three-object-viewer-environment {
// display: flex !important;
- background-color: rgb(42, 42, 42);
+ // background-color: rgb(42, 42, 42);
+ background-color: #FFFFFF;
}
.stats {
@@ -220,33 +370,240 @@ div:has(> .three-object-viewer-component-container, > .three-object-viewer-inner
z-index: 100;
}
.threeov-chat-container {
- background-color: rgba(0,0,0,.8) !important;
- z-index: 1001 !important;
- width: 100% !important;
- padding: 0px !important;
- margin-left: -8px !important;
+ background-color: rgba(0,0,0,.8) !important;
+ z-index: 1001 !important;
+ width: 100% !important;
+ padding: 0px !important;
+ margin-left: -8px !important;
}
+.threeov-transform-button {
+ border-radius: 30px !important;
+ box-shadow: 0px 0px 5px 1px #0000005e;
+ border: none !important;
+ background-color: #ffffff5e;
+ margin-right: 8px !important;
+}
.three-object-block-url-input {
padding-bottom: 20px;
}
-.three-object-block-url-input input{
+.three-object-block-url-input input {
height: 40px;
}
.threeov-spinner {
+ margin-top:-40px;
border: 8px solid #f3f3f3;
border-top: 8px solid #007bff;
border-radius: 50%;
width: 60px;
height: 60px;
animation: spin 1s linear infinite;
- }
-
- @keyframes spin {
+}
+
+@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
- }
-
\ No newline at end of file
+}
+
+@keyframes move {
+ 0%{
+ transform: translateZ(-500px) rotate(0deg);
+ }
+ 100%{
+ transform: translateZ(500px) rotate(0deg);
+ }
+}
+
+@keyframes fade {
+ 0%{
+ opacity: 1;
+ }
+ 25% {
+ opacity: 1;
+ }
+ 75% {
+ opacity: 1;
+ }
+ 100%{
+ opacity: 1;
+ }
+}
+
+.threeov-entry-scene{
+ display: inline-block;
+ vertical-align: middle;
+ perspective: 6px;
+ perspective-origin: center;
+ position: relative;
+ background-color: #FFFFFF !important;
+}
+
+.threeov-entry-scene-parent {
+ background: radial-gradient(circle, transparent, transparent 10%, white 2%);
+ width: 100% !important;
+ height: 100vh;
+ position: fixed;
+ z-index: 100;
+}
+
+.threeov-entry-wrap{
+ position: absolute;
+ width: 100vw;
+ height: 100vh;
+ left: -500px;
+ top: -500px;
+ transform-style: preserve-3d;
+ animation: move 4s infinite linear;
+ animation-fill-mode: forwards;
+}
+
+.threeov-entry-wrap:nth-child(2){
+ animation: move 4s infinite linear;
+ animation-delay: 6s;
+}
+
+.threeov-entry-wall {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ opacity: 0;
+ animation: fade 2s infinite linear;
+ animation-delay: 0;
+}
+
+.threeov-entry-wrap:nth-child(2) .threeov-entry-wall {
+ animation-delay: 6s;
+}
+
+.threeov-entry-wall-right {
+ transform: rotateY(90deg) translateZ(500px);
+}
+
+.threeov-entry-wall-left {
+ transform: rotateY(-90deg) translateZ(500px);
+}
+
+.threeov-entry-wall-top {
+ transform: rotateX(90deg) translateZ(500px);
+}
+
+.threeov-entry-wall-bottom {
+ transform: rotateX(-90deg) translateZ(500px);
+}
+
+.threeov-entry-wall-back {
+ transform: rotateX(180deg) translateZ(500px);
+}
+
+.threeov-load-world-button {
+ background-color: #b7ff00;
+ border-radius: 20px;
+ padding: 5px 20px !important;
+ border: 1px solid #98d400;
+ color: #1a1a1a;
+ cursor: pointer;
+ margin-top: 20px;
+ color: #451a00;
+ font-weight: 600;
+ margin-right: 5px;
+ letter-spacing: .01em;
+ max-width: 140px;
+ box-shadow: 0px 0px 17px 9px rgb(15 0 0 / 57%);
+}
+
+.threeov-load-world-button-secondary {
+ background-color: #673AB7;
+ border: none;
+ border-radius: 20px;
+ padding: 5px 12px !important;
+ color: rgb(227, 198, 255) !important;
+ cursor: pointer;
+ margin-top: 20px;
+ color: #451a00;
+ font-weight: 600;
+ margin-right: 5px;
+ letter-spacing: .01em;
+ max-width: 140px;
+ font-size: 0.8em;
+ box-shadow: 0px 0px 17px 9px rgb(15 0 0 / 57%);
+}
+
+.threeov-load-world-button-secondary:hover {
+ background-color: #7e57c2 !important;
+ color: rgb(227, 198, 255) !important;
+ border: none !important;
+}
+
+
+
+.threeov-entry-flow button:hover {
+ background-color: #d1ff5b;
+ border: 1px solid #98d400;
+ // add dropshadow
+ // box-shadow: 0px 0px 5px 1px #7f7f7f5e;
+ box-shadow: 0px 0px 17px 9px rgb(15 0 0 / 57%);
+
+ color: rgb(160, 88, 255);
+ cursor: pointer;
+ color: #451a00;
+ font-weight: 600;
+}
+
+@keyframes glow {
+ 0% {
+ box-shadow: 0px 0px 8px 5px rgb(15 0 0 / 57%);
+ }
+ 50% {
+ box-shadow: 0px 0px 8px 5px rgba(95, 95, 95, 0.851);
+ }
+ 100% {
+ box-shadow: 0px 0px 8px 5px rgb(15 0 0 / 57%);
+ }
+}
+
+.threeov-entry-flow input {
+ box-shadow: 0px 0px 8px 5px rgb(15 0 0 / 57%);
+ background-color: #af7bd40f;
+ padding: 5px;
+ color: white;
+ border: 1px solid #a82dff0f;
+ margin-bottom: 10px;
+ width: 180px;
+}
+.threeov-entry-flow div div {
+ margin-top: 10px;
+}
+
+.threeov-entry-flow input:focus {
+ animation: glow 2s infinite linear;
+}
+
+.threeov-entry-pfp {
+ width: 100px;
+ height: 100px;
+ border-radius: 50%;
+ background-color: #6d6d6d;
+ border: 3.5px solid #98d400;
+ box-shadow: 0px 0px 17px 9px rgb(15 0 0 / 57%);
+ margin: 0 auto;
+ margin-top: 20px;
+ background-position: center;
+ background-size: cover;
+ padding: 0px;
+ padding-bottom: 5px;
+ align-items: center;
+ justify-content: center;
+ display: flex;
+ margin-bottom: 10px;
+}
+
+.threeov-entry-flow-instruction {
+ font-size: 0.7em;
+ // background-color: #161616;
+ // border-radius: 8px;
+ // padding: 9px;
+ margin-top: 20px;
+}
diff --git a/blocks/environment/frontend.js b/blocks/environment/frontend.js
index 1d725f3..92253e4 100644
--- a/blocks/environment/frontend.js
+++ b/blocks/environment/frontend.js
@@ -1,125 +1,267 @@
-const { Component, render } = wp.element;
-import React, { Suspense, useRef, useState, useEffect, useMemo } from "react";
+const { Component, render, createRoot } = wp.element;
+
+// import React from "react";
+// import { createRoot } from "react-dom/client"; // Corrected import
import EnvironmentFront from "./components/EnvironmentFront";
import Networking from "./components/Networking";
+import ThreeObjectFront from "./components/ThreeObjectFront";
+import { XRButton } from '@react-three/xr';
+import hmdIcon from '../../inc/assets/hmdicon.png';
+
+let threeObjectViewerBlocks;
+
+if(document.querySelectorAll('three-object-viewer-block').length > 0) {
+ threeObjectViewerBlocks = document.querySelectorAll('three-object-viewer-block');
+} else {
+ threeObjectViewerBlocks = document.querySelectorAll(".three-object-three-app");
+}
+let threeApp;
+if(document.querySelectorAll('three-environment-block').length > 0) {
+ threeApp = document.querySelectorAll('three-environment-block');
+} else {
+ threeApp = document.querySelectorAll(
+ ".three-object-three-app-environment"
+ );
+}
+
+let modelsToAdd;
+if(document.querySelectorAll('three-model-block').length > 0) {
+ modelsToAdd = document.querySelectorAll('three-model-block');
+} else {
+ modelsToAdd = document.querySelectorAll(
+ ".three-object-three-app-model-block"
+ );
+}
+
+let networkingBlock;
+if(document.querySelectorAll('three-networking-block').length > 0) {
+ networkingBlock = document.querySelectorAll('three-networking-block');
+} else {
+ networkingBlock = document.querySelectorAll(
+ ".three-object-three-app-networking-block"
+ );
+}
+
+let npcsToAdd;
+if(document.querySelectorAll('three-npc-block').length > 0) {
+ npcsToAdd = document.querySelectorAll('three-npc-block');
+} else {
+ npcsToAdd = document.querySelectorAll(
+ ".three-object-three-app-npc-block"
+ );
+}
+
+let textToAdd;
+if(document.querySelectorAll('three-text-block').length > 0) {
+ textToAdd = document.querySelectorAll('three-text-block');
+} else {
+ textToAdd = document.querySelectorAll(
+ ".three-object-three-app-three-text-block"
+ );
+}
+
+let portalsToAdd;
+if(document.querySelectorAll('three-portal-block').length > 0) {
+ portalsToAdd = document.querySelectorAll('three-portal-block');
+} else {
+ portalsToAdd = document.querySelectorAll(
+ ".three-object-three-app-three-portal-block"
+ );
+}
+
+let sky;
+if(document.querySelectorAll('three-sky-block').length > 0) {
+ sky = document.querySelectorAll('three-sky-block');
+} else {
+ sky = document.querySelectorAll(".three-object-three-app-sky-block");
+}
+
+let imagesToAdd;
+if(document.querySelectorAll('three-image-block').length > 0) {
+ imagesToAdd = document.querySelectorAll('three-image-block');
+} else {
+ imagesToAdd = document.querySelectorAll(
+ ".three-object-three-app-image-block"
+ );
+}
+
+let spawnToAdd;
+if(document.querySelectorAll('three-spawn-point-block').length > 0) {
+ spawnToAdd = document.querySelectorAll('three-spawn-point-block');
+} else {
+ spawnToAdd = document.querySelectorAll(
+ ".three-object-three-app-spawn-point-block"
+ );
+}
+
+let videosToAdd;
+if(document.querySelectorAll('three-video-block').length > 0) {
+ videosToAdd = document.querySelectorAll('three-video-block');
+} else {
+ videosToAdd = document.querySelectorAll(".three-object-three-app-video-block");
+}
-const threeApp = document.querySelectorAll(
- ".three-object-three-app-environment"
-);
-
-const modelsToAdd = document.querySelectorAll(
- ".three-object-three-app-model-block"
-);
-const npcsToAdd = document.querySelectorAll(
- ".three-object-three-app-npc-block"
-);
-const htmlToAdd = document.querySelectorAll(
- ".three-object-three-app-three-text-block"
-);
-const portalsToAdd = document.querySelectorAll(
- ".three-object-three-app-three-portal-block"
-);
-const sky = document.querySelectorAll(".three-object-three-app-sky-block");
-const imagesToAdd = document.querySelectorAll(
- ".three-object-three-app-image-block"
-);
-const spawnToAdd = document.querySelectorAll(
- ".three-object-three-app-spawn-point-block"
-);
-const videosToAdd = document.querySelectorAll(
- ".three-object-three-app-video-block"
-);
-const audiosToAdd = document.querySelectorAll(
- ".three-object-three-app-audio-block"
-);
-
-const lightsToAdd = document.querySelectorAll(
- ".three-object-three-app-light-block"
-);
+let audiosToAdd;
+if(document.querySelectorAll('three-audio-block').length > 0) {
+ audiosToAdd = document.querySelectorAll('three-audio-block');
+} else {
+ audiosToAdd = document.querySelectorAll(
+ ".three-object-three-app-audio-block"
+ );
+}
+
+let lightsToAdd;
+if(document.querySelectorAll('three-light-block').length > 0) {
+ lightsToAdd = document.querySelectorAll('three-light-block');
+} else {
+ lightsToAdd = document.querySelectorAll(
+ ".three-object-three-app-light-block"
+ );
+}
// All blocks.
-window.threeApp = threeApp[0].querySelectorAll("div");
+if(threeApp[0]){
+ window.threeApp = threeApp[0].querySelectorAll("div");
+}
threeApp.forEach((threeApp) => {
+ const root = createRoot( threeApp );
+
if (threeApp) {
const hdr = document.querySelector(
"p.three-object-block-hdr"
)? document.querySelector(
"p.three-object-block-hdr"
).innerText : "";
+ let spawnPoint;
+ let spawnPointX;
+ let spawnPointY;
+ let spawnPointZ;
+ let spawnPointRotationX;
+ let spawnPointRotationY;
+ let spawnPointRotationZ;
+ let savedPoint = spawnToAdd[0];
+ let camCollisions = true;
+ if(savedPoint?.tagName.toLowerCase() === 'three-spawn-point-block') {
+ spawnPointX = savedPoint.getAttribute('positionX');
+ spawnPointY = savedPoint.getAttribute('positionY');
+ spawnPointZ = savedPoint.getAttribute('positionZ');
+ spawnPointRotationX = savedPoint.getAttribute('rotationX');
+ spawnPointRotationY = savedPoint.getAttribute('rotationY');
+ spawnPointRotationZ = savedPoint.getAttribute('rotationZ');
+ spawnPoint = [
+ spawnPointX ? spawnPointX : 0,
+ spawnPointY ? spawnPointY : 0,
+ spawnPointZ ? spawnPointZ : 0,
+ ];
+ } else {
+ spawnPointX = spawnToAdd[0].querySelector( "p.spawn-point-block-positionX" );
+ spawnPointY = spawnToAdd[0].querySelector( "p.spawn-point-block-positionY" );
+ spawnPointZ = spawnToAdd[0].querySelector( "p.spawn-point-block-positionZ" );
+ spawnPointRotationX = spawnToAdd[0].querySelector( "p.spawn-point-block-rotationX" );
+ spawnPointRotationY = spawnToAdd[0].querySelector( "p.spawn-point-block-rotationY" );
+ spawnPointRotationZ = spawnToAdd[0].querySelector( "p.spawn-point-block-rotationZ" );
+ spawnPoint = [
+ spawnPointX ? spawnPointX.innerText : 0,
+ spawnPointY ? spawnPointY.innerText : 0,
+ spawnPointZ ? spawnPointZ.innerText : 0,
+ ];
+ }
+
+ let threeUrl, threePreviewImage, deviceTarget, backgroundColor, zoom, scale, hasZoom, hasTip, positionY, rotationY, animations;
+ if(threeApp.tagName.toLowerCase() === 'three-environment-block') {
+ threeUrl = threeApp.getAttribute('threeObjectUrl');
+ threePreviewImage = threeApp.getAttribute('threePreviewImage');
+ deviceTarget = threeApp.getAttribute('deviceTarget');
+ backgroundColor = threeApp.getAttribute('bg_color');
+ zoom = threeApp.getAttribute('zoom');
+ scale = threeApp.getAttribute('scale');
+ hasZoom = threeApp.getAttribute('hasZoom');
+ hasTip = threeApp.getAttribute('hasTip');
+ positionY = threeApp.getAttribute('positionY');
+ rotationY = threeApp.getAttribute('rotationY');
+ animations = threeApp.getAttribute('animations');
+ camCollisions = threeApp.getAttribute('camCollisions') ? threeApp.getAttribute('camCollisions') : true;
+ } else {
+ threeUrl = threeApp.querySelector("p.three-object-block-url")
+ ? threeApp.querySelector("p.three-object-block-url").innerText
+ : "";
+ threePreviewImage = threeApp.querySelector(
+ "p.three-object-preview-image"
+ )
+ ? threeApp.querySelector("p.three-object-preview-image").innerText
+ : "";
+ deviceTarget = threeApp.querySelector(
+ "p.three-object-block-device-target"
+ )
+ ? threeApp.querySelector("p.three-object-block-device-target")
+ .innerText
+ : "2D";
+ backgroundColor = threeApp.querySelector(
+ "p.three-object-background-color"
+ )
+ ? threeApp.querySelector("p.three-object-background-color")
+ .innerText
+ : "#ffffff";
+ zoom = threeApp.querySelector("p.three-object-zoom")
+ ? threeApp.querySelector("p.three-object-zoom").innerText
+ : 90;
+ scale = threeApp.querySelector("p.three-object-scale")
+ ? threeApp.querySelector("p.three-object-scale").innerText
+ : 1;
+ hasZoom = threeApp.querySelector("p.three-object-has-zoom")
+ ? threeApp.querySelector("p.three-object-has-zoom").innerText
+ : false;
+ hasTip = threeApp.querySelector("p.three-object-has-tip")
+ ? threeApp.querySelector("p.three-object-has-tip").innerText
+ : true;
+ positionY = threeApp.querySelector("p.three-object-position-y")
+ ? threeApp.querySelector("p.three-object-position-y").innerText
+ : 0;
+ rotationY = threeApp.querySelector("p.three-object-rotation-y")
+ ? threeApp.querySelector("p.three-object-rotation-y").innerText
+ : 0;
+ animations = threeApp.querySelector("p.three-object-animations")
+ ? threeApp.querySelector("p.three-object-animations").innerText
+ : "";
+ }
- const spawnPoint =
- spawnToAdd.length !== 0
- ? [
- spawnToAdd[0].querySelector(
- "p.spawn-point-block-positionX"
- ).innerText,
- spawnToAdd[0].querySelector(
- "p.spawn-point-block-positionY"
- ).innerText,
- spawnToAdd[0].querySelector(
- "p.spawn-point-block-positionZ"
- ).innerText
- ]
- : [0, 0, 0];
- const threeUrl = threeApp.querySelector("p.three-object-block-url")
- ? threeApp.querySelector("p.three-object-block-url").innerText
- : "";
- const threePreviewImage = threeApp.querySelector(
- "p.three-object-preview-image"
- )
- ? threeApp.querySelector("p.three-object-preview-image").innerText
- : "";
- const deviceTarget = threeApp.querySelector(
- "p.three-object-block-device-target"
- )
- ? threeApp.querySelector("p.three-object-block-device-target")
- .innerText
- : "2D";
- const backgroundColor = threeApp.querySelector(
- "p.three-object-background-color"
- )
- ? threeApp.querySelector("p.three-object-background-color")
- .innerText
- : "#ffffff";
- const zoom = threeApp.querySelector("p.three-object-zoom")
- ? threeApp.querySelector("p.three-object-zoom").innerText
- : 90;
- const scale = threeApp.querySelector("p.three-object-scale")
- ? threeApp.querySelector("p.three-object-scale").innerText
- : 1;
- const hasZoom = threeApp.querySelector("p.three-object-has-zoom")
- ? threeApp.querySelector("p.three-object-has-zoom").innerText
- : false;
- const hasTip = threeApp.querySelector("p.three-object-has-tip")
- ? threeApp.querySelector("p.three-object-has-tip").innerText
- : true;
- const positionY = threeApp.querySelector("p.three-object-position-y")
- ? threeApp.querySelector("p.three-object-position-y").innerText
- : 0;
- const rotationY = threeApp.querySelector("p.three-object-rotation-y")
- ? threeApp.querySelector("p.three-object-rotation-y").innerText
- : 0;
- const animations = threeApp.querySelector("p.three-object-animations")
- ? threeApp.querySelector("p.three-object-animations").innerText
- : "";
-
- render(
+ root.render(
<>
- {/*
-
-
Peers
-
-
Messages
-
-
Connect Audio
-
-
*/}
- {/* */}
+ <>
+
+ {/*
Room:
*/}
+ {/*
Peers
*/}
+ {/*
*/}
+ {/*
Messages
*/}
+
+
+
+
+
+ { ( networkingBlock.length > 0 ) && (
+
+ JOIN
+ VOICE
+
+ )}
+
+
+
+
+
+ >
{
positionY={positionY}
rotationY={rotationY}
animations={animations}
+ camCollisions={camCollisions}
backgroundColor={backgroundColor}
userData={userData}
postSlug={postSlug}
defaultAvatarAnimation={defaultAvatarAnimation}
+ networkingBlock={networkingBlock}
modelsToAdd={modelsToAdd}
portalsToAdd={portalsToAdd}
imagesToAdd={imagesToAdd}
@@ -141,14 +285,87 @@ threeApp.forEach((threeApp) => {
audiosToAdd={audiosToAdd}
lightsToAdd={lightsToAdd}
spawnPoint={spawnPoint ? spawnPoint : null}
- htmlToAdd={htmlToAdd}
+ textToAdd={textToAdd}
npcsToAdd={npcsToAdd}
sky={sky ? sky : ""}
previewImage={threePreviewImage}
hdr ={hdr ? hdr : ""}
/>
-\ >,
- threeApp
+ >
+ );
+ }
+});
+
+threeObjectViewerBlocks.forEach((threeApp) => {
+ const root = createRoot( threeApp );
+
+ let threeUrl, deviceTarget, backgroundColor, zoom, scale, hasZoom, hasTip, positionY, rotationY, animations;
+ if (threeApp) {
+ if(threeApp.tagName.toLowerCase() === 'three-object-viewer-block') {
+ deviceTarget = threeApp.getAttribute('device-target');
+ threeUrl = threeApp.getAttribute('three-object-url');
+ scale = threeApp.getAttribute('scale');
+ backgroundColor = threeApp.getAttribute('bg-color');
+ zoom = threeApp.getAttribute('zoom');
+ hasZoom = threeApp.getAttribute('has-zoom');
+ hasTip = threeApp.getAttribute('has-tip');
+ positionY = threeApp.getAttribute('position-y');
+ rotationY = threeApp.getAttribute('rotation-y');
+ animations = threeApp.getAttribute('animations');
+ } else {
+ threeUrl = threeApp.querySelector("p.three-object-block-url")
+ ? threeApp.querySelector("p.three-object-block-url").innerText
+ : "";
+ deviceTarget = threeApp.querySelector(
+ "p.three-object-block-device-target"
+ )
+ ? threeApp.querySelector("p.three-object-block-device-target")
+ .innerText
+ : "2D";
+ backgroundColor = threeApp.querySelector(
+ "p.three-object-background-color"
+ )
+ ? threeApp.querySelector("p.three-object-background-color")
+ .innerText
+ : "#ffffff";
+ zoom = threeApp.querySelector("p.three-object-zoom")
+ ? threeApp.querySelector("p.three-object-zoom").innerText
+ : 90;
+ scale = threeApp.querySelector("p.three-object-scale")
+ ? threeApp.querySelector("p.three-object-scale").innerText
+ : 1;
+ hasZoom = threeApp.querySelector("p.three-object-has-zoom")
+ ? threeApp.querySelector("p.three-object-has-zoom").innerText
+ : false;
+ hasTip = threeApp.querySelector("p.three-object-has-tip")
+ ? threeApp.querySelector("p.three-object-has-tip").innerText
+ : true;
+ positionY = threeApp.querySelector("p.three-object-position-y")
+ ? threeApp.querySelector("p.three-object-position-y").innerText
+ : 0;
+ rotationY = threeApp.querySelector("p.three-object-rotation-y")
+ ? threeApp.querySelector("p.three-object-rotation-y").innerText
+ : 0;
+ animations = threeApp.querySelector("p.three-object-animations")
+ ? threeApp.querySelector("p.three-object-animations").innerText
+ : "";
+ }
+ root.render(
+
);
}
});
+
diff --git a/blocks/environment/index.js b/blocks/environment/index.js
index 1caacdd..6110389 100644
--- a/blocks/environment/index.js
+++ b/blocks/environment/index.js
@@ -3,8 +3,7 @@ import Edit from "./Edit";
import Save from "./Save";
import { useBlockProps } from "@wordpress/block-editor";
import React from "react";
-
-import { useState } from '@wordpress/element';
+import Deprecated from "./Deprecated";
function Loading() {
return (
@@ -29,7 +28,7 @@ const icon = (
);
-
+const deprecated = Deprecated();
const blockConfig = require("./block.json");
registerBlockType(blockConfig.name, {
...blockConfig,
@@ -37,261 +36,5 @@ registerBlockType(blockConfig.name, {
apiVersion: 2,
edit: Edit,
save: Save,
- deprecated: [
- {
- attributes: {
- bg_color: {
- type: "string",
- default: "#FFFFFF"
- },
- zoom: {
- type: "integer",
- default: 90
- },
- scale: {
- type: "integer",
- default: 1
- },
- positionX: {
- type: "integer",
- default: 0
- },
- positionY: {
- type: "integer",
- default: 0
- },
- rotationY: {
- type: "integer",
- default: 0
- },
- threeObjectUrl: {
- type: "string",
- default: null
- },
- hasZoom: {
- type: "bool",
- default: false
- },
- hasTip: {
- type: "bool",
- default: true
- },
- deviceTarget: {
- type: "string",
- default: "2d"
- }
- },
- save(props) {
- return (
-
- <>
-
-
- {props.attributes.deviceTarget}
-
-
- {props.attributes.threeObjectUrl}
-
-
- {props.attributes.scale}
-
-
- {props.attributes.bg_color}
-
-
- {props.attributes.zoom}
-
-
- {props.attributes.hasZoom ? 1 : 0}
-
-
- {props.attributes.hasTip ? 1 : 0}
-
-
- {props.attributes.positionY}
-
-
- {props.attributes.rotationY}
-
-
- {props.attributes.scale}
-
-
- >
-
- );
- }
- },
- {
- attributes: {
- bg_color: {
- type: "string",
- default: "#FFFFFF"
- },
- zoom: {
- type: "integer",
- default: 90
- },
- scale: {
- type: "integer",
- default: 1
- },
- positionX: {
- type: "integer",
- default: 0
- },
- positionY: {
- type: "integer",
- default: 0
- },
- rotationY: {
- type: "integer",
- default: 0
- },
- threeObjectUrl: {
- type: "string",
- default: null
- },
- hasZoom: {
- type: "bool",
- default: false
- },
- hasTip: {
- type: "bool",
- default: true
- },
- deviceTarget: {
- type: "string",
- default: "2d"
- },
- animations: {
- type: "string",
- default: ""
- }
- },
- save(props) {
- return (
-
- <>
-
-
- {props.attributes.deviceTarget}
-
-
- {props.attributes.threeObjectUrl}
-
-
- {props.attributes.scale}
-
-
- {props.attributes.bg_color}
-
-
- {props.attributes.zoom}
-
-
- {props.attributes.hasZoom ? 1 : 0}
-
-
- {props.attributes.hasTip ? 1 : 0}
-
-
- {props.attributes.positionY}
-
-
- {props.attributes.rotationY}
-
-
- {props.attributes.scale}
-
-
- {props.attributes.animations}
-
-
- >
-
- );
- }
- },
- {
- attributes: {
- align: {
- type: "string",
- default: "full"
- },
- scale: {
- type: "integer",
- default: 1
- },
- positionX: {
- type: "integer",
- default: 0
- },
- positionY: {
- type: "integer",
- default: 0
- },
- rotationY: {
- type: "integer",
- default: 0
- },
- threeObjectUrl: {
- type: "string",
- default: null
- },
- threePreviewImage: {
- type: "string",
- default: null
- },
- deviceTarget: {
- type: "string",
- default: "vr"
- },
- animations: {
- type: "string",
- default: ""
- }
- },
- save(props) {
- return (
-
- <>
-
-
- {props.attributes.deviceTarget}
-
-
- {props.attributes.threeObjectUrl}
-
-
{props.attributes.scale}
-
- {props.attributes.bg_color}
-
-
{props.attributes.zoom}
-
- {props.attributes.hasZoom ? 1 : 0}
-
-
- {props.attributes.hasTip ? 1 : 0}
-
-
- {props.attributes.positionY}
-
-
- {props.attributes.rotationY}
-
-
{props.attributes.scale}
-
- {props.attributes.threePreviewImage}
-
-
- {props.attributes.animations}
-
-
- >
-
- );
- }
- }
- ]
+ deprecated: deprecated
});
diff --git a/blocks/environment/style.scss b/blocks/environment/style.scss
index 5e8f67d..ad74d13 100644
--- a/blocks/environment/style.scss
+++ b/blocks/environment/style.scss
@@ -1,3 +1,8 @@
+.asset-item.selected {
+ border: 2px solid #0073aa;
+ box-shadow: 0 0 5px rgba(0, 115, 170, 0.8);
+ }
+
.wp-block-three-object-block {
background-color: #fff;
color: #000;
diff --git a/blocks/environment/utils/DynLineMesh.js b/blocks/environment/utils/DynLineMesh.js
new file mode 100644
index 0000000..235931a
--- /dev/null
+++ b/blocks/environment/utils/DynLineMesh.js
@@ -0,0 +1,180 @@
+import * as THREE from 'three';
+
+class DynLineMesh extends THREE.LineSegments{
+ _defaultColor = 0x00ff00;
+ _cnt = 0;
+ _verts = [];
+ _color = [];
+ _config = [];
+ _dirty = false;
+
+ constructor( initSize = 20 ){
+ super(
+ _newDynLineMeshGeometry(
+ new Float32Array( initSize * 2 * 3 ), // Two Points for Each Line
+ new Float32Array( initSize * 2 * 3 ),
+ new Float32Array( initSize * 2 * 1 ),
+ false
+ ),
+ newDynLineMeshMaterial() //new THREE.PointsMaterial( { color: 0xffffff, size:8, sizeAttenuation:false } )
+ );
+
+ this.geometry.setDrawRange( 0, 0 );
+ this.onBeforeRender = ()=>{ if( this._dirty ) this._updateGeometry(); }
+ }
+
+ reset(){
+ this._cnt = 0;
+ this._verts.length = 0;
+ this._color.length = 0;
+ this._config.length = 0;
+ this.geometry.setDrawRange( 0, 0 );
+ return this;
+ }
+
+ add( p0, p1, color0=this._defaultColor, color1=null, isDash=false ){
+ this._verts.push( p0[0], p0[1], p0[2], p1[0], p1[1], p1[2] );
+ this._color.push( ...glColor( color0 ), ...glColor( (color1 != null) ? color1:color0 ) );
+
+ if( isDash ){
+ const len = Math.sqrt(
+ (p1[0] - p0[0]) ** 2 +
+ (p1[1] - p0[1]) ** 2 +
+ (p1[2] - p0[2]) ** 2
+ );
+ this._config.push( 0, len );
+ }else{
+ this._config.push( 0, 0 );
+ }
+
+ this._cnt++;
+ this._dirty = true;
+ return this;
+ }
+
+ _updateGeometry(){
+ const geo = this.geometry;
+ const bVerts = geo.attributes.position;
+ const bColor = geo.attributes.color; //this.geometry.index;
+ const bConfig = geo.attributes.config;
+
+ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ if( this._verts.length > bVerts.array.length ||
+ this._color.length > bColor.array.length ||
+ this._config.length > bConfig.array.length
+ ){
+ if( this.geometry ) this.geometry.dispose();
+ this.geometry = _newDynLineMeshGeometry( this._verts, this._color, this._config );
+ this._dirty = false;
+ return;
+ }
+
+ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ bVerts.array.set( this._verts );
+ bVerts.count = this._verts.length / 3;
+ bVerts.needsUpdate = true;
+
+ bColor.array.set( this._color );
+ bColor.count = this._color.length / 3;
+ bColor.needsUpdate = true;
+
+ bConfig.array.set( this._config );
+ bConfig.count = this._config.length / 1;
+ bConfig.needsUpdate = true;
+
+ geo.setDrawRange( 0, bVerts.count );
+ geo.computeBoundingBox();
+ geo.computeBoundingSphere();
+
+ this._dirty = false;
+ }
+}
+
+//#region SUPPORT
+function _newDynLineMeshGeometry( aVerts, aColor, aConfig, doCompute=true ){
+ //if( !( aVerts instanceof Float32Array) ) aVerts = new Float32Array( aVerts );
+ //if( !( aColor instanceof Float32Array) ) aColor = new Float32Array( aColor );
+
+ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ const bVerts = new THREE.Float32BufferAttribute( aVerts, 3 );
+ const bColor = new THREE.Float32BufferAttribute( aColor, 3 );
+ const bConfig = new THREE.Float32BufferAttribute( aConfig, 1 );
+ bVerts.setUsage( THREE.DynamicDrawUsage );
+ bColor.setUsage( THREE.DynamicDrawUsage );
+ bConfig.setUsage( THREE.DynamicDrawUsage );
+
+ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ const geo = new THREE.BufferGeometry();
+ geo.setAttribute( 'position', bVerts );
+ geo.setAttribute( 'color', bColor );
+ geo.setAttribute( 'config', bConfig );
+
+ if( doCompute ){
+ geo.computeBoundingSphere();
+ geo.computeBoundingBox();
+ }
+ return geo;
+}
+
+function glColor( hex, out = null ){
+ const NORMALIZE_RGB = 1 / 255;
+ out = out || [0,0,0];
+
+ out[0] = ( hex >> 16 & 255 ) * NORMALIZE_RGB;
+ out[1] = ( hex >> 8 & 255 ) * NORMALIZE_RGB;
+ out[2] = ( hex & 255 ) * NORMALIZE_RGB;
+
+ return out;
+}
+//#endregion
+
+//#region SHADER
+
+function newDynLineMeshMaterial(){
+ return new THREE.RawShaderMaterial({
+ depthTest : false,
+ transparent : true,
+ uniforms : {
+ dashSeg : { value : 1 / 0.07 },
+ dashDiv : { value : 0.4 },
+ },
+ vertexShader : `#version 300 es
+ in vec3 position;
+ in vec3 color;
+ in float config;
+
+ uniform mat4 modelViewMatrix;
+ uniform mat4 projectionMatrix;
+ uniform float u_scale;
+
+ out vec3 fragColor;
+ out float fragLen;
+
+ void main(){
+ vec4 wPos = modelViewMatrix * vec4( position, 1.0 );
+
+ fragColor = color;
+ fragLen = config;
+
+ gl_Position = projectionMatrix * wPos;
+ }`,
+ fragmentShader : `#version 300 es
+ precision mediump float;
+
+ uniform float dashSeg;
+ uniform float dashDiv;
+
+ in vec3 fragColor;
+ in float fragLen;
+ out vec4 outColor;
+
+ void main(){
+ float alpha = 1.0;
+ if( fragLen > 0.0 ) alpha = step( dashDiv, fract( fragLen * dashSeg ) );
+ outColor = vec4( fragColor, alpha );
+ }`});
+}
+
+//#endregion
+
+export default DynLineMesh;
\ No newline at end of file
diff --git a/blocks/environment/utils/ShapePointsMesh.js b/blocks/environment/utils/ShapePointsMesh.js
new file mode 100644
index 0000000..8842a53
--- /dev/null
+++ b/blocks/environment/utils/ShapePointsMesh.js
@@ -0,0 +1,278 @@
+import * as THREE from 'three';
+
+class ShapePointsMesh extends THREE.Points{
+ _defaultShape = 1;
+ _defaultSize = 6;
+ _defaultColor = 0x00ff00;
+ _cnt = 0;
+ _verts = [];
+ _color = [];
+ _config = [];
+ _dirty = false;
+
+ constructor( initSize = 20 ){
+ super(
+ _newShapePointsMeshGeometry(
+ new Float32Array( initSize * 3 ),
+ new Float32Array( initSize * 3 ),
+ new Float32Array( initSize * 2 ),
+ false
+ ),
+ newShapePointsMeshMaterial() //new THREE.PointsMaterial( { color: 0xffffff, size:8, sizeAttenuation:false } )
+ );
+
+ this.geometry.setDrawRange( 0, 0 );
+ this.onBeforeRender = ()=>{ if( this._dirty ) this._updateGeometry(); }
+ }
+
+ reset(){
+ this._cnt = 0;
+ this._verts.length = 0;
+ this._color.length = 0;
+ this._config.length = 0;
+ this.geometry.setDrawRange( 0, 0 );
+ return this;
+ }
+
+ add( pos, color = this._defaultColor, size = this._defaultSize, shape = this._defaultShape ){
+ this._verts.push( pos[0], pos[1], pos[2] );
+ this._color.push( ...glColor( color ) );
+ this._config.push( size, shape );
+ this._cnt++;
+ this._dirty = true;
+ return this;
+ }
+
+ setColorAt( idx, color ){
+ const c = glColor( color );
+ idx *= 3;
+
+ this._color[ idx ] = c[ 0 ];
+ this._color[ idx + 1 ] = c[ 1 ];
+ this._color[ idx + 2 ] = c[ 2 ];
+ this._dirty = true;
+ return this;
+ }
+
+ setPosAt( idx, pos ){
+ idx *= 3;
+ this._verts[ idx ] = pos[ 0 ];
+ this._verts[ idx + 1 ] = pos[ 1 ];
+ this._verts[ idx + 2 ] = pos[ 2 ];
+ this._dirty = true;
+ return this;
+ }
+
+ getPosAt( idx ){
+ idx *= 3;
+ return [
+ this._verts[ idx + 0 ],
+ this._verts[ idx + 1 ],
+ this._verts[ idx + 2 ],
+ ];
+ }
+
+ _updateGeometry(){
+ const geo = this.geometry;
+ const bVerts = geo.attributes.position;
+ const bColor = geo.attributes.color; //this.geometry.index;
+ const bConfig = geo.attributes.config;
+
+ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ if( this._verts.length > bVerts.array.length ||
+ this._color.length > bColor.array.length ||
+ this._config.length > bConfig.array.length
+ ){
+ if( this.geometry ) this.geometry.dispose();
+ this.geometry = _newShapePointsMeshGeometry( this._verts, this._color, this._config );
+ this._dirty = false;
+ return;
+ }
+
+ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ bVerts.array.set( this._verts );
+ bVerts.count = this._verts.length / 3;
+ bVerts.needsUpdate = true;
+
+ bColor.array.set( this._color );
+ bColor.count = this._color.length / 3;
+ bColor.needsUpdate = true;
+
+ bConfig.array.set( this._config );
+ bConfig.count = this._config.length / 2;
+ bConfig.needsUpdate = true;
+
+ geo.setDrawRange( 0, bVerts.count );
+ geo.computeBoundingBox();
+ geo.computeBoundingSphere();
+
+ this._dirty = false;
+ }
+}
+
+//#region SUPPORT
+function _newShapePointsMeshGeometry( aVerts, aColor, aConfig, doCompute=true ){
+ //if( !( aVerts instanceof Float32Array) ) aVerts = new Float32Array( aVerts );
+ //if( !( aColor instanceof Float32Array) ) aColor = new Float32Array( aColor );
+
+ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ const bVerts = new THREE.Float32BufferAttribute( aVerts, 3 );
+ const bColor = new THREE.Float32BufferAttribute( aColor, 3 );
+ const bConfig = new THREE.Float32BufferAttribute( aConfig, 2 );
+ bVerts.setUsage( THREE.DynamicDrawUsage );
+ bColor.setUsage( THREE.DynamicDrawUsage );
+ bConfig.setUsage( THREE.DynamicDrawUsage );
+
+ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ const geo = new THREE.BufferGeometry();
+ geo.setAttribute( 'position', bVerts );
+ geo.setAttribute( 'color', bColor );
+ geo.setAttribute( 'config', bConfig );
+
+ if( doCompute ){
+ geo.computeBoundingSphere();
+ geo.computeBoundingBox();
+ }
+ return geo;
+}
+
+function glColor( hex, out = null ){
+ const NORMALIZE_RGB = 1 / 255;
+ out = out || [0,0,0];
+
+ out[0] = ( hex >> 16 & 255 ) * NORMALIZE_RGB;
+ out[1] = ( hex >> 8 & 255 ) * NORMALIZE_RGB;
+ out[2] = ( hex & 255 ) * NORMALIZE_RGB;
+
+ return out;
+}
+//#endregion
+
+//#region SHADER
+
+function newShapePointsMeshMaterial(){
+
+ return new THREE.RawShaderMaterial({
+ depthTest : false,
+ transparent : true,
+ uniforms : { u_scale:{ value : 20.0 } },
+ vertexShader : `#version 300 es
+ in vec3 position;
+ in vec3 color;
+ in vec2 config;
+
+ uniform mat4 modelViewMatrix;
+ uniform mat4 projectionMatrix;
+ uniform float u_scale;
+
+ out vec3 fragColor;
+ flat out int fragShape;
+
+ void main(){
+ vec4 wPos = modelViewMatrix * vec4( position.xyz, 1.0 );
+
+ fragColor = color;
+ fragShape = int( config.y );
+
+ gl_Position = projectionMatrix * wPos;
+ gl_PointSize = config.x * ( u_scale / -wPos.z );
+
+ // Get pnt to be World Space Size
+ //gl_PointSize = view_port_size.y * projectionMatrix[1][5] * 1.0 / gl_Position.w;
+ //gl_PointSize = view_port_size.y * projectionMatrix[1][1] * 1.0 / gl_Position.w;
+ }`,
+ fragmentShader : `#version 300 es
+ precision mediump float;
+
+ #define PI 3.14159265359
+ #define PI2 6.28318530718
+
+ in vec3 fragColor;
+ flat in int fragShape;
+ out vec4 outColor;
+
+ float circle(){
+ vec2 coord = gl_PointCoord * 2.0 - 1.0; // v_uv * 2.0 - 1.0;
+ float radius = dot( coord, coord );
+ float dxdy = fwidth( radius );
+ return smoothstep( 0.90 + dxdy, 0.90 - dxdy, radius );
+ }
+
+ float ring( float inner ){
+ vec2 coord = gl_PointCoord * 2.0 - 1.0;
+ float radius = dot( coord, coord );
+ float dxdy = fwidth( radius );
+ return smoothstep( inner - dxdy, inner + dxdy, radius ) -
+ smoothstep( 1.0 - dxdy, 1.0 + dxdy, radius );
+ }
+
+ float diamond(){
+ // http://www.numb3r23.net/2015/08/17/using-fwidth-for-distance-based-anti-aliasing/
+ const float radius = 0.5;
+
+ float dst = dot( abs(gl_PointCoord-vec2(0.5)), vec2(1.0) );
+ float aaf = fwidth( dst );
+ return 1.0 - smoothstep( radius - aaf, radius, dst );
+ }
+
+ float poly( int sides, float offset, float scale ){
+ // https://thebookofshaders.com/07/
+ vec2 coord = gl_PointCoord * 2.0 - 1.0;
+
+ coord.y += offset;
+ coord *= scale;
+
+ float a = atan( coord.x, coord.y ) + PI; // Angle of Pixel
+ float r = PI2 / float( sides ); // Radius of Pixel
+ float d = cos( floor( 0.5 + a / r ) * r-a ) * length( coord );
+ float f = fwidth( d );
+ return smoothstep( 0.5, 0.5 - f, d );
+ }
+
+ // signed distance to a n-star polygon with external angle en
+ float sdStar( float r, int n, float m ){ // m=[2,n]
+ vec2 p = vec2( gl_PointCoord.x, 1.0 - gl_PointCoord.y ) * 2.0 - 1.0;
+
+ // these 4 lines can be precomputed for a given shape
+ float an = 3.141593/float(n);
+ float en = 3.141593/m;
+ vec2 acs = vec2(cos(an),sin(an));
+ vec2 ecs = vec2(cos(en),sin(en)); // ecs=vec2(0,1) and simplify, for regular polygon,
+
+ // reduce to first sector
+ float bn = mod(atan(p.x,p.y),2.0*an) - an;
+ p = length(p)*vec2(cos(bn),abs(sin(bn)));
+
+ // line sdf
+ p -= r*acs;
+ p += ecs*clamp( -dot(p,ecs), 0.0, r*acs.y/ecs.y);
+
+ float dist = length(p)*sign(p.x);
+ float f = fwidth( dist );
+
+ return smoothstep( 0.0, 0.0 - f, dist );
+ }
+
+
+ void main(){
+ float alpha = 1.0;
+
+ if( fragShape == 1 ) alpha = circle();
+ if( fragShape == 2 ) alpha = diamond();
+ if( fragShape == 3 ) alpha = poly( 3, 0.2, 1.0 ); // Triangle
+ if( fragShape == 4 ) alpha = poly( 5, 0.0, 0.65 ); // Pentagram
+ if( fragShape == 5 ) alpha = poly( 6, 0.0, 0.65 ); // Hexagon
+ if( fragShape == 6 ) alpha = ring( 0.2 );
+ if( fragShape == 7 ) alpha = ring( 0.7 );
+ if( fragShape == 8 ) alpha = sdStar( 1.0, 3, 2.3 );
+ if( fragShape == 9 ) alpha = sdStar( 1.0, 6, 2.5 );
+ if( fragShape == 10 ) alpha = sdStar( 1.0, 4, 2.4 );
+ if( fragShape == 11 ) alpha = sdStar( 1.0, 5, 2.8 );
+
+ outColor = vec4( fragColor, alpha );
+ }`});
+}
+
+//#endregion
+
+export default ShapePointsMesh;
\ No newline at end of file
diff --git a/blocks/environment/utils/rigMap.js b/blocks/environment/utils/rigMap.js
new file mode 100644
index 0000000..9501d52
--- /dev/null
+++ b/blocks/environment/utils/rigMap.js
@@ -0,0 +1,63 @@
+
+ /**
+ * A map from Mixamo rig name to VRM Humanoid bone name
+ */
+ const mixamoVRMRigMap = {
+ mixamorigHips: 'hips',
+ mixamorigSpine: 'spine',
+ mixamorigSpine1: 'chest',
+ mixamorigSpine2: 'upperChest',
+ mixamorigNeck: 'neck',
+ mixamorigHead: 'head',
+ mixamorigLeftShoulder: 'leftShoulder',
+ mixamorigLeftArm: 'leftUpperArm',
+ mixamorigLeftForeArm: 'leftLowerArm',
+ mixamorigLeftHand: 'leftHand',
+ mixamorigLeftHandThumb1: 'leftThumbMetacarpal',
+ mixamorigLeftHandThumb2: 'leftThumbProximal',
+ mixamorigLeftHandThumb3: 'leftThumbDistal',
+ mixamorigLeftHandIndex1: 'leftIndexProximal',
+ mixamorigLeftHandIndex2: 'leftIndexIntermediate',
+ mixamorigLeftHandIndex3: 'leftIndexDistal',
+ mixamorigLeftHandMiddle1: 'leftMiddleProximal',
+ mixamorigLeftHandMiddle2: 'leftMiddleIntermediate',
+ mixamorigLeftHandMiddle3: 'leftMiddleDistal',
+ mixamorigLeftHandRing1: 'leftRingProximal',
+ mixamorigLeftHandRing2: 'leftRingIntermediate',
+ mixamorigLeftHandRing3: 'leftRingDistal',
+ mixamorigLeftHandPinky1: 'leftLittleProximal',
+ mixamorigLeftHandPinky2: 'leftLittleIntermediate',
+ mixamorigLeftHandPinky3: 'leftLittleDistal',
+ mixamorigRightShoulder: 'rightShoulder',
+ mixamorigRightArm: 'rightUpperArm',
+ mixamorigRightForeArm: 'rightLowerArm',
+ mixamorigRightHand: 'rightHand',
+ mixamorigRightHandPinky1: 'rightLittleProximal',
+ mixamorigRightHandPinky2: 'rightLittleIntermediate',
+ mixamorigRightHandPinky3: 'rightLittleDistal',
+ mixamorigRightHandRing1: 'rightRingProximal',
+ mixamorigRightHandRing2: 'rightRingIntermediate',
+ mixamorigRightHandRing3: 'rightRingDistal',
+ mixamorigRightHandMiddle1: 'rightMiddleProximal',
+ mixamorigRightHandMiddle2: 'rightMiddleIntermediate',
+ mixamorigRightHandMiddle3: 'rightMiddleDistal',
+ mixamorigRightHandIndex1: 'rightIndexProximal',
+ mixamorigRightHandIndex2: 'rightIndexIntermediate',
+ mixamorigRightHandIndex3: 'rightIndexDistal',
+ mixamorigRightHandThumb1: 'rightThumbMetacarpal',
+ mixamorigRightHandThumb2: 'rightThumbProximal',
+ mixamorigRightHandThumb3: 'rightThumbDistal',
+ mixamorigLeftUpLeg: 'leftUpperLeg',
+ mixamorigLeftLeg: 'leftLowerLeg',
+ mixamorigLeftFoot: 'leftFoot',
+ mixamorigLeftToeBase: 'leftToes',
+ mixamorigRightUpLeg: 'rightUpperLeg',
+ mixamorigRightLeg: 'rightLowerLeg',
+ mixamorigRightFoot: 'rightFoot',
+ mixamorigRightToeBase: 'rightToes',
+};
+
+// make function to return the mixamo rig
+export function getMixamoRig() {
+ return mixamoVRMRigMap;
+}
diff --git a/blocks/model-block/Deprecated.js b/blocks/model-block/Deprecated.js
new file mode 100644
index 0000000..132ab6a
--- /dev/null
+++ b/blocks/model-block/Deprecated.js
@@ -0,0 +1,106 @@
+import { useBlockProps } from "@wordpress/block-editor";
+
+export default function Deprecated(){
+ return [
+ {
+ attributes: {
+ scaleX: {
+ type: "int",
+ default:1
+ },
+ name: {
+ type: "string"
+ },
+ scaleY: {
+ type: "int",
+ default:1
+ },
+ scaleZ: {
+ type: "int",
+ default:1
+ },
+ positionX: {
+ type: "int",
+ default:0
+ },
+ positionY: {
+ type: "int",
+ default:0
+ },
+ positionZ: {
+ type: "int",
+ default:0
+ },
+ rotationX: {
+ type: "int",
+ default:0
+ },
+ rotationY: {
+ type: "int",
+ default:0
+ },
+ rotationZ: {
+ type: "int",
+ default:0
+ },
+ threeObjectUrl: {
+ type: "string",
+ default: null
+ },
+ animations: {
+ type: "string",
+ default: ""
+ },
+ alt: {
+ type: "string",
+ default: ""
+ },
+ collidable: {
+ type: "boolean",
+ default: false
+ }
+ },
+ save(props) {
+ return (
+
+ <>
+
+
+ {props.attributes.threeObjectUrl}
+
+
{props.attributes.scaleX}
+
{props.attributes.scaleY}
+
{props.attributes.scaleZ}
+
+ {props.attributes.positionX}
+
+
+ {props.attributes.positionY}
+
+
+ {props.attributes.positionZ}
+
+
+ {props.attributes.rotationX}
+
+
+ {props.attributes.rotationY}
+
+
+ {props.attributes.rotationZ}
+
+
+ {props.attributes.animations}
+
+
+ {props.attributes.collidable ? 1 : 0}
+
+
{props.attributes.alt}
+
+ >
+
+ );
+ }
+ }
+ ];
+}
\ No newline at end of file
diff --git a/blocks/model-block/Edit.js b/blocks/model-block/Edit.js
index 423d888..50d2462 100644
--- a/blocks/model-block/Edit.js
+++ b/blocks/model-block/Edit.js
@@ -149,6 +149,7 @@ export default function Edit({ attributes, setAttributes, isSelected, clientId }
label={ __( "GLB File", 'three-object-viewer' ) }
allowedTypes={ALLOWED_MEDIA_TYPES}
value={attributes.threeObjectUrl}
+ threeov={true}
render={({ open }) => (
{attributes.threeObjectUrl
@@ -323,6 +324,9 @@ export default function Edit({ attributes, setAttributes, isSelected, clientId }
onSelect={(imageObject) =>
onImageSelect(imageObject)
}
+ additionalProps={{
+ three_object_viewer_modal: true,
+ }}
type="image"
allowedTypes={ALLOWED_MEDIA_TYPES}
value={attributes.threeObjectUrl}
@@ -383,6 +387,7 @@ export default function Edit({ attributes, setAttributes, isSelected, clientId }
}
type="image"
allowedTypes={ALLOWED_MEDIA_TYPES}
+ threeov={true}
value={attributes.threeObjectUrl}
render={({ open }) => (
- <>
-
-
- {attributes.threeObjectUrl}
-
-
{attributes.scaleX}
-
{attributes.scaleY}
-
{attributes.scaleZ}
-
- {attributes.positionX}
-
-
- {attributes.positionY}
-
-
- {attributes.positionZ}
-
-
- {attributes.rotationX}
-
-
- {attributes.rotationY}
-
-
- {attributes.rotationZ}
-
-
- {attributes.animations}
-
-
- {attributes.collidable ? 1 : 0}
-
-
{attributes.alt}
-
- >
-
+
);
}
diff --git a/blocks/model-block/block.json b/blocks/model-block/block.json
index 8d1cb20..11b3304 100644
--- a/blocks/model-block/block.json
+++ b/blocks/model-block/block.json
@@ -54,10 +54,10 @@
},
"collidable": {
"type": "boolean",
- "default": true
+ "default": false
}
},
- "category": "design",
+ "category": "spatial",
"parent": [ "three-object-viewer/environment" ],
"apiVersion": 2,
"supports": {
diff --git a/blocks/model-block/index.js b/blocks/model-block/index.js
index fc9e326..7bf6e78 100644
--- a/blocks/model-block/index.js
+++ b/blocks/model-block/index.js
@@ -2,6 +2,7 @@ import { registerBlockType } from "@wordpress/blocks";
import Edit from "./Edit";
import Save from "./Save";
import { useBlockProps } from "@wordpress/block-editor";
+import Deprecated from "./Deprecated";
const icon = (
);
-
+const deprecated = Deprecated();
const blockConfig = require("./block.json");
registerBlockType(blockConfig.name, {
...blockConfig,
@@ -23,106 +24,5 @@ registerBlockType(blockConfig.name, {
apiVersion: 2,
edit: Edit,
save: Save,
- deprecated: [
- {
- attributes: {
- scaleX: {
- type: "int",
- default:1
- },
- name: {
- type: "string"
- },
- scaleY: {
- type: "int",
- default:1
- },
- scaleZ: {
- type: "int",
- default:1
- },
- positionX: {
- type: "int",
- default:0
- },
- positionY: {
- type: "int",
- default:0
- },
- positionZ: {
- type: "int",
- default:0
- },
- rotationX: {
- type: "int",
- default:0
- },
- rotationY: {
- type: "int",
- default:0
- },
- rotationZ: {
- type: "int",
- default:0
- },
- threeObjectUrl: {
- type: "string",
- default: null
- },
- animations: {
- type: "string",
- default: ""
- },
- alt: {
- type: "string",
- default: ""
- },
- collidable: {
- type: "boolean",
- default: false
- }
- },
- save(props) {
- return (
-
- <>
-
-
- {props.attributes.threeObjectUrl}
-
-
{props.attributes.scaleX}
-
{props.attributes.scaleY}
-
{props.attributes.scaleZ}
-
- {props.attributes.positionX}
-
-
- {props.attributes.positionY}
-
-
- {props.attributes.positionZ}
-
-
- {props.attributes.rotationX}
-
-
- {props.attributes.rotationY}
-
-
- {props.attributes.rotationZ}
-
-
- {props.attributes.animations}
-
-
- {props.attributes.collidable ? 1 : 0}
-
-
{props.attributes.alt}
-
- >
-
- );
- }
- }
- ]
+ deprecated: deprecated
});
diff --git a/blocks/npc-block/Deprecated.js b/blocks/npc-block/Deprecated.js
new file mode 100644
index 0000000..f07cd55
--- /dev/null
+++ b/blocks/npc-block/Deprecated.js
@@ -0,0 +1,269 @@
+import { useBlockProps } from "@wordpress/block-editor";
+
+export default function Deprecated(){
+return [
+ {
+ attributes: {
+ bg_color: {
+ type: "string",
+ default: "#FFFFFF"
+ },
+ zoom: {
+ type: "integer",
+ default: 90
+ },
+ scale: {
+ type: "integer",
+ default: 1
+ },
+ positionX: {
+ type: "integer",
+ default: 0
+ },
+ positionY: {
+ type: "integer",
+ default: 0
+ },
+ rotationY: {
+ type: "integer",
+ default: 0
+ },
+ threeObjectUrl: {
+ type: "string",
+ default: null
+ },
+ hasZoom: {
+ type: "bool",
+ default: false
+ },
+ hasTip: {
+ type: "bool",
+ default: true
+ },
+ deviceTarget: {
+ type: "string",
+ default: "2d"
+ }
+ },
+ save(props) {
+ return (
+
+ <>
+
+
+ {props.attributes.deviceTarget}
+
+
+ {props.attributes.threeObjectUrl}
+
+
+ {props.attributes.scale}
+
+
+ {props.attributes.bg_color}
+
+
+ {props.attributes.zoom}
+
+
+ {props.attributes.hasZoom ? 1 : 0}
+
+
+ {props.attributes.hasTip ? 1 : 0}
+
+
+ {props.attributes.positionY}
+
+
+ {props.attributes.rotationY}
+
+
+ {props.attributes.scale}
+
+
+ >
+
+ );
+ }
+ },
+ {
+ attributes: {
+ bg_color: {
+ type: "string",
+ default: "#FFFFFF"
+ },
+ zoom: {
+ type: "integer",
+ default: 90
+ },
+ scale: {
+ type: "integer",
+ default: 1
+ },
+ positionX: {
+ type: "integer",
+ default: 0
+ },
+ positionY: {
+ type: "integer",
+ default: 0
+ },
+ rotationY: {
+ type: "integer",
+ default: 0
+ },
+ threeObjectUrl: {
+ type: "string",
+ default: null
+ },
+ hasZoom: {
+ type: "bool",
+ default: false
+ },
+ hasTip: {
+ type: "bool",
+ default: true
+ },
+ deviceTarget: {
+ type: "string",
+ default: "2d"
+ },
+ animations: {
+ type: "string",
+ default: ""
+ }
+ },
+ save(props) {
+ return (
+
+ <>
+
+
+ {props.attributes.deviceTarget}
+
+
+ {props.attributes.threeObjectUrl}
+
+
+ {props.attributes.scale}
+
+
+ {props.attributes.bg_color}
+
+
+ {props.attributes.zoom}
+
+
+ {props.attributes.hasZoom ? 1 : 0}
+
+
+ {props.attributes.hasTip ? 1 : 0}
+
+
+ {props.attributes.positionY}
+
+
+ {props.attributes.rotationY}
+
+
+ {props.attributes.scale}
+
+
+ {props.attributes.animations}
+
+
+ >
+
+ );
+ }
+ },
+ {
+ attributes: {
+ name: {
+ type: "string"
+ },
+ personality: {
+ type: "string"
+ },
+ defaultMessage: {
+ type: "string"
+ },
+ positionX: {
+ type: "int",
+ default:0
+ },
+ positionY: {
+ type: "int",
+ default:0
+ },
+ positionZ: {
+ type: "int",
+ default:0
+ },
+ rotationX: {
+ type: "int",
+ default:0
+ },
+ rotationY: {
+ type: "int",
+ default:0
+ },
+ rotationZ: {
+ type: "int",
+ default:0
+ },
+ threeObjectUrl: {
+ type: "string",
+ default: null
+ },
+ objectAwareness: {
+ type: "boolean",
+ default: false
+ }
+ },
+ save(props) {
+ return (
+
+ <>
+
+
+ {props.attributes.threeObjectUrl}
+
+
+ {props.attributes.positionX}
+
+
+ {props.attributes.positionY}
+
+
+ {props.attributes.positionZ}
+
+
+ {props.attributes.rotationX}
+
+
+ {props.attributes.rotationY}
+
+
+ {props.attributes.rotationZ}
+
+
+ {props.attributes.name}
+
+
+ {props.attributes.defaultMessage}
+
+
+ {props.attributes.personality}
+
+
+ {props.attributes.objectAwareness ? 1 : 0}
+
+
+ >
+
+ );
+ }
+ }
+];
+}
\ No newline at end of file
diff --git a/blocks/npc-block/Save.js b/blocks/npc-block/Save.js
index 91a342e..bba140e 100644
--- a/blocks/npc-block/Save.js
+++ b/blocks/npc-block/Save.js
@@ -2,45 +2,22 @@ import { __ } from "@wordpress/i18n";
import { useBlockProps } from "@wordpress/block-editor";
export default function save({ attributes }) {
+ const blockProps = useBlockProps.save();
+
return (
-
- <>
-
-
- {attributes.threeObjectUrl}
-
-
- {attributes.positionX}
-
-
- {attributes.positionY}
-
-
- {attributes.positionZ}
-
-
- {attributes.rotationX}
-
-
- {attributes.rotationY}
-
-
- {attributes.rotationZ}
-
-
- {attributes.name}
-
-
- {attributes.defaultMessage}
-
-
- {attributes.personality}
-
-
- {attributes.objectAwareness ? 1 : 0}
-
-
- >
-
+
);
}
diff --git a/blocks/npc-block/block.json b/blocks/npc-block/block.json
index 6391b3d..3bee901 100644
--- a/blocks/npc-block/block.json
+++ b/blocks/npc-block/block.json
@@ -43,7 +43,7 @@
"default": false
}
},
- "category": "design",
+ "category": "spatial",
"parent": [ "three-object-viewer/environment" ],
"apiVersion": 2,
"supports": {
diff --git a/blocks/npc-block/index.js b/blocks/npc-block/index.js
index 0b83de5..7bf6e78 100644
--- a/blocks/npc-block/index.js
+++ b/blocks/npc-block/index.js
@@ -2,6 +2,7 @@ import { registerBlockType } from "@wordpress/blocks";
import Edit from "./Edit";
import Save from "./Save";
import { useBlockProps } from "@wordpress/block-editor";
+import Deprecated from "./Deprecated";
const icon = (
);
-
+const deprecated = Deprecated();
const blockConfig = require("./block.json");
registerBlockType(blockConfig.name, {
...blockConfig,
@@ -23,181 +24,5 @@ registerBlockType(blockConfig.name, {
apiVersion: 2,
edit: Edit,
save: Save,
- deprecated: [
- {
- attributes: {
- bg_color: {
- type: "string",
- default: "#FFFFFF"
- },
- zoom: {
- type: "integer",
- default: 90
- },
- scale: {
- type: "integer",
- default: 1
- },
- positionX: {
- type: "integer",
- default: 0
- },
- positionY: {
- type: "integer",
- default: 0
- },
- rotationY: {
- type: "integer",
- default: 0
- },
- threeObjectUrl: {
- type: "string",
- default: null
- },
- hasZoom: {
- type: "bool",
- default: false
- },
- hasTip: {
- type: "bool",
- default: true
- },
- deviceTarget: {
- type: "string",
- default: "2d"
- }
- },
- save(props) {
- return (
-
- <>
-
-
- {props.attributes.deviceTarget}
-
-
- {props.attributes.threeObjectUrl}
-
-
- {props.attributes.scale}
-
-
- {props.attributes.bg_color}
-
-
- {props.attributes.zoom}
-
-
- {props.attributes.hasZoom ? 1 : 0}
-
-
- {props.attributes.hasTip ? 1 : 0}
-
-
- {props.attributes.positionY}
-
-
- {props.attributes.rotationY}
-
-
- {props.attributes.scale}
-
-
- >
-
- );
- }
- },
- {
- attributes: {
- bg_color: {
- type: "string",
- default: "#FFFFFF"
- },
- zoom: {
- type: "integer",
- default: 90
- },
- scale: {
- type: "integer",
- default: 1
- },
- positionX: {
- type: "integer",
- default: 0
- },
- positionY: {
- type: "integer",
- default: 0
- },
- rotationY: {
- type: "integer",
- default: 0
- },
- threeObjectUrl: {
- type: "string",
- default: null
- },
- hasZoom: {
- type: "bool",
- default: false
- },
- hasTip: {
- type: "bool",
- default: true
- },
- deviceTarget: {
- type: "string",
- default: "2d"
- },
- animations: {
- type: "string",
- default: ""
- }
- },
- save(props) {
- return (
-
- <>
-
-
- {props.attributes.deviceTarget}
-
-
- {props.attributes.threeObjectUrl}
-
-
- {props.attributes.scale}
-
-
- {props.attributes.bg_color}
-
-
- {props.attributes.zoom}
-
-
- {props.attributes.hasZoom ? 1 : 0}
-
-
- {props.attributes.hasTip ? 1 : 0}
-
-
- {props.attributes.positionY}
-
-
- {props.attributes.rotationY}
-
-
- {props.attributes.scale}
-
-
- {props.attributes.animations}
-
-
- >
-
- );
- }
- }
- ]
+ deprecated: deprecated
});
diff --git a/blocks/sky-block/Deprecated.js b/blocks/sky-block/Deprecated.js
new file mode 100644
index 0000000..509da87
--- /dev/null
+++ b/blocks/sky-block/Deprecated.js
@@ -0,0 +1,69 @@
+import { useBlockProps } from "@wordpress/block-editor";
+
+export default function Deprecated(){
+ return [
+ {
+ attributes: {
+ skyUrl: {
+ type: "string",
+ default: null
+ },
+ },
+ save(props) {
+ return (
+
+ <>
+
+
{props.attributes.skyUrl}
+
+ >
+
+ );
+ }
+ },
+ {
+ attributes: {
+ skyUrl: {
+ type: "string",
+ default: null
+ },
+ distance: {
+ type: "int",
+ default: 170000
+ },
+ rayleigh: {
+ type: "int",
+ default: 1
+ },
+ sunPositionX: {
+ type: "int",
+ default: 0
+ },
+ sunPositionY: {
+ type: "int",
+ default: 10000
+ },
+ sunPositionZ: {
+ type: "int",
+ default: -10000
+ }
+ },
+ save(props) {
+ return (
+
+ <>
+
+
{props.attributes.skyUrl}
+
{props.attributes.distance}
+
{props.attributes.rayleigh}
+
{props.attributes.sunPositionX}
+
{props.attributes.sunPositionY}
+
{props.attributes.sunPositionZ}
+
+ >
+
+ );
+ }
+ }
+ ];
+}
diff --git a/blocks/sky-block/Edit.js b/blocks/sky-block/Edit.js
index 4fdfbee..70125e4 100644
--- a/blocks/sky-block/Edit.js
+++ b/blocks/sky-block/Edit.js
@@ -160,7 +160,7 @@ export default function Edit({ attributes, setAttributes, isSelected }) {
- { __( 'Sky block', 'three-object-viewer' ) }
+ { __( 'Sky Block', 'three-object-viewer' ) }
{/* URL: {attributes.skyUrl}
*/}
@@ -181,7 +181,7 @@ export default function Edit({ attributes, setAttributes, isSelected }) {