Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/architecture/WORKSPACE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ modules implement is fixed in
| Identity | Directory | Kind | Status | Responsibility |
| --- | --- | --- | --- | --- |
| `usdGeoCore` | `libs/usd-geo-core` | plain CMake/OpenStrata static library | implemented | Format-independent geospatial values: coordinates and local-origin transforms, spatial bounds, CRS-related values, deterministic tile IDs, normalized cache-key inputs, and the typed diagnostic vocabulary shared across modules. |
| `usdPointCloudCore` | `libs/usd-pointcloud-core` | plain CMake/OpenStrata static library | implemented | Format-independent point-cloud contracts: point attribute definitions and types, `PointChunk` / `PointData` / `PointCloudAsset`, point validation, attribute selection, range and chunk read options, deterministic sampling, and the shared LOD hierarchy and tile value types. |
| `usdPointCloudCore` | `libs/usd-pointcloud-core` | plain CMake/OpenStrata static library | implemented | Format-independent point-cloud contracts: point attribute definitions and types, `PointChunk` / `PointData` / `PointCloudAsset`, multi-scan `PointCloudScan` / `PointCloudCollection` values, point validation, attribute selection, range and chunk read options, deterministic sampling, and the shared LOD hierarchy and tile value types. |
| `usdLas` | `libs/usd-las` | plain CMake/OpenStrata static library | implemented | LAS parsing and decoding: 1.2-1.4 headers, VLR/EVLR, point formats 0-10, waveform packet metadata, GeoTIFF key records, scalar and vector Extra Bytes, metadata-only reads, and chunked and range-based reads behind `LasReader`. |
| `usdLaz` | `libs/usd-laz` | plain CMake/OpenStrata static library | implemented | LAZ decompression through the vendored laz-perf codec, converted into the shared LAS and point-cloud contracts, with metadata-only reads, chunked reads, and stable diagnostics for unsupported compressed formats. Owns the vendored laz-perf subset. |
| `usdPointCloudAuthoring` | `libs/usd-pointcloud-authoring` | plain CMake/OpenStrata static library | implemented | Shared OpenUSD authoring: `UsdGeomPoints` and geospatial metadata authoring, point-attribute authoring, `usdLod` hierarchy authoring, tiled point-cloud authoring, payload-backed tile assets and the ownership of generated payload files, root layer generation, and stage/layer validation. |
Expand All @@ -32,7 +32,7 @@ modules implement is fixed in
| `usdPointCloudTiling` | `libs/usd-pointcloud-tiling` | plain CMake/OpenStrata static library | implemented | Format-independent fixed-grid partitioning, spill-backed bounded-memory routing, deterministic tile and LOD ordering, validated tile manifest serialization, spool validation, and cleanup contracts. See the [streaming and tiling plan](../roadmap/streaming-and-tiling.md). |
| `usdGeoCache` | `libs/usd-geo-cache` | plain CMake/OpenStrata static library | implemented | Descriptor-based stable cache keys, deterministic USDC root/payload layout, machine-readable lookup states, process-local lookup statistics, the stable cache-decision vocabulary, and entry invalidation. Entries are addressed by a generation key over what the caller asked for and a source identity key over everything read out of the source, so revisions of one source are siblings. The conversion tool owns generation and atomic publication; direct FileFormat adapters reuse committed entries through `USDGEO_CACHE_ROOT`. |
| `usdPly` | `libs/usd-ply` | plain CMake/OpenStrata static library | implemented | PLY 1.0 header inspection, scalar vertex decoding, source filters, and explicit georeference conversion into shared point-cloud assets. |
| `usdAsciiPoints`, `usdE57` | `libs/` | plain libraries | reserved, not implemented | Additional point-cloud readers targeting the same shared contracts. |
| `usdAsciiPoints`, `usdE57` | `libs/` | plain libraries | reserved, not implemented | Additional point-cloud readers targeting the same shared contracts. The multi-scan collection contract needed by E57 is implemented in `usdPointCloudCore`; the decoder remains reserved. |
| `pointcloud-ply` | `plugins/pointcloud-ply` | OpenStrata plugin bundle (`usd-fileformat`) | implemented | Thin `.ply` adapter requiring an explicit `epsg` argument and authoring through shared point-cloud contracts. |
| `pointcloud-points-text`, `pointcloud-e57` | `plugins/` | reserved, not implemented | Future point-cloud FileFormat Plugin adapters, in the order fixed by [format support order](../roadmap/format-support-order.md). |

Expand Down
3 changes: 3 additions & 0 deletions docs/roadmap/implementation-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,9 @@ transaction state creation and retrying the same output workspace.
and local COPC reads onto it
- [x] Add resolver-backed `ArAsset` adaptation without introducing OpenUSD or
transport dependencies into `usdCopc`
- [x] Add the format-independent multi-scan collection contract required by
E57, including per-scan assets, bounds, attributes, and poses; the E57
decoder and multi-scan authoring remain deferred

### Next release direction

Expand Down
18 changes: 15 additions & 3 deletions libs/usd-pointcloud-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
`usdPointCloudCore` defines the format-independent point-cloud contracts every
reader produces and the authoring library consumes: the attribute schema, the
chunk and asset shapes, read options, deterministic sampling, and the shared
LOD value types. It is the seam that lets LAS and LAZ — and later COPC, PLY,
and E57 — reach OpenUSD through one path instead of one writer per format.
LOD value types, and multi-scan collection values. It is the seam that lets
LAS and LAZ — and later COPC, PLY, and E57 — reach OpenUSD through one path
instead of one writer per format.

CMake package `usdPointCloudCore`, target `usdpointcloud::core`, C++ namespace
`usdpointcloud`.
Expand All @@ -18,6 +19,10 @@ CMake package `usdPointCloudCore`, target `usdpointcloud::core`, C++ namespace
- `PointData` — the parallel per-attribute arrays, including RGB, NIR, GPS
time, waveform packet fields, and named scalar Extra Bytes columns.
- `PointCloudAsset` — a `GeoReference`, bounds, chunk, and data together.
- `PointCloudScan` — a named asset and its row-major rigid scan-to-collection
pose.
- `PointCloudCollection` — shared georeference and bounds for multiple scans;
collection bounds must contain every pose-transformed scan bound.
- `MakePointChunk`, which derives a chunk schema from populated `PointData`.
- Point data validation: array lengths agree, values are finite, the schema
matches what is populated.
Expand All @@ -44,7 +49,7 @@ CMake package `usdPointCloudCore`, target `usdpointcloud::core`, C++ namespace
## Public API

```text
usdpointcloud/PointCloud.h attributes, chunks, data, assets, read options
usdpointcloud/PointCloud.h attributes, chunks, data, assets, scans, read options
usdpointcloud/Lod.h tile ids, LOD items, hierarchies, validation
usdpointcloud/Sampling.h deterministic sampling and LOD asset building
usdpointcloud/FileFormatArguments.h argument parsing, normalization, attribute selection
Expand Down Expand Up @@ -89,6 +94,11 @@ PointChunk (+ GeoReference, SpatialBounds) -> PointCloudAsset
PointLodHierarchy + per-level PointCloudAsset
v
usdPointCloudAuthoring

E57 Data3D scans
| per-scan asset, bounds, attributes, and pose
v
PointCloudCollection
```

Sampling preserves source order and applies the same selected indices to every
Expand Down Expand Up @@ -147,6 +157,8 @@ ctest --test-dir build -C Release -R usdPointCloudCore_unit --output-on-failure
so adding an attribute is a source change in this module.
- The whole-cloud shape (`PointCloudAsset`) still assumes the data fits in
memory; format readers expose the bounded-memory path through `PointStream`.
- E57 decoding and multi-scan OpenUSD authoring are not implemented yet; the
collection types only establish the format-independent contract.

## Planned work

Expand Down
21 changes: 21 additions & 0 deletions libs/usd-pointcloud-core/include/usdpointcloud/PointCloud.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "usdgeo/GeoReference.h"
#include "usdgeo/SpatialBounds.h"

#include <array>
#include <cstdint>
#include <functional>
#include <optional>
Expand Down Expand Up @@ -84,6 +85,26 @@ struct PointCloudAsset {
bool IsValid() const noexcept;
};

struct PointCloudScan {
std::string id;
PointCloudAsset asset;
std::array<double, 16> pose{
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0};

bool IsValid() const noexcept;
};

struct PointCloudCollection {
usdgeo::GeoReference reference;
usdgeo::SpatialBounds bounds = usdgeo::SpatialBounds::Empty();
std::vector<PointCloudScan> scans;

bool IsValid() const noexcept;
};

PointChunk MakePointChunk(const PointData& data,
const usdgeo::SpatialBounds& bounds);

Expand Down
108 changes: 108 additions & 0 deletions libs/usd-pointcloud-core/src/PointCloud.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "usdpointcloud/PointCloud.h"

#include <algorithm>
#include <cmath>
#include <limits>
#include <set>

Expand Down Expand Up @@ -152,6 +153,113 @@ bool PointCloudAsset::IsValid() const noexcept {
chunk.IsValid() && chunk.pointCount == data.positions.size();
}

namespace {

constexpr double kPoseTolerance = 1.0e-9;

bool IsRigidPose(const std::array<double, 16>& pose) {
if (!std::all_of(pose.begin(), pose.end(),
[](double value) { return std::isfinite(value); }) ||
std::abs(pose[12]) > kPoseTolerance ||
std::abs(pose[13]) > kPoseTolerance ||
std::abs(pose[14]) > kPoseTolerance ||
std::abs(pose[15] - 1.0) > kPoseTolerance) {
return false;
}

const auto dot = [&](int firstRow, int secondRow) {
double result = 0.0;
for (int column = 0; column != 3; ++column) {
result += pose[firstRow * 4 + column] *
pose[secondRow * 4 + column];
}
return result;
};
for (int row = 0; row != 3; ++row) {
if (std::abs(dot(row, row) - 1.0) > kPoseTolerance) {
return false;
}
for (int otherRow = row + 1; otherRow != 3; ++otherRow) {
if (std::abs(dot(row, otherRow)) > kPoseTolerance) {
return false;
}
}
}

const auto determinant =
pose[0] * (pose[5] * pose[10] - pose[6] * pose[9]) -
pose[1] * (pose[4] * pose[10] - pose[6] * pose[8]) +
pose[2] * (pose[4] * pose[9] - pose[5] * pose[8]);
return std::abs(determinant - 1.0) <= kPoseTolerance;
}

usdgeo::Vec3d ApplyPose(const std::array<double, 16>& pose,
const usdgeo::Vec3d& point) {
return {pose[0] * point.x + pose[1] * point.y + pose[2] * point.z +
pose[3],
pose[4] * point.x + pose[5] * point.y + pose[6] * point.z +
pose[7],
pose[8] * point.x + pose[9] * point.y + pose[10] * point.z +
pose[11]};
}

bool ContainsTransformedBounds(const usdgeo::SpatialBounds& container,
const PointCloudScan& scan) {
for (int x = 0; x != 2; ++x) {
for (int y = 0; y != 2; ++y) {
for (int z = 0; z != 2; ++z) {
const usdgeo::Vec3d corner{
x == 0 ? scan.asset.bounds.minimum.x
: scan.asset.bounds.maximum.x,
y == 0 ? scan.asset.bounds.minimum.y
: scan.asset.bounds.maximum.y,
z == 0 ? scan.asset.bounds.minimum.z
: scan.asset.bounds.maximum.z};
const auto transformed = ApplyPose(scan.pose, corner);
const auto tolerance = 1.0e-9 *
(std::max)({1.0, std::abs(container.minimum.x),
std::abs(container.minimum.y),
std::abs(container.minimum.z),
std::abs(container.maximum.x),
std::abs(container.maximum.y),
std::abs(container.maximum.z),
std::abs(transformed.x),
std::abs(transformed.y),
std::abs(transformed.z)});
if (transformed.x < container.minimum.x - tolerance ||
transformed.y < container.minimum.y - tolerance ||
transformed.z < container.minimum.z - tolerance ||
transformed.x > container.maximum.x + tolerance ||
transformed.y > container.maximum.y + tolerance ||
transformed.z > container.maximum.z + tolerance) {
return false;
}
}
}
}
return true;
}

} // namespace

bool PointCloudScan::IsValid() const noexcept {
return !id.empty() && asset.IsValid() && IsRigidPose(pose);
}

bool PointCloudCollection::IsValid() const noexcept {
if (!reference.IsValid() || !bounds.IsValid() || scans.empty()) {
return false;
}
std::set<std::string> ids;
for (const auto& scan : scans) {
if (!scan.IsValid() || !ids.insert(scan.id).second ||
!ContainsTransformedBounds(bounds, scan)) {
return false;
}
}
return true;
}

PointChunk MakePointChunk(const PointData& data,
const usdgeo::SpatialBounds& bounds) {
PointChunk chunk;
Expand Down
44 changes: 44 additions & 0 deletions libs/usd-pointcloud-core/tests/test_point_cloud.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,49 @@ void TestPointDataAndAssetChunk() {
Check(!data.IsValid());
}

void TestMultiScanContract() {
usdgeo::GeoReference reference;
reference.epsgCode = 4978;
reference.localOrigin = {10.0, 20.0, 30.0};

usdpointcloud::PointData data;
data.positions = {{1.0, 2.0, 3.0}};
usdgeo::SpatialBounds bounds;
bounds.Expand(data.positions.front());

usdpointcloud::PointCloudScan first;
first.id = "scan-1";
first.asset = {reference, bounds,
usdpointcloud::MakePointChunk(data, bounds), data};
Check(first.IsValid());

usdpointcloud::PointCloudScan second = first;
second.id = "scan-2";
second.pose[3] = 4.0;
Check(second.IsValid());

usdpointcloud::PointCloudCollection collection;
collection.reference = reference;
collection.bounds = bounds;
collection.scans = {first, second};
Check(!collection.IsValid());

collection.bounds.maximum.x = 5.0;
Check(collection.IsValid());

second.id = first.id;
collection.scans[1] = second;
Check(!collection.IsValid());

collection.scans[1] = first;
collection.scans[1].pose[15] = 2.0;
Check(!collection.scans[1].IsValid());

collection.scans[1] = first;
collection.scans[1].pose[0] = 0.5;
Check(!collection.scans[1].IsValid());
}

void TestExtraByteNameNormalization() {
const auto names = usdpointcloud::NormalizeExtraByteNames(
{"air temperature", "2nd-return", "intensity", "air-temperature",
Expand Down Expand Up @@ -487,6 +530,7 @@ int main() {
TestAttributesAndChunks();
TestInvalidChunk();
TestPointDataAndAssetChunk();
TestMultiScanContract();
TestExtraByteNameNormalization();
TestReadOptions();
TestPointStreamContract();
Expand Down
Loading