@geoblocks/cesium-walk 0.2.1 → 0.3.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 (3) hide show
  1. package/README.md +7 -3
  2. package/cesium-walk.js +103 -11
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -92,10 +92,14 @@ viewer.scene.primitives.add(await Model.fromGltfAsync({url, modelMatrix}));
92
92
  const mesh = createTriangleMeshFromGltf(await (await fetch(url)).arrayBuffer(), modelMatrix);
93
93
 
94
94
  const walkMode = new CesiumWalk(viewer);
95
- // stand on the highest floor below the knees, climb steps up to 0.4 m, stop at the walls 0.5 m ahead
96
- walkOn(walkMode, mesh, {eye: 1.6, step: 0.4, reach: 0.5});
95
+ // stand on the highest floor below the knees, climb steps up to 0.4 m, stop at the walls 0.5 m ahead, keep
96
+ // 0.4 m from the walls beside
97
+ walkOn(walkMode, mesh, {eye: 1.6, step: 0.4, reach: 0.5, radius: 0.4});
97
98
  ```
98
99
 
99
100
  The walker stands on the highest floor below its knees, up the stairs and under the arches, else on the ground
100
101
  it had (the terrain). The walls stop it in every direction, from rays cast at its knees, waist and eyes
101
- (`rayHeights(eye, step)`); the floors and stairs are left to its ground.
102
+ (`rayHeights(eye, step)`); the floors and stairs are left to its ground. With a `radius`, its body at those
103
+ heights is pushed out of any wall nearer than that, the side or the corner of a piece it walks past, which no ray
104
+ ahead meets, and which the camera's near plane would otherwise cut open; a step's riser under the knees is
105
+ climbed, not kept from. Without, the walker is a point, as on the depth probe.
package/cesium-walk.js CHANGED
@@ -1,4 +1,4 @@
1
- import {Cartesian3, Math as CesiumMath} from "@cesium/core";
1
+ import {Cartesian3, Ellipsoid, FixedFrameTransforms, Math as CesiumMath, Matrix4} from "@cesium/core";
2
2
  import Controls from "@geoblocks/cesium-input";
3
3
  import {ObstacleProbe, blockedStep, meshGroundHeight, meshObstacleAhead} from "@geoblocks/cesium-obstacles";
4
4
 
@@ -12,6 +12,11 @@ const LANDING = 0.15;
12
12
  const BOB_TIME = 0.15;
13
13
  // in meters: nearer its height, the camera stays, so that a walker standing still renders no frame
14
14
  const SETTLED = 1e-3;
15
+ // at a concave corner the slide along one wall heads into the other: a step is checked again along its slide, this
16
+ // many times at most, before it is taken
17
+ const SLIDE_PASSES = 3;
18
+ // in a corner, pushed out of one wall, the body may be in the other: it is pushed again, this many times at most
19
+ const CLEAR_PASSES = 3;
15
20
 
16
21
  // the default bindings: W, A, S, D or the arrows to walk, Shift to sprint and Space to jump; on a gamepad, the
17
22
  // left stick, at any speed and in any direction, Cross to sprint and Square to jump, as in GTA 5. Keys by
@@ -34,6 +39,10 @@ const normalScratch = new Cartesian3();
34
39
  const forwardScratch = new Cartesian3();
35
40
  const stepScratch = new Cartesian3();
36
41
  const allowedScratch = new Cartesian3();
42
+ const awayScratch = new Cartesian3();
43
+ const frameScratch = new Matrix4();
44
+ const bodyScratch = new Cartesian3();
45
+ const upScratch = new Cartesian3();
37
46
  const targetScratch = new Cartesian3();
38
47
  const changeScratch = new Cartesian3();
39
48
 
@@ -170,6 +179,21 @@ export default class CesiumWalk {
170
179
  */
171
180
  this.obstacleAhead = (toward, key) => this.obstacleProbe_.ahead(toward, key);
172
181
 
182
+ /**
183
+ * The walker's body: how far from the walls it keeps, in meters, pushed out of any it walks past, sideways
184
+ * or at a corner, where no ray ahead meets it; 0 by default, a walker without a body.
185
+ * @type {number}
186
+ */
187
+ this.radius = 0;
188
+
189
+ /**
190
+ * The surface nearest the walker's body, within its radius: a point of it, the surface's normal toward the
191
+ * body, and the point of the body it is nearest, if any; asked after each step while collision is on. None by
192
+ * default, as the obstacles in view tell nothing about what is beside the walker; walkOn gives the mesh's.
193
+ * @type {() => {point: Cartesian3, normal: Cartesian3, origin: Cartesian3} | undefined}
194
+ */
195
+ this.surfaceNear = () => undefined;
196
+
173
197
  /**
174
198
  * The height of the ground under a position, in meters above the ellipsoid, or undefined when unknown
175
199
  * yet. The terrain by default, none without a globe; a model's floors otherwise, for a walker on several levels.
@@ -322,18 +346,63 @@ export default class CesiumWalk {
322
346
  }
323
347
 
324
348
  /**
325
- * Moves the camera a step, stopping at obstacles or sliding along them.
349
+ * Moves the camera a step, stopping at obstacles or sliding along them; a slide is checked against the obstacles
350
+ * ahead of it in turn, so that at a concave corner it stops at the second wall rather than going through it.
326
351
  * @param {Cartesian3} direction The unit direction of the step, level.
327
352
  * @param {number} step The length of the step, negative backward, in meters.
328
353
  * @param {string} kind Which steps these are, for the obstacle looked for last time.
329
354
  */
330
355
  step_(direction, step, kind) {
331
356
  const camera = this.viewer.camera;
332
- const toward = Cartesian3.multiplyByScalar(direction, Math.sign(step), stepScratch);
333
- const obstacle = this.collision ? this.obstacleAhead(toward, `${kind}${Math.sign(step)}`) : undefined;
334
- const allowed = blockedStep(toward, Math.abs(step), camera.positionWC, obstacle, allowedScratch);
335
- if (allowed.length > 0) {
336
- camera.move(allowed.direction, allowed.length);
357
+ let toward = Cartesian3.multiplyByScalar(direction, Math.sign(step), stepScratch);
358
+ let length = Math.abs(step);
359
+ for (let pass = 0; pass < SLIDE_PASSES && length > 0; pass++) {
360
+ // a slide's look is its own, not the step's: the probe keeps its last look in each direction by its key
361
+ const obstacle = this.collision ? this.obstacleAhead(toward, `${kind}${Math.sign(step)}${pass ? "slide" : ""}`) : undefined;
362
+ const allowed = blockedStep(toward, length, camera.positionWC, obstacle, allowedScratch);
363
+ const same = Cartesian3.equalsEpsilon(allowed.direction, toward, CesiumMath.EPSILON9);
364
+ length = allowed.length;
365
+ toward = Cartesian3.clone(allowed.direction, stepScratch);
366
+ if (same) {
367
+ break;
368
+ }
369
+ }
370
+ if (length > 0) {
371
+ camera.move(toward, length);
372
+ }
373
+ if (this.collision) {
374
+ this.keepClear_();
375
+ }
376
+ }
377
+
378
+ /**
379
+ * Pushes the camera level out of the walls the walker's body is in, each in turn.
380
+ */
381
+ keepClear_() {
382
+ const camera = this.viewer.camera;
383
+ for (let pass = 0; pass < CLEAR_PASSES; pass++) {
384
+ const surface = this.surfaceNear();
385
+ if (!surface) {
386
+ return;
387
+ }
388
+ const up = this.surfaceNormal_();
389
+ // level, from the surface to the body; a surface straight above or below it is left to the walker's ground
390
+ const away = Cartesian3.subtract(surface.origin, surface.point, awayScratch);
391
+ Cartesian3.subtract(away, Cartesian3.projectVector(away, up, stepScratch), away);
392
+ const distance = Cartesian3.magnitude(away);
393
+ if (distance >= this.radius) {
394
+ return;
395
+ }
396
+ if (distance > CesiumMath.EPSILON6) {
397
+ Cartesian3.divideByScalar(away, distance, away);
398
+ } else {
399
+ Cartesian3.subtract(surface.normal, Cartesian3.projectVector(surface.normal, up, stepScratch), away);
400
+ if (Cartesian3.magnitude(away) < CesiumMath.EPSILON6) {
401
+ return;
402
+ }
403
+ Cartesian3.normalize(away, away);
404
+ }
405
+ camera.move(away, this.radius - distance);
337
406
  }
338
407
  }
339
408
 
@@ -395,17 +464,40 @@ export function rayHeights(eye, step) {
395
464
  /**
396
465
  * Walks a CesiumWalk on a triangle mesh of cesium-obstacles: it stands on the highest floor below its knees, up
397
466
  * the stairs, under the arches, else on what it stood on before (the terrain); the walls ahead stop it, from rays
398
- * cast at the knees, the waist and the eyes, the floors and stairs left to the ground below, in every direction.
467
+ * cast at the knees, the waist and the eyes, the floors and stairs left to the ground below, in every direction;
468
+ * with a radius, its body at those heights is kept that far from the walls beside it too, a step's riser under
469
+ * the knees excepted.
399
470
  * @param {CesiumWalk} walk
400
471
  * @param {import("@geoblocks/cesium-obstacles").TriangleMesh} mesh
401
- * @param {{eye: number, step: number, reach: number}} options In meters: the eyes above the floor, the
402
- * highest step, how far ahead the walls are looked for.
472
+ * @param {{eye: number, step: number, reach: number, radius?: number}} options In meters: the eyes above the
473
+ * floor, the highest step, how far ahead the walls are looked for, the body's radius (none by default).
403
474
  */
404
- export function walkOn(walk, mesh, {eye, step, reach}) {
475
+ export function walkOn(walk, mesh, {eye, step, reach, radius = 0}) {
405
476
  const camera = walk.viewer.scene.camera;
406
477
  const fallback = walk.groundHeight;
407
478
  const offsets = rayHeights(eye, step).map(height => new Cartesian3(0, 0, height - eye));
408
479
  walk.obstacleAhead = meshObstacleAhead(mesh, () => camera.positionWC, {offsets, reach});
480
+ walk.radius = radius;
481
+ if (radius > 0) {
482
+ walk.surfaceNear = () => {
483
+ const position = camera.positionWC;
484
+ const frame = FixedFrameTransforms.eastNorthUpToFixedFrame(position, Ellipsoid.WGS84, frameScratch);
485
+ const up = Ellipsoid.WGS84.geodeticSurfaceNormal(position, upScratch);
486
+ let nearest;
487
+ for (const [k, offset] of offsets.entries()) {
488
+ const origin = Matrix4.multiplyByPoint(frame, offset, bodyScratch);
489
+ const hit = mesh.nearest(origin, radius, {walls: true});
490
+ // under the knees, a step's riser: climbed, not kept from
491
+ if (!hit || (k === 0 && Cartesian3.dot(Cartesian3.subtract(hit.position, origin, awayScratch), up) < 0)) {
492
+ continue;
493
+ }
494
+ if (!nearest || hit.distance < nearest.distance) {
495
+ nearest = {point: hit.position, normal: hit.normal, origin: Cartesian3.clone(origin), distance: hit.distance};
496
+ }
497
+ }
498
+ return nearest;
499
+ };
500
+ }
409
501
  const ground = meshGroundHeight(mesh, {below: eye - step});
410
502
  walk.groundHeight = cartographic => ground(cartographic) ?? fallback(cartographic);
411
503
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geoblocks/cesium-walk",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "license": "BSD-3-Clause",
5
5
  "type": "module",
6
6
  "repository": {
@@ -22,11 +22,11 @@
22
22
  },
23
23
  "dependencies": {
24
24
  "@geoblocks/cesium-input": "^0.1.0",
25
- "@geoblocks/cesium-obstacles": "^0.2.1"
25
+ "@geoblocks/cesium-obstacles": "^0.3.0"
26
26
  },
27
27
  "peerDependencies": {
28
28
  "@cesium/core": ">=0.1.0",
29
29
  "@cesium/engine": ">=10"
30
30
  },
31
- "gitHead": "0abb7f3381fdf13b15d50047a46d3789f5f95a9e"
31
+ "gitHead": "18519930ac006985391a708114d9bd746eec4328"
32
32
  }