27 nodes · 9 groups · 22 connections
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/>
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.
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.
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.
Let A be input 1 and B be input 2.
center_dist = ‖c_A − c_B‖ (metres).inside = 1 if A is inside B, otherwise 0.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.| Parameter | Function |
|---|---|
| Distance between points | distance between the centres of both inputs |
| Distance between shapes | signed distance to the surface of the shape(s) |
| In or outside of a shape | containment test: is input 1 inside input 2 |
| Fade to sphere | smooth value between inner sphere and outer shell |
| Channel | Unit / range | Meaning |
|---|---|---|
center_dist | metres, ≥ 0 | distance between the centres |
shell_dist | metres; negative = inside | signed distance to the surface |
inside | 0 / 1 | containment |
fade | 0 … 1 | 1 inside the inner sphere, 0 outside the outer shell |
inside output.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.
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.
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.
<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.| Parameter | Function |
|---|---|
| Image | the stream delivered (colour, …) |
| Configure | rebuilds the outlets after Image changes |
null to each outlet (null_cam_left, null_cam_right, …).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:
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.
The components are tied to one physical space and its hardware:
| Subsystem | Configuration |
|---|---|
| Room | approx. 5 × 5 × 3.5 m (width × depth × height). The entrance (the "gap") is on the south side. |
| Rigging | a grid of scaffold pipes suspended below the concrete ceiling. It carries the sensors (on drop rods), the elevated loudspeakers and the projectors |
| Sensing | three 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 |
| Image | three 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. |
| Sound | 16 full-range loudspeakers in a third-order ambisonic layout, plus one subwoofer |
| Computing | an 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). |
| Transport | Dante audio network at 48 kHz (Dante Via on the workstation, Dante Virtual Soundcard on the NUC). OSC carries positions. |
All components follow the same conventions. Once you know them for one component, you know them for the rest.
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. 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:
| Note | Layer | In | Out |
|---|---|---|---|
| Azure Kinects calibration | Sensing (back end) | 3 × Azure Kinect | fused skeletons (OSC), images (NDI) |
| Azure Kinect sensors | Sensing | none | one CHOP per joint (tx ty tz) |
| Azure Kinect cameras | Sensing | none | one TOP per sensor |
| Virtual objects | Spatial logic | optional position (CHOP) | shape (POP), position + size (CHOP) |
| Calculations | Spatial logic | 2 POPs | 1 CHOP |
| Virtual object viewer | Spatial logic | positions (CHOP), shapes (POP) | 3D preview |
| Ambisonics sound control | Output | one position per source (CHOP) + audio | ambisonic mix on the speaker array |
| Max-MSP ambisonic renderer | Output (back end) | OSC positions + 16 Dante audio channels | 16 + 1 loudspeaker feeds |
| Wall projection | Output | 1–3 TOPs | projection window, resolution CHOP |
| Floor projection | Output | 1 TOP | projection 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.
The components are distributed as one folder: an example .toe project file and a toxes folder that holds each .tox and its README.
.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..toe freely. Do not rename the .tox files or their READMEs, because updates replace them by name..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.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.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.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:
COURSES
DOCUMENTATION
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.
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.
| Parameter | Function |
|---|---|
| Virtual Object | shape: sphere, box or cylinder |
| Width X, Height Y, Depth Z, Radius | size. Only the fields for the current shape are active. |
| Position XYZ, Rotate XYZ | placement in the room frame |
| Get pos (shift + M) | captures a position instead of typing it |
| Outer shell + Radius | sphere only. Adds an outer sphere for a soft edge, used by Fade to sphere. |
| Viewport | camera for the node preview only. It does not move the object. |
tx ty tz and a size value taken from the radius. In Ambisonics sound control, this size value sets the source's Rangegain.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:
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:
In June 2026, at the FilmEU Summit in Brussels, the lab hosted two WIRE pilot showcases:
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
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.
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.
Related: Abstract Spatial Computing Lab · Lab Tools
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.
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.
resX, resY as a CHOP that you wire into your source TOP.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.resX, resY. This mapping is the user's responsibility, as the component does not apply it.| Parameter | Function |
|---|---|
| License | off = home resolution, on = full lab resolution |
| Fit Input | what happens to an image of the wrong size |
| Configure | rebuilds after settings change |
| Open Window / Close Window | opens or closes the projection |
resX, resY for the user's source TOP.INTERACTION trigger
example list here?
Earlier versions, previous attempts and related tech.
multiplekinectcalibration_touchdesigner, TDAzureMerger) were explored before the in-house fusion.p1 → p2 → p3) whenever a sensor picked someone up or lost them.The full list is in Inventory.
Earlier versions, previous attempts and related tech.
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.Earlier versions, previous attempts and related tech.
Earlier versions, previous attempts and related tech.
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.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.
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.
| Machine | Role |
|---|---|
| HP Z8 workstation | TouchDesigner: tracking, content and source audio. Audio goes onto the Dante network through Dante Via. Positions are sent over OSC. |
| Windows NUC | Max 9.1.5 running this patch. Audio input and output go through Dante Virtual Soundcard in ASIO mode. |
| Dante network | carries 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. |
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)./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.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.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.)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.lores~, 100 Hz) and sent to channel 17.dac~ 1–17 sends the speaker feeds to Dante.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.
| # | Ring | X | Y | Z | Azimuth | Elevation | Distance |
|---|---|---|---|---|---|---|---|
| 1 | ground | −2.6 | 0.5 | 4.5 | −127.57° | −16.95° | 3.43 m |
| 2 | ground | −2.6 | 0.5 | 2.5 | −90.00° | −21.04° | 2.79 m |
| 3 | ground | −2.6 | 0.5 | 0.1 | −47.29° | −15.78° | 3.68 m |
| 4 | ground | 0 | 0.5 | −0.3 | 0.00° | −19.65° | 2.97 m |
| 5 | ground | 2.6 | 0.5 | 0.1 | 47.29° | −15.78° | 3.68 m |
| 6 | ground | 2.6 | 0.5 | 2.5 | 90.00° | −21.04° | 2.79 m |
| 7 | ground | 2.6 | 0.5 | 4.5 | 127.57° | −16.95° | 3.43 m |
| 8 | rear | 0 | 3.0 | 7.15 | 180.00° | 17.88° | 4.89 m |
| 9 | high | −2.5 | 3.7 | 3.25 | −106.70° | 40.13° | 3.41 m |
| 10 | high | −2.5 | 3.7 | 1.75 | −73.30° | 40.13° | 3.41 m |
| 11 | high | −1.25 | 3.7 | 0 | −26.57° | 38.21° | 3.56 m |
| 12 | high | 1.25 | 3.7 | 0 | 26.57° | 38.21° | 3.56 m |
| 13 | high | 2.5 | 3.7 | 1.75 | 73.30° | 40.13° | 3.41 m |
| 14 | high | 2.5 | 3.7 | 3.25 | 106.70° | 40.13° | 3.41 m |
| 15 | high | 1.25 | 3.7 | 5 | 153.43° | 38.21° | 3.56 m |
| 16 | high | −1.25 | 3.7 | 5 | −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.
| Mode | Options | Default |
|---|---|---|
| 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 recording | live |
| Listener reference | fixed point, or tracked listener (with optional mirroring) | — |
| Gain active | distance-based gain, or full gain on all sources | on |
| Ambisonic file players (2) | off / play | off |
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.
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.The renderer is not part of the public Td-updates repository. It runs on the lab's NUC and is maintained by the lab.
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.
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.
ambiencode~ object.ambidecode~.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.
| Ring | Speakers | Height (Y) | Azimuths (°) | Elevation (°) |
|---|---|---|---|---|
| Ground | 1–7 | 0.5 m | −127.6, −90, −47.3, 0, 47.3, 90, 127.6 | −16 … −21 |
| Elevated | 9–16 | 3.7 m | ±26.6, ±73.3, ±106.7, ±153.4 | 38 … 40 |
| Rear | 8 | 3.0 m | 180 | 17.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.
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.
| Parameter | Function |
|---|---|
| Add virtual object (+/−) | adds or removes sources. Each source gets an inlet, top to bottom in source order. |
| Posin | read-only: shows where each source's position comes from |
| Rangegain | size and gain of the source. Bound to a wired virtual object's Radius, otherwise typed in. |
| Audio in | the CHOP or channel carrying the source's sound. Resampled to 48 kHz. |
Planned work includes:
All TouchDesigner components, READMEs and the example project: github.com/SpatialComputingL/Td-updates
DOCUMENTATION
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 that runs in the lab. TouchDesigner runs the lab's own components (Lab Tools), and Max/MSP runs the Max-MSP ambisonic renderer.
| Category | Software |
|---|---|
| General | TouchDesigner, Max/MSP, Adobe Creative Cloud |
| Projection mapping | MadMapper |
| Game engines | Unreal Engine, Unity |
| Audio | Reaper |
| Lab infrastructure | Dante Via, Dante Virtual Soundcard and Dante Controller (audio network), NDI (video over the network), ICST Ambisonics externals for Max |
The room's own hardware, set up permanently.
| Subsystem | Hardware |
|---|---|
| Projection | three 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 sound | 16 loudspeakers in a third-order ambisonic layout plus 1 subwoofer, on a Dante audio network at 48 kHz |
| Body tracking and cameras | 3 × Azure Kinect on drop rods below the ceiling grid |
| Computing | HP Z8 workstation (TouchDesigner); Intel NUC (Kinect host); Windows NUC (ambisonic renderer) |
| Network | local network (router) for OSC and NDI; Dante network for audio |
| Rigging | scaffold-pipe grid below the concrete ceiling, carrying sensors, elevated speakers and projectors |
Tools you can use in the lab.
| Category | Hardware |
|---|---|
| Computing | NUC (bare-bone workstation), HP ZBook (laptop) |
| Microcontrollers | Raspberry Pi 3 and 4B, Arduino |
| Depth cameras | Microsoft Azure Kinect (three installed, see above), ZED 2i, OAK-D |
| Hand tracking | Leap Motion Controller |
| Cameras | RGB cameras, Insta360 (360° camera) |
| MIDI controllers | KORG nanoPAD, KORG nanoKONTROL, Intech Studio, Midi Fighter |
| Interfaces | Full HD touchscreens |
| Audio | RME Fireface UC (8-channel audio interface) |
| Network | router (local network) |
Small software, plug-ins and apps.
| Category | Application |
|---|---|
| Interfaces | TouchOSC |
| Tracking and data | ZIG SIM, ZIG SIM Pro, MediaPipe (in TouchDesigner) |
| 3D scanning | Polycam, Scaniverse |
| Audio | Sonic Pi |
External links for inspiration
Learning material (on the lab's SharePoint, for LUCA users)
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.
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.
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.
| Parameter | Function |
|---|---|
| Viewport | perspective (quick check) or top (floor plan) |
| Colors | off = grey, on = one colour per object |
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.
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.
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.| Parameter | Function |
|---|---|
| License | off = home resolution, on = full lab resolution |
| Panoramic projection | one wide image across the wall, uses input 1 only |
| Single projection | output to one projector only |
| (both off) | three screens: left, centre, right |
| Fit Input | what happens to an image of the wrong size |
| Output Resolution | readout of the current resolution |
| Configure | rebuilds after a mode change |
| Open Window / Close Window | opens or closes the projection |
resX, resY for the user's source TOP.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.
The host turns three independent sensors into one room-wide tracking system. It does three things:
p1 … p8).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.
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.
Each Kinect Azure CHOP runs Microsoft's body tracking for its own sensor. Relevant settings:
:confidence channel per joint (see Fusion per joint).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).
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°.
| Sensor | X | Y | Z | Rotate (Rx, Ry, Rz) |
|---|---|---|---|---|
| Left | −1.25 | 3.03 | 4.40 | 45°, 180°, 0° |
| Right | 1.25 | 3.03 | 4.40 | 43.5°, 180°, 0° |
| Rear centre | 0 | 3.03 | 6.31 | 45.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).
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:
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.
From each of the three inputs, every body slot is read. A body counts as a detection only if:
id is greater than 0 (the Kinect reports 0 for an empty slot), andpelvis 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.
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):
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.
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.
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.
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:
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).
Only confirmed people are sent, in number order:
| Channel | Content |
|---|---|
p{N}/{joint}:tx, :ty, :tz | every joint of every confirmed person, metres, room frame |
p1/present … p8/present | 1 while that number belongs to someone, otherwise 0. Always sent, also as 0. |
persons | how many people are seen in this frame |
heartbeat | a 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.
| Event | Takes |
|---|---|
| Someone enters the empty room | about 5 frames (0.17 s) until p1 appears |
| A second sensor picks up a tracked person | immediately, same number |
| A person is briefly hidden | keeps number and last pose for up to 15 frames (0.5 s) |
| A person leaves | number released after 0.5 s, present goes to 0 |
| A one-frame false detection | discarded, never gets a number |
Two people, A and B, in an empty room:
p1.p1, so it simply joins p1. A's pelvis is now fused from two sensors.p1 (no match), so it becomes a candidate and, 5 frames later, p2.p1 is released and p1/present goes to 0. B stays p2.p1.All constants are at the top of fusion_callbacks:
| Constant | Default | Raise it when… | Lower it when… |
|---|---|---|---|
| MATCH_DIST | 0.75 m | fast movement makes people get new numbers | two people close together swap numbers |
| MERGE_DIST | 1.20 m | one person shows up twice | two people standing close are merged; possible once the spread stays under 0.25 m (then about 0.6 m) |
| CONFIRM_FRAMES | 5 | glitches still take numbers | new people appear too late |
| DROP_FRAMES | 15 | people lose their number behind others | people who left linger too long |
| MAX_SLOTS | 8 | — | — |
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.
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.
| Direction | Protocol / port | Content |
|---|---|---|
| host → workstation | OSC, UDP 1030 | every joint of every person (p{N}/{joint}:tx/ty/tz), p{N}/present, persons, heartbeat |
| host → workstation | OSC, UDP 1031 | tracking frame rate |
| host → workstation | NDI | one video stream per sensor, named after the host and the sensor: <host> (AK1), (AK2), (AK3) |
| workstation → host | OSC broadcast, UDP 1051 | Configure (re-initialises the sensors), sent from Azure Kinect sensors |
| workstation → host | OSC broadcast, UDP 27 | Image, 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.
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:
| Option | Assessment |
|---|---|
| Orbbec Femto Bolt (×3) with a sync hub | The 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 port | Separate 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.
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.
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.
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.
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.
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.
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 0OSC 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:
p{N}/present for this component's Person number is 0.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.heartbeat hasn't changed for 0.5 s: the host has stopped, frozen or lost the connection.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.
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:
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 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.
joint_watch compares the joint list and the Person number with the previous frame. Only when one of them changes does it rebuild:
joint_sel_i) per slot, picking p{Person}/{joint}:tx/ty/tz and renaming it to tx ty tzout_i) per slot, labelled with the joint nameExisting 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.
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:
joints_resolved and an OSC In).fps_oscin plugged into the view.fps_oscin from merge_view;shared_stream, that reads the receiver's stream, and wires it into joints_resolved. Its comment says which component it reads from.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).
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.
| Parameter | Function |
|---|---|
| Mouse | off = sensor data. On = every joint follows the mouse. |
| Mouse orientation | the plane the mouse moves in (e.g. XZ = floor plane). Only active when Mouse is on. |
| Person | which 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. |
| Configure | re-initialises the sensors on the Kinect host. Only active in the receiving copy. |
| Fallback origin / Position | where joints rest when there is no usable data. Off = 0 0 0. |
tx ty tz in metres.p1/head:tx, …).null named after the joint (null_head, null_handl, …) to each outlet and reference that null downstream.p1 stays empty until someone new arrives. How numbers are assigned is explained in Azure Kinects calibration.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.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.
| Element | Source | Status |
|---|---|---|
| Room volume 5 × 5 × 3.5 m, three fixed walls, floor | lab documentation | measured |
| Mobile fourth wall (south) | lab documentation | shown on demand; not installed by default |
| 16 loudspeakers | speaker calibration (iteration 22) | measured |
| Reference listening point (0, 1.5, 2.5) | ambisonic renderer | defined |
| Ceiling grid (scaffold pipes below the concrete ceiling) | photo | schematic: height and spacing not measured |
| 3 Azure Kinect sensors (position and tilt) | sensor Transform CHOPs in TouchDesigner | calibrated |
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.
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.
?view=top (or entrance, listener) to the URL to open the viewer on a specific view..glb and loaded into the same viewer.The interactive viewer is next to this note on the map.
DOCUMENTATION