Udostępnij za pośrednictwem


Samouczek: odtwarzanie fortepianu 3D

W poprzednim samouczku udało nam się utworzyć model pełnej klawiatury fortepianowej z 88 klawiszami. Teraz sprawimy, że będzie to możliwe do odtworzenia w przestrzeni XR.

Niniejszy samouczek zawiera informacje na temat wykonywania następujących czynności:

  • Dodawanie interaktywnych funkcji fortepianu przy użyciu zdarzeń wskaźnika
  • Skalowanie siatki do innego rozmiaru
  • Włączanie obsługi teleportacji i wielu wskaźników w XR

Zanim rozpoczniesz

Upewnij się, że poprzedni samouczek z serii jest gotowy do dalszego dodawania do kodu.

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;
}

Tworzenie klawiatury fortepianowej do gry

W tej chwili utworzona klawiatura fortepianowa jest modelem statycznym, który nie reaguje na żadne interakcje użytkownika. W tej sekcji zaprogramujemy klawisze, aby przejść w dół i odtworzyć dźwięk, gdy ktoś naciska na nich.

  1. Babylon.js zapewnia różne rodzaje zdarzeń lub możliwości obserwowania, z którymi możemy korzystać. W naszym przypadku będziemy radzić sobie z onPointerObservable tym, ponieważ chcemy programować klawisze do wykonywania akcji, gdy ktoś naciska na nich wskaźnik, który może być kliknięciem myszy, dotknięciem, kliknięciem przycisku kontrolera XR itp.

    Oto podstawowa struktura sposobu dodawania dowolnego zachowania do elementu onPointerObservable:

    scene.onPointerObservable.add((pointerInfo) => {
        // do something
    });
    
  2. Podczas gdy Babylon.js zapewnia wiele różnych typów zdarzeń wskaźnika, będziemy używać tylko zdarzeń i POINTERUP do programowania zachowania klawiszy fortepianowych, korzystając z POINTERDOWN poniższej struktury:

    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. Najpierw pracujmy nad przeniesieniem klawisza fortepianu w dół i w górę, gdy naciskamy i zwalniamy klawisz.

    W przypadku zdarzenia w dół wskaźnika musimy wykryć klikniętą siatkę, upewnić się, że jest to klawisz fortepianowy, i zmienić współrzędną y siatki negatywnie przez niewielką ilość, aby wyglądała jak klawisz został naciśnięty w dół.

    W przypadku zdarzenia wskaźnika jest to nieco bardziej skomplikowane, ponieważ wskaźnik, który naciśnięty na klawiszu może nie zostać zwolniony na klawiszu. Na przykład ktoś może kliknąć klawisz C4, przeciągnąć mysz do E4, a następnie zwolnić ich kliknięcie. W tym przypadku nadal chcemy zwolnić klawisz, który został naciśnięty (C4) zamiast miejsca pointerUp wystąpienia zdarzenia (E4).

    Przyjrzyjmy się, w jaki sposób następujący kod osiąga to, czego chcemy:

    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;
        }
    });
    

    Wskaźnik pointerId jest unikatowy dla każdego wskaźnika i może pomóc nam zidentyfikować wskaźnik, gdy mamy wiele kontrolerów lub jeśli używamy ekranu dotykowego. W tym miejscu zainicjowaliśmy Map obiekt o nazwie pointerToKey do przechowywania relacji, w której wskaźnik nacisnął klawisz, aby wiedzieć, który klucz ma być zwalniany po wydaniu wskaźnika, niezależnie od tego, gdzie ma miejsce wydanie.

  4. Oto, jak wygląda interakcja z powyższym kodem:

    Interaktywne klawisze fortepianowe

  5. Teraz pracujemy nad odtwarzaniem i zatrzymywaniem dźwięku po naciśnięciu i zwolnieniu klawisza. Aby to osiągnąć, będziemy korzystać z biblioteki JavaScript o nazwie soundfont-player, która pozwala nam łatwo odtwarzać dźwięki MIDI instrumentu, który wybieramy.

    Pobierz minyfikowany kod biblioteki, zapisz go w tym samym folderze co index.htmli dołącz go do tagu <header> w 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>
    

    Po zaimportowaniu biblioteki poniżej przedstawiono sposób inicjowania instrumentu i odtwarzania/zatrzymywania dźwięków MIDI przy użyciu biblioteki:

    const pianoSound = await Soundfont.instrument(new AudioContext(), 'acoustic_grand_piano');
    const C4 = piano.play("C4"); // Play note C4
    C4.stop(); // Stop note C4
    
  6. Teraz uwzględnijmy to w zdarzeniach wskaźnika i sfinalizujmy kod dla tej sekcji:

    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;
        }
    });
    

    Ponieważ nazwaliśmy siatkę każdego klucza za pomocą notatki, którą reprezentuje, możemy łatwo wskazać, która notatka ma być odtwarzana, przekazując nazwę siatki do pianoSound.play() funkcji. Należy również pamiętać, że przechowujemy dźwięk na mapie, abyśmy wiedzieli, jaki dźwięk zatrzymać po wydaniu pointerToKey klucza.

Skalowanie fortepianu w trybie immersywnym VR

Do tej pory prawdopodobnie grałeś z fortepianem za pomocą myszy (a nawet z ekranem dotykowym), jak dodano funkcje interaktywne. W tej sekcji przeniesiemy się do immersyjnej przestrzeni VR.

  1. Aby otworzyć stronę w immersywnym zestawie słuchawkowym VR, należy najpierw połączyć zestaw słuchawkowy z maszyną dewelopera i upewnić się, że jest on skonfigurowany do użycia w aplikacji Windows Mixed Reality. Jeśli używasz symulatora Windows Mixed Reality, upewnij się, że jest ona włączona.

  2. Teraz zobaczysz przycisk Immersyjny VR w prawym dolnym rogu strony internetowej. Kliknij go i zobaczysz fortepian na urządzeniu XR, z którym masz połączenie.

    Immersyjny przycisk VR

  3. Gdy jesteś w przestrzeni wirtualnej, możesz zauważyć, że fortepian, który zbudowaliśmy, jest niezwykle ogromny. W świecie VR możemy stać tylko na dole i grać go, wskazując wskaźnik do kluczy w odległości.

    Ogromny fortepian

  4. Skalujmy fortepian w dół, tak aby jego rozmiar był bardziej jak normalny fortepian standup w prawdziwym życiu. W tym celu musimy użyć funkcji narzędzia, która umożliwia skalowanie siatki względem punktu w przestrzeni. Dodaj tę funkcję do scene.js (poza 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));
    }
    

    Ta funkcja przyjmuje 3 parametry:

    • transformNode: TransformNode skalowany element
    • pivotPoint: Vector3 obiekt wskazujący punkt, w którym skalowanie jest względne
    • skalowanie: współczynnik skalowania
  5. Użyjemy tej funkcji do skalowania ramki i klawiszy fortepianu przez współczynnik 0,015, z punktem przestawnym na początku. Dołącz wywołanie funkcji do createScene() funkcji, umieszczając ją po 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. Nie zapomnijmy również skalować pozycji kamery:

    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. Teraz, gdy ponownie wchodzimy do przestrzeni VR, fortepian będzie miał rozmiar zwykłego fortepianu standup.

    Normalny fortepian standup w VR

Włączanie funkcji WebXR

Teraz, gdy przeskalowaliśmy fortepian do odpowiedniego rozmiaru w przestrzeni VR, włączmy kilka fajnych funkcji WebXR, aby poprawić nasze doświadczenie w grze na fortepianie w przestrzeni.

  1. Jeśli grasz na fortepianie przy użyciu immersyjnych kontrolerów VR, możesz zauważyć, że można używać tylko jednego kontrolera naraz. Włączmy obsługę wielu wskaźników w przestrzeni XR przy użyciu menedżera funkcji WebXR Babylon.js.

    Dodaj następujący kod do createScene() funkcji po wierszu inicjowania xrHelper :

    const featuresManager = xrHelper.baseExperience.featuresManager;
    
    const pointerSelection = featuresManager.enableFeature(BABYLON.WebXRFeatureName.POINTER_SELECTION, "stable", {
        xrInput: xrHelper.input,
        enablePointerSelectionOnAllControllers: true        
    });
    
  2. Ponadto, w zależności od tego, gdzie znajduje się punkt wyjścia, może okazać się trochę trudne, aby umieścić się przed fortepianem. Jeśli znasz środowisko immersywne VR, możesz już wiedzieć o teleportacji, która umożliwia natychmiastowe przejście do innego miejsca w przestrzeni, wskazując na nią.

  3. Aby móc korzystać z funkcji teleportacji Babylon.js, najpierw musimy mieć siatkę ziemi, na której możemy "stać" w przestrzeni VR. Dodaj następujący kod do funkcji w createScene() celu utworzenia podstawy:

    const ground = BABYLON.MeshBuilder.CreateGround("ground", {width: 400, height: 400});
    
  4. Obsługa teleportacji jest również wyposażona w bardzo przydatną funkcję o nazwie przyciąganie do pozycji. Krótko mówiąc, pozycja przyciągania do pozycji to konkretne pozycje, na których chcemy, aby użytkownicy wylądowali.

    Na przykład możemy ustawić przystawkę, aby umieścić przed fortepianem, aby użytkownicy mogli łatwo teleportować do tej lokalizacji, gdy wskazują swoje wskaźniki w pobliżu fortepianu.

    Dołącz poniższy kod, aby włączyć funkcję teleportacji i określić punkt przyciągania:

    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. Teraz powinieneś być w stanie łatwo ustawić się przed fortepianem, teleportując do przystawki do punktu przed fortepianem, i powinieneś być w stanie grać dwa klucze w czasie przy użyciu obu kontrolerów.

    Teleportacja na fortepian

    Gra na fortepianie przy użyciu kontrolerów

Podsumowanie

Gratulacje! Ukończyliśmy naszą serię samouczka Babylon.js kompilowania fortepianu i nauczyliśmy się:

  • Tworzenie, pozycjonowanie i scalanie siatki w celu utworzenia modelu klawiatury fortepianowej
  • Importowanie modelu Babylon.js ramki fortepianu standup
  • Dodawanie interakcji wskaźnika do każdego klawisza fortepianowego
  • Skalowanie rozmiaru siatki na podstawie punktu przestawnego
  • Włączanie kluczowych funkcji webXR, takich jak obsługa teleportacji i multipointera

Oto końcowy kod dlascene.js i 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>

Następne kroki

Aby uzyskać więcej informacji na temat Mixed Reality programowania w języku JavaScript, zobacz Omówienie programowania w języku JavaScript.