Displays a panoramic bubble.

+Z
Giro3D version
THREE.js version
OpenLayers version
CRS
Memory usage (CPU)
Memory usage (GPU)
Frames
Clear color
Clear alpha
Status
Local clipping enabled
Capabilities
WebGL 2
Max texture units
Max texture size
Precision
Max fragment shader uniforms
Logarithmic depth buffer
Max shader attributes
Check shader errors
EXT_clip_control
EXT_color_buffer_float
EXT_color_buffer_half_float
EXT_conservative_depth
EXT_depth_clamp
EXT_float_blend
EXT_polygon_offset_clamp
EXT_texture_compression_bptc
EXT_texture_compression_rgtc
EXT_texture_filter_anisotropic
EXT_texture_mirror_clamp_to_edge
EXT_texture_norm16
NV_shader_noperspective_interpolation
OES_draw_buffers_indexed
OES_sample_variables
OES_shader_multisample_interpolation
OES_texture_float_linear
OVR_multiview2
WEBGL_clip_cull_distance
WEBGL_compressed_texture_astc
WEBGL_compressed_texture_etc
WEBGL_compressed_texture_etc1
WEBGL_compressed_texture_s3tc
WEBGL_compressed_texture_s3tc_srgb
WEBGL_debug_renderer_info
WEBGL_debug_shaders
WEBGL_lose_context
WEBGL_multi_draw
WEBGL_polygon_mode
WEBGL_stencil_texturing
MSAA
EDL
EDL Radius
EDL Strength
Inpainting
Inpainting steps
Inpainting depth contrib.
Point cloud occlusion
Type
FOV
Automatic plane computation
Far plane
Near plane
Max far plane
Min near plane
Width (pixels)
Height (pixels)
Horizon distance
x
y
z
color
Enable cache
Default TTL (seconds)
Capacity (MB)
Capacity (entries)
Entries
Memory usage (approx)
Pending requests
Memory tracker
Identifier
Memory usage (CPU)
Memory usage (GPU)
Status
Render order
Enable
Plane normal X
Plane normal Y
Plane normal Z
Distance
Helper size
Negate plane
👁️ Visible
❄️ Freeze
🪟 Opacity
🔳 Show volumes
Volume color
Discard no-data values
Sidedness
Front
Depth test
Visible tiles
Reachable tiles
Loaded tiles
Complete paints
Cast shadow
Receive shadow
Show grid
Background
Background opacity
Show tiles outlines
Tile outline color
Show extent
Extent color
Show bounding spheres
Subdivision threshold
Show tile info
Image size
Elevation range
Layer info
Deformation
Wireframe
Tile subdivisions
Show collider meshes
Stitching
Enabled
Mode
Hillshade
Hillshade intensity
Z factor
Hillshade zenith
Hillshade azimuth
Elevation layers only
Enable
Color
Opacity
X step
Y step
X Offset
Y Offset
Thickness
Enable
Color
Thickness
Opacity
Primary interval (m)
Secondary interval (m)
Brightness
Contrast
Saturation
Layer count
Render state
Normal
Layers
Identifier
Memory usage (CPU)
Memory usage (GPU)
Name
Source CRS
Status
Resolution factor
Visible
Frozen
Interpretation
Loaded images
Blending mode
Normal
Brightness
Contrast
Saturation
Opacity
Show extent
Extent color
Enabled
Mode
Elevation
Lower bound
Upper bound
Type
Color space
Data type
Flip Y
Synchronous
CRS
Memory usage (CPU)
Memory usage (GPU)
Feature count
Identifier
Memory usage (CPU)
Memory usage (GPU)
Name
Source CRS
Status
Resolution factor
Visible
Frozen
Interpretation
Loaded images
Blending mode
Normal
Brightness
Contrast
Saturation
Opacity
Show extent
Extent color
Enabled
Mode
Elevation
Lower bound
Upper bound
Type
Color space
Data type
Flip Y
Synchronous
CRS
Memory usage (CPU)
Memory usage (GPU)
Show helpers
Show hidden objects
Name filter
Hierarchy
Properties
isObject3D
uuid
name
type
matrixAutoUpdate
matrixWorldAutoUpdate
matrixWorldNeedsUpdate
visible
castShadow
receiveShadow
frustumCulled
renderOrder
x
y
z
x
y
z
Parameters

50% © Daniel Vorndran for Wikimedia Commons (the image has been resized to 4096x2048).

The SphericalPanorama entity can display images in the 'equirectangular' projection.

index.js
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);
});
index.html
<!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>
package.json
{
    "name": "360_panoramic_image",
    "dependencies": {
        "@giro3d/giro3d": "0.43.2"
    },
    "devDependencies": {
        "vite": "^3.2.3"
    },
    "scripts": {
        "start": "vite",
        "build": "vite build"
    }
}