spoint 0.1.0 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +134 -209
- package/SKILL.md +95 -0
- package/apps/environment/index.js +200 -1
- package/apps/environment/models/decorative/.gitkeep +0 -0
- package/apps/environment/models/hazards/.gitkeep +0 -0
- package/apps/environment/models/interactive/.gitkeep +0 -0
- package/apps/environment/models/structures/.gitkeep +0 -0
- package/apps/environment/smartObjects.js +114 -0
- package/apps/interactable/index.js +155 -0
- package/apps/physics-crate/index.js +15 -9
- package/apps/power-crate/index.js +18 -12
- package/apps/tps-game/$GDUPI.vrm +0 -0
- package/apps/tps-game/Cleetus.vrm +0 -0
- package/apps/tps-game/index.js +185 -27
- package/apps/world/index.js +68 -22
- package/bin/create-app.js +337 -0
- package/client/ARControls.js +301 -0
- package/client/LoadingManager.js +117 -0
- package/client/MobileControls.js +1122 -0
- package/client/anim-lib.glb +0 -0
- package/client/animation.js +306 -0
- package/client/app.js +1341 -65
- package/client/camera.js +191 -33
- package/client/createLoadingScreen.js +69 -0
- package/client/editor/bridge.js +113 -0
- package/client/editor/css/main.css +794 -0
- package/client/editor/images/rotate.svg +4 -0
- package/client/editor/images/scale.svg +4 -0
- package/client/editor/images/translate.svg +4 -0
- package/client/editor/index.html +103 -0
- package/client/editor/js/Command.js +41 -0
- package/client/editor/js/Config.js +81 -0
- package/client/editor/js/Editor.js +785 -0
- package/client/editor/js/EditorControls.js +438 -0
- package/client/editor/js/History.js +321 -0
- package/client/editor/js/Loader.js +987 -0
- package/client/editor/js/LoaderUtils.js +90 -0
- package/client/editor/js/Menubar.Add.js +510 -0
- package/client/editor/js/Menubar.Edit.js +145 -0
- package/client/editor/js/Menubar.File.js +466 -0
- package/client/editor/js/Menubar.Help.js +73 -0
- package/client/editor/js/Menubar.Status.js +51 -0
- package/client/editor/js/Menubar.View.js +183 -0
- package/client/editor/js/Menubar.js +27 -0
- package/client/editor/js/Player.js +53 -0
- package/client/editor/js/Resizer.js +58 -0
- package/client/editor/js/Script.js +503 -0
- package/client/editor/js/Selector.js +102 -0
- package/client/editor/js/Sidebar.Geometry.BoxGeometry.js +121 -0
- package/client/editor/js/Sidebar.Geometry.BufferGeometry.js +115 -0
- package/client/editor/js/Sidebar.Geometry.CapsuleGeometry.js +97 -0
- package/client/editor/js/Sidebar.Geometry.CircleGeometry.js +97 -0
- package/client/editor/js/Sidebar.Geometry.CylinderGeometry.js +121 -0
- package/client/editor/js/Sidebar.Geometry.DodecahedronGeometry.js +73 -0
- package/client/editor/js/Sidebar.Geometry.ExtrudeGeometry.js +196 -0
- package/client/editor/js/Sidebar.Geometry.IcosahedronGeometry.js +73 -0
- package/client/editor/js/Sidebar.Geometry.LatheGeometry.js +98 -0
- package/client/editor/js/Sidebar.Geometry.Modifiers.js +73 -0
- package/client/editor/js/Sidebar.Geometry.OctahedronGeometry.js +74 -0
- package/client/editor/js/Sidebar.Geometry.PlaneGeometry.js +97 -0
- package/client/editor/js/Sidebar.Geometry.RingGeometry.js +121 -0
- package/client/editor/js/Sidebar.Geometry.ShapeGeometry.js +76 -0
- package/client/editor/js/Sidebar.Geometry.SphereGeometry.js +133 -0
- package/client/editor/js/Sidebar.Geometry.TetrahedronGeometry.js +74 -0
- package/client/editor/js/Sidebar.Geometry.TorusGeometry.js +109 -0
- package/client/editor/js/Sidebar.Geometry.TorusKnotGeometry.js +121 -0
- package/client/editor/js/Sidebar.Geometry.TubeGeometry.js +135 -0
- package/client/editor/js/Sidebar.Geometry.js +332 -0
- package/client/editor/js/Sidebar.Material.BooleanProperty.js +60 -0
- package/client/editor/js/Sidebar.Material.ColorProperty.js +87 -0
- package/client/editor/js/Sidebar.Material.ConstantProperty.js +62 -0
- package/client/editor/js/Sidebar.Material.MapProperty.js +249 -0
- package/client/editor/js/Sidebar.Material.NumberProperty.js +60 -0
- package/client/editor/js/Sidebar.Material.Program.js +73 -0
- package/client/editor/js/Sidebar.Material.RangeValueProperty.js +63 -0
- package/client/editor/js/Sidebar.Material.js +751 -0
- package/client/editor/js/Sidebar.Object.Animation.js +102 -0
- package/client/editor/js/Sidebar.Object.js +898 -0
- package/client/editor/js/Sidebar.Project.App.js +165 -0
- package/client/editor/js/Sidebar.Project.Image.js +225 -0
- package/client/editor/js/Sidebar.Project.Materials.js +82 -0
- package/client/editor/js/Sidebar.Project.Renderer.js +144 -0
- package/client/editor/js/Sidebar.Project.Video.js +242 -0
- package/client/editor/js/Sidebar.Project.js +31 -0
- package/client/editor/js/Sidebar.Properties.js +73 -0
- package/client/editor/js/Sidebar.Scene.js +585 -0
- package/client/editor/js/Sidebar.Script.js +129 -0
- package/client/editor/js/Sidebar.Settings.History.js +146 -0
- package/client/editor/js/Sidebar.Settings.Shortcuts.js +175 -0
- package/client/editor/js/Sidebar.Settings.js +60 -0
- package/client/editor/js/Sidebar.js +41 -0
- package/client/editor/js/Storage.js +98 -0
- package/client/editor/js/Strings.js +2028 -0
- package/client/editor/js/Toolbar.js +84 -0
- package/client/editor/js/Viewport.Controls.js +92 -0
- package/client/editor/js/Viewport.Info.js +136 -0
- package/client/editor/js/Viewport.Pathtracer.js +91 -0
- package/client/editor/js/Viewport.ViewHelper.js +39 -0
- package/client/editor/js/Viewport.XR.js +222 -0
- package/client/editor/js/Viewport.js +900 -0
- package/client/editor/js/commands/AddObjectCommand.js +68 -0
- package/client/editor/js/commands/AddScriptCommand.js +75 -0
- package/client/editor/js/commands/Commands.js +23 -0
- package/client/editor/js/commands/MoveObjectCommand.js +111 -0
- package/client/editor/js/commands/MultiCmdsCommand.js +85 -0
- package/client/editor/js/commands/RemoveObjectCommand.js +88 -0
- package/client/editor/js/commands/RemoveScriptCommand.js +81 -0
- package/client/editor/js/commands/SetColorCommand.js +73 -0
- package/client/editor/js/commands/SetGeometryCommand.js +87 -0
- package/client/editor/js/commands/SetGeometryValueCommand.js +70 -0
- package/client/editor/js/commands/SetMaterialColorCommand.js +86 -0
- package/client/editor/js/commands/SetMaterialCommand.js +79 -0
- package/client/editor/js/commands/SetMaterialMapCommand.js +143 -0
- package/client/editor/js/commands/SetMaterialRangeCommand.js +91 -0
- package/client/editor/js/commands/SetMaterialValueCommand.js +90 -0
- package/client/editor/js/commands/SetMaterialVectorCommand.js +79 -0
- package/client/editor/js/commands/SetPositionCommand.js +84 -0
- package/client/editor/js/commands/SetRotationCommand.js +84 -0
- package/client/editor/js/commands/SetScaleCommand.js +84 -0
- package/client/editor/js/commands/SetSceneCommand.js +103 -0
- package/client/editor/js/commands/SetScriptValueCommand.js +80 -0
- package/client/editor/js/commands/SetShadowValueCommand.js +73 -0
- package/client/editor/js/commands/SetUuidCommand.js +70 -0
- package/client/editor/js/commands/SetValueCommand.js +75 -0
- package/client/editor/js/libs/acorn/acorn.js +3236 -0
- package/client/editor/js/libs/acorn/acorn_loose.js +1299 -0
- package/client/editor/js/libs/acorn/walk.js +344 -0
- package/client/editor/js/libs/app/index.html +57 -0
- package/client/editor/js/libs/app.js +251 -0
- package/client/editor/js/libs/codemirror/addon/dialog.css +32 -0
- package/client/editor/js/libs/codemirror/addon/dialog.js +163 -0
- package/client/editor/js/libs/codemirror/addon/show-hint.css +36 -0
- package/client/editor/js/libs/codemirror/addon/show-hint.js +529 -0
- package/client/editor/js/libs/codemirror/addon/tern.css +87 -0
- package/client/editor/js/libs/codemirror/addon/tern.js +750 -0
- package/client/editor/js/libs/codemirror/codemirror.css +344 -0
- package/client/editor/js/libs/codemirror/codemirror.js +9849 -0
- package/client/editor/js/libs/codemirror/mode/glsl.js +233 -0
- package/client/editor/js/libs/codemirror/mode/javascript.js +959 -0
- package/client/editor/js/libs/codemirror/theme/monokai.css +41 -0
- package/client/editor/js/libs/esprima.js +6401 -0
- package/client/editor/js/libs/jsonlint.js +453 -0
- package/client/editor/js/libs/signals.min.js +14 -0
- package/client/editor/js/libs/tern-threejs/threejs.js +5031 -0
- package/client/editor/js/libs/ternjs/comment.js +87 -0
- package/client/editor/js/libs/ternjs/def.js +588 -0
- package/client/editor/js/libs/ternjs/doc_comment.js +401 -0
- package/client/editor/js/libs/ternjs/infer.js +1635 -0
- package/client/editor/js/libs/ternjs/polyfill.js +80 -0
- package/client/editor/js/libs/ternjs/signal.js +26 -0
- package/client/editor/js/libs/ternjs/tern.js +993 -0
- package/client/editor/js/libs/ui.js +1346 -0
- package/client/editor/js/libs/ui.three.js +855 -0
- package/client/facial-animation.js +455 -0
- package/client/index.html +7 -4
- package/client/loading.css +147 -0
- package/client/loading.html +25 -0
- package/client/style.css +251 -0
- package/package.json +7 -3
- package/server.js +9 -1
- package/src/apps/AppContext.js +1 -1
- package/src/apps/AppLoader.js +50 -37
- package/src/apps/AppRuntime.js +32 -8
- package/src/client/InputHandler.js +233 -0
- package/src/client/JitterBuffer.js +207 -0
- package/src/client/KalmanFilter.js +125 -0
- package/src/client/MessageHandler.js +101 -0
- package/src/client/PhysicsNetworkClient.js +141 -68
- package/src/client/ReconnectManager.js +62 -0
- package/src/client/SmoothInterpolation.js +127 -0
- package/src/client/SnapshotProcessor.js +144 -0
- package/src/connection/ConnectionManager.js +21 -3
- package/src/connection/SessionStore.js +13 -3
- package/src/index.client.js +4 -6
- package/src/netcode/EventLog.js +29 -15
- package/src/netcode/LagCompensator.js +25 -26
- package/src/netcode/NetworkState.js +4 -1
- package/src/netcode/PhysicsIntegration.js +20 -6
- package/src/netcode/PlayerManager.js +10 -2
- package/src/netcode/SnapshotEncoder.js +66 -19
- package/src/netcode/TickSystem.js +13 -4
- package/src/physics/World.js +66 -13
- package/src/protocol/msgpack.js +90 -63
- package/src/sdk/ReloadHandlers.js +12 -2
- package/src/sdk/ReloadManager.js +5 -0
- package/src/sdk/ServerHandlers.js +50 -11
- package/src/sdk/StaticHandler.js +22 -6
- package/src/sdk/TickHandler.js +101 -34
- package/src/sdk/scaffold.js +28 -0
- package/src/sdk/server.js +59 -33
- package/src/shared/movement.js +2 -1
- package/src/spatial/Octree.js +5 -0
- package/apps/interactive-door/index.js +0 -33
- package/apps/patrol-npc/index.js +0 -37
- package/src/connection/QualityMonitor.js +0 -46
- package/src/debug/StateInspector.js +0 -42
- package/src/index.js +0 -1
- package/src/index.server.js +0 -27
- package/src/protocol/Codec.js +0 -60
- package/src/protocol/SequenceTracker.js +0 -71
- package/src/sdk/ClientMessageHandler.js +0 -80
- package/src/sdk/client.js +0 -122
- package/world/kaira.glb +0 -0
- package/world/schwust.glb +0 -0
package/client/app.js
CHANGED
|
@@ -1,30 +1,655 @@
|
|
|
1
1
|
import * as THREE from 'three'
|
|
2
2
|
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
|
|
3
|
+
import { VRMLoaderPlugin, VRMUtils } from '@pixiv/three-vrm'
|
|
3
4
|
import { PhysicsNetworkClient, InputHandler, MSG } from '/src/index.client.js'
|
|
4
5
|
import { createElement, applyDiff } from 'webjsx'
|
|
5
6
|
import { createCameraController } from './camera.js'
|
|
7
|
+
import { loadAnimationLibrary, createPlayerAnimator } from './animation.js'
|
|
8
|
+
import { initFacialSystem } from './facial-animation.js'
|
|
9
|
+
import { VRButton } from 'three/addons/webxr/VRButton.js'
|
|
10
|
+
import { XRControllerModelFactory } from 'three/addons/webxr/XRControllerModelFactory.js'
|
|
11
|
+
import { XRHandModelFactory } from 'three/addons/webxr/XRHandModelFactory.js'
|
|
12
|
+
import { LoadingManager } from './LoadingManager.js'
|
|
13
|
+
import { createLoadingScreen } from './createLoadingScreen.js'
|
|
14
|
+
import { MobileControls, detectDevice } from './MobileControls.js'
|
|
15
|
+
import { ARControls, createARButton } from './ARControls.js'
|
|
16
|
+
|
|
17
|
+
const loadingMgr = new LoadingManager()
|
|
18
|
+
const loadingScreen = createLoadingScreen(loadingMgr)
|
|
19
|
+
loadingMgr.setStage('CONNECTING')
|
|
6
20
|
|
|
7
21
|
const scene = new THREE.Scene()
|
|
8
22
|
scene.background = new THREE.Color(0x87ceeb)
|
|
9
23
|
scene.fog = new THREE.Fog(0x87ceeb, 80, 200)
|
|
10
|
-
const camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.
|
|
11
|
-
|
|
24
|
+
const camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.05, 500)
|
|
25
|
+
let worldConfig = {}
|
|
26
|
+
let inputConfig = { pointerLock: true }
|
|
27
|
+
const isMobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
|
|
28
|
+
const renderer = new THREE.WebGLRenderer({ antialias: !isMobileDevice, powerPreference: 'high-performance' })
|
|
12
29
|
renderer.setSize(window.innerWidth, window.innerHeight)
|
|
13
|
-
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
|
|
30
|
+
renderer.setPixelRatio(isMobileDevice ? Math.min(window.devicePixelRatio, 2) : window.devicePixelRatio)
|
|
14
31
|
renderer.shadowMap.enabled = true
|
|
15
32
|
renderer.shadowMap.type = THREE.PCFSoftShadowMap
|
|
33
|
+
renderer.xr.enabled = true
|
|
16
34
|
document.body.appendChild(renderer.domElement)
|
|
17
35
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
36
|
+
async function initVRButton() {
|
|
37
|
+
if (navigator.xr && await navigator.xr.isSessionSupported('immersive-vr')) {
|
|
38
|
+
document.body.appendChild(VRButton.createButton(renderer, { optionalFeatures: ['hand-tracking'] }))
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
initVRButton()
|
|
42
|
+
|
|
43
|
+
const controllerModels = new Map()
|
|
44
|
+
const controllerGrips = new Map()
|
|
45
|
+
const laserPointers = new Map()
|
|
46
|
+
const controllerModelFactory = new XRControllerModelFactory()
|
|
47
|
+
|
|
48
|
+
const handModels = new Map()
|
|
49
|
+
const handRays = new Map()
|
|
50
|
+
const handModelFactory = new XRHandModelFactory()
|
|
51
|
+
let handsDetected = false
|
|
52
|
+
|
|
53
|
+
let wristUI = null
|
|
54
|
+
let wristUICanvas = null
|
|
55
|
+
let wristUIContext = null
|
|
56
|
+
|
|
57
|
+
let vrSettingsPanel = null
|
|
58
|
+
let vrSettings = {
|
|
59
|
+
snapTurnAngle: 30,
|
|
60
|
+
smoothTurnSpeed: 0,
|
|
61
|
+
vignetteEnabled: false,
|
|
62
|
+
playerHeight: 1.6,
|
|
63
|
+
teleportEnabled: false
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let teleportArc = null
|
|
67
|
+
let teleportMarker = null
|
|
68
|
+
let teleportTarget = null
|
|
69
|
+
let isTeleporting = false
|
|
70
|
+
let xrBaseReferenceSpace = null
|
|
71
|
+
const ARC_SEGMENTS = 20
|
|
72
|
+
const ARC_GRAVITY = -9.8
|
|
73
|
+
const ARC_VELOCITY = 8
|
|
74
|
+
|
|
75
|
+
let fadeQuad = null
|
|
76
|
+
let fadeOpacity = 0
|
|
77
|
+
let fadeState = 'none'
|
|
78
|
+
const FADE_SPEED = 5
|
|
79
|
+
const FADE_DELAY = 50
|
|
80
|
+
|
|
81
|
+
let vignetteMesh = null
|
|
82
|
+
let vignetteOpacity = 0
|
|
83
|
+
let vignetteTargetOpacity = 0
|
|
84
|
+
|
|
85
|
+
let mobileControls = null
|
|
86
|
+
let arControls = null
|
|
87
|
+
let arButton = null
|
|
88
|
+
let arEnabled = false
|
|
89
|
+
const deviceInfo = detectDevice()
|
|
90
|
+
|
|
91
|
+
mobileControls = new MobileControls({
|
|
92
|
+
joystickRadius: 45,
|
|
93
|
+
rotationSensitivity: 0.003,
|
|
94
|
+
zoomSensitivity: 0.008
|
|
95
|
+
})
|
|
96
|
+
inputConfig.pointerLock = !deviceInfo.isMobile
|
|
97
|
+
console.log('[Mobile] Touch controls initialized:', deviceInfo)
|
|
98
|
+
|
|
99
|
+
arControls = new ARControls({ placementMode: true, planeDetection: true })
|
|
100
|
+
const arReticle = arControls.createReticle()
|
|
101
|
+
scene.add(arReticle)
|
|
102
|
+
|
|
103
|
+
function createLaserPointer() {
|
|
104
|
+
const geometry = new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, -5)])
|
|
105
|
+
const material = new THREE.LineBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.6 })
|
|
106
|
+
const line = new THREE.Line(geometry, material)
|
|
107
|
+
line.name = 'laserPointer'
|
|
108
|
+
return line
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function createTeleportArc() {
|
|
112
|
+
const geometry = new THREE.BufferGeometry()
|
|
113
|
+
const positions = new Float32Array(ARC_SEGMENTS * 3)
|
|
114
|
+
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))
|
|
115
|
+
const material = new THREE.LineBasicMaterial({ color: 0x00ffff, transparent: true, opacity: 0.6 })
|
|
116
|
+
const line = new THREE.Line(geometry, material)
|
|
117
|
+
line.name = 'teleportArc'
|
|
118
|
+
line.visible = false
|
|
119
|
+
return line
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function createTeleportMarker() {
|
|
123
|
+
const geometry = new THREE.RingGeometry(0.3, 0.4, 32)
|
|
124
|
+
const material = new THREE.MeshBasicMaterial({ color: 0x00ffff, transparent: true, opacity: 0.5, side: THREE.DoubleSide })
|
|
125
|
+
const mesh = new THREE.Mesh(geometry, material)
|
|
126
|
+
mesh.name = 'teleportMarker'
|
|
127
|
+
mesh.rotation.x = -Math.PI / 2
|
|
128
|
+
mesh.visible = false
|
|
129
|
+
return mesh
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function createHandRay() {
|
|
133
|
+
const geometry = new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, -2)])
|
|
134
|
+
const material = new THREE.LineBasicMaterial({ color: 0x00ffff, transparent: true, opacity: 0.5 })
|
|
135
|
+
const line = new THREE.Line(geometry, material)
|
|
136
|
+
line.name = 'handRay'
|
|
137
|
+
return line
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function setupControllers() {
|
|
141
|
+
for (const handedness of [0, 1]) {
|
|
142
|
+
const controller = renderer.xr.getController(handedness)
|
|
143
|
+
const grip = renderer.xr.getControllerGrip(handedness)
|
|
144
|
+
const laser = createLaserPointer()
|
|
145
|
+
controller.add(laser)
|
|
146
|
+
laserPointers.set(handedness, laser)
|
|
147
|
+
|
|
148
|
+
const model = controllerModelFactory.createControllerModel(grip)
|
|
149
|
+
grip.add(model)
|
|
150
|
+
controllerModels.set(handedness, model)
|
|
151
|
+
controllerGrips.set(handedness, grip)
|
|
152
|
+
|
|
153
|
+
scene.add(controller)
|
|
154
|
+
scene.add(grip)
|
|
155
|
+
|
|
156
|
+
controller.visible = false
|
|
157
|
+
grip.visible = false
|
|
158
|
+
}
|
|
159
|
+
teleportArc = createTeleportArc()
|
|
160
|
+
scene.add(teleportArc)
|
|
161
|
+
teleportMarker = createTeleportMarker()
|
|
162
|
+
scene.add(teleportMarker)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function createWristUI() {
|
|
166
|
+
const canvas = document.createElement('canvas')
|
|
167
|
+
canvas.width = 256
|
|
168
|
+
canvas.height = 128
|
|
169
|
+
const ctx = canvas.getContext('2d')
|
|
170
|
+
|
|
171
|
+
const texture = new THREE.CanvasTexture(canvas)
|
|
172
|
+
const geometry = new THREE.PlaneGeometry(0.12, 0.06)
|
|
173
|
+
const material = new THREE.MeshBasicMaterial({
|
|
174
|
+
map: texture,
|
|
175
|
+
transparent: true,
|
|
176
|
+
opacity: 0.9,
|
|
177
|
+
side: THREE.DoubleSide
|
|
178
|
+
})
|
|
179
|
+
const mesh = new THREE.Mesh(geometry, material)
|
|
180
|
+
mesh.name = 'wristUI'
|
|
181
|
+
|
|
182
|
+
return { mesh, canvas, ctx, texture }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function updateWristUI(health, ammo, reloading) {
|
|
186
|
+
if (!wristUIContext) return
|
|
187
|
+
|
|
188
|
+
const ctx = wristUIContext
|
|
189
|
+
const canvas = wristUICanvas
|
|
190
|
+
|
|
191
|
+
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'
|
|
192
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
|
193
|
+
|
|
194
|
+
ctx.strokeStyle = '#00ffff'
|
|
195
|
+
ctx.lineWidth = 4
|
|
196
|
+
ctx.strokeRect(0, 0, canvas.width, canvas.height)
|
|
197
|
+
|
|
198
|
+
ctx.font = 'bold 36px monospace'
|
|
199
|
+
ctx.textAlign = 'left'
|
|
200
|
+
ctx.fillStyle = health > 60 ? '#00ff00' : health > 30 ? '#ffff00' : '#ff0000'
|
|
201
|
+
ctx.fillText(`HP ${Math.round(health)}`, 10, 45)
|
|
202
|
+
|
|
203
|
+
ctx.textAlign = 'right'
|
|
204
|
+
ctx.fillStyle = reloading ? '#ffff00' : '#00ffff'
|
|
205
|
+
ctx.fillText(reloading ? 'RELOAD' : `${ammo}/30`, 246, 45)
|
|
206
|
+
|
|
207
|
+
ctx.font = '24px monospace'
|
|
208
|
+
ctx.textAlign = 'center'
|
|
209
|
+
ctx.fillStyle = '#ffffff'
|
|
210
|
+
ctx.fillText('SPAWNPOINT VR', 128, 100)
|
|
211
|
+
|
|
212
|
+
wristUI.texture.needsUpdate = true
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function createVRSettingsPanel() {
|
|
216
|
+
const canvas = document.createElement('canvas')
|
|
217
|
+
canvas.width = 512
|
|
218
|
+
canvas.height = 512
|
|
219
|
+
const ctx = canvas.getContext('2d')
|
|
220
|
+
|
|
221
|
+
const texture = new THREE.CanvasTexture(canvas)
|
|
222
|
+
const geometry = new THREE.PlaneGeometry(0.5, 0.5)
|
|
223
|
+
const material = new THREE.MeshBasicMaterial({
|
|
224
|
+
map: texture,
|
|
225
|
+
transparent: true,
|
|
226
|
+
opacity: 0.95,
|
|
227
|
+
side: THREE.DoubleSide
|
|
228
|
+
})
|
|
229
|
+
const mesh = new THREE.Mesh(geometry, material)
|
|
230
|
+
mesh.name = 'vrSettingsPanel'
|
|
231
|
+
mesh.visible = false
|
|
232
|
+
mesh.position.set(0, 0, -0.6)
|
|
233
|
+
|
|
234
|
+
return { mesh, canvas, ctx, texture, visible: false }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function updateVRSettingsPanel() {
|
|
238
|
+
if (!vrSettingsPanel) return
|
|
239
|
+
|
|
240
|
+
const ctx = vrSettingsPanel.ctx
|
|
241
|
+
const canvas = vrSettingsPanel.canvas
|
|
242
|
+
|
|
243
|
+
ctx.fillStyle = 'rgba(20, 20, 40, 0.95)'
|
|
244
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
|
245
|
+
|
|
246
|
+
ctx.strokeStyle = '#00ffff'
|
|
247
|
+
ctx.lineWidth = 4
|
|
248
|
+
ctx.strokeRect(10, 10, canvas.width - 20, canvas.height - 20)
|
|
249
|
+
|
|
250
|
+
ctx.font = 'bold 32px sans-serif'
|
|
251
|
+
ctx.fillStyle = '#00ffff'
|
|
252
|
+
ctx.textAlign = 'center'
|
|
253
|
+
ctx.fillText('VR SETTINGS', 256, 50)
|
|
254
|
+
|
|
255
|
+
ctx.font = '24px sans-serif'
|
|
256
|
+
ctx.textAlign = 'left'
|
|
257
|
+
ctx.fillStyle = '#ffffff'
|
|
258
|
+
|
|
259
|
+
ctx.fillText(`Snap Turn: ${vrSettings.snapTurnAngle}°`, 40, 120)
|
|
260
|
+
ctx.fillText('[B/Y] to cycle', 280, 120)
|
|
261
|
+
|
|
262
|
+
ctx.fillText(`Smooth Turn: ${vrSettings.smoothTurnSpeed === 0 ? 'OFF' : vrSettings.smoothTurnSpeed.toFixed(1)}`, 40, 180)
|
|
263
|
+
ctx.fillText('[X/A] to cycle', 280, 180)
|
|
264
|
+
|
|
265
|
+
ctx.fillText(`Vignette: ${vrSettings.vignetteEnabled ? 'ON' : 'OFF'}`, 40, 240)
|
|
266
|
+
ctx.fillText('[Grip] to toggle', 280, 240)
|
|
267
|
+
|
|
268
|
+
ctx.fillText(`Height: ${vrSettings.playerHeight.toFixed(2)}m`, 40, 300)
|
|
269
|
+
ctx.fillText('[Menu] adjust', 280, 300)
|
|
270
|
+
|
|
271
|
+
ctx.fillStyle = vrSettings.teleportEnabled ? '#00ff00' : '#ff0000'
|
|
272
|
+
ctx.fillText(`Teleport: ${vrSettings.teleportEnabled ? 'ON' : 'OFF'}`, 40, 360)
|
|
273
|
+
ctx.fillStyle = '#ffffff'
|
|
274
|
+
ctx.fillText('[Trigger] toggle', 280, 360)
|
|
275
|
+
|
|
276
|
+
ctx.fillStyle = '#888888'
|
|
277
|
+
ctx.font = '20px sans-serif'
|
|
278
|
+
ctx.textAlign = 'center'
|
|
279
|
+
ctx.fillText('Press [Menu] button to close', 256, 480)
|
|
280
|
+
|
|
281
|
+
vrSettingsPanel.texture.needsUpdate = true
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function toggleVRSettings() {
|
|
285
|
+
if (!vrSettingsPanel) {
|
|
286
|
+
vrSettingsPanel = createVRSettingsPanel()
|
|
287
|
+
camera.add(vrSettingsPanel.mesh)
|
|
288
|
+
}
|
|
289
|
+
vrSettingsPanel.visible = !vrSettingsPanel.visible
|
|
290
|
+
vrSettingsPanel.mesh.visible = vrSettingsPanel.visible
|
|
291
|
+
if (vrSettingsPanel.visible) updateVRSettingsPanel()
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function setupHands() {
|
|
295
|
+
for (const handedness of [0, 1]) {
|
|
296
|
+
const hand = renderer.xr.getHand(handedness)
|
|
297
|
+
const handModel = handModelFactory.createHandModel(hand)
|
|
298
|
+
hand.add(handModel)
|
|
299
|
+
handModels.set(handedness, { hand, model: handModel })
|
|
300
|
+
|
|
301
|
+
const ray = createHandRay()
|
|
302
|
+
hand.add(ray)
|
|
303
|
+
handRays.set(handedness, ray)
|
|
304
|
+
|
|
305
|
+
if (handedness === 0 && !wristUI) {
|
|
306
|
+
wristUI = createWristUI()
|
|
307
|
+
wristUICanvas = wristUI.canvas
|
|
308
|
+
wristUIContext = wristUI.ctx
|
|
309
|
+
wristUI.mesh.position.set(0, -0.05, 0.08)
|
|
310
|
+
wristUI.mesh.rotation.x = -Math.PI / 3
|
|
311
|
+
hand.add(wristUI.mesh)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
scene.add(hand)
|
|
315
|
+
hand.visible = false
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function detectHandGestures(hand, handedness) {
|
|
320
|
+
const joints = hand.joints
|
|
321
|
+
if (!joints) return { pinch: false, grab: false, point: false }
|
|
322
|
+
|
|
323
|
+
const thumbTip = joints['thumb-tip']
|
|
324
|
+
const indexTip = joints['index-finger-tip']
|
|
325
|
+
const middleTip = joints['middle-finger-tip']
|
|
326
|
+
const ringTip = joints['ring-finger-tip']
|
|
327
|
+
const pinkyTip = joints['pinky-finger-tip']
|
|
328
|
+
const indexMcp = joints['index-finger-metacarpal']
|
|
329
|
+
const wrist = joints['wrist']
|
|
330
|
+
|
|
331
|
+
if (!thumbTip || !indexTip || !wrist) return { pinch: false, grab: false, point: false }
|
|
332
|
+
|
|
333
|
+
const pinchDist = thumbTip.position.distanceTo(indexTip.position)
|
|
334
|
+
const pinch = pinchDist < 0.02
|
|
335
|
+
|
|
336
|
+
let grab = false
|
|
337
|
+
if (middleTip && ringTip && pinkyTip) {
|
|
338
|
+
const palmDist = wrist.position.distanceTo(middleTip.position)
|
|
339
|
+
const tipsToPalm = [middleTip, ringTip, pinkyTip].every(tip => wrist.position.distanceTo(tip.position) < palmDist * 0.7)
|
|
340
|
+
grab = tipsToPalm
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
let point = false
|
|
344
|
+
if (indexMcp && middleTip && ringTip && pinkyTip) {
|
|
345
|
+
const indexExtended = indexTip.position.distanceTo(wrist.position) > indexMcp.position.distanceTo(wrist.position) * 1.5
|
|
346
|
+
const othersCurled = [middleTip, ringTip, pinkyTip].every(tip => wrist.position.distanceTo(tip.position) < 0.08)
|
|
347
|
+
point = indexExtended && othersCurled
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return { pinch, grab, point, pinchDist }
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function updateControllerVisibility() {
|
|
354
|
+
const inVR = renderer.xr.isPresenting
|
|
355
|
+
const session = renderer.xr.getSession()
|
|
356
|
+
let hasHands = false
|
|
357
|
+
|
|
358
|
+
if (session) {
|
|
359
|
+
for (const source of session.inputSources) {
|
|
360
|
+
if (source.hand) {
|
|
361
|
+
hasHands = true
|
|
362
|
+
break
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
handsDetected = hasHands
|
|
368
|
+
|
|
369
|
+
for (const handedness of [0, 1]) {
|
|
370
|
+
const grip = controllerGrips.get(handedness)
|
|
371
|
+
const controller = renderer.xr.getController(handedness)
|
|
372
|
+
const handData = handModels.get(handedness)
|
|
373
|
+
|
|
374
|
+
if (grip) grip.visible = inVR && !handsDetected
|
|
375
|
+
if (controller) controller.visible = inVR && !handsDetected
|
|
376
|
+
if (handData) handData.hand.visible = inVR && handsDetected
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function updateTeleportArc() {
|
|
381
|
+
if (!renderer.xr.isPresenting || !teleportArc || !teleportMarker || !vrSettings.teleportEnabled) {
|
|
382
|
+
if (teleportArc) teleportArc.visible = false
|
|
383
|
+
if (teleportMarker) teleportMarker.visible = false
|
|
384
|
+
return
|
|
385
|
+
}
|
|
386
|
+
const session = renderer.xr.getSession()
|
|
387
|
+
if (!session) return
|
|
388
|
+
|
|
389
|
+
let origin = null
|
|
390
|
+
let direction = null
|
|
391
|
+
let triggerTeleport = false
|
|
392
|
+
|
|
393
|
+
if (handsDetected) {
|
|
394
|
+
const leftHand = handModels.get(0)
|
|
395
|
+
if (leftHand) {
|
|
396
|
+
const gestures = detectHandGestures(leftHand.hand, 0)
|
|
397
|
+
const joints = leftHand.hand.joints
|
|
398
|
+
if (joints && joints['index-finger-tip']) {
|
|
399
|
+
joints['index-finger-tip'].getWorldPosition(_tmpOrigin)
|
|
400
|
+
joints['index-finger-tip'].getWorldDirection(_tmpDir)
|
|
401
|
+
origin = _tmpOrigin.clone()
|
|
402
|
+
direction = _tmpDir.clone().multiplyScalar(-1)
|
|
403
|
+
triggerTeleport = gestures.pinch
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
} else {
|
|
407
|
+
let leftPressed = false
|
|
408
|
+
let rightPressed = false
|
|
409
|
+
let leftController = null
|
|
410
|
+
for (const source of session.inputSources) {
|
|
411
|
+
if (source.handedness === 'left' && source.gamepad) {
|
|
412
|
+
leftController = renderer.xr.getController(0)
|
|
413
|
+
leftPressed = source.gamepad.buttons[0]?.pressed
|
|
414
|
+
}
|
|
415
|
+
if (source.handedness === 'right' && source.gamepad) {
|
|
416
|
+
rightPressed = source.gamepad.buttons[1]?.pressed
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (leftController && (leftPressed || rightPressed)) {
|
|
420
|
+
leftController.getWorldPosition(_tmpOrigin)
|
|
421
|
+
leftController.getWorldDirection(_tmpDir).multiplyScalar(-1)
|
|
422
|
+
origin = _tmpOrigin.clone()
|
|
423
|
+
direction = _tmpDir.clone()
|
|
424
|
+
triggerTeleport = rightPressed
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (!origin || !direction) {
|
|
429
|
+
teleportArc.visible = false
|
|
430
|
+
teleportMarker.visible = false
|
|
431
|
+
teleportTarget = null
|
|
432
|
+
return
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const hit = computeParabolicArc(origin, direction, ARC_VELOCITY, ARC_GRAVITY)
|
|
436
|
+
if (hit && hit.valid) {
|
|
437
|
+
teleportTarget = hit.point
|
|
438
|
+
teleportMarker.position.set(hit.point.x, hit.point.y + 0.02, hit.point.z)
|
|
439
|
+
teleportMarker.material.color.setHex(0x00ff00)
|
|
440
|
+
teleportMarker.visible = true
|
|
441
|
+
if (triggerTeleport && !isTeleporting) {
|
|
442
|
+
executeTeleport(teleportTarget)
|
|
443
|
+
}
|
|
444
|
+
} else {
|
|
445
|
+
teleportTarget = null
|
|
446
|
+
teleportMarker.visible = false
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const _tmpOrigin = new THREE.Vector3()
|
|
451
|
+
const _tmpDir = new THREE.Vector3()
|
|
452
|
+
const _tmpPoints = []
|
|
453
|
+
|
|
454
|
+
function computeParabolicArc(origin, direction, velocity, gravity) {
|
|
455
|
+
_tmpPoints.length = 0
|
|
456
|
+
const dt = 0.05
|
|
457
|
+
const positions = teleportArc.geometry.attributes.position.array
|
|
458
|
+
let idx = 0
|
|
459
|
+
let hit = null
|
|
460
|
+
for (let i = 0; i < ARC_SEGMENTS; i++) {
|
|
461
|
+
const t = i * dt
|
|
462
|
+
const x = origin.x + direction.x * velocity * t
|
|
463
|
+
const y = origin.y + direction.y * velocity * t + 0.5 * gravity * t * t
|
|
464
|
+
const z = origin.z + direction.z * velocity * t
|
|
465
|
+
if (idx < positions.length) {
|
|
466
|
+
positions[idx++] = x
|
|
467
|
+
positions[idx++] = y
|
|
468
|
+
positions[idx++] = z
|
|
469
|
+
}
|
|
470
|
+
if (!hit && y < 0.1) {
|
|
471
|
+
const prevT = (i - 1) * dt
|
|
472
|
+
const prevY = origin.y + direction.y * velocity * prevT + 0.5 * gravity * prevT * prevT
|
|
473
|
+
if (prevY > 0.1) {
|
|
474
|
+
const frac = (0.1 - prevY) / (y - prevY)
|
|
475
|
+
const hitT = prevT + frac * dt
|
|
476
|
+
hit = {
|
|
477
|
+
point: new THREE.Vector3(
|
|
478
|
+
origin.x + direction.x * velocity * hitT,
|
|
479
|
+
0,
|
|
480
|
+
origin.z + direction.z * velocity * hitT
|
|
481
|
+
),
|
|
482
|
+
valid: true
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
teleportArc.geometry.attributes.position.needsUpdate = true
|
|
488
|
+
teleportArc.visible = true
|
|
489
|
+
return hit
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function createFadeQuad() {
|
|
493
|
+
const geometry = new THREE.PlaneGeometry(2, 2)
|
|
494
|
+
const material = new THREE.MeshBasicMaterial({
|
|
495
|
+
color: 0x000000,
|
|
496
|
+
transparent: true,
|
|
497
|
+
opacity: 0,
|
|
498
|
+
depthTest: false,
|
|
499
|
+
depthWrite: false
|
|
500
|
+
})
|
|
501
|
+
const quad = new THREE.Mesh(geometry, material)
|
|
502
|
+
quad.renderOrder = 9999
|
|
503
|
+
return quad
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function updateFade(dt) {
|
|
507
|
+
if (!fadeQuad) {
|
|
508
|
+
fadeQuad = createFadeQuad()
|
|
509
|
+
camera.add(fadeQuad)
|
|
510
|
+
fadeQuad.position.z = -0.1
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (fadeState === 'in') {
|
|
514
|
+
fadeOpacity += FADE_SPEED * dt
|
|
515
|
+
if (fadeOpacity >= 1) {
|
|
516
|
+
fadeOpacity = 1
|
|
517
|
+
fadeState = 'delay'
|
|
518
|
+
setTimeout(() => { fadeState = 'out' }, FADE_DELAY)
|
|
519
|
+
}
|
|
520
|
+
} else if (fadeState === 'out') {
|
|
521
|
+
fadeOpacity -= FADE_SPEED * dt
|
|
522
|
+
if (fadeOpacity <= 0) {
|
|
523
|
+
fadeOpacity = 0
|
|
524
|
+
fadeState = 'none'
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
fadeQuad.material.opacity = fadeOpacity
|
|
529
|
+
fadeQuad.visible = fadeOpacity > 0.01
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function createVignette() {
|
|
533
|
+
const canvas = document.createElement('canvas')
|
|
534
|
+
canvas.width = 512
|
|
535
|
+
canvas.height = 512
|
|
536
|
+
const ctx = canvas.getContext('2d')
|
|
537
|
+
|
|
538
|
+
const gradient = ctx.createRadialGradient(256, 256, 100, 256, 256, 400)
|
|
539
|
+
gradient.addColorStop(0, 'rgba(0, 0, 0, 0)')
|
|
540
|
+
gradient.addColorStop(0.5, 'rgba(0, 0, 0, 0.3)')
|
|
541
|
+
gradient.addColorStop(1, 'rgba(0, 0, 0, 0.8)')
|
|
542
|
+
|
|
543
|
+
ctx.fillStyle = gradient
|
|
544
|
+
ctx.fillRect(0, 0, 512, 512)
|
|
545
|
+
|
|
546
|
+
const texture = new THREE.CanvasTexture(canvas)
|
|
547
|
+
const geometry = new THREE.PlaneGeometry(2, 2)
|
|
548
|
+
const material = new THREE.MeshBasicMaterial({
|
|
549
|
+
map: texture,
|
|
550
|
+
transparent: true,
|
|
551
|
+
opacity: 0,
|
|
552
|
+
depthTest: false,
|
|
553
|
+
depthWrite: false
|
|
554
|
+
})
|
|
555
|
+
const mesh = new THREE.Mesh(geometry, material)
|
|
556
|
+
mesh.renderOrder = 9998
|
|
557
|
+
return mesh
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function updateVignette(dt, isMoving) {
|
|
561
|
+
if (!vrSettings.vignetteEnabled) {
|
|
562
|
+
if (vignetteMesh) vignetteMesh.visible = false
|
|
563
|
+
return
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (!vignetteMesh) {
|
|
567
|
+
vignetteMesh = createVignette()
|
|
568
|
+
camera.add(vignetteMesh)
|
|
569
|
+
vignetteMesh.position.z = -0.15
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
vignetteTargetOpacity = isMoving ? 0.6 : 0
|
|
573
|
+
vignetteOpacity += (vignetteTargetOpacity - vignetteOpacity) * 5 * dt
|
|
574
|
+
|
|
575
|
+
vignetteMesh.material.opacity = vignetteOpacity
|
|
576
|
+
vignetteMesh.visible = vignetteOpacity > 0.01
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function executeTeleport(targetPoint) {
|
|
580
|
+
isTeleporting = true
|
|
581
|
+
fadeState = 'in'
|
|
582
|
+
|
|
583
|
+
setTimeout(() => {
|
|
584
|
+
const base = xrBaseReferenceSpace || renderer.xr.getReferenceSpace()
|
|
585
|
+
if (!base) {
|
|
586
|
+
isTeleporting = false
|
|
587
|
+
fadeState = 'out'
|
|
588
|
+
return
|
|
589
|
+
}
|
|
590
|
+
const offsetPosition = { x: -targetPoint.x, y: -targetPoint.y, z: -targetPoint.z }
|
|
591
|
+
const transform = new XRRigidTransform(offsetPosition, { x: 0, y: 0, z: 0, w: 1 })
|
|
592
|
+
renderer.xr.setReferenceSpace(base.getOffsetReferenceSpace(transform))
|
|
593
|
+
}, 200)
|
|
594
|
+
|
|
595
|
+
setTimeout(() => { isTeleporting = false }, 400)
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
scene.add(camera)
|
|
599
|
+
const ambient = new THREE.AmbientLight(0xfff4d6, 0.3)
|
|
600
|
+
scene.add(ambient)
|
|
601
|
+
const studio = new THREE.DirectionalLight(0x4488ff, 0.4)
|
|
602
|
+
studio.position.set(-20, 30, -10)
|
|
603
|
+
studio.castShadow = false
|
|
604
|
+
scene.add(studio)
|
|
605
|
+
const sun = new THREE.DirectionalLight(0xffffff, 1.5)
|
|
606
|
+
sun.position.set(21, 50, 20)
|
|
21
607
|
sun.castShadow = true
|
|
22
|
-
sun.shadow.mapSize.set(
|
|
23
|
-
sun.shadow.
|
|
24
|
-
sun.shadow.
|
|
25
|
-
|
|
26
|
-
|
|
608
|
+
sun.shadow.mapSize.set(1024, 1024)
|
|
609
|
+
sun.shadow.bias = 0.0038
|
|
610
|
+
sun.shadow.normalBias = 0.6
|
|
611
|
+
sun.shadow.radius = 12
|
|
612
|
+
sun.shadow.blurSamples = 8
|
|
613
|
+
sun.shadow.camera.left = -80; sun.shadow.camera.right = 80; sun.shadow.camera.top = 80; sun.shadow.camera.bottom = -80
|
|
614
|
+
sun.shadow.camera.near = 0.5; sun.shadow.camera.far = 200
|
|
27
615
|
scene.add(sun)
|
|
616
|
+
scene.add(sun.target)
|
|
617
|
+
|
|
618
|
+
function fitShadowFrustum() {
|
|
619
|
+
const box = new THREE.Box3()
|
|
620
|
+
scene.traverse(o => { if (o.isMesh && (o.castShadow || o.receiveShadow) && o.geometry) box.expandByObject(o) })
|
|
621
|
+
if (box.isEmpty()) return
|
|
622
|
+
const center = box.getCenter(new THREE.Vector3())
|
|
623
|
+
const size = box.getSize(new THREE.Vector3())
|
|
624
|
+
const pad = 2
|
|
625
|
+
const half = (Math.max(size.x, size.z) / 2 + pad) * 1.06
|
|
626
|
+
const sc = sun.shadow.camera
|
|
627
|
+
sc.left = -half; sc.right = half; sc.top = half; sc.bottom = -half
|
|
628
|
+
const lightDir = new THREE.Vector3().subVectors(sun.target.position, sun.position).normalize()
|
|
629
|
+
const corners = [new THREE.Vector3(box.min.x, box.min.y, box.min.z), new THREE.Vector3(box.max.x, box.max.y, box.max.z)]
|
|
630
|
+
let minProj = Infinity, maxProj = -Infinity
|
|
631
|
+
for (const c of corners) { const d = new THREE.Vector3().subVectors(c, sun.position).dot(lightDir); minProj = Math.min(minProj, d); maxProj = Math.max(maxProj, d) }
|
|
632
|
+
sc.near = Math.max(0.5, minProj - 10); sc.far = maxProj + 10
|
|
633
|
+
sc.updateProjectionMatrix()
|
|
634
|
+
sun.target.position.copy(center)
|
|
635
|
+
sun.target.updateMatrixWorld()
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function applySceneConfig(s) {
|
|
639
|
+
if (s.skyColor != null) { scene.background = new THREE.Color(s.skyColor) }
|
|
640
|
+
if (s.fogColor != null) { scene.fog = new THREE.Fog(s.fogColor, s.fogNear ?? 80, s.fogFar ?? 200) }
|
|
641
|
+
if (s.ambientColor != null) { ambient.color.set(s.ambientColor); ambient.intensity = s.ambientIntensity ?? 0.3 }
|
|
642
|
+
if (s.sunColor != null) { sun.color.set(s.sunColor); sun.intensity = s.sunIntensity ?? 1.5 }
|
|
643
|
+
if (s.sunPosition) sun.position.set(...s.sunPosition)
|
|
644
|
+
if (s.fillColor != null) { studio.color.set(s.fillColor); studio.intensity = s.fillIntensity ?? 0.4 }
|
|
645
|
+
if (s.fillPosition) studio.position.set(...s.fillPosition)
|
|
646
|
+
if (s.shadowMapSize) sun.shadow.mapSize.set(s.shadowMapSize, s.shadowMapSize)
|
|
647
|
+
if (s.shadowBias != null) sun.shadow.bias = s.shadowBias
|
|
648
|
+
if (s.shadowNormalBias != null) sun.shadow.normalBias = s.shadowNormalBias
|
|
649
|
+
if (s.shadowRadius != null) sun.shadow.radius = s.shadowRadius
|
|
650
|
+
if (s.shadowBlurSamples != null) sun.shadow.blurSamples = s.shadowBlurSamples
|
|
651
|
+
if (s.fov) { camera.fov = s.fov; camera.updateProjectionMatrix() }
|
|
652
|
+
}
|
|
28
653
|
|
|
29
654
|
const ground = new THREE.Mesh(new THREE.PlaneGeometry(200, 200), new THREE.MeshStandardMaterial({ color: 0x444444 }))
|
|
30
655
|
ground.rotation.x = -Math.PI / 2
|
|
@@ -32,57 +657,283 @@ ground.receiveShadow = true
|
|
|
32
657
|
scene.add(ground)
|
|
33
658
|
|
|
34
659
|
const gltfLoader = new GLTFLoader()
|
|
660
|
+
gltfLoader.register((parser) => new VRMLoaderPlugin(parser))
|
|
35
661
|
const playerMeshes = new Map()
|
|
662
|
+
const playerAnimators = new Map()
|
|
663
|
+
const playerVrms = new Map()
|
|
664
|
+
const playerStates = new Map()
|
|
36
665
|
const entityMeshes = new Map()
|
|
666
|
+
const entityParentMap = new Map()
|
|
667
|
+
const entityGroups = new Map()
|
|
37
668
|
const appModules = new Map()
|
|
38
669
|
const entityAppMap = new Map()
|
|
39
|
-
const
|
|
670
|
+
const playerTargets = new Map()
|
|
671
|
+
let inputHandler = null
|
|
40
672
|
const uiRoot = document.getElementById('ui-root')
|
|
41
673
|
const clickPrompt = document.getElementById('click-prompt')
|
|
674
|
+
if (deviceInfo.isMobile && clickPrompt) clickPrompt.style.display = 'none'
|
|
42
675
|
const cam = createCameraController(camera, scene)
|
|
43
676
|
cam.restore(JSON.parse(sessionStorage.getItem('cam') || 'null'))
|
|
44
677
|
sessionStorage.removeItem('cam')
|
|
45
|
-
let
|
|
678
|
+
let latestState = null
|
|
679
|
+
let latestInput = null
|
|
680
|
+
let uiTimer = 0
|
|
46
681
|
let lastFrameTime = performance.now()
|
|
682
|
+
let fpsFrames = 0, fpsLast = performance.now(), fpsDisplay = 0
|
|
683
|
+
let vrmBuffer = null
|
|
684
|
+
let animAssets = null
|
|
685
|
+
let assetsReady = null
|
|
686
|
+
let assetsLoaded = false
|
|
687
|
+
|
|
688
|
+
function detectVrmVersion(buffer) {
|
|
689
|
+
try {
|
|
690
|
+
const arrayBuffer = buffer instanceof ArrayBuffer ? buffer : buffer.buffer
|
|
691
|
+
const view = new DataView(arrayBuffer)
|
|
692
|
+
const jsonLen = view.getUint32(12, true)
|
|
693
|
+
const json = JSON.parse(new TextDecoder().decode(new Uint8Array(arrayBuffer, 20, jsonLen)))
|
|
694
|
+
if (json.extensions?.VRM) return '0'
|
|
695
|
+
} catch (e) {}
|
|
696
|
+
return '1'
|
|
697
|
+
}
|
|
47
698
|
|
|
48
|
-
function
|
|
699
|
+
function initAssets(playerModelUrl) {
|
|
700
|
+
loadingMgr.setStage('DOWNLOAD')
|
|
701
|
+
assetsReady = loadingMgr.fetchWithProgress(playerModelUrl).then(b => {
|
|
702
|
+
vrmBuffer = b
|
|
703
|
+
loadingMgr.setStage('PROCESS')
|
|
704
|
+
return loadAnimationLibrary(detectVrmVersion(b), null)
|
|
705
|
+
}).then(result => {
|
|
706
|
+
animAssets = result
|
|
707
|
+
assetsLoaded = true
|
|
708
|
+
checkAllLoaded()
|
|
709
|
+
})
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
async function createPlayerVRM(id) {
|
|
49
713
|
const group = new THREE.Group()
|
|
50
|
-
const body = new THREE.Mesh(new THREE.CapsuleGeometry(0.4, 1.0, 4, 8), new THREE.MeshStandardMaterial({ color: isLocal ? 0x00ff88 : 0xff4444 }))
|
|
51
|
-
body.position.y = 0.9; body.castShadow = true; group.add(body)
|
|
52
|
-
const head = new THREE.Mesh(new THREE.SphereGeometry(0.25, 8, 6), new THREE.MeshStandardMaterial({ color: isLocal ? 0x00cc66 : 0xcc3333 }))
|
|
53
|
-
head.position.y = 1.7; head.castShadow = true; group.add(head)
|
|
54
714
|
scene.add(group)
|
|
715
|
+
playerMeshes.set(id, group)
|
|
716
|
+
if (assetsReady) await assetsReady
|
|
717
|
+
if (!vrmBuffer) return group
|
|
718
|
+
try {
|
|
719
|
+
const gltf = await gltfLoader.parseAsync(vrmBuffer.buffer.slice(0), '')
|
|
720
|
+
const vrm = gltf.userData.vrm
|
|
721
|
+
VRMUtils.removeUnnecessaryVertices(vrm.scene)
|
|
722
|
+
VRMUtils.combineSkeletons(vrm.scene)
|
|
723
|
+
const vrmVersion = detectVrmVersion(vrmBuffer)
|
|
724
|
+
vrm.scene.rotation.y = Math.PI
|
|
725
|
+
vrm.scene.traverse(c => { if (c.isMesh) { c.castShadow = true; c.receiveShadow = true } })
|
|
726
|
+
const pc = worldConfig.player || {}
|
|
727
|
+
const modelScale = pc.modelScale || 1.323
|
|
728
|
+
const feetOffsetRatio = pc.feetOffset || 0.212
|
|
729
|
+
vrm.scene.scale.multiplyScalar(modelScale)
|
|
730
|
+
vrm.scene.position.y = -feetOffsetRatio * modelScale
|
|
731
|
+
group.userData.feetOffset = 1.3
|
|
732
|
+
group.add(vrm.scene)
|
|
733
|
+
playerVrms.set(id, vrm)
|
|
734
|
+
initVRMFeatures(id, vrm)
|
|
735
|
+
if (animAssets) {
|
|
736
|
+
const animator = createPlayerAnimator(vrm, animAssets, vrmVersion, worldConfig.animation || {})
|
|
737
|
+
playerAnimators.set(id, animator)
|
|
738
|
+
}
|
|
739
|
+
if (id === client.playerId && vrm.humanoid) {
|
|
740
|
+
const head = vrm.humanoid.getRawBoneNode('head')
|
|
741
|
+
if (head) cam.setCameraBone(head)
|
|
742
|
+
if (head) cam.setHeadBone(head)
|
|
743
|
+
if (cam.getMode() === 'fps' && head) head.scale.set(0, 0, 0)
|
|
744
|
+
}
|
|
745
|
+
} catch (e) { console.error('[vrm]', id, e.message) }
|
|
55
746
|
return group
|
|
56
747
|
}
|
|
57
748
|
|
|
749
|
+
const playerExpressions = new Map()
|
|
750
|
+
const playerBlinkTimers = new Map()
|
|
751
|
+
|
|
752
|
+
function initVRMFeatures(id, vrm) {
|
|
753
|
+
const features = { vrm, expressions: null, lookAt: null, springBone: null, meta: null, blinkTimer: 0, nextBlink: Math.random() * 2 + 2 }
|
|
754
|
+
if (vrm.expressionManager) {
|
|
755
|
+
features.expressions = vrm.expressionManager
|
|
756
|
+
features.expressions.setValue('blink', 0)
|
|
757
|
+
}
|
|
758
|
+
if (vrm.lookAt) {
|
|
759
|
+
features.lookAt = vrm.lookAt
|
|
760
|
+
features.lookAt.smoothFactor = 0.1
|
|
761
|
+
}
|
|
762
|
+
if (vrm.springBoneManager) features.springBone = vrm.springBoneManager
|
|
763
|
+
if (vrm.meta) features.meta = vrm.meta
|
|
764
|
+
playerExpressions.set(id, features)
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function updateVRMFeatures(id, dt, targetPosition) {
|
|
768
|
+
const features = playerExpressions.get(id)
|
|
769
|
+
if (!features) return
|
|
770
|
+
if (features.springBone) features.springBone.update(dt)
|
|
771
|
+
if (features.lookAt && targetPosition) {
|
|
772
|
+
const lookTarget = new THREE.Vector3(targetPosition.x, targetPosition.y + 1.6, targetPosition.z)
|
|
773
|
+
features.lookAt.lookAt(lookTarget)
|
|
774
|
+
}
|
|
775
|
+
if (features.expressions) {
|
|
776
|
+
features.blinkTimer += dt
|
|
777
|
+
if (features.blinkTimer >= features.nextBlink) {
|
|
778
|
+
features.expressions.setValue('blink', 1)
|
|
779
|
+
if (features.blinkTimer >= features.nextBlink + 0.15) {
|
|
780
|
+
features.expressions.setValue('blink', 0)
|
|
781
|
+
features.blinkTimer = 0
|
|
782
|
+
features.nextBlink = Math.random() * 3 + 2
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function setVRMExpression(id, expressionName, value) {
|
|
789
|
+
const features = playerExpressions.get(id)
|
|
790
|
+
if (features?.expressions) features.expressions.setValue(expressionName, value)
|
|
791
|
+
}
|
|
792
|
+
|
|
58
793
|
function removePlayerMesh(id) {
|
|
59
794
|
const mesh = playerMeshes.get(id)
|
|
60
795
|
if (!mesh) return
|
|
61
796
|
scene.remove(mesh)
|
|
797
|
+
const animator = playerAnimators.get(id)
|
|
798
|
+
if (animator) animator.dispose()
|
|
799
|
+
playerAnimators.delete(id)
|
|
800
|
+
const vrm = playerVrms.get(id)
|
|
801
|
+
if (vrm) VRMUtils.deepDispose(vrm.scene)
|
|
802
|
+
playerVrms.delete(id)
|
|
62
803
|
mesh.traverse(c => { if (c.geometry) c.geometry.dispose(); if (c.material) c.material.dispose() })
|
|
63
804
|
playerMeshes.delete(id)
|
|
805
|
+
playerTargets.delete(id)
|
|
806
|
+
playerStates.delete(id)
|
|
807
|
+
playerExpressions.delete(id)
|
|
64
808
|
}
|
|
65
809
|
|
|
66
810
|
function evaluateAppModule(code) {
|
|
67
811
|
try {
|
|
68
812
|
const stripped = code.replace(/^import\s+.*$/gm, '')
|
|
69
|
-
const wrapped = stripped.replace(/export\s+default\s
|
|
813
|
+
const wrapped = stripped.replace(/export\s+default\s*/, 'return ').replace(/export\s+/g, '')
|
|
70
814
|
return new Function(wrapped)()
|
|
71
|
-
} catch (e) { console.error('[app-eval]', e.message); return null }
|
|
815
|
+
} catch (e) { console.error('[app-eval]', e.message, e.stack); return null }
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const PLACEHOLDER_DIMS = {
|
|
819
|
+
door: [1.5, 2.5, 0.1],
|
|
820
|
+
platform: [4, 0.5, 4],
|
|
821
|
+
trigger: [2, 3, 2],
|
|
822
|
+
hazard: [2, 2, 2],
|
|
823
|
+
lootBox: [1, 1.5, 1],
|
|
824
|
+
pillar: [1, 4, 1]
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function createEditorPlaceholder(entityId, templateName, custom) {
|
|
828
|
+
const dims = PLACEHOLDER_DIMS[templateName] || [1, 1, 1]
|
|
829
|
+
const geo = new THREE.BoxGeometry(dims[0], dims[1], dims[2])
|
|
830
|
+
const color = custom?.color ?? 0xcccccc
|
|
831
|
+
const mat = new THREE.MeshStandardMaterial({ color, roughness: 0.8, metalness: 0.1, transparent: true, opacity: 0.7 })
|
|
832
|
+
const group = new THREE.Group()
|
|
833
|
+
const mesh = new THREE.Mesh(geo, mat)
|
|
834
|
+
mesh.castShadow = true
|
|
835
|
+
mesh.receiveShadow = true
|
|
836
|
+
mesh.userData.isPlaceholder = true
|
|
837
|
+
mesh.userData.templateName = templateName
|
|
838
|
+
group.add(mesh)
|
|
839
|
+
group.userData.spin = custom?.spin || 0
|
|
840
|
+
group.userData.hover = custom?.hover || 0
|
|
841
|
+
return group
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const MESH_BUILDERS = {
|
|
845
|
+
box: (c) => new THREE.BoxGeometry(c.sx || 1, c.sy || 1, c.sz || 1),
|
|
846
|
+
cylinder: (c) => new THREE.CylinderGeometry(c.r || 0.4, c.r || 0.4, c.h || 0.1, c.seg || 16),
|
|
847
|
+
sphere: (c) => new THREE.SphereGeometry(c.r || 0.5, c.seg || 16, c.seg || 16)
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function buildEntityMesh(entityId, custom) {
|
|
851
|
+
const c = custom || {}
|
|
852
|
+
const geoType = c.mesh || 'box'
|
|
853
|
+
const geo = MESH_BUILDERS[geoType] ? MESH_BUILDERS[geoType](c) : MESH_BUILDERS.box(c)
|
|
854
|
+
const mat = new THREE.MeshStandardMaterial({
|
|
855
|
+
color: c.color ?? 0xff8800, roughness: c.roughness ?? 1, metalness: c.metalness ?? 0,
|
|
856
|
+
emissive: c.emissive ?? 0x000000, emissiveIntensity: c.emissiveIntensity ?? 0
|
|
857
|
+
})
|
|
858
|
+
const group = new THREE.Group()
|
|
859
|
+
const mesh = new THREE.Mesh(geo, mat)
|
|
860
|
+
if (c.rotX) mesh.rotation.x = c.rotX
|
|
861
|
+
if (c.rotZ) mesh.rotation.z = c.rotZ
|
|
862
|
+
mesh.castShadow = true; mesh.receiveShadow = true
|
|
863
|
+
group.add(mesh)
|
|
864
|
+
if (c.light) { group.add(new THREE.PointLight(c.light, c.lightIntensity || 1, c.lightRange || 4)) }
|
|
865
|
+
if (c.spin) group.userData.spin = c.spin
|
|
866
|
+
if (c.hover) group.userData.hover = c.hover
|
|
867
|
+
return group
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const pendingLoads = new Set()
|
|
871
|
+
|
|
872
|
+
function rebuildEntityHierarchy(entities) {
|
|
873
|
+
for (const e of entities) {
|
|
874
|
+
entityParentMap.set(e.id, e.parent || null)
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
for (const e of entities) {
|
|
878
|
+
const mesh = entityMeshes.get(e.id)
|
|
879
|
+
if (!mesh) continue
|
|
880
|
+
|
|
881
|
+
const parentId = entityParentMap.get(e.id)
|
|
882
|
+
const currentParent = mesh.parent && mesh.parent !== scene ? mesh.parent : null
|
|
883
|
+
const currentParentId = Array.from(entityParentMap.entries()).find(([k, v]) => v === null || entityMeshes.get(k) === currentParent)?.[0]
|
|
884
|
+
|
|
885
|
+
if (parentId === null) {
|
|
886
|
+
if (currentParent && currentParent !== scene) {
|
|
887
|
+
scene.add(mesh)
|
|
888
|
+
}
|
|
889
|
+
} else {
|
|
890
|
+
const parentMesh = entityMeshes.get(parentId)
|
|
891
|
+
if (parentMesh && parentMesh !== currentParent) {
|
|
892
|
+
parentMesh.add(mesh)
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
72
896
|
}
|
|
73
897
|
|
|
74
898
|
function loadEntityModel(entityId, entityState) {
|
|
75
|
-
if (
|
|
899
|
+
if (entityMeshes.has(entityId) || pendingLoads.has(entityId)) return
|
|
900
|
+
pendingLoads.add(entityId)
|
|
901
|
+
|
|
902
|
+
const isEditorPlaceholder = entityState.custom?.editorPlaceholder === true
|
|
903
|
+
const smartObjectTemplate = entityState.custom?.template
|
|
904
|
+
|
|
905
|
+
if (!entityState.model || isEditorPlaceholder) {
|
|
906
|
+
let group
|
|
907
|
+
if (isEditorPlaceholder && smartObjectTemplate) {
|
|
908
|
+
group = createEditorPlaceholder(entityId, smartObjectTemplate, entityState.custom)
|
|
909
|
+
} else {
|
|
910
|
+
group = buildEntityMesh(entityId, entityState.custom)
|
|
911
|
+
}
|
|
912
|
+
group.position.set(...entityState.position)
|
|
913
|
+
if (entityState.rotation) group.quaternion.set(...entityState.rotation)
|
|
914
|
+
scene.add(group)
|
|
915
|
+
entityMeshes.set(entityId, group)
|
|
916
|
+
pendingLoads.delete(entityId)
|
|
917
|
+
if (!environmentLoaded) { environmentLoaded = true; checkAllLoaded() }
|
|
918
|
+
return
|
|
919
|
+
}
|
|
920
|
+
loadingMgr.setStage('RESOURCES')
|
|
76
921
|
const url = entityState.model.startsWith('./') ? '/' + entityState.model.slice(2) : entityState.model
|
|
77
922
|
gltfLoader.load(url, (gltf) => {
|
|
78
923
|
const model = gltf.scene
|
|
79
924
|
model.position.set(...entityState.position)
|
|
80
925
|
if (entityState.rotation) model.quaternion.set(...entityState.rotation)
|
|
81
|
-
model.traverse(c => { if (c.isMesh) { c.castShadow = true; c.receiveShadow = true } })
|
|
926
|
+
model.traverse(c => { if (c.isMesh) { c.castShadow = true; c.receiveShadow = true; if (c.material) { c.material.shadowSide = THREE.DoubleSide; c.material.roughness = 1; c.material.metalness = 0; if (c.material.specularIntensity !== undefined) c.material.specularIntensity = 0 } } })
|
|
82
927
|
scene.add(model)
|
|
83
928
|
entityMeshes.set(entityId, model)
|
|
929
|
+
const colliders = []
|
|
930
|
+
model.traverse(c => { if (c.isMesh && c.name === 'Collider') colliders.push(c) })
|
|
931
|
+
if (colliders.length) cam.setEnvironment(colliders)
|
|
84
932
|
scene.remove(ground)
|
|
85
|
-
|
|
933
|
+
fitShadowFrustum()
|
|
934
|
+
pendingLoads.delete(entityId)
|
|
935
|
+
if (!environmentLoaded) { environmentLoaded = true; checkAllLoaded() }
|
|
936
|
+
}, undefined, (err) => { console.error('[gltf]', entityId, err); pendingLoads.delete(entityId) })
|
|
86
937
|
}
|
|
87
938
|
|
|
88
939
|
function renderAppUI(state) {
|
|
@@ -93,89 +944,514 @@ function renderAppUI(state) {
|
|
|
93
944
|
const appClient = appModules.get(appName)
|
|
94
945
|
if (!appClient?.render) continue
|
|
95
946
|
try {
|
|
96
|
-
const result = appClient.render({ entity, state: entity.custom || {}, h: createElement })
|
|
947
|
+
const result = appClient.render({ entity, state: entity.custom || {}, h: createElement, engine: engineCtx, players: state.players })
|
|
97
948
|
if (result?.ui) uiFragments.push({ id: entity.id, ui: result.ui })
|
|
98
949
|
} catch (e) { console.error('[ui]', entity.id, e.message) }
|
|
99
950
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const hudVdom = createElement('div', { id: 'hud' },
|
|
103
|
-
createElement('div', { id: 'crosshair' }, '+'),
|
|
104
|
-
createElement('div', { id: 'health-bar' },
|
|
105
|
-
createElement('div', { id: 'health-fill', style: `width:${hp}%;background:${hp > 60 ? '#0f0' : hp > 30 ? '#ff0' : '#f00'}` }),
|
|
106
|
-
createElement('span', { id: 'health-text' }, String(hp))
|
|
107
|
-
),
|
|
108
|
-
createElement('div', { id: 'info' }, `Players: ${state.players.length} | Tick: ${client.currentTick}`),
|
|
951
|
+
const hudVdom = createElement('div', { id: 'hud' },
|
|
952
|
+
createElement('div', { id: 'info' }, `FPS: ${fpsDisplay} | Players: ${state.players.length} | Tick: ${client.currentTick} | RTT: ${Math.round(client.getRTT())}ms | Buf: ${client.getBufferHealth()}`),
|
|
109
953
|
...uiFragments.map(f => createElement('div', { 'data-app': f.id }, f.ui))
|
|
110
954
|
)
|
|
111
955
|
try { applyDiff(uiRoot, hudVdom) } catch (e) { console.error('[ui] diff:', e.message) }
|
|
112
956
|
}
|
|
113
957
|
|
|
114
958
|
const client = new PhysicsNetworkClient({
|
|
115
|
-
|
|
959
|
+
url: `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws`,
|
|
960
|
+
predictionEnabled: true,
|
|
961
|
+
smoothInterpolation: true,
|
|
116
962
|
onStateUpdate: (state) => {
|
|
117
|
-
|
|
118
|
-
|
|
963
|
+
const smoothState = client.getSmoothState()
|
|
964
|
+
for (const p of smoothState.players) {
|
|
965
|
+
if (!playerMeshes.has(p.id)) createPlayerVRM(p.id)
|
|
119
966
|
const mesh = playerMeshes.get(p.id)
|
|
120
|
-
mesh
|
|
121
|
-
|
|
967
|
+
const feetOff = mesh?.userData?.feetOffset ?? 1.3
|
|
968
|
+
const tx = p.position[0], ty = p.position[1] - feetOff, tz = p.position[2]
|
|
969
|
+
playerTargets.set(p.id, { x: tx, y: ty, z: tz })
|
|
970
|
+
playerStates.set(p.id, p)
|
|
971
|
+
const dx = tx - mesh.position.x, dy = ty - mesh.position.y, dz = tz - mesh.position.z
|
|
972
|
+
if (!mesh.userData.initialized || dx * dx + dy * dy + dz * dz > 100) { mesh.position.set(tx, ty, tz); mesh.userData.initialized = true }
|
|
122
973
|
}
|
|
123
|
-
|
|
974
|
+
for (const e of smoothState.entities) {
|
|
975
|
+
const mesh = entityMeshes.get(e.id)
|
|
976
|
+
if (mesh && e.position) mesh.position.set(...e.position)
|
|
977
|
+
if (mesh && e.rotation) mesh.quaternion.set(...e.rotation)
|
|
978
|
+
if (!entityMeshes.has(e.id)) loadEntityModel(e.id, e)
|
|
979
|
+
}
|
|
980
|
+
rebuildEntityHierarchy(smoothState.entities)
|
|
981
|
+
latestState = state
|
|
982
|
+
if (!firstSnapshotReceived) { firstSnapshotReceived = true; checkAllLoaded() }
|
|
124
983
|
},
|
|
125
|
-
onPlayerJoined: (id) => { if (!playerMeshes.has(id))
|
|
984
|
+
onPlayerJoined: (id) => { if (!playerMeshes.has(id)) createPlayerVRM(id) },
|
|
126
985
|
onPlayerLeft: (id) => removePlayerMesh(id),
|
|
127
986
|
onEntityAdded: (id, state) => loadEntityModel(id, state),
|
|
128
|
-
|
|
129
|
-
|
|
987
|
+
onEntityRemoved: (id) => { const m = entityMeshes.get(id); if (m) { scene.remove(m); m.traverse(c => { if (c.geometry) c.geometry.dispose(); if (c.material) c.material.dispose() }); entityMeshes.delete(id) }; pendingLoads.delete(id) },
|
|
988
|
+
onWorldDef: (wd) => {
|
|
989
|
+
loadingMgr.setStage('SERVER_SYNC')
|
|
990
|
+
worldConfig = wd
|
|
991
|
+
if (wd.playerModel) initAssets(wd.playerModel.startsWith('./') ? '/' + wd.playerModel.slice(2) : wd.playerModel)
|
|
992
|
+
else { assetsReady = Promise.resolve(); assetsLoaded = true; checkAllLoaded() }
|
|
993
|
+
if (wd.entities) for (const e of wd.entities) { if (e.app) entityAppMap.set(e.id, e.app); if (e.model && !entityMeshes.has(e.id)) loadEntityModel(e.id, e) }
|
|
994
|
+
if (wd.scene) applySceneConfig(wd.scene)
|
|
995
|
+
if (wd.camera) cam.applyConfig(wd.camera)
|
|
996
|
+
if (wd.input) {
|
|
997
|
+
inputConfig = { pointerLock: true, ...wd.input }
|
|
998
|
+
if (!inputConfig.pointerLock) { clickPrompt.style.display = 'none' }
|
|
999
|
+
}
|
|
1000
|
+
},
|
|
1001
|
+
onAppModule: (d) => {
|
|
1002
|
+
loadingMgr.setStage('APPS')
|
|
1003
|
+
const a = evaluateAppModule(d.code)
|
|
1004
|
+
if (a?.client) {
|
|
1005
|
+
appModules.set(d.app, a.client)
|
|
1006
|
+
if (a.client.setup) try { a.client.setup(engineCtx) } catch (e) { console.error('[app-setup]', d.app, e.message) }
|
|
1007
|
+
}
|
|
1008
|
+
},
|
|
130
1009
|
onAssetUpdate: () => {},
|
|
131
|
-
onAppEvent: () => {
|
|
1010
|
+
onAppEvent: (payload) => {
|
|
1011
|
+
for (const [, mod] of appModules) { if (mod.onEvent) try { mod.onEvent(payload, engineCtx) } catch (e) { console.error('[app-event]', e.message) } }
|
|
1012
|
+
},
|
|
132
1013
|
onHotReload: () => { sessionStorage.setItem('cam', JSON.stringify(cam.save())); location.reload() },
|
|
133
1014
|
debug: false
|
|
134
1015
|
})
|
|
135
1016
|
|
|
1017
|
+
const engineCtx = {
|
|
1018
|
+
scene, camera, renderer,
|
|
1019
|
+
get client() { return client },
|
|
1020
|
+
get playerId() { return client.playerId },
|
|
1021
|
+
get cam() { return cam },
|
|
1022
|
+
get worldConfig() { return worldConfig },
|
|
1023
|
+
get inputConfig() { return inputConfig },
|
|
1024
|
+
get _tps() { return engineCtx._tpsState },
|
|
1025
|
+
set _tps(val) { engineCtx._tpsState = val },
|
|
1026
|
+
playerVrms,
|
|
1027
|
+
setInputConfig(cfg) { Object.assign(inputConfig, cfg); if (!inputConfig.pointerLock) { clickPrompt.style.display = 'none'; if (document.pointerLockElement) document.exitPointerLock() } },
|
|
1028
|
+
players: {
|
|
1029
|
+
getMesh: (id) => playerMeshes.get(id),
|
|
1030
|
+
getState: (id) => playerStates.get(id),
|
|
1031
|
+
getAnimator: (id) => playerAnimators.get(id),
|
|
1032
|
+
setExpression: (id, name, val) => setVRMExpression(id, name, val),
|
|
1033
|
+
setAiming: (id, val) => { const s = playerStates.get(id); if (s) s._aiming = val }
|
|
1034
|
+
},
|
|
1035
|
+
createElement,
|
|
1036
|
+
THREE,
|
|
1037
|
+
get mobileControls() { return mobileControls }
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
initFacialSystem(engineCtx)
|
|
1041
|
+
|
|
136
1042
|
let inputLoopId = null
|
|
1043
|
+
let loadingScreenHidden = false
|
|
1044
|
+
let environmentLoaded = false
|
|
1045
|
+
let firstSnapshotReceived = false
|
|
1046
|
+
let lastShootState = false
|
|
1047
|
+
let lastHealth = 100
|
|
1048
|
+
|
|
1049
|
+
function checkAllLoaded() {
|
|
1050
|
+
if (loadingScreenHidden) return
|
|
1051
|
+
if (!assetsLoaded) return
|
|
1052
|
+
if (!environmentLoaded) return
|
|
1053
|
+
if (!firstSnapshotReceived) return
|
|
1054
|
+
loadingMgr.setStage('INIT')
|
|
1055
|
+
loadingMgr.complete()
|
|
1056
|
+
loadingScreen.hide().catch(() => {})
|
|
1057
|
+
loadingScreenHidden = true
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
function initInputHandler() {
|
|
1061
|
+
inputHandler = new InputHandler({
|
|
1062
|
+
renderer,
|
|
1063
|
+
snapTurnAngle: vrSettings.snapTurnAngle,
|
|
1064
|
+
smoothTurnSpeed: vrSettings.smoothTurnSpeed,
|
|
1065
|
+
onMenuPressed: () => {
|
|
1066
|
+
if (renderer.xr.isPresenting) toggleVRSettings()
|
|
1067
|
+
}
|
|
1068
|
+
})
|
|
1069
|
+
|
|
1070
|
+
if (mobileControls) {
|
|
1071
|
+
inputHandler.setMobileControls(mobileControls)
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
renderer.xr.addEventListener('sessionstart', () => {
|
|
1075
|
+
if (inputHandler) {
|
|
1076
|
+
inputHandler.vrYaw = cam.yaw
|
|
1077
|
+
console.log('[VR] Session started, vrYaw initialized to:', cam.yaw)
|
|
1078
|
+
}
|
|
1079
|
+
setTimeout(() => {
|
|
1080
|
+
xrBaseReferenceSpace = renderer.xr.getReferenceSpace()
|
|
1081
|
+
if (!xrBaseReferenceSpace) return
|
|
1082
|
+
const local = client.state?.players?.find(p => p.id === client.playerId)
|
|
1083
|
+
if (local?.position) {
|
|
1084
|
+
const headHeight = local.crouch ? 1.1 : 1.6
|
|
1085
|
+
const pos = { x: -local.position[0], y: -(local.position[1] + headHeight), z: -local.position[2] }
|
|
1086
|
+
const t = new XRRigidTransform(pos, { x: 0, y: 0, z: 0, w: 1 })
|
|
1087
|
+
renderer.xr.setReferenceSpace(xrBaseReferenceSpace.getOffsetReferenceSpace(t))
|
|
1088
|
+
camera.position.set(local.position[0], local.position[1] + headHeight, local.position[2])
|
|
1089
|
+
console.log('[VR] Camera synced to player position:', local.position, 'headHeight:', headHeight)
|
|
1090
|
+
}
|
|
1091
|
+
}, 100)
|
|
1092
|
+
})
|
|
1093
|
+
renderer.xr.addEventListener('sessionend', () => {
|
|
1094
|
+
xrBaseReferenceSpace = null
|
|
1095
|
+
})
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
async function initAR() {
|
|
1099
|
+
const supported = await arControls.init(renderer)
|
|
1100
|
+
if (supported) {
|
|
1101
|
+
arButton = await createARButton(renderer, async () => {
|
|
1102
|
+
const started = await arControls.start()
|
|
1103
|
+
if (started) {
|
|
1104
|
+
arEnabled = true
|
|
1105
|
+
scene.background = null
|
|
1106
|
+
ground.visible = false
|
|
1107
|
+
console.log('[AR] AR mode started')
|
|
1108
|
+
return true
|
|
1109
|
+
}
|
|
1110
|
+
return false
|
|
1111
|
+
}, async () => {
|
|
1112
|
+
await arControls.end()
|
|
1113
|
+
arEnabled = false
|
|
1114
|
+
scene.background = new THREE.Color(0x87ceeb)
|
|
1115
|
+
ground.visible = true
|
|
1116
|
+
if (arButton) {
|
|
1117
|
+
arButton.textContent = 'Enter XR'
|
|
1118
|
+
arButton.style.background = 'rgba(0, 150, 0, 0.8)'
|
|
1119
|
+
}
|
|
1120
|
+
console.log('[AR] AR mode ended')
|
|
1121
|
+
})
|
|
1122
|
+
if (arButton) {
|
|
1123
|
+
document.body.appendChild(arButton)
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
let settingsTriggerCooldown = false
|
|
1129
|
+
let settingsSnapCooldown = false
|
|
1130
|
+
let settingsSmoothCooldown = false
|
|
1131
|
+
|
|
1132
|
+
const SMOOTH_TURN_SPEEDS = [0, 1.5, 3.0, 4.5]
|
|
1133
|
+
const SNAP_TURN_ANGLES = [15, 30, 45, 60, 90]
|
|
1134
|
+
|
|
1135
|
+
function cycleSmoothTurnSpeed() {
|
|
1136
|
+
const idx = SMOOTH_TURN_SPEEDS.indexOf(vrSettings.smoothTurnSpeed)
|
|
1137
|
+
vrSettings.smoothTurnSpeed = SMOOTH_TURN_SPEEDS[(idx + 1) % SMOOTH_TURN_SPEEDS.length]
|
|
1138
|
+
if (inputHandler) inputHandler.setSmoothTurnSpeed(vrSettings.smoothTurnSpeed)
|
|
1139
|
+
updateVRSettingsPanel()
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function cycleSnapTurnAngle() {
|
|
1143
|
+
const idx = SNAP_TURN_ANGLES.indexOf(vrSettings.snapTurnAngle)
|
|
1144
|
+
vrSettings.snapTurnAngle = SNAP_TURN_ANGLES[(idx + 1) % SNAP_TURN_ANGLES.length]
|
|
1145
|
+
if (inputHandler) inputHandler.setSnapTurnAngle(vrSettings.snapTurnAngle)
|
|
1146
|
+
updateVRSettingsPanel()
|
|
1147
|
+
}
|
|
1148
|
+
|
|
137
1149
|
function startInputLoop() {
|
|
138
1150
|
if (inputLoopId) return
|
|
1151
|
+
if (!inputHandler) initInputHandler()
|
|
139
1152
|
inputLoopId = setInterval(() => {
|
|
140
1153
|
if (!client.connected) return
|
|
141
1154
|
const input = inputHandler.getInput()
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (input.
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
1155
|
+
latestInput = input
|
|
1156
|
+
|
|
1157
|
+
if (input.editToggle && !cam.getEditMode()) {
|
|
1158
|
+
cam.setEditMode(true)
|
|
1159
|
+
console.log('[EditMode] Enabled')
|
|
1160
|
+
} else if (!input.editToggle && cam.getEditMode() && inputHandler.editModeCooldown === false) {
|
|
1161
|
+
cam.setEditMode(false)
|
|
1162
|
+
console.log('[EditMode] Disabled')
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
if (input.yaw !== undefined) {
|
|
1166
|
+
cam.setVRYaw(input.yaw)
|
|
1167
|
+
} else {
|
|
1168
|
+
input.yaw = cam.yaw
|
|
1169
|
+
input.pitch = cam.pitch
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
if (input.zoom !== undefined && input.zoom !== 0) {
|
|
1173
|
+
cam.onWheel({ deltaY: -input.zoom * 100, preventDefault: () => {} })
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
if (input.isMobile && input.yawDelta !== undefined) {
|
|
1177
|
+
cam.setVRYaw(input.yaw)
|
|
1178
|
+
}
|
|
1179
|
+
if (input.isMobile && input.pitchDelta !== undefined) {
|
|
1180
|
+
cam.adjustVRPitch(input.pitchDelta)
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
if (vrSettingsPanel?.visible) {
|
|
1184
|
+
if (input.shoot && !settingsTriggerCooldown) {
|
|
1185
|
+
vrSettings.teleportEnabled = !vrSettings.teleportEnabled
|
|
1186
|
+
settingsTriggerCooldown = true
|
|
1187
|
+
updateVRSettingsPanel()
|
|
1188
|
+
}
|
|
1189
|
+
if (input.sprint && !settingsSmoothCooldown) {
|
|
1190
|
+
cycleSmoothTurnSpeed()
|
|
1191
|
+
settingsSmoothCooldown = true
|
|
1192
|
+
}
|
|
1193
|
+
if (input.reload && !settingsSnapCooldown) {
|
|
1194
|
+
cycleSnapTurnAngle()
|
|
1195
|
+
settingsSnapCooldown = true
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
if (!vrSettingsPanel?.visible || !input.shoot) {
|
|
1200
|
+
settingsTriggerCooldown = false
|
|
1201
|
+
}
|
|
1202
|
+
if (!vrSettingsPanel?.visible || !input.sprint) {
|
|
1203
|
+
settingsSmoothCooldown = false
|
|
1204
|
+
}
|
|
1205
|
+
if (!vrSettingsPanel?.visible || !input.reload) {
|
|
1206
|
+
settingsSnapCooldown = false
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
if (input.shoot && !lastShootState) {
|
|
1210
|
+
inputHandler.pulse('right', 0.5, 100)
|
|
1211
|
+
}
|
|
1212
|
+
lastShootState = input.shoot
|
|
1213
|
+
const local = client.state?.players?.find(p => p.id === client.playerId)
|
|
1214
|
+
if (local) {
|
|
1215
|
+
if (local.health < lastHealth) {
|
|
1216
|
+
inputHandler.pulse('left', 0.8, 200)
|
|
1217
|
+
inputHandler.pulse('right', 0.8, 200)
|
|
154
1218
|
}
|
|
1219
|
+
lastHealth = local.health
|
|
155
1220
|
}
|
|
1221
|
+
for (const [, mod] of appModules) { if (mod.onInput) try { mod.onInput(input, engineCtx) } catch (e) { console.error('[app-input]', e.message) } }
|
|
1222
|
+
client.sendInput(input)
|
|
156
1223
|
}, 1000 / 60)
|
|
157
1224
|
}
|
|
158
1225
|
|
|
159
|
-
renderer.domElement.addEventListener('click', () => { if (!document.pointerLockElement) renderer.domElement.requestPointerLock() })
|
|
1226
|
+
renderer.domElement.addEventListener('click', () => { if (inputConfig.pointerLock && !document.pointerLockElement) renderer.domElement.requestPointerLock() })
|
|
160
1227
|
document.addEventListener('pointerlockchange', () => {
|
|
161
1228
|
const locked = document.pointerLockElement === renderer.domElement
|
|
162
|
-
clickPrompt.style.display = locked ? 'none' : 'block'
|
|
1229
|
+
clickPrompt.style.display = locked ? 'none' : (inputConfig.pointerLock ? 'block' : 'none')
|
|
163
1230
|
if (locked) document.addEventListener('mousemove', cam.onMouseMove)
|
|
164
1231
|
else document.removeEventListener('mousemove', cam.onMouseMove)
|
|
165
1232
|
})
|
|
166
1233
|
renderer.domElement.addEventListener('wheel', cam.onWheel, { passive: false })
|
|
1234
|
+
renderer.domElement.addEventListener('mousedown', (e) => { for (const [, mod] of appModules) { if (mod.onMouseDown) try { mod.onMouseDown(e, engineCtx) } catch (ex) {} } })
|
|
1235
|
+
renderer.domElement.addEventListener('mouseup', (e) => { for (const [, mod] of appModules) { if (mod.onMouseUp) try { mod.onMouseUp(e, engineCtx) } catch (ex) {} } })
|
|
1236
|
+
renderer.domElement.addEventListener('contextmenu', (e) => e.preventDefault())
|
|
167
1237
|
window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight) })
|
|
168
1238
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
const
|
|
1239
|
+
let modelLoadQueue = []
|
|
1240
|
+
function createGimbal(scale = 1) {
|
|
1241
|
+
const gimbal = new THREE.Group()
|
|
1242
|
+
const lineGeom = new THREE.BufferGeometry()
|
|
1243
|
+
const linePoints = [
|
|
1244
|
+
new THREE.Vector3(-scale, 0, 0), new THREE.Vector3(scale, 0, 0),
|
|
1245
|
+
new THREE.Vector3(0, -scale, 0), new THREE.Vector3(0, scale, 0),
|
|
1246
|
+
new THREE.Vector3(0, 0, -scale), new THREE.Vector3(0, 0, scale)
|
|
1247
|
+
]
|
|
1248
|
+
lineGeom.setFromPoints(linePoints)
|
|
1249
|
+
const lineMat = new THREE.LineBasicMaterial({ color: 0xcccccc, transparent: true, opacity: 0.6 })
|
|
1250
|
+
const lines = new THREE.LineSegments(lineGeom, lineMat)
|
|
1251
|
+
gimbal.add(lines)
|
|
1252
|
+
const ringGeoms = [
|
|
1253
|
+
{ rot: [Math.PI / 2, 0, 0], color: 0xff0000 },
|
|
1254
|
+
{ rot: [0, Math.PI / 2, 0], color: 0x00ff00 },
|
|
1255
|
+
{ rot: [0, 0, 0], color: 0x0000ff }
|
|
1256
|
+
]
|
|
1257
|
+
for (const r of ringGeoms) {
|
|
1258
|
+
const ringGeo = new THREE.TorusGeometry(scale * 0.9, scale * 0.08, 16, 100)
|
|
1259
|
+
const ringMat = new THREE.MeshBasicMaterial({ color: r.color, transparent: true, opacity: 0.5 })
|
|
1260
|
+
const ring = new THREE.Mesh(ringGeo, ringMat)
|
|
1261
|
+
ring.rotation.fromArray(r.rot)
|
|
1262
|
+
gimbal.add(ring)
|
|
1263
|
+
}
|
|
1264
|
+
gimbal.userData.isGimbal = true
|
|
1265
|
+
return gimbal
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
function loadQueuedModels() {
|
|
1269
|
+
if (modelLoadQueue.length === 0) return
|
|
1270
|
+
const file = modelLoadQueue.shift()
|
|
1271
|
+
const reader = new FileReader()
|
|
1272
|
+
reader.onload = (e) => {
|
|
1273
|
+
try {
|
|
1274
|
+
const buffer = e.target.result
|
|
1275
|
+
gltfLoader.parse(buffer, '', (gltf) => {
|
|
1276
|
+
const local = client.state?.players?.find(p => p.id === client.playerId)
|
|
1277
|
+
if (!local) return
|
|
1278
|
+
const sy = Math.sin(cam.yaw), cy = Math.cos(cam.yaw)
|
|
1279
|
+
const spawnDist = 1.0
|
|
1280
|
+
const spawnHeight = 0.3
|
|
1281
|
+
const standingHeight = local.crouch ? 1.1 : 1.6
|
|
1282
|
+
const x = local.position[0] + sy * spawnDist
|
|
1283
|
+
const y = local.position[1] + standingHeight + spawnHeight
|
|
1284
|
+
const z = local.position[2] + cy * spawnDist
|
|
1285
|
+
const group = new THREE.Group()
|
|
1286
|
+
if (gltf.scene) group.add(gltf.scene)
|
|
1287
|
+
const gimbal = createGimbal(0.5)
|
|
1288
|
+
gimbal.position.copy(group.position)
|
|
1289
|
+
group.add(gimbal)
|
|
1290
|
+
group.position.set(x, y, z)
|
|
1291
|
+
group.userData.isDroppedModel = true
|
|
1292
|
+
scene.add(group)
|
|
1293
|
+
const envApp = appModules.get('environment')
|
|
1294
|
+
if (envApp?.onEvent) {
|
|
1295
|
+
envApp.onEvent({
|
|
1296
|
+
type: 'dropModel',
|
|
1297
|
+
position: [x, y, z],
|
|
1298
|
+
rotation: [0, 0, 0, 1],
|
|
1299
|
+
modelPath: file.name,
|
|
1300
|
+
scale: [1, 1, 1]
|
|
1301
|
+
}, engineCtx)
|
|
1302
|
+
}
|
|
1303
|
+
console.log('[ModelLoader] Loaded:', file.name, 'position:', [x, y, z])
|
|
1304
|
+
setTimeout(loadQueuedModels, 100)
|
|
1305
|
+
}, (err) => {
|
|
1306
|
+
console.error('[ModelLoader] Parse error:', err.message)
|
|
1307
|
+
setTimeout(loadQueuedModels, 100)
|
|
1308
|
+
})
|
|
1309
|
+
} catch (err) {
|
|
1310
|
+
console.error('[ModelLoader] Load error:', err.message)
|
|
1311
|
+
setTimeout(loadQueuedModels, 100)
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
reader.readAsArrayBuffer(file)
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
document.addEventListener('dragover', (e) => {
|
|
1318
|
+
if (!cam.getEditMode()) return
|
|
1319
|
+
e.preventDefault()
|
|
1320
|
+
e.stopPropagation()
|
|
1321
|
+
renderer.domElement.style.opacity = '0.8'
|
|
1322
|
+
})
|
|
1323
|
+
|
|
1324
|
+
document.addEventListener('dragleave', (e) => {
|
|
1325
|
+
if (!cam.getEditMode()) return
|
|
1326
|
+
renderer.domElement.style.opacity = '1'
|
|
1327
|
+
})
|
|
1328
|
+
|
|
1329
|
+
document.addEventListener('drop', (e) => {
|
|
1330
|
+
if (!cam.getEditMode()) return
|
|
1331
|
+
e.preventDefault()
|
|
1332
|
+
e.stopPropagation()
|
|
1333
|
+
renderer.domElement.style.opacity = '1'
|
|
1334
|
+
const files = e.dataTransfer.files
|
|
1335
|
+
for (let i = 0; i < files.length; i++) {
|
|
1336
|
+
const file = files[i]
|
|
1337
|
+
if (file.type === 'model/gltf-binary' || file.type === 'model/gltf+json' || file.name.endsWith('.glb') || file.name.endsWith('.gltf')) {
|
|
1338
|
+
modelLoadQueue.push(file)
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
if (modelLoadQueue.length > 0) {
|
|
1342
|
+
loadQueuedModels()
|
|
1343
|
+
}
|
|
1344
|
+
})
|
|
1345
|
+
|
|
1346
|
+
let smoothDt = 1 / 60
|
|
1347
|
+
function animate(timestamp) {
|
|
1348
|
+
const now = timestamp || performance.now()
|
|
1349
|
+
const rawDt = Math.min((now - lastFrameTime) / 1000, 0.1)
|
|
173
1350
|
lastFrameTime = now
|
|
1351
|
+
smoothDt += (rawDt - smoothDt) * 0.2
|
|
1352
|
+
const frameDt = smoothDt
|
|
1353
|
+
fpsFrames++
|
|
1354
|
+
if (now - fpsLast >= 1000) { fpsDisplay = fpsFrames; fpsFrames = 0; fpsLast = now }
|
|
1355
|
+
const lerpFactor = 1.0 - Math.exp(-16.0 * frameDt)
|
|
1356
|
+
for (const [id, target] of playerTargets) {
|
|
1357
|
+
const mesh = playerMeshes.get(id)
|
|
1358
|
+
if (!mesh) continue
|
|
1359
|
+
const ps = playerStates.get(id)
|
|
1360
|
+
const vx = ps?.velocity?.[0] || 0, vy = ps?.velocity?.[1] || 0, vz = ps?.velocity?.[2] || 0
|
|
1361
|
+
const goalX = target.x + vx * frameDt, goalY = target.y + vy * frameDt, goalZ = target.z + vz * frameDt
|
|
1362
|
+
mesh.position.x += (goalX - mesh.position.x) * lerpFactor
|
|
1363
|
+
mesh.position.y += (goalY - mesh.position.y) * lerpFactor
|
|
1364
|
+
mesh.position.z += (goalZ - mesh.position.z) * lerpFactor
|
|
1365
|
+
}
|
|
1366
|
+
for (const [id, animator] of playerAnimators) {
|
|
1367
|
+
const ps = playerStates.get(id)
|
|
1368
|
+
if (!ps) continue
|
|
1369
|
+
animator.update(frameDt, ps.velocity, ps.onGround, ps.health, ps._aiming || false, ps.crouch || 0)
|
|
1370
|
+
const mesh = playerMeshes.get(id)
|
|
1371
|
+
if (!mesh) continue
|
|
1372
|
+
const vx = ps.velocity?.[0] || 0, vz = ps.velocity?.[2] || 0
|
|
1373
|
+
if (Math.sqrt(vx * vx + vz * vz) > 0.5) mesh.userData.lastYaw = Math.atan2(vx, vz)
|
|
1374
|
+
if (mesh.userData.lastYaw !== undefined) {
|
|
1375
|
+
let diff = mesh.userData.lastYaw - mesh.rotation.y
|
|
1376
|
+
while (diff > Math.PI) diff -= Math.PI * 2
|
|
1377
|
+
while (diff < -Math.PI) diff += Math.PI * 2
|
|
1378
|
+
mesh.rotation.y += diff * lerpFactor
|
|
1379
|
+
}
|
|
1380
|
+
const target = playerTargets.get(id)
|
|
1381
|
+
updateVRMFeatures(id, frameDt, target)
|
|
1382
|
+
if (id !== client.playerId && ps.lookPitch !== undefined) {
|
|
1383
|
+
const vrm = playerVrms.get(id)
|
|
1384
|
+
if (vrm?.humanoid) {
|
|
1385
|
+
const head = vrm.humanoid.getNormalizedBoneNode('head')
|
|
1386
|
+
if (head) head.rotation.x = -(ps.lookPitch || 0) * 0.6
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
for (const [eid, mesh] of entityMeshes) {
|
|
1391
|
+
if (mesh.userData.spin) mesh.rotation.y += mesh.userData.spin * frameDt
|
|
1392
|
+
if (mesh.userData.hover) {
|
|
1393
|
+
mesh.userData.hoverTime = (mesh.userData.hoverTime || 0) + frameDt
|
|
1394
|
+
const child = mesh.children[0]
|
|
1395
|
+
if (child) child.position.y = Math.sin(mesh.userData.hoverTime * 2) * mesh.userData.hover
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
for (const [, mod] of appModules) { if (mod.onFrame) try { mod.onFrame(frameDt, engineCtx) } catch (e) {} }
|
|
1399
|
+
if (engineCtx.facial) engineCtx.facial.update(frameDt)
|
|
1400
|
+
uiTimer += frameDt
|
|
1401
|
+
if (latestState && uiTimer >= 0.25) { uiTimer = 0; renderAppUI(latestState) }
|
|
174
1402
|
const local = client.state?.players?.find(p => p.id === client.playerId)
|
|
175
|
-
|
|
1403
|
+
const inVR = renderer.xr.isPresenting
|
|
1404
|
+
if (!inVR || cam.getEditMode()) {
|
|
1405
|
+
cam.update(local, playerMeshes.get(client.playerId), frameDt, latestInput)
|
|
1406
|
+
}
|
|
1407
|
+
if (inVR && !cam.getEditMode() && local?.position && xrBaseReferenceSpace && !isTeleporting) {
|
|
1408
|
+
const headHeight = local.crouch ? 1.1 : 1.6
|
|
1409
|
+
const pos = { x: -local.position[0], y: -(local.position[1] + headHeight), z: -local.position[2] }
|
|
1410
|
+
const t = new XRRigidTransform(pos, { x: 0, y: 0, z: 0, w: 1 })
|
|
1411
|
+
renderer.xr.setReferenceSpace(xrBaseReferenceSpace.getOffsetReferenceSpace(t))
|
|
1412
|
+
}
|
|
1413
|
+
if (inVR && local && wristUI) {
|
|
1414
|
+
const tps = appModules.get('tps-game')?._tps
|
|
1415
|
+
const ammo = tps?.ammo ?? 0
|
|
1416
|
+
const reloading = tps?.reloading ?? false
|
|
1417
|
+
updateWristUI(local.health ?? 100, ammo, reloading)
|
|
1418
|
+
}
|
|
1419
|
+
updateControllerVisibility()
|
|
1420
|
+
updateTeleportArc()
|
|
1421
|
+
updateFade(frameDt)
|
|
1422
|
+
|
|
1423
|
+
let isMoving = false
|
|
1424
|
+
if (inVR && local?.velocity) {
|
|
1425
|
+
const speed = Math.sqrt(local.velocity[0] ** 2 + local.velocity[2] ** 2)
|
|
1426
|
+
isMoving = speed > 0.5
|
|
1427
|
+
}
|
|
1428
|
+
updateVignette(frameDt, isMoving)
|
|
1429
|
+
|
|
1430
|
+
if (arEnabled) {
|
|
1431
|
+
const xrFrame = renderer.xr.getFrame()
|
|
1432
|
+
if (xrFrame) {
|
|
1433
|
+
arControls.update(xrFrame, camera, scene)
|
|
1434
|
+
const local = client.state?.players?.find(p => p.id === client.playerId)
|
|
1435
|
+
if (local?.position && !arControls.anchorPlaced) {
|
|
1436
|
+
arControls.setInitialFPSPosition(local.position, cam.yaw)
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
|
|
176
1441
|
renderer.render(scene, camera)
|
|
177
1442
|
}
|
|
178
|
-
animate
|
|
1443
|
+
renderer.setAnimationLoop(animate)
|
|
179
1444
|
|
|
180
|
-
client.connect().then(() => { console.log('Connected'); startInputLoop() }).catch(err => console.error('Connection failed:', err))
|
|
181
|
-
|
|
1445
|
+
client.connect().then(() => { console.log('Connected'); startInputLoop(); initAR() }).catch(err => console.error('Connection failed:', err))
|
|
1446
|
+
setupControllers()
|
|
1447
|
+
setupHands()
|
|
1448
|
+
window.__VR_DEBUG__ = false
|
|
1449
|
+
window.debug = {
|
|
1450
|
+
scene, camera, renderer, client, playerMeshes, entityMeshes, appModules, inputHandler, playerVrms, playerAnimators, loadingMgr, loadingScreen, controllerModels, controllerGrips, handModels, mobileControls, arControls,
|
|
1451
|
+
enableVRDebug: () => { window.__VR_DEBUG__ = true; console.log('[VR] Debug enabled - button/axis logging active') },
|
|
1452
|
+
disableVRDebug: () => { window.__VR_DEBUG__ = false; console.log('[VR] Debug disabled') },
|
|
1453
|
+
vrInput: () => inputHandler?.getInput() || null,
|
|
1454
|
+
vrSettings: () => vrSettings,
|
|
1455
|
+
deviceInfo: () => deviceInfo,
|
|
1456
|
+
placeARAnchor: () => arControls?.placeAnchor() || arControls?.placeAtCamera()
|
|
1457
|
+
}
|