A Unity base project for building interactive floor/wall installations with a Kinect v2 depth camera. It gives you three things out of the box:
- Calibration — map the Kinect's sensor space onto a real physical floor or wall, and keep the render camera lined up with it.
- ArUco marker tracking — detect printed markers in the scene and mirror them as live 3D GameObjects (position, rotation, and real-world scale).
- Depth blob / hand detection — find free-standing objects or hands in the depth image and turn them into extruded 3D meshes in real time.
Everything is driven off one shared Settings asset and one central frame loop, so the rest of
this document is mostly about how those pieces talk to each other.
-
Kinect for Windows SDK v2, with a Kinect v2 sensor connected — or use
SourceManager'sUseTestImagesmode to develop without hardware (see below). -
The project already has native plugins for:
- ArucoUnity (OpenCV ArUco bindings) — used by
ArucoDetector. - OpenCvSharp — used by
BlobObjectDetectorandPlaygroundMapping. - TextMeshPro — used by
FPSDisplay. - Unity's new Input System (
Keyboard.current) — used byCalibrationManagerandFPSDisplay.
These ship with the repo/project — you shouldn't need to install them separately, but if something doesn't compile, check the
PackagesandPluginsfolders first. - ArucoUnity (OpenCV ArUco bindings) — used by
-
Kinect + ArucoUnity are Windows-only, so this project targets Windows.
-
Start from the calibration scene — open it directly, or duplicate it as the base for your own scene rather than building one from scratch. It already has
SourceManager, the sharedSettingsasset,CalibrationManager,PlaygroundPlane, andCameraManagerwired up and cross-referenced, which is the fiddly part to get right by hand. Everything else in this guide (test images, calibration, markers, blob detection) builds on top of that scene. If you duplicate it, double-check theSettingsasset reference onSourceManagercarried over correctly. -
Check the
Settingsasset (Assets > Create > ScriptableObjects > Settingsif you need a new one). This one asset is wired into almost every script in the scene — if you duplicate it or forget to assign it somewhere, that system will silently drift out of sync with the rest. -
No Kinect on hand? Tick
UseTestImagesonSourceManagerand assignColorTestImage/DepthTestImage/InfraTestImage. The whole pipeline (ArUco, blob detection, calibration) runs the same way against static test images. If you do have a Kinect connected andUseTestImagesis off,SourceManagerfalls back to test images automatically if no sensor is found. -
Press Play.
SourceManagerstarts pulling frames immediately. -
Calibrate the playground:
- Hold Tab and press F1 to enter Playground Edit Mode.
- Drag the magenta center handle to move the rect, the yellow edge handles to resize it.
- Press C to cycle between Floor and Wall orientation.
- Press F1 again (Tab not required this time) to exit — this saves the rect back into the
Settingsasset and intoStreamingAssets/settings.json.
Heads up:
FPSDisplay's uses a specific toggle key, you can change it with whatever key you'd like!.
Think of the project as one event-driven pipeline rather than a set of scripts polling each
other every Update():
SourceManager (captures Kinect/test frames)
│
├─ OnTexturesInitialized → fired once, when stream resolution is known
└─ OnFrameUpdated → fired every captured frame
│
├─► SourceTextureViewer (pushes the active mode's texture to a preview material)
├─► ArucoDetector (runs marker detection → OnMarkersUpdated)
│ └─► MarkerWorldOverlay (syncs a GameObject per detected marker)
└─► BlobObjectDetector (thresholds depth → extrudes blobs into meshes)
Settings.OnSettingsChanged is the other event nearly everything subscribes to — whenever a
value changes (in the Inspector, via calibration, or loaded from JSON), consumers re-read the
fields they care about (crop bounds, flip flags, camera framing, ArUco tuning, etc.) instead of
polling Settings every frame.
| Script | Role |
|---|---|
Settings |
Central ScriptableObject config — crop/playground/projection rects, flip flags, camera settings, ArUco tuning, frame rate. Loads/saves StreamingAssets/settings.json. |
SourceManager |
Owns the Kinect connection (or test images). Runs the main capture loop; exposes raw and "prepared" (cropped + flipped) textures per SourceMode (Depth/Infrared/Color). |
PlaygroundMapping |
Static utility — the only place crop-rect math, pixel↔world mapping, and flip logic should live. Everything else calls into this instead of reimplementing it. |
PlaygroundPlane |
Represents the calibrated physical surface as a Transform, auto-sized from the Kinect resolution. |
OrientationUtility |
Tiny static helper for Floor vs. Wall basis vectors, shared by several components. |
CameraManager |
Positions/orients the render camera to match the calibrated playground rect. |
CalibrationManager |
The interactive drag-to-calibrate tool described above; also draws the Scene-view gizmos (red = detection area, yellow = projection, green/magenta = playground). |
SourceTextureViewer |
Debug/preview material driver — shows the live Color/Depth/Infrared feed, with a depth colour ramp. |
ArucoDetector |
Detects ArUco markers each frame (from Infrared by default, or a webcam for desk-testing) and fills the static ArucoDetector.Markers list. |
MarkerObjectRegistry |
ScriptableObject mapping marker IDs → prefabs. Shared by ArucoDetector and MarkerWorldOverlay so they never disagree about which IDs are bound to what. |
MarkerWorldOverlay |
Spawns/updates one GameObject per assigned marker, matching its real-world position, rotation, and scale every frame. |
BlobObjectDetector |
Thresholds the depth image for free-standing objects/hands, cleans up the mask, and extrudes each contour into a 3D mesh. Publishes results via the static Hands list. |
BlobObjectDetectorUtilities |
Static geometry helpers (triangulation, contour smoothing, mesh edges) used by BlobObjectDetector. |
Structures |
Shared value types: Marker (ArUco result) and Hand (blob detection result). |
Extensions |
General-purpose static helpers (mesh building, texture creation, popups, quad rotation math) used throughout. |
FPSDisplay |
On-screen FPS/VSync/orientation HUD. Tab toggles it, Escape quits, V toggles VSync. |
- No Kinect at all:
SourceManager.UseTestImages+ assign the three test image fields. - Capture your own test set: with a Kinect connected and the game running,
SourceManager's custom Inspector has a "Capture Live Frames as Test Images" button that saves the current Color/Depth/Infrared frames intoAssets/KinectTestImages/Capture_<timestamp>/and wires them up as importable test image assets. - Testing markers without a Kinect:
ArucoDetectorhas auseWebcamInspectortoggle to run detection against a regular webcam instead of the Kinect IR stream — handy for tuning marker detection parameters at your desk.
ArucoDetector—showDebugOverlayshows the exact processed image being fed into OpenCV, as a quad in the corner of the screen, with rejected marker candidates outlined in red.BlobObjectDetector— thedebugStagedropdown (Grayscale/Threshold/Cleaned/Downscaled) shows any stage of the blob-detection pipeline the same way.- Scene view gizmos —
ArucoDetectordraws each detected marker's outline, ID, and size;CalibrationManagerdraws the detection/projection/playground rects and (in edit mode) the drag handles.
- One
Settingsasset per setup. If a script'sSettingsfield is empty or points at a different asset than everyone else's, that script will quietly use stale or default values. - Flip math lives in
PlaygroundMappingonly. If you're chasing an orientation/mirroring bug, that's the one file to fix — every consumer (ArucoDetector,SourceTextureViewer,BlobObjectDetector) is supposed to call into it rather than flip things themselves. MarkerObjectRegistrymust be the same asset on bothArucoDetectorandMarkerWorldOverlay, or marker IDs the detector "assigns" won't resolve to a prefab on the overlay side.- In the Editor, the
Settingsasset is authoritative.settings.jsongets regenerated from the asset each session, so an Inspector edit won't be silently overwritten by a stale JSON file. In a standalone build, the JSON is what persists between runs.