Clip various entities with clipping planes.
Giro3D entities support three.js's clipping planes. To add one or more clipping planes to an entity, use the Entity3D.clippingPlanes
property. In this examples, we create a cube-shaped clipping volume by combining 6 clipping planes (one for each side of the cube).
import {
Box3,
Box3Helper,
BoxGeometry,
DoubleSide,
Group,
Mesh,
MeshBasicMaterial,
Plane,
Vector3,
} from "three";
import { MapControls } from "three/examples/jsm/controls/MapControls.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 ColorMap from "@giro3d/giro3d/core/ColorMap.js";
import ElevationLayer from "@giro3d/giro3d/core/layer/ElevationLayer.js";
import Map from "@giro3d/giro3d/entities/Map.js";
import Tiles3D from "@giro3d/giro3d/entities/Tiles3D.js";
import BilFormat from "@giro3d/giro3d/formats/BilFormat.js";
import Inspector from "@giro3d/giro3d/gui/Inspector.js";
import DrawTool from "@giro3d/giro3d/interactions/DrawTool.js";
import { MODE } from "@giro3d/giro3d/renderer/PointCloudMaterial.js";
import WmtsSource from "@giro3d/giro3d/sources/WmtsSource.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 bindDropDown(id, onChange) {
const element = document.getElementById(id);
if (!(element instanceof HTMLSelectElement)) {
throw new Error(
"invalid binding element: expected HTMLSelectElement, got: " +
element.constructor.name,
);
}
element.onchange = () => {
onChange(element.value);
};
const callback = (v) => {
element.value = v;
onChange(element.value);
};
const setOptions = (options) => {
const items = options.map(
(opt) =>
`<option value=${opt.id} ${opt.selected ? "selected" : ""}>${opt.name}</option>`,
);
element.innerHTML = items.join("\n");
};
return [callback, element.value, element, setOptions];
}
function bindSlider(id, onChange) {
const element = document.getElementById(id);
if (!(element instanceof HTMLInputElement)) {
throw new Error(
"invalid binding element: expected HTMLInputElement, got: " +
element.constructor.name,
);
}
element.oninput = function oninput() {
onChange(element.valueAsNumber);
};
const setValue = (v, min, max, step) => {
if (min != null && max != null) {
element.min = min.toString();
element.max = max.toString();
if (step != null) {
element.step = step;
}
}
element.valueAsNumber = v;
onChange(element.valueAsNumber);
};
const initialValue = element.valueAsNumber;
return [setValue, initialValue, 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];
}
function makeColorRamp(
preset,
discrete = false,
invert = false,
mirror = false,
) {
let nshades = discrete ? 10 : 256;
const values = colormap({ colormap: preset, nshades });
const colors = values.map((v) => new Color(v));
if (invert) {
colors.reverse();
}
if (mirror) {
const mirrored = [...colors, ...colors.reverse()];
return mirrored;
}
return colors;
}
Instance.registerCRS(
"EPSG:2154",
"+proj=lcc +lat_0=46.5 +lon_0=3 +lat_1=49 +lat_2=44 +x_0=700000 +y_0=6600000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs +type=crs",
);
Instance.registerCRS(
"IGNF:WGS84G",
'GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]]',
);
const instance = new Instance({
target: "view",
crs: "EPSG:2154",
});
instance.renderingOptions.enableEDL = true;
const controls = new MapControls(instance.view.camera, instance.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.2;
instance.view.setControls(controls);
const pointCloud = new Tiles3D({
url: "https://3d.oslandia.com/lidar_hd/tileset.json",
pointCloudMode: MODE.ELEVATION,
colorMap: new ColorMap({
colors: makeColorRamp("rdbu").reverse(),
min: 200,
max: 1800,
}),
});
instance.add(pointCloud);
let planes = null;
let boxSize = 3000;
function getPlanesFromBoxSides(box) {
const result = [];
// Notice that when the plane has a positive normal, the distance to the box must be negated
result.push(new Plane(new Vector3(0, 0, +1), -box.min.z));
result.push(new Plane(new Vector3(0, 0, -1), +box.max.z));
result.push(new Plane(new Vector3(+1, 0, 0), -box.min.x));
result.push(new Plane(new Vector3(-1, 0, 0), +box.max.x));
result.push(new Plane(new Vector3(0, +1, 0), -box.min.y));
result.push(new Plane(new Vector3(0, -1, 0), +box.max.y));
return result;
}
const extent = new Extent(
"EPSG:2154",
902000.3307342547,
927999.9889373797,
6444999.999618538,
6466999.990463264,
);
const options = {
showHelper: true,
enableClippingPlanes: true,
applyOnMap: false,
showMap: true,
applyOnPointCloud: true,
showPointCloud: true,
mode: "slice",
};
// create a map
const map = new Map({
extent,
lighting: false,
discardNoData: true,
side: DoubleSide,
});
instance.add(map);
const noDataValue = -1000;
const capabilitiesUrl =
"https://data.geopf.fr/wmts?SERVICE=WMTS&VERSION=1.0.0&REQUEST=GetCapabilities";
WmtsSource.fromCapabilities(capabilitiesUrl, {
layer: "ELEVATION.ELEVATIONGRIDCOVERAGE",
format: new BilFormat(),
noDataValue,
})
.then((elevationWmts) => {
map.addLayer(
new ElevationLayer({
name: "wmts_elevation",
extent: map.extent,
// We don't need the full resolution of terrain because we are not using any shading
resolutionFactor: 0.25,
minmax: { min: 0, max: 5000 },
noDataOptions: {
replaceNoData: false,
},
source: elevationWmts,
}),
);
})
.catch(console.error);
WmtsSource.fromCapabilities(capabilitiesUrl, {
layer: "HR.ORTHOIMAGERY.ORTHOPHOTOS",
})
.then((orthophotoWmts) => {
map.addLayer(
new ColorLayer({
name: "wmts_orthophotos",
extent: map.extent,
source: orthophotoWmts,
}),
);
})
.catch(console.error);
const box3 = new Box3();
const center = map.extent.centerAsVector2();
const boxCenter = new Vector3(center.x, center.y, 800);
const volumeHelpers = new Group();
instance.scene.add(volumeHelpers);
let helper;
let box;
const helperMaterial = new MeshBasicMaterial({
color: "yellow",
opacity: 0.1,
transparent: true,
});
function deleteBox() {
box?.geometry?.dispose();
box?.removeFromParent();
helper?.dispose();
helper?.removeFromParent();
}
function generateBoxHelper() {
deleteBox();
box3.setFromCenterAndSize(boxCenter, new Vector3(boxSize, boxSize, boxSize));
const boxGeometry = new BoxGeometry(boxSize, boxSize, boxSize);
box = new Mesh(boxGeometry, helperMaterial);
helper = new Box3Helper(box3, "yellow");
box.renderOrder = 2;
volumeHelpers.add(helper);
volumeHelpers.add(box);
box.position.copy(boxCenter);
box.updateMatrixWorld();
helper.updateMatrixWorld();
volumeHelpers.updateMatrixWorld();
}
// refresh scene
instance.notifyChange(instance.view.camera);
function update() {
volumeHelpers.visible = options.showHelper && options.enableClippingPlanes;
map.visible = options.showMap;
pointCloud.visible = options.showPointCloud;
map.clippingPlanes =
options.enableClippingPlanes && options.applyOnMap ? planes : null;
pointCloud.clippingPlanes =
options.enableClippingPlanes && options.applyOnPointCloud ? planes : null;
instance.notifyChange();
}
const updateFromBox = () => {
generateBoxHelper();
planes = getPlanesFromBoxSides(box3);
update();
};
let currentSegment;
bindDropDown("mode", (mode) => {
options.mode = mode;
const volumeOptions = document.getElementById("volume-options");
const sliceOptions = document.getElementById("slice-options");
volumeOptions.style.display = "block";
sliceOptions.style.display = "block";
switch (mode) {
case "slice":
volumeOptions.style.display = "none";
planes = [];
deleteBox();
update();
break;
case "volume":
sliceOptions.style.display = "none";
if (currentSegment) {
instance.remove(currentSegment);
currentSegment = null;
}
updateFromBox();
break;
}
});
const drawTool = new DrawTool({ instance });
drawTool.addEventListener("start-drag", () => (controls.enabled = false));
drawTool.addEventListener("end-drag", () => (controls.enabled = true));
drawTool.enterEditMode();
bindButton("draw", () => {
const plane = new Plane();
if (currentSegment) {
instance.remove(currentSegment);
currentSegment = null;
}
const updatePlanes = (shape) => {
if (shape && shape.points.length === 2) {
const a = shape.points[0];
const b = shape.points[1];
const c = shape.points[1].clone().setZ(b.z + 100);
plane.setFromCoplanarPoints(a, b, c);
planes = [plane];
update();
}
};
drawTool
.createSegment({
onTemporaryPointMoved: updatePlanes,
afterUpdatePoint: ({ shape }) => updatePlanes(shape),
})
.then((shape) => {
currentSegment = shape;
updatePlanes(shape);
});
});
bindToggle("toggle-show-volume", (v) => {
options.showHelper = v;
updateFromBox();
});
bindToggle("toggle-pointcloud", (v) => {
options.applyOnPointCloud = v;
update();
});
bindToggle("toggle-show-pointcloud", (v) => {
options.showPointCloud = v;
update();
});
bindToggle("toggle-show-map", (v) => {
options.showMap = v;
update();
});
bindToggle("toggle-map", (v) => {
options.applyOnMap = v;
update();
});
bindSlider("slider-size", (v) => {
boxSize = v;
updateFromBox();
});
// Configure camera
const lookAt = new Vector3(915833, 6455879, 121);
instance.view.camera.position.set(909914, 6448629, 7925);
instance.view.camera.lookAt(lookAt);
controls.target.copy(lookAt);
controls.saveState();
Inspector.attach("inspector", instance);
<!doctype html>
<html lang="en">
<head>
<title>Clipping planes</title>
<meta charset="UTF-8" />
<meta name="name" content="clipping_planes" />
<meta
name="description"
content="Clip various entities with clipping planes."
/>
<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">
<fieldset id="options">
<!-- Mode -->
<div class="input-group mb-2">
<label class="input-group-text" for="mode">Mode</label>
<select class="form-select" id="mode" autocomplete="off">
<option value="slice" selected>Slice</option>
<option value="volume">Volume</option>
</select>
</div>
<div id="volume-options" class="my-3" style="display: none">
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
checked="true"
role="switch"
id="toggle-show-volume"
autocomplete="off"
/>
<label class="form-check-label" for="toggle-show-volume"
>Show volume</label
>
</div>
<label for="slider-size" class="form-label">Volume size</label>
<div class="input-group">
<input
type="range"
min="1"
max="30000"
value="3000"
step="10"
class="form-range"
id="slider-size"
autocomplete="off"
/>
</div>
</div>
<div id="slice-options" class="my-3">
<button class="btn btn-primary w-100" id="draw">
<i class="bi bi-pencil-fill"></i> Draw cross-section
</button>
</div>
<hr />
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
checked="true"
role="switch"
id="toggle-show-map"
autocomplete="off"
/>
<label class="form-check-label" for="toggle-show-map"
>Show Map</label
>
</div>
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
checked="true"
role="switch"
id="toggle-show-pointcloud"
autocomplete="off"
/>
<label class="form-check-label" for="toggle-show-pointcloud"
>Show point cloud</label
>
</div>
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
role="switch"
id="toggle-map"
/>
<label class="form-check-label" for="toggle-map"
>Apply clipping planes on map</label
>
</div>
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
checked="true"
role="switch"
id="toggle-pointcloud"
autocomplete="off"
/>
<label class="form-check-label" for="toggle-pointcloud"
>Apply clipping planes on point cloud</label
>
</div>
</fieldset>
</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": "clipping_planes",
"dependencies": {
"@giro3d/giro3d": "0.42.0"
},
"devDependencies": {
"vite": "^3.2.3"
},
"scripts": {
"start": "vite",
"build": "vite build"
}
}