Parameters
Displays a panoramic bubble.
The SphericalPanorama
entity can display images in the 'equirectangular'
projection.
import { AxesHelper, Group, PolarGridHelper, Vector3 } from "three";
import { Feature } from "ol";
import { LineString, Point } from "ol/geom.js";
import { Circle, Fill, Stroke, Style } from "ol/style.js";
import FirstPersonControls from "@giro3d/giro3d/controls/FirstPersonControls.js";
import Ellipsoid from "@giro3d/giro3d/core/geographic/Ellipsoid.js";
import Extent from "@giro3d/giro3d/core/geographic/Extent.js";
import Instance from "@giro3d/giro3d/core/Instance.js";
import ColorLayer from "@giro3d/giro3d/core/layer/ColorLayer.js";
import SphericalPanorama from "@giro3d/giro3d/entities/SphericalPanorama.js";
import Inspector from "@giro3d/giro3d/gui/Inspector.js";
import EllipsoidHelper from "@giro3d/giro3d/helpers/EllipsoidHelper.js";
import StaticImageSource from "@giro3d/giro3d/sources/StaticImageSource.js";
import VectorSource from "@giro3d/giro3d/sources/VectorSource.js";
function bindButton(id, onClick) {
const element = document.getElementById(id);
if (!(element instanceof HTMLButtonElement)) {
throw new Error(
"invalid binding element: expected HTMLButtonElement, got: " +
element.constructor.name,
);
}
element.onclick = () => {
onClick(element);
};
return element;
}
function bindNumberInput(id, onChange) {
const element = document.getElementById(id);
if (!(element instanceof HTMLInputElement)) {
throw new Error(
"invalid binding element: expected HTMLInputElement, got: " +
element.constructor.name,
);
}
element.onchange = () => {
onChange(element.valueAsNumber);
};
const callback = (v) => {
element.value = v.toString();
onChange(element.valueAsNumber);
};
return [callback, element.valueAsNumber, element];
}
function bindToggle(id, onChange) {
const element = document.getElementById(id);
if (!(element instanceof HTMLInputElement)) {
throw new Error(
"invalid binding element: expected HTMLButtonElement, got: " +
element.constructor.name,
);
}
element.oninput = function oninput() {
onChange(element.checked);
};
const callback = (v) => {
element.checked = v;
onChange(element.checked);
};
return [callback, element.checked, element];
}
const instance = new Instance({
target: "view",
crs: "EPSG:4978",
});
const ellipsoid = Ellipsoid.WGS84.scale(0.0001);
const ellipsoidHelper = new EllipsoidHelper({
ellipsoid,
parallels: 91,
meridians: 180,
segments: 64,
});
instance.add(ellipsoidHelper);
const radius = 5;
const panorama = new SphericalPanorama({ depthTest: true, radius });
instance.add(panorama);
const source = new StaticImageSource({
source: "https://3d.oslandia.com/giro3d/images/panorama.jpg",
// Since the image is covering the entire sphere, we must express the extent as such
extent: Extent.fullEquirectangularProjection,
});
const panoramicLayer = new ColorLayer({
name: "panorama",
source,
});
panorama.addLayer(panoramicLayer).catch((e) => console.error(e));
// Let's create a vector layer with various geometries to help us navigate in the panoramic image.
const debugLayer = new ColorLayer({
name: "debug",
source: new VectorSource({
data: [
// Center of image
new Feature(new Point([0, 0])),
// Equator of image
new Feature(
new LineString([
[-180, 0],
[+180, 0],
]),
),
// Prime meridian of image
new Feature(
new LineString([
[0, -90],
[0, +90],
]),
),
],
style: new Style({
stroke: new Stroke({ color: "yellow" }),
image: new Circle({
radius: 8,
fill: new Fill({ color: "yellow" }),
stroke: new Stroke({ color: "orange" }),
}),
}),
}),
});
debugLayer.visible = false;
panorama.addLayer(debugLayer).catch((e) => console.error(e));
const view = instance.view;
const camera = view.camera;
// Set camera at the center of the panorama sphere
camera.position.set(0, 0, 0);
// Look at the center of the panoramic image
camera.lookAt(new Vector3(0, 1, 0));
const controls = new FirstPersonControls(instance, { focusOnMouseOver: true });
controls.options.moveSpeed = 5;
instance.domElement.focus();
controls.reset();
instance.addEventListener("after-camera-update", () => controls.reset());
instance.notifyChange(panorama);
// Let's configure the panorama graticule with 1° step to
// help us visualize the rotation of the sphere.
panorama.graticule.xStep = 1;
panorama.graticule.yStep = 1;
panorama.graticule.color = "#f6d32d";
panorama.graticule.opacity = 0.25;
panorama.graticule.thickness = 0.2;
camera.fov = 60;
const axes = new AxesHelper(7);
panorama.object3d.add(axes);
Inspector.attach("inspector", instance);
const params = {
latitude: 0,
longitude: 0,
heading: 0,
pitch: 0,
roll: 0,
};
const horizontalGrid = new PolarGridHelper(radius, 18, 4, 64, "red", "red");
horizontalGrid.rotateX(-Math.PI / 2);
horizontalGrid.updateMatrixWorld(true);
const verticalGrid = new PolarGridHelper(radius, 18, 4, 64, "blue", "blue");
verticalGrid.rotateZ(-Math.PI / 2);
verticalGrid.updateMatrixWorld(true);
axes.visible = false;
verticalGrid.visible = false;
horizontalGrid.visible = false;
const helperGroup = new Group();
helperGroup.add(verticalGrid, horizontalGrid);
instance.add(helperGroup);
const updateOrientation = () => {
panorama.setOrientation({
heading: params.heading,
pitch: params.pitch,
roll: params.roll,
});
instance.notifyChange(panorama);
};
const updatePosition = () => {
// Compute the cartesian coordinates from the geographic coordinates
const position = ellipsoid.toCartesian(params.latitude, params.longitude, 0);
panorama.object3d.position.copy(position);
panorama.object3d.updateMatrixWorld(true);
// Update the camera up vector to match the normal of the ellipsoid at our location
// Useful for navigation controls to know where "up" is.
instance.view.camera.up = ellipsoid.getNormalFromCartesian(position);
// Get the local rotation matrix that matches the normal vector
const localMatrix = ellipsoid.getEastNorthUpMatrixFromCartesian(position);
helperGroup.position.copy(position);
helperGroup.setRotationFromMatrix(localMatrix);
helperGroup.updateMatrixWorld(true);
updateOrientation();
instance.notifyChange(panorama);
};
updatePosition();
instance.view.goTo(panorama);
bindNumberInput("latitude", (latitude) => {
params.latitude = latitude;
updatePosition();
});
bindNumberInput("longitude", (longitude) => {
params.longitude = longitude;
updatePosition();
});
bindNumberInput("azimuth", (azimuth) => {
params.heading = azimuth;
updateOrientation();
});
bindNumberInput("pitch", (pitch) => {
params.pitch = pitch;
updateOrientation();
});
bindNumberInput("roll", (roll) => {
params.roll = roll;
updateOrientation();
});
bindToggle("graticule", (show) => {
panorama.graticule.enabled = show;
instance.notifyChange(panorama);
});
bindToggle("show-rotation-helpers", (show) => {
horizontalGrid.visible = show;
verticalGrid.visible = show;
axes.visible = show;
instance.notifyChange();
});
bindToggle("show-debug-layer", (show) => {
debugLayer.visible = show;
instance.notifyChange(debugLayer);
});
bindButton("go-to-panorama", () => {
instance.view.goTo(panorama);
});
<!doctype html>
<html lang="en">
<head>
<title>360° panoramic image</title>
<meta charset="UTF-8" />
<meta name="name" content="360_panoramic_image" />
<meta name="description" content="Displays a panoramic bubble." />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="https://giro3d.org/images/favicon.svg" />
<link
href="https://giro3d.org/assets/bootstrap-custom.css"
rel="stylesheet"
/>
<script src="https://giro3d.org/assets/bootstrap.bundle.min.js"></script>
<link
rel="stylesheet"
type="text/css"
href="https://giro3d.org/latest/examples/css/example.css"
/>
</head>
<body>
<div id="view" class="m-0 p-0 w-100 h-100"></div>
<div
id="inspector"
class="position-absolute top-0 start-0 mh-100 overflow-auto"
></div>
<div class="side-pane-with-status-bar">
<div class="card">
<div class="card-header">Parameters</div>
<div class="card-body">
<!-- Show/Hide graticule -->
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
role="switch"
id="graticule"
autocomplete="off"
/>
<label class="form-check-label" for="graticule"
>Show graticule</label
>
</div>
<!-- Show/Hide debug layer -->
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
role="switch"
id="show-debug-layer"
autocomplete="off"
/>
<label class="form-check-label" for="show-debug-layer"
>Show debug layer</label
>
</div>
<!-- Show/Hide rotation helpers -->
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
role="switch"
id="show-rotation-helpers"
autocomplete="off"
/>
<label class="form-check-label" for="show-rotation-helpers"
>Show rotation helpers</label
>
</div>
<hr />
<!-- Latitude -->
<label for="latitude" class="form-label">Latitude</label>
<div class="input-group">
<input
type="number"
min="-180"
max="180"
value="0"
step="1"
class="form-control"
id="latitude"
autocomplete="off"
/>
</div>
<!-- Longitude -->
<label for="longitude" class="form-label">Longitude</label>
<div class="input-group">
<input
type="number"
min="-90"
max="90"
value="0"
step="1"
class="form-control"
id="longitude"
autocomplete="off"
/>
</div>
<!-- Azimuth -->
<label for="azimuth" class="form-label">Azimuth</label>
<div class="input-group">
<input
type="number"
min="0"
max="360"
value="0"
step="1"
class="form-control"
id="azimuth"
autocomplete="off"
/>
</div>
<!-- Pitch -->
<label for="pitch" class="form-label">Pitch</label>
<div class="input-group">
<input
type="number"
min="-90"
max="90"
value="0"
step="1"
class="form-control"
id="pitch"
autocomplete="off"
/>
</div>
<!-- Roll -->
<label for="roll" class="form-label">Roll</label>
<div class="input-group">
<input
type="number"
min="-90"
max="90"
value="0"
step="1"
class="form-control"
id="roll"
autocomplete="off"
/>
</div>
<!-- Go to panorama -->
<button
type="button"
class="btn btn-primary w-100 mt-3"
id="go-to-panorama"
>
<i class="bi bi-cursor-fill"></i>
Go to panorama
</button>
</div>
</div>
</div>
<script type="module" src="index.js"></script>
<script>
/* activate popovers */
const popoverTriggerList = [].slice.call(
document.querySelectorAll('[data-bs-toggle="popover"]'),
);
popoverTriggerList.map(
// bootstrap is used as script in the template, disable warning about undef
// eslint-disable-next-line no-undef
(popoverTriggerEl) =>
new bootstrap.Popover(popoverTriggerEl, {
trigger: "hover",
placement: "left",
content: document.getElementById(
popoverTriggerEl.getAttribute("data-bs-content"),
).innerHTML,
html: true,
}),
);
</script>
</body>
</html>
{
"name": "360_panoramic_image",
"dependencies": {
"@giro3d/giro3d": "0.43.1"
},
"devDependencies": {
"vite": "^3.2.3"
},
"scripts": {
"start": "vite",
"build": "vite build"
}
}