@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.
Files changed (79) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/CODE_OF_CONDUCT.md +79 -0
  3. package/CONTRIBUTORS.md +27 -0
  4. package/LICENSE +203 -0
  5. package/README.md +70 -0
  6. package/SECURITY.md +17 -0
  7. package/dist/adaptivedpr.d.ts +2 -0
  8. package/dist/adaptivedpr.d.ts.map +1 -0
  9. package/dist/adaptivedpr.js +65 -0
  10. package/dist/camera/cameraRigProfile.d.ts +12 -0
  11. package/dist/camera/cameraRigProfile.d.ts.map +1 -0
  12. package/dist/camera/cameraRigProfile.js +18 -0
  13. package/dist/camera/managedCameraController.d.ts +49 -0
  14. package/dist/camera/managedCameraController.d.ts.map +1 -0
  15. package/dist/camera/managedCameraController.js +271 -0
  16. package/dist/index.d.ts +4 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +3 -0
  19. package/dist/landscape.d.ts +2 -0
  20. package/dist/landscape.d.ts.map +1 -0
  21. package/dist/landscape.js +120 -0
  22. package/dist/player/player.d.ts +8 -0
  23. package/dist/player/player.d.ts.map +1 -0
  24. package/dist/player/player.js +203 -0
  25. package/dist/player/playerstore.d.ts +205 -0
  26. package/dist/player/playerstore.d.ts.map +1 -0
  27. package/dist/player/playerstore.js +500 -0
  28. package/dist/renderStateProvider.d.ts +57 -0
  29. package/dist/renderStateProvider.d.ts.map +1 -0
  30. package/dist/renderStateProvider.js +50 -0
  31. package/dist/renderer.d.ts +9 -0
  32. package/dist/renderer.d.ts.map +1 -0
  33. package/dist/renderer.js +165 -0
  34. package/dist/scene.d.ts +7 -0
  35. package/dist/scene.d.ts.map +1 -0
  36. package/dist/scene.js +10 -0
  37. package/dist/shaders/fragment/landscapeFragmentShader.js +141 -0
  38. package/dist/shaders/landscapeShader.d.ts +13 -0
  39. package/dist/shaders/landscapeShader.d.ts.map +1 -0
  40. package/dist/shaders/landscapeShader.js +25 -0
  41. package/dist/shaders/vertex/landscapeVertexShader.js +67 -0
  42. package/dist/styles/renderer.module.css +90 -0
  43. package/dist/worldSpaceCompositor.d.ts +50 -0
  44. package/dist/worldSpaceCompositor.d.ts.map +1 -0
  45. package/dist/worldSpaceCompositor.js +159 -0
  46. package/dist/xr/rendererXrBridge.d.ts +12 -0
  47. package/dist/xr/rendererXrBridge.d.ts.map +1 -0
  48. package/dist/xr/rendererXrBridge.js +17 -0
  49. package/docs/adrs/adr-0001-renderer-package-scope.md +21 -0
  50. package/docs/adrs/adr-0002-public-repo-governance.md +24 -0
  51. package/docs/adrs/adr-0003-world-space-compositor-contracts.md +34 -0
  52. package/docs/adrs/adr-template.md +35 -0
  53. package/docs/design/0001-public-package-scope.md +18 -0
  54. package/docs/tdrs/index.md +3 -0
  55. package/docs/tdrs/tdr-0001-renderer-public-package-standards-alignment.md +19 -0
  56. package/legal/CLA-REGISTRY.csv +1 -0
  57. package/legal/CLA.md +22 -0
  58. package/legal/CORPORATE_CLA.md +57 -0
  59. package/legal/INDIVIDUAL_CLA.md +91 -0
  60. package/package.json +117 -0
  61. package/src/adaptivedpr.tsx +74 -0
  62. package/src/camera/cameraRigProfile.ts +29 -0
  63. package/src/camera/managedCameraController.tsx +401 -0
  64. package/src/global.d.ts +10 -0
  65. package/src/index.ts +3 -0
  66. package/src/landscape.tsx +321 -0
  67. package/src/player/player.tsx +257 -0
  68. package/src/player/playerstore.tsx +733 -0
  69. package/src/renderStateProvider.tsx +121 -0
  70. package/src/renderer.tsx +294 -0
  71. package/src/scene.tsx +42 -0
  72. package/src/shaders/fragment/landscapeFragmentShader.d.ts +4 -0
  73. package/src/shaders/fragment/landscapeFragmentShader.js +141 -0
  74. package/src/shaders/landscapeShader.tsx +39 -0
  75. package/src/shaders/vertex/landscapeVertexShader.d.ts +4 -0
  76. package/src/shaders/vertex/landscapeVertexShader.js +67 -0
  77. package/src/styles/renderer.module.css +90 -0
  78. package/src/worldSpaceCompositor.ts +265 -0
  79. package/src/xr/rendererXrBridge.ts +44 -0
@@ -0,0 +1,74 @@
1
+ import { useEffect } from "react";
2
+ import { useThree } from "@react-three/fiber";
3
+ import { Stats } from "@react-three/drei";
4
+ import { RenderStore } from "./renderStateProvider.js";
5
+
6
+ enum Perf {
7
+ Low,
8
+ Medium,
9
+ High,
10
+ Ultra,
11
+ }
12
+
13
+ export function AdaptiveDPR() {
14
+ const { gl } = useThree();
15
+ const dispatch = RenderStore.useDispatch();
16
+ useEffect(() => {
17
+ let lastTime = performance.now();
18
+ let frames = 0;
19
+ let currentPerf: Perf = Perf.Medium;
20
+ let currentAnimationHandler = 0;
21
+
22
+ const SAMPLE_WINDOW = 15;
23
+ const frameHistory: number[] = [];
24
+
25
+ const checkPerformance = () => {
26
+ const now = performance.now();
27
+ frames++;
28
+ if (now - lastTime >= 1000) {
29
+ dispatch({ type: "set_frame_rate", payload: frames });
30
+
31
+ frameHistory.push(frames);
32
+ if (frameHistory.length > SAMPLE_WINDOW) {
33
+ frameHistory.shift();
34
+ }
35
+
36
+ const avgFPS =
37
+ frameHistory.reduce((a, b) => a + b, 0) / frameHistory.length;
38
+
39
+ if (avgFPS < 30 && currentPerf !== Perf.Low) {
40
+ currentPerf = Perf.Low;
41
+ dispatch({ type: "set_performance_tier", payload: "low" });
42
+ gl.setPixelRatio(1);
43
+ } else if (avgFPS >= 40 && avgFPS < 80 && currentPerf !== Perf.Medium) {
44
+ currentPerf = Perf.Medium;
45
+ dispatch({ type: "set_performance_tier", payload: "medium" });
46
+
47
+ gl.setPixelRatio(2);
48
+ } else if (avgFPS >= 70 && avgFPS < 120 && currentPerf !== Perf.High) {
49
+ currentPerf = Perf.High;
50
+ dispatch({ type: "set_performance_tier", payload: "high" });
51
+
52
+ gl.setPixelRatio(3);
53
+ } else if (avgFPS >= 120 && currentPerf !== Perf.Ultra) {
54
+ currentPerf = Perf.Ultra;
55
+ dispatch({ type: "set_performance_tier", payload: "ultra" });
56
+
57
+ gl.setPixelRatio(4);
58
+ }
59
+
60
+ frames = 0;
61
+ lastTime = now;
62
+ }
63
+ currentAnimationHandler = requestAnimationFrame(checkPerformance);
64
+ };
65
+
66
+ checkPerformance();
67
+ return () => {
68
+ cancelAnimationFrame(currentAnimationHandler);
69
+ dispatch({ type: "set_frame_rate", payload: 0 });
70
+ };
71
+ }, [gl, dispatch]);
72
+
73
+ return <Stats className="performanceContainer" />;
74
+ }
@@ -0,0 +1,29 @@
1
+ export interface CameraRigProfile {
2
+ orbitSpeed: number;
3
+ panSpeed: number;
4
+ dollySpeed: number;
5
+ minDistance: number;
6
+ maxDistance: number;
7
+ minPolarAngle: number;
8
+ maxPolarAngle: number;
9
+ }
10
+
11
+ export const VIEW_PROFILE: CameraRigProfile = {
12
+ orbitSpeed: 0.0085,
13
+ panSpeed: 0.0018,
14
+ dollySpeed: 0.0012,
15
+ minDistance: 1.0,
16
+ maxDistance: 200,
17
+ minPolarAngle: (12 * Math.PI) / 180,
18
+ maxPolarAngle: (150 * Math.PI) / 180,
19
+ };
20
+
21
+ export const EDIT_PROFILE: CameraRigProfile = {
22
+ orbitSpeed: 0.01,
23
+ panSpeed: 0.0022,
24
+ dollySpeed: 0.0012,
25
+ minDistance: 5.0,
26
+ maxDistance: 200,
27
+ minPolarAngle: 0,
28
+ maxPolarAngle: (15 * Math.PI) / 32,
29
+ };
@@ -0,0 +1,401 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { useFrame, useThree } from "@react-three/fiber";
3
+ import {
4
+ createCameraManager,
5
+ type CameraDefinition,
6
+ type CameraManager,
7
+ type CameraState,
8
+ type Vec3,
9
+ } from "@plasius/gpu-camera";
10
+ import { Vector3 } from "three";
11
+ import type { CameraRigProfile } from "./cameraRigProfile.js";
12
+
13
+ type ProjectionCamera = {
14
+ fov?: number;
15
+ near?: number;
16
+ far?: number;
17
+ aspect?: number;
18
+ left?: number;
19
+ right?: number;
20
+ top?: number;
21
+ bottom?: number;
22
+ isPerspectiveCamera?: boolean;
23
+ isOrthographicCamera?: boolean;
24
+ updateProjectionMatrix?: () => void;
25
+ };
26
+
27
+ type ManagedThreeCamera = ProjectionCamera & {
28
+ position: { x: number; y: number; z: number; set: (x: number, y: number, z: number) => void };
29
+ up: { x: number; y: number; z: number; set: (x: number, y: number, z: number) => void };
30
+ lookAt: (x: number, y: number, z: number) => void;
31
+ getWorldDirection?: (target: Vector3) => Vector3;
32
+ };
33
+
34
+ type PointerMode = "orbit" | "pan" | null;
35
+
36
+ function clamp(value: number, min: number, max: number) {
37
+ return Math.min(max, Math.max(min, value));
38
+ }
39
+
40
+ function length3(value: Vec3) {
41
+ return Math.hypot(value[0], value[1], value[2]);
42
+ }
43
+
44
+ function normalize3(value: Vec3, fallback: Vec3): Vec3 {
45
+ const len = length3(value);
46
+ if (len <= Number.EPSILON) {
47
+ return [...fallback];
48
+ }
49
+ return [value[0] / len, value[1] / len, value[2] / len];
50
+ }
51
+
52
+ function sub3(a: Vec3, b: Vec3): Vec3 {
53
+ return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
54
+ }
55
+
56
+ function add3(a: Vec3, b: Vec3): Vec3 {
57
+ return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
58
+ }
59
+
60
+ function scale3(value: Vec3, scalar: number): Vec3 {
61
+ return [value[0] * scalar, value[1] * scalar, value[2] * scalar];
62
+ }
63
+
64
+ function cross3(a: Vec3, b: Vec3): Vec3 {
65
+ return [
66
+ a[1] * b[2] - a[2] * b[1],
67
+ a[2] * b[0] - a[0] * b[2],
68
+ a[0] * b[1] - a[1] * b[0],
69
+ ];
70
+ }
71
+
72
+ export function resolveCameraAspect(width: number, height: number): number {
73
+ if (!Number.isFinite(width) || !Number.isFinite(height) || height <= 0) {
74
+ return 1;
75
+ }
76
+ return Math.max(1 / 4096, width / height);
77
+ }
78
+
79
+ export function createRendererCameraManager(options?: {
80
+ maxParallelViews?: number;
81
+ maxHotCameras?: number;
82
+ }): CameraManager {
83
+ return createCameraManager({
84
+ maxParallelViews: options?.maxParallelViews ?? 2,
85
+ maxHotCameras: options?.maxHotCameras ?? 3,
86
+ });
87
+ }
88
+
89
+ export function derivePanDeltaFromCameraState(
90
+ cameraState: CameraState,
91
+ deltaX: number,
92
+ deltaY: number,
93
+ panSpeed: number
94
+ ): Vec3 {
95
+ const position = cameraState.transform.position;
96
+ const target = cameraState.transform.target;
97
+ const up = normalize3(cameraState.transform.up ?? [0, 1, 0], [0, 1, 0]);
98
+
99
+ const forward = normalize3(sub3(target, position), [0, 0, -1]);
100
+ const right = normalize3(cross3(forward, up), [1, 0, 0]);
101
+ const radius = Math.max(0.01, length3(sub3(position, target)));
102
+ const scale = Math.max(0.00001, panSpeed * radius);
103
+
104
+ const horizontal = scale3(right, -deltaX * scale);
105
+ const vertical = scale3(up, deltaY * scale);
106
+ return add3(horizontal, vertical);
107
+ }
108
+
109
+ export function buildCameraDefinitionFromThreeCamera(
110
+ camera: ManagedThreeCamera,
111
+ aspect: number
112
+ ): CameraDefinition {
113
+ const position: Vec3 = [camera.position.x, camera.position.y, camera.position.z];
114
+ const up: Vec3 = [camera.up.x, camera.up.y, camera.up.z];
115
+ const worldDirection = new Vector3(0, 0, -1);
116
+ camera.getWorldDirection?.(worldDirection);
117
+ const target: Vec3 = [
118
+ position[0] + worldDirection.x,
119
+ position[1] + worldDirection.y,
120
+ position[2] + worldDirection.z,
121
+ ];
122
+
123
+ if (camera.isOrthographicCamera) {
124
+ return {
125
+ transform: { position, target, up },
126
+ projection: {
127
+ kind: "orthographic",
128
+ left: camera.left ?? -1,
129
+ right: camera.right ?? 1,
130
+ top: camera.top ?? 1,
131
+ bottom: camera.bottom ?? -1,
132
+ near: camera.near ?? 0.1,
133
+ far: camera.far ?? 2000,
134
+ aspect,
135
+ },
136
+ };
137
+ }
138
+
139
+ return {
140
+ transform: { position, target, up },
141
+ projection: {
142
+ kind: "perspective",
143
+ fovY: camera.fov ?? 50,
144
+ near: camera.near ?? 0.1,
145
+ far: camera.far ?? 2000,
146
+ aspect,
147
+ },
148
+ };
149
+ }
150
+
151
+ export function syncThreeCameraFromManagedState(
152
+ camera: ManagedThreeCamera,
153
+ cameraState: CameraState,
154
+ aspect: number
155
+ ) {
156
+ const transform = cameraState.transform;
157
+ const up = transform.up ?? [0, 1, 0];
158
+ camera.position.set(
159
+ transform.position[0],
160
+ transform.position[1],
161
+ transform.position[2]
162
+ );
163
+ camera.up.set(up[0], up[1], up[2]);
164
+ camera.lookAt(
165
+ transform.target[0],
166
+ transform.target[1],
167
+ transform.target[2]
168
+ );
169
+
170
+ const projection = cameraState.projection;
171
+ if (projection.kind === "perspective" && camera.isPerspectiveCamera) {
172
+ camera.fov = projection.fovY;
173
+ camera.near = projection.near;
174
+ camera.far = projection.far;
175
+ camera.aspect = aspect;
176
+ camera.updateProjectionMatrix?.();
177
+ return;
178
+ }
179
+
180
+ if (projection.kind === "orthographic" && camera.isOrthographicCamera) {
181
+ const sourceAspect = projection.aspect || 1;
182
+ const aspectScale = sourceAspect > 0 ? aspect / sourceAspect : 1;
183
+ camera.left = projection.left * aspectScale;
184
+ camera.right = projection.right * aspectScale;
185
+ camera.top = projection.top;
186
+ camera.bottom = projection.bottom;
187
+ camera.near = projection.near;
188
+ camera.far = projection.far;
189
+ camera.updateProjectionMatrix?.();
190
+ }
191
+ }
192
+
193
+ export interface ManagedCameraControllerProps {
194
+ manager: CameraManager;
195
+ profile: CameraRigProfile;
196
+ cameraId?: string;
197
+ enabled?: boolean;
198
+ }
199
+
200
+ export function ManagedCameraController({
201
+ manager,
202
+ profile,
203
+ cameraId = "main",
204
+ enabled = true,
205
+ }: ManagedCameraControllerProps) {
206
+ const { camera, gl, size } = useThree();
207
+ const pointerRef = useRef<{
208
+ active: boolean;
209
+ mode: PointerMode;
210
+ x: number;
211
+ y: number;
212
+ }>({
213
+ active: false,
214
+ mode: null,
215
+ x: 0,
216
+ y: 0,
217
+ });
218
+
219
+ const managedThreeCamera = camera as unknown as ManagedThreeCamera;
220
+ const aspect = resolveCameraAspect(size.width, size.height);
221
+
222
+ useEffect(() => {
223
+ const cameraDefinition = buildCameraDefinitionFromThreeCamera(
224
+ managedThreeCamera,
225
+ aspect
226
+ );
227
+
228
+ if (manager.hasCamera(cameraId)) {
229
+ manager.updateCamera(cameraId, cameraDefinition);
230
+ } else {
231
+ manager.registerCamera({
232
+ id: cameraId,
233
+ priority: 100,
234
+ ...cameraDefinition,
235
+ });
236
+ }
237
+
238
+ manager.activateCamera(cameraId);
239
+ }, [manager, cameraId, managedThreeCamera, aspect]);
240
+
241
+ useEffect(() => {
242
+ const registered = manager.getCamera(cameraId);
243
+ if (!registered) {
244
+ return;
245
+ }
246
+ if (registered.projection.kind === "perspective") {
247
+ manager.updateCamera(cameraId, {
248
+ projection: {
249
+ ...registered.projection,
250
+ aspect,
251
+ },
252
+ });
253
+ return;
254
+ }
255
+ manager.updateCamera(cameraId, {
256
+ projection: {
257
+ ...registered.projection,
258
+ aspect,
259
+ },
260
+ });
261
+ }, [manager, cameraId, aspect]);
262
+
263
+ useEffect(() => {
264
+ if (!enabled) {
265
+ pointerRef.current.active = false;
266
+ pointerRef.current.mode = null;
267
+ return;
268
+ }
269
+
270
+ const element = gl.domElement;
271
+ if (!element) {
272
+ return;
273
+ }
274
+
275
+ const onPointerDown = (event: PointerEvent) => {
276
+ if (event.button === 0 && !event.shiftKey) {
277
+ pointerRef.current.mode = "orbit";
278
+ } else if (event.button === 2 || event.button === 1 || event.shiftKey) {
279
+ pointerRef.current.mode = "pan";
280
+ } else {
281
+ pointerRef.current.mode = null;
282
+ }
283
+
284
+ if (!pointerRef.current.mode) {
285
+ return;
286
+ }
287
+
288
+ pointerRef.current.active = true;
289
+ pointerRef.current.x = event.clientX;
290
+ pointerRef.current.y = event.clientY;
291
+ element.setPointerCapture?.(event.pointerId);
292
+ };
293
+
294
+ const onPointerMove = (event: PointerEvent) => {
295
+ if (!pointerRef.current.active || !pointerRef.current.mode) {
296
+ return;
297
+ }
298
+
299
+ const deltaX = event.clientX - pointerRef.current.x;
300
+ const deltaY = event.clientY - pointerRef.current.y;
301
+ pointerRef.current.x = event.clientX;
302
+ pointerRef.current.y = event.clientY;
303
+
304
+ if (pointerRef.current.mode === "orbit") {
305
+ manager.applyControl(
306
+ cameraId,
307
+ {
308
+ type: "orbit",
309
+ deltaAzimuth: -deltaX * profile.orbitSpeed,
310
+ deltaPolar: -deltaY * profile.orbitSpeed,
311
+ },
312
+ {
313
+ minDistance: profile.minDistance,
314
+ maxDistance: profile.maxDistance,
315
+ minPolarAngle: profile.minPolarAngle,
316
+ maxPolarAngle: profile.maxPolarAngle,
317
+ makeActive: true,
318
+ }
319
+ );
320
+ return;
321
+ }
322
+
323
+ const activeCamera = manager.getCamera(cameraId);
324
+ if (!activeCamera) {
325
+ return;
326
+ }
327
+ const delta = derivePanDeltaFromCameraState(
328
+ activeCamera,
329
+ deltaX,
330
+ deltaY,
331
+ profile.panSpeed
332
+ );
333
+ manager.applyControl(
334
+ cameraId,
335
+ {
336
+ type: "pan",
337
+ delta,
338
+ },
339
+ { makeActive: true }
340
+ );
341
+ };
342
+
343
+ const onPointerUp = (event: PointerEvent) => {
344
+ pointerRef.current.active = false;
345
+ pointerRef.current.mode = null;
346
+ element.releasePointerCapture?.(event.pointerId);
347
+ };
348
+
349
+ const onWheel = (event: WheelEvent) => {
350
+ event.preventDefault();
351
+ const activeCamera = manager.getCamera(cameraId);
352
+ if (!activeCamera) {
353
+ return;
354
+ }
355
+ const distanceToTarget = Math.max(
356
+ 0.01,
357
+ length3(sub3(activeCamera.transform.position, activeCamera.transform.target))
358
+ );
359
+ const step = -event.deltaY * profile.dollySpeed * Math.max(0.05, distanceToTarget * 0.2);
360
+ manager.applyControl(
361
+ cameraId,
362
+ { type: "dolly", distance: step },
363
+ {
364
+ minDistance: profile.minDistance,
365
+ maxDistance: profile.maxDistance,
366
+ makeActive: true,
367
+ }
368
+ );
369
+ };
370
+
371
+ const onContextMenu = (event: MouseEvent) => {
372
+ event.preventDefault();
373
+ };
374
+
375
+ element.addEventListener("pointerdown", onPointerDown);
376
+ window.addEventListener("pointermove", onPointerMove);
377
+ window.addEventListener("pointerup", onPointerUp);
378
+ element.addEventListener("wheel", onWheel, { passive: false });
379
+ element.addEventListener("contextmenu", onContextMenu);
380
+
381
+ return () => {
382
+ element.removeEventListener("pointerdown", onPointerDown);
383
+ window.removeEventListener("pointermove", onPointerMove);
384
+ window.removeEventListener("pointerup", onPointerUp);
385
+ element.removeEventListener("wheel", onWheel);
386
+ element.removeEventListener("contextmenu", onContextMenu);
387
+ };
388
+ }, [manager, cameraId, profile, gl, enabled]);
389
+
390
+ useFrame(() => {
391
+ const snapshot = manager.getSnapshot();
392
+ const activeId = snapshot.activeCameraId ?? cameraId;
393
+ const activeCamera = manager.getCamera(activeId);
394
+ if (!activeCamera) {
395
+ return;
396
+ }
397
+ syncThreeCameraFromManagedState(managedThreeCamera, activeCamera, aspect);
398
+ });
399
+
400
+ return null;
401
+ }
@@ -0,0 +1,10 @@
1
+ // global.d.ts
2
+ declare module "*.module.css" {
3
+ const classes: { [key: string]: string };
4
+ export default classes;
5
+ }
6
+
7
+ declare module "*.module.scss" {
8
+ const classes: { [key: string]: string };
9
+ export default classes;
10
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ import "./shaders/landscapeShader.js";
2
+ export * from "./renderer.js";
3
+ export * from "./worldSpaceCompositor.js";