The structure half of the RF solver's profile construction: a pluggable,
camera-independent source of BUILDING and structure heights, rasterized onto
the exact grid the terrain field already occupies.
WHY THIS EXISTS (owner directive 2026-08-08, verbatim):
"Also, I want to see the RF shader work with obstructions like buildings,
etc."
The coverage solve's path profile was terrain only. A 200 m tower block
between a transmitter and a street does not exist as far as ITU-R P.526 is
concerned if the profile it marches is a bare heightfield, so an urban solve
painted a clear link straight through Midtown. The diffraction math already
handles an arbitrary sequence of obstacles; what was missing was the
obstacles. This port supplies them.
WHY IT IS NOT A 3D TILES GEOMETRY READ, which is the first thing anyone
proposes and the reason this file is a port rather than a helper:
1. There is no CPU-side triangle geometry to read. `GltfLoader`'s
`loadAttributesAsTypedArray` is FALSE for b3dm/i3dm content
(`Model/B3dmLoader.js:71-72`, `Model/I3dmLoader.js:84-85`); tile vertex
data is uploaded to GPU buffers and the typed arrays are released. A
tileset in memory is not a heightfield you can index.
2. Every native path that DOES answer "how high is the structure here"
goes through rendering. `Cesium3DTileset.enableCollision`
(`Cesium3DTileset.js:935`, consumed at `:2842`) feeds
`Scene.pickFromRay`, and `Scene.sampleHeightMostDetailed` is a per
position GPU readback. Both depend on what has been drawn, which makes
the answer depend on where the camera is looking — the exact
resident-tile trap
ProviderAccessPort was built to escape, and a
coverage raster that changes when the user pans is not a measurement.
3. A 262,144-sample field cannot afford a readback per sample regardless.
So the obstruction model is VECTOR: footprint polygons carrying heights, the
form the data actually has at its origin (OSM `building` ways with
`height` / `building:levels`, municipal building-footprint datasets, a CAD
export, an operator's own site plan). Rasterizing footprints is deterministic,
camera-independent, costs O(vertices + covered posts), and — because the
SAME footprints can be handed to the native Entity API as extruded polygons —
what the user SEES is provably what the solver READ.
THE ONE-COPY CONTRACT. A source holds its footprint vertices as ECEF
`Float64Array`s built ONCE at construction. A read transforms them into the
request's local ENU frame and scanline-fills the caller's own `Float32Array`.
No per-read object allocation per footprint, no intermediate rasters, and the
output buffer is the one the solver goes on to composite with terrain.
SAMPLING DISCIPLINE, inherited from the terrain half and non-negotiable:
- The obstruction field is rasterized on the SAME posts, at the SAME
spacing, in the SAME frame as the terrain field it will be composited
with. Two fields sampled differently disagree, and the solver's worst
historical defect (2026-08-08) was exactly that disagreement.
- A post is inside a footprint or it is not. There is NO dilation, no
max-of-neighbours, no "grow by one cell to be safe". A max filter over
posts is a morphological dilation that widens every structure by a cell
in all four directions and biases every path pessimistic; it was removed
from the terrain path for that reason and it is not coming back here.
- Structures smaller than a post are not resolved. That is a RESOLUTION
statement, reported in the descriptor as such, not a licence to smear.
| Name | Type | Description | ||||||
|---|---|---|---|---|---|---|---|---|
options |
object |
optional
|
Example:
const port = new Cesium.RfObstructionPort({
sources: [Cesium.RfObstructionPort.fromGeoJson(buildingFootprints)],
});
const coverage = await Cesium.Analysis.computeRfCoverage({
scene, txPosition, obstructions: port, profileResolution: 512,
});
See:
Members
What a source's heights are measured from.
ABOVE_GROUND is the default because it is what building data actually
carries: OSM `height` and `building:levels`, a municipal footprint layer's
storey count, a site plan's parapet height — all of them are above the ground
the building stands on, and the port composites them onto the terrain field
the solver has already sampled. ELLIPSOID exists for the sources that are
genuinely absolute (a photogrammetric DSM, a surveyed antenna tip).
No-data sentinel for the obstruction field: a post with no structure over it.
-FLT_MAX, NOT the -DBL_MAX
ProviderAccessPort uses. The two ports
follow the same reasoning — a sentinel must have exactly one encoding and
must never be a real height — but they carry different buffers, and copying
the constant across without checking the type is a silent, total failure:
this field is a `Float32Array`, because it composites post for post with the
`Float32Array` terrain field, and `-Number.MAX_VALUE` is not representable in
f32. Writing it rounds to `-Infinity`, after which EVERY
`value === NO_DATA` test in the port is false, every post reads as covered,
and the provenance reports 100 % of a city with no buildings in it. That is
not a hypothetical — it is what this constant was on first run, caught by the
empty-port test asserting the sentinel rather than assuming it.
Total footprint count across every registered source.
A deterministic fingerprint of the registered sources.
The RF solver caches a whole coverage state by signature. Obstructions are
part of the physics, so they are part of that signature: without this, a
solve run after a source was registered would be served the terrain-only
raster out of the cache. It is also the mechanism by which a solve with NO
sources stays bit-identical to the pre-obstruction engine — the fingerprint
of an empty port is a constant.
readonly sources : Array.<RfObstructionPort.Source>
The registered sources, in registration order. A later source never
replaces an earlier one — every source contributes and the field keeps the
MAXIMUM, because two datasets covering the same block describe the same
skyline from different surveys and the taller reading is the one that
blocks.
Methods
static Cesium.RfObstructionPort.fromEntities(entities, options) → RfObstructionPort.Source
Builds a source from extruded polygon ENTITIES already in the scene.
This is the seam that makes the picture honest. A demo that draws its
buildings with the native Entity API and hands the SAME entity collection to
this source cannot show a shadow that disagrees with the geometry the user is
looking at, because there is one dataset and the solver read it.
Height is `extrudedHeight - height` when both are present, so a building
whose base is lifted off the ground contributes its own extent rather than
its absolute top. `heightReference` on the entity decides whether that extent
is above ground or above the ellipsoid.
| Name | Type | Description | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
entities |
EntityCollection | Array.<Entity> | |||||||||||||||||||||
options |
object |
optional
|
Returns:
static Cesium.RfObstructionPort.fromFootprints(options) → RfObstructionPort.Source
Builds a source from explicit footprint polygons.
| Name | Type | Description | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
options |
object |
|
Returns:
static Cesium.RfObstructionPort.fromGeoJson(geoJson, options) → RfObstructionPort.Source
Builds a source from GeoJSON building footprints.
Reads `Polygon` and `MultiPolygon` geometries (and the same inside a
`Feature`/`FeatureCollection`). Heights come from the feature's properties:
`height`, then `render_height`, then `building:levels` x `levelHeight`, then
`defaultHeight`.
| Name | Type | Description | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
geoJson |
object | A Feature, FeatureCollection or bare geometry. | ||||||||||||||||||||||||||||||||||||
options |
object |
optional
|
Returns:
static Cesium.RfObstructionPort.resolve(value) → RfObstructionPort|undefined
Coerces whatever a caller passed for `obstructions` into a port.
Accepts a port (returned as-is), a single source, an array of sources, or
`undefined`/`false` (no obstructions at all, which must remain the exact
pre-obstruction behaviour).
| Name | Type | Description |
|---|---|---|
value |
RfObstructionPort | RfObstructionPort.Source | Array.<RfObstructionPort.Source> | optional |
Returns:
Reads structure tops at ARBITRARY points rather than on a grid.
The coverage raster marches a grid; a point-to-point path profile walks a
geodesic, which is not axis-aligned in anybody's ENU frame and cannot be
served by a rasterized field without resampling it. Two APIs of the same
analysis disagreeing about whether a building exists is worse than either
answer alone, so both go through this port — the grid read above, and this
one.
| Name | Type | Description | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
request |
object |
|
||||||||||||
result |
Float32Array | optional |
Returns:
`{heights, covered, sources}` — ellipsoid-relative tops,
RfObstructionPort.NO_DATA where nothing stands.
Rasterizes every registered source onto the requested grid.
The grid is the caller's, described exactly as the RF solver's profile field
describes itself: an `width` x `height` array of posts at CELL CENTRES in a
local east-north-up frame, so post (x, y) sits at
`minEast + (x + 0.5) * cellWidth`, `minNorth + (y + 0.5) * cellHeight`. Being
handed the caller's geometry rather than inventing one is the whole point:
the obstruction field and the terrain field must be the same samples.
| Name | Type | Description | |||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
request |
object |
|
|||||||||||||||||||||||||||
result |
Float32Array | optional Optional output buffer of `width * height`. |
Returns:
`{heights, covered, featureCount, sources, strategy}` —
`heights` is ellipsoid-relative structure TOPS, with
RfObstructionPort.NO_DATA at every post no structure covers.
registerSource(source) → RfObstructionPort.Source
Registers an obstruction source.
| Name | Type | Description |
|---|---|---|
source |
RfObstructionPort.Source |
Returns:
The registered source.
Removes a previously registered source.
| Name | Type | Description |
|---|---|---|
source |
RfObstructionPort.Source |
Returns:
true if it was registered.
Type Definitions
A registered obstruction source.
The port owns compositing and provenance; a source owns nothing but "which
posts do I cover, and how high am I there". Implement this to plug in a
dataset the built-in factories do not cover — a municipal DSM raster, a CAD
site plan, a wasm module reading a tiled vector store — without the solver
learning anything about it.
Properties:
| Name | Type | Attributes | Description |
|---|---|---|---|
id |
string |
<optional> |
Stable identifier, reported in provenance and used in the port's cache fingerprint. |
name |
string |
<optional> |
Human-readable label. |
featureCount |
number |
<optional> |
How many structures the source holds. |
signature |
string |
<optional> |
A CONTENT-derived fingerprint. Two sources over the same data must produce the same string, and mutating the data must change it — the RF solver's coverage cache is keyed on it. |
isReady |
function |
<optional> |
Return `false` to be skipped this read (data still loading). Absent means always ready. |
rasterize |
function | Fill the grid described by the request; return `{covered, featureCount}`. | |
sample |
function |
<optional> |
Fill scattered points. Optional: a source without it contributes to coverage rasters but not to point-to-point path profiles. |