@plasius/renderer 1.0.0
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/CHANGELOG.md +59 -0
- package/CODE_OF_CONDUCT.md +79 -0
- package/CONTRIBUTORS.md +27 -0
- package/LICENSE +203 -0
- package/README.md +70 -0
- package/SECURITY.md +17 -0
- package/dist/adaptivedpr.d.ts +2 -0
- package/dist/adaptivedpr.d.ts.map +1 -0
- package/dist/adaptivedpr.js +65 -0
- package/dist/camera/cameraRigProfile.d.ts +12 -0
- package/dist/camera/cameraRigProfile.d.ts.map +1 -0
- package/dist/camera/cameraRigProfile.js +18 -0
- package/dist/camera/managedCameraController.d.ts +49 -0
- package/dist/camera/managedCameraController.d.ts.map +1 -0
- package/dist/camera/managedCameraController.js +271 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/landscape.d.ts +2 -0
- package/dist/landscape.d.ts.map +1 -0
- package/dist/landscape.js +120 -0
- package/dist/player/player.d.ts +8 -0
- package/dist/player/player.d.ts.map +1 -0
- package/dist/player/player.js +203 -0
- package/dist/player/playerstore.d.ts +205 -0
- package/dist/player/playerstore.d.ts.map +1 -0
- package/dist/player/playerstore.js +500 -0
- package/dist/renderStateProvider.d.ts +57 -0
- package/dist/renderStateProvider.d.ts.map +1 -0
- package/dist/renderStateProvider.js +50 -0
- package/dist/renderer.d.ts +9 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +165 -0
- package/dist/scene.d.ts +7 -0
- package/dist/scene.d.ts.map +1 -0
- package/dist/scene.js +10 -0
- package/dist/shaders/fragment/landscapeFragmentShader.js +141 -0
- package/dist/shaders/landscapeShader.d.ts +13 -0
- package/dist/shaders/landscapeShader.d.ts.map +1 -0
- package/dist/shaders/landscapeShader.js +25 -0
- package/dist/shaders/vertex/landscapeVertexShader.js +67 -0
- package/dist/styles/renderer.module.css +90 -0
- package/dist/worldSpaceCompositor.d.ts +50 -0
- package/dist/worldSpaceCompositor.d.ts.map +1 -0
- package/dist/worldSpaceCompositor.js +159 -0
- package/dist/xr/rendererXrBridge.d.ts +12 -0
- package/dist/xr/rendererXrBridge.d.ts.map +1 -0
- package/dist/xr/rendererXrBridge.js +17 -0
- package/docs/adrs/adr-0001-renderer-package-scope.md +21 -0
- package/docs/adrs/adr-0002-public-repo-governance.md +24 -0
- package/docs/adrs/adr-0003-world-space-compositor-contracts.md +34 -0
- package/docs/adrs/adr-template.md +35 -0
- package/docs/design/0001-public-package-scope.md +18 -0
- package/docs/tdrs/index.md +3 -0
- package/docs/tdrs/tdr-0001-renderer-public-package-standards-alignment.md +19 -0
- package/legal/CLA-REGISTRY.csv +1 -0
- package/legal/CLA.md +22 -0
- package/legal/CORPORATE_CLA.md +57 -0
- package/legal/INDIVIDUAL_CLA.md +91 -0
- package/package.json +117 -0
- package/src/adaptivedpr.tsx +74 -0
- package/src/camera/cameraRigProfile.ts +29 -0
- package/src/camera/managedCameraController.tsx +401 -0
- package/src/global.d.ts +10 -0
- package/src/index.ts +3 -0
- package/src/landscape.tsx +321 -0
- package/src/player/player.tsx +257 -0
- package/src/player/playerstore.tsx +733 -0
- package/src/renderStateProvider.tsx +121 -0
- package/src/renderer.tsx +294 -0
- package/src/scene.tsx +42 -0
- package/src/shaders/fragment/landscapeFragmentShader.d.ts +4 -0
- package/src/shaders/fragment/landscapeFragmentShader.js +141 -0
- package/src/shaders/landscapeShader.tsx +39 -0
- package/src/shaders/vertex/landscapeVertexShader.d.ts +4 -0
- package/src/shaders/vertex/landscapeVertexShader.js +67 -0
- package/src/styles/renderer.module.css +90 -0
- package/src/worldSpaceCompositor.ts +265 -0
- package/src/xr/rendererXrBridge.ts +44 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { useFrame, useThree } from "@react-three/fiber";
|
|
3
|
+
import { createCameraManager, } from "@plasius/gpu-camera";
|
|
4
|
+
import { Vector3 } from "three";
|
|
5
|
+
function clamp(value, min, max) {
|
|
6
|
+
return Math.min(max, Math.max(min, value));
|
|
7
|
+
}
|
|
8
|
+
function length3(value) {
|
|
9
|
+
return Math.hypot(value[0], value[1], value[2]);
|
|
10
|
+
}
|
|
11
|
+
function normalize3(value, fallback) {
|
|
12
|
+
const len = length3(value);
|
|
13
|
+
if (len <= Number.EPSILON) {
|
|
14
|
+
return [...fallback];
|
|
15
|
+
}
|
|
16
|
+
return [value[0] / len, value[1] / len, value[2] / len];
|
|
17
|
+
}
|
|
18
|
+
function sub3(a, b) {
|
|
19
|
+
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
20
|
+
}
|
|
21
|
+
function add3(a, b) {
|
|
22
|
+
return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
|
|
23
|
+
}
|
|
24
|
+
function scale3(value, scalar) {
|
|
25
|
+
return [value[0] * scalar, value[1] * scalar, value[2] * scalar];
|
|
26
|
+
}
|
|
27
|
+
function cross3(a, b) {
|
|
28
|
+
return [
|
|
29
|
+
a[1] * b[2] - a[2] * b[1],
|
|
30
|
+
a[2] * b[0] - a[0] * b[2],
|
|
31
|
+
a[0] * b[1] - a[1] * b[0],
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
export function resolveCameraAspect(width, height) {
|
|
35
|
+
if (!Number.isFinite(width) || !Number.isFinite(height) || height <= 0) {
|
|
36
|
+
return 1;
|
|
37
|
+
}
|
|
38
|
+
return Math.max(1 / 4096, width / height);
|
|
39
|
+
}
|
|
40
|
+
export function createRendererCameraManager(options) {
|
|
41
|
+
return createCameraManager({
|
|
42
|
+
maxParallelViews: options?.maxParallelViews ?? 2,
|
|
43
|
+
maxHotCameras: options?.maxHotCameras ?? 3,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
export function derivePanDeltaFromCameraState(cameraState, deltaX, deltaY, panSpeed) {
|
|
47
|
+
const position = cameraState.transform.position;
|
|
48
|
+
const target = cameraState.transform.target;
|
|
49
|
+
const up = normalize3(cameraState.transform.up ?? [0, 1, 0], [0, 1, 0]);
|
|
50
|
+
const forward = normalize3(sub3(target, position), [0, 0, -1]);
|
|
51
|
+
const right = normalize3(cross3(forward, up), [1, 0, 0]);
|
|
52
|
+
const radius = Math.max(0.01, length3(sub3(position, target)));
|
|
53
|
+
const scale = Math.max(0.00001, panSpeed * radius);
|
|
54
|
+
const horizontal = scale3(right, -deltaX * scale);
|
|
55
|
+
const vertical = scale3(up, deltaY * scale);
|
|
56
|
+
return add3(horizontal, vertical);
|
|
57
|
+
}
|
|
58
|
+
export function buildCameraDefinitionFromThreeCamera(camera, aspect) {
|
|
59
|
+
const position = [camera.position.x, camera.position.y, camera.position.z];
|
|
60
|
+
const up = [camera.up.x, camera.up.y, camera.up.z];
|
|
61
|
+
const worldDirection = new Vector3(0, 0, -1);
|
|
62
|
+
camera.getWorldDirection?.(worldDirection);
|
|
63
|
+
const target = [
|
|
64
|
+
position[0] + worldDirection.x,
|
|
65
|
+
position[1] + worldDirection.y,
|
|
66
|
+
position[2] + worldDirection.z,
|
|
67
|
+
];
|
|
68
|
+
if (camera.isOrthographicCamera) {
|
|
69
|
+
return {
|
|
70
|
+
transform: { position, target, up },
|
|
71
|
+
projection: {
|
|
72
|
+
kind: "orthographic",
|
|
73
|
+
left: camera.left ?? -1,
|
|
74
|
+
right: camera.right ?? 1,
|
|
75
|
+
top: camera.top ?? 1,
|
|
76
|
+
bottom: camera.bottom ?? -1,
|
|
77
|
+
near: camera.near ?? 0.1,
|
|
78
|
+
far: camera.far ?? 2000,
|
|
79
|
+
aspect,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
transform: { position, target, up },
|
|
85
|
+
projection: {
|
|
86
|
+
kind: "perspective",
|
|
87
|
+
fovY: camera.fov ?? 50,
|
|
88
|
+
near: camera.near ?? 0.1,
|
|
89
|
+
far: camera.far ?? 2000,
|
|
90
|
+
aspect,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export function syncThreeCameraFromManagedState(camera, cameraState, aspect) {
|
|
95
|
+
const transform = cameraState.transform;
|
|
96
|
+
const up = transform.up ?? [0, 1, 0];
|
|
97
|
+
camera.position.set(transform.position[0], transform.position[1], transform.position[2]);
|
|
98
|
+
camera.up.set(up[0], up[1], up[2]);
|
|
99
|
+
camera.lookAt(transform.target[0], transform.target[1], transform.target[2]);
|
|
100
|
+
const projection = cameraState.projection;
|
|
101
|
+
if (projection.kind === "perspective" && camera.isPerspectiveCamera) {
|
|
102
|
+
camera.fov = projection.fovY;
|
|
103
|
+
camera.near = projection.near;
|
|
104
|
+
camera.far = projection.far;
|
|
105
|
+
camera.aspect = aspect;
|
|
106
|
+
camera.updateProjectionMatrix?.();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (projection.kind === "orthographic" && camera.isOrthographicCamera) {
|
|
110
|
+
const sourceAspect = projection.aspect || 1;
|
|
111
|
+
const aspectScale = sourceAspect > 0 ? aspect / sourceAspect : 1;
|
|
112
|
+
camera.left = projection.left * aspectScale;
|
|
113
|
+
camera.right = projection.right * aspectScale;
|
|
114
|
+
camera.top = projection.top;
|
|
115
|
+
camera.bottom = projection.bottom;
|
|
116
|
+
camera.near = projection.near;
|
|
117
|
+
camera.far = projection.far;
|
|
118
|
+
camera.updateProjectionMatrix?.();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
export function ManagedCameraController({ manager, profile, cameraId = "main", enabled = true, }) {
|
|
122
|
+
const { camera, gl, size } = useThree();
|
|
123
|
+
const pointerRef = useRef({
|
|
124
|
+
active: false,
|
|
125
|
+
mode: null,
|
|
126
|
+
x: 0,
|
|
127
|
+
y: 0,
|
|
128
|
+
});
|
|
129
|
+
const managedThreeCamera = camera;
|
|
130
|
+
const aspect = resolveCameraAspect(size.width, size.height);
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
const cameraDefinition = buildCameraDefinitionFromThreeCamera(managedThreeCamera, aspect);
|
|
133
|
+
if (manager.hasCamera(cameraId)) {
|
|
134
|
+
manager.updateCamera(cameraId, cameraDefinition);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
manager.registerCamera({
|
|
138
|
+
id: cameraId,
|
|
139
|
+
priority: 100,
|
|
140
|
+
...cameraDefinition,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
manager.activateCamera(cameraId);
|
|
144
|
+
}, [manager, cameraId, managedThreeCamera, aspect]);
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
const registered = manager.getCamera(cameraId);
|
|
147
|
+
if (!registered) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (registered.projection.kind === "perspective") {
|
|
151
|
+
manager.updateCamera(cameraId, {
|
|
152
|
+
projection: {
|
|
153
|
+
...registered.projection,
|
|
154
|
+
aspect,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
manager.updateCamera(cameraId, {
|
|
160
|
+
projection: {
|
|
161
|
+
...registered.projection,
|
|
162
|
+
aspect,
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
}, [manager, cameraId, aspect]);
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
if (!enabled) {
|
|
168
|
+
pointerRef.current.active = false;
|
|
169
|
+
pointerRef.current.mode = null;
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const element = gl.domElement;
|
|
173
|
+
if (!element) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const onPointerDown = (event) => {
|
|
177
|
+
if (event.button === 0 && !event.shiftKey) {
|
|
178
|
+
pointerRef.current.mode = "orbit";
|
|
179
|
+
}
|
|
180
|
+
else if (event.button === 2 || event.button === 1 || event.shiftKey) {
|
|
181
|
+
pointerRef.current.mode = "pan";
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
pointerRef.current.mode = null;
|
|
185
|
+
}
|
|
186
|
+
if (!pointerRef.current.mode) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
pointerRef.current.active = true;
|
|
190
|
+
pointerRef.current.x = event.clientX;
|
|
191
|
+
pointerRef.current.y = event.clientY;
|
|
192
|
+
element.setPointerCapture?.(event.pointerId);
|
|
193
|
+
};
|
|
194
|
+
const onPointerMove = (event) => {
|
|
195
|
+
if (!pointerRef.current.active || !pointerRef.current.mode) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const deltaX = event.clientX - pointerRef.current.x;
|
|
199
|
+
const deltaY = event.clientY - pointerRef.current.y;
|
|
200
|
+
pointerRef.current.x = event.clientX;
|
|
201
|
+
pointerRef.current.y = event.clientY;
|
|
202
|
+
if (pointerRef.current.mode === "orbit") {
|
|
203
|
+
manager.applyControl(cameraId, {
|
|
204
|
+
type: "orbit",
|
|
205
|
+
deltaAzimuth: -deltaX * profile.orbitSpeed,
|
|
206
|
+
deltaPolar: -deltaY * profile.orbitSpeed,
|
|
207
|
+
}, {
|
|
208
|
+
minDistance: profile.minDistance,
|
|
209
|
+
maxDistance: profile.maxDistance,
|
|
210
|
+
minPolarAngle: profile.minPolarAngle,
|
|
211
|
+
maxPolarAngle: profile.maxPolarAngle,
|
|
212
|
+
makeActive: true,
|
|
213
|
+
});
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const activeCamera = manager.getCamera(cameraId);
|
|
217
|
+
if (!activeCamera) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const delta = derivePanDeltaFromCameraState(activeCamera, deltaX, deltaY, profile.panSpeed);
|
|
221
|
+
manager.applyControl(cameraId, {
|
|
222
|
+
type: "pan",
|
|
223
|
+
delta,
|
|
224
|
+
}, { makeActive: true });
|
|
225
|
+
};
|
|
226
|
+
const onPointerUp = (event) => {
|
|
227
|
+
pointerRef.current.active = false;
|
|
228
|
+
pointerRef.current.mode = null;
|
|
229
|
+
element.releasePointerCapture?.(event.pointerId);
|
|
230
|
+
};
|
|
231
|
+
const onWheel = (event) => {
|
|
232
|
+
event.preventDefault();
|
|
233
|
+
const activeCamera = manager.getCamera(cameraId);
|
|
234
|
+
if (!activeCamera) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const distanceToTarget = Math.max(0.01, length3(sub3(activeCamera.transform.position, activeCamera.transform.target)));
|
|
238
|
+
const step = -event.deltaY * profile.dollySpeed * Math.max(0.05, distanceToTarget * 0.2);
|
|
239
|
+
manager.applyControl(cameraId, { type: "dolly", distance: step }, {
|
|
240
|
+
minDistance: profile.minDistance,
|
|
241
|
+
maxDistance: profile.maxDistance,
|
|
242
|
+
makeActive: true,
|
|
243
|
+
});
|
|
244
|
+
};
|
|
245
|
+
const onContextMenu = (event) => {
|
|
246
|
+
event.preventDefault();
|
|
247
|
+
};
|
|
248
|
+
element.addEventListener("pointerdown", onPointerDown);
|
|
249
|
+
window.addEventListener("pointermove", onPointerMove);
|
|
250
|
+
window.addEventListener("pointerup", onPointerUp);
|
|
251
|
+
element.addEventListener("wheel", onWheel, { passive: false });
|
|
252
|
+
element.addEventListener("contextmenu", onContextMenu);
|
|
253
|
+
return () => {
|
|
254
|
+
element.removeEventListener("pointerdown", onPointerDown);
|
|
255
|
+
window.removeEventListener("pointermove", onPointerMove);
|
|
256
|
+
window.removeEventListener("pointerup", onPointerUp);
|
|
257
|
+
element.removeEventListener("wheel", onWheel);
|
|
258
|
+
element.removeEventListener("contextmenu", onContextMenu);
|
|
259
|
+
};
|
|
260
|
+
}, [manager, cameraId, profile, gl, enabled]);
|
|
261
|
+
useFrame(() => {
|
|
262
|
+
const snapshot = manager.getSnapshot();
|
|
263
|
+
const activeId = snapshot.activeCameraId ?? cameraId;
|
|
264
|
+
const activeCamera = manager.getCamera(activeId);
|
|
265
|
+
if (!activeCamera) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
syncThreeCameraFromManagedState(managedThreeCamera, activeCamera, aspect);
|
|
269
|
+
});
|
|
270
|
+
return null;
|
|
271
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,8BAA8B,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,2BAA2B,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"landscape.d.ts","sourceRoot":"","sources":["../src/landscape.tsx"],"names":[],"mappings":"AA4GA,wBAAgB,SAAS,gCAsDxB"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as THREE from "three";
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import * as BufferGeometryUtils from "three/examples/jsm/utils/BufferGeometryUtils.js";
|
|
5
|
+
import { RigidBody } from "@react-three/rapier";
|
|
6
|
+
const HEX_RADIUS = 66;
|
|
7
|
+
//const TILE_RESOLUTION = 2048;
|
|
8
|
+
const AREA_WIDTH = 2000;
|
|
9
|
+
const AREA_HEIGHT = 2000;
|
|
10
|
+
// Generate a single high-resolution hexagon as BufferGeometry
|
|
11
|
+
function generateHexTile(radius) {
|
|
12
|
+
const geom = new THREE.BufferGeometry();
|
|
13
|
+
const vertices = [];
|
|
14
|
+
const indices = [];
|
|
15
|
+
const segments = 30; // More segments for smoother curve
|
|
16
|
+
const angleStep = (Math.PI * 2) / segments;
|
|
17
|
+
// Center point
|
|
18
|
+
vertices.push(0, 0, 0);
|
|
19
|
+
// Outer ring vertices
|
|
20
|
+
for (let i = 0; i <= segments; i++) {
|
|
21
|
+
const angle = angleStep * i;
|
|
22
|
+
const x = radius * Math.cos(angle);
|
|
23
|
+
const z = -radius * Math.sin(angle); // flip Z axis for pointy top upward
|
|
24
|
+
vertices.push(x, 0, z);
|
|
25
|
+
}
|
|
26
|
+
// Create triangle fan indices
|
|
27
|
+
for (let i = 1; i <= segments; i++) {
|
|
28
|
+
indices.push(0, i, i + 1);
|
|
29
|
+
}
|
|
30
|
+
geom.setAttribute("position", new THREE.Float32BufferAttribute(vertices, 3));
|
|
31
|
+
geom.setIndex(indices);
|
|
32
|
+
geom.computeVertexNormals();
|
|
33
|
+
return geom;
|
|
34
|
+
}
|
|
35
|
+
// Axial to world coordinates (pointy-topped hex)
|
|
36
|
+
function axialToWorld(q, r, radius) {
|
|
37
|
+
const x = ((radius * 3) / 2) * q;
|
|
38
|
+
const z = radius * Math.sqrt(3) * (r + q / 2);
|
|
39
|
+
return [x, z];
|
|
40
|
+
}
|
|
41
|
+
// Simple seeded noise for per-tile deformation
|
|
42
|
+
function seededNoise(x, z, seed = 42) {
|
|
43
|
+
const s = Math.sin(x * 12.9898 + z * 78.233 + seed) * 43758.5453;
|
|
44
|
+
return (s - Math.floor(s)) * 2 - 1;
|
|
45
|
+
}
|
|
46
|
+
// Deform only the interior (non-edge) vertices of a hex tile
|
|
47
|
+
function deformHexInterior(geom, radius, seed = 42) {
|
|
48
|
+
const pos = geom.attributes.position;
|
|
49
|
+
const vertexCount = pos.count;
|
|
50
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
51
|
+
const x = pos.getX(i);
|
|
52
|
+
const z = pos.getZ(i);
|
|
53
|
+
const r = Math.sqrt(x * x + z * z);
|
|
54
|
+
// Only deform vertices inside the hex edge (leave edge vertices flat)
|
|
55
|
+
if (r < radius * 0.98) {
|
|
56
|
+
// Blend multiple nearby noise samples for smoother curvature
|
|
57
|
+
const n = (dx, dz) => seededNoise(x + dx, z + dz, seed);
|
|
58
|
+
const smoothY = (n(0, 0) * 4 +
|
|
59
|
+
n(-1, 0) +
|
|
60
|
+
n(1, 0) +
|
|
61
|
+
n(0, -1) +
|
|
62
|
+
n(0, 1) +
|
|
63
|
+
n(-1, -1) +
|
|
64
|
+
n(1, 1) +
|
|
65
|
+
n(-1, 1) +
|
|
66
|
+
n(1, -1)) /
|
|
67
|
+
12;
|
|
68
|
+
pos.setY(i, smoothY);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
pos.needsUpdate = true;
|
|
72
|
+
geom.computeVertexNormals();
|
|
73
|
+
}
|
|
74
|
+
function generateHexDepthMap(cols, rows, seed = 42) {
|
|
75
|
+
const map = [];
|
|
76
|
+
for (let q = 0; q < cols; q++) {
|
|
77
|
+
const row = [];
|
|
78
|
+
for (let r = 0; r < rows; r++) {
|
|
79
|
+
const value = Math.sin((q + seed) * 0.3) * Math.cos((r + seed) * 0.3);
|
|
80
|
+
row.push(value);
|
|
81
|
+
}
|
|
82
|
+
map.push(row);
|
|
83
|
+
}
|
|
84
|
+
return map;
|
|
85
|
+
}
|
|
86
|
+
export function Landscape() {
|
|
87
|
+
const geometry = useMemo(() => {
|
|
88
|
+
const radius = HEX_RADIUS;
|
|
89
|
+
const tiles = [];
|
|
90
|
+
const spacingX = (3 / 2) * radius;
|
|
91
|
+
const spacingZ = Math.sqrt(3) * radius;
|
|
92
|
+
const cols = Math.ceil(AREA_WIDTH / spacingX);
|
|
93
|
+
const rows = Math.ceil(AREA_HEIGHT / spacingZ);
|
|
94
|
+
const offset = Math.floor(cols / 2);
|
|
95
|
+
const depthMap = generateHexDepthMap(cols, rows);
|
|
96
|
+
const elevationScale = 20;
|
|
97
|
+
const baseHex = generateHexTile(radius);
|
|
98
|
+
for (let q = -Math.floor(cols / 2); q < Math.ceil(cols / 2); q++) {
|
|
99
|
+
for (let r = -Math.floor(rows / 2); r < Math.ceil(rows / 2); r++) {
|
|
100
|
+
const hex = baseHex.clone();
|
|
101
|
+
const dq = q + offset;
|
|
102
|
+
const dr = r + offset;
|
|
103
|
+
const elevation = depthMap?.[dq]?.[dr] ?? 0;
|
|
104
|
+
deformHexInterior(hex, radius, 42 + q * 1000 + r + Math.floor(elevation * 100));
|
|
105
|
+
const [x, z] = axialToWorld(q, r, radius);
|
|
106
|
+
hex.translate(x, elevation * elevationScale, z);
|
|
107
|
+
tiles.push(hex);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const merged = BufferGeometryUtils.mergeGeometries(tiles, false);
|
|
111
|
+
// Remove normals before merging vertices to ensure proper welding
|
|
112
|
+
if (merged.attributes.normal) {
|
|
113
|
+
merged.deleteAttribute("normal");
|
|
114
|
+
}
|
|
115
|
+
const stitched = BufferGeometryUtils.mergeVertices(merged, 1e-2);
|
|
116
|
+
stitched.computeVertexNormals();
|
|
117
|
+
return stitched;
|
|
118
|
+
}, []);
|
|
119
|
+
return (_jsx(RigidBody, { type: "fixed", colliders: "trimesh", children: _jsx("mesh", { geometry: geometry, castShadow: true, receiveShadow: true, children: _jsx("landscapeShaderMaterial", { color: "#77aa66" }) }) }));
|
|
120
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React, { type PropsWithChildren } from "react";
|
|
2
|
+
import type { CameraManager } from "@plasius/gpu-camera";
|
|
3
|
+
declare function WrappedPlayer({ cameraManager, cameraId, children, }: PropsWithChildren<{
|
|
4
|
+
cameraManager: CameraManager;
|
|
5
|
+
cameraId: string;
|
|
6
|
+
}>): React.JSX.Element;
|
|
7
|
+
export { WrappedPlayer as Player };
|
|
8
|
+
//# sourceMappingURL=player.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"player.d.ts","sourceRoot":"","sources":["../../src/player/player.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAqB,KAAK,iBAAiB,EAAE,MAAM,OAAO,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAQ,MAAM,qBAAqB,CAAC;AAiP/D,iBAAS,aAAa,CAAC,EACrB,aAAa,EACb,QAAQ,EACR,QAAQ,GACT,EAAE,iBAAiB,CAAC;IAAE,aAAa,EAAE,aAAa,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,qBAQvE;AAED,OAAO,EAAE,aAAa,IAAI,MAAM,EAAE,CAAC"}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import { PlayerStore } from "./playerstore.js";
|
|
4
|
+
function length3(value) {
|
|
5
|
+
return Math.hypot(value[0], value[1], value[2]);
|
|
6
|
+
}
|
|
7
|
+
function normalize3(value, fallback) {
|
|
8
|
+
const len = length3(value);
|
|
9
|
+
if (len <= Number.EPSILON) {
|
|
10
|
+
return [...fallback];
|
|
11
|
+
}
|
|
12
|
+
return [value[0] / len, value[1] / len, value[2] / len];
|
|
13
|
+
}
|
|
14
|
+
function add3(a, b) {
|
|
15
|
+
return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
|
|
16
|
+
}
|
|
17
|
+
function sub3(a, b) {
|
|
18
|
+
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
19
|
+
}
|
|
20
|
+
function cross3(a, b) {
|
|
21
|
+
return [
|
|
22
|
+
a[1] * b[2] - a[2] * b[1],
|
|
23
|
+
a[2] * b[0] - a[0] * b[2],
|
|
24
|
+
a[0] * b[1] - a[1] * b[0],
|
|
25
|
+
];
|
|
26
|
+
}
|
|
27
|
+
function Player({ cameraManager, cameraId, children, }) {
|
|
28
|
+
// Local refs for movement-relevant stats (used by the rAF ticker)
|
|
29
|
+
const strRef = useRef(1);
|
|
30
|
+
const dexRef = useRef(1);
|
|
31
|
+
const endRef = useRef(1);
|
|
32
|
+
const movingKeys = useRef(new Set());
|
|
33
|
+
const movingRef = useRef(false);
|
|
34
|
+
const runLoadRef = useRef(0); // seconds of continuous run load
|
|
35
|
+
const lastTickRef = useRef(performance.now());
|
|
36
|
+
// Subscribe via inline selector (useSyncExternalStore under the hood)
|
|
37
|
+
const physEff = PlayerStore.useSelector((s) => {
|
|
38
|
+
const phys = s?.attributesBase?.physical ?? {
|
|
39
|
+
strength: 1,
|
|
40
|
+
dexterity: 1,
|
|
41
|
+
endurance: 1,
|
|
42
|
+
};
|
|
43
|
+
const g = s?.attributesGear ?? {};
|
|
44
|
+
const e = s?.attributesEffects ?? {};
|
|
45
|
+
let strength = (phys.strength ?? 1) + (g.strength ?? 0) + (e.strength ?? 0);
|
|
46
|
+
let dexterity = (phys.dexterity ?? 1) + (g.dexterity ?? 0) + (e.dexterity ?? 0);
|
|
47
|
+
let endurance = (phys.endurance ?? 1) + (g.endurance ?? 0) + (e.endurance ?? 0);
|
|
48
|
+
// Add equipped item modifiers
|
|
49
|
+
if (s?.equipment && s?.items) {
|
|
50
|
+
for (const slot of Object.keys(s.equipment)) {
|
|
51
|
+
const itemId = s.equipment[slot];
|
|
52
|
+
const item = s.items[itemId];
|
|
53
|
+
const m = item?.modifiers;
|
|
54
|
+
if (m) {
|
|
55
|
+
strength += m.strength ?? 0;
|
|
56
|
+
dexterity += m.dexterity ?? 0;
|
|
57
|
+
endurance += m.endurance ?? 0;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Add status effect attribute modifiers
|
|
62
|
+
if (s?.effects) {
|
|
63
|
+
for (const eff of Object.values(s.effects)) {
|
|
64
|
+
const mods = eff?.modifiers?.attributes;
|
|
65
|
+
if (mods) {
|
|
66
|
+
strength += mods.strength ?? 0;
|
|
67
|
+
dexterity += mods.dexterity ?? 0;
|
|
68
|
+
endurance += mods.endurance ?? 0;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { strength, dexterity, endurance };
|
|
73
|
+
});
|
|
74
|
+
// Keep refs in sync for rAF without causing extra renders
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
strRef.current = physEff.strength;
|
|
77
|
+
dexRef.current = physEff.dexterity;
|
|
78
|
+
endRef.current = physEff.endurance;
|
|
79
|
+
}, [physEff.strength, physEff.dexterity, physEff.endurance]);
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
window.focus();
|
|
82
|
+
}, []);
|
|
83
|
+
// Fatigue/exhaustion ticker
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
let raf = 0;
|
|
86
|
+
const tick = () => {
|
|
87
|
+
const now = performance.now();
|
|
88
|
+
const dt = (now - lastTickRef.current) / 1000;
|
|
89
|
+
lastTickRef.current = now;
|
|
90
|
+
const ln = (x) => Math.log(1 + Math.max(0, x) / 25);
|
|
91
|
+
const endurance = endRef.current;
|
|
92
|
+
const recovery = 1.5 + ln(endurance); // recover faster with Endurance
|
|
93
|
+
if (movingRef.current) {
|
|
94
|
+
runLoadRef.current = Math.max(0, runLoadRef.current + dt);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
runLoadRef.current = Math.max(0, runLoadRef.current - dt * recovery);
|
|
98
|
+
}
|
|
99
|
+
raf = requestAnimationFrame(tick);
|
|
100
|
+
};
|
|
101
|
+
lastTickRef.current = performance.now();
|
|
102
|
+
raf = requestAnimationFrame(tick);
|
|
103
|
+
return () => cancelAnimationFrame(raf);
|
|
104
|
+
}, []);
|
|
105
|
+
// Movement handlers
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
const ln = (x) => Math.log(1 + Math.max(0, x) / 25);
|
|
108
|
+
const baseSpeed = () => {
|
|
109
|
+
const dex = dexRef.current;
|
|
110
|
+
const str = strRef.current;
|
|
111
|
+
const dexBoost = 0.25 * ln(dex);
|
|
112
|
+
const strBoost = 0.1 * ln(str);
|
|
113
|
+
return 1 + dexBoost + strBoost;
|
|
114
|
+
};
|
|
115
|
+
const currentSpeed = () => {
|
|
116
|
+
const endurance = endRef.current;
|
|
117
|
+
const cap = 4 + 2 * ln(endurance);
|
|
118
|
+
const load = runLoadRef.current;
|
|
119
|
+
if (load <= cap)
|
|
120
|
+
return baseSpeed();
|
|
121
|
+
const over = Math.min(1, (load - cap) / cap);
|
|
122
|
+
const fatigue = 1 - 0.5 * over; // down to 50%
|
|
123
|
+
return baseSpeed() * fatigue;
|
|
124
|
+
};
|
|
125
|
+
const handleKeyDown = (event) => {
|
|
126
|
+
const cameraState = cameraManager.getCamera(cameraId);
|
|
127
|
+
if (!cameraState)
|
|
128
|
+
return;
|
|
129
|
+
const position = [...cameraState.transform.position];
|
|
130
|
+
const target = [...cameraState.transform.target];
|
|
131
|
+
const up = normalize3(cameraState.transform.up ?? [0, 1, 0], [0, 1, 0]);
|
|
132
|
+
const forward = normalize3(sub3(target, position), [0, 0, -1]);
|
|
133
|
+
const forwardY = forward[1];
|
|
134
|
+
const planarForward = normalize3([forward[0], 0, forward[2]], [0, 0, -1]);
|
|
135
|
+
const right = normalize3(cross3(planarForward, up), [1, 0, 0]);
|
|
136
|
+
const moveKeys = new Set(["w", "a", "s", "d", "q", "e"]);
|
|
137
|
+
if (moveKeys.has(event.key)) {
|
|
138
|
+
movingKeys.current.add(event.key);
|
|
139
|
+
movingRef.current = true;
|
|
140
|
+
}
|
|
141
|
+
switch (event.key) {
|
|
142
|
+
case "w":
|
|
143
|
+
position[0] += planarForward[0] * currentSpeed();
|
|
144
|
+
position[1] += planarForward[1] * currentSpeed();
|
|
145
|
+
position[2] += planarForward[2] * currentSpeed();
|
|
146
|
+
break;
|
|
147
|
+
case "s":
|
|
148
|
+
position[0] -= planarForward[0] * currentSpeed();
|
|
149
|
+
position[1] -= planarForward[1] * currentSpeed();
|
|
150
|
+
position[2] -= planarForward[2] * currentSpeed();
|
|
151
|
+
break;
|
|
152
|
+
case "a":
|
|
153
|
+
position[0] -= right[0] * currentSpeed();
|
|
154
|
+
position[1] -= right[1] * currentSpeed();
|
|
155
|
+
position[2] -= right[2] * currentSpeed();
|
|
156
|
+
break;
|
|
157
|
+
case "d":
|
|
158
|
+
position[0] += right[0] * currentSpeed();
|
|
159
|
+
position[1] += right[1] * currentSpeed();
|
|
160
|
+
position[2] += right[2] * currentSpeed();
|
|
161
|
+
break;
|
|
162
|
+
case "q":
|
|
163
|
+
position[1] += currentSpeed();
|
|
164
|
+
break;
|
|
165
|
+
case "e":
|
|
166
|
+
position[1] -= currentSpeed();
|
|
167
|
+
break;
|
|
168
|
+
default:
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
const nextTarget = add3(position, [
|
|
172
|
+
planarForward[0],
|
|
173
|
+
forwardY,
|
|
174
|
+
planarForward[2],
|
|
175
|
+
]);
|
|
176
|
+
cameraManager.applyControl(cameraId, {
|
|
177
|
+
type: "set-look-at",
|
|
178
|
+
position,
|
|
179
|
+
target: nextTarget,
|
|
180
|
+
up,
|
|
181
|
+
}, { makeActive: true });
|
|
182
|
+
};
|
|
183
|
+
const handleKeyUp = (event) => {
|
|
184
|
+
const moveKeys = new Set(["w", "a", "s", "d", "q", "e"]);
|
|
185
|
+
if (moveKeys.has(event.key)) {
|
|
186
|
+
movingKeys.current.delete(event.key);
|
|
187
|
+
if (movingKeys.current.size === 0)
|
|
188
|
+
movingRef.current = false;
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
192
|
+
window.addEventListener("keyup", handleKeyUp);
|
|
193
|
+
return () => {
|
|
194
|
+
window.removeEventListener("keydown", handleKeyDown);
|
|
195
|
+
window.removeEventListener("keyup", handleKeyUp);
|
|
196
|
+
};
|
|
197
|
+
}, [cameraManager, cameraId]);
|
|
198
|
+
return _jsx(_Fragment, { children: children });
|
|
199
|
+
}
|
|
200
|
+
function WrappedPlayer({ cameraManager, cameraId, children, }) {
|
|
201
|
+
return (_jsx(PlayerStore.Provider, { children: _jsx(Player, { cameraManager: cameraManager, cameraId: cameraId, children: children }) }));
|
|
202
|
+
}
|
|
203
|
+
export { WrappedPlayer as Player };
|