@miris-inc/three 0.0.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/chunk.ts +17 -0
- package/controls.ts +129 -0
- package/detector.ts +61 -0
- package/index.html +59 -0
- package/index.ts +5 -0
- package/lod.ts +70 -0
- package/miris.ts +92 -0
- package/package.json +19 -0
- package/scene.ts +29 -0
- package/stream.ts +244 -0
- package/vite.config.ts +19 -0
package/chunk.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Chunk as CoreChunk } from "@miris-inc/core";
|
|
2
|
+
import { Group } from "three";
|
|
3
|
+
|
|
4
|
+
export default class Chunk extends Group {
|
|
5
|
+
#coreChunk: CoreChunk;
|
|
6
|
+
constructor(coreChunk: CoreChunk) {
|
|
7
|
+
super();
|
|
8
|
+
this.#coreChunk = coreChunk;
|
|
9
|
+
this.matrix.fromArray(this.transform());
|
|
10
|
+
this.matrixAutoUpdate = false;
|
|
11
|
+
this.matrixWorldNeedsUpdate = true;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
transform() {
|
|
15
|
+
return this.#coreChunk.transform;
|
|
16
|
+
}
|
|
17
|
+
}
|
package/controls.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Camera,
|
|
3
|
+
Object3D,
|
|
4
|
+
Quaternion,
|
|
5
|
+
Raycaster,
|
|
6
|
+
Vector2,
|
|
7
|
+
Vector3,
|
|
8
|
+
} from "three";
|
|
9
|
+
|
|
10
|
+
const speed = 5;
|
|
11
|
+
const maxPitch = Math.PI / 2 - 0.1;
|
|
12
|
+
|
|
13
|
+
export default class Controls {
|
|
14
|
+
#camera;
|
|
15
|
+
#domElement;
|
|
16
|
+
#raycaster = new Raycaster();
|
|
17
|
+
#activeObject: Object3D | null = null;
|
|
18
|
+
#isDragging = false;
|
|
19
|
+
#prevCoords = new Vector2();
|
|
20
|
+
#dispose: () => void;
|
|
21
|
+
|
|
22
|
+
enableRotate = true;
|
|
23
|
+
enableZoom = true;
|
|
24
|
+
|
|
25
|
+
declare readonly objects: Set<Object3D>;
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
objects: Object3D | Iterable<Object3D> | null,
|
|
29
|
+
camera: Camera,
|
|
30
|
+
domElement: HTMLElement
|
|
31
|
+
) {
|
|
32
|
+
Object.defineProperty(this, "objects", {
|
|
33
|
+
value: new Set(
|
|
34
|
+
objects ? (objects instanceof Object3D ? [objects] : objects) : null
|
|
35
|
+
),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
this.#camera = camera;
|
|
39
|
+
this.#domElement = domElement;
|
|
40
|
+
|
|
41
|
+
const pointerdown = this.#pointerdown.bind(this);
|
|
42
|
+
const pointermove = this.#pointermove.bind(this);
|
|
43
|
+
const pointerup = this.#pointerup.bind(this);
|
|
44
|
+
const pointerleave = this.#pointerup.bind(this);
|
|
45
|
+
|
|
46
|
+
domElement.addEventListener("pointerdown", pointerdown);
|
|
47
|
+
domElement.addEventListener("pointermove", pointermove);
|
|
48
|
+
domElement.addEventListener("pointerup", pointerup);
|
|
49
|
+
domElement.addEventListener("pointerleave", pointerleave);
|
|
50
|
+
|
|
51
|
+
this.#dispose = () => {
|
|
52
|
+
domElement.removeEventListener("pointerdown", pointerdown);
|
|
53
|
+
domElement.removeEventListener("pointermove", pointermove);
|
|
54
|
+
domElement.removeEventListener("pointerup", pointerup);
|
|
55
|
+
domElement.removeEventListener("pointerleave", pointerleave);
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
#pointerdown({ clientX, clientY }: MouseEvent) {
|
|
60
|
+
const { left, top, width, height } =
|
|
61
|
+
this.#domElement.getBoundingClientRect();
|
|
62
|
+
const coords = new Vector2(
|
|
63
|
+
((clientX - left) / width) * 2 - 1,
|
|
64
|
+
-((clientY - top) / height) * 2 + 1
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
this.#raycaster.setFromCamera(coords, this.#camera);
|
|
68
|
+
const [intersection] = this.#raycaster.intersectObjects(
|
|
69
|
+
[...this.objects],
|
|
70
|
+
true
|
|
71
|
+
);
|
|
72
|
+
this.#activeObject = null;
|
|
73
|
+
|
|
74
|
+
if (intersection) {
|
|
75
|
+
intersection.object.traverseAncestors((object) => {
|
|
76
|
+
if (this.objects.has(object)) this.#activeObject = object;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (this.#activeObject) {
|
|
81
|
+
this.#isDragging = true;
|
|
82
|
+
this.#prevCoords = coords;
|
|
83
|
+
this.#domElement.style.cursor = "grabbing";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#pointermove({ clientX, clientY }: MouseEvent) {
|
|
88
|
+
const { left, top, width, height } =
|
|
89
|
+
this.#domElement.getBoundingClientRect();
|
|
90
|
+
const coords = new Vector2(
|
|
91
|
+
((clientX - left) / width) * 2 - 1,
|
|
92
|
+
-((clientY - top) / height) * 2 + 1
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
if (!this.#isDragging) {
|
|
96
|
+
this.#raycaster.setFromCamera(coords, this.#camera);
|
|
97
|
+
const [intersection] = this.#raycaster.intersectObjects(
|
|
98
|
+
[...this.objects],
|
|
99
|
+
true
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
this.#domElement.style.cursor = intersection ? "grab" : "default";
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!this.#activeObject) return;
|
|
107
|
+
|
|
108
|
+
const deltaX = coords.x - this.#prevCoords.x;
|
|
109
|
+
const deltaY = coords.y - this.#prevCoords.y;
|
|
110
|
+
const { rotation } = this.#activeObject;
|
|
111
|
+
|
|
112
|
+
rotation.x -= deltaY * speed;
|
|
113
|
+
rotation.x = Math.max(-maxPitch, Math.min(maxPitch, rotation.x));
|
|
114
|
+
rotation.y += deltaX * speed;
|
|
115
|
+
|
|
116
|
+
this.#prevCoords.copy(coords);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
#pointerup() {
|
|
120
|
+
this.#isDragging = false;
|
|
121
|
+
this.#activeObject = null;
|
|
122
|
+
this.#domElement.style.cursor = "default";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
dispose() {
|
|
126
|
+
this.#dispose();
|
|
127
|
+
this.#domElement.style.cursor = "default";
|
|
128
|
+
}
|
|
129
|
+
}
|
package/detector.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Camera as CoreCamera, Scene as CoreScene } from "@miris-inc/core";
|
|
2
|
+
import {
|
|
3
|
+
type Camera,
|
|
4
|
+
Mesh,
|
|
5
|
+
OrthographicCamera,
|
|
6
|
+
PerspectiveCamera,
|
|
7
|
+
Scene,
|
|
8
|
+
type WebGLRenderer,
|
|
9
|
+
} from "three";
|
|
10
|
+
|
|
11
|
+
export default class Detector extends Mesh {
|
|
12
|
+
override frustumCulled = false;
|
|
13
|
+
|
|
14
|
+
override onBeforeRender(
|
|
15
|
+
_renderer: WebGLRenderer,
|
|
16
|
+
scene: Scene,
|
|
17
|
+
camera: Camera
|
|
18
|
+
) {
|
|
19
|
+
if (camera.matrixAutoUpdate) {
|
|
20
|
+
const coreScene = CoreScene.forKey(scene);
|
|
21
|
+
|
|
22
|
+
if (!coreScene) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let aspect, fov;
|
|
27
|
+
|
|
28
|
+
if (camera instanceof PerspectiveCamera) {
|
|
29
|
+
aspect = camera.aspect;
|
|
30
|
+
fov = camera.fov;
|
|
31
|
+
} else if (camera instanceof OrthographicCamera) {
|
|
32
|
+
aspect = (camera.right - camera.left) / (camera.top - camera.bottom);
|
|
33
|
+
fov = 0;
|
|
34
|
+
} else {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const { near, far } = camera;
|
|
39
|
+
|
|
40
|
+
const coreCamera =
|
|
41
|
+
CoreCamera.forKey(camera) ??
|
|
42
|
+
new CoreCamera({
|
|
43
|
+
aspect,
|
|
44
|
+
fov,
|
|
45
|
+
near,
|
|
46
|
+
far,
|
|
47
|
+
matrix: camera.matrixWorld.toArray(),
|
|
48
|
+
scene: coreScene,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Associate core camera and core scene.
|
|
52
|
+
coreScene.camera = coreCamera;
|
|
53
|
+
|
|
54
|
+
coreCamera.matrix = camera.matrixWorld.toArray();
|
|
55
|
+
coreCamera.aspect = aspect;
|
|
56
|
+
coreCamera.fov = fov;
|
|
57
|
+
coreCamera.near = near;
|
|
58
|
+
coreCamera.far = far;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
package/index.html
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
5
|
+
<title>Miris Web</title>
|
|
6
|
+
<style>
|
|
7
|
+
body {
|
|
8
|
+
margin: 0;
|
|
9
|
+
background-color: black;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
canvas {
|
|
13
|
+
width: 100vw;
|
|
14
|
+
height: 100vh;
|
|
15
|
+
}
|
|
16
|
+
</style>
|
|
17
|
+
<script type="module" lang="ts">
|
|
18
|
+
// First, we'll import some dependencies
|
|
19
|
+
import * as THREE from "three";
|
|
20
|
+
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
|
|
21
|
+
import { MirisStream } from ".";
|
|
22
|
+
|
|
23
|
+
// Next, we'll set up some variables needed by Three.js
|
|
24
|
+
const canvas = document.getElementById("canvas");
|
|
25
|
+
const scene = new THREE.Scene();
|
|
26
|
+
const camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight);
|
|
27
|
+
const controls = new OrbitControls(camera, canvas);
|
|
28
|
+
const renderer = new THREE.WebGLRenderer({ canvas });
|
|
29
|
+
|
|
30
|
+
// When the window resizes, we want to resize our canvas, too
|
|
31
|
+
new ResizeObserver(() => {
|
|
32
|
+
renderer.setPixelRatio(devicePixelRatio);
|
|
33
|
+
renderer.setSize(innerWidth, innerHeight, false);
|
|
34
|
+
|
|
35
|
+
camera.aspect = innerWidth / innerHeight;
|
|
36
|
+
camera.updateProjectionMatrix();
|
|
37
|
+
}).observe(canvas);
|
|
38
|
+
|
|
39
|
+
// This is the main render loop
|
|
40
|
+
renderer.setAnimationLoop(() => {
|
|
41
|
+
renderer.render(scene, camera);
|
|
42
|
+
controls.update();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Create our Miris stream
|
|
46
|
+
const stream = new MirisStream("prod/black_white_bike");
|
|
47
|
+
stream.position.set(0, -500, 750);
|
|
48
|
+
|
|
49
|
+
// Set our orbit controls target to the asset's position
|
|
50
|
+
controls.target = new THREE.Vector3(0, 0, 750);
|
|
51
|
+
|
|
52
|
+
// Finally, we'll add the stream to our scene
|
|
53
|
+
scene.add(stream);
|
|
54
|
+
</script>
|
|
55
|
+
</head>
|
|
56
|
+
<body>
|
|
57
|
+
<canvas id="canvas" data-testid="canvas"></canvas>
|
|
58
|
+
</body>
|
|
59
|
+
</html>
|
package/index.ts
ADDED
package/lod.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Lod as CoreLod } from "@miris-inc/core";
|
|
2
|
+
import { Matrix4 } from "three";
|
|
3
|
+
import {
|
|
4
|
+
PackedSplats,
|
|
5
|
+
SplatMesh,
|
|
6
|
+
type SplatEncoding,
|
|
7
|
+
type SplatMeshContext,
|
|
8
|
+
} from "@sparkjsdev/spark";
|
|
9
|
+
|
|
10
|
+
export default class Lod extends SplatMesh {
|
|
11
|
+
#coreLod: CoreLod;
|
|
12
|
+
|
|
13
|
+
constructor(coreLod: CoreLod) {
|
|
14
|
+
let encoding: SplatEncoding = {};
|
|
15
|
+
|
|
16
|
+
let extra: Record<string, unknown> = {};
|
|
17
|
+
if (coreLod.sh1Data != null) {
|
|
18
|
+
extra["sh1"] = coreLod.sh1Data;
|
|
19
|
+
encoding.sh1Min = coreLod.sh1Min;
|
|
20
|
+
encoding.sh1Max = coreLod.sh1Max;
|
|
21
|
+
}
|
|
22
|
+
if (coreLod.sh2Data != null) {
|
|
23
|
+
extra["sh2"] = coreLod.sh2Data;
|
|
24
|
+
encoding.sh2Min = coreLod.sh2Min;
|
|
25
|
+
encoding.sh2Max = coreLod.sh2Max;
|
|
26
|
+
}
|
|
27
|
+
if (coreLod.sh3Data != null) {
|
|
28
|
+
extra["sh3"] = coreLod.sh3Data;
|
|
29
|
+
encoding.sh3Min = coreLod.sh3Min;
|
|
30
|
+
encoding.sh3Max = coreLod.sh3Max;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// coreLod.splats is already padded to alignment in core layer
|
|
34
|
+
const packedSplats = new PackedSplats({
|
|
35
|
+
packedArray: coreLod.splats,
|
|
36
|
+
extra: extra,
|
|
37
|
+
splatEncoding: encoding,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
super({ packedSplats });
|
|
41
|
+
|
|
42
|
+
this.#coreLod = coreLod;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
bounds() {
|
|
46
|
+
return this.#coreLod.bounds;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
lodIndex() {
|
|
50
|
+
return this.#coreLod.lodIndex;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
transform() {
|
|
54
|
+
return this.#coreLod.transform;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
parentedTransform() {
|
|
58
|
+
const lodTransform = this.#coreLod.transform;
|
|
59
|
+
const lodMatrix = new Matrix4();
|
|
60
|
+
lodMatrix.fromArray(lodTransform);
|
|
61
|
+
const parentMatrix = this.parent?.matrix;
|
|
62
|
+
return lodMatrix.premultiply(parentMatrix);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// override constructGenerator(context: SplatMeshContext) {
|
|
66
|
+
// context.viewToObject.translate.value.y *= -1;
|
|
67
|
+
// super.constructGenerator(context);
|
|
68
|
+
// context.viewToObject.translate.value.y *= -1;
|
|
69
|
+
// }
|
|
70
|
+
}
|
package/miris.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Miris as CoreMiris,
|
|
3
|
+
type Lod as CoreLod,
|
|
4
|
+
type Change,
|
|
5
|
+
} from "@miris-inc/core";
|
|
6
|
+
import Chunk from "./chunk";
|
|
7
|
+
import Lod from "./lod";
|
|
8
|
+
import Stream from "./stream";
|
|
9
|
+
import { Matrix4 as ThreeMat } from "three";
|
|
10
|
+
|
|
11
|
+
export default class Miris extends CoreMiris {
|
|
12
|
+
// Tracks a LOD ID to its fade animation so we can cancel previous
|
|
13
|
+
// fades if a new one is assigned to the object.
|
|
14
|
+
|
|
15
|
+
override update(entries: Change[]) {
|
|
16
|
+
for (const { type, lod: coreLod } of entries) {
|
|
17
|
+
switch (type) {
|
|
18
|
+
case "created":
|
|
19
|
+
this.#createLod(coreLod);
|
|
20
|
+
break;
|
|
21
|
+
case "activated":
|
|
22
|
+
this.#activateLod(coreLod);
|
|
23
|
+
break;
|
|
24
|
+
case "deactivated":
|
|
25
|
+
this.#deactivateLod(coreLod);
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
override updateChunkTransform(
|
|
32
|
+
chunkTransform: Float32Array,
|
|
33
|
+
parentTransform: Float32Array
|
|
34
|
+
): Float32Array {
|
|
35
|
+
const chunkMatrix = new ThreeMat().fromArray(chunkTransform);
|
|
36
|
+
const parentMatrix = new ThreeMat().fromArray(parentTransform);
|
|
37
|
+
return new Float32Array(chunkMatrix.premultiply(parentMatrix).elements);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#createLod(coreLod: CoreLod) {
|
|
41
|
+
const lod = new Lod(coreLod);
|
|
42
|
+
const coreChunk = coreLod.chunk;
|
|
43
|
+
const coreStream = coreChunk.stream;
|
|
44
|
+
const chunk = (coreChunk.key as Chunk) ?? new Chunk(coreChunk);
|
|
45
|
+
const stream = coreStream.key as Stream;
|
|
46
|
+
|
|
47
|
+
coreLod.key = lod;
|
|
48
|
+
coreChunk.key = chunk;
|
|
49
|
+
|
|
50
|
+
chunk.add(lod);
|
|
51
|
+
stream.add(chunk);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#activateLod(coreLod: CoreLod) {
|
|
55
|
+
const lod = coreLod.key as Lod;
|
|
56
|
+
if (!lod) {
|
|
57
|
+
console.log(
|
|
58
|
+
"Warning: Tried to activate a LOD that does not exist key: " +
|
|
59
|
+
coreLod.id
|
|
60
|
+
);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
lod.opacity = 1;
|
|
64
|
+
|
|
65
|
+
// Re-add to chunk in case it was removed during deactivation
|
|
66
|
+
const chunk = coreLod.chunk.key as Chunk;
|
|
67
|
+
if (chunk && !lod.parent) {
|
|
68
|
+
chunk.add(lod);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#modifyLod(coreLod: CoreLod) {}
|
|
73
|
+
|
|
74
|
+
#deactivateLod(coreLod: CoreLod) {
|
|
75
|
+
const lod = coreLod.key as Lod;
|
|
76
|
+
if (!lod) {
|
|
77
|
+
console.log(
|
|
78
|
+
"Warning: Tried to deactivate a LOD that does not exist key: " +
|
|
79
|
+
coreLod.id
|
|
80
|
+
);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
lod.opacity = 0;
|
|
84
|
+
|
|
85
|
+
// Only remove from scene graph, do NOT dispose!
|
|
86
|
+
// The LOD may be reactivated later during octree collapses.
|
|
87
|
+
// Disposal should only happen when the scene object is deleted.
|
|
88
|
+
lod.removeFromParent();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const miris = new Miris();
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@miris-inc/three",
|
|
3
|
+
"exports": "./index.ts",
|
|
4
|
+
"scripts": {
|
|
5
|
+
"d": "bun dev",
|
|
6
|
+
"dev": "vite",
|
|
7
|
+
"b": "vite build",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"s": "vite serve",
|
|
10
|
+
"serve": "vite serve"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@miris-inc/core": "*",
|
|
14
|
+
"@sparkjsdev/spark": "^0.1.10",
|
|
15
|
+
"@types/three": "^0.182.0",
|
|
16
|
+
"three": "^0.182.0"
|
|
17
|
+
},
|
|
18
|
+
"version": "0.0.1"
|
|
19
|
+
}
|
package/scene.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Scene as CoreScene } from "@miris-inc/core";
|
|
2
|
+
import { Scene as ThreeScene } from "three";
|
|
3
|
+
import { miris } from "./miris";
|
|
4
|
+
|
|
5
|
+
export default class Scene extends ThreeScene {
|
|
6
|
+
#coreScene;
|
|
7
|
+
|
|
8
|
+
constructor(
|
|
9
|
+
key: string | null,
|
|
10
|
+
...args: ConstructorParameters<typeof ThreeScene>
|
|
11
|
+
) {
|
|
12
|
+
super(...args);
|
|
13
|
+
|
|
14
|
+
this.#coreScene = new CoreScene({ miris, viewerKey: key });
|
|
15
|
+
this.#coreScene.key = this;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
setViewerKey(viewerKey: string) {
|
|
19
|
+
this.#coreScene.setViewerKey(viewerKey);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async fetchAssets(...args: Parameters<CoreScene["fetchAssets"]>) {
|
|
23
|
+
return this.#coreScene.fetchAssets(...args);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get coreScene() : CoreScene {
|
|
27
|
+
return this.#coreScene;
|
|
28
|
+
}
|
|
29
|
+
}
|
package/stream.ts
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { Stream as CoreStream, Scene as CoreScene } from "@miris-inc/core";
|
|
2
|
+
import {
|
|
3
|
+
Group,
|
|
4
|
+
type Object3D,
|
|
5
|
+
Scene as ThreeScene,
|
|
6
|
+
BoxGeometry as ThreeBoxGeometry,
|
|
7
|
+
EdgesGeometry as ThreeEdgesGeometry,
|
|
8
|
+
InstancedBufferGeometry as ThreeInstancedGeo,
|
|
9
|
+
LineBasicMaterial as ThreeLineMaterial,
|
|
10
|
+
LineSegments as ThreeLineSegments,
|
|
11
|
+
InstancedBufferAttribute as ThreeIBA,
|
|
12
|
+
Vector3 as ThreeVec,
|
|
13
|
+
Quaternion as ThreeQuat,
|
|
14
|
+
Color as ThreeColor,
|
|
15
|
+
Matrix4 as ThreeMat,
|
|
16
|
+
DynamicDrawUsage as ThreeDynamicDraw,
|
|
17
|
+
} from "three";
|
|
18
|
+
import { miris } from "./miris";
|
|
19
|
+
import Detector from "./detector";
|
|
20
|
+
|
|
21
|
+
export default class Stream extends Group {
|
|
22
|
+
static key: string;
|
|
23
|
+
|
|
24
|
+
declare readonly isStream: true;
|
|
25
|
+
|
|
26
|
+
#coreStream?: CoreStream;
|
|
27
|
+
#boxes: ThreeLineSegments | null = null;
|
|
28
|
+
#instanceBoxGeometry: ThreeInstancedGeo | null = null;
|
|
29
|
+
|
|
30
|
+
constructor(
|
|
31
|
+
uuid: string,
|
|
32
|
+
key?: string | null,
|
|
33
|
+
...args: ConstructorParameters<typeof Group>
|
|
34
|
+
) {
|
|
35
|
+
super(...args);
|
|
36
|
+
|
|
37
|
+
Object.defineProperty(this, "isStream", { value: true });
|
|
38
|
+
|
|
39
|
+
this.addEventListener("added", () => {
|
|
40
|
+
let coreScene;
|
|
41
|
+
let threeObject: Object3D | null = this;
|
|
42
|
+
|
|
43
|
+
threeObject.traverseAncestors((threeObject) => {
|
|
44
|
+
if (!(threeObject instanceof ThreeScene)) return;
|
|
45
|
+
|
|
46
|
+
const threeScene = threeObject;
|
|
47
|
+
|
|
48
|
+
coreScene = CoreScene.forKey(threeScene);
|
|
49
|
+
|
|
50
|
+
if (!coreScene) {
|
|
51
|
+
coreScene = new CoreScene({ miris, viewerKey: key ?? Stream.key });
|
|
52
|
+
coreScene.key = threeScene;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let hasDetector = false;
|
|
56
|
+
|
|
57
|
+
threeScene.traverse((object) => {
|
|
58
|
+
if (object instanceof Detector) hasDetector = true;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (!hasDetector) threeScene.add(new Detector());
|
|
62
|
+
|
|
63
|
+
const coreStream = new CoreStream({ uuid, scene: coreScene });
|
|
64
|
+
|
|
65
|
+
coreStream.key = this;
|
|
66
|
+
coreStream.matrix = this.matrixWorld.toArray();
|
|
67
|
+
this.#coreStream = coreStream;
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
this.addEventListener("removed", () => this.#coreStream?.end());
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
override updateMatrixWorld(...args: Parameters<Group["updateMatrixWorld"]>) {
|
|
75
|
+
super.updateMatrixWorld(...args);
|
|
76
|
+
|
|
77
|
+
if (this.#coreStream) this.#coreStream.matrix = this.matrixWorld.toArray();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#createBoxes() {
|
|
81
|
+
if (this.#instanceBoxGeometry) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// create Instance Buffer Geometry
|
|
86
|
+
const edgesGeometry = new ThreeEdgesGeometry(new ThreeBoxGeometry(1, 1, 1));
|
|
87
|
+
const bufferGeometry = new ThreeInstancedGeo();
|
|
88
|
+
bufferGeometry.index = edgesGeometry.index;
|
|
89
|
+
bufferGeometry.attributes.position = edgesGeometry.attributes.position;
|
|
90
|
+
|
|
91
|
+
// allocate Instanced Buffer Attributes
|
|
92
|
+
const startBoxCount: number = 3000;
|
|
93
|
+
const matrixAttrcount: number = startBoxCount * 16;
|
|
94
|
+
const colorAttrCount: number = startBoxCount * 3;
|
|
95
|
+
const instanceMatrix = new ThreeIBA(new Float32Array(matrixAttrcount), 16);
|
|
96
|
+
const instanceColor = new ThreeIBA(new Float32Array(colorAttrCount), 3);
|
|
97
|
+
|
|
98
|
+
// set Instanced Buffer Attributes of Geometry to Dynamic Draw
|
|
99
|
+
bufferGeometry.setAttribute("instanceMatrix", instanceMatrix);
|
|
100
|
+
bufferGeometry.setAttribute("instanceColor", instanceColor);
|
|
101
|
+
bufferGeometry.attributes.instanceMatrix.setUsage(ThreeDynamicDraw);
|
|
102
|
+
bufferGeometry.attributes.instanceColor.setUsage(ThreeDynamicDraw);
|
|
103
|
+
|
|
104
|
+
const material = new ThreeLineMaterial(); // default color is white
|
|
105
|
+
|
|
106
|
+
material.onBeforeCompile = (shader) => {
|
|
107
|
+
// ---- VERTEX SHADER ----
|
|
108
|
+
shader.vertexShader =
|
|
109
|
+
`
|
|
110
|
+
attribute mat4 instanceMatrix;
|
|
111
|
+
attribute vec3 instanceColor;
|
|
112
|
+
varying vec3 vInstanceColor;
|
|
113
|
+
` + shader.vertexShader;
|
|
114
|
+
|
|
115
|
+
shader.vertexShader = shader.vertexShader.replace(
|
|
116
|
+
"#include <begin_vertex>",
|
|
117
|
+
`
|
|
118
|
+
vec3 transformed = (instanceMatrix * vec4(position, 1.0)).xyz;
|
|
119
|
+
vInstanceColor = instanceColor;
|
|
120
|
+
`
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// ---- FRAGMENT SHADER ----
|
|
124
|
+
shader.fragmentShader =
|
|
125
|
+
`
|
|
126
|
+
varying vec3 vInstanceColor;
|
|
127
|
+
` + shader.fragmentShader;
|
|
128
|
+
|
|
129
|
+
shader.fragmentShader = shader.fragmentShader.replace(
|
|
130
|
+
"vec4 diffuseColor = vec4( diffuse, opacity );",
|
|
131
|
+
"vec4 diffuseColor = vec4( vInstanceColor, opacity );"
|
|
132
|
+
);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
bufferGeometry.instanceCount = 0;
|
|
136
|
+
this.#instanceBoxGeometry = bufferGeometry;
|
|
137
|
+
|
|
138
|
+
this.#boxes = new ThreeLineSegments(this.#instanceBoxGeometry, material);
|
|
139
|
+
this.#boxes.frustumCulled = false;
|
|
140
|
+
this.#boxes.visible = false;
|
|
141
|
+
this.parent.add(this.#boxes);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
clearBoxes() {
|
|
145
|
+
if (!this.#instanceBoxGeometry) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const attr = this.#instanceBoxGeometry?.attributes.instanceMatrix;
|
|
149
|
+
attr.array.fill(0);
|
|
150
|
+
attr.needsUpdate = true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
#getRenderableObjects = () => {
|
|
154
|
+
if (this.children === null || this.children.length === 0) {
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
return this.children[0].children;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
#hueToRgba(hue: number) {
|
|
161
|
+
const clamp01 = (value: number) => {
|
|
162
|
+
return Math.max(0, Math.min(value, 1));
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const r: number = Math.abs(hue * 6 - 3) - 1;
|
|
166
|
+
const g: number = 2 - Math.abs(hue * 6 - 2);
|
|
167
|
+
const b: number = 2 - Math.abs(hue * 6 - 4);
|
|
168
|
+
|
|
169
|
+
return new ThreeColor(clamp01(r), clamp01(g), clamp01(b));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
#mapLodIndexToColor(lodIndex: number) {
|
|
173
|
+
const m_lodHeatMapMaxLodIndex: number = 5;
|
|
174
|
+
const m_lodHeatMapMinLodIndex: number = 0;
|
|
175
|
+
let lodIndexNormalized: number = 0.0;
|
|
176
|
+
|
|
177
|
+
let minMaxDifference = m_lodHeatMapMaxLodIndex - m_lodHeatMapMinLodIndex;
|
|
178
|
+
if (minMaxDifference > 0) {
|
|
179
|
+
lodIndexNormalized =
|
|
180
|
+
(lodIndex - m_lodHeatMapMinLodIndex) / minMaxDifference;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return this.#hueToRgba(1.0 - lodIndexNormalized);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
#updateInstanceTransform(index, center, extents, objectMatrix) {
|
|
187
|
+
const attr = this.#instanceBoxGeometry?.attributes.instanceMatrix;
|
|
188
|
+
|
|
189
|
+
const matrix = new ThreeMat();
|
|
190
|
+
const pos = new ThreeVec(center.x, center.y, center.z);
|
|
191
|
+
const scale = new ThreeVec(extents.x, extents.y, extents.z);
|
|
192
|
+
|
|
193
|
+
matrix.compose(pos, new ThreeQuat(), scale);
|
|
194
|
+
matrix.premultiply(objectMatrix);
|
|
195
|
+
|
|
196
|
+
matrix.toArray(attr.array, index * 16);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
#updateInstanceColor(index, color) {
|
|
200
|
+
const attr = this.#instanceBoxGeometry?.attributes.instanceColor;
|
|
201
|
+
attr?.setXYZ(index, color.r, color.g, color.b);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
updateBounds(
|
|
205
|
+
bounds: Float32Array,
|
|
206
|
+
mat: ThreeMat,
|
|
207
|
+
lodIndex: number,
|
|
208
|
+
id: number
|
|
209
|
+
) {
|
|
210
|
+
let size = new ThreeVec(bounds[3], bounds[4], bounds[5]);
|
|
211
|
+
let center = new ThreeVec(bounds[0], bounds[1], bounds[2]);
|
|
212
|
+
|
|
213
|
+
this.#updateInstanceTransform(id, center, size, mat);
|
|
214
|
+
this.#updateInstanceColor(id, this.#mapLodIndexToColor(lodIndex));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
toggleRenderBounds(shouldRender: boolean = false) {
|
|
218
|
+
this.#createBoxes();
|
|
219
|
+
this.#boxes.visible = shouldRender;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
#updateRenderBounds() {
|
|
223
|
+
if (this.#boxes !== null && this.#boxes.visible) {
|
|
224
|
+
const renderables = this.#getRenderableObjects();
|
|
225
|
+
const childCount: number = renderables.length;
|
|
226
|
+
for (let i = 0; i < childCount; i++) {
|
|
227
|
+
const bounds = renderables[i].bounds();
|
|
228
|
+
const matrix = renderables[i]
|
|
229
|
+
.parentedTransform()
|
|
230
|
+
.premultiply(this.matrix);
|
|
231
|
+
const index = renderables[i].lodIndex();
|
|
232
|
+
this.updateBounds(bounds, matrix, index, i);
|
|
233
|
+
}
|
|
234
|
+
const bufferGeometry = this.#instanceBoxGeometry;
|
|
235
|
+
bufferGeometry.instanceCount = childCount;
|
|
236
|
+
bufferGeometry.attributes.instanceMatrix.needsUpdate = true;
|
|
237
|
+
bufferGeometry.attributes.instanceColor.needsUpdate = true;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
update() {
|
|
242
|
+
this.#updateRenderBounds();
|
|
243
|
+
}
|
|
244
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineConfig } from "vite";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
build: {
|
|
5
|
+
lib: { entry: "index.ts", formats: ["es"] },
|
|
6
|
+
rollupOptions: { external: ["three"] },
|
|
7
|
+
},
|
|
8
|
+
server: {
|
|
9
|
+
headers: {
|
|
10
|
+
// CORS headers (COOP/COEP) are disabled to ease customer integration.
|
|
11
|
+
// These headers make embedding our viewer in customer websites difficult and break compatibility
|
|
12
|
+
// with many external libraries. They are only required for threaded WASM builds that use
|
|
13
|
+
// SharedArrayBuffer. Our single-threaded WASM build does not require these restrictions.
|
|
14
|
+
"Cross-Origin-Opener-Policy": "same-origin",
|
|
15
|
+
"Cross-Origin-Embedder-Policy": "credentialless",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
worker: { format: "es" },
|
|
19
|
+
});
|