Condividi tramite


Esercitazione: Riprodurre il pianoforte 3D

Nell'esercitazione precedente è stato creato un modello di tastiera per pianoforte a 88 tasti completa. Ora, lo si rende giocabile nello spazio XR.

In questa esercitazione si apprenderà come:

  • Aggiungere funzionalità interattive del pianoforte usando gli eventi puntatore
  • Ridimensionare le mesh in base a dimensioni diverse
  • Abilitare il teletrasporto e il supporto multi puntatore in XR

Prima di iniziare

Assicurarsi di completare l'esercitazione precedente nella serie e di essere pronti per continuare ad aggiungere al codice.

index.html

<html>
    <head>
        <title>Piano in BabylonJS</title>
        <script src="https://cdn.babylonjs.com/babylon.js"></script>
        <script src="scene.js"></script>
        <style>
            body,#renderCanvas { width: 100%; height: 100%;}
        </style>
    </head>
    <body>
        <canvas id="renderCanvas"></canvas>
        <script type="text/javascript">
            const canvas = document.getElementById("renderCanvas");
            const engine = new BABYLON.Engine(canvas, true); 

            createScene(engine).then(sceneToRender => {
                engine.runRenderLoop(() => sceneToRender.render());
            });
            
            // Watch for browser/canvas resize events
            window.addEventListener("resize", function () {
                engine.resize();
            });
        </script>
    </body>
</html>

scene.js

const buildKey = function (scene, parent, props) {
    if (props.type === "white") {
        /*
        Props for building a white key should contain: 
        note, topWidth, bottomWidth, topPositionX, wholePositionX, register, referencePositionX

        As an example, the props for building the middle C white key would be
        {type: "white", note: "C", topWidth: 1.4, bottomWidth: 2.3, topPositionX: -0.45, wholePositionX: -14.4, register: 4, referencePositionX: 0}
        */

        // Create bottom part
        const bottom = BABYLON.MeshBuilder.CreateBox("whiteKeyBottom", {width: props.bottomWidth, height: 1.5, depth: 4.5}, scene);

        // Create top part
        const top = BABYLON.MeshBuilder.CreateBox("whiteKeyTop", {width: props.topWidth, height: 1.5, depth: 5}, scene);
        top.position.z =  4.75;
        top.position.x += props.topPositionX;

        // Merge bottom and top parts
        // Parameters of BABYLON.Mesh.MergeMeshes: (arrayOfMeshes, disposeSource, allow32BitsIndices, meshSubclass, subdivideWithSubMeshes, multiMultiMaterials)
        const key = BABYLON.Mesh.MergeMeshes([bottom, top], true, false, null, false, false);
        key.position.x = props.referencePositionX + props.wholePositionX;
        key.name = props.note + props.register;
        key.parent = parent;

        return key;
    }
    else if (props.type === "black") {
        /*
        Props for building a black key should contain: 
        note, wholePositionX, register, referencePositionX

        As an example, the props for building the C#4 black key would be
        {type: "black", note: "C#", wholePositionX: -13.45, register: 4, referencePositionX: 0}
        */

        // Create black color material
        const blackMat = new BABYLON.StandardMaterial("black");
        blackMat.diffuseColor = new BABYLON.Color3(0, 0, 0);

        // Create black key
        const key = BABYLON.MeshBuilder.CreateBox(props.note + props.register, {width: 1.4, height: 2, depth: 5}, scene);
        key.position.z += 4.75;
        key.position.y += 0.25;
        key.position.x = props.referencePositionX + props.wholePositionX;
        key.material = blackMat;
        key.parent = parent;

        return key;
    }
}

const createScene = async function(engine) {
    const scene = new BABYLON.Scene(engine);

    const alpha =  3*Math.PI/2;
    const beta = Math.PI/50;
    const radius = 220;
    const target = new BABYLON.Vector3(0, 0, 0);
    
    const camera = new BABYLON.ArcRotateCamera("Camera", alpha, beta, radius, target, scene);
    camera.attachControl(canvas, true);
    
    const light = new BABYLON.HemisphericLight("light", new BABYLON.Vector3(0, 1, 0), scene);
    light.intensity = 0.6;

    const keyParams = [
        {type: "white", note: "C", topWidth: 1.4, bottomWidth: 2.3, topPositionX: -0.45, wholePositionX: -14.4},
        {type: "black", note: "C#", wholePositionX: -13.45},
        {type: "white", note: "D", topWidth: 1.4, bottomWidth: 2.4, topPositionX: 0, wholePositionX: -12},
        {type: "black", note: "D#", wholePositionX: -10.6},
        {type: "white", note: "E", topWidth: 1.4, bottomWidth: 2.3, topPositionX: 0.45, wholePositionX: -9.6},
        {type: "white", note: "F", topWidth: 1.3, bottomWidth: 2.4, topPositionX: -0.55, wholePositionX: -7.2},
        {type: "black", note: "F#", wholePositionX: -6.35},
        {type: "white", note: "G", topWidth: 1.3, bottomWidth: 2.3, topPositionX: -0.2, wholePositionX: -4.8},
        {type: "black", note: "G#", wholePositionX: -3.6},
        {type: "white", note: "A", topWidth: 1.3, bottomWidth: 2.3, topPositionX: 0.2, wholePositionX: -2.4},
        {type: "black", note: "A#", wholePositionX: -0.85},
        {type: "white", note: "B", topWidth: 1.3, bottomWidth: 2.4, topPositionX: 0.55, wholePositionX: 0},
    ]

    // Transform Node that acts as the parent of all piano keys
    const keyboard = new BABYLON.TransformNode("keyboard");

    // Register 1 through 7
    var referencePositionX = -2.4*14;
    for (let register = 1; register <= 7; register++) {
        keyParams.forEach(key => {
            buildKey(scene, keyboard, Object.assign({register: register, referencePositionX: referencePositionX}, key));
        })
        referencePositionX += 2.4*7;
    }

    // Register 0
    buildKey(scene, keyboard, {type: "white", note: "A", topWidth: 1.9, bottomWidth: 2.3, topPositionX: -0.20, wholePositionX: -2.4, register: 0, referencePositionX: -2.4*21});
    keyParams.slice(10, 12).forEach(key => {
        buildKey(scene, keyboard, Object.assign({register: 0, referencePositionX: -2.4*21}, key));
    })

    // Register 8
    buildKey(scene, keyboard, {type: "white", note: "C", topWidth: 2.3, bottomWidth: 2.3, topPositionX: 0, wholePositionX: -2.4*6, register: 8, referencePositionX: 84});

    // Transform node that acts as the parent of all piano components
    const piano = new BABYLON.TransformNode("piano");
    keyboard.parent = piano;

    // Import and scale piano frame
    BABYLON.SceneLoader.ImportMesh("frame", "https://raw.githubusercontent.com/MicrosoftDocs/mixed-reality/docs/mixed-reality-docs/mr-dev-docs/develop/javascript/tutorials/babylonjs-webxr-piano/files/", "pianoFrame.babylon", scene, function(meshes) {
        const frame = meshes[0];
        frame.parent = piano;
    });

    // Lift the piano keyboard
    keyboard.position.y += 80;

    const xrHelper = await scene.createDefaultXRExperienceAsync();

    return scene;
}

Rendere riproducibile la tastiera del pianoforte

Al momento, la tastiera per pianoforte creata è un modello statico che non risponde ad alcuna interazione utente. In questa sezione verranno programmati i tasti per spostarsi verso il basso e riprodurre un suono quando qualcuno preme su di essi.

  1. Babylon.js fornisce diversi tipi di eventi, o osservabili, con cui è possibile interagire. Nel nostro caso, abbiamo a che fare con il onPointerObservable dal momento che vogliamo programmare i tasti per eseguire azioni quando qualcuno preme su di loro attraverso un puntatore, che può essere un clic del mouse, tocco, XR pulsante del controller clic, ecc.

    Ecco la struttura di base di come aggiungere qualsiasi comportamento a un oggetto onPointerObservable:

    scene.onPointerObservable.add((pointerInfo) => {
        // do something
    });
    
  2. Anche se Babylon.js fornisce molti tipi diversi di eventi puntatore, verranno usati solo gli POINTERDOWN eventi e POINTERUP per programmare il comportamento dei tasti di pianoforte, usando la struttura seguente:

    scene.onPointerObservable.add((pointerInfo) => {
        switch (pointerInfo.type) {
            case BABYLON.PointerEventTypes.POINTERDOWN:
                // When the pointer is down on a piano key,
                // move the piano key downward (to show that it is pressed)
                // and play the sound of the note
                break;
            case BABYLON.PointerEventTypes.POINTERUP:
                // When the pointer is released,
                // move the piano key upward to its original position
                // and stop the sound of the note of the key that is released
                break;
        }
    });
    
  3. Si inizierà a spostare il tasto del pianoforte verso il basso e verso l'alto quando si preme e si rilascia il tasto.

    All'evento puntatore verso il basso, è necessario rilevare la mesh su cui si fa clic, assicurarsi che si tratti di un tasto per pianoforte e modificare negativamente la coordinata y della mesh di una piccola quantità per far sembrare che il tasto sia stato premuto.

    Per l'evento puntatore verso l'alto, è un po ' più complicato perché il puntatore che ha premuto sul tasto potrebbe non essere rilasciato sul tasto. Ad esempio, qualcuno potrebbe fare clic sul tasto C4, trascinare il mouse su E4 e quindi rilasciare il clic. In questo caso, si vuole comunque rilasciare il tasto premuto (C4) anziché la posizione in cui si verifica l'evento pointerUp (E4).

    Di seguito viene illustrato il modo in cui il codice seguente ottiene ciò che si vuole:

    const pointerToKey = new Map();
    scene.onPointerObservable.add((pointerInfo) => {
        switch (pointerInfo.type) {
            case BABYLON.PointerEventTypes.POINTERDOWN:
                if(pointerInfo.pickInfo.hit) {
                    const pickedMesh = pointerInfo.pickInfo.pickedMesh;
                    const pointerId = pointerInfo.event.pointerId;
                    if (pickedMesh.parent === keyboard) {
                        pickedMesh.position.y -= 0.5;
                        // play the sound of the note
                        pointerToKey.set(pointerId, {
                            mesh: pickedMesh
                        });
                    }
                }
                break;
            case BABYLON.PointerEventTypes.POINTERUP:
                const pointerId = pointerInfo.event.pointerId;
                if (pointerToKey.has(pointerId)) {
                    pointerToKey.get(pointerId).mesh.position.y += 0.5;
                    // stop the sound of the note of the key that is released
                    pointerToKey.delete(pointerId);
                }
                break;
        }
    });
    

    È pointerId univoco per ogni puntatore e può essere utile per identificare un puntatore quando sono presenti più controller o se si usa un touchscreen. In questo caso è stato inizializzato un Map oggetto denominato pointerToKey per archiviare la relazione di quale puntatore ha premuto su quale tasto, in modo da sapere quale chiave rilasciare quando viene rilasciato il puntatore, indipendentemente dalla posizione in cui si verifica la versione.

  4. Ecco l'aspetto dell'interazione con il codice precedente:

    Tasti per pianoforte interattivi

  5. Ora lavoriamo sulla riproduzione e l'arresto di un suono quando un tasto viene premuto e rilasciato. A questo scopo, useremo una libreria Javascript denominata soundfont-player, che ci consente di riprodurre facilmente i suoni MIDI di uno strumento scelto.

    Scaricare il codice minimizzato della libreria, salvarlo nella stessa cartella di index.htmle includerlo nel <header> tag in index.html:

    <head>
        <title>Babylon Template</title>
        <script src="https://cdn.babylonjs.com/babylon.js"></script>
        <script src="scene.js"></script>
        <script src="soundfont-player.min.js"></script>
        <style>
                body,#renderCanvas { width: 100%; height: 100%;}
        </style>
    </head>
    

    Una volta importata la libreria, ecco come inizializzare uno strumento e riprodurre/arrestare i suoni MIDI usando la libreria:

    const pianoSound = await Soundfont.instrument(new AudioContext(), 'acoustic_grand_piano');
    const C4 = piano.play("C4"); // Play note C4
    C4.stop(); // Stop note C4
    
  6. A questo punto è possibile incorporarlo negli eventi del puntatore e finalizzare il codice per questa sezione:

    const pointerToKey = new Map()
    const piano = await Soundfont.instrument(new AudioContext(), 'acoustic_grand_piano');
    
    scene.onPointerObservable.add((pointerInfo) => {
        switch (pointerInfo.type) {
            case BABYLON.PointerEventTypes.POINTERDOWN:
                if(pointerInfo.pickInfo.hit) {
                    let pickedMesh = pointerInfo.pickInfo.pickedMesh;
                    let pointerId = pointerInfo.event.pointerId;
                    if (keys.has(pickedMesh)) {
                        pickedMesh.position.y -= 0.5; // Move the key downward
                        pointerToKey.set(pointerId, {
                            mesh: pickedMesh,
                            note: pianoSound.play(pointerInfo.pickInfo.pickedMesh.name) // Play the sound of the note
                        });
                    }
                }
                break;
            case BABYLON.PointerEventTypes.POINTERUP:
                let pointerId = pointerInfo.event.pointerId;
                if (pointerToKey.has(pointerId)) {
                    pointerToKey.get(pointerId).mesh.position.y += 0.5; // Move the key upward
                    pointerToKey.get(pointerId).note.stop(); // Stop the sound of the note
                    pointerToKey.delete(pointerId);
                }
                break;
        }
    });
    

    Poiché la mesh di ogni chiave è stata denominata dalla nota che rappresenta, è possibile indicare facilmente quale nota riprodurre passando il nome della mesh alla pianoSound.play() funzione. Si noti anche che il suono viene archiviato nella pointerToKey mappa in modo da sapere quale suono arrestare quando viene rilasciata una chiave.

Ridimensionamento del pianoforte per la modalità VR immersiva

A questo punto, probabilmente hai già suonato con il pianoforte con il mouse (o anche con un touch screen) quando hai aggiunto le funzionalità interattive. In questa sezione si passerà allo spazio VR immersivo.

  1. Per aprire la pagina nel visore VR immersivo, è prima necessario connettere l'auricolare al computer sviluppatore e assicurarsi che sia configurato per l'uso nell'app Windows Mixed Reality. Se si usa il simulatore di Windows Mixed Reality, assicurarsi che sia abilitato.

  2. Verrà ora visualizzato un pulsante Vr immersiva nella parte inferiore destra della pagina Web. Fai clic su di esso e potrai vedere il pianoforte nel dispositivo XR a cui sei connesso.

    Pulsante VR immersiva

  3. Una volta che sei nello spazio virtuale, potresti notare che il pianoforte che abbiamo costruito è estremamente grande. Nel mondo vr, possiamo stare in piedi solo nella parte inferiore di esso e giocare puntando il puntatore ai tasti in distanza.

    Pianoforte enorme

  4. Ridurre il piano in modo che le sue dimensioni siano più simili a un normale pianoforte in piedi nella vita reale. A tale scopo, è necessario usare una funzione di utilità che consenta di ridimensionare una mesh rispetto a un punto nello spazio. Aggiungere questa funzione a scene.js (all'esterno di createScene()):

    const scaleFromPivot = function(transformNode, pivotPoint, scale) {
        const _sx = scale / transformNode.scaling.x;
        const _sy = scale / transformNode.scaling.y;
        const _sz = scale / transformNode.scaling.z;
        transformNode.scaling = new BABYLON.Vector3(_sx, _sy, _sz); 
        transformNode.position = new BABYLON.Vector3(pivotPoint.x + _sx * (transformNode.position.x - pivotPoint.x), pivotPoint.y + _sy * (transformNode.position.y - pivotPoint.y), pivotPoint.z + _sz * (transformNode.position.z - pivotPoint.z));
    }
    

    Questa funzione accetta 3 parametri:

    • transformNode: oggetto TransformNode da ridimensionare
    • pivotPoint: un Vector3 oggetto che indica il punto in cui il ridimensionamento è relativo a
    • scale: il fattore di scala
  5. Questa funzione verrà usata per ridimensionare la cornice e i tasti del pianoforte di un fattore di 0,015, con un punto pivot all'origine. Accodare la chiamata di funzione alla createScene() funzione inserendola dopo keyboard.position.y += 80;:

    // Put this line at the beginning of createScene()
    const scale = 0.015;
    
    // Put this function call after keyboard.position.y += 80;
    
    // Scale the entire piano
    scaleFromPivot(piano, new BABYLON.Vector3(0, 0, 0), scale);
    
  6. Non dimenticate di ridimensionare anche la posizione della fotocamera:

    const alpha =  3*Math.PI/2;
    const beta = Math.PI/50;
    const radius = 220*scale; // scale the radius
    const target = new BABYLON.Vector3(0, 0, 0);
    
    const camera = new BABYLON.ArcRotateCamera("Camera", alpha, beta, radius, target, scene);
    camera.attachControl(canvas, true);
    
  7. Ora, quando entriamo di nuovo nello spazio VR, il pianoforte sarebbe della dimensione di un normale pianoforte standup.

    Piano standup normale in VR

Abilitazione delle funzionalità WebXR

Ora che abbiamo ridimensionato il pianoforte alla giusta dimensione nello spazio VR, abilitiamo alcune interessanti funzionalità WebXR per migliorare la nostra esperienza di riproduzione del pianoforte nello spazio.

  1. Se hai suonato il pianoforte usando i controller VR immersivi, potresti aver notato che puoi usare un solo controller alla volta. Abilitare il supporto multi puntatore nello spazio XR usando la gestione delle funzionalità WebXR di Babylon.js.

    Aggiungere il codice seguente alla createScene() funzione, dopo la riga di xrHelper inizializzazione:

    const featuresManager = xrHelper.baseExperience.featuresManager;
    
    const pointerSelection = featuresManager.enableFeature(BABYLON.WebXRFeatureName.POINTER_SELECTION, "stable", {
        xrInput: xrHelper.input,
        enablePointerSelectionOnAllControllers: true        
    });
    
  2. Inoltre, a seconda di dove si trova il punto di partenza, potrebbe essere difficile posizionarsi davanti al pianoforte. Se hai familiarità con l'ambiente VR immersivo, potresti già conoscere il teletrasporto, che è una funzionalità che ti consente di spostarti immediatamente in un altro punto dello spazio puntandolo verso di esso.

  3. Per usare la funzionalità di teletrasporto di Babylon.js, è prima necessario avere una rete a terra che possiamo "stare" nello spazio VR. Aggiungere il codice seguente alla createScene() funzione per creare una base:

    const ground = BABYLON.MeshBuilder.CreateGround("ground", {width: 400, height: 400});
    
  4. Il supporto per il teletrasporto include anche una funzionalità molto utile denominata posizioni snap-to. In breve, le posizioni di snap-to sono posizioni specifiche a cui vogliamo che gli utenti atterrano.

    Ad esempio, è possibile impostare una posizione snap-to davanti al pianoforte in modo che gli utenti possano teletrasportarsi facilmente in tale posizione quando puntano i puntatori vicino al pianoforte.

    Aggiungere il codice seguente per abilitare la funzionalità di teletrasporto e specificare un punto di allineamento:

    const teleportation = featuresManager.enableFeature(BABYLON.WebXRFeatureName.TELEPORTATION, "stable", {
        xrInput: xrHelper.input,
        floorMeshes: [ground],
        snapPositions: [new BABYLON.Vector3(2.4*3.5*scale, 0, -10*scale)],
    });
    
  5. Ora, dovresti essere in grado di posizionarti facilmente davanti al pianoforte teletrasportandosi al punto snap-to davanti al pianoforte, e dovresti essere in grado di suonare due tasti alla volta usando entrambi i controller.

    Teletrasporto al pianoforte

    Riproduzione del pianoforte con controller

Riepilogo

Congratulazioni! È stata completata la serie dell'esercitazione sulla costruzione di piano Babylon.js e si è appreso come:

  • Creare, posizionare e unire mesh per creare un modello di tastiera per pianoforte
  • Importare un modello Babylon.js di una cornice per pianoforte in piedi
  • Aggiungere interazioni puntatore a ogni tasto del pianoforte
  • Ridimensionare le dimensioni delle mesh in base a un punto pivot
  • Abilitare le principali funzionalità WebXR, ad esempio il teletrasporto e il supporto multipointer

Ecco il codice finale per scene.js e index.html:

scene.js

const buildKey = function (scene, parent, props) {
    if (props.type === "white") {
        /*
        Props for building a white key should contain: 
        note, topWidth, bottomWidth, topPositionX, wholePositionX, register, referencePositionX

        As an example, the props for building the middle C white key would be
        {type: "white", note: "C", topWidth: 1.4, bottomWidth: 2.3, topPositionX: -0.45, wholePositionX: -14.4, register: 4, referencePositionX: 0}
        */

        // Create bottom part
        const bottom = BABYLON.MeshBuilder.CreateBox("whiteKeyBottom", {width: props.bottomWidth, height: 1.5, depth: 4.5}, scene);

        // Create top part
        const top = BABYLON.MeshBuilder.CreateBox("whiteKeyTop", {width: props.topWidth, height: 1.5, depth: 5}, scene);
        top.position.z =  4.75;
        top.position.x += props.topPositionX;

        // Merge bottom and top parts
        // Parameters of BABYLON.Mesh.MergeMeshes: (arrayOfMeshes, disposeSource, allow32BitsIndices, meshSubclass, subdivideWithSubMeshes, multiMultiMaterials)
        const key = BABYLON.Mesh.MergeMeshes([bottom, top], true, false, null, false, false);
        key.position.x = props.referencePositionX + props.wholePositionX;
        key.name = props.note + props.register;
        key.parent = parent;

        return key;
    }
    else if (props.type === "black") {
        /*
        Props for building a black key should contain: 
        note, wholePositionX, register, referencePositionX

        As an example, the props for building the C#4 black key would be
        {type: "black", note: "C#", wholePositionX: -13.45, register: 4, referencePositionX: 0}
        */

        // Create black color material
        const blackMat = new BABYLON.StandardMaterial("black");
        blackMat.diffuseColor = new BABYLON.Color3(0, 0, 0);

        // Create black key
        const key = BABYLON.MeshBuilder.CreateBox(props.note + props.register, {width: 1.4, height: 2, depth: 5}, scene);
        key.position.z += 4.75;
        key.position.y += 0.25;
        key.position.x = props.referencePositionX + props.wholePositionX;
        key.material = blackMat;
        key.parent = parent;

        return key;
    }
}

const scaleFromPivot = function(transformNode, pivotPoint, scale) {
    const _sx = scale / transformNode.scaling.x;
    const _sy = scale / transformNode.scaling.y;
    const _sz = scale / transformNode.scaling.z;
    transformNode.scaling = new BABYLON.Vector3(_sx, _sy, _sz); 
    transformNode.position = new BABYLON.Vector3(pivotPoint.x + _sx * (transformNode.position.x - pivotPoint.x), pivotPoint.y + _sy * (transformNode.position.y - pivotPoint.y), pivotPoint.z + _sz * (transformNode.position.z - pivotPoint.z));
}

const createScene = async function(engine) {
    const scale = 0.015;
    const scene = new BABYLON.Scene(engine);

    const alpha =  3*Math.PI/2;
    const beta = Math.PI/50;
    const radius = 220*scale;
    const target = new BABYLON.Vector3(0, 0, 0);

    const camera = new BABYLON.ArcRotateCamera("Camera", alpha, beta, radius, target, scene);
    camera.attachControl(canvas, true);

    const light = new BABYLON.HemisphericLight("light", new BABYLON.Vector3(0, 1, 0), scene);
    light.intensity = 0.6;

    const keyParams = [
        {type: "white", note: "C", topWidth: 1.4, bottomWidth: 2.3, topPositionX: -0.45, wholePositionX: -14.4},
        {type: "black", note: "C#", wholePositionX: -13.45},
        {type: "white", note: "D", topWidth: 1.4, bottomWidth: 2.4, topPositionX: 0, wholePositionX: -12},
        {type: "black", note: "D#", wholePositionX: -10.6},
        {type: "white", note: "E", topWidth: 1.4, bottomWidth: 2.3, topPositionX: 0.45, wholePositionX: -9.6},
        {type: "white", note: "F", topWidth: 1.3, bottomWidth: 2.4, topPositionX: -0.55, wholePositionX: -7.2},
        {type: "black", note: "F#", wholePositionX: -6.35},
        {type: "white", note: "G", topWidth: 1.3, bottomWidth: 2.3, topPositionX: -0.2, wholePositionX: -4.8},
        {type: "black", note: "G#", wholePositionX: -3.6},
        {type: "white", note: "A", topWidth: 1.3, bottomWidth: 2.3, topPositionX: 0.2, wholePositionX: -2.4},
        {type: "black", note: "A#", wholePositionX: -0.85},
        {type: "white", note: "B", topWidth: 1.3, bottomWidth: 2.4, topPositionX: 0.55, wholePositionX: 0},
    ]

    // Transform Node that acts as the parent of all piano keys
    const keyboard = new BABYLON.TransformNode("keyboard");

    // Register 1 through 7
    var referencePositionX = -2.4*14;
    for (let register = 1; register <= 7; register++) {
        keyParams.forEach(key => {
            buildKey(scene, keyboard, Object.assign({register: register, referencePositionX: referencePositionX}, key));
        })
        referencePositionX += 2.4*7;
    }

    // Register 0
    buildKey(scene, keyboard, {type: "white", note: "A", topWidth: 1.9, bottomWidth: 2.3, topPositionX: -0.20, wholePositionX: -2.4, register: 0, referencePositionX: -2.4*21});
    keyParams.slice(10, 12).forEach(key => {
        buildKey(scene, keyboard, Object.assign({register: 0, referencePositionX: -2.4*21}, key));
    })

    // Register 8
    buildKey(scene, keyboard, {type: "white", note: "C", topWidth: 2.3, bottomWidth: 2.3, topPositionX: 0, wholePositionX: -2.4*6, register: 8, referencePositionX: 84});

    // Transform node that acts as the parent of all piano components
    const piano = new BABYLON.TransformNode("piano");
    keyboard.parent = piano;

    // Import and scale piano frame
    BABYLON.SceneLoader.ImportMesh("frame", "https://raw.githubusercontent.com/MicrosoftDocs/mixed-reality/docs/mixed-reality-docs/mr-dev-docs/develop/javascript/tutorials/babylonjs-webxr-piano/files/", "pianoFrame.babylon", scene, function(meshes) {
        const frame = meshes[0];
        frame.parent = piano;
    });

    // Lift the piano keyboard
    keyboard.position.y += 80;

    // Scale the entire piano
    scaleFromPivot(piano, new BABYLON.Vector3(0, 0, 0), scale);

    const pointerToKey = new Map()
    const pianoSound = await Soundfont.instrument(new AudioContext(), 'acoustic_grand_piano');

    scene.onPointerObservable.add((pointerInfo) => {
        switch (pointerInfo.type) {
            case BABYLON.PointerEventTypes.POINTERDOWN:
                // Only take action if the pointer is down on a mesh
                if(pointerInfo.pickInfo.hit) {
                    let pickedMesh = pointerInfo.pickInfo.pickedMesh;
                    let pointerId = pointerInfo.event.pointerId;
                    if (pickedMesh.parent === keyboard) {
                        pickedMesh.position.y -= 0.5; // Move the key downward
                        pointerToKey.set(pointerId, {
                            mesh: pickedMesh,
                            note: pianoSound.play(pointerInfo.pickInfo.pickedMesh.name) // Play the sound of the note
                        });
                    }
                }
                break;
            case BABYLON.PointerEventTypes.POINTERUP:
                let pointerId = pointerInfo.event.pointerId;
                // Only take action if the released pointer was recorded in pointerToKey
                if (pointerToKey.has(pointerId)) {
                    pointerToKey.get(pointerId).mesh.position.y += 0.5; // Move the key upward
                    pointerToKey.get(pointerId).note.stop(); // Stop the sound of the note
                    pointerToKey.delete(pointerId);
                }
                break;
        }
    });

    const xrHelper = await scene.createDefaultXRExperienceAsync();

    const featuresManager = xrHelper.baseExperience.featuresManager;

    featuresManager.enableFeature(BABYLON.WebXRFeatureName.POINTER_SELECTION, "stable", {
        xrInput: xrHelper.input,
        enablePointerSelectionOnAllControllers: true        
    });

    const ground = BABYLON.MeshBuilder.CreateGround("ground", {width: 400, height: 400});

    featuresManager.enableFeature(BABYLON.WebXRFeatureName.TELEPORTATION, "stable", {
        xrInput: xrHelper.input,
        floorMeshes: [ground],
        snapPositions: [new BABYLON.Vector3(2.4*3.5*scale, 0, -10*scale)],
    });

    return scene;
}

index.html

<html>
    <head>
        <title>Babylon Template</title>
        <script src="https://cdn.babylonjs.com/babylon.js"></script>
        <script src="scene.js"></script>
        <script src="soundfont-player.min.js"></script>
        <style>
            body,#renderCanvas { width: 100%; height: 100%;}
        </style>
    </head>
   <body>
    <canvas id="renderCanvas"></canvas>
    <script>
        const canvas = document.getElementById("renderCanvas"); // Get the canvas element
        const engine = new BABYLON.Engine(canvas, true); // Generate the BABYLON 3D engine

        // Register a render loop to repeatedly render the scene
        createScene(engine).then(sceneToRender => {
            engine.runRenderLoop(() => sceneToRender.render());
        });

        // Watch for browser/canvas resize events
        window.addEventListener("resize", function () {
                engine.resize();
        });
    </script>
   </body>
</html>

Passaggi successivi

Per altre informazioni sullo sviluppo di Realtà mista JavaScript, vedere Panoramica dello sviluppo JavaScript.