Skip to content

Commit 0e62806

Browse files
committed
Add VR client functionality: Implement app.js for the VR client, including WebSocket connection handling, scene setup with Three.js, and basic event listeners for VR interaction. This file lays the groundwork for character mode selection and input handling in the virtual reality environment.
1 parent 24b3272 commit 0e62806

1 file changed

Lines changed: 175 additions & 0 deletions

File tree

VR/client/app.js

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import * as THREE from "three";
2+
import { VRButton } from "three/addons/webxr/VRButton.js";
3+
import { CHARACTER_MODES_REGISTRY } from "./modes/modes_registry.js";
4+
5+
const statusEl = document.getElementById("status");
6+
const startButton = document.getElementById("start-btn");
7+
const joinPanel = document.getElementById("join-panel");
8+
const joinButton = document.getElementById("join-btn");
9+
const characterModeSelect = document.getElementById("character-mode");
10+
let selectedCharacterMode = "demo-spline";
11+
12+
const searchParams = new URLSearchParams(window.location.search);
13+
const defaultWsScheme = window.location.protocol === "https:" ? "wss" : "ws";
14+
const defaultWsHost = window.location.hostname || "127.0.0.1";
15+
const defaultWsPort = searchParams.get("ws_port") ?? "8765";
16+
const serverHost =
17+
searchParams.get("ws") ??
18+
`${defaultWsScheme}://${defaultWsHost}:${defaultWsPort}`;
19+
20+
const calibrationOffset = new THREE.Vector3(
21+
Number(searchParams.get("ox") ?? 0.0),
22+
Number(searchParams.get("oy") ?? 0.0),
23+
Number(searchParams.get("oz") ?? 0.0)
24+
);
25+
26+
let socket = null;
27+
let userId = null;
28+
let sessionRole = "vr_client";
29+
let sessionMode = "vr_client";
30+
31+
const scene = new THREE.Scene();
32+
scene.background = new THREE.Color(0x1a1f2b);
33+
const worldRoot = new THREE.Group();
34+
worldRoot.position.copy(calibrationOffset);
35+
scene.add(worldRoot);
36+
37+
const camera = new THREE.PerspectiveCamera(
38+
70,
39+
window.innerWidth / window.innerHeight,
40+
0.01,
41+
50
42+
);
43+
camera.position.set(0, 1.4, 2.0);
44+
45+
const renderer = new THREE.WebGLRenderer({ antialias: true });
46+
renderer.setSize(window.innerWidth, window.innerHeight);
47+
renderer.xr.enabled = true;
48+
document.body.appendChild(renderer.domElement);
49+
50+
const hemi = new THREE.HemisphereLight(0xffffff, 0x223344, 1.2);
51+
scene.add(hemi);
52+
53+
const grid = new THREE.GridHelper(8, 16, 0x6c757d, 0x495057);
54+
grid.position.y = 0;
55+
worldRoot.add(grid);
56+
const worldAxes = new THREE.AxesHelper(0.3);
57+
worldRoot.add(worldAxes);
58+
59+
function setStatus(text) {
60+
statusEl.textContent = text;
61+
}
62+
63+
function applyCharacterMode(modeKey) {
64+
// TODO: Implement character mode selection
65+
}
66+
67+
function connect(joinConfig) {
68+
socket = new WebSocket(serverHost);
69+
sessionRole = joinConfig.serverRole ?? joinConfig.role;
70+
sessionMode = joinConfig.role;
71+
72+
socket.onopen = () => {
73+
setStatus(`connected: ${serverHost}`);
74+
socket.send(
75+
JSON.stringify({
76+
version: 1,
77+
type: "hello",
78+
payload: {
79+
client: "sparc-webxr",
80+
role: sessionRole,
81+
},
82+
})
83+
);
84+
};
85+
86+
socket.onclose = (event) => {
87+
setStatus(`disconnected: ${serverHost} (code=${event.code})`);
88+
};
89+
socket.onerror = () => {
90+
setStatus(`error: failed to connect ${serverHost}`);
91+
};
92+
93+
socket.onmessage = (event) => {
94+
// TODO: Implement message handling
95+
96+
const message = JSON.parse(event.data);
97+
98+
if (message.type === "error") {
99+
setStatus(`server error: ${message.payload?.reason ?? "unknown"}`);
100+
}
101+
102+
};
103+
}
104+
105+
function sendXRInput(frame) {
106+
// TODO: Implement XR input sending
107+
}
108+
109+
function animate() {
110+
// TODO: Implement animation loop
111+
let lastTime = performance.now() / 1000;
112+
renderer.setAnimationLoop((_, frame) => {
113+
const now = performance.now() / 1000;
114+
const dt = Math.min(0.05, now - lastTime);
115+
lastTime = now;
116+
117+
if (frame) sendXRInput(frame);
118+
119+
renderer.render(scene, camera);
120+
});
121+
}
122+
123+
startButton.addEventListener("click", () => {
124+
document.body.appendChild(VRButton.createButton(renderer));
125+
startButton.remove();
126+
});
127+
128+
window.addEventListener("resize", () => {
129+
camera.aspect = window.innerWidth / window.innerHeight;
130+
camera.updateProjectionMatrix();
131+
renderer.setSize(window.innerWidth, window.innerHeight);
132+
});
133+
134+
/* TODO: Implement key handling for desktop mode */
135+
window.addEventListener("keydown", (event) => {
136+
const key = event.key.toLowerCase();
137+
// TODO: Implement key handling
138+
});
139+
140+
window.addEventListener("keyup", (event) => {
141+
const key = event.key.toLowerCase();
142+
// TODO: Implement key handling
143+
});
144+
/* TODO */
145+
146+
renderer.domElement.addEventListener("click", () => {
147+
if (!renderer.xr.isPresenting && document.pointerLockElement !== renderer.domElement) {
148+
renderer.domElement.requestPointerLock();
149+
}
150+
});
151+
152+
document.addEventListener("mousemove", (event) => {
153+
if (document.pointerLockElement !== renderer.domElement) return;
154+
if (renderer.xr.isPresenting) return;
155+
156+
// TODO: Implement desktop mode mouse movement handling
157+
});
158+
159+
function setupJoinPanel() {
160+
// TODO: Implement join panel setup
161+
}
162+
163+
function populateCharacterModeSelect() {
164+
characterModeSelect.replaceChildren();
165+
for (const { id, label } of CHARACTER_MODES_REGISTRY) {
166+
const opt = document.createElement("option");
167+
opt.value = id;
168+
opt.textContent = label;
169+
characterModeSelect.appendChild(opt);
170+
}
171+
}
172+
173+
populateCharacterModeSelect();
174+
setupJoinPanel();
175+
animate();

0 commit comments

Comments
 (0)