UNPKG

@giro3d/giro3d

Version:

A JS/WebGL framework for 3D geospatial data visualization

434 lines (421 loc) 14.8 kB
/* * Copyright (c) 2015-2018, IGN France. * Copyright (c) 2018-2026, Giro3D team. * SPDX-License-Identifier: MIT */ import { Box3, Color, DoubleSide, Euler, Group, InstancedMesh, MathUtils, Matrix4, Mesh, MeshBasicMaterial, Sphere, SphereGeometry, SRGBColorSpace, Vector3 } from 'three'; import { getGeometryMemoryUsage, getMaterialMemoryUsage, getObject3DMemoryUsage } from '../core/MemoryUsage'; import pickObjectsAt from '../core/picking/PickObjectsAt'; import Fetcher from '../utils/Fetcher'; import Entity3D from './Entity3D'; const DEFAULT_DISTANCE = 10; /** * Constructor options for the {@link OrientedImageCollection} entity. */ /** * Displays a collection of oriented images coming from a {@link ImageCollectionBaseSource} in the 3D space. * * Each oriented image is displayed as 3 distinct elements: * - a sphere positioned at the location of the camera receptor * - a wireframe to show the camera receptor * - a texture plane on which the image is projected * * Each of these 3 elements can be made visible or invisible independently. * * If the collection contains images that are too spread out geographically, visual issues may occur. * This is why we advise to group images that are relatively close together. */ export class ImageCollectionBase extends Entity3D { /** The source of this entity. */ _origin = new Vector3(); constructor(imageGeometry, wireframeGeometry, options) { super(options); this._container = new Group(); this._container.name = 'OrientedImageCollection-container'; this.object3d.add(this._container); this.source = options.source; this._images = { container: new Group(), bufferGeometry: imageGeometry, opacity: options.images?.opacity ?? 1, objects: null }; this._images.container.name = 'images-container'; this._images.container.visible = false; this._container.add(this._images.container); this._spheres = { bufferGeometry: new SphereGeometry(options.locationSpheres?.radius ?? 0.5, 6, 5), material: new MeshBasicMaterial({ color: options.locationSpheres?.color ?? 0x00ff00 }), instancedMesh: null }; this._wireframes = { bufferGeometry: wireframeGeometry, material: new MeshBasicMaterial({ color: options.wireframes?.color ?? 0x00ff00, wireframe: true }), instancedMesh: null }; if (this.source.images.length > 0) { this._origin.set(Infinity, Infinity, Infinity); for (const imageSource of this.source.images) { this._origin.min(imageSource.position); } this._container.position.copy(this._origin); } this.showLocationSpheres = options.locationSpheres?.visible ?? true; this.showWireframes = options.wireframes?.visible ?? true; this.showImages = options.images?.visible ?? false; this.object3d.updateMatrixWorld(true); } getMemoryUsage(context) { getGeometryMemoryUsage(context, this._images.bufferGeometry); if (this._images.objects) { for (const imageObject of this._images.objects) { getObject3DMemoryUsage(context, imageObject.mesh); } } getGeometryMemoryUsage(context, this._spheres.bufferGeometry); getMaterialMemoryUsage(context, this._spheres.material); if (this._spheres.instancedMesh) { getObject3DMemoryUsage(context, this._spheres.instancedMesh); } getGeometryMemoryUsage(context, this._wireframes.bufferGeometry); getMaterialMemoryUsage(context, this._wireframes.material); if (this._wireframes.instancedMesh) { getObject3DMemoryUsage(context, this._wireframes.instancedMesh); } } /** * Gets or sets the spheres visibility. * * @defaultValue true */ get showLocationSpheres() { return this._spheres.instancedMesh?.visible === true; } set showLocationSpheres(visible) { if (this.showLocationSpheres === visible) { return; } if (this._spheres.instancedMesh) { this._spheres.instancedMesh.visible = visible; } else if (visible) { this.computeSpheres(); } this.notifyChange(this); } /** * Gets or sets the wireframes visibility. * * @defaultValue true */ get showWireframes() { return this._wireframes.instancedMesh?.visible === true; } set showWireframes(visible) { if (this.showWireframes === visible) { return; } if (this._wireframes.instancedMesh) { this._wireframes.instancedMesh.visible = visible; } else if (visible) { this.computeWireframes(); } this.notifyChange(this); } /** * Gets or sets the images opacity. * * @defaultValue 1 */ get imageOpacity() { return this._images.opacity; } set imageOpacity(opacity) { if (this._images.opacity === opacity) { return; } this._images.opacity = opacity; if (this._images.objects) { const actualOpacity = this.opacity * this._images.opacity; for (const imageObject of this._images.objects) { const currentTransparent = imageObject.material.transparent; imageObject.material.transparent = actualOpacity < 1; imageObject.material.opacity = actualOpacity; if (currentTransparent !== imageObject.material.transparent) { imageObject.material.needsUpdate = true; } } this.notifyChange(this); } } /** * Gets or sets the images visibility. * * @defaultValue false */ get showImages() { return !!this._images.objects && this._images.container.visible; } set showImages(visible) { if (this.showImages === visible) { return; } this._images.container.visible = visible; if (visible && !this._images.objects) { const createImageObject = this.createImageObject.bind(this); this._images.objects = this.source.images.map(createImageObject); } this.notifyChange(this); } updateOpacity() { super.updateOpacity(); if (this._images.objects) { const imagesOpacity = this.opacity * this._images.opacity; for (const imageObject of this._images.objects) { const currenTransparent = imageObject.material.transparent; imageObject.material.transparent = imagesOpacity < 1; imageObject.material.opacity = imagesOpacity; if (currenTransparent !== imageObject.material.transparent) { imageObject.material.needsUpdate = true; } } } } /** * Sets the projection distance of a specific image in the collection. */ setImageProjectionDistance(imageIndex, distance) { const source = this.getImageSource(imageIndex); if (source.distance === distance) { return; } source.distance = distance; if (this._images.objects || this._wireframes.instancedMesh) { const wireframeMatrix = this.computeWireframeMatrix(source); if (this._images.objects) { const imageObject = this._images.objects[imageIndex]; imageObject.mesh.matrix.copy(wireframeMatrix); imageObject.mesh.updateMatrixWorld(true); } if (this._wireframes.instancedMesh) { this._wireframes.instancedMesh.setMatrixAt(imageIndex, wireframeMatrix); this._wireframes.instancedMesh.instanceMatrix.needsUpdate = true; } } this.notifyChange(this); } /** * Gets the projection distance of a specific image in the collection. */ getImageProjectionDistance(imageIndex) { return this.getImageSource(imageIndex).distance ?? DEFAULT_DISTANCE; } /** * Gets the point of view of the first image if there is one. */ getDefaultPointOfView() { const firstSource = this.source.images[0]; if (firstSource == null) { return null; } return this.computePointOfView(firstSource); } /** * Gets the point of view of a specific image in the collection. */ getImagePointOfView(imageIndex) { const source = this.getImageSource(imageIndex); return this.computePointOfView(source); } /** * Disposes this entity and deletes unmanaged graphical resources. */ dispose() { this._container.clear(); if (this._images.objects) { for (const imageObject of this._images.objects) { imageObject.material.map?.dispose(); imageObject.material.dispose(); imageObject.wasDisposed = true; } this._images.objects = null; } this._images.bufferGeometry.dispose(); this._spheres.bufferGeometry.dispose(); this._spheres.material.dispose(); this._spheres.instancedMesh?.dispose(); this._spheres.instancedMesh = null; this._wireframes.bufferGeometry.dispose(); this._wireframes.material.dispose(); this._wireframes.instancedMesh?.dispose(); this._wireframes.instancedMesh = null; super.dispose(); } pick(canvasCoords, options) { const result = pickObjectsAt(this.instance, canvasCoords, this.object3d, options); const hit = result[0]; if (hit == null) { return []; } let imageIndex = null; if (hit.instanceId != null) { imageIndex = hit.instanceId; delete hit.instanceId; } else { if (this._images.objects) { this._images.objects.forEach((imageObject, index) => { if (imageObject.mesh === hit.object) { imageIndex = index; } }); } if (imageIndex === null) { return []; } } return [{ ...hit, imageIndex: imageIndex }]; } postUpdate(context) { this.updateMinMaxDistance(context); } computeSpheres() { if (this._spheres.instancedMesh) { return; } const instancedMesh = new InstancedMesh(this._spheres.bufferGeometry, this._spheres.material, this.source.images.length); this._spheres.instancedMesh = instancedMesh; this._spheres.instancedMesh.name = 'spheres'; this._container.add(instancedMesh); this.source.images.forEach((source, index) => { const matrix = this.computeLocalTranslationMatrix(source.position); instancedMesh.setMatrixAt(index, matrix); }); instancedMesh.instanceMatrix.needsUpdate = true; } computeWireframes() { if (this._wireframes.instancedMesh) { return; } const instancedMesh = new InstancedMesh(this._wireframes.bufferGeometry, this._wireframes.material, this.source.images.length); this._wireframes.instancedMesh = instancedMesh; this._wireframes.instancedMesh.name = 'wireframes'; this._container.add(instancedMesh); this.source.images.forEach((source, index) => { const matrix = this.computeWireframeMatrix(source); instancedMesh.setMatrixAt(index, matrix); }); instancedMesh.instanceMatrix.needsUpdate = true; } computeWireframeMatrix(source) { const translationMatrix = this.computeLocalTranslationMatrix(source.position); const rotationMatrix = this.computeLocalRotationMatrix(source.orientation); const scaleMatrix = this.computeWireframeScaleMatrix(source); return new Matrix4().multiply(translationMatrix).multiply(rotationMatrix).multiply(scaleMatrix); } computeLocalRotationMatrix(orientation) { const heading = orientation.heading ?? 0; const pitch = orientation.pitch ?? 0; const roll = orientation.roll ?? 0; return new Matrix4().makeRotationFromEuler(new Euler(MathUtils.degToRad(pitch), MathUtils.degToRad(roll), MathUtils.degToRad(-heading), 'ZYX')); } computeLocalTranslationMatrix(position) { return new Matrix4().makeTranslation(position.x - this._origin.x, position.y - this._origin.y, position.z - this._origin.z); } computePointOfView(source) { const rotationMatrix = this.computeLocalRotationMatrix(source.orientation); return { origin: new Vector3().copy(source.position), target: new Vector3(0, 1, 0).applyMatrix4(rotationMatrix).add(source.position), orthographicZoom: 1 }; } getImageSource(imageIndex) { const source = this.source.images[imageIndex]; if (source == null) { throw new Error(`OrientedImageCollection "${this.id}" does not have image index "${imageIndex}".`); } return source; } updateMinMaxDistance(context) { if (!this.visible) { return; } const boundingBox = new Box3(); if (this._spheres.instancedMesh?.visible === true) { boundingBox.expandByObject(this._spheres.instancedMesh); } if (this._wireframes.instancedMesh?.visible === true) { boundingBox.expandByObject(this._wireframes.instancedMesh); } if (this._images.container.visible) { boundingBox.expandByObject(this._images.container); } const boundingSphere = boundingBox.getBoundingSphere(new Sphere()); const distance = context.distance.plane.distanceToSphere(boundingSphere); this._distance.min = distance; this._distance.max = distance + 2 * boundingSphere.radius; } createImageObject(imageSource, index) { const url = imageSource.imageUrl; const hasUrl = url != null; const actualOpacity = hasUrl ? this.opacity * this._images.opacity : this.opacity * 0.3; const material = new MeshBasicMaterial({ side: DoubleSide, transparent: true, opacity: 0, color: hasUrl ? undefined : this._wireframes.material.color }); const mesh = new Mesh(this._images.bufferGeometry, material); mesh.name = `image-${index}`; this._images.container.add(mesh); mesh.matrix.copy(this.computeWireframeMatrix(imageSource)); mesh.matrixAutoUpdate = false; mesh.updateMatrixWorld(true); const imageObject = { mesh, material, wasDisposed: false }; if (hasUrl) { // only trigger texture fetching once when the mesh is visible mesh.onBeforeRender = async () => { mesh.onBeforeRender = () => {}; try { const texture = await Fetcher.texture(url, { flipY: true }); if (imageObject.wasDisposed) { texture.dispose(); } else { texture.generateMipmaps = true; texture.colorSpace = SRGBColorSpace; material.map = texture; material.visible = true; material.transparent = actualOpacity < 1; material.opacity = actualOpacity; material.needsUpdate = true; this.notifyChange(this); } } catch (error) { material.color = new Color('red'); material.visible = true; material.opacity = 1; console.error(`Failed to load texture "${imageSource.imageUrl}": `, error); this.notifyChange(this); } }; } else { material.color = this._wireframes.material.color; material.opacity = actualOpacity; } return imageObject; } } export default ImageCollectionBase;