Needle Engine

Changes between version 3.47.1-beta and 3.47.2-beta
Files changed (7) hide show
  1. src/engine-components/Animation.ts +2 -2
  2. src/engine/webcomponents/buttons.ts +6 -1
  3. src/engine-components/Collider.ts +17 -1
  4. src/engine/engine_create_objects.ts +207 -15
  5. src/engine/engine_physics_rapier.ts +1 -1
  6. src/engine/engine_types.ts +1 -0
  7. src/engine-components/ShadowCatcher.ts +2 -0
src/engine-components/Animation.ts CHANGED
@@ -433,8 +433,8 @@
433
433
  const clip = action.getClip();
434
434
  action.time = Mathf.lerp(options.minMaxOffsetNormalized.x, options.minMaxOffsetNormalized.y, Math.random()) * clip.duration;
435
435
  }
436
- // If the animation is not running and the time is at the end, reset the time
437
- else if(!action.isRunning() && action.time >= action.getClip().duration) {
436
+ // If the animation is at the end, reset the time
437
+ else if(action.time >= action.getClip().duration) {
438
438
  action.time = 0;
439
439
  }
440
440
 
src/engine/webcomponents/buttons.ts CHANGED
@@ -210,7 +210,12 @@
210
210
  window.addEventListener("resize", hideQRCode);
211
211
  window.addEventListener("scroll", hideQRCode);
212
212
 
213
- document.body.appendChild(qrCodeContainer);
213
+ // if we're in fullscreen:
214
+ if (document.fullscreenElement) {
215
+ document.fullscreenElement.appendChild(qrCodeContainer);
216
+ }
217
+ else
218
+ document.body.appendChild(qrCodeContainer);
214
219
  }
215
220
 
216
221
  /** hides to QRCode overlay and unsubscribes from events */
src/engine-components/Collider.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { BufferGeometry, Group, Mesh, Vector3 } from "three"
1
+ import { BufferGeometry, Group, Mesh, Object3D, Vector3 } from "three"
2
2
 
3
3
  import type { PhysicsMaterial } from "../engine/engine_physics.types.js";
4
4
  import { serializable } from "../engine/engine_serialization_decorator.js";
@@ -10,6 +10,7 @@
10
10
  import { NEEDLE_progressive } from "../engine/extensions/NEEDLE_progressive.js";
11
11
  import { Behaviour } from "./Component.js";
12
12
  import { Rigidbody } from "./RigidBody.js";
13
+ import { addComponent } from "../engine/engine_components.js";
13
14
 
14
15
  /**
15
16
  * Collider is the base class for all colliders. A collider is a physical shape that is used to detect collisions with other objects in the scene.
@@ -130,6 +131,21 @@
130
131
  */
131
132
  export class BoxCollider extends Collider implements IBoxCollider {
132
133
 
134
+ static add(obj: Mesh, opts?: { rigidbody: boolean }) {
135
+ const collider = new BoxCollider();
136
+
137
+ if (!obj.geometry.boundingBox) obj.geometry.computeBoundingBox();
138
+ const bb = obj.geometry.boundingBox!;
139
+ collider.size = bb!.getSize(new Vector3()) || new Vector3(1, 1, 1);
140
+ collider.center = bb!.getCenter(new Vector3()) || new Vector3(0, 0, 0);
141
+ addComponent(obj, collider);
142
+
143
+ if (opts?.rigidbody === true) {
144
+ addComponent(obj, Rigidbody, { isKinematic: false });
145
+ }
146
+ return collider;
147
+ }
148
+
133
149
  @validate()
134
150
  @serializable(Vector3)
135
151
  size: Vector3 = new Vector3(1, 1, 1);
src/engine/engine_create_objects.ts CHANGED
@@ -1,11 +1,20 @@
1
- import { BoxGeometry, DoubleSide, Material, Mesh, MeshStandardMaterial, Object3D, PlaneGeometry, SphereGeometry, Sprite, SpriteMaterial, Texture } from "three"
1
+ import { BoxGeometry, BufferGeometry, ColorRepresentation, DoubleSide, ExtrudeGeometry, Material, Mesh, MeshStandardMaterial, Object3D, PlaneGeometry, Shape, SphereGeometry, Sprite, SpriteMaterial, Texture } from "three"
2
+ import { TextGeometry } from "three/examples/jsm/geometries/TextGeometry.js";
2
3
 
3
4
  import type { Vec3 } from "./engine_types.js";
5
+ import { Font, FontLoader } from "three/examples/jsm/loaders/FontLoader.js";
4
6
 
5
7
  export enum PrimitiveType {
8
+ /**
9
+ * A quad with a width and height of 1 facing the positive Z axis
10
+ */
6
11
  Quad = 0,
12
+ /**
13
+ * A cube with a width, height, and depth of 1
14
+ */
7
15
  Cube = 1,
8
16
  Sphere = 2,
17
+ RoundedCube = 10,
9
18
  }
10
19
  export type PrimitiveTypeNames = keyof typeof PrimitiveType;
11
20
 
@@ -13,18 +22,99 @@
13
22
  * Options to create an object. Used by {@link ObjectUtils.createPrimitive}
14
23
  */
15
24
  export type ObjectOptions = {
25
+ /**
26
+ * The parent object to add the created object to
27
+ */
28
+ parent?: Object3D,
29
+ /**
30
+ * The name of the object
31
+ */
16
32
  name?: string,
17
- /** Optional: The material to apply to the object */
33
+ /** The material to apply to the object */
18
34
  material?: Material,
19
- /** Optional: The texture will applied to the material's main texture slot e.g. `material.map` if any is passed in */
35
+ /** The color of the object. This color will only be used if no material is provided */
36
+ color?: ColorRepresentation,
37
+ /** The texture will applied to the material's main texture slot e.g. `material.map` if any is passed in */
20
38
  texture?: Texture,
21
- position?: Vec3,
22
- /** euler */
23
- rotation?: Vec3,
24
- scale?: Vec3,
39
+ /**
40
+ * The position of the object in local space
41
+ */
42
+ position?: Vec3 | [number, number, number],
43
+ /** The rotation of the object in local space */
44
+ rotation?: Vec3 | [number, number, number],
45
+ /**
46
+ * The scale of the object in local space
47
+ */
48
+ scale?: Vec3 | number,
49
+ /**
50
+ * If the object should receive shadows
51
+ * @default true
52
+ */
53
+ receiveShadow?: boolean,
54
+ /**
55
+ * If the object should cast shadows
56
+ * @default true
57
+ */
58
+ castShadow?: boolean,
25
59
  }
26
60
 
27
61
  /**
62
+ * Options to create a 3D text object. Used by {@link ObjectUtils.createText}
63
+ */
64
+ export type TextOptions = Omit<ObjectOptions, "texture"> & {
65
+ /**
66
+ * Optional: The font to use for the text. If not provided, the default font will be used
67
+ */
68
+ font?: Font,
69
+ /**
70
+ * If the font is not provided, the familyFamily can be used to load a font from the default list
71
+ */
72
+ familyFamily?: "OpenSans" | "Helvetiker";// "Optimer" | "Gentilis" | "DroidSans"
73
+ /**
74
+ * Optional: The depth of the text.
75
+ * @default .1
76
+ */
77
+ depth?: number;
78
+ /**
79
+ * Optional: If the text should have a bevel effect
80
+ * @default false
81
+ */
82
+ bevel?: boolean;
83
+ /**
84
+ * Invoked when the font geometry is loaded
85
+ */
86
+ onGeometry?: (obj: Mesh) => void;
87
+ }
88
+
89
+ const fontsDict = new Map<string, Font | Promise<Font>>();
90
+ function loadFont(family: string | null): Font | Promise<Font> {
91
+ let url: string = "";
92
+ switch (family) {
93
+ default:
94
+ case "OpenSans":
95
+ url = "https://cdn.needle.tools/static/fonts/facetype/Open Sans_Regular_ascii.json";
96
+ break;
97
+ case "Helvetiker":
98
+ url = "https://raw.githubusercontent.com/mrdoob/three.js/master/examples/fonts/helvetiker_regular.typeface.json";
99
+ break;
100
+ }
101
+ if (fontsDict.has(url)) {
102
+ const res = fontsDict.get(url);
103
+ if (res) return res;
104
+ }
105
+ const loader = new FontLoader();
106
+ const promise = new Promise<Font>((resolve, reject) => {
107
+ loader.load(url, res => {
108
+ fontsDict.set(url, res);
109
+ resolve(res);
110
+ }, undefined, reject);
111
+ });
112
+ fontsDict.set(url, promise);
113
+ return promise;
114
+ }
115
+
116
+
117
+ /**
28
118
  * Utility class to create primitive objects
29
119
  * @example
30
120
  * ```typescript
@@ -34,6 +124,51 @@
34
124
  export class ObjectUtils {
35
125
 
36
126
  /**
127
+ * Creates a 3D text object
128
+ * @param text The text to display
129
+ * @param opts Options to create the object
130
+ */
131
+ static createText(text: string, opts?: TextOptions): Mesh {
132
+
133
+ let geometry: BufferGeometry | null = null;
134
+ let font: Font | Promise<Font> = opts?.font || loadFont(opts?.familyFamily || null);
135
+
136
+ if (font instanceof Font) {
137
+ geometry = this.#createTextGeometry(text, font, opts);
138
+ }
139
+ else if (geometry == null) {
140
+ geometry = new BufferGeometry();
141
+ }
142
+ const color = opts?.color || 0xffffff;
143
+ const mesh = new Mesh(geometry, opts?.material ?? new MeshStandardMaterial({ color: color }));
144
+ this.applyDefaultObjectOptions(mesh, opts);
145
+ if (font instanceof Promise) {
146
+ font.then(res => {
147
+ mesh.geometry = this.#createTextGeometry(text, res, opts);
148
+ if (opts?.onGeometry) opts.onGeometry(mesh);
149
+ });
150
+ }
151
+ else {
152
+ if (opts?.onGeometry) opts.onGeometry(mesh);
153
+ }
154
+ return mesh;
155
+ }
156
+ static #createTextGeometry(text: string, font: Font, opts?: TextOptions) {
157
+ const depth = opts?.depth || .1;
158
+ const geo = new TextGeometry(text, {
159
+ font,
160
+ size: 1,
161
+ depth: depth,
162
+ height: depth,
163
+ bevelEnabled: opts?.bevel || false,
164
+ bevelThickness: .01,
165
+ bevelOffset: .01,
166
+ bevelSize: .01,
167
+ });
168
+ return geo;
169
+ }
170
+
171
+ /**
37
172
  * Creates an occluder object that only render depth but not color
38
173
  * @param type The type of primitive to create
39
174
  * @returns The created object
@@ -50,7 +185,7 @@
50
185
  */
51
186
  static createPrimitive(type: PrimitiveType | PrimitiveTypeNames, opts?: ObjectOptions): Mesh {
52
187
  let obj: Mesh;
53
- const color = 0xffffff;
188
+ const color = opts?.color || 0xffffff;
54
189
  switch (type) {
55
190
  case "Quad":
56
191
  case PrimitiveType.Quad:
@@ -70,6 +205,15 @@
70
205
  obj = new Mesh(boxGeo, mat);
71
206
  }
72
207
  break;
208
+ case PrimitiveType.RoundedCube:
209
+ case "RoundedCube":
210
+ {
211
+ const boxGeo = createBoxWithRoundedEdges(1, 1, 1, .1, 2);
212
+ const mat = opts?.material ?? new MeshStandardMaterial({ color: color });
213
+ if (opts?.texture && "map" in mat) mat.map = opts.texture;
214
+ obj = new Mesh(boxGeo, mat);
215
+ }
216
+ break;
73
217
  case "Sphere":
74
218
  case PrimitiveType.Sphere:
75
219
  {
@@ -99,13 +243,61 @@
99
243
  }
100
244
 
101
245
  private static applyDefaultObjectOptions(obj: Object3D, opts?: ObjectOptions) {
246
+ obj.receiveShadow = true;
247
+ obj.castShadow = true;
248
+
102
249
  if (opts?.name)
103
250
  obj.name = opts.name;
104
- if (opts?.position)
105
- obj.position.set(opts.position.x, opts.position.y, opts.position.z);
106
- if (opts?.rotation)
107
- obj.rotation.set(opts.rotation.x, opts.rotation.y, opts.rotation.z);
108
- if (opts?.scale)
109
- obj.scale.set(opts.scale.x, opts.scale.y, opts.scale.z);
251
+ if (opts?.position) {
252
+ if (Array.isArray(opts.position))
253
+ obj.position.set(opts.position[0], opts.position[1], opts.position[2]);
254
+ else
255
+ obj.position.set(opts.position.x, opts.position.y, opts.position.z);
256
+ }
257
+ if (opts?.rotation) {
258
+ if (Array.isArray(opts.rotation))
259
+ obj.rotation.set(opts.rotation[0], opts.rotation[1], opts.rotation[2]);
260
+ else
261
+ obj.rotation.set(opts.rotation.x, opts.rotation.y, opts.rotation.z);
262
+ }
263
+ if (opts?.scale) {
264
+ if (typeof opts.scale === "number")
265
+ obj.scale.set(opts.scale, opts.scale, opts.scale);
266
+ else
267
+ obj.scale.set(opts.scale.x, opts.scale.y, opts.scale.z);
268
+ }
269
+
270
+ if (opts?.receiveShadow != undefined) {
271
+ obj.receiveShadow = opts.receiveShadow;
272
+ }
273
+ if (opts?.castShadow != undefined) {
274
+ obj.castShadow = opts.castShadow;
275
+ }
276
+
277
+ if (opts?.parent) {
278
+ opts.parent.add(obj);
279
+ }
110
280
  }
111
- }
281
+ }
282
+
283
+ function createBoxWithRoundedEdges(width: number, height: number, _depth: number, radius0: number, smoothness: number) {
284
+ const shape = new Shape();
285
+ const eps = 0.00001;
286
+ const radius = radius0 - eps;
287
+ shape.absarc(eps, eps, eps, -Math.PI / 2, -Math.PI, true);
288
+ shape.absarc(eps, height - radius * 2, eps, Math.PI, Math.PI / 2, true);
289
+ shape.absarc(width - radius * 2, height - radius * 2, eps, Math.PI / 2, 0, true);
290
+ shape.absarc(width - radius * 2, eps, eps, 0, -Math.PI / 2, true);
291
+ const geometry = new ExtrudeGeometry(shape, {
292
+ bevelEnabled: true,
293
+ bevelSegments: smoothness * 2,
294
+ steps: 1,
295
+ bevelSize: radius,
296
+ bevelThickness: radius0,
297
+ curveSegments: smoothness,
298
+ });
299
+ geometry.scale(1, 1, 1 - radius0)
300
+ geometry.center();
301
+ geometry.computeVertexNormals();
302
+ return geometry;
303
+ }
src/engine/engine_physics_rapier.ts CHANGED
@@ -1239,7 +1239,7 @@
1239
1239
  if (center && collider.gameObject) {
1240
1240
  if (center.x !== 0 || center.y !== 0 || center.z !== 0) {
1241
1241
  // TODO: fix export of center in editor integrations so we dont have to flip here
1242
- this._tempCenterPos.x = -center.x;
1242
+ this._tempCenterPos.x = center.x;
1243
1243
  this._tempCenterPos.y = center.y;
1244
1244
  this._tempCenterPos.z = center.z;
1245
1245
  getWorldScale(collider.gameObject, this._tempCenterVec);
src/engine/engine_types.ts CHANGED
@@ -139,6 +139,7 @@
139
139
  guid: string;
140
140
  }
141
141
 
142
+ // TODO: we might want to separate the IComponent and IBehaviour where the Behaviour is the one with custom methods and the component only has e.g. the gameobject reference
142
143
  export interface IComponent extends IHasGuid {
143
144
  get isComponent(): boolean;
144
145
 
src/engine-components/ShadowCatcher.ts CHANGED
@@ -63,6 +63,8 @@
63
63
  // make sure we have a unique material to work with
64
64
  this.gameObject.material = this.gameObject.material.clone();
65
65
  this.targetMesh = this.gameObject;
66
+ // make sure the mesh can receive shadows
67
+ this.targetMesh.receiveShadow = true;
66
68
  }
67
69
 
68
70
  if(!this.targetMesh) {