What is georeferencing?

Some mesh, point cloud, or Gaussian splatting datasets don’t have any georeference. In this case, the coordinates of the dataset aren’t defined in an absolute reference, but rather in a local coordinate system. Such a local coordinate system is called a Cartesian reference: a self-contained coordinate system that represents positions directly as XYZ coordinates, but not corresponding to a location on Earth. Data positioned on a local coordinate system only is referred to as non-georeferenced data.

There are several scenarios where datasets are non-georeferenced. For example:

  • They were modeled in a CAD tool that doesn’t deal with georeferenced data.

  • They were captured with a regular camera that didn’t record the GPS position of the images.

  • They were scanned indoors with a LIDAR sensor, and there was no GPS signal available at the time of recording.

Georeferencing is the act of placing such non-georeferenced data at a geolocation, a point on the globe, thereby making the data georeferenced.

How to georeference a 3D Tiles model

In LuciadCPillar, georeferencing happens on the ITileSet3DModelITileSet3DModelITileSet3DModel. To visualize models with just a Cartesian reference, you must georeference them with a geolocation.

Program: Detecting whether a model needs a geolocation
auto model = *Ogc3DTilesModelDecoder::decode(path);
if (model->getReference()->getType() == CoordinateReferenceType::Cartesian) {
  // This model is ungeoreferenced. It needs a geolocation to be visualized.
} else {
  // This model is georeferenced. It can be visualized as-is.
}
var model = Ogc3DTilesModelDecoder.Decode(path);
if (model.Reference.Type == CoordinateReferenceType.Cartesian)
{
    // This model is ungeoreferenced. It needs a geolocation to be visualized.
}
else
{
    // This model is georeferenced. It can be visualized as-is.
}
val model = Ogc3DTilesModelDecoder.decode(path)
if (model.reference.type == CoordinateReferenceType.Cartesian) {
    // This model is ungeoreferenced. It needs a geolocation to be visualized.
} else {
    // This model is georeferenced. It can be visualized as-is.
}

To georeference a model, you can pass the geolocation to the decoder options:

Program: Decoding a model with a geolocation
auto geodetic = *CoordinateReferenceProvider::create("EPSG:4326");
auto targetLocation = GeometryFactory::createPoint(geodetic, Coordinate{4.6687, 50.865, 0});
auto geoLocation = GeoLocation::newBuilder().targetLocation(targetLocation).build();

// Create the model with the geolocation already set
auto options = Ogc3DTilesModelDecoder::Options::newBuilder()
                   .geoLocation(geoLocation)
                   .build();
auto modelOrError = Ogc3DTilesModelDecoder::decode(path, options);
if (!modelOrError.has_value()) {
  throw std::runtime_error("Model could not be decoded");
}
const auto& model = *modelOrError;
var geodetic = CoordinateReferenceProvider.Create("EPSG:4326");
var targetLocation = GeometryFactory.CreatePoint(geodetic, 4.6687, 50.865, 0);
var geoLocation = GeoLocation.NewBuilder()
    .TargetLocation(targetLocation)
    .Build();

// Create the model with the geolocation already set
var options = Ogc3DTilesModelDecoder.Options.NewBuilder()
    .GeoLocation(geoLocation)
    .Build();
var model = Ogc3DTilesModelDecoder.Decode(path, options);
val geodetic = CoordinateReferenceProvider.create("EPSG:4326")
val targetLocation = GeometryFactory.createPoint(geodetic, 4.6687, 50.865, 0.0)
val geoLocation = GeoLocation.newBuilder()
    .targetLocation(targetLocation)
    .build()

// Create the model with the geolocation already set
val options = Ogc3DTilesModelDecoder.Options.newBuilder()
    .geoLocation(geoLocation)
    .build()
val model = Ogc3DTilesModelDecoder.decode(path, options)

You can also set the geolocation after the model has been created:

Program: Applying a geolocation to an existing model
// Change the geolocation at runtime
auto geoLocation = GeoLocation::newBuilder()
                       .targetLocation(targetLocation)
                       .azimuth(45)
                       .build();
model->setGeoLocation(geoLocation);
// Change the geolocation at runtime
var geoLocation = GeoLocation.NewBuilder()
    .TargetLocation(targetLocation)
    .Azimuth(45)
    .Build();
model.GeoLocation = geoLocation;
// Change the geolocation at runtime
val geoLocation = GeoLocation.newBuilder()
    .targetLocation(targetLocation)
    .azimuth(Azimuth(45.0))
    .build()
model.setGeoLocation(geoLocation)

You can visualize manually georeferenced models using a TileSet3DLayerTileSet3DLayerTileSet3DLayer, just like any other georeferenced model. Any updates to the geolocation on the model are reflected in the visualization automatically.

For geolocations with a target referencetarget referencetarget reference that matches the map referencemap referencemap reference, any changes to the geolocation on the model are reflected instantly in the visualization.

If the target reference and map reference don’t match, for example when the map is in 2D, LuciadCPillar may need to reload the dataset, causing it to briefly disappear and reappear with the new geolocation. For this reason, interactively georeferencing datasets is only recommended in 3D.

Transforming tilesets

If you want more control over the positioning of your dataset, you can also apply a transformation to the TileSet3DLayerTileSet3DLayerTileSet3DLayer. The TileSet3DLayerTileSet3DLayerTileSet3DLayer provides a transformationtransformationtransformation, which you can also use for already-georeferenced datasets. This transformation is applied in the map referencemap referencemap reference. It allows you to freely apply translations, rotations, and scalings, in the form of a 4x4 matrix4x4 matrix4x4 matrix. You can even chain transformations by multiplying their matrices right-to-left.

For example, to scale a dataset around a given center point, you can apply a transformation like this:

Program: Scaling a dataset around a given center point
// Pick lon=12, lat=34 as the center point of our scale
auto geodetic = *CoordinateReferenceProvider::create("EPSG:4326");
auto scaleCenter = GeometryFactory::createPoint(geodetic, Coordinate(12, 34));

// Convert the center point to the map reference
auto geodeticToMap = Transformation::create(geodetic, map->getReference());
auto mapScaleCenter = geodeticToMap->transform(*scaleCenter)->getLocation();

// Build the transformation matrix:
// first translate the dataset to (0, 0),
// then scale, then translate back to the center point
auto translateTo00 = Matrix4x4::translate(-mapScaleCenter);
auto scale = Matrix4x4::scale(2); // Double the size
auto translateToCenter = Matrix4x4::translate(mapScaleCenter);
auto transformation = translateToCenter * scale * translateTo00;

// Apply the transformation to the layer
layer->setTransformation(transformation);
// Pick lon=12, lat=34 as the center point of our scale
var geodetic = CoordinateReferenceProvider.Create("EPSG:4326");
var scaleCenter = GeometryFactory.CreatePoint(geodetic, new Coordinate(12, 34));

// Convert the center point to the map reference
var geodeticToMap = Transformation.Create(geodetic, map.Reference);
var mapScaleCenter = geodeticToMap.Transform(scaleCenter).Location;

// Build the transformation matrix:
// first translate the dataset to (0, 0),
// then scale, then translate back to the center point
var translateTo00 = Matrix4x4.Translate(mapScaleCenter * -1);
var scale = Matrix4x4.Scale(2); // Double the size
var translateToCenter = Matrix4x4.Translate(mapScaleCenter);
var transformation = translateToCenter.Multiply(scale).Multiply(translateTo00);

// Apply the transformation to the layer
layer.Transformation = transformation;
// Pick lon=12, lat=34 as the center point of our scale
val geodetic = CoordinateReferenceProvider.create("EPSG:4326")
val scaleCenter = GeometryFactory.createPoint(geodetic, Coordinate(12.0, 34.0))

// Convert the center point to the map reference
val geodeticToMap = Transformation.create(geodetic, map.reference)
val mapScaleCenter = geodeticToMap.transform(scaleCenter)!!.location

// Build the transformation matrix:
// first translate the dataset to (0, 0),
// then scale, then translate back to the center point
val translateTo00 = Matrix4x4.translate(mapScaleCenter.multiply(-1.0))
val scale = Matrix4x4.scale(2.0) // Double the size
val translateToCenter = Matrix4x4.translate(mapScaleCenter)
val transformation = translateToCenter.multiply(scale).multiply(translateTo00)

// Apply the transformation to the layer
layer.transformation = transformation