Skip to main content
Version: Development

Field Layout & Native Interop

This page describes how field layout data is loaded and how QuestNav interfaces with its native C/C++ libraries.

Field Layout

Loading

The field layout is loaded during QuestNav.Awake() from a JSON file in StreamingAssets/apriltag/fieldlayouts/. The filename is currently hardcoded:

await aprilTagFieldLayout.LoadJsonFromFileAsync("2026-rebuilt-welded.json");

To use a different field layout, this line must be edited and the app rebuilt.

JSON Format

The layout file follows the WPILib field layout format. Each tag entry contains:

{
"ID": 1,
"pose": {
"translation": { "x": 16.697198, "y": 0.655294, "z": 1.4859 },
"rotation": {
"quaternion": { "W": 0.4539905, "X": 0.0, "Y": 0.0, "Z": 0.8910065 }
}
}
}
  • Translation: Tag center position in meters, in FRC field coordinates (origin at blue alliance corner)
  • Rotation: Tag orientation as a quaternion in FRC coordinates (the tag's normal direction)

Tag Size

The physical tag size (the black square of the tag36h11 family) is a constant, QuestNavConstants.AprilTag.TAG_SIZE_METERS (0.1651 m / 6.5 in). The default constructor uses it:

new AprilTagFieldLayout() // standard FRC tags
new AprilTagFieldLayout(0.2032) // overload, for different-sized tags (meters)

The size is not carried in the layout JSON — the WPILib schema has only tags and field — so a practice field printed with different-sized tags must pass an explicit size to the overload.

3D Corner Computation

AprilTagFieldLayout.GetTagCorners() computes the four 3D corner positions of each tag in field coordinates. From the tag center pose, corners are offset in the tag's local frame:

CornerTag-local offset
Bottom-Right(0, -halfSize, -halfSize)
Bottom-Left(0, +halfSize, -halfSize)
Upper-Left(0, +halfSize, +halfSize)
Upper-Right(0, -halfSize, +halfSize)

where halfSize = tagSize / 2 = 0.08255m.

Each offset is a Transform3d applied to the tag's field pose via tagPose.Plus(cornerTransform), producing corners in FRC field coordinates.

warning

The corner ordering (BR, BL, UL, UR) is intentionally non-standard. The Meta Quest passthrough camera produces a mirrored image, causing the AprilTag detector to return 2D corners in clockwise order. The 3D corners must match this order for correct 2D-3D correspondences. Do not reorder them to match the standard counter-clockwise AprilTag convention.

Native Library Architecture

QuestNav uses three native C/C++ libraries accessed via P/Invoke ([DllImport]). All use CallingConvention.Cdecl.

Library Overview

LibrarySourcePurposePlatform Files
libapriltagUniversity of MichiganTag detection in grayscale imageslibapriltag.so (Quest), apriltag.dll (Editor)
libposelibPoseLib projectMulti-tag PnP pose estimationlibposelib.so (Quest), poselib.dll (Editor)
libntcoreWPILibNetworkTables 4 protocollibntcore.so + deps (Quest), ntcore.dll + deps (Editor)

Interop Pattern

All native interop follows a consistent pattern:

  1. Native struct mirrors with [StructLayout(LayoutKind.Sequential)] match C struct memory layout exactly
  2. *Natives classes contain all [DllImport] declarations as static methods
  3. Managed wrappers hold unsafe pointers to native memory and implement IDisposable for cleanup

AprilTag Detection (libapriltag)

Key native functions:

FunctionPurpose
apriltag_detector_create()Create a detector instance
apriltag_detector_add_family_bits()Register the tag36h11 family
apriltag_detector_detect()Run detection on a grayscale image
tag36h11_create() / destroy()Tag family lifecycle
image_u8_create() / destroy()Grayscale image buffer lifecycle

The zarray_get and zarray_size functions from the C library are re-implemented in C# by directly reading native struct memory, because the C originals are static inline and cannot be P/Invoked.

PoseLib Solver (libposelib)

A single function:

poselib_estimate_absolute_pose_simple(
points2D[], // Flat array of 2D image coordinates
points3D[], // Flat array of 3D field coordinates
numPoints, // Number of correspondences
cameraModelId, // POSELIB_CAMERA_PINHOLE (0)
cameraParams[], // [fx, fy, cx, cy]
imageWidth,
imageHeight,
maxReprojError, // 12 pixels (hardcoded)
&qw, &qx, &qy, &qz, // Output quaternion
&tx, &ty, &tz, // Output translation
&numInliers // Output inlier count
)

Camera intrinsics come from PassthroughCameraAccess.Intrinsics:

  • Focal length: Intrinsics.FocalLength (fx, fy)
  • Principal point: Intrinsics.PrincipalPoint (cx, cy)
  • Image dimensions: Intrinsics.SensorResolution (not RequestedResolution — they may differ if the capture resolution is downscaled)

Memory Management

ResourceStrategy
ImageU8Static cached buffer; reallocated only on resolution change. Returned wrapper does not own the handle.
AprilTagDetectionResultsWraps a zarray_t*. Freed when Detect() is called again (libapriltag overwrites previous results). Not explicitly disposed in the coroutine.
AprilTagDetectorIDisposable. Disposes all added families, clears, then destroys.
AprilTagFamilyIDisposable with a Disposed flag guard (safe for double-dispose).
caution

AprilTagDetectionResults from Detect() is valid only until the next Detect() call. Do not hold references to detection objects across frames.

WPILib Geometry Library

QuestNav includes a C# port of WPILib's Java geometry classes in the QuestNav.QuestNav.Geometry namespace. These provide double-precision 3D math operations.

ClassDescription
Pose3dTranslation3d + Rotation3d. JSON-serializable. Converts to/from protobuf.
Rotation3dQuaternion-backed rotation. Constructs from matrix, axis-angle, or Euler angles.
Translation3d3D vector with norm, distance, and rotation operations.
Transform3dRelative transform between two poses.
QuaternionDouble-precision quaternion. Not UnityEngine.Quaternion.
Field2dField dimensions container.
Namespace collision

QuestNav.QuestNav.Geometry.Quaternion and UnityEngine.Quaternion are different types. The geometry quaternion uses double precision and (W, X, Y, Z) constructor order. Files that need both types add using Quaternion = UnityEngine.Quaternion; at the top. The double namespace (QuestNav.QuestNav.Geometry) is intentional — do not "fix" it.