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 colormap from "colormap";
import {
Vector3,
Mesh,
BoxGeometry,
MeshBasicMaterial,
Box3,
Box3Helper,
Group,
Plane,
DoubleSide,
} from "three";
import { MapControls } from "three/examples/jsm/controls/MapControls.js";
import Instance from "@giro3d/giro3d/core/Instance.js";
import ColorLayer from "@giro3d/giro3d/core/layer/ColorLayer.js";
import Tiles3D from "@giro3d/giro3d/entities/Tiles3D.js";
import Tiles3DSource from "@giro3d/giro3d/sources/Tiles3DSource.js";
import Inspector from "@giro3d/giro3d/gui/Inspector.js";
import Extent from "@giro3d/giro3d/core/geographic/Extent.js";
import ElevationLayer from "@giro3d/giro3d/core/layer/ElevationLayer.js";
import BilFormat from "@giro3d/giro3d/formats/BilFormat.js";
import Map from "@giro3d/giro3d/entities/Map.js";
import PointCloudMaterial, {
MODE,
} from "@giro3d/giro3d/renderer/PointCloudMaterial.js";
import DrawTool from "@giro3d/giro3d/interactions/DrawTool.js";
import WmtsSource from "@giro3d/giro3d/sources/WmtsSource.js";
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;
}
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 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 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 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];
}
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 material = new PointCloudMaterial({
size: 4,
mode: MODE.ELEVATION,
});
material.colorMap.colors = makeColorRamp("rdbu").reverse();
material.colorMap.min = 200;
material.colorMap.max = 1800;
const pointCloud = new Tiles3D(
new Tiles3DSource("https://3d.oslandia.com/lidar_hd/tileset.json"),
{
material,
},
);
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,
hillshading: 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.HIGHRES",
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.41.0"
},
"devDependencies": {
"vite": "^3.2.3"
},
"scripts": {
"start": "vite",
"build": "vite build"
}
}