â—€ Overview
Custom VR Button, that appears only on headsets and not on mobile phones

I combined two checks - Needle's check to see if it's a mobile device (this way, you can exclude desktops), and then a second check that uses user agent info to see if it's one of the most common mobile systems. Result: the button does not appear on mobile phones, but it is visible on Quest and Pico 🙂

P.S: I am using Svelte for 2D UI handling.

import { isMobileDevice, NeedleXRSession } from "@needle-tools/engine";

...

// check if this is a mobile phone
function isMobilePhone() {
    return /Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
}

...

// show the button, if the device is not a mobile phone and VR is supported
{#if  isMobileDevice() && !isMobilePhone() && $haveVR }
    <VrButton buttonFunction={() => StartVR()} />
{/if} 
Set fallback material for USDZ exporter

If you want to set a fallback material for an object that will be exported as USDZ (for AR-mode on iOS), you can add this script to the object, which material should be replaced.

This is especially useful if you use custom shaders in your scene (they are visible on Desktop+WebXR, but not in AR on iOS).

import { Behaviour, GameObject, Renderer, USDZExporter, serializable } from "@needle-tools/engine";
import { Material, Object3D } from "three";

export class FallbackMaterial extends Behaviour {

    @serializable(Material)
    fallbackMaterial!: Material;

    private originalMaterial?: Material;
    private usdzExporter!: USDZExporter;

    onEnable() {
        this.usdzExporter = GameObject.findObjectOfType(USDZExporter)!;
        this.subscribeToBeforeExportEvent();
    }

    onDisable() {
        this.unsubscribeFromBeforeExportEvent();
    }

    private subscribeToBeforeExportEvent() {
        this.usdzExporter.addEventListener("before-export", this.onBeforeExport);
        this.usdzExporter.addEventListener("after-export", this.onAfterExport);
    }

    private unsubscribeFromBeforeExportEvent() {
        this.usdzExporter.removeEventListener("before-export", this.onBeforeExport);
        this.usdzExporter.removeEventListener("after-export", this.onAfterExport);
    }


    onBeforeExport = () => {
        console.log("onBeforeExport");
        const renderer = this.gameObject.getComponent(Renderer)!;
        this.originalMaterial = renderer.sharedMaterial;
        renderer.sharedMaterial = this.fallbackMaterial;

    }

    onAfterExport = () => {
        console.log("onAfterExport");
        const renderer = this.gameObject.getComponent(Renderer)!;
        renderer.sharedMaterial = this.originalMaterial;
    }
}