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