@geoblocks/cesium-walk 0.1.1 → 0.2.1
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/README.md +11 -19
- package/cesium-walk.js +33 -6
- package/package.json +5 -9
- package/mesh-builder.js +0 -109
- package/walk-mesh.js +0 -427
package/README.md
CHANGED
|
@@ -79,31 +79,23 @@ ellipsoid at a `Cartographic`, or `undefined` while unknown.
|
|
|
79
79
|
|
|
80
80
|
## Walking on a mesh
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
`walkOn` walks on a triangle mesh of [cesium-obstacles](../cesium-obstacles), floors, stairs and walls known on
|
|
83
|
+
the CPU, from groups of triangles or a glTF, with no wait for the GPU:
|
|
84
84
|
|
|
85
85
|
```javascript
|
|
86
|
-
import
|
|
87
|
-
import {
|
|
88
|
-
|
|
89
|
-
import {Group} from '@geoblocks/cesium-walk/mesh-builder.js';
|
|
86
|
+
import {Model} from '@cesium/engine';
|
|
87
|
+
import {createTriangleMeshFromGltf} from '@geoblocks/cesium-obstacles';
|
|
88
|
+
import CesiumWalk, {walkOn} from '@geoblocks/cesium-walk';
|
|
90
89
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const walls = new Group([0.9, 0.9, 0.85, 1]);
|
|
95
|
-
walls.box(0, 0, 0, 10, 0.2, 2.5);
|
|
96
|
-
const mesh = await createWalkMesh([floor.walkGroup, walls.walkGroup], modelMatrix);
|
|
97
|
-
viewer.scene.primitives.add(mesh.primitive);
|
|
90
|
+
const url = 'building.glb';
|
|
91
|
+
viewer.scene.primitives.add(await Model.fromGltfAsync({url, modelMatrix}));
|
|
92
|
+
const mesh = createTriangleMeshFromGltf(await (await fetch(url)).arrayBuffer(), modelMatrix);
|
|
98
93
|
|
|
99
94
|
const walkMode = new CesiumWalk(viewer);
|
|
100
95
|
// stand on the highest floor below the knees, climb steps up to 0.4 m, stop at the walls 0.5 m ahead
|
|
101
96
|
walkOn(walkMode, mesh, {eye: 1.6, step: 0.4, reach: 0.5});
|
|
102
97
|
```
|
|
103
98
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
`
|
|
107
|
-
walls. `Group` (`mesh-builder.js`) builds the groups face by face: `face`, `polygon` (with holes), `sides`, `box`;
|
|
108
|
-
`charts: true` records each face, to lay them out in an atlas. `mesh.floorBelow(x, y, z)` and `mesh.raycast(origin, direction, maxDistance)` answer in the
|
|
109
|
-
mesh's frame and on the globe.
|
|
99
|
+
The walker stands on the highest floor below its knees, up the stairs and under the arches, else on the ground
|
|
100
|
+
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.
|
package/cesium-walk.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {Cartesian3, Math as CesiumMath} from "@cesium/core";
|
|
2
2
|
import Controls from "@geoblocks/cesium-input";
|
|
3
|
-
import {ObstacleProbe, blockedStep} from "@geoblocks/cesium-obstacles";
|
|
3
|
+
import {ObstacleProbe, blockedStep, meshGroundHeight, meshObstacleAhead} from "@geoblocks/cesium-obstacles";
|
|
4
4
|
|
|
5
5
|
// in seconds: a frame after a backgrounded tab must not walk or fall far
|
|
6
6
|
const MAX_DELTA = 0.1;
|
|
@@ -269,12 +269,11 @@ export default class CesiumWalk {
|
|
|
269
269
|
// walk along the view direction projected onto the ground plane,
|
|
270
270
|
// so that looking down does not push the camera into the terrain
|
|
271
271
|
const normal = this.surfaceNormal_();
|
|
272
|
-
const along = Cartesian3.
|
|
273
|
-
|
|
274
|
-
Cartesian3.
|
|
272
|
+
const along = Cartesian3.subtract(
|
|
273
|
+
camera.direction,
|
|
274
|
+
Cartesian3.projectVector(camera.direction, normal, forwardScratch),
|
|
275
275
|
forwardScratch
|
|
276
276
|
);
|
|
277
|
-
Cartesian3.subtract(camera.direction, along, along);
|
|
278
277
|
const level = Cartesian3.magnitude(along) > CesiumMath.EPSILON6;
|
|
279
278
|
if (level) {
|
|
280
279
|
Cartesian3.normalize(along, along);
|
|
@@ -318,7 +317,7 @@ export default class CesiumWalk {
|
|
|
318
317
|
if (this.landing_ < 1e-3) {
|
|
319
318
|
this.landing_ = 0;
|
|
320
319
|
}
|
|
321
|
-
this.stepPhase_ += (Cartesian3.magnitude(this.velocity_) * deltaTime / STEP_LENGTH) *
|
|
320
|
+
this.stepPhase_ += (Cartesian3.magnitude(this.velocity_) * deltaTime / STEP_LENGTH) * CesiumMath.PI;
|
|
322
321
|
this.clampCameraToTerrain_();
|
|
323
322
|
}
|
|
324
323
|
|
|
@@ -382,3 +381,31 @@ export default class CesiumWalk {
|
|
|
382
381
|
return scene.ellipsoid.geodeticSurfaceNormal(scene.camera.position, normalScratch);
|
|
383
382
|
}
|
|
384
383
|
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* The heights above the floor the walls are looked for at: the knees, the waist and the eyes.
|
|
387
|
+
* @param {number} eye
|
|
388
|
+
* @param {number} step
|
|
389
|
+
* @return {number[]} In meters, from the lowest.
|
|
390
|
+
*/
|
|
391
|
+
export function rayHeights(eye, step) {
|
|
392
|
+
return [step + 0.05, (step + eye) / 2, eye];
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Walks a CesiumWalk on a triangle mesh of cesium-obstacles: it stands on the highest floor below its knees, up
|
|
397
|
+
* 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.
|
|
399
|
+
* @param {CesiumWalk} walk
|
|
400
|
+
* @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.
|
|
403
|
+
*/
|
|
404
|
+
export function walkOn(walk, mesh, {eye, step, reach}) {
|
|
405
|
+
const camera = walk.viewer.scene.camera;
|
|
406
|
+
const fallback = walk.groundHeight;
|
|
407
|
+
const offsets = rayHeights(eye, step).map(height => new Cartesian3(0, 0, height - eye));
|
|
408
|
+
walk.obstacleAhead = meshObstacleAhead(mesh, () => camera.positionWC, {offsets, reach});
|
|
409
|
+
const ground = meshGroundHeight(mesh, {below: eye - step});
|
|
410
|
+
walk.groundHeight = cartographic => ground(cartographic) ?? fallback(cartographic);
|
|
411
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geoblocks/cesium-walk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"license": "BSD-3-Clause",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -12,25 +12,21 @@
|
|
|
12
12
|
"module": "cesium-walk.js",
|
|
13
13
|
"sideEffects": false,
|
|
14
14
|
"exports": {
|
|
15
|
-
".": "./cesium-walk.js"
|
|
16
|
-
"./walk-mesh.js": "./walk-mesh.js",
|
|
17
|
-
"./mesh-builder.js": "./mesh-builder.js"
|
|
15
|
+
".": "./cesium-walk.js"
|
|
18
16
|
},
|
|
19
17
|
"files": [
|
|
20
|
-
"cesium-walk.js"
|
|
21
|
-
"walk-mesh.js",
|
|
22
|
-
"mesh-builder.js"
|
|
18
|
+
"cesium-walk.js"
|
|
23
19
|
],
|
|
24
20
|
"publishConfig": {
|
|
25
21
|
"access": "public"
|
|
26
22
|
},
|
|
27
23
|
"dependencies": {
|
|
28
24
|
"@geoblocks/cesium-input": "^0.1.0",
|
|
29
|
-
"@geoblocks/cesium-obstacles": "^0.
|
|
25
|
+
"@geoblocks/cesium-obstacles": "^0.2.1"
|
|
30
26
|
},
|
|
31
27
|
"peerDependencies": {
|
|
32
28
|
"@cesium/core": ">=0.1.0",
|
|
33
29
|
"@cesium/engine": ">=10"
|
|
34
30
|
},
|
|
35
|
-
"gitHead": "
|
|
31
|
+
"gitHead": "0abb7f3381fdf13b15d50047a46d3789f5f95a9e"
|
|
36
32
|
}
|
package/mesh-builder.js
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import {Cartesian2, PolygonPipeline} from "@cesium/core";
|
|
2
|
-
|
|
3
|
-
// Triangles of one color, built face by face, for createWalkMesh (walk-mesh.js).
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* @typedef {object} Chart A face's place among a group's vertices, for laying it out in an atlas.
|
|
7
|
-
* @property {number} start Its first vertex.
|
|
8
|
-
* @property {number} count Its vertices.
|
|
9
|
-
* @property {number[]} normal Its normal, not of unit length.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
/** The normal of a flat ring of corners, by Newell's method, of any length. */
|
|
13
|
-
function newell(/** @type {number[][]} */ corners) {
|
|
14
|
-
const n = [0, 0, 0];
|
|
15
|
-
corners.forEach(([x, y, z], i) => {
|
|
16
|
-
const [nx, ny, nz] = corners[(i + 1) % corners.length];
|
|
17
|
-
n[0] += (y - ny) * (z + nz);
|
|
18
|
-
n[1] += (z - nz) * (x + nx);
|
|
19
|
-
n[2] += (x - nx) * (y + ny);
|
|
20
|
-
});
|
|
21
|
-
return n;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export class Group {
|
|
25
|
-
/**
|
|
26
|
-
* Triangles of one color, in meters, z up.
|
|
27
|
-
* @param {number[]} color Linear, and its alpha.
|
|
28
|
-
* @param {{charts?: boolean} & Record<string, any>} [options] The walk group's (show, collide, shadows, emissive,
|
|
29
|
-
* uv, customShader, ...), passed on; charts: each face recorded, for an atlas.
|
|
30
|
-
*/
|
|
31
|
-
constructor(color, {charts = false, ...options} = {}) {
|
|
32
|
-
this.color = color;
|
|
33
|
-
this.options = options;
|
|
34
|
-
/** @type {number[]} */
|
|
35
|
-
this.positions = [];
|
|
36
|
-
/** @type {number[]} */
|
|
37
|
-
this.indices = [];
|
|
38
|
-
/** @type {Chart[] | undefined} */
|
|
39
|
-
this.charts = charts ? [] : undefined;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* A planar convex face, its corners in order.
|
|
44
|
-
* @param {...number[]} corners [x, y, z]
|
|
45
|
-
*/
|
|
46
|
-
face(...corners) {
|
|
47
|
-
const first = this.positions.length / 3;
|
|
48
|
-
corners.forEach((corner) => this.positions.push(...corner));
|
|
49
|
-
for (let i = 1; i + 1 < corners.length; i++) {
|
|
50
|
-
this.indices.push(first, first + i, first + i + 1);
|
|
51
|
-
}
|
|
52
|
-
this.charts?.push({start: first, count: corners.length, normal: newell(corners)});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* A flat polygon with holes, its height at each corner.
|
|
57
|
-
* @param {number[][]} outline [x, y]
|
|
58
|
-
* @param {number[][][]} holes
|
|
59
|
-
* @param {(x: number, y: number) => number} z
|
|
60
|
-
*/
|
|
61
|
-
polygon(outline, holes, z) {
|
|
62
|
-
const points = [outline, ...holes].flat();
|
|
63
|
-
/** @type {number[]} */
|
|
64
|
-
const starts = [];
|
|
65
|
-
let start = outline.length;
|
|
66
|
-
for (const hole of holes) {
|
|
67
|
-
starts.push(start);
|
|
68
|
-
start += hole.length;
|
|
69
|
-
}
|
|
70
|
-
const first = this.positions.length / 3;
|
|
71
|
-
points.forEach(([x, y]) => this.positions.push(x, y, z(x, y)));
|
|
72
|
-
PolygonPipeline.triangulate(points.map(([x, y]) => new Cartesian2(x, y)), starts).forEach((i) => this.indices.push(first + i));
|
|
73
|
-
this.charts?.push({start: first, count: points.length, normal: newell(outline.map(([x, y]) => [x, y, z(x, y)]))});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* The faces of a ring between two heights.
|
|
78
|
-
* @param {number[][]} ring [x, y]
|
|
79
|
-
* @param {number} z0
|
|
80
|
-
* @param {number} z1
|
|
81
|
-
*/
|
|
82
|
-
sides(ring, z0, z1) {
|
|
83
|
-
ring.forEach((a, i) => {
|
|
84
|
-
const b = ring[(i + 1) % ring.length];
|
|
85
|
-
this.face([...a, z0], [...b, z0], [...b, z1], [...a, z1]);
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* An axis-aligned box.
|
|
91
|
-
* @param {number} x0
|
|
92
|
-
* @param {number} y0
|
|
93
|
-
* @param {number} z0
|
|
94
|
-
* @param {number} x1
|
|
95
|
-
* @param {number} y1
|
|
96
|
-
* @param {number} z1
|
|
97
|
-
*/
|
|
98
|
-
box(x0, y0, z0, x1, y1, z1) {
|
|
99
|
-
this.sides([[x0, y0], [x1, y0], [x1, y1], [x0, y1]], z0, z1);
|
|
100
|
-
this.face([x0, y0, z1], [x1, y0, z1], [x1, y1, z1], [x0, y1, z1]);
|
|
101
|
-
this.face([x0, y0, z0], [x1, y0, z0], [x1, y1, z0], [x0, y1, z0]);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** The group for createWalkMesh: its color, vertices and triangles, its options, its charts if recorded. */
|
|
105
|
-
get walkGroup() {
|
|
106
|
-
return {color: this.color, vertices: new Float32Array(this.positions), triangles: new Uint32Array(this.indices), ...this.options,
|
|
107
|
-
...(this.charts ? {charts: this.charts} : {})};
|
|
108
|
-
}
|
|
109
|
-
}
|
package/walk-mesh.js
DELETED
|
@@ -1,427 +0,0 @@
|
|
|
1
|
-
import {Cartesian3, Cartographic, Ellipsoid, Matrix4} from "@cesium/core";
|
|
2
|
-
import {Axis, Model, PrimitiveCollection, ShadowMode} from "@cesium/engine";
|
|
3
|
-
|
|
4
|
-
// Triangles to walk on, in meters, z up, in the frame of a model matrix: their models, and the floors and
|
|
5
|
-
// walls a walker meets, without reading the depth, which waits for the GPU.
|
|
6
|
-
|
|
7
|
-
// the ground's cells, in meters
|
|
8
|
-
const CELL = 2;
|
|
9
|
-
// the faces flatter than this, of the cosine of their slope, are floors, stairs and ramps
|
|
10
|
-
const FLOOR = 0.5;
|
|
11
|
-
|
|
12
|
-
const originScratch = new Cartesian3();
|
|
13
|
-
const directionScratch = new Cartesian3();
|
|
14
|
-
const upScratch = new Cartesian3();
|
|
15
|
-
const rayScratch = new Cartesian3();
|
|
16
|
-
const rayOriginScratch = new Cartesian3();
|
|
17
|
-
const kneesScratch = new Cartographic();
|
|
18
|
-
const floorScratch = new Cartesian3();
|
|
19
|
-
const floorCartographicScratch = new Cartographic();
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* @typedef {object} WalkGroup Triangles of one color.
|
|
23
|
-
* @property {number[]} color The base color, linear, and its alpha: translucent below 1.
|
|
24
|
-
* @property {Float32Array} vertices x, y, z, in meters, z up.
|
|
25
|
-
* @property {Uint32Array} triangles
|
|
26
|
-
* @property {boolean} [show] false: walked on, not seen (an invisible edge).
|
|
27
|
-
* @property {boolean} [collide] false: seen, not walked on (the battens, the water).
|
|
28
|
-
* @property {boolean} [shadows] false: casts no shadow (the glass, the water).
|
|
29
|
-
* @property {number} [emissive] The share of its color it gives off: the light indoors, which Cesium does not
|
|
30
|
-
* bounce off the walls.
|
|
31
|
-
* @property {boolean} [uv] Whether its vertices carry their x and y as texture coordinates, in meters, for a
|
|
32
|
-
* shader drawing on the floors.
|
|
33
|
-
* @property {Float32Array} [lightmap] Its vertices' coordinates in a baked light's atlas, u and v each, written as
|
|
34
|
-
* the second texture coordinates.
|
|
35
|
-
* @property {import("@cesium/engine").CustomShader} [customShader]
|
|
36
|
-
*/
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* The flat shaded model of the triangles, untextured, in glTF's y up: the positions in floats, a corner the same
|
|
40
|
-
* in every model sharing it, no crack between them; the normals quantized (KHR_mesh_quantization) in 8 bits,
|
|
41
|
-
* padded to 4 bytes, 16 in all, as against 24 in floats. Exported for the tests.
|
|
42
|
-
* @param {Float32Array} vertices
|
|
43
|
-
* @param {Uint32Array} triangles
|
|
44
|
-
* @param {number[]} color
|
|
45
|
-
* @param {number} emissive
|
|
46
|
-
* @param {boolean} uv Whether to give the vertices their x and y as texture coordinates, in floats.
|
|
47
|
-
* @param {Float32Array | undefined} lightmap The vertices' second texture coordinates, in floats, if any.
|
|
48
|
-
* @return {ArrayBuffer} The binary glTF.
|
|
49
|
-
*/
|
|
50
|
-
export function toGlb(vertices, triangles, color, emissive, uv, lightmap) {
|
|
51
|
-
const count = triangles.length;
|
|
52
|
-
const toGltf = (/** @type {number} */ v) => [vertices[v], vertices[v + 2], -vertices[v + 1]];
|
|
53
|
-
const positions = new Float32Array(count * 3);
|
|
54
|
-
const normals = new Int8Array(count * 4);
|
|
55
|
-
const texCoords = new Float32Array(uv ? count * 2 : 0);
|
|
56
|
-
const lightmapCoords = new Float32Array(lightmap ? count * 2 : 0);
|
|
57
|
-
const min = [Infinity, Infinity, Infinity];
|
|
58
|
-
const max = [-Infinity, -Infinity, -Infinity];
|
|
59
|
-
for (let i = 0; i < count; i += 3) {
|
|
60
|
-
const corners = [0, 1, 2].map(k => toGltf(triangles[i + k] * 3));
|
|
61
|
-
const [a, b, c] = corners.map(([x, y, z]) => new Cartesian3(x, y, z));
|
|
62
|
-
const normal = Cartesian3.cross(Cartesian3.subtract(b, a, new Cartesian3()), Cartesian3.subtract(c, a, new Cartesian3()), new Cartesian3());
|
|
63
|
-
if (Cartesian3.magnitudeSquared(normal) > 0) {
|
|
64
|
-
Cartesian3.normalize(normal, normal);
|
|
65
|
-
}
|
|
66
|
-
corners.forEach((corner, k) => {
|
|
67
|
-
positions.set(corner, (i + k) * 3);
|
|
68
|
-
normals.set([normal.x, normal.y, normal.z].map(n => Math.round(n * 127)), (i + k) * 4);
|
|
69
|
-
for (let axis = 0; axis < 3; axis++) {
|
|
70
|
-
min[axis] = Math.min(min[axis], Math.fround(corner[axis]));
|
|
71
|
-
max[axis] = Math.max(max[axis], Math.fround(corner[axis]));
|
|
72
|
-
}
|
|
73
|
-
if (uv) {
|
|
74
|
-
const v = triangles[i + k] * 3;
|
|
75
|
-
texCoords.set([vertices[v], vertices[v + 1]], (i + k) * 2);
|
|
76
|
-
}
|
|
77
|
-
if (lightmap) {
|
|
78
|
-
const v = triangles[i + k];
|
|
79
|
-
lightmapCoords.set([lightmap[v * 2], lightmap[v * 2 + 1]], (i + k) * 2);
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
// the float attributes after the positions and normals, each its own buffer view
|
|
84
|
-
const floats = /** @type {[string, Float32Array][]} */ ([...(uv ? [['TEXCOORD_0', texCoords]] : []), ...(lightmap ? [['TEXCOORD_1', lightmapCoords]] : [])]);
|
|
85
|
-
const floatsLength = floats.reduce((n, [, array]) => n + array.byteLength, 0);
|
|
86
|
-
let floatOffset = positions.byteLength + normals.byteLength;
|
|
87
|
-
const json = new TextEncoder().encode(JSON.stringify({
|
|
88
|
-
asset: {version: '2.0'},
|
|
89
|
-
extensionsUsed: ['KHR_mesh_quantization', ...(emissive > 1 ? ['KHR_materials_emissive_strength'] : [])],
|
|
90
|
-
extensionsRequired: ['KHR_mesh_quantization'],
|
|
91
|
-
scene: 0,
|
|
92
|
-
scenes: [{nodes: [0]}],
|
|
93
|
-
nodes: [{mesh: 0}],
|
|
94
|
-
meshes: [{primitives: [{attributes: {POSITION: 0, NORMAL: 1, ...Object.fromEntries(floats.map(([name], k) => [name, 2 + k]))}, material: 0}]}],
|
|
95
|
-
materials: [{
|
|
96
|
-
pbrMetallicRoughness: {baseColorFactor: color, metallicFactor: 0, roughnessFactor: 1},
|
|
97
|
-
// glTF caps the factor at 1, the strength carries the rest
|
|
98
|
-
emissiveFactor: color.slice(0, 3).map(c => Math.min(Math.max(c * Math.min(emissive, 1), 0), 1)),
|
|
99
|
-
...(emissive > 1 ? {extensions: {KHR_materials_emissive_strength: {emissiveStrength: emissive}}} : {}),
|
|
100
|
-
doubleSided: true,
|
|
101
|
-
...(color[3] < 1 ? {alphaMode: 'BLEND'} : {}),
|
|
102
|
-
}],
|
|
103
|
-
buffers: [{byteLength: positions.byteLength + normals.byteLength + floatsLength}],
|
|
104
|
-
bufferViews: [
|
|
105
|
-
{buffer: 0, byteOffset: 0, byteLength: positions.byteLength, target: 34962},
|
|
106
|
-
{buffer: 0, byteOffset: positions.byteLength, byteLength: normals.byteLength, byteStride: 4, target: 34962},
|
|
107
|
-
...floats.map(([, array]) => {
|
|
108
|
-
const view = {buffer: 0, byteOffset: floatOffset, byteLength: array.byteLength, target: 34962};
|
|
109
|
-
floatOffset += array.byteLength;
|
|
110
|
-
return view;
|
|
111
|
-
}),
|
|
112
|
-
],
|
|
113
|
-
accessors: [
|
|
114
|
-
{bufferView: 0, componentType: 5126, count, type: 'VEC3', min, max},
|
|
115
|
-
{bufferView: 1, componentType: 5120, normalized: true, count, type: 'VEC3'},
|
|
116
|
-
...floats.map((_, k) => ({bufferView: 2 + k, componentType: 5126, count, type: 'VEC2'})),
|
|
117
|
-
],
|
|
118
|
-
}));
|
|
119
|
-
const jsonLength = Math.ceil(json.length / 4) * 4;
|
|
120
|
-
const binLength = positions.byteLength + normals.byteLength + floatsLength;
|
|
121
|
-
const glb = new ArrayBuffer(12 + 8 + jsonLength + 8 + binLength);
|
|
122
|
-
const view = new DataView(glb);
|
|
123
|
-
view.setUint32(0, 0x46546c67, true);
|
|
124
|
-
view.setUint32(4, 2, true);
|
|
125
|
-
view.setUint32(8, glb.byteLength, true);
|
|
126
|
-
view.setUint32(12, jsonLength, true);
|
|
127
|
-
view.setUint32(16, 0x4e4f534a, true);
|
|
128
|
-
new Uint8Array(glb, 20, jsonLength).fill(0x20).set(json);
|
|
129
|
-
view.setUint32(20 + jsonLength, binLength, true);
|
|
130
|
-
view.setUint32(24 + jsonLength, 0x004e4942, true);
|
|
131
|
-
new Float32Array(glb, 28 + jsonLength, count * 3).set(positions);
|
|
132
|
-
new Int8Array(glb, 28 + jsonLength + positions.byteLength, count * 4).set(normals);
|
|
133
|
-
let offset = 28 + jsonLength + positions.byteLength + normals.byteLength;
|
|
134
|
-
for (const [, array] of floats) {
|
|
135
|
-
new Float32Array(glb, offset, array.length).set(array);
|
|
136
|
-
offset += array.byteLength;
|
|
137
|
-
}
|
|
138
|
-
return glb;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* @param {WalkGroup[]} groups
|
|
143
|
-
* @param {Matrix4} modelMatrix From the triangles' frame to the globe.
|
|
144
|
-
*/
|
|
145
|
-
export async function createWalkMesh(groups, modelMatrix) {
|
|
146
|
-
// a single point, or nothing, has nothing to draw
|
|
147
|
-
const visible = groups.filter(group => group.show !== false && group.triangles.length > 0 &&
|
|
148
|
-
group.vertices.some((value, k) => value !== group.vertices[k % 3]));
|
|
149
|
-
const urls = visible.map(group => URL.createObjectURL(new Blob([toGlb(group.vertices, group.triangles, group.color, group.emissive ?? 0, group.uv ?? false, group.lightmap)], {type: 'model/gltf-binary'})));
|
|
150
|
-
const loaded = await Promise.allSettled(visible.map(async (group, k) => {
|
|
151
|
-
// with glTF's y up only: Cesium would also turn the model's +z to +x, a quarter turn off the frame's axes
|
|
152
|
-
const model = await Model.fromGltfAsync({
|
|
153
|
-
url: urls[k],
|
|
154
|
-
modelMatrix,
|
|
155
|
-
forwardAxis: Axis.X,
|
|
156
|
-
shadows: group.shadows === false ? ShadowMode.RECEIVE_ONLY : ShadowMode.ENABLED,
|
|
157
|
-
...(group.customShader ? {customShader: group.customShader} : {}),
|
|
158
|
-
});
|
|
159
|
-
model.readyEvent.addEventListener(() => URL.revokeObjectURL(urls[k]));
|
|
160
|
-
model.errorEvent.addEventListener(() => URL.revokeObjectURL(urls[k]));
|
|
161
|
-
return model;
|
|
162
|
-
}));
|
|
163
|
-
// in the groups' order
|
|
164
|
-
const primitive = new PrimitiveCollection();
|
|
165
|
-
for (const result of loaded) {
|
|
166
|
-
if (result.status === 'fulfilled') {
|
|
167
|
-
primitive.add(result.value);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
const failed = loaded.find(result => result.status === 'rejected');
|
|
171
|
-
if (failed) {
|
|
172
|
-
primitive.destroy();
|
|
173
|
-
urls.forEach(url => URL.revokeObjectURL(url));
|
|
174
|
-
throw failed.reason;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// the groups walked on as one: their vertices one after the other, their triangles renumbered
|
|
178
|
-
const colliding = groups.filter(group => group.collide !== false);
|
|
179
|
-
const vertices = new Float32Array(colliding.reduce((n, group) => n + group.vertices.length, 0));
|
|
180
|
-
const triangles = new Uint32Array(colliding.reduce((n, group) => n + group.triangles.length, 0));
|
|
181
|
-
let vertexOffset = 0;
|
|
182
|
-
let triangleOffset = 0;
|
|
183
|
-
for (const group of colliding) {
|
|
184
|
-
vertices.set(group.vertices, vertexOffset);
|
|
185
|
-
for (let k = 0; k < group.triangles.length; k++) {
|
|
186
|
-
triangles[triangleOffset + k] = group.triangles[k] + vertexOffset / 3;
|
|
187
|
-
}
|
|
188
|
-
vertexOffset += group.vertices.length;
|
|
189
|
-
triangleOffset += group.triangles.length;
|
|
190
|
-
}
|
|
191
|
-
const toLocal = Matrix4.inverse(modelMatrix, new Matrix4());
|
|
192
|
-
// the normals' matrix, the inverse transpose
|
|
193
|
-
const toNormal = Matrix4.transpose(toLocal, new Matrix4());
|
|
194
|
-
|
|
195
|
-
// the triangles, and the floors among them, by the cells they overlap
|
|
196
|
-
/** @type {Map<string, number[]>} */
|
|
197
|
-
const cells = new Map();
|
|
198
|
-
/** @type {Map<string, number[]>} */
|
|
199
|
-
const allCells = new Map();
|
|
200
|
-
const isFloor = new Uint8Array(triangles.length / 3);
|
|
201
|
-
const vertex = (/** @type {number} */ index) => vertices.subarray(index * 3, index * 3 + 3);
|
|
202
|
-
const add = (/** @type {Map<string, number[]>} */ map, /** @type {string} */ key, /** @type {number} */ i) => {
|
|
203
|
-
const list = map.get(key);
|
|
204
|
-
if (list) {
|
|
205
|
-
list.push(i);
|
|
206
|
-
} else {
|
|
207
|
-
map.set(key, [i]);
|
|
208
|
-
}
|
|
209
|
-
};
|
|
210
|
-
for (let i = 0; i < triangles.length; i += 3) {
|
|
211
|
-
const [a, b, c] = [vertex(triangles[i]), vertex(triangles[i + 1]), vertex(triangles[i + 2])];
|
|
212
|
-
const nx = (b[1] - a[1]) * (c[2] - a[2]) - (b[2] - a[2]) * (c[1] - a[1]);
|
|
213
|
-
const ny = (b[2] - a[2]) * (c[0] - a[0]) - (b[0] - a[0]) * (c[2] - a[2]);
|
|
214
|
-
const nz = (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]);
|
|
215
|
-
isFloor[i / 3] = Math.abs(nz) >= FLOOR * Math.hypot(nx, ny, nz) ? 1 : 0;
|
|
216
|
-
for (let x = Math.floor(Math.min(a[0], b[0], c[0]) / CELL); x <= Math.floor(Math.max(a[0], b[0], c[0]) / CELL); x++) {
|
|
217
|
-
for (let y = Math.floor(Math.min(a[1], b[1], c[1]) / CELL); y <= Math.floor(Math.max(a[1], b[1], c[1]) / CELL); y++) {
|
|
218
|
-
const key = `${x},${y}`;
|
|
219
|
-
add(allCells, key, i);
|
|
220
|
-
if (isFloor[i / 3]) {
|
|
221
|
-
add(cells, key, i);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* The highest floor at a position of the frame, not above it.
|
|
229
|
-
* @param {number} x
|
|
230
|
-
* @param {number} y
|
|
231
|
-
* @param {number} z
|
|
232
|
-
* @return {number | undefined} Its height, in meters.
|
|
233
|
-
*/
|
|
234
|
-
const floorBelow = (x, y, z) => {
|
|
235
|
-
let highest;
|
|
236
|
-
for (const i of cells.get(`${Math.floor(x / CELL)},${Math.floor(y / CELL)}`) ?? []) {
|
|
237
|
-
const a = triangles[i] * 3, b = triangles[i + 1] * 3, c = triangles[i + 2] * 3;
|
|
238
|
-
const ux = vertices[b] - vertices[a], uy = vertices[b + 1] - vertices[a + 1];
|
|
239
|
-
const vx = vertices[c] - vertices[a], vy = vertices[c + 1] - vertices[a + 1];
|
|
240
|
-
const d = ux * vy - vx * uy;
|
|
241
|
-
if (d === 0) {
|
|
242
|
-
continue;
|
|
243
|
-
}
|
|
244
|
-
const s = ((x - vertices[a]) * vy - vx * (y - vertices[a + 1])) / d;
|
|
245
|
-
const t = (ux * (y - vertices[a + 1]) - (x - vertices[a]) * uy) / d;
|
|
246
|
-
if (s < 0 || t < 0 || s + t > 1) {
|
|
247
|
-
continue;
|
|
248
|
-
}
|
|
249
|
-
const height = vertices[a + 2] + s * (vertices[b + 2] - vertices[a + 2]) + t * (vertices[c + 2] - vertices[a + 2]);
|
|
250
|
-
if (height <= z && (highest === undefined || height > highest)) {
|
|
251
|
-
highest = height;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
return highest;
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
// the ray each triangle was last tested against, so that a triangle over several cells is tested once
|
|
258
|
-
const tested = new Uint32Array(triangles.length / 3);
|
|
259
|
-
let ray = 0;
|
|
260
|
-
/**
|
|
261
|
-
* The nearest triangle along a ray, in meters, by Möller and Trumbore's test, through the cells the
|
|
262
|
-
* ray crosses, nearest first.
|
|
263
|
-
* @param {Cartesian3} o The origin.
|
|
264
|
-
* @param {Cartesian3} d The unit direction.
|
|
265
|
-
* @param {number} far The longest distance.
|
|
266
|
-
* @param {boolean} walls Whether to skip the floors.
|
|
267
|
-
* @return {{distance: number, triangle: number} | undefined}
|
|
268
|
-
*/
|
|
269
|
-
const nearestHit = (o, d, far, walls) => {
|
|
270
|
-
ray++;
|
|
271
|
-
let best;
|
|
272
|
-
let x = Math.floor(o.x / CELL);
|
|
273
|
-
let y = Math.floor(o.y / CELL);
|
|
274
|
-
const stepX = Math.sign(d.x);
|
|
275
|
-
const stepY = Math.sign(d.y);
|
|
276
|
-
// the distances along the ray to the next cell borders, and between borders
|
|
277
|
-
let nextX = stepX === 0 ? Infinity : ((x + (stepX > 0 ? 1 : 0)) * CELL - o.x) / d.x;
|
|
278
|
-
let nextY = stepY === 0 ? Infinity : ((y + (stepY > 0 ? 1 : 0)) * CELL - o.y) / d.y;
|
|
279
|
-
const deltaX = stepX === 0 ? Infinity : CELL / Math.abs(d.x);
|
|
280
|
-
const deltaY = stepY === 0 ? Infinity : CELL / Math.abs(d.y);
|
|
281
|
-
let entry = 0;
|
|
282
|
-
while (entry <= far) {
|
|
283
|
-
const exit = Math.min(nextX, nextY, far);
|
|
284
|
-
for (const i of allCells.get(`${x},${y}`) ?? []) {
|
|
285
|
-
if (tested[i / 3] === ray || (walls && isFloor[i / 3])) {
|
|
286
|
-
continue;
|
|
287
|
-
}
|
|
288
|
-
tested[i / 3] = ray;
|
|
289
|
-
const a = triangles[i] * 3, b = triangles[i + 1] * 3, c = triangles[i + 2] * 3;
|
|
290
|
-
const e1x = vertices[b] - vertices[a], e1y = vertices[b + 1] - vertices[a + 1], e1z = vertices[b + 2] - vertices[a + 2];
|
|
291
|
-
const e2x = vertices[c] - vertices[a], e2y = vertices[c + 1] - vertices[a + 1], e2z = vertices[c + 2] - vertices[a + 2];
|
|
292
|
-
const px = d.y * e2z - d.z * e2y, py = d.z * e2x - d.x * e2z, pz = d.x * e2y - d.y * e2x;
|
|
293
|
-
const det = e1x * px + e1y * py + e1z * pz;
|
|
294
|
-
if (Math.abs(det) < 1e-9) {
|
|
295
|
-
continue;
|
|
296
|
-
}
|
|
297
|
-
const sx = o.x - vertices[a], sy = o.y - vertices[a + 1], sz = o.z - vertices[a + 2];
|
|
298
|
-
const u = (sx * px + sy * py + sz * pz) / det;
|
|
299
|
-
if (u < 0 || u > 1) {
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
302
|
-
const qx = sy * e1z - sz * e1y, qy = sz * e1x - sx * e1z, qz = sx * e1y - sy * e1x;
|
|
303
|
-
const v = (d.x * qx + d.y * qy + d.z * qz) / det;
|
|
304
|
-
if (v < 0 || u + v > 1) {
|
|
305
|
-
continue;
|
|
306
|
-
}
|
|
307
|
-
const t = (e2x * qx + e2y * qy + e2z * qz) / det;
|
|
308
|
-
if (t >= 0 && t <= far && (!best || t < best.distance)) {
|
|
309
|
-
best = {distance: t, triangle: i};
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
// a hit within this cell is the nearest; one beyond it may still be beaten in the next cells
|
|
313
|
-
if (best && best.distance <= exit) {
|
|
314
|
-
return best;
|
|
315
|
-
}
|
|
316
|
-
if (exit >= far) {
|
|
317
|
-
return best;
|
|
318
|
-
}
|
|
319
|
-
entry = exit;
|
|
320
|
-
if (nextX < nextY) {
|
|
321
|
-
x += stepX;
|
|
322
|
-
nextX += deltaX;
|
|
323
|
-
} else {
|
|
324
|
-
y += stepY;
|
|
325
|
-
nextY += deltaY;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
return best;
|
|
329
|
-
};
|
|
330
|
-
|
|
331
|
-
return {
|
|
332
|
-
primitive,
|
|
333
|
-
floorBelow,
|
|
334
|
-
/**
|
|
335
|
-
* The first surface along a ray on the globe.
|
|
336
|
-
* @param {Cartesian3} origin
|
|
337
|
-
* @param {Cartesian3} direction A unit vector.
|
|
338
|
-
* @param {number} maxDistance In meters.
|
|
339
|
-
* @param {{walls?: boolean}} [options] walls: skip the floors, stairs and ramps, which the walker climbs.
|
|
340
|
-
* @return {{position: Cartesian3, normal: Cartesian3, distance: number} | undefined} Where the
|
|
341
|
-
* ray stops, the surface's normal toward the ray's origin, and the distance in meters.
|
|
342
|
-
*/
|
|
343
|
-
raycast: (origin, direction, maxDistance, {walls = false} = {}) => {
|
|
344
|
-
const o = Matrix4.multiplyByPoint(toLocal, origin, originScratch);
|
|
345
|
-
const d = Matrix4.multiplyByPointAsVector(toLocal, direction, directionScratch);
|
|
346
|
-
// the frame's units in a meter along the ray, should the matrix scale
|
|
347
|
-
const perMeter = Cartesian3.magnitude(d);
|
|
348
|
-
Cartesian3.divideByScalar(d, perMeter, d);
|
|
349
|
-
const hit = nearestHit(o, d, maxDistance * perMeter, walls);
|
|
350
|
-
if (!hit) {
|
|
351
|
-
return undefined;
|
|
352
|
-
}
|
|
353
|
-
const position = Matrix4.multiplyByPoint(modelMatrix, Cartesian3.add(o, Cartesian3.multiplyByScalar(d, hit.distance, rayScratch), rayScratch), new Cartesian3());
|
|
354
|
-
const i = hit.triangle;
|
|
355
|
-
const [a, b, c] = [vertex(triangles[i]), vertex(triangles[i + 1]), vertex(triangles[i + 2])];
|
|
356
|
-
const e1 = new Cartesian3(b[0] - a[0], b[1] - a[1], b[2] - a[2]);
|
|
357
|
-
const e2 = new Cartesian3(c[0] - a[0], c[1] - a[1], c[2] - a[2]);
|
|
358
|
-
const normal = Matrix4.multiplyByPointAsVector(toNormal, Cartesian3.cross(e1, e2, e1), new Cartesian3());
|
|
359
|
-
Cartesian3.normalize(normal, normal);
|
|
360
|
-
if (Cartesian3.dot(normal, direction) > 0) {
|
|
361
|
-
Cartesian3.negate(normal, normal);
|
|
362
|
-
}
|
|
363
|
-
return {position, normal, distance: Cartesian3.distance(origin, position)};
|
|
364
|
-
},
|
|
365
|
-
/**
|
|
366
|
-
* @param {number} x
|
|
367
|
-
* @param {number} y
|
|
368
|
-
* @param {number} z
|
|
369
|
-
* @param {Cartesian3} [result]
|
|
370
|
-
* @return {Cartesian3} A position of the frame on the globe.
|
|
371
|
-
*/
|
|
372
|
-
position: (x, y, z, result = new Cartesian3()) => Matrix4.multiplyByPoint(modelMatrix, Cartesian3.fromElements(x, y, z, result), result),
|
|
373
|
-
/**
|
|
374
|
-
* @param {Cartesian3} position
|
|
375
|
-
* @param {Cartesian3} [result]
|
|
376
|
-
* @return {Cartesian3} A position on the globe in the frame.
|
|
377
|
-
*/
|
|
378
|
-
local: (/** @type {Cartesian3} */ position, result = new Cartesian3()) => Matrix4.multiplyByPoint(toLocal, position, result),
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/**
|
|
383
|
-
* The heights above the floor the walls are looked for at: the knees, the waist and the eyes.
|
|
384
|
-
* @param {number} eye
|
|
385
|
-
* @param {number} step
|
|
386
|
-
* @return {number[]} In meters, from the lowest.
|
|
387
|
-
*/
|
|
388
|
-
export function rayHeights(eye, step) {
|
|
389
|
-
return [step + 0.05, (step + eye) / 2, eye];
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/**
|
|
393
|
-
* Walks a CesiumWalk on a mesh: it stands on the highest floor below its knees, up the stairs, under the
|
|
394
|
-
* arches, else on what it stood on before (the terrain); the walls ahead stop it, from rays cast at the knees,
|
|
395
|
-
* the waist and the eyes, the floors and stairs left to the ground below, in every direction.
|
|
396
|
-
* @param {import('./cesium-walk.js').default} walk
|
|
397
|
-
* @param {Awaited<ReturnType<typeof createWalkMesh>>} mesh
|
|
398
|
-
* @param {{eye: number, step: number, reach: number}} options In meters: the eyes above the floor, the
|
|
399
|
-
* highest step, how far ahead the walls are looked for.
|
|
400
|
-
*/
|
|
401
|
-
export function walkOn(walk, mesh, {eye, step, reach}) {
|
|
402
|
-
const camera = walk.viewer.scene.camera;
|
|
403
|
-
const fallback = walk.groundHeight;
|
|
404
|
-
const heights = rayHeights(eye, step);
|
|
405
|
-
const walls = {walls: true};
|
|
406
|
-
walk.obstacleAhead = toward => {
|
|
407
|
-
const eyes = camera.positionWC;
|
|
408
|
-
const up = Ellipsoid.WGS84.geodeticSurfaceNormal(eyes, upScratch);
|
|
409
|
-
let nearest;
|
|
410
|
-
for (const height of heights) {
|
|
411
|
-
const origin = Cartesian3.add(eyes, Cartesian3.multiplyByScalar(up, height - eye, rayOriginScratch), rayOriginScratch);
|
|
412
|
-
const hit = mesh.raycast(origin, toward, reach, walls);
|
|
413
|
-
if (hit && (!nearest || hit.distance < nearest.distance)) {
|
|
414
|
-
nearest = hit;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
return nearest && {point: nearest.position, normal: nearest.normal};
|
|
418
|
-
};
|
|
419
|
-
walk.groundHeight = cartographic => {
|
|
420
|
-
const knees = Cartographic.clone(cartographic, kneesScratch);
|
|
421
|
-
knees.height -= eye - step;
|
|
422
|
-
const {x, y, z} = mesh.local(Cartographic.toCartesian(knees, Ellipsoid.WGS84, floorScratch), floorScratch);
|
|
423
|
-
const floor = mesh.floorBelow(x, y, z);
|
|
424
|
-
return floor === undefined ? fallback(cartographic) :
|
|
425
|
-
Cartographic.fromCartesian(mesh.position(x, y, floor, floorScratch), Ellipsoid.WGS84, floorCartographicScratch).height;
|
|
426
|
-
};
|
|
427
|
-
}
|