Problem Overview
When working with 3D building models in Cesium, positioning billboards accurately on top of structures requires precise geometric calculations. The standard bounding sphere appraoch often places billboards at incorrect heights, as it doesn't conform to the actual shape of the building.
Initial Approach and Issue
The initial implementation used Cesium's bounding sphere to determine billboard placement:
viewer.screenSpaceEventHandler.setInputAction((event) => {
const pickedFeature = viewer.scene.pick(event.position)
const tileset = pickedFeature.tileset
const centerCartographic = Cesium.Cartographic.fromCartesian(tileset.boundingSphere.center)
const positionCartographic = new Cesium.Cartographic(
centerCartographic.longitude,
centerCartographic.latitude,
centerCartographic.height + tileset.boundingSphere.radius
)
const positionCartesian3 = Cesium.Cartographic.toCartesian(positionCartographic)
viewer.entities.add({
id: 'BillboardPopup',
name: 'Model Information',
position: positionCartesian3,
billboard: {
image: 'info.png',
verticalOrigin: Cesium.VerticalOrigin.BOTTOM
}
})
})
This approach resulted in billboards floating above the building rather than sitting on its roof surface, as the bounding sphere doesn't conform to the building's actual geometry.
Soultion: Using Oriented Bounding Boxes
The solution involves utilizing OrientedBoundingBox (OBB) wich provides a more accurate representation of the 3D model's boundaries. The OBB contains half-axes vectors that define the model's local coordinate system.
Step 1: Extracting OBB Vectors
First, we need to extract the three axis vectors from the OBB's halfAxes matrix:
function extractOBBVectors(tileset) {
const obb = tileset.root.boundingVolume.boundingVolume
const halfAxes = obb.halfAxes
const xAxis = new Cesium.Cartesian3()
const yAxis = new Cesium.Cartesian3()
const zAxis = new Cesium.Cartesian3()
Cesium.Matrix3.getColumn(halfAxes, 0, xAxis)
Cesium.Matrix3.getColumn(halfAxes, 1, yAxis)
Cesium.Matrix3.getColumn(halfAxes, 2, zAxis)
return { xAxis, yAxis, zAxis, center: obb.center }
}
Step 2: Calculating Roof Position
With the OBB vectors, we can calculate the exact position on the building's roof by adding the z-axis vector to the center point:
function calculateRoofPosition(tileset) {
const { xAxis, yAxis, zAxis, center } = extractOBBVectors(tileset)
const roofPosition = new Cesium.Cartesian3()
Cesium.Cartesian3.add(center, zAxis, roofPosition)
return roofPosition
}
Step 3: Updated Billboard Placement
The revised implementation uses the roof position for billboard placement:
viewer.screenSpaceEventHandler.setInputAction((event) => {
const pickedFeature = viewer.scene.pick(event.position)
const tileset = pickedFeature.tileset
const roofPosition = calculateRoofPosition(tileset)
viewer.entities.add({
id: 'BillboardPopup',
name: 'Model Information',
position: roofPosition,
billboard: {
image: 'info.png',
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
heightReference: Cesium.HeightReference.NONE
}
})
})
Visualizing the OBB
To better understand the OBB, we can visualize its corners and local coordinate system:
Displaying OBB Corners
function displayOBBCorners(viewer, tileset) {
const obb = tileset.root.boundingVolume.boundingVolume
const halfAxes = obb.halfAxes
const center = obb.center
const xAxis = new Cesium.Cartesian3()
const yAxis = new Cesium.Cartesian3()
const zAxis = new Cesium.Cartesian3()
Cesium.Matrix3.getColumn(halfAxes, 0, xAxis)
Cesium.Matrix3.getColumn(halfAxes, 1, yAxis)
Cesium.Matrix3.getColumn(halfAxes, 2, zAxis)
// Calculate all 8 corners
const corners = [
Cesium.Cartesian3.subtract(
Cesium.Cartesian3.subtract(
Cesium.Cartesian3.subtract(center, xAxis, new Cesium.Cartesian3()),
yAxis, new Cesium.Cartesian3()
),
zAxis, new Cesium.Cartesian3()
),
Cesium.Cartesian3.add(
Cesium.Cartesian3.subtract(
Cesium.Cartesian3.subtract(center, xAxis, new Cesium.Cartesian3()),
yAxis, new Cesium.Cartesian3()
),
zAxis, new Cesium.Cartesian3()
),
Cesium.Cartesian3.add(
Cesium.Cartesian3.add(
Cesium.Cartesian3.subtract(center, xAxis, new Cesium.Cartesian3()),
yAxis, new Cesium.Cartesian3()
),
zAxis, new Cesium.Cartesian3()
),
Cesium.Cartesian3.subtract(
Cesium.Cartesian3.add(
Cesium.Cartesian3.subtract(center, xAxis, new Cesium.Cartesian3()),
yAxis, new Cesium.Cartesian3()
),
zAxis, new Cesium.Cartesian3()
),
Cesium.Cartesian3.subtract(
Cesium.Cartesian3.subtract(
Cesium.Cartesian3.add(center, xAxis, new Cesium.Cartesian3()),
yAxis, new Cesium.Cartesian3()
),
zAxis, new Cesium.Cartesian3()
),
Cesium.Cartesian3.add(
Cesium.Cartesian3.subtract(
Cesium.Cartesian3.add(center, xAxis, new Cesium.Cartesian3()),
yAxis, new Cesium.Cartesian3()
),
zAxis, new Cesium.Cartesian3()
),
Cesium.Cartesian3.add(
Cesium.Cartesian3.add(
Cesium.Cartesian3.add(center, xAxis, new Cesium.Cartesian3()),
yAxis, new Cesium.Cartesian3()
),
zAxis, new Cesium.Cartesian3()
),
Cesium.Cartesian3.subtract(
Cesium.Cartesian3.add(
Cesium.Cartesian3.add(center, xAxis, new Cesium.Cartesian3()),
yAxis, new Cesium.Cartesian3()
),
zAxis, new Cesium.Cartesian3()
)
]
// Add corner points to viewer
corners.forEach((corner, index) => {
viewer.entities.add({
position: corner,
point: {
color: Cesium.Color.RED,
pixelSize: 10
},
label: {
text: (index + 1).toString(),
font: '28px sans-serif',
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
fillColor: Cesium.Color.BLUE,
outlineColor: Cesium.Color.WHITE,
outlineWidth: 2
}
})
})
}
Displaying Local Coordinate Axes
function displayLocalAxes(viewer, tileset) {
const obb = tileset.root.boundingVolume.boundingVolume
const halfAxes = obb.halfAxes
const center = obb.center
const xAxis = new Cesium.Cartesian3()
const yAxis = new Cesium.Cartesian3()
const zAxis = new Cesium.Cartesian3()
Cesium.Matrix3.getColumn(halfAxes, 0, xAxis)
Cesium.Matrix3.getColumn(halfAxes, 1, yAxis)
Cesium.Matrix3.getColumn(halfAxes, 2, zAxis)
// X-axis (Red)
viewer.entities.add({
position: center,
polyline: {
positions: [center, Cesium.Cartesian3.add(center, xAxis, new Cesium.Cartesian3())],
width: 4,
material: Cesium.Color.RED
}
})
// Y-axis (Green)
viewer.entities.add({
position: center,
polyline: {
positions: [center, Cesium.Cartesian3.add(center, yAxis, new Cesium.Cartesian3())],
width: 4,
material: Cesium.Color.GREEN
}
})
// Z-axis (Blue)
viewer.entities.add({
position: center,
polyline: {
positions: [center, Cesium.Cartesian3.add(center, zAxis, new Cesium.Cartesian3())],
width: 4,
material: Cesium.Color.BLUE
}
})
}
Displaying the OBB Box
function displayOrientedBoundingBox(viewer, tileset) {
const obb = tileset.root.boundingVolume.boundingVolume
const halfAxes = obb.halfAxes
const center = obb.center
const xAxis = new Cesium.Cartesian3()
const yAxis = new Cesium.Cartesian3()
const zAxis = new Cesium.Cartesian3()
Cesium.Matrix3.getColumn(halfAxes, 0, xAxis)
Cesium.Matrix3.getColumn(halfAxes, 1, yAxis)
Cesium.Matrix3.getColumn(halfAxes, 2, zAxis)
const length = Cesium.Cartesian3.distance(Cesium.Cartesian3.add(center, xAxis, new Cesium.Cartesian3()), center) * 2
const width = Cesium.Cartesian3.distance(Cesium.Cartesian3.add(center, yAxis, new Cesium.Cartesian3()), center) * 2
const height = Cesium.Cartesian3.distance(Cesium.Cartesian3.add(center, zAxis, new Cesium.Cartesian3()), center) * 2
viewer.entities.add({
position: center,
box: {
dimensions: new Cesium.Cartesian3(length, width, height),
material: Cesium.Color.BLUEVIOLET.withAlpha(0.2),
outline: true,
outlineColor: Cesium.Color.CYAN
}
})
}
Understanding Matrix3 Operations
The Cesium.Matrix3.getColumn method retrieves a column from the matrix at the specified index. Despite the matrix being defined in row-major order for readability, the getColumn function accesses columns as expected. This behavior is documented in the Cesium API reference, where it's specified that the method retrieves a copy of the matrix column at the provided index.
When working with OrientedBoundingBox, the three columns of the halfAxes matrix represent the local x, y, and z axes of the 3D model, allowing for precise geometric calculations relative to the model's orientation.