2025-04-25 21:14:54 +09:00

343 lines
14 KiB
C#

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using UniGLTF;
using UnityEngine;
using VRMShaders;
namespace UniVRM10
{
/// <summary>
/// High-level VRM-1.0 loading API.
/// </summary>
public static class Vrm10
{
/// <summary>
/// You can receive the thumbnail texture and the meta information.
/// `vrm10Meta` will be available if the model was vrm-1.0.
/// `vrm0Meta` will be available if the model was vrm-0.x.
/// </summary>
public delegate void VrmMetaInformationCallback(Texture2D thumbnail, UniGLTF.Extensions.VRMC_vrm.Meta vrm10Meta, Migration.Vrm0Meta vrm0Meta);
/// <summary>
/// Load the VRM file from the path.
///
/// You should call this on Unity main thread.
/// This will throw Exceptions (include OperationCanceledException).
/// </summary>
/// <param name="path">vrm file path</param>
/// <param name="canLoadVrm0X">if true, this loader can load the vrm-0.x model as vrm-1.0 model with migration.</param>
/// <param name="controlRigGenerationOption">the flag of generating the control rig provides bone manipulation unified between models.</param>
/// <param name="showMeshes">if true, show meshes when loaded.</param>
/// <param name="awaitCaller">this loader use specified await strategy.</param>
/// <param name="textureDeserializer">this loader use specified texture deserialization strategy.</param>
/// <param name="materialGenerator">this loader use specified material generation strategy.</param>
/// <param name="vrmMetaInformationCallback">return callback that notify meta information before loading.</param>
/// <param name="ct">CancellationToken</param>
/// <returns>vrm-1.0 instance. Maybe return null if unexpected error was raised.</returns>
public static async Task<Vrm10Instance> LoadPathAsync(
string path,
bool canLoadVrm0X = true,
ControlRigGenerationOption controlRigGenerationOption = ControlRigGenerationOption.Generate,
bool showMeshes = true,
IAwaitCaller awaitCaller = null,
ITextureDeserializer textureDeserializer = null,
IMaterialDescriptorGenerator materialGenerator = null,
VrmMetaInformationCallback vrmMetaInformationCallback = null,
CancellationToken ct = default)
{
if (awaitCaller == null)
{
awaitCaller = Application.isPlaying
? (IAwaitCaller)new RuntimeOnlyAwaitCaller()
: (IAwaitCaller)new ImmediateCaller();
}
return await LoadAsync(
path,
System.IO.File.ReadAllBytes(path),
canLoadVrm0X,
controlRigGenerationOption,
showMeshes,
awaitCaller,
textureDeserializer,
materialGenerator,
vrmMetaInformationCallback,
ct);
}
/// <summary>
/// Load the VRM file from the binary.
///
/// You should call this on Unity main thread.
/// This will throw Exceptions (include OperationCanceledException).
/// </summary>
/// <param name="bytes">vrm file data</param>
/// <param name="canLoadVrm0X">if true, this loader can load the vrm-0.x model as vrm-1.0 model with migration.</param>
/// <param name="controlRigGenerationOption">the flag of generating the control rig provides bone manipulation unified between models.</param>
/// <param name="showMeshes">if true, show meshes when loaded.</param>
/// <param name="awaitCaller">this loader use specified await strategy.</param>
/// <param name="textureDeserializer">this loader use specified texture deserialization strategy.</param>
/// <param name="materialGenerator">this loader use specified material generation strategy.</param>
/// <param name="vrmMetaInformationCallback">return callback that notify meta information before loading.</param>
/// <param name="ct">CancellationToken</param>
/// <returns>vrm-1.0 instance. Maybe return null if unexpected error was raised.</returns>
public static async Task<Vrm10Instance> LoadBytesAsync(
byte[] bytes,
bool canLoadVrm0X = true,
ControlRigGenerationOption controlRigGenerationOption = ControlRigGenerationOption.Generate,
bool showMeshes = true,
IAwaitCaller awaitCaller = null,
ITextureDeserializer textureDeserializer = null,
IMaterialDescriptorGenerator materialGenerator = null,
VrmMetaInformationCallback vrmMetaInformationCallback = null,
CancellationToken ct = default)
{
if (awaitCaller == null)
{
awaitCaller = Application.isPlaying
? (IAwaitCaller)new RuntimeOnlyAwaitCaller()
: (IAwaitCaller)new ImmediateCaller();
}
return await LoadAsync(
string.Empty,
bytes,
canLoadVrm0X,
controlRigGenerationOption,
showMeshes,
awaitCaller,
textureDeserializer,
materialGenerator,
vrmMetaInformationCallback,
ct);
}
private static async Task<Vrm10Instance> LoadAsync(
string name,
byte[] bytes,
bool canLoadVrm0X,
ControlRigGenerationOption controlRigGenerationOption,
bool showMeshes,
IAwaitCaller awaitCaller,
ITextureDeserializer textureDeserializer,
IMaterialDescriptorGenerator materialGenerator,
VrmMetaInformationCallback vrmMetaInformationCallback,
CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
if (awaitCaller == null)
{
throw new ArgumentNullException();
}
using (var gltfData = new GlbLowLevelParser(name, bytes).Parse())
{
// 1. Try loading as vrm-1.0
var instance = await TryLoadingAsVrm10Async(
gltfData,
controlRigGenerationOption,
showMeshes,
awaitCaller,
textureDeserializer,
materialGenerator,
vrmMetaInformationCallback,
ct);
if (instance != null)
{
if (ct.IsCancellationRequested)
{
UnityObjectDestroyer.DestroyRuntimeOrEditor(instance.gameObject);
ct.ThrowIfCancellationRequested();
}
return instance;
}
// 2. Stop loading if not allowed migration.
if (!canLoadVrm0X)
{
throw new Exception($"Failed to load as VRM 1.0: {name}");
}
// 3. Try migration from vrm-0.x into vrm-1.0
var migratedInstance = await TryMigratingFromVrm0XAsync(
gltfData,
controlRigGenerationOption,
showMeshes,
awaitCaller,
textureDeserializer,
materialGenerator,
vrmMetaInformationCallback,
ct);
if (migratedInstance != null)
{
if (ct.IsCancellationRequested)
{
UnityObjectDestroyer.DestroyRuntimeOrEditor(migratedInstance.gameObject);
ct.ThrowIfCancellationRequested();
}
return migratedInstance;
}
// 4. Failed loading.
throw new Exception($"Failed to load: {name}");
}
}
private static async Task<Vrm10Instance> TryLoadingAsVrm10Async(
GltfData gltfData,
ControlRigGenerationOption controlRigGenerationOption,
bool showMeshes,
IAwaitCaller awaitCaller,
ITextureDeserializer textureDeserializer,
IMaterialDescriptorGenerator materialGenerator,
VrmMetaInformationCallback vrmMetaInformationCallback,
CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
if (awaitCaller == null)
{
throw new ArgumentNullException();
}
var vrm10Data = await awaitCaller.Run(() => Vrm10Data.Parse(gltfData));
ct.ThrowIfCancellationRequested();
if (vrm10Data == null)
{
// NOTE: Failed to parse as VRM 1.0.
return null;
}
return await LoadVrm10DataAsync(
vrm10Data,
null,
controlRigGenerationOption,
showMeshes,
awaitCaller,
textureDeserializer,
materialGenerator,
vrmMetaInformationCallback,
ct);
}
private static async Task<Vrm10Instance> TryMigratingFromVrm0XAsync(
GltfData gltfData,
ControlRigGenerationOption controlRigGenerationOption,
bool showMeshes,
IAwaitCaller awaitCaller,
ITextureDeserializer textureDeserializer,
IMaterialDescriptorGenerator materialGenerator,
VrmMetaInformationCallback vrmMetaInformationCallback,
CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
if (awaitCaller == null)
{
throw new ArgumentNullException();
}
Vrm10Data migratedVrm10Data = default;
MigrationData migrationData = default;
using (var migratedGltfData = await awaitCaller.Run(() => Vrm10Data.Migrate(gltfData, out migratedVrm10Data, out migrationData)))
{
ct.ThrowIfCancellationRequested();
if (migratedVrm10Data == null)
{
throw new Exception(migrationData?.Message ?? "Failed to migrate.");
}
var migratedVrm10Instance = await LoadVrm10DataAsync(
migratedVrm10Data,
migrationData,
controlRigGenerationOption,
showMeshes,
awaitCaller,
textureDeserializer,
materialGenerator,
vrmMetaInformationCallback,
ct);
if (migratedVrm10Instance == null)
{
throw new Exception(migrationData?.Message ?? "Failed to load migrated.");
}
return migratedVrm10Instance;
}
}
private static async Task<Vrm10Instance> LoadVrm10DataAsync(
Vrm10Data vrm10Data,
MigrationData migrationData,
ControlRigGenerationOption controlRigGenerationOption,
bool showMeshes,
IAwaitCaller awaitCaller,
ITextureDeserializer textureDeserializer,
IMaterialDescriptorGenerator materialGenerator,
VrmMetaInformationCallback vrmMetaInformationCallback,
CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
if (awaitCaller == null)
{
throw new ArgumentNullException();
}
if (vrm10Data == null)
{
throw new ArgumentNullException(nameof(vrm10Data));
}
using (var loader = new Vrm10Importer(
vrm10Data,
textureDeserializer: textureDeserializer,
materialGenerator: materialGenerator,
useControlRig: controlRigGenerationOption != ControlRigGenerationOption.None))
{
// 1. Load meta information if callback was available.
if (vrmMetaInformationCallback != null)
{
var thumbnail = await loader.LoadVrmThumbnailAsync();
if (migrationData != null)
{
vrmMetaInformationCallback(thumbnail, default, migrationData.OriginalMetaBeforeMigration);
}
else
{
vrmMetaInformationCallback(thumbnail, vrm10Data.VrmExtension.Meta, default);
}
}
// 2. Load
// NOTE: Current Vrm10Importer.LoadAsync implementation CAN'T ABORT.
var gltfInstance = await loader.LoadAsync(awaitCaller);
if (gltfInstance == null)
{
throw new Exception("Failed to load by unknown reason.");
}
var vrm10Instance = gltfInstance.GetComponent<Vrm10Instance>();
if (vrm10Instance == null)
{
gltfInstance.Dispose();
throw new Exception("Failed to load as VRM by unknown reason.");
}
if (ct.IsCancellationRequested)
{
// NOTE: Destroy before showing meshes if canceled.
gltfInstance.Dispose();
ct.ThrowIfCancellationRequested();
}
if (showMeshes)
{
gltfInstance.ShowMeshes();
}
return vrm10Instance;
}
}
}
}