Add XR sample assets and update settings

This commit is contained in:
2026-01-21 14:18:30 +01:00
parent da213b4475
commit 0ae28bf32d
1990 changed files with 506411 additions and 76 deletions

View File

@@ -0,0 +1,115 @@
using UnityEngine;
using System.Runtime.InteropServices;
/// <summary>
/// A static class that initializes a test surface on Android
/// using Java Native Interface (JNI) calls. It facilitates interaction between Unity and
/// Android native code, particularly for handling texture and surface operations.
/// </summary>
public static class AndroidTestSurface
{
private static System.IntPtr? _TestSurfaceClass;
private static System.IntPtr initTestSurfaceMethodId;
/// <summary>
/// Gets the reference to the Java class 'TestSurface'. It uses JNI to find and hold a
/// global reference to the class for future use.
/// </summary>
private static System.IntPtr TestSurfaceClass
{
get
{
if (!_TestSurfaceClass.HasValue)
{
try
{
// Find the Java class and create a global reference
System.IntPtr testSurfaceClassLocal = AndroidJNI.FindClass("com/unity/xr/compositorlayers/TestSurface");
if (testSurfaceClassLocal != System.IntPtr.Zero)
{
_TestSurfaceClass = AndroidJNI.NewGlobalRef(testSurfaceClassLocal);
AndroidJNI.DeleteLocalRef(testSurfaceClassLocal);
}
else
{
Debug.LogError("Failed to find Java class `TestSurface`");
_TestSurfaceClass = System.IntPtr.Zero;
}
}
catch (System.Exception ex)
{
Debug.LogError("Exception occurred while finding Java class `TestSurface`");
Debug.LogException(ex);
_TestSurfaceClass = System.IntPtr.Zero;
}
}
return _TestSurfaceClass.GetValueOrDefault();
}
}
/// <summary>
/// Initializes a test surface using a JNI call to the 'InitTestSurface' method of the
/// 'TestSurface' Java class. It passes a native Android object and a texture converted to
/// a Bitmap to the Java method.
/// </summary>
/// <param name="jobject">The native Android object to which the surface is attached.</param>
/// <param name="texture">The Unity Texture2D to be converted and passed as a Bitmap.</param>
public static void InitTestSurface(System.IntPtr jobject, Texture2D texture)
{
if (initTestSurfaceMethodId == System.IntPtr.Zero)
{
// Retrieve the method ID for 'InitTestSurface'
initTestSurfaceMethodId = AndroidJNI.GetStaticMethodID(TestSurfaceClass, "InitTestSurface", "(Ljava/lang/Object;Landroid/graphics/Bitmap;)V");
}
// Convert the texture to a Bitmap and call the Java method
using (AndroidJavaObject bitmap = Texture2DToBitmap(texture))
{
System.IntPtr bitmapPtr = bitmap.GetRawObject();
jvalue[] args = new jvalue[2];
args[0] = new jvalue { l = jobject };
args[1] = new jvalue { l = bitmapPtr };
AndroidJNI.CallStaticVoidMethod(TestSurfaceClass, initTestSurfaceMethodId, args);
}
}
/// <summary>
/// Converts a Unity Texture2D to an Android Bitmap object. This is used to pass textures
/// from Unity to Android native code.
/// </summary>
/// <param name="texture">The Unity Texture2D to be converted.</param>
/// <returns>An AndroidJavaObject representing the Bitmap.</returns>
private static AndroidJavaObject Texture2DToBitmap(Texture2D texture)
{
byte[] imageBytes = texture.EncodeToPNG();
sbyte[] signedImageBytes = (sbyte[])(System.Array)imageBytes;
using (AndroidJavaClass byteBufferClass = new AndroidJavaClass("java.nio.ByteBuffer"))
{
AndroidJavaObject byteBuffer = byteBufferClass.CallStatic<AndroidJavaObject>("wrap", signedImageBytes);
using (AndroidJavaClass bitmapFactory = new AndroidJavaClass("android.graphics.BitmapFactory"))
{
AndroidJavaObject bitmap = bitmapFactory.CallStatic<AndroidJavaObject>("decodeByteArray", byteBuffer.Call<sbyte[]>("array"), 0, signedImageBytes.Length);
if (SystemInfo.graphicsDeviceType == UnityEngine.Rendering.GraphicsDeviceType.OpenGLES3)
{
using (var matrix = new AndroidJavaObject("android.graphics.Matrix"))
{
matrix.Call<bool>("preScale", 1.0f, -1.0f);
using (var bitmapClass = new AndroidJavaClass("android.graphics.Bitmap"))
{
AndroidJavaObject flippedBitmap = bitmapClass.CallStatic<AndroidJavaObject>("createBitmap",
bitmap, 0, 0, bitmap.Call<int>("getWidth"), bitmap.Call<int>("getHeight"), matrix, false);
return flippedBitmap;
}
}
}
return bitmap;
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 907440e96477d434b8027232e72b4b33
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ac2dd0c0c645be04ba9fa58a6af05431
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,44 @@
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;
using UnityEngine;
/// <summary>
/// A class that ensures that the Android minimum SDK version meets certain requirements.
/// </summary>
[InitializeOnLoad]
public class AndroidBuildSettings : IPreprocessBuildWithReport
{
/// Specifies the order in which this pre-build process should be executed.
public int callbackOrder { get { return 0; } }
/// Static constructor called on load to initially set Android build settings.
static AndroidBuildSettings()
{
SetAndroidBuildSettings();
}
/// <summary>
/// Called before the build process begins. Ensures Android build settings are correctly set.
/// </summary>
/// <param name="report">Contains information about the build, such as its target platform and output path.</param>
public void OnPreprocessBuild(BuildReport report)
{
SetAndroidBuildSettings();
}
/// <summary>
/// Sets the minimum SDK version for Android builds to a specified level if it is currently set lower.
/// This ensures compatibility with features provided in composition layers.
/// </summary>
private static void SetAndroidBuildSettings()
{
if (PlayerSettings.Android.minSdkVersion < AndroidSdkVersions.AndroidApiLevel24)
{
PlayerSettings.Android.minSdkVersion = AndroidSdkVersions.AndroidApiLevel24;
Debug.Log("Android minimum SDK version has been updated to Level 24. The lowest level supported by the composition layers package.");
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 66d508d62ebbcb742b8674838d4b2a11
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,68 @@
#if UNITY_EDITOR && UNITY_ANDROID
using UnityEditor;
using UnityEditor.Android;
using System.IO;
using UnityEngine;
/// <summary>
/// This class primarily focuses on copying specific files
/// from a source folder to the Android project's assets directory after the Gradle project
/// has been generated.
/// </summary>
public class UploadAndroidFiles : IPostGenerateGradleAndroidProject
{
/// <summary>
/// Specifies the order in which this post-processing task should be executed.
/// </summary>
public int callbackOrder => 1;
/// <summary>
/// Called after the Gradle Android project is generated. Copies files from a specified
/// source folder to the project's assets directory.
/// </summary>
/// <param name="path">The path to the generated Gradle project.</param>
public void OnPostGenerateGradleAndroidProject(string path)
{
string sourceFolder = Path.Combine(Application.dataPath, "Samples/XR Composition Layers/2.2.0/Sample External Android Surface Project/StreamingAssets");
string destinationFolder = Path.Combine(path, "src/main/assets");
// Create the destination folder if it does not exist
if (!Directory.Exists(destinationFolder))
{
Directory.CreateDirectory(destinationFolder);
}
CopyFilesRecursively(sourceFolder, destinationFolder);
}
/// <summary>
/// Copies all files and subdirectories from a source path to a target path recursively.
/// </summary>
/// <param name="sourcePath">The source directory path.</param>
/// <param name="targetPath">The target directory path where files should be copied.</param>
private static void CopyFilesRecursively(string sourcePath, string targetPath)
{
// Copy each file in the source directory to the target directory
foreach (string filePath in Directory.GetFiles(sourcePath))
{
string fileName = Path.GetFileName(filePath);
string destFile = Path.Combine(targetPath, fileName);
File.Copy(filePath, destFile, true);
}
// Recursively copy each subdirectory
foreach (string subdirectoryPath in Directory.GetDirectories(sourcePath))
{
string subdirectoryName = Path.GetFileName(subdirectoryPath);
string destSubdirectory = Path.Combine(targetPath, subdirectoryName);
if (!Directory.Exists(destSubdirectory))
{
Directory.CreateDirectory(destSubdirectory);
}
CopyFilesRecursively(subdirectoryPath, destSubdirectory);
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9c6257f7aad5b4445ad10809fcc8f816
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,137 @@
using UnityEngine;
using System;
using System.Collections;
using System.IO;
using Unity.XR.CompositionLayers;
#if UNITY_XR_OPENXR_COMPLAYER
using UnityEngine.XR.OpenXR.CompositionLayers;
#endif
using UnityEngine.Networking;
/// <summary>
/// Class for loading and displaying an image on Android devices.
/// </summary>
public class TestAndroidImage : MonoBehaviour
{
public string ImageName;
/// <summary>
/// Coroutine started upon script activation to initiate image loading.
/// </summary>
/// <returns>IEnumerator for coroutine management.</returns>
private IEnumerator Start()
{
yield return StartCoroutine(LoadImage());
}
/// <summary>
/// Main coroutine handling the image loading process based on the platform.
/// Loads an image using specified ImageName and displays it if found.
/// </summary>
/// <returns>IEnumerator for coroutine management.</returns>
private IEnumerator LoadImage()
{
if (!string.IsNullOrEmpty(ImageName))
{
Texture2D image = null;
#if UNITY_EDITOR
// Load image for Unity Editor
image = LoadImageForEditor();
#elif UNITY_ANDROID
// Load image for Android
yield return StartCoroutine(LoadImageForAndroid(loadedImage => image = loadedImage));
#endif
if (image != null)
{
// Display the loaded image
yield return DisplayImage(image);
}
else
{
Debug.LogError("Image not found at path: " + ImageName);
}
}
}
#if UNITY_EDITOR
/// <summary>
/// Loads an image from the project's assets when running in the Unity Editor.
/// </summary>
/// <returns>Texture2D - The loaded image texture.</returns>
private Texture2D LoadImageForEditor()
{
var guids = UnityEditor.AssetDatabase.FindAssets(Path.GetFileNameWithoutExtension(ImageName));
if (guids.Length > 0)
{
string assetPath = UnityEditor.AssetDatabase.GUIDToAssetPath(guids[0]);
return UnityEditor.AssetDatabase.LoadAssetAtPath<Texture2D>(assetPath);
}
return null;
}
#elif UNITY_ANDROID
/// <summary>
/// Coroutine for loading an image in an Android build. Uses UnityWebRequest to fetch the image.
/// </summary>
/// <param name="onLoaded">Callback action to handle the loaded Texture2D.</param>
/// <returns>IEnumerator for coroutine management.</returns>
private IEnumerator LoadImageForAndroid(Action<Texture2D> onLoaded)
{
string filePath = Path.Combine(Application.streamingAssetsPath, ImageName);
using (UnityWebRequest uwr = UnityWebRequestTexture.GetTexture(filePath))
{
yield return uwr.SendWebRequest();
if (uwr.result == UnityWebRequest.Result.ConnectionError || uwr.result == UnityWebRequest.Result.ProtocolError)
{
Debug.LogError("Error while loading image: " + uwr.error);
onLoaded(null);
}
else
{
onLoaded(DownloadHandlerTexture.GetContent(uwr));
}
}
}
#endif
/// <summary>
/// Coroutine handling the display of a loaded image using XR composition layers.
/// Waits for the surface from OpenXRLayerUtility before displaying the image.
/// </summary>
/// <param name="image">The Texture2D image to be displayed.</param>
/// <returns>IEnumerator for coroutine management.</returns>
private IEnumerator DisplayImage(Texture2D image)
{
if (image != null)
{
CompositionLayer layer = gameObject.GetComponent<CompositionLayer>();
IntPtr surface = IntPtr.Zero;
yield return new WaitUntil(() =>
{
#if UNITY_XR_OPENXR_COMPLAYER
surface = OpenXRLayerUtility.GetLayerAndroidSurfaceObject(layer.GetInstanceID());
#endif
return (surface != IntPtr.Zero);
});
AndroidTestSurface.InitTestSurface(surface, image);
}
else
{
Debug.LogError("No image provided.");
}
}
/// <summary>
/// Event handler triggered when the application gains or loses focus.
/// Restarts image loading process when the application gains focus.
/// </summary>
/// <param name="hasFocus">Boolean indicating if the application has focus.</param>
void OnApplicationFocus(bool hasFocus)
{
if (hasFocus)
{
StartCoroutine(LoadImage());
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e1537674bf1cc874f9b390067e444367
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: