Skip to content

Recipe ‐ VR Drumkit with Racing Pedals

selleriegruen edited this page Jun 3, 2025 · 20 revisions

This recipe describes how to build a VR drum kit using the Meta Quest 2 headset, where the controllers are virtual drumsticks, and Thrustmaster T-LCM racing pedals control the bass drum and hi-hat sounds.

Requirements

Hardware

  • VR-ready PC/Laptop (Windows based)
  • Meta quest 2
  • Racing Pedals: Thrustmaster T-LCM
  • Hex wrench (optional, for fine-tuning the pedals)

Software

  • Unity Version 2022.3.X LTS (tested with 2022.3.59f1 LTS)
  • Meta XR All-in-One SDK
  • optional: blender or similar software to edit 3D models/.fbx files
  • optional: audacity, cubase or similar audio software to edit sounds

Installation & Setup

This project supports two different setups depending on your hardware and goals.

  1. Standalone Headset (Android build)
  • for playing directly on Meta Quest
  • great for simple controller-based drumming
  • pedals not supported
  1. PC-VR (Windows build with Meta Quest Link)
  • for full setup with pedals
  • required if you're using USB pedals or other PC peripherals

For both versions you need to:

Android Build

  1. Switch platform to Android File → Build Settings → Android → Switch Platform
  2. Install XR Plug-in Management: Edit → Project Settings → XR Plug-in Management
  3. Enable Meta XR under Android
  4. Go to Player Settings → Other Settings and set
    • Minimum API Level: 32
    • Scripting Backend: IL2CPP
    • Target Architectures: ARM64

Windows Build (full set)

  1. Keep the platform as PC, Mac & Linux in Unity.
  2. Install XR Plug-in Management: Edit → Project Settings → XR Plug-in Management
  3. Enable Oculus under Desktop Version

Asset Import

To get started quickly, here are 3D assets for a complete drumset, including a room and floor. https://drive.google.com/drive/folders/1K155Vs1DyMEJ_5A8IoHp_cAvBAsRVsMH?usp=drive_link

These models are not required for the technical setup, but they offer:

  • Proper scaling and alignment
  • Optimized polygon count for VR performance
  • something "to show" from the very beginning of your project

You are free to use any other drumset model or even simple geometric shapes instead. The interactive functionality will later rely only on trigger colliders, not the visual models themselves. The original sources of these models are credited under Reference Links.

Implementation

Controller Interaction

The latest version of the Meta XR SDK (May 2025) provides preconfigured Building Blocks for controller tracking, which handle hand/controller movement and input without extra setup. We take advantage of this and simply attach drumsticks to the appropriate controller anchors.

No scripting or tracking configuration is needed.


1. Add Meta XR Camera Rig

Add the [BuildingBlock] Camera Rig from the Meta XR SDK to your scene, it already contains

  • A working XR Origin with tracking space
  • Anchors for hands, controllers and VR-eyes
  • Full controller tracking, preconfigured for Meta Quest

You can find it under:
Meta XR Tools > BuildingBlocks > [BuildingBlock] Camera Rig

Once in the scene, expand it and locate the anchor structure

image

2. Attach the Drumsticks

  • Create or import your stick model from the link above (e.g. a .fbx)
  • Drag the stick object into the corresponding controller anchor:
    • Drumstick_L → under LeftControllerAnchor
    • Drumstick_R → under RightControllerAnchor
  • Adjust the stick’s position and rotation so that it appears naturally held in VR
  • Disable or delete all other children in this layer, otherwise the sticks will also follow the hands.

image

3. Add Physics Components

To enable collision with drum trigger zones, add the following to both stick objects:

  • Capsule Collider and align it with the stick length
  • Rigidbody and set to isKinematic = true
  • also under Rigidbody uncheck use gravity
  • Create a new tag DrumStick and assign it in the inspector to both drumsticks objects. This will help your logic when coding later.

Drum Triggerzones

This section explains how to implement interactive trigger areas for the toms and cymbals in the VR drum set. Each trigger represents a surface that reacts when hit by the virtual drumsticks.

There are (more than) two ways to set up the interactive hit zones for each drum surface:

  • If you want a quick working version using placeholder objects and automatic collider assignment, skip ahead to the Just Make It Work (Fast Track) section.
  • If you prefer full control and want to manually position and configure everything, start here with the Manual Setup below.

Manual Setup

This method gives you full control over the placement, size, and naming of all trigger zones. It is especially useful if you want a clean scene structure and reduce code.

1. Create a parent container

  • In the Hierarchy, right-click → Create Empty
  • Rename it to DrumSetTrigger

This object will hold all the individual trigger elements.

2. Add a trigger object

  • Right-click on DrumSetTriggerCreate Empty
  • Rename the object (e.g. CrashTrigger, TomHighTrigger, etc.)
  • In the Inspector, click Add Component → choose a Box Collider, Sphere Collider, or Capsule Collider
  • Enable Is Trigger in the collider component
  • Adjust the transform and collider dimensions so that it matches the hit zone of the corresponding drum surface

3. Repeat for each surface

Create a new child under DrumSetTrigger for every interactive part of the drum set and add the above components to each of them, e.g.:

  • TomHighTrigger
  • TomMidTrigger
  • TomLowTrigger
  • CrashTrigger
  • RideTrigger
  • HiHatTrigger
  • SnareTrigger

Each of these will be used to detect hits from the virtual sticks.

Note: Do not add any Mesh Renderer or Mesh Filter. These objects will be invisible in play mode. If needed, assign a tag like DrumTrigger to all trigger objects for filtering in code.

image


Just Make It Work (Fast Track)

This method uses visible placeholder spheres that you can move and see in the Scene view. A script will automatically assign colliders to these objects, saving setup time.

1. Create a parent container

  • In the Hierarchy, right-click → Create Empty
  • Rename it to DrumSetTrigger

2. Add visual spheres as placeholders

  • Right-click on DrumSetTrigger3D Object > Sphere
  • Rename it (e.g. TomLowSphere, CrashSphere, etc.)
  • Scale the sphere to match the desired hit area
  • Copy + Paste + Adjust Sphere for every other interactive drum element

Note: Use pink/untextured materials so the objects stand out clearly for debugging. Once testing is complete, you can optionally delete the spheres or disable their renderers.

triggerzones2

  1. Run the collider assignment script

Use this Editor script to automatically generate trigger colliders for all drum parts based on their child spheres. Create a folder called Editor inside your Assets folder, if it doesn't exist. You will see a new menu item in Unity's Toolbar: Tools → Generate Drum Trigger Colliders

This script only works in the Unity Editor! Do not include it in builds!

This script will:

  • find all GameObjects ending with "Sphere"
  • add a CapsuleCollider to the parent object
  • set the collider as a trigger
  • use the child sphere’s position and size to place the collider correctly
using UnityEngine;
using UnityEditor;

public class DrumTriggerGenerator : EditorWindow
{
    [MenuItem("Tools/Generate Drum Trigger Colliders")]
    public static void GenerateTriggerColliders()
    {
        int count = 0;

        foreach (Transform obj in GameObject.FindObjectsOfType<Transform>())
        {
            if (!obj.name.EndsWith("Sphere")) continue;

            GameObject sphere = obj.gameObject;
            Transform parent = sphere.transform.parent;

            if (parent == null)
            {
                Debug.LogWarning("Sphere " + sphere.name + " has no parent!");
                continue;
            }

            // Search for TriggerCollider
            if (parent.GetComponent<CapsuleCollider>())
            {
                Debug.Log("CapsuleCollider already exists on " + parent.name + " – skipping.");
                continue;
            }

            // Add Collider to Parent
            CapsuleCollider trigger = parent.gameObject.AddComponent<CapsuleCollider>();
            trigger.isTrigger = true;
            trigger.direction = 1; // Y-Axis

            SphereCollider source = sphere.GetComponent<SphereCollider>();
            if (source != null)
            {
                trigger.radius = source.radius;
                trigger.height = source.radius * 2f + 0.01f; // very flat
                trigger.center = sphere.transform.localPosition;
            }
            else
            {
                trigger.radius = 0.2f;
                trigger.height = 0.05f;
                trigger.center = sphere.transform.localPosition;
                Debug.LogWarning("SphereCollider missing on " + sphere.name);
            }

            count++;
        }

        Debug.Log($"[DrumTriggerGenerator] {count} trigger colliders generated.");
    }
}

Audio

In the same folder as for the assets above, you can also find audio samples for each element of the drumset. They were modified from the original source using cubase. Here are the exported .wav files that come at 44.000 Hz and 16-bit. https://drive.google.com/drive/folders/1K155Vs1DyMEJ_5A8IoHp_cAvBAsRVsMH?usp=drive_link

Each drum element comes with three sound files to simulate different hit intensities (e.g., soft / medium / hard). This allows for a more realistic playing experience, especially when mapped to varying input velocities.

  • Create a folder in your project Assets/Audio/Drums

  • Drag all .wav files from the drive link into this folder

  • On each drum GameObject

    • Add an AudioSource component: Inspector → Add Component → AudioSource
    • Disable Play on Awake

The script we will create in the following steps (like DrumTrigger.cs) will play sounds using AudioSource.PlayOneShot(). It will also create an Array in the Inspector that you can fill with the .wav files to your liking.

The original sources of the audio files are credited under Reference Links.

Skript for Drum Controls

Use this script to make a drum element (like snare, tom, or cymbal) react to stick collisions and play a sound.

  • Attach this script to every top drum GameObject (e.g. Snare, TomHigh, Crash).
  • Make sure the object has:
    • A Collider component (set Is Trigger = true)
    • An AudioSource component
  • Tag the VR stick(s) as "DrumStick"

What this skript does:

  • Plays a random AudioClip from the assigned array when hit by a stick
  • Logs debug messages to help with troubleshooting
  • Supports optional Test Mode, so you can trigger sounds via keyboard
using UnityEngine;

public class DrumTrigger : MonoBehaviour
{
    public AudioClip[] drumSounds;     
    private AudioSource audioSource;

    void Start()
    {
        audioSource = GetComponent<AudioSource>();
    }

    void OnTriggerEnter(Collider other)
    {
        Debug.Log("Trigger hit by: " + other.name);

        if (!TestModeManager.IsTestMode && other.CompareTag("DrumStick"))
        {
            Debug.Log("DrumStick hit: " + name);
            PlayRandomSound();
        }
    }

    void Update()
        {
            if (!TestModeManager.IsTestMode) return;

            if (Input.GetKeyDown(KeyCode.Alpha1) && name.Contains("Snare")) PlayRandomSound();
            if (Input.GetKeyDown(KeyCode.Alpha2) && name.Contains("TomHigh")) PlayRandomSound();
            if (Input.GetKeyDown(KeyCode.Alpha3) && name.Contains("TomMid")) PlayRandomSound();
            if (Input.GetKeyDown(KeyCode.Alpha4) && name.Contains("TomLow")) PlayRandomSound();

            if (Input.GetKeyDown(KeyCode.Q) && name.Contains("Crash")) PlayRandomSound();
            if (Input.GetKeyDown(KeyCode.W) && name.Contains("Cymbal")) PlayRandomSound();
            if (Input.GetKeyDown(KeyCode.E) && name.Contains("Ride")) PlayRandomSound();

            if (Input.GetKeyDown(KeyCode.A) && name.Contains("HiHatOpen")) PlayRandomSound();
            if (Input.GetKeyDown(KeyCode.S) && name.Contains("HiHatClosed")) PlayRandomSound();
            if (Input.GetKeyDown(KeyCode.D) && name.Contains("HiHatPedal")) PlayRandomSound();

            if (Input.GetKeyDown(KeyCode.Space) && name.Contains("Kick")) PlayRandomSound();
        }


    void PlayRandomSound()
    {
        if (drumSounds.Length == 0) return;
        int index = Random.Range(0, drumSounds.Length);
        audioSource.PlayOneShot(drumSounds[index]);
    }
}

Optional, create TestModeManager: If TestModeManager.IsTestMode = true, you can hit keys to simulate drum hits. This may help with debugging.

  • 1 = Snare
  • 24 = Toms
  • Q, W, E = Crash, Cymbal, Ride
  • A, S, D = HiHat variants
  • Space = Kick
using UnityEngine;

public class TestModeManager : MonoBehaviour
{
    public static bool IsTestMode = false; 
}

Each of your upper drum set elements should now have the following components in the inspector, on the same layer:

image

Pedals

The base drum and HiHat in this setup are triggered using external USB pedals. This section explains how to bind the pedal using Unity’s Input System and trigger the drum sound accordingly.

1. Identify the Pedal Axis

Before creating an input binding in Unity, check which physical pedal maps to which axis:

  • Go to https://gamepad-tester.com in a browser
  • Press each pedal and observe which axis value changes
  • Note the axis (e.g. Axis 0 or Brake) that corresponds to the pedal you want to use for the base drum and a second one for the hihat

2. Create an Input Action

In Unity, create a new Input Actions asset in the Project window:

  • Right-click → Create > Input Actions → name it PedalControls.inputactions
  • Double-click the asset to open the Input Actions editor
  • Add a new Action Map, Name: Pedals
  • Add new Actions to the Pedals map, Name: BaseDrum and Name: HiHat
  • Action Type: Value
  • Control Type: Axis
  • Add a Binding, Click the + icon → Add Binding
  • Click Listen, then press the pedal you want to use e.g. for the base drum

Unity will automatically assign the correct axis (e.g. <HID::Thrustmaster Sim Pedals>/stick/x or <HID::Thrustmaster Sim Pedals>/stick/y).

Copy each path and save the asset.

3. Read the Pedal Value in Script

Create a script that reads the pedal value for the base drum and hihat if the axis value passes a certain threshold.

Note: This is why it is important to check the axis of the connected pedal device beforehand. Some manufacturers may apply different logic or thresholds to their devices. The positive axis of one pedal may be the opposite one for another pedal in unity.

using UnityEngine;
using UnityEngine.InputSystem;

public class PedalReader : MonoBehaviour
{
    public static PedalReader Instance { get; private set; }

    private InputAction bassDrum;
    private InputAction hiHat;

    public float KickRaw { get; private set; }
    public float KickVelocity { get; private set; }

    public float HiHatLevel { get; private set; }     // 0 = open, 1 = closed
    public float HiHatVelocity { get; private set; }  // positive = closing, negative = opening

    private float lastKick = 0f;
    private float lastHiHatRaw = 0f;

    void Awake()
    {
        if (Instance != null && Instance != this) Destroy(gameObject);
        Instance = this;
    }

    void OnEnable()
    {
        bassDrum = new InputAction(type: InputActionType.Value, binding: "<HID::Thrustmaster Sim Pedals>/stick/x");
        hiHat = new InputAction(type: InputActionType.Value, binding: "<HID::Thrustmaster Sim Pedals>/stick/y");

        bassDrum.Enable();
        hiHat.Enable();
    }

    void OnDisable()
    {
        bassDrum.Disable();
        hiHat.Disable();
    }

    void Update()
    {
        // Kick
        float kick = bassDrum.ReadValue<float>();  // -1 to +1
        KickVelocity = (kick - lastKick) / Time.deltaTime;
        lastKick = kick;
        KickRaw = kick;

        // Hi-Hat
        float rawHat = hiHat.ReadValue<float>();   // +1 (released) bis -1 (pressed)
        HiHatVelocity = (rawHat - lastHiHatRaw) / Time.deltaTime;
        lastHiHatRaw = rawHat;

        HiHatLevel = (1f - rawHat) / 2f; // 0 = open, 1 = closed
    }
}

Base Drum

The kick (base drum) is triggered by pressing the assigned pedal fast and deep enough.
This section explains how to use the pedal data from PedalReader.cs to play a kick sound dynamically.

  • Make sure PedalReader.cs is present in your scene
  • For setup instructions, see the Pedals section
  1. In your scene, select or create the GameObject representing the kick drum (e.g., KickDrum)
  2. Add the following components:
    • AudioSource (disable Play on Awake)
    • KickPedalTrigger (script below)
  3. Assign an appropriate sample (kick.wav) to the kickSample field in the Inspector
  • The script uses PedalReader.Instance.KickRaw to measure pedal position
  • It calculates how fast the pedal was pressed (KickVelocity)
  • If the velocity and depth exceed a defined threshold, a sound is triggered
  • The script prevents retriggering until the pedal is released

Note: You can tweak the threshold values to make the pedal more or less sensitive. If you want to support multiple velocity levels with different samples, use an array of clips and select them by velocity range.

using UnityEngine;

public class KickPedalTrigger : MonoBehaviour
{
    public AudioClip kickSample;
    private AudioSource audioSource;
    private bool hasTriggered = false;

    [Tooltip("Minimum velocity required to trigger a hit")]
    public float velocityThreshold = 1.5f;

    [Tooltip("How deep the pedal must be pressed (0 = top, 1 = fully down)")]
    public float minKickDepth = 0.5f;

    [Tooltip("Pedal must return above this value to allow another hit")]
    public float resetThreshold = 0.2f;

    void Start()
    {
        audioSource = GetComponent<AudioSource>();
    }

    void Update()
    {
        float raw = PedalReader.Instance.KickRaw;
        float velocity = PedalReader.Instance.KickVelocity;
        float depth = (raw + 1f) / 2f;

        if (!hasTriggered && velocity > velocityThreshold && depth > minKickDepth)
        {
            hasTriggered = true;
            float volume = Mathf.Clamp01(Mathf.Pow(velocity / 6f, 1.5f));
            audioSource.PlayOneShot(kickSample, volume);
        }

        if (hasTriggered && depth < resetThreshold)
        {
            hasTriggered = false;
        }
    }
}

HiHat

The hi-hat consists of two parts:

  1. The pedal trigger, which plays a "chick" sound when the assigned foot pedal is pressed fast enough
  2. The top trigger, which plays an open or closed sound when hit with a stick

For both

  • Make sure PedalReader.cs is active in your scene
  • For input setup, refer to the Pedals section

1. HiHat PedalTrigger

This script plays a hi-hat "chick" sound when the pedal is closed fast and far enough – without stick interaction.

using UnityEngine;

public class HiHatPedalTrigger : MonoBehaviour
{
    public AudioClip softChick;
    public AudioClip mediumChick;
    public AudioClip hardChick;

    private AudioSource audioSource;
    private bool hasTriggered = false;

    [Tooltip("Minimum velocity required to trigger a chick")]
    public float velocityThreshold = 1.0f;

    [Tooltip("How deep the pedal must be pressed (0 = open, 1 = fully closed)")]
    public float minPedalDepth = 0.7f;

    [Tooltip("Pedal must return above this value to allow another trigger")]
    public float resetThreshold = 0.4f;

    void Start()
    {
        audioSource = GetComponent<AudioSource>();
    }

    void Update()
    {
        float raw = PedalReader.Instance.HiHatLevel;
        float velocity = PedalReader.Instance.HiHatVelocity;

        if (!hasTriggered && velocity > velocityThreshold && raw > minPedalDepth)
        {
            hasTriggered = true;

            AudioClip selected = hardChick;
            if (velocity < 1.5f) selected = softChick;
            else if (velocity < 3.0f) selected = mediumChick;

            float volume = Mathf.Clamp01(Mathf.Pow(velocity / 5f, 2f));
            audioSource.PlayOneShot(selected, volume);
        }

        if (hasTriggered && raw < resetThreshold)
        {
            hasTriggered = false;
        }
    }
}

2. HiHat TopTrigger

This script plays a sound when the hi-hat is hit with a DrumStick. Depending on the pedal position, it chooses between open and closed sounds, and adjusts loudness based on predefined velocity levels. Currently, the hit velocity is hardcoded. You can extend this to use real velocity data based on stick speed or controller movement in future versions.

  • Attach this script to the top of the hihat
  • Add an AudioSource component (disable Play on Awake)
  • Assign arrays of open and closed sounds (usually three each)
using UnityEngine;

public class HiHatTriggerTop : MonoBehaviour
{
    public AudioClip[] openSounds;
    public AudioClip[] closedSounds;

    private AudioSource audioSource;

    void Start()
    {
        audioSource = GetComponent<AudioSource>();
    }

    void OnTriggerEnter(Collider other)
    {
        Debug.Log("HiHatTrigger was entered by: " + other.name);
        if (!other.CompareTag("DrumStick")) return;

        float pedal = PedalReader.Instance.HiHatLevel;
        float vel = 2.5f; // constant simulated velocity

        AudioClip[] selectedSet = pedal < 0.5f ? openSounds : closedSounds;
        int index = GetLoudnessIndex(vel);
        AudioClip clip = selectedSet[Mathf.Clamp(index, 0, selectedSet.Length - 1)];

        float volume = Mathf.Clamp01(Mathf.Pow(vel / 5f, 2f));
        audioSource.PlayOneShot(clip, volume);
    }

    int GetLoudnessIndex(float velocity)
    {
        if (velocity < 1.5f) return 0;
        else if (velocity < 3.5f) return 1;
        else return 2;
    }
}

Demo Results

test_basedrum.mp4
full_set_test.mp4
full_set_test2.mp4

Reference Links

Please refer to the individual licenses on each platform if you plan to reuse these assets in other projects. The versions used here were adapted for educational and non-commercial use.

Clone this wiki locally