Folding
Abstract Spatial Computing Lab

Spatial Computing Lab (LUCAscl*)

Institution: LUCA School of Arts (LUCA)
Partner: FilmEU, as part of WIRE
Coordinator: Jan Duerinck (jan.duerinck@luca-arts.be)
Website: <https://www.researchcatalogue.net/>

Info

The Spatial Computing Lab (LUCAscl*) at LUCA School of Arts explores real-time spatial interaction, augmented and mixed reality, and immersive sound. Integrating panoramic video, depth sensors, and tactile interfaces, it fosters artistic research into the dynamic relations between digital and physical space. The lab aims to develop critical, experimental, and participatory approaches to spatial computing and media art. As part of WIRE, it contributes to interdisciplinary research across XR, installation practices, and interactive design.

Focus

  • Augmented and mixed reality
  • Spatial computing
  • Interactive media

Goals

  • Advance interdisciplinary research in XR and spatial computing.
  • Strengthen doctoral and practice-based research within LUCA and FilmEU.
  • Strengthen collaborations.
  • Expand the lab's technological capabilities.
  • Build a sustainable framework that supports this research.

Relations

References

Calculations

CALC.tox

Layer: spatial logic
In: 2 POPs (point or shape)
Out: 1 CHOP

CALC measures how two spatial entities relate to each other: two points, a point and a shape, or two shapes. It reduces that relation to one control signal. Four measurements are available: centre-to-centre distance, signed distance to a surface, an inside/outside test, and a smooth fade between two concentric spheres. Together these are enough for most body-to-object interactions in the lab, such as proximity, entering a zone, and gradual transitions. They come from a single component with one predictable output.

Role in the setup

CALC turns geometry into control data. A typical setup has a tracked joint from Azure Kinect sensors on input 1 and a Virtual objects on input 2. The resulting CHOP drives any parameter: the opacity of a projected layer, the gain of a sound, the state of a scene.

Technical specification

Let A be input 1 and B be input 2.

  • Distance between points. The Euclidean distance between the centres of A and B:
    center_dist = ‖c_A − c_B‖ (metres).
  • Distance between shapes. The distance from A to the surface of B, or between the surfaces of A and B. The result is signed: positive outside, negative inside. This works for every closed shape. For boxes and cylinders, the distance is measured outward from the shape's centre, so values near edges and corners are slightly larger than the true Euclidean surface distance.
  • In or outside of a shape. A binary containment test: inside = 1 if A is inside B, otherwise 0.
  • Fade to sphere. A continuous value between the inner sphere (radius r_in) and the outer shell (radius r_out) of a sphere-type virtual object. It is 1 inside r_in, 0 beyond r_out, and changes smoothly in between. This makes a soft, non-binary interaction zone. It requires B to be a sphere with Outer shell turned on.
  • One calculation at a time. Only one calculation can be on. The others are greyed out. This keeps the output unambiguous and keeps the GPU/CPU cost per instance predictable. For several measurements, use several CALC instances in parallel.
  • POP inputs. Both inputs are POPs, so the calculations work directly on the GPU-side geometry from Virtual objects. A single tracked joint counts as a one-point POP.

Parameters

ParameterFunction
Distance between pointsdistance between the centres of both inputs
Distance between shapessigned distance to the surface of the shape(s)
In or outside of a shapecontainment test: is input 1 inside input 2
Fade to spheresmooth value between inner sphere and outer shell

Outputs

ChannelUnit / rangeMeaning
center_distmetres, ≥ 0distance between the centres
shell_distmetres; negative = insidesigned distance to the surface
inside0 / 1containment
fade0 … 11 inside the inner sphere, 0 outside the outer shell

Limitations and expected behaviour

  • Fade to sphere greyed out: input 2 is not a sphere with Outer shell on.
  • Wrong distances: the shape is not closed (it has an open side).
  • Box or cylinder distances slightly too large near edges and corners: this comes from measuring outward from the centre (see above).
  • Yellow warning inside the component when a point is on input 1: harmless.
  • Raw tracking noise passes straight through. For stable triggers, smooth the joint input, or add hysteresis to the inside output.

History

Since the October 2026 revision, CALC replaces four earlier components: DIST_between_points, DIST_to_object, IN_or_OUT_object and FADE_to_sphere. Distance to a shape now works for any closed shape and between two shapes.

Relations

References

  • User README: CALC-README
  • Repository: <https://github.com/SpatialComputingL/Td-updates>
  • Overview: Lab Tools
Azure Kinect cameras

Azure_Kinect_camera.tox

Layer: sensing
In: none
Out: one TOP per sensor

This component gives the live image streams of the lab's three Azure Kinect sensors as TouchDesigner TOPs, one per sensor. It is for work that uses the camera image itself, such as live video in a projection, image analysis, or material for compositing. Skeleton data is not included: for body positions, use Azure Kinect sensors.

Role in the setup

The camera and the sensor component use the same hardware, but they keep image and position data apart. The camera's TOPs normally go to image processing and then to Wall projection or Floor projection.

Technical specification

  • Source. The video comes from the Azure Kinects calibration as NDI streams, one per sensor (<host> (AK1), (AK2), (AK3)), and not from the workstation itself. This spreads the processing load: the high-bandwidth camera streams do not compete with rendering on the workstation. The component lists the NDI sources it finds on the network when the project starts.
  • Viewpoint. The sensors are mounted on the ceiling grid, so all images are high-angle views looking down into the room.
  • Stream selection. Image sets which stream is delivered (for example colour). The choice is sent to the host as an OSC broadcast (UDP 27), and the host switches what it publishes. Other stream types the sensor provides, such as depth or infrared, can be selected here if the host publishes them.
  • Outlets. There is one outlet per sensor in a fixed order (1, 2, 3), which matches the physical sensors. To find out which outlet is which corner, wave at a camera.
  • Resolution and frame rate are set by the sensor and the stream configuration, not by the component. Fit or resample downstream to match the target resolution of a projection component.

Parameters

ParameterFunction
Imagethe stream delivered (colour, …)
Configurerebuilds the outlets after Image changes

Outputs

  • Outlets 1–3: one TOP per sensor, in fixed order.
  • Recommended convention: attach a named null to each outlet (null_cam_left, null_cam_right, …).

Limitations and expected behaviour

  • Black or empty outputs: you are not in the lab, or the sensors are off.
  • All views look alike: they show the same empty room from different corners.
  • No change after switching Image: press Configure.
  • Unexpected resolution or frame rate: this comes from the sensor or stream. Use a Fit TOP or resample downstream.
  • Privacy: these are identifiable camera images. When recording or passing on material from public sessions, you need visitors' consent.

Relations

References

Lab Tools

Info

The Spatial Computing Lab (LUCAscl*) gives artists, students and researchers a shared set of custom TouchDesigner components ("toxes"). These components hide the lab's specific hardware behind a small number of stable interfaces. There are eight components, grouped into three layers:

  • Sensing: body tracking and camera images from Azure Kinect sensors.
  • Spatial logic: virtual objects, and calculations between bodies and objects.
  • Output: wall projection, floor projection and third-order ambisonic sound.

Together they let a user go from a tracked body in the room to an image or sound that responds to it, without rebuilding the lab's signal chain. The components are meant to be used both at home and in the lab. Projects can be prepared and simulated remotely, and then run at full resolution and with live sensor data on site. On the sound side, the components are complemented by a rendering patch in Max/MSP. This note describes the system the components belong to, the conventions they share and how the other notes in this collection fit together.

System overview

The components are tied to one physical space and its hardware:

SubsystemConfiguration
Roomapprox. 5 × 5 × 3.5 m (width × depth × height). The entrance (the "gap") is on the south side.
Rigginga grid of scaffold pipes suspended below the concrete ceiling. It carries the sensors (on drop rods), the elevated loudspeakers and the projectors
Sensingthree Azure Kinect sensors (depth + RGB) mounted on the ceiling grid, used as cameras and for skeleton tracking of several people at once, fused into one skeleton per person
Imagethree fixed projection walls: left, centre and right as seen from the entrance (west, north and east). They can be used as three channels or one panoramic image. There is also a floor projection. A fourth (south) wall is mobile and is not installed by default.
Sound16 full-range loudspeakers in a third-order ambisonic layout, plus one subwoofer
Computingan HP Z8 workstation running TouchDesigner 2025.33230 with an Educational licence (content, tracking, control), and a Windows NUC running Max 9.1.5 (ambisonic rendering, see Max-MSP ambisonic renderer). The Kinects are connected to a separate Intel NUC, the Azure Kinects calibration, which calibrates and fuses the sensors and streams skeletons (OSC) and images (NDI).
TransportDante audio network at 48 kHz (Dante Via on the workstation, Dante Virtual Soundcard on the NUC). OSC carries positions.

Shared conventions

All components follow the same conventions. Once you know them for one component, you know them for the rest.

  • Units and axes. Positions are in metres. X = width, Y = height, Z = depth. The sound system uses the same frame: Z = 0 is the north wall, Z increases towards the entrance, and the reference listening point is at X = 0, Y = 1.5 m (ear height), Z = 2.5 m (room centre).
  • Data types. Each component uses the native TouchDesigner family that fits the data:
  • images are TOPs
  • positions, distances and control values are CHOPs
  • geometry is POPs, the GPU-based point operators that replaced SOPs in October 2026
  • Configure pattern. Components that build their own outlets or windows (camera, projections) rebuild only when you press Configure. This keeps a running performance from being rebuilt by accident. The skeleton sensor is the exception: its outlets follow the joint list by themselves, and its Configure re-initialises the sensors on the Kinect host.
  • Named nulls. Users attach a named null to each outlet (null_head, null_cam_left, …) and work from those. If a component is reconfigured or updated, references to the nulls still work.
  • Home/lab parity. Every component that depends on the lab can run without it:
  • the skeleton sensor can follow the mouse instead of a body
  • the projection components have a License switch that changes between home resolution, which stays within the 1280 × 1280 limit of TouchDesigner's free Non-Commercial licence, and full lab resolution, which the lab's Educational licence allows.
  • all components behave the same either way, so a patch built at home runs unchanged in the lab.
  • "Normal, not bugs". Each component documents behaviour that looks like a fault but is expected, such as empty outputs outside the lab or a source that takes a moment to appear. This saves time when troubleshooting on site.

Architecture

          SENSING                    SPATIAL LOGIC                    OUTPUT
 ┌──────────────────────┐   ┌──────────────────────────┐   ┌──────────────────────────┐
 │ Azure Kinect sensor  │──►│ Calculations (CALC)      │──►│ Ambisonics sound control │
 │  (joints, CHOP)      │   │  distance / inside / fade│   │  (Max/MSP, 16 + 1 ch)    │
 │                      │──►│ Virtual object viewer    │   │                          │
 │ Azure Kinect camera  │   │  (verification only)     │   │ Wall projection          │
 │  (images, TOP)       │   │                          │   │  (L / C / R, panoramic)  │
 └──────────────────────┘   │ Virtual object           │──►│                          │
                            │  (shape POP, pos CHOP)   │   │ Floor projection         │
                            └──────────────────────────┘   └──────────────────────────┘

A typical interaction chain:

  1. A tracked joint (for example the right hand) comes from the sensor.
  2. A virtual object (for example a sphere with a soft outer shell) is placed in the room.
  3. CALC turns their spatial relation into one control value.
  4. That value drives the content, which reaches the audience through the wall and floor projections and through ambisonics sound control. The sound component can also take the virtual object's position directly, so the object can be heard in the room.

Reading guide

NoteLayerInOut
Azure Kinects calibrationSensing (back end)3 × Azure Kinectfused skeletons (OSC), images (NDI)
Azure Kinect sensorsSensingnoneone CHOP per joint (tx ty tz)
Azure Kinect camerasSensingnoneone TOP per sensor
Virtual objectsSpatial logicoptional position (CHOP)shape (POP), position + size (CHOP)
CalculationsSpatial logic2 POPs1 CHOP
Virtual object viewerSpatial logicpositions (CHOP), shapes (POP)3D preview
Ambisonics sound controlOutputone position per source (CHOP) + audioambisonic mix on the speaker array
Max-MSP ambisonic rendererOutput (back end)OSC positions + 16 Dante audio channels16 + 1 loudspeaker feeds
Wall projectionOutput1–3 TOPsprojection window, resolution CHOP
Floor projectionOutput1 TOPprojection window, resolution CHOP

Each note follows the same structure: an opening summary, role in the setup, technical specification, parameters, outputs, limitations and relations to the other components. The shorter user READMEs are stored next to each .tox and inside the example .toe. Other equipment in the lab (alternative sensors, controllers, software) and learning resources are listed in Inventory.

Project structure and updates

The components are distributed as one folder: an example .toe project file and a toxes folder that holds each .tox and its README.

  • Keep the .toe and the toxes folder together in one project folder with a name of your choice. This makes it easy to move the project between your own computer and the lab workstation.
  • You can rename the .toe freely. Do not rename the .tox files or their READMEs, because updates replace them by name.
  • To update a component, download the new .tox and replace the old one in the toxes folder. Components are built so that the parameter values stored in your project survive the update.
  • TouchDesigner does not autosave. Save often with Cmd/Ctrl + S. TouchDesigner keeps the numbered backups itself.

Revision, 2 October 2026 (SOP → POP).

  • virtual_object and virtual_object_viewer now output POPs, which run on the GPU, so scenes with many objects are noticeably faster.
  • Each virtual object has its own colour.
  • Four separate calculation components (DIST_between_points, DIST_to_object, IN_or_OUT_object, FADE_to_sphere) were replaced by one component, CALC. Its output channels are center_dist, shell_dist, inside and fade.
  • Patches made before this revision need to:
  • swap the old calculators for CALC
  • update their channel references
  • give a POP to anything that expected a SOP

Design rationale

The components follow one lesson from running the lab: visiting researchers have limited time on site, and that time should go to the parts of the work that need the physical room. Interfaces stay stable and the lab can be simulated, so most of the setup can be done remotely. Users can work with the lab at different depths:

  • treat each component as a black box
  • open it up and adapt it
  • skip it entirely and work with the raw data streams

Source and licence

  • Component repository: <https://github.com/SpatialComputingL/Td-updates>
  • The components are released under the Apache License 2.0. You may use, modify and redistribute them, including commercially, as long as you keep the licence and copyright notice and state any changes.
  • Context: Abstract Spatial Computing Lab

COURSES

DOCUMENTATION

Virtual objects

virtual_object.tox

Layer: spatial logic
In: position (CHOP, optional)
Out: shape (POP, blue), position + size (CHOP, green)

A virtual object is an invisible geometric volume (a sphere, box or cylinder) that you place in the physical room for other components to interact with. It does not draw anything itself. It is a spatial definition that other components can measure against, listen to or display. It is the basic building block of the lab's interaction logic: zones, triggers, attractors and sound sources are all made from virtual objects.

Role in the setup

Virtual objects connect the tracked body to the content. A tracked hand has no meaning until it can be related to something. The virtual object provides that "something" in the shared room frame. Its shape output feeds Calculations and the Virtual object viewer. Its position output can place a sound source in Ambisonics sound control.

Technical specification

  • Primitives. You can choose a sphere (radius), a box (width X, height Y, depth Z) or a cylinder. Only the size fields that belong to the current shape are active.
  • Transform. Position XYZ and Rotate XYZ place the object in the room frame (metres; X = width, Y = height, Z = depth). If you wire a CHOP into the optional input, it drives the position, so an object can follow a tracked joint or an animated path.
  • Soft boundary. For spheres, Outer shell adds a second, larger radius around the core. The space between the two radii is a transition zone that CALC uses for its Fade to sphere calculation.
  • GPU geometry. Since the October 2026 revision, the shape is a POP (point operator), so geometry is processed on the GPU. Scenes with many objects stay responsive. Each object gets its own colour, which the viewer picks up automatically.
  • Closed volumes. Distances and inside/outside tests only make sense for closed shapes. All built-in primitives are closed.
  • Position capture. Get pos (shift + M) captures a position interactively instead of typing it.

Parameters

ParameterFunction
Virtual Objectshape: sphere, box or cylinder
Width X, Height Y, Depth Z, Radiussize. Only the fields for the current shape are active.
Position XYZ, Rotate XYZplacement in the room frame
Get pos (shift + M)captures a position instead of typing it
Outer shell + Radiussphere only. Adds an outer sphere for a soft edge, used by Fade to sphere.
Viewportcamera for the node preview only. It does not move the object.

Outputs

  • Blue (POP): the shape geometry, for CALC and the viewer.
  • Green (CHOP): centre position tx ty tz and a size value taken from the radius. In Ambisonics sound control, this size value sets the source's Rangegain.

Limitations and expected behaviour

  • Size fields greyed out: they belong to another shape.
  • Outer shell greyed out: the shape is not a sphere.
  • Preview looks squashed or off-centre: this is the preview viewport. Switch viewports.
  • Nothing visible in the room: this is expected. The component only defines the shape. To see it, use the viewer or render it into a projection.

Relations

References

Partner FilmEU

FilmEU & WIRE by FilmEU

Framework: FilmEU, the European University for Film and Media Arts (FilmEU+, Erasmus+ European Universities)
Related research project: WIRE

FilmEU is an alliance of European higher-education institutions in film and media arts. Its Imagination Centre connects the partners' local labs, each with its own expertise:

  • virtual production and motion capture
  • immersive sound and Dolby Atmos
  • digital fabrication
  • generative AI

The Spatial Computing Lab is LUCA's contribution, and one of the first labs in the network that is fully operating. It brings a rare combination to the network: immersive projection on walls and floor, a 16-speaker third-order ambisonic sound system, and real-time body tracking in one room.

Within FilmEU the lab has two roles:

  • education: students from across the alliance work in it through coursework, electives and joint programmes, such as the planned BA Music and Sound in Immersive Multimedia Environments
  • artistic research: through the WIRE project, visiting researchers develop and present work in the lab

In June 2026, at the FilmEU Summit in Brussels, the lab hosted two WIRE pilot showcases:

  • Agnes Meng (Lusófona University): Por Este Rio Acima, an immersive documentary installation on Northern Portuguese folklore
  • Diego Barajas (Tallinn University): Eclosion, an interactive immersive experience

These visits shaped one of the lab's central principles: preparation should happen remotely. The documented components and simulation tools let visitors arrive ready to spend their time on site on the work that needs the room. FilmEU is also developing shared access and governance models, such as a token-based system for booking labs across partners.

Related: Abstract Spatial Computing Lab · Lab Tools

Support STAGEtech

StageTech Flanders open call

Partners: LUCA School of Arts, Corda Campus, imec
Format: artistic research trajectory through an open call
Role of LUCA / SCL: artistic and technological expertise, knowledge and feedback

Technology doesn't make art. Artists do. StageTech Flanders is a programme led by imec, supported by VLAIO and the Flemish government, in which makers, companies and higher-education institutions experiment with new technologies for the stage and for live experiences. LUCA School of Arts is one of its education and knowledge partners. The pilot phase (2026–2027) runs at Club Wintercircus, Corda Arena and deSingel, with five pilot projects on immersive audio, content creation for immersive experiences and stage robotics.

Within StageTech, LUCA, Corda Campus and imec run an artistic open call. Artistic questions lead, and production comes after experimentation. The first call (deadline 15 September 2026) supports a trajectory from October 2026. It has a project budget of €30,000 and ends in a public presentation at the Corda Arena, planned for April 2027.

What LUCA and the Spatial Computing Lab contribute

StageTech is not a residency where artists simply come to use the lab. LUCA and the Spatial Computing Lab take part as an expert partner: they bring artistic and technological expertise into each trajectory, share their knowledge, and give feedback throughout the process.

  • Artistic expertise: critical feedback on concept, form and audience experience, from LUCA's practice in audiovisual arts, sound, performance and immersive media.
  • Technological expertise: knowledge of spatial computing in practice, including real-time tracking of several people, immersive projection and third-order ambisonic sound, and of choosing technology in proportion to the artistic question.
  • Knowledge transfer: documented components, workflows and lessons from earlier projects, so a trajectory starts from what already works instead of from zero. Where it helps, ideas can be tested in the lab before they move to a larger stage.
  • Feedback in both directions: the questions raised in each trajectory feed back into the lab's research and teaching. LUCA students can take part through workshops or experimental sessions.

Related: Abstract Spatial Computing Lab · Lab Tools

Floor projection

floor_projection.tox

Layer: output (image)
In: 1 TOP
Out: projection window + resolution CHOP (resX, resY)

This component is the output stage for the lab's floor projector. It works the same way as Wall projection, but for a roughly square surface with no natural "up". The floor matters in spatial computing because it is where tracked bodies, virtual objects and projected images line up most directly. A position in the room's XZ plane corresponds to a point on the floor image.

Role in the setup

The floor projection completes the lab's image output. It is especially suited to interaction based on location: zones, paths and traces that respond to where visitors stand. These are typically made by combining Azure Kinect sensors data with Virtual objects zones and Calculations.

Technical specification

  • Resolution contract. As with the wall projection, the component publishes resX, resY as a CHOP that you wire into your source TOP.
  • Recommended chain: moviefilein → fit → floor_projection. A Fit TOP before the component makes sure that content made for other aspect ratios (for example wide wall content) is fitted on purpose and not distorted.
  • Home/lab switch. License off = reduced home resolution (within the 1280 × 1280 limit of the Non-Commercial licence). On = full lab resolution (Educational licence on the lab workstation).
  • Orientation. The floor has no inherent up. By convention, orient the content towards where the audience enters (the south side, where the entrance is) and write the orientation down in the project documentation.
  • Mapping to room coordinates. Because the room frame uses X = width and Z = depth, positions from the sensor and from virtual objects can be mapped to floor pixel coordinates with a linear scale to resX, resY. This mapping is the user's responsibility, as the component does not apply it.

Parameters

ParameterFunction
Licenseoff = home resolution, on = full lab resolution
Fit Inputwhat happens to an image of the wrong size
Configurerebuilds after settings change
Open Window / Close Windowopens or closes the projection

Outputs

  • Resolution CHOP: resX, resY for the user's source TOP.
  • The projection window on the floor projector.

Limitations and expected behaviour

  • Wall content looks wrong: the aspect ratio is different. Add a Fit.
  • Small resolution: License is off. Even at full lab resolution, the floor projector has a lower resolution than the wall projection, so avoid fine detail and small text on the floor.
  • Image "upside down": the floor has no up. Orient the content towards the entrance and write it down.
  • Nothing visible after a change: press Configure, then Open Window.
  • Occlusion: visitors cast shadows on a projected floor. Take this into account in the content design, for example by keeping important information away from where people usually stand.

Relations

References

INTERACTION trigger
example list here?

Comments on Sensing

Earlier versions, previous attempts and related tech.

  • Before the component: Azure Kinect data was introduced in a workshop (Sensor data: how to connect and interact with an Azure Kinect sensor kit). Each project set up the sensor by hand from a quick guide.
  • Community tools: projects for calibrating and merging several Kinects in TouchDesigner (multiplekinectcalibration_touchdesigner, TDAzureMerger) were explored before the in-house fusion.
  • Switch phases: the first multi-sensor host used one sensor's data at a time and switched between sensors based on margins. This broke with more than one person, or when someone walked from one sensor's field of view into another's.
  • Group-first fusion: the next version grouped the sensors' bodies into people first and matched them with the previous frame afterwards. Numbers jumped (p1 → p2 → p3) whenever a sensor picked someone up or lost them.
  • Track-first fusion (current, since early October 2026): detections are matched with known people first, and new people are confirmed over 5 frames before they get a number. Several people are tracked at once with stable numbers. See Kinect host.
  • Frozen hands: an earlier fusion kept the last known position of joints that every sensor rated 0. Hands froze in place, in one test 2.5 m behind the body. Such joints are now averaged across all sensors.
  • Left/right swap correction: a fix based on the shoulder axis and the confidence of the face joints was tried in September 2026 and reverted, because it didn't improve real results.
  • Lag CHOP → One Euro filter: smoothing moved to the client and from a Lag CHOP to a One Euro filter. In a simulation of a fast hand swing, the lag trailed by 133 ms and the One Euro filter by 50 ms.
  • Camera as a proximity sensor (2023): an early snippet that used a plain camera image to detect nearness, before depth sensing was used.
  • Hardware lifecycle: the Azure Kinect DK went out of production in 2023. The working recommendation for a successor is the Orbbec Femto Bolt (see the Kinect host note).
  • Add: what was tried, what didn't work, and why.

Other sensors and inputs in the lab

The full list is in Inventory.

Comments on Spatial Logic

Earlier versions, previous attempts and related tech.

  • Distance meter: a workshop on triggers based on position tracking, the forerunner of CALC.
  • Four calculators → CALC (October 2026): DIST_between_points, DIST_to_object, IN_or_OUT_object and FADE_to_sphere were merged into one component with one output. Distance now works for any closed shape.
  • SOP → POP (October 2026): virtual objects and the viewer moved from CPU geometry (SOPs) to GPU point operators (POPs), so scenes with many objects run faster.
  • Related: importing 3D meshes or point clouds (a workshop topic) as another way to define shapes in the room. Phone scanning apps (Polycam, Scaniverse) can capture real objects or the room itself for this.
  • Add: what was tried, what didn't work, and why.

Comments on Video

Earlier versions, previous attempts and related tech.

  • Projection: first taught as a manual setup in a workshop (single, double, triple projection). Now covered by the wall and floor projection components, with a home/lab resolution switch.
  • Floor projection: the floor projector has a lower resolution than the walls.
  • Related tools in the lab: MadMapper (projection mapping outside TouchDesigner), Unreal Engine and Unity (game engines as content sources), and Insta360 for recording 360° footage for the panoramic walls.
  • Add: what was tried, what didn't work, and why.

Comments on Audio

Earlier versions, previous attempts and related tech.

  • Spatial audio, 8 → 16 + 1: the lab used to have an octophonic (8-channel) speaker installation. It is now a third-order ambisonic array with 16 speakers and a subwoofer.
  • Renderer development (Aug–Oct 2026): OSC position-intake prototypes (osc_position_intake), then the POS_Ambix iterations, then a version with ambisonic file input, and now Ambix_3th_order-sound_ctrl (iteration 37). Speaker positions were first estimated from a photo and later measured with a calibration tool.
  • Speaker test remote: a small Node.js server inside Max lets a phone or watch step through the speakers (next, previous, toggle) while you stand in the room.
  • Planned: head-tracked headphone audio (Augmented Audio Reality) alongside the speakers.
  • Related tools in the lab: Reaper and Sonic Pi, and an RME Fireface UC for smaller 8-channel setups.
  • Add: what was tried, what didn't work, and why.
Max-MSP ambisonic renderer

Ambix_3th_order-sound_ctrl.maxpat

Host: Windows NUC, Max 9.1.5
Layer: output (sound, back end)
In: OSC positions (UDP 10000) + 16 audio channels over Dante
Out: 16 loudspeaker feeds + 1 subwoofer feed over Dante

This patch is the rendering engine behind Ambisonics sound control. It receives the positions of up to sixteen sound sources and their audio, encodes them into a third-order ambisonic sound field, and decodes that field to the lab's measured, irregular layout of sixteen loudspeakers plus a subwoofer, all in real time. Most users never touch it: the TouchDesigner component is the interface, and the renderer runs on a dedicated machine. Splitting the work this way keeps audio rendering apart from the graphics and tracking load on the main workstation. It also keeps the decoder calibration in one place, independent of any project.

Role in the setup

The renderer is the second half of the lab's spatial sound chain. Ambisonics sound control decides where each sound is and what it plays. The renderer turns that into speaker signals for this particular room. Because the calibration lives here, any TouchDesigner project that sends positions and audio in the expected format sounds correct in the lab without project-specific tuning.

System context

MachineRole
HP Z8 workstationTouchDesigner: tracking, content and source audio. Audio goes onto the Dante network through Dante Via. Positions are sent over OSC.
Windows NUCMax 9.1.5 running this patch. Audio input and output go through Dante Virtual Soundcard in ASIO mode.
Dante networkcarries all audio between the machines and to the amplifiers and loudspeakers at 48 kHz. Routing is managed in Dante Controller.
Authoring machine (Mac)where the patch is edited. It is not part of the live chain. Patch iterations reach the NUC through a shared OneDrive folder.

Signal processing pipeline

  1. Position intake (OSC). udpreceive 10000 receives the positions. The patch routes the addresses /tx1 /ty1 /tz1 … /tx16 /ty16 /tz16 (one triplet per source), a tracked listener /lx1 /ly1 /lz1, a global /gain_range, per-source ranges /range_gain1 … /range_gain16, and /_samplerate (information only).
  2. Cartesian → spherical. Each source position (X = width, Y = height, Z = depth, metres) is converted to azimuth, elevation and distance relative to the reference listening point (X = 0, Y = 1.5, Z = 2.5). A sign-preserving minimum-magnitude floor on the depth term removes the singularity when a source is at exactly the reference depth.
  3. Gain. Each source's level falls with its distance to the listening reference. The gain range is the distance in metres at which the level reaches zero. It is set globally (/gain_range) or per source (/range_gain1 … /range_gain16); per source, it is fed by Rangegain in TouchDesigner. The reference is either fixed or tracked. An inversion toggle mirrors the tracked listener through the room centre to correct for the orientation of the tracking. Direction never comes from the listener, because listener orientation cannot be tracked reliably. A minimum floor on the range parameter prevents a division by zero while the patch loads. A global "gain active" switch sets all sources to full gain for direction-only tests.
  4. Encoding. ambiencode~ 3 16 (ICST) encodes the 16 audio inputs (adc~ 1–16, from Dante) into a third-order AmbiX field: 16 channels, ACN order, SN3D normalisation.
  5. File input. Two looped players (buffer~ + groove~, 16 channels each) add pre-encoded third-order B-format WAV files to the field before decoding. Each player has its own toggle and is off by default. (In development.)
  6. Decoding. ambidecode~ 3 16 (ICST) decodes the field to the 16 measured loudspeaker positions. Each speaker is defined by azimuth, elevation and distance in a decoder grid that can be edited live.
  7. Subwoofer. The omnidirectional W component (ACN 0) is taken from the encoder, low-pass filtered (lores~, 100 Hz) and sent to channel 17.
  8. Output. dac~ 1–17 sends the speaker feeds to Dante.

Loudspeaker calibration

Speaker positions were measured in the room and converted to decoder coordinates with a purpose-built interactive calibration tool, which has a top-down view and an elevation view. The tool exports azimuth, elevation and distance per speaker. These values are typed into the decoder grid, take effect immediately, and need no new patch version. The encoder's speaker-matched default positions (used in calibration mode) are derived from the same data, so the two stay consistent.

#RingXYZAzimuthElevationDistance
1ground−2.60.54.5−127.57°−16.95°3.43 m
2ground−2.60.52.5−90.00°−21.04°2.79 m
3ground−2.60.50.1−47.29°−15.78°3.68 m
4ground00.5−0.30.00°−19.65°2.97 m
5ground2.60.50.147.29°−15.78°3.68 m
6ground2.60.52.590.00°−21.04°2.79 m
7ground2.60.54.5127.57°−16.95°3.43 m
8rear03.07.15180.00°17.88°4.89 m
9high−2.53.73.25−106.70°40.13°3.41 m
10high−2.53.71.75−73.30°40.13°3.41 m
11high−1.253.70−26.57°38.21°3.56 m
12high1.253.7026.57°38.21°3.56 m
13high2.53.71.7573.30°40.13°3.41 m
14high2.53.73.25106.70°40.13°3.41 m
15high1.253.75153.43°38.21°3.56 m
16high−1.253.75−153.43°38.21°3.56 m

Convention: azimuth 0° points from the listening point towards the north wall. Positive azimuth turns towards +X. Z increases towards the entrance. The table also appears in the Lab model.

Operating modes

ModeOptionsDefault
Direction (global and per source)live tracked position, or a fixed position that matches the source's own speaker (calibration)tracked
Test audio (global and per source)live Dante input, or a looped voice recordinglive
Listener referencefixed point, or tracked listener (with optional mirroring)—
Gain activedistance-based gain, or full gain on all sourceson
Ambisonic file players (2)off / playoff

A debug panel shows each source's tracked position, its distance to the listener and the gain value sent to the encoder. This makes reactivity problems visible without console output.

Implementation notes

  • Re-evaluation in expr. In practice, Max's expr only reliably fires on its leftmost inlet. The distance and listener-blend expressions therefore route all their inputs through pak/unpack, which forces re-evaluation whenever any input changes.
  • Sample rate. All content has to arrive at 48 kHz. Dropouts with 44.1 kHz material turned out to be a resampling problem on the sending side, not in the renderer.
  • State. An encoder instance that has been corrupted by invalid coefficients does not recover when the patch file is reopened. Max has to be fully restarted. The same applies to stale test-audio buffers.

Limitations and open questions

  • Directional rendering. Checking the panning across this irregular layout is ongoing work. A controlled comparison with a regular reference layout is planned.
  • Listener model. There is one listening reference. Multiple listeners and listener orientation are outside the current scope.
  • Built-in distance model. ICST's own distance attenuation is disabled in favour of the custom gain model above. It remains an option.
  • Speaker aiming. The decoder stores position only. It does not store per-speaker aim (yaw/pitch).

Outlook

  • A Dante input for B-format audio rendered in TouchDesigner, mixed in at the same point as the file players.
  • A control interface in TouchDesigner, so live operation no longer requires editing the Max patch.
  • A head-tracked headphone mode ("Augmented Audio Reality") alongside the loudspeaker array.

Availability

The renderer is not part of the public Td-updates repository. It runs on the lab's NUC and is maintained by the lab.

Relations

References

  • ICST Ambisonics externals for Max: Institute for Computer Music and Sound Technology, Zurich University of the Arts.
  • AmbiX format: ACN channel ordering, SN3D normalisation.
Ambisonics sound control

Ambisonics_sound_ctrl.tox

Layer: output (sound)
In: one position per source (CHOP) + audio (CHOP)
Out: third-order ambisonic mix on the lab's 16 + 1 loudspeaker array

This component places sound sources in the three-dimensional space of the lab. Each source has its own position, size and audio input, and the component renders them through the lab's third-order ambisonic loudspeaker system. The user works only in TouchDesigner: wire in a position (for example a Virtual objects or a tracked joint), point to an audio signal, and set a range. Encoding, decoding to the irregular speaker layout and transport over the Dante network are handled by a dedicated rendering chain in Max/MSP. This gives everyday users of the lab position-driven spatial sound, which would otherwise require specialist knowledge of ambisonics.

Role in the setup

This is the sound counterpart of Wall projection and Floor projection. It is where the spatial logic of a project becomes audible. A virtual object wired into a source makes that object heard at its location. A tracked joint makes a sound follow a body.

Technical specification

Signal chain

  1. TouchDesigner (HP Z8 workstation). The component holds one source per virtual object slot. Each source has a position (CHOP inlet), an audio input (a CHOP or channel) and a Rangegain. Audio is resampled automatically to the Dante network rate of 48 kHz. The internal network is built and wired by script, which re-checks the structure about once per second.
  2. Transport. Source positions are sent over OSC. Source audio is sent over Dante (Dante Via on the workstation, Dante Virtual Soundcard in ASIO mode on the rendering machine).
  3. Rendering (Windows NUC, Max/MSP). Described in full in Max-MSP ambisonic renderer.
  • Each position is converted to azimuth, elevation and distance relative to a listening reference point.
  • Up to 16 sources are encoded into a third-order AmbiX sound field (ACN channel order, SN3D normalisation, 16 channels) with the ICST ambiencode~ object.
  • The field is decoded to the measured loudspeaker positions with ICST ambidecode~.
  1. Output. There are 16 discrete speaker feeds plus a subwoofer feed. The subwoofer feed is taken from the omnidirectional (W) component and low-pass filtered.

Loudspeaker layout

Room frame: metres. X = width, Y = height, Z = depth. Z = 0 is the north wall, and Z increases towards the entrance. The reference listening point is X = 0, Y = 1.5, Z = 2.5. Azimuth 0° points north, and positive azimuth turns towards +X.

RingSpeakersHeight (Y)Azimuths (°)Elevation (°)
Ground1–70.5 m−127.6, −90, −47.3, 0, 47.3, 90, 127.6−16 … −21
Elevated9–163.7 m±26.6, ±73.3, ±106.7, ±153.438 … 40
Rear83.0 m18017.9
Sub——(omnidirectional, LP-filtered W)—

The layout is irregular: it is set by the room and the entrance gap. Positions were measured with a purpose-built calibration tool and entered into the decoder as azimuth, elevation and distance per speaker. They can be recalibrated live without rebuilding the patch.

Gain model

Rangegain sets each source's size and gain. If a virtual object is wired in, Rangegain follows that object's Radius (shown as a bind), so a source's acoustic "size" matches its spatial size. In the renderer, each source's level is scaled by its distance to the listening reference. That reference is either a fixed point or a separately tracked listener. Distance affects loudness only: direction always comes from the source position, never from the listener. This is because listener orientation cannot be tracked reliably. A global bypass forces all sources to full gain, so direction can be tested without level changes.

Test and development modes

  • Direction mode: live tracked position, or a fixed default that matches each speaker (for calibration).
  • Test audio: a looped voice recording replaces the live input, for testing without performers.
  • Ambisonic file input: two players for pre-encoded 16-channel third-order B-format WAV files, mixed into the decoder alongside the live sources (in development).

Parameters (TouchDesigner component)

ParameterFunction
Add virtual object (+/−)adds or removes sources. Each source gets an inlet, top to bottom in source order.
Posinread-only: shows where each source's position comes from
Rangegainsize and gain of the source. Bound to a wired virtual object's Radius, otherwise typed in.
Audio inthe CHOP or channel carrying the source's sound. Resampled to 48 kHz.

Outputs

  • The ambisonic mix of all sources on the speaker array (16 + 1 channels).
  • A 3D view inside the component: one sphere per source, sized by Rangegain.

Limitations and expected behaviour

  • Posin shows the middle of the room: nothing is wired into that source's inlet.
  • Rangegain is a plain number instead of a bind: nothing is wired in, or the input is not a virtual object.
  • A new or removed source takes a moment to appear: the script rebuilds about once per second.
  • A manual rewire inside the component reverts: the script restores the internal chain. Never rewire it by hand.
  • Sample-rate mismatch: 44.1 kHz material has to be resampled to 48 kHz before it reaches Dante. The component does this for its own inputs.
  • Single listening reference: the decoder is optimised for one central listening area. Listeners far from the centre will hear the spatial image less precisely, which is normal for loudspeaker-based ambisonics on an irregular layout. Checking directional rendering on this layout is ongoing work.

Outlook

Planned work includes:

  • a TouchDesigner control interface for the renderer
  • a direct Dante input for B-format audio rendered in TouchDesigner
  • per-speaker aiming data
  • a headphone-based, head-tracked "Augmented Audio Reality" mode alongside the loudspeaker system

Relations

References

  • User README: Ambisonics_sound_ctrl-README
  • Repository: <https://github.com/SpatialComputingL/Td-updates>
  • ICST Ambisonics externals for Max (Zurich University of the Arts)
  • Overview: Lab Tools

All TouchDesigner components, READMEs and the example project: github.com/SpatialComputingL/Td-updates

DOCUMENTATION

Inventory

Info

This note lists the software, hardware and applications available in the Spatial Computing Lab, together with the learning resources that support them. It starts with the fixed installation, which is the hardware the Lab Tools components are built for. After that come the equipment around it (alternative sensors, interfaces and capture devices) and the software used for content, mapping and sound.

Software

Software that runs in the lab. TouchDesigner runs the lab's own components (Lab Tools), and Max/MSP runs the Max-MSP ambisonic renderer.

CategorySoftware
GeneralTouchDesigner, Max/MSP, Adobe Creative Cloud
Projection mappingMadMapper
Game enginesUnreal Engine, Unity
AudioReaper
Lab infrastructureDante Via, Dante Virtual Soundcard and Dante Controller (audio network), NDI (video over the network), ICST Ambisonics externals for Max

Hardware

Fixed installation

The room's own hardware, set up permanently.

SubsystemHardware
Projectionthree fixed walls (left, centre, right), a floor projection with a lower resolution than the walls, and a mobile fourth wall (not installed by default)
Spatial sound16 loudspeakers in a third-order ambisonic layout plus 1 subwoofer, on a Dante audio network at 48 kHz
Body tracking and cameras3 × Azure Kinect on drop rods below the ceiling grid
ComputingHP Z8 workstation (TouchDesigner); Intel NUC (Kinect host); Windows NUC (ambisonic renderer)
Networklocal network (router) for OSC and NDI; Dante network for audio
Riggingscaffold-pipe grid below the concrete ceiling, carrying sensors, elevated speakers and projectors

Equipment

Tools you can use in the lab.

CategoryHardware
ComputingNUC (bare-bone workstation), HP ZBook (laptop)
MicrocontrollersRaspberry Pi 3 and 4B, Arduino
Depth camerasMicrosoft Azure Kinect (three installed, see above), ZED 2i, OAK-D
Hand trackingLeap Motion Controller
CamerasRGB cameras, Insta360 (360° camera)
MIDI controllersKORG nanoPAD, KORG nanoKONTROL, Intech Studio, Midi Fighter
InterfacesFull HD touchscreens
AudioRME Fireface UC (8-channel audio interface)
Networkrouter (local network)

Applications

Small software, plug-ins and apps.

CategoryApplication
InterfacesTouchOSC
Tracking and dataZIG SIM, ZIG SIM Pro, MediaPipe (in TouchDesigner)
3D scanningPolycam, Scaniverse
AudioSonic Pi

Learning resources

External links for inspiration

  • OLIB: TouchDesigner components
  • Derivative: TouchDesigner download, tutorials and forum
  • AllTouchDesigner: learning community
  • YouTube channels: The Interactive & Immersive HQ, Bileam Tschepe, Matthew Ragan, Supermarket Sallad, Torin Blankensmith

Learning material (on the lab's SharePoint, for LUCA users)

  • TD Fundamentals 100 and 200
  • TouchDesigner POP examples and Learning About POPs
  • The Interactive & Immersive HQ templates
  • a TouchDesigner book and a beginner course

Snippet library (on the lab's SharePoint, for LUCA users)
A collection of small TouchDesigner examples and community components. It includes MediaPipe, multi-Kinect calibration and merging, OSC tools, UI layouts and utility patches such as counters, value mapping and camera fly-throughs.

Relations

Virtual object viewer

virtual_object_viewer.tox

Layer: spatial logic (verification)
In: positions (CHOP), shapes (POP)
Out: 3D preview

The viewer shows tracked points and virtual objects together in one 3D scene of the lab. Its only job is to check that everything is where you expect it to be. It does not change any data. The physical room is otherwise blind to what the system "thinks", and the viewer makes that state visible. Most spatial interaction design depends on this view for debugging and for explaining a setup.

Role in the setup

Interaction logic in the lab is invisible: virtual objects have no physical form, and tracking errors cannot be seen in the room. The viewer is where the sensor layer and the spatial-logic layer can be checked together before content is connected to the outputs.

Technical specification

  • Scene. A wireframe box shows the physical room. Inside it, every tracked point wired into the green inputs is drawn as a sphere, and every Virtual objects in the project is drawn in its own colour.
  • Automatic discovery. Virtual objects in the project are found automatically. They do not need to be wired one by one.
  • Viewports. The perspective view gives a quick overall check. The top view works as a live floor plan, which is useful for placing zones relative to the entrance and the projection surfaces.
  • Read-only. The component has no side effects. You can add or remove it at any time without changing a patch's behaviour.

Parameters

ParameterFunction
Viewportperspective (quick check) or top (floor plan)
Colorsoff = grey, on = one colour per object

Outputs

  • Preview: a 3D render of the scene. The node preview is low resolution; open a larger viewer when you need detail.

Limitations and expected behaviour

  • Everything in one spot: nothing is wired, or the sensor isn't running.
  • Spheres frozen: the source is frozen. Check the sensor.
  • A sphere outside the box: the position is out of range, or outside the sensor's field of view.
  • Low-resolution preview: this is a node preview. Open a bigger viewer.

Relations

References

Wall projection

wall_projection.tox

Layer: output (image)
In: 1–3 TOPs
Out: projection window + resolution CHOP (resX, resY)

This component is the output stage for the lab's wall projectors. It opens the projection window, sends the images to the wall surfaces, and tells the user the exact resolution the source images must have, both at home and in the lab. The resolution comes from the component as a CHOP that the user wires into their own source. This keeps content and output in sync by design, so there is no stretching, cropping or black bars.

Role in the setup

Together with Floor projection and Ambisonics sound control, this component forms the lab's output layer. Any image from a project, whether generated, filmed (Azure Kinect cameras) or controlled by interaction logic, reaches the walls through this component.

Technical specification

  • Projection modes.
  • Three screens (default, both switches off): three separate images on the left, centre and right walls (inputs 1, 2 and 3).
  • Panoramic projection: one wide image across the whole wall surface from input 1. Inputs 2 and 3 are ignored.
  • Single projection: output to one projector only, for tests and small setups.
  • Wall layout. The three channels correspond to the three fixed walls: left, centre and right as seen from the entrance (west, north and east). The fourth (south) wall is mobile and is not installed by default. When a project uses it, its output is set up for that project and is not part of this component's standard configuration.
  • Resolution contract. The resolution CHOP (resX, resY) always gives the resolution for the current mode and licence. Users wire it into their source TOP instead of typing numbers. If the mode or licence changes, the content follows automatically.
  • Home/lab switch. License off gives a reduced home resolution that stays within the 1280 × 1280 limit of TouchDesigner's free Non-Commercial licence. On gives the full lab resolution, which the lab workstation's Educational licence allows. The patch stays the same either way.
  • Fit Input sets what happens to an image of the wrong size, as a safety net. Following the resolution CHOP is still the preferred way to work.
  • Window management. Open Window and Close Window switch the projection on and off. Keep the window closed while editing, so that it does not take focus from the TouchDesigner editor.

Parameters

ParameterFunction
Licenseoff = home resolution, on = full lab resolution
Panoramic projectionone wide image across the wall, uses input 1 only
Single projectionoutput to one projector only
(both off)three screens: left, centre, right
Fit Inputwhat happens to an image of the wrong size
Output Resolutionreadout of the current resolution
Configurerebuilds after a mode change
Open Window / Close Windowopens or closes the projection

Outputs

  • Resolution CHOP: resX, resY for the user's source TOP.
  • The projection window on the wall projectors.

Limitations and expected behaviour

  • Small resolution: License is off (home setting).
  • Inputs 2 and 3 do nothing: Panoramic is on.
  • Stretched, cropped or black bars: the source isn't following the resolution CHOP.
  • Content on the wrong wall: the input order is left, centre, right.
  • Nothing visible after a change: press Configure, then Open Window.

Relations

References

Azure Kinects calibration

HOST.onstartup.toe

Host: Intel NUC, TouchDesigner
Layer: sensing (back end)
In: 3 × Azure Kinect
Out: fused skeletons (OSC), camera images (NDI)

The Kinect host is the computer that the three Azure Kinect sensors are connected to. It runs a TouchDesigner project that reads each sensor, calibrates its data into the room's coordinate frame, decides which bodies seen by which sensors belong to the same person, fuses those views into one skeleton per person, and publishes the result to the lab network. Users never open it. The Azure Kinect sensors and Azure Kinect cameras components on the workstation are its clients. Keeping sensing on its own machine means the processing for depth, body tracking and video encoding does not compete with rendering on the main workstation. It also means every project in the lab receives the same calibrated data.

Role in the setup

The host turns three independent sensors into one room-wide tracking system. It does three things:

  • Calibration: each sensor's data is moved from the sensor's own viewpoint into the shared room frame (metres; X = width, Y = height, Z = depth; Z = 0 at the centre wall).
  • Decision making and fusion: every frame, it decides which detections are the same person, which person they are, and which sensor to trust for each joint. Each person appears once, with a stable number (p1 … p8).
  • Distribution: every joint of every person goes out over OSC, together with presence flags and a heartbeat. Camera images go out as NDI video streams.

The host deliberately makes no choice on behalf of the user. It always sends everything. Which person and which joints a project uses is decided on the client side (see Azure Kinect sensors), so several projects or users can use the same stream at the same time.

Technical specification

Signal chain

Kinect left  ─► Kinect Azure CHOP ─► transformxyz_Y2  (pose left)  ─┐
Kinect right ─► Kinect Azure CHOP ─► transformxyz_Y19 (pose right) ─┼─► fusion (Script CHOP) ─► OSC Out ─► lab network
Kinect rear  ─► Kinect Azure CHOP ─► transformxyz_Y13 (pose rear)  ─┘

There is no selection, renaming or smoothing between these nodes. All of the decision making happens in one place: the Script CHOP fusion inside Sensor_Azure_Multiple_HOST, with its code in the DAT fusion_callbacks. Smoothing is done on the client, not here, so the host adds no delay.

Sensors

Each Kinect Azure CHOP runs Microsoft's body tracking for its own sensor. Relevant settings:

  • World Space Positions on. Joint positions in metres, relative to that sensor.
  • Bone Confidence on. Adds a :confidence channel per joint (see Fusion per joint).
  • Max Players. The number of bodies one sensor tracks. Each extra body costs GPU time on the NUC.

Per tracked body, a sensor outputs p{k}/id, and for each of its 32 joints p{k}/{joint}:tx/ty/tz/confidence. The k here is the sensor's own body number. It means nothing outside that sensor (see Decision making).

Calibration

The sensors hang on drop rods below the pipe grid, 3.03 m above the floor. All three face the centre wall and tilt down by about 45°.

SensorXYZRotate (Rx, Ry, Rz)
Left−1.253.034.4045°, 180°, 0°
Right1.253.034.4043.5°, 180°, 0°
Rear centre03.036.3145.87°, 180°, 0°

Each pose is stored in a Transform XYZ CHOP per sensor (transform order Scale Rotate Translate, rotate order Rx Ry Rz). The Transform CHOP turns that sensor's skeleton data from sensor coordinates into room coordinates. Calibration therefore comes down to these six values per sensor. If a sensor is moved, only its own Transform CHOP needs updating. The poses are also shown in the Lab model.

Calibration is what makes the decision making possible: only after this step can a position measured by one sensor be compared with a position measured by another. Any remaining calibration error shows up as the same person being placed in slightly different spots by different sensors. In a test walk (25 September 2026) that difference was 0.1–0.5 m, with the largest differences always involving the left sensor (about 0.3 m in X and 0.1 m in Y).

Decision making

The problem

Microsoft's body tracking runs separately on each sensor, and each sensor numbers the bodies it sees on its own. "Body 1" on the left sensor and "body 1" on the right sensor are unrelated labels. The same person usually has a different number on each sensor, and that number changes when they leave a sensor's view and come back.

An earlier version of the host worked around this by picking one sensor's data per moment ("switch phases" based on margins). That broke as soon as more than one person was in the room, or someone walked from one sensor's field of view into another's.

The host therefore ignores the sensors' own body numbers and decides identities itself, based only on where bodies are in the room. Every frame, fusion answers four questions:

  1. Which detections are real?
  2. Which detections belong to a person who is already known?
  3. Which of the remaining detections are new people, and are they real?
  4. For each joint of each person, which sensors should be trusted?

The answers are kept between frames as a list of tracks. A track is one person: their number (or none yet), their last fused pose, how many frames in a row they were seen, and how many frames they have been missed.

Step 1: valid detections

From each of the three inputs, every body slot is read. A body counts as a detection only if:

  • its id is greater than 0 (the Kinect reports 0 for an empty slot), and
  • it has a pelvis joint.

Positions can't be used to recognise an empty slot, because the calibration transform turns the sensor's zeros into the sensor's own position in the room.

The pelvis is used as the reference point for all comparisons in steps 2 and 3. It is the root of the skeleton, the most stable joint, and the one the Kinect rates with the highest confidence.

Step 2: known people first (track-first matching)

Every detection is first compared with every existing track, using the distance between the detection's pelvis and the track's pelvis from the previous frame. All pairs are sorted from closest to farthest and handed out in that order (a greedy nearest-neighbour assignment):

  • A pair is accepted if the distance is under MATCH_DIST = 0.75 m. That is far more than anyone moves in one frame, and less than the usual distance between two people.
  • One body per sensor per track. A sensor can't contribute two bodies to the same person.
  • One track per body. Once a detection is used, it's gone.
  • On equal distance, a confirmed person wins over a candidate that has no number yet.

Then a second pass catches sensors that disagree more because of calibration error: a detection that is still unused may join a confirmed person within MERGE_DIST = 1.20 m, as long as that person has no body from that sensor yet. Without this pass, a badly calibrated sensor would keep creating a second, ghost person next to the real one.

Why track-first. The first version did it the other way around: it grouped the sensors' bodies into people first, and compared those groups with the previous frame afterwards. That caused numbers to jump (p1 → p2 → p3) every time a sensor picked someone up or lost them. When a sensor joined, the combined position shifted, or a sensor that briefly disagreed formed its own group and "stole" the number. In the current order, a sensor joining or leaving only adds a body to an existing person or takes one away. It can never create a new number for someone who is already tracked.

Step 3: new people, with confirmation

Only detections that matched no track can start a new person. They are grouped first: a detection joins the nearest group whose average pelvis is within MERGE_DIST, provided that group has no body from that sensor yet. Each group becomes a candidate: a track without a number.

A candidate gets a number only after it has been seen in CONFIRM_FRAMES = 5 frames. A candidate missed for more than TENTATIVE_DROP = 2 frames is discarded. This filters out one-frame glitches, such as a coat on a chair briefly recognised as a body, so they never take a number or reach the clients.

Which number. A confirmed candidate gets the lowest free number from 1 to MAX_SLOTS = 8. The first person in an empty room is always p1. That way a client set to Person 1 always gets someone as long as anyone is in the room. When a person leaves, the others keep their numbers: nobody is renumbered, and the free number goes to the next person who enters.

Step 4: keeping and releasing people

A confirmed person who isn't matched in a frame keeps their last fused pose and their number. They are released only after DROP_FRAMES = 15 frames in a row without a match (about 0.5 s at the host's 30 fps). A short occlusion, such as walking behind another person, therefore doesn't cost anyone their number.

Step 5: fusion per joint

For each person, every joint is fused from the bodies attached to that person in this frame. The Kinect rates every joint: 0 = none (out of range), 1 = low (occluded, predicted), 2 = medium. The decision per joint:

  1. Find the best confidence level any of the sensors reports for this joint.
  2. Use only the sensors at that level, and ignore the rest.
  3. Take their weighted average (weights: level 2 = 1.0, level 1 = 0.3).

If every sensor rates a joint 0, it is still averaged across all sensors rather than frozen. This matters for hands: the Kinect very often rates hand, hand tip and thumb as 0, even when the hand is clearly visible. An earlier version kept the last known position for such joints, and hands froze in place (in one test 2.5 m behind the body).

Output

Only confirmed people are sent, in number order:

ChannelContent
p{N}/{joint}:tx, :ty, :tzevery joint of every confirmed person, metres, room frame
p1/present … p8/present1 while that number belongs to someone, otherwise 0. Always sent, also as 0.
personshow many people are seen in this frame
heartbeata counter that changes every frame while the host runs

The presence flags and the heartbeat exist because of how OSC behaves on the receiving side: a channel that stops arriving keeps showing its last value forever. Without them, a client can't tell a person who left from one standing still, or a crashed host from a quiet room.

Timing at a glance

EventTakes
Someone enters the empty roomabout 5 frames (0.17 s) until p1 appears
A second sensor picks up a tracked personimmediately, same number
A person is briefly hiddenkeeps number and last pose for up to 15 frames (0.5 s)
A person leavesnumber released after 0.5 s, present goes to 0
A one-frame false detectiondiscarded, never gets a number

Worked example

Two people, A and B, in an empty room:

  1. A walks in on the left. The left sensor sees one body. No tracks exist, so it becomes a candidate. After 5 frames it becomes p1.
  2. A walks to the middle. The rear sensor now also sees A, 0.3 m from where the left sensor places A. In step 2 that body is within 0.75 m of p1, so it simply joins p1. A's pelvis is now fused from two sensors.
  3. B enters on the right, 3 m from A. The right sensor's body is far from p1 (no match), so it becomes a candidate and, 5 frames later, p2.
  4. A leaves. After 15 frames without a match, p1 is released and p1/present goes to 0. B stays p2.
  5. C enters. C gets the lowest free number: p1.

Tuning and diagnosis

All constants are at the top of fusion_callbacks:

ConstantDefaultRaise it when…Lower it when…
MATCH_DIST0.75 mfast movement makes people get new numberstwo people close together swap numbers
MERGE_DIST1.20 mone person shows up twicetwo people standing close are merged; possible once the spread stays under 0.25 m (then about 0.6 m)
CONFIRM_FRAMES5glitches still take numbersnew people appear too late
DROP_FRAMES15people lose their number behind otherspeople who left linger too long
MAX_SLOTS8——

With DEBUG = True, the Textport prints once per second how many bodies each sensor sees and every track as (number, frames missed), where 0 means a candidate without a number. For every person seen by more than one sensor, it also prints where each sensor puts their pelvis and the spread between them: a direct measurement of the calibration. Under about 0.3 m is good. Regularly above 0.5 m means a Transform needs re-measuring; the line shows which sensor differs.

Why this design

  • Identities come from positions in the room, not from any single sensor. That makes the method independent of the hardware: the same code works with Azure Kinects or with a successor such as an Orbbec sensor (see Decision making: sensor succession).
  • Three overlapping views mean fewer dropouts when one sensor's view is blocked, and confidence-based fusion takes each joint from the sensors that see it best.
  • Track-first matching keeps numbers stable while people walk between sensor views.
  • The host never filters for a user. Everything is sent; clients choose. One stream serves any number of projects.

Network interface

All traffic stays on the lab's local network. Commands from the workstation are sent as broadcasts, so the components don't need to know the host's IP address.

DirectionProtocol / portContent
host → workstationOSC, UDP 1030every joint of every person (p{N}/{joint}:tx/ty/tz), p{N}/present, persons, heartbeat
host → workstationOSC, UDP 1031tracking frame rate
host → workstationNDIone video stream per sensor, named after the host and the sensor: <host> (AK1), (AK2), (AK3)
workstation → hostOSC broadcast, UDP 1051Configure (re-initialises the sensors), sent from Azure Kinect sensors
workstation → hostOSC broadcast, UDP 27Image, the stream type chosen in Azure Kinect cameras

What the client does with this stream (choosing a person and joints, detecting stale data, smoothing, sharing one receiver between copies) is described in Azure Kinect sensors.

Decision making: sensor succession

The Azure Kinect DK went out of production in 2023, and an Orbbec sensor is on the lab's purchase planning for 2026. The options discussed at the workshop meeting of 24 September 2026:

OptionAssessment
Orbbec Femto Bolt (×3) with a sync hubThe lower-risk choice for skeleton tracking. It works the way the Azure Kinect does, so the host and its fusion stay as they are.
Active USB-C cables (e.g. Newnex FireNEX-uLINK-C2)Carry USB 3.2 Gen 1 data and bus power up to 30 m, which is enough for the ceiling-grid mounting. Some users report the host not recognising a camera until it is restarted or moved to another port, so the exact cables should be tested before buying three.
PCIe USB card with one controller per portSeparate USB ports on a PC can share one host controller inside, which limits how many cameras run at once. A card with a controller per port has been verified with four Femto Bolts.
Orbbec Femto Mega (Ethernet)Only worth considering after testing Ethernet body tracking in TouchDesigner with one borrowed unit.

Working recommendation: three Femto Bolts, a sync hub, three tested active USB-C cables and a PCIe USB card with a separate controller per port, on a Windows host. Because the fusion works on room coordinates, swapping the sensors changes nothing for the clients or for projects built on them.

Limitations and open questions

  • Calibration is static. The poses are fixed values in the Transform CHOPs. If a sensor is bumped or moved, its pose has to be re-measured, or the fused skeletons drift apart. The left sensor currently differs most from the other two (see Calibration).
  • Left/right swaps. Body tracking assumes the person faces the sensor. Seen from behind, or when someone pulls up their shoulders so the neck "disappears", a sensor can return a mirrored skeleton with left and right exchanged. A correction based on the shoulder axis and face-joint confidence was tried and reverted in September 2026 because it didn't improve real results.
  • Skin, hair and clothing. The depth camera measures reflected infrared light. Dark hair, long hair (which also hides neck and shoulders), darker skin and dark clothing return less of it, so skeletons are less reliable for those people. This is a limitation of the sensor type and needs testing with sensor placement and settings.
  • Close contact. People closer together than the merge distance can be merged into one person. When they separate, one of them gets a new number.
  • Numbers are tracks, not individuals. Someone who leaves and comes back may get a different number. Content should not assume that a particular visitor keeps a particular number for a whole session.
  • Hands. Hand joints are rated uncertain by the Kinect most of the time, so they are noisier than torso and head. Wrists are a steadier alternative.

Relations

Azure Kinect sensors

Azure_Kinect_sensor.tox

Layer: sensing
In: none
Out: one CHOP per selected joint (tx ty tz, metres)

This component gives live three-dimensional positions of human body joints, measured by the lab's Azure Kinect sensors. You choose which joints you need, and each one gets its own outlet as a CHOP with the position in room coordinates. Several people can be tracked at the same time (active since the first week of October 2026): each copy of the component follows one person. When you are away from the lab, a mouse mode stands in for the tracked body. This lets you develop and test interaction logic without sensors, and the patch keeps the same structure in the lab.

Role in the setup

The component is the main way bodies enter the system. Its outputs feed:

It is the client of the Kinect host. The host decides who is in the room and sends every joint of every person. This component decides what this project uses: which person, which joints, and what to output when there is no usable data. It covers position only. For images from the same sensors, use Azure Kinect cameras.

Technical specification

Inside the component

OSC In (all_oscin1) ─► null1 ─┐                                 ┌─► joint_sel_0 ─► out_0
   or, in a copy:             ├─► joints_resolved (Script CHOP) ─┼─► joint_sel_1 ─► out_1
   shared_stream (Select) ────┘        ▲ input 1                 └─► …
Mouse_base (tx ty tz) ─────────────────┘
joint_utils (shared code) · joint_watch (Execute DAT, every frame) · fps_oscin ─► merge_view
  • joints_resolved (Script CHOP, code in joints_resolved_callbacks) makes every per-frame decision about values: live, mouse or fallback, and smoothing.
  • joint_watch (Execute DAT) maintains the structure: it builds the outlets from the joint list and decides which copy is the receiver.
  • joint_utils holds the code both share: reading the joint list and the parameters, and finding the receiver.

The joint list on the component (Add joints) is the single source of truth. It is saved with the project, and nothing that arrives from the host can change it. That is what keeps the list editable without a connection, and what keeps the outlets from disappearing when the connection drops.

Source data

The host sends every joint of every tracked person over OSC (UDP 1030), named p{N}/{joint}:tx/ty/tz, plus p{N}/present and a heartbeat (see Azure Kinects calibration). Joints are matched by name, never by position in the stream, so the order of joints on the host and in the component never has to agree.

Coordinate frame

Output values are in metres in the lab's room frame: X = width, Y = height, Z = depth. This is the same frame used by Virtual objects and the sound system, so you can compare joint and object positions directly without converting.

Decision making

Per joint, every frame

joints_resolved cooks every frame (it reads the clock on purpose, so it also notices when the stream stops). For each joint in the list it decides, in this order:

Mouse on? ──yes──► mouse position (same value for every joint)
   │ no
Usable live data for this person? ──yes──► tracked value ─► One Euro smoothing
   │ no
Fallback origin on? ──yes──► Position
   │ no
0 0 0

What counts as "usable live data"

OSC has one property that drives most of this design: a channel that stops arriving keeps showing its last value forever. A person who left, a frozen host and a dropped network all look like someone standing perfectly still. The component therefore treats live data as unusable when any of these is true:

  1. No stream is connected.
  2. The person is absent: the host's p{N}/present for this component's Person number is 0.
  3. This person's values have stopped changing for 0.5 s (STALE_SEC). Real tracking always jitters by a fraction of a millimetre, so values that are exactly constant mean the data has stopped. This check works even with an older host that sends no presence flags or heartbeat.
  4. The host's heartbeat hasn't changed for 0.5 s: the host has stopped, frozen or lost the connection.
  5. None of the chosen joints exist in the stream for this person.

If live data is unusable, every joint of this component goes to the fallback at once. The outlets themselves never change; only their values do.

Smoothing

Live values pass through a One Euro filter (Casiez et al., CHI 2012), per channel. It is an adaptive low-pass filter whose cut-off frequency rises with the speed of the joint:

  • When a joint is (nearly) still, the cut-off is low (MIN_CUTOFF), so sensor jitter disappears.
  • When it moves fast, the cut-off rises by BETA × speed, so the filter hardly smooths and adds almost no delay.

A Lag CHOP smooths everything equally, so it always trails behind fast movement. In a simulation with 1 cm sensor noise and a fast 1 m hand swing, both reduced jitter at rest from 8.6 mm to about 2 mm, but the lag trailed by 133 ms and the One Euro filter by 50 ms. There are no Lag CHOPs left in the chain, on the host or in this component.

Two details matter in practice:

  • The filter only updates when a new sample arrives. The sensors deliver 15–30 samples per second while TouchDesigner runs at 60 fps. Repeated values between samples would otherwise read as "standing still" and distort the speed estimate.
  • The filter resets whenever live data stops. A person who comes back snaps to their real position instead of gliding in from the fallback origin.

The constants are at the top of joints_resolved_callbacks: MIN_CUTOFF (Hz), BETA and D_CUTOFF. Tune in this order: stand still and lower MIN_CUTOFF until the jitter is gone, then move fast and raise BETA until the lag is gone. SMOOTHING = False switches the filter off for comparison. Mouse mode and the fallback are never smoothed.

Outlets: when the structure changes

joint_watch compares the joint list and the Person number with the previous frame. Only when one of them changes does it rebuild:

  • one Select CHOP (joint_sel_i) per slot, picking p{Person}/{joint}:tx/ty/tz and renaming it to tx ty tz
  • one Out CHOP (out_i) per slot, labelled with the joint name

Existing Out CHOPs are reused, never recreated, and only outlets beyond the end of the list are removed. Wires from a student's patch therefore stay connected when the list changes, and nothing that arrives from the host (people entering or leaving, a dropped connection) can ever change the outlets. A joint chosen twice gets one channel in the viewer, but each slot keeps its own outlet. Configure has nothing to do with this: it only re-initialises the sensors on the host.

Several people: one receiver, many copies

Each copy of the component follows one person, set with Person. The copies share one network connection, because two OSC In CHOPs can't listen on the same port. The decision is made automatically by joint_watch, about once per second:

  1. Find all copies of the component in the project (every component with a joints_resolved and an OSC In).
  2. The oldest copy is the receiver: the one with the lowest operator ID, normally the original.
  3. The receiver keeps its OSC In, OSC Out and Configure active, and its fps_oscin plugged into the view.
  4. Every other copy:
  • switches its OSC In and OSC Out off;
  • greys out Configure, because only one component should be able to re-initialise the host's sensors;
  • unplugs fps_oscin from merge_view;
  • creates a blue Select CHOP, shared_stream, that reads the receiver's stream, and wires it into joints_resolved. Its comment says which component it reads from.
  1. If the receiver is deleted, the next oldest copy becomes the receiver within a second: it removes its shared_stream, reconnects its own stream and switches everything back on.

Nothing is deleted in a copy, so every copy stays a complete component. Settings are found by their label in the panel, not by their internal name (Mouse is internally Sensor, Joint is Skeleton, Fallback origin is Index, Position is Posu/Posv/Posw).

Simulation

With Mouse on, every joint follows the mouse in a chosen plane (Mouse orientation, for example XZ = across the floor), already mapped to room metres by Mouse_base. This lets you test downstream logic without the lab. The mouse is never used as a fallback when Mouse is off.

Parameters

ParameterFunction
Mouseoff = sensor data. On = every joint follows the mouse.
Mouse orientationthe plane the mouse moves in (e.g. XZ = floor plane). Only active when Mouse is on.
Personwhich tracked person (1–8, i.e. p1 … p8) this copy outputs. For several people, use one copy per person.
Add joints (+/−)adds or removes joint slots. Each slot has a joint selector. Outlets follow automatically.
Configurere-initialises the sensors on the Kinect host. Only active in the receiving copy.
Fallback origin / Positionwhere joints rest when there is no usable data. Off = 0 0 0.

Outputs

  • One outlet per slot, in slot order, each with channels tx ty tz in metres.
  • The component's viewer shows the chosen joints with their full names (p1/head:tx, …).
  • Recommended convention: attach a null named after the joint (null_head, null_handl, …) to each outlet and reference that null downstream.

Limitations and expected behaviour

  • Values at 0 or at Position: you are outside the lab, nobody with that Person number is in the room, or no new data has arrived for 0.5 s. Turn on Mouse to keep working.
  • Numbers are tracks, not individuals: someone who leaves and comes back may get a different number. When person 1 leaves, person 2 keeps their number and p1 stays empty until someone new arrives. How numbers are assigned is explained in Azure Kinects calibration.
  • Hands are less steady than head and pelvis: the sensors rate hands as uncertain most of the time. Wrists are steadier and move almost the same.
  • Left/right swaps when someone turns their back to the sensors, and less reliable tracking for dark or long hair, darker skin and dark clothing: limitations of the sensors, see Azure Kinects calibration.
  • External .tox: the component is loaded from Azure_Kinect_sensor.tox, and copies are loaded from that file. After changing the component, save it back to the .tox from the original, never from a copy, and make new copies.
  • Hardware lifecycle: Microsoft stopped producing the Azure Kinect DK in 2023. In the long run the lab will need a successor sensor. Because the component's output is a sensor-independent CHOP interface, patches built on it should keep working when the hardware changes.

Relations

References

Lab model

Info

This 3D model is an interactive spatial reference for the Spatial Computing Lab. It shows the three fixed projection walls, the floor projection, the mobile fourth wall, the ceiling grid, the sixteen loudspeakers, the Azure Kinect sensors and the reference listening point, all in the room coordinate frame used by every lab component. It is meant to answer where is what? before a visit: to plan where zones and virtual objects go relative to the walls and the entrance, to understand the speaker layout behind Max-MSP ambisonic renderer, and to compare positions in TouchDesigner with the physical room.

Contents and sources

ElementSourceStatus
Room volume 5 × 5 × 3.5 m, three fixed walls, floorlab documentationmeasured
Mobile fourth wall (south)lab documentationshown on demand; not installed by default
16 loudspeakersspeaker calibration (iteration 22)measured
Reference listening point (0, 1.5, 2.5)ambisonic rendererdefined
Ceiling grid (scaffold pipes below the concrete ceiling)photoschematic: height and spacing not measured
3 Azure Kinect sensors (position and tilt)sensor Transform CHOPs in TouchDesignercalibrated

Approximate elements are drawn faded and labelled approx. in the viewer. The subwoofer is not drawn: it radiates omnidirectionally and its position has no effect on decoding.

Coordinate frame

Metres. X = width (west −, east +), Y = height, Z = depth. The origin is on the floor at the middle of the north (centre) wall. Z increases towards the entrance (south). This is the same frame used by Azure Kinect sensors, Virtual objects and the sound system, so a position read in the viewer can be typed straight into a component.

Using the viewer

  • Views:
  • 3D overview
  • Floor plan: top-down, with north at the top
  • From entrance: what a visitor sees on entering
  • Listener: from the reference listening point, looking north
  • Layers: walls and floor, mobile wall, loudspeakers, sensors, ceiling grid, helpers and labels can each be switched on or off.
  • Details: click any object to see its coordinates. For speakers this includes azimuth, elevation and distance to the listening point.
  • Download .glb: exports the geometry as a standard glTF binary, for use in Blender, game engines or other 3D tools.
  • Add ?view=top (or entrance, listener) to the URL to open the viewer on a specific view.

Technical notes

  • One small file. The model is not a stored mesh. It is built in the browser from a short data block at the top of the HTML file, with three.js loaded from a CDN. The file is about 15 KB, so it hardly uses any of the website's storage. To update a position, edit the numbers in that data block. Nothing else needs to change.
  • Not a scan. The model is schematic. A photogrammetric or LiDAR scan of the room can be added later as a .glb and loaded into the same viewer.

Viewer

The interactive viewer is next to this note on the map.

Relations

DOCUMENTATION

Infrastructure
Spatial logic
Sensing
Projects & partners
Output Ambisonics audio
Output: 270° Video projection
Simulation
Hardware & Software
Education