258 lines
6.1 KiB
TypeScript
258 lines
6.1 KiB
TypeScript
import {
|
|
PerspectiveCamera,
|
|
Scene,
|
|
WebGLRenderer,
|
|
AmbientLight,
|
|
DirectionalLight,
|
|
Group,
|
|
Object3D,
|
|
OrthographicCamera,
|
|
Color,
|
|
Vector3,
|
|
BufferGeometry,
|
|
BufferAttribute,
|
|
DoubleSide,
|
|
Mesh,
|
|
MeshBasicMaterial,
|
|
ConeGeometry,
|
|
} from "three";
|
|
|
|
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
|
|
import { getCenter3D, getMaxSize } from "./utils";
|
|
|
|
export interface Extent {
|
|
xmin: number;
|
|
ymin: number;
|
|
xmax: number;
|
|
ymax: number;
|
|
zmin: number;
|
|
zmax: number;
|
|
}
|
|
|
|
let controls: OrbitControls;
|
|
let renderer: WebGLRenderer;
|
|
let camera: PerspectiveCamera;
|
|
let scene: Scene;
|
|
let overlayCamera: OrthographicCamera;
|
|
let overlayScene: Scene;
|
|
let maxSize = 0;
|
|
const compass = new Group();
|
|
const UI_WIDTH = 150;
|
|
const UI_HEIGHT = 150;
|
|
|
|
export function buildScene(container: HTMLElement, extent: Extent) {
|
|
maxSize = getMaxSize(extent);
|
|
const center = getCenter3D(extent);
|
|
|
|
const width = container.clientWidth;
|
|
const height = container.clientHeight;
|
|
|
|
camera = new PerspectiveCamera(50, width / height, 10, maxSize * 20);
|
|
|
|
camera.position.set(center.x, center.y - 200000, extent.zmax + 100000);
|
|
camera.up.set(0, 0, 1);
|
|
camera.lookAt(center);
|
|
|
|
// Initialize the renderer
|
|
renderer = new WebGLRenderer({
|
|
logarithmicDepthBuffer: true,
|
|
});
|
|
|
|
renderer.setPixelRatio(window.devicePixelRatio);
|
|
renderer.setSize(width, height);
|
|
renderer.localClippingEnabled = true;
|
|
renderer.autoClear = false;
|
|
// renderer.setAnimationLoop(animate);
|
|
|
|
// Handle window resize event to adapt the aspect ratio
|
|
window.addEventListener("resize", () => onWindowResize(container));
|
|
|
|
container.appendChild(renderer.domElement);
|
|
|
|
controls = new OrbitControls(camera, renderer.domElement);
|
|
controls.target.set(center.x, center.y, center.z);
|
|
controls.enableDamping = true;
|
|
controls.dampingFactor = 0.1;
|
|
controls.maxDistance = maxSize * 5;
|
|
controls.minDistance = maxSize / 5;
|
|
controls.update();
|
|
controls.saveState();
|
|
|
|
// Set wireframe to false on initial load
|
|
scene = new Scene();
|
|
scene.userData.wireframe = false;
|
|
const backgroundColor = new Color(0xbfd1e5);
|
|
scene.background = backgroundColor;
|
|
|
|
// Add lights to the scene
|
|
buildDefaultLights(scene, extent);
|
|
|
|
// Create Scene for UI overlay
|
|
overlayScene = new Scene();
|
|
|
|
// Create an overlay camera
|
|
overlayCamera = new OrthographicCamera(
|
|
-maxSize / 2,
|
|
maxSize / 2,
|
|
maxSize / 2,
|
|
-maxSize / 2,
|
|
0.1,
|
|
10 * maxSize
|
|
);
|
|
|
|
// Sync overlay camera with main camera
|
|
overlayCamera.position.copy(camera.position);
|
|
overlayCamera.rotation.copy(camera.rotation);
|
|
|
|
// Create compass
|
|
createCompass(center);
|
|
|
|
return { renderer, scene, camera, controls };
|
|
}
|
|
|
|
function onWindowResize(container: HTMLElement) {
|
|
// Update the camera's aspect ratio and the renderer's size to reflect
|
|
// the new screen dimensions upon a browser window resize
|
|
camera.aspect = container.clientWidth / container.clientHeight;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(container.clientWidth, container.clientHeight);
|
|
|
|
// required if controls.enableDamping or controls.autoRotate are set to true
|
|
controls.update();
|
|
}
|
|
|
|
// Callback for animation loop
|
|
export function animate(cb: () => void) {
|
|
return () => {
|
|
// Update controls for main camera
|
|
controls.update();
|
|
|
|
renderer.render(scene, camera);
|
|
|
|
// Render the UI overlay
|
|
renderOverlay();
|
|
|
|
cb();
|
|
};
|
|
}
|
|
|
|
// Render the overlay scene as an overlay
|
|
function renderOverlay() {
|
|
// Sync overlay camera
|
|
overlayCamera.position.copy(camera.position);
|
|
overlayCamera.rotation.copy(camera.rotation);
|
|
|
|
// Sync compass
|
|
const dir = new Vector3();
|
|
camera.getWorldDirection(dir);
|
|
compass.position.set(
|
|
camera.position.x + maxSize * dir.x,
|
|
camera.position.y + maxSize * dir.y,
|
|
camera.position.z + maxSize * dir.z
|
|
);
|
|
|
|
// Render the overlay scene to the screen (position it in the bottom left)
|
|
renderer.setViewport(10, 10, UI_WIDTH, UI_HEIGHT);
|
|
renderer.render(overlayScene, overlayCamera);
|
|
renderer.setViewport(
|
|
0,
|
|
0,
|
|
renderer.domElement.width,
|
|
renderer.domElement.height
|
|
);
|
|
}
|
|
|
|
function buildDefaultLights(scene: Scene, extent: Extent) {
|
|
const center = getCenter3D(extent);
|
|
|
|
// Directional light position
|
|
const lightPosition = {
|
|
x: center.x,
|
|
y: center.y - 15000,
|
|
z: extent.zmax + 100000,
|
|
};
|
|
|
|
const lights = [];
|
|
|
|
// Ambient light
|
|
const ambient = new AmbientLight(0xffffff, 1);
|
|
lights.push(ambient);
|
|
|
|
// Directional lights
|
|
const directionalLight = new DirectionalLight(0xffffff, 1.5);
|
|
directionalLight.position.set(
|
|
lightPosition.x,
|
|
lightPosition.y,
|
|
lightPosition.z
|
|
);
|
|
|
|
// Create a target for the directional light
|
|
const target = new Object3D();
|
|
target.position.set(center.x, center.y, center.z);
|
|
scene.add(target);
|
|
|
|
directionalLight.target = target;
|
|
lights.push(directionalLight);
|
|
|
|
const lightsGroup = new Group();
|
|
lightsGroup.name = "lights";
|
|
lightsGroup.add(...lights);
|
|
scene.add(lightsGroup);
|
|
}
|
|
|
|
function createCompass(center: Vector3) {
|
|
const vertices = new Float32Array(
|
|
[
|
|
[0.2, 0, 0],
|
|
[0, 1, 0],
|
|
[-0.2, 0, 0],
|
|
[-0.2, 0, 0],
|
|
[0, -1, 0],
|
|
[0.2, 0, 0],
|
|
].flat()
|
|
);
|
|
const positions = new BufferAttribute(vertices, 3);
|
|
|
|
const geometry = new BufferGeometry();
|
|
geometry.setAttribute("position", positions);
|
|
|
|
const colors = new Float32Array([
|
|
1,
|
|
12 / 255,
|
|
0,
|
|
1,
|
|
12 / 255,
|
|
0,
|
|
1,
|
|
12 / 255,
|
|
0,
|
|
113 / 255,
|
|
99 / 255,
|
|
183 / 255,
|
|
113 / 255,
|
|
99 / 255,
|
|
183 / 255,
|
|
113 / 255,
|
|
99 / 255,
|
|
183 / 255,
|
|
]);
|
|
geometry.setAttribute("color", new BufferAttribute(colors, 3));
|
|
|
|
const material = new MeshBasicMaterial({
|
|
side: DoubleSide,
|
|
vertexColors: true,
|
|
});
|
|
|
|
const needle = new Mesh(geometry, material);
|
|
|
|
const coneGeometry = new ConeGeometry(0.1, 0.1, 32);
|
|
const coneMaterial = new MeshBasicMaterial({ color: 0x444444 });
|
|
const cone = new Mesh(coneGeometry, coneMaterial);
|
|
cone.position.z = 0.055;
|
|
cone.rotateX(Math.PI / 2);
|
|
|
|
compass.add(needle, cone);
|
|
compass.position.copy(center);
|
|
compass.scale.set(maxSize * 0.5, maxSize * 0.5, maxSize * 0.5);
|
|
overlayScene.add(compass);
|
|
}
|