Needle Engine

Changes between version 3.5.12-beta and 3.5.13-beta
Files changed (6) hide show
  1. src/engine-components/CameraUtils.ts +2 -1
  2. src/engine/debug/debug.ts +3 -0
  3. src/engine/engine_networking_instantiate.ts +1 -1
  4. src/engine/engine_utils.ts +6 -0
  5. src/engine-components/OrbitControls.ts +55 -12
  6. src/engine-components/Skybox.ts +30 -6
src/engine-components/CameraUtils.ts CHANGED
@@ -20,6 +20,7 @@
20
20
  cam.sourceId = srcId;
21
21
  cam.clearFlags = 2;
22
22
  cam.backgroundColor = new RGBAColor(0.5, 0.5, 0.5, 1);
23
+ cam.fieldOfView = 35;
23
24
 
24
25
  cameraObject.position.x = 0;
25
26
  cameraObject.position.y = 1;
@@ -48,7 +49,7 @@
48
49
  const orbit = addNewComponent(cameraObject, new OrbitControls(), false) as OrbitControls;
49
50
  orbit.sourceId = "unknown";
50
51
  setTimeout(() => {
51
- orbit.fitCameraToObjects(evt.context.scene.children, 1);
52
+ orbit.fitCameraToObjects(evt.context.scene.children);
52
53
  }, 100);
53
54
  }
54
55
  else {
src/engine/debug/debug.ts CHANGED
@@ -5,14 +5,17 @@
5
5
  export { showDebugConsole }
6
6
  export { LogType };
7
7
 
8
+ /** Displays a debug message on screen for a certain amount of time */
8
9
  export function showBalloonMessage(text: string, logType: LogType = LogType.Log) {
9
10
  addLog(logType, text);
10
11
  }
11
12
 
13
+ /** Displays a warning message on screen for a certain amount of time */
12
14
  export function showBalloonWarning(text: string) {
13
15
  showBalloonMessage(text, LogType.Warn);
14
16
  }
15
17
 
18
+ /** True when the application runs on a local url */
16
19
  export function isDevEnvironment(){
17
20
  return isLocalNetwork();
18
21
  }
src/engine/engine_networking_instantiate.ts CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  // https://github.com/uuidjs/uuid
9
9
  // v5 takes string and namespace
10
- import { v5, v1 } from 'uuid';
10
+ import { v5 } from 'uuid';
11
11
  import { UIDProvider } from "./engine_types";
12
12
  import { IModel } from "./engine_networking_types";
13
13
  import { SendQueue } from "./engine_networking_types";
src/engine/engine_utils.ts CHANGED
@@ -49,6 +49,12 @@
49
49
  return new URLSearchParams(window.location.search);
50
50
  }
51
51
 
52
+ /** Checks if a url parameter exists.
53
+ * Returns true if it exists but has no value (e.g. ?help)
54
+ * Returns false if it does not exist
55
+ * Returns false if it's set to 0 e.g. ?debug=0
56
+ * Returns the value if it exists e.g. ?message=hello
57
+ */
52
58
  export function getParam(paramName: string): string | boolean | number {
53
59
 
54
60
  if (saveParams && !requestedParams.includes(paramName))
src/engine-components/OrbitControls.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  import { Behaviour, GameObject } from "./Component";
2
2
  import { Camera } from "./Camera";
3
3
  import { LookAtConstraint } from "./LookAtConstraint";
4
- import { getWorldPosition, slerp } from "../engine/engine_three_utils";
4
+ import { getWorldPosition, getWorldRotation, setWorldPosition, setWorldRotation, slerp } from "../engine/engine_three_utils";
5
5
  import { RaycastOptions } from "../engine/engine_physics";
6
6
  import { serializable } from "../engine/engine_serialization_decorator";
7
7
  import { getParam, isMobileDevice } from "../engine/engine_utils";
8
8
 
9
- import { Camera as ThreeCamera, Box3, Object3D, PerspectiveCamera, Vector2, Vector3 } from "three";
9
+ import { Camera as ThreeCamera, Box3, Object3D, PerspectiveCamera, Vector2, Vector3, Box3Helper } from "three";
10
10
  import { OrbitControls as ThreeOrbitControls } from "three/examples/jsm/controls/OrbitControls";
11
11
  import { AfterHandleInputEvent, EventSystem, EventSystemEvents } from "./ui/EventSystem";
12
12
  import { ICameraController } from "../engine/engine_types";
@@ -14,6 +14,7 @@
14
14
  import { SyncedTransform } from "./SyncedTransform";
15
15
 
16
16
  const freeCam = getParam("freecam");
17
+ const debugCameraFit = getParam("debugcamerafit");
17
18
 
18
19
  const disabledKeys = { LEFT: "", UP: "", RIGHT: "", BOTTOM: "" };
19
20
  let defaultKeys: any = undefined;
@@ -351,7 +352,7 @@
351
352
 
352
353
  // Adapted from https://discourse.threejs.org/t/camera-zoom-to-fit-object/936/24
353
354
  // Slower but better implementation that takes bones and exact vertex positions into account: https://github.com/google/model-viewer/blob/04e900c5027de8c5306fe1fe9627707f42811b05/packages/model-viewer/src/three-components/ModelScene.ts#L321
354
- fitCameraToObjects(objects: Array<Object3D>, fitOffset: number = 1.5) {
355
+ fitCameraToObjects(objects: Array<Object3D>, fitOffset: number = 1.1) {
355
356
  const camera = this._cameraObject as PerspectiveCamera;
356
357
  const controls = this._controls as ThreeOrbitControls | null;
357
358
 
@@ -361,20 +362,43 @@
361
362
  const center = new Vector3();
362
363
  const box = new Box3();
363
364
 
365
+ // TODO would be much better to calculate the bounds in camera space instead of world space -
366
+ // we would get proper view-dependant fit.
367
+ // Right now it's independent from where the camera is actually looking from,
368
+ // and thus we're just getting some maximum that will work for sure.
369
+
364
370
  box.makeEmpty();
365
- for (const object of objects)
366
- box.expandByObject(object);
371
+ for (const object of objects) {
372
+ // ignore Box3Helpers
373
+ if (object instanceof Box3Helper) continue;
374
+ box.expandByObject(object, true);
375
+ }
367
376
 
368
- box.getSize(size);
377
+ camera.updateMatrixWorld();
378
+ camera.updateProjectionMatrix();
379
+
369
380
  box.getCenter(center);
370
381
 
371
- const maxSize = Math.max(size.x, size.y, size.z);
372
- const fitHeightDistance = maxSize / (2 * Math.atan(Math.PI * camera.fov / 360));
373
- const fitWidthDistance = fitHeightDistance / camera.aspect;
374
- const distance = fitOffset * Math.max(fitHeightDistance, fitWidthDistance);
382
+ // project this box into camera space
383
+ box.applyMatrix4(camera.matrixWorldInverse);
375
384
 
385
+ box.getSize(size);
386
+ box.setFromCenterAndSize(center, size);
387
+
388
+ const verticalFov = camera.fov;
389
+ const horizontalFov = 2 * Math.atan(Math.tan(verticalFov * Math.PI / 360 / 2) * camera.aspect) / Math.PI * 360;
390
+ const fitHeightDistance = size.y / (2 * Math.atan(Math.PI * verticalFov / 360));
391
+ const fitWidthDistance = size.x / (2 * Math.atan(Math.PI * horizontalFov / 360));
392
+
393
+ const distance = fitOffset * Math.max(fitHeightDistance, fitWidthDistance) + size.z / 2;
394
+
395
+ if (debugCameraFit) {
396
+ console.log("Fit camera to objects", fitHeightDistance, fitWidthDistance, "distance", distance);
397
+ }
398
+
399
+ const cameraWp = getWorldPosition(camera);
376
400
  const direction = controls.target.clone()
377
- .sub(camera.position)
401
+ .sub(cameraWp)
378
402
  .normalize()
379
403
  .multiplyScalar(distance);
380
404
 
@@ -384,13 +408,32 @@
384
408
 
385
409
  camera.near = distance / 100;
386
410
  camera.far = distance * 100;
411
+
412
+ camera.updateMatrixWorld();
387
413
  camera.updateProjectionMatrix();
388
414
 
389
- camera.position.copy(controls.target).sub(direction);
415
+ setWorldPosition(camera, controls.target.clone().sub(direction));
390
416
 
417
+ if (debugCameraFit) {
418
+ const helper = new Box3Helper(box);
419
+ this.context.scene.add(helper);
420
+ setWorldRotation(helper, getWorldRotation(camera));
421
+
422
+ if (!this._haveAttachedKeyboardEvents) {
423
+ this._haveAttachedKeyboardEvents = true;
424
+ document.body.addEventListener("keydown", (e) => {
425
+ if (e.code === "KeyF") {
426
+ this.fitCameraToObjects(objects);
427
+ }
428
+ });
429
+ }
430
+ }
431
+
391
432
  controls.update();
392
433
  }
393
434
 
435
+ private _haveAttachedKeyboardEvents: boolean = false;
436
+
394
437
  // private onPositionDrag(){
395
438
 
396
439
  // }
src/engine-components/Skybox.ts CHANGED
@@ -5,10 +5,34 @@
5
5
  import { EquirectangularRefractionMapping, sRGBEncoding, Texture, TextureLoader } from "three"
6
6
  import { syncField } from "../engine/engine_networking_auto";
7
7
  import { Camera } from "./Camera";
8
- import { getParam, resolveUrl } from "../engine/engine_utils";
8
+ import { getParam } from "../engine/engine_utils";
9
+ import { ContextRegistry } from "../engine/engine_context_registry";
9
10
 
10
11
  const debug = getParam("debugskybox");
11
12
 
13
+ ContextRegistry.addContextCreatedCallback((args) => {
14
+ const context = args.context;
15
+ const skyboxImage = context.domElement.getAttribute("skybox-image");
16
+ const environmentImage = context.domElement.getAttribute("environment-image");
17
+ if (skyboxImage) {
18
+ if (debug) console.log("Creating remote skybox to load " + skyboxImage);
19
+ const remote = new RemoteSkybox();
20
+ remote.url = skyboxImage;
21
+ remote.allowDrop = false;
22
+ remote.environment = false;
23
+ remote.background = true;
24
+ GameObject.addComponent(context.scene, remote);
25
+ }
26
+ if (environmentImage) {
27
+ const remote = new RemoteSkybox();
28
+ remote.url = environmentImage;
29
+ remote.allowDrop = false;
30
+ remote.environment = true;
31
+ remote.background = false;
32
+ GameObject.addComponent(context.scene, remote);
33
+ }
34
+ });
35
+
12
36
  export class RemoteSkybox extends Behaviour {
13
37
 
14
38
  @syncField("setSkybox")
@@ -53,7 +77,7 @@
53
77
  console.warn("Potentially invalid skybox url", this.url, "on", this.name);
54
78
  }
55
79
 
56
- if(debug) console.log("Remote skybox url?: " + url);
80
+ if (debug) console.log("Remote skybox url?: " + url);
57
81
 
58
82
  if (this._prevUrl === url && this._prevLoadedEnvironment) {
59
83
  this.applySkybox();
@@ -81,7 +105,7 @@
81
105
  this._loader = new TextureLoader();
82
106
  }
83
107
 
84
- if(debug) console.log("Loading skybox: " + url);
108
+ if (debug) console.log("Loading skybox: " + url);
85
109
  const envMap = await this._loader.loadAsync(url);
86
110
  if (!envMap) return;
87
111
  // Check if we're still enabled
@@ -110,7 +134,7 @@
110
134
  if (this.context.scene.environment !== envMap)
111
135
  this._prevEnvironment = this.context.scene.environment;
112
136
 
113
- if(debug) console.log("Set remote skybox", this.url);
137
+ if (debug) console.log("Set remote skybox", this.url);
114
138
  if (this.environment)
115
139
  this.context.scene.environment = envMap;
116
140
  if (this.background && !Camera.backgroundShouldBeTransparent(this.context))
@@ -147,12 +171,12 @@
147
171
  for (const type of e.dataTransfer.types) {
148
172
  if (type === "text/uri-list") {
149
173
  const url = e.dataTransfer.getData(type);
150
- if(debug) console.log(type, url);
174
+ if (debug) console.log(type, url);
151
175
  let name = new RegExp(/polyhaven.com\/asset_img\/.+?\/(?<name>.+)\.png/).exec(url)?.groups?.name;
152
176
  if (!name) {
153
177
  name = new RegExp(/polyhaven\.com\/a\/(?<name>.+)/).exec(url)?.groups?.name;
154
178
  }
155
- if(debug) console.log(name);
179
+ if (debug) console.log(name);
156
180
  if (name) {
157
181
  const envurl = "https://dl.polyhaven.org/file/ph-assets/HDRIs/exr/1k/" + name + "_1k.exr";
158
182
  this.setSkybox(envurl);