diff --git a/Example/Example.xcodeproj/project.pbxproj b/Example/Example.xcodeproj/project.pbxproj index 9f57e2c..db20673 100644 --- a/Example/Example.xcodeproj/project.pbxproj +++ b/Example/Example.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 067C5C522AAD6D8B00F8FBB3 /* VRMSceneKit in Frameworks */ = {isa = PBXBuildFile; productRef = 067C5C512AAD6D8B00F8FBB3 /* VRMSceneKit */; }; 06E1164E2F277C6D00D74CA4 /* VRMKit in Frameworks */ = {isa = PBXBuildFile; productRef = 06E1164D2F277C6D00D74CA4 /* VRMKit */; }; 06E116502F277C6D00D74CA4 /* VRMRealityKit in Frameworks */ = {isa = PBXBuildFile; productRef = 06E1164F2F277C6D00D74CA4 /* VRMRealityKit */; }; + 06E116552F333D8700D74CA4 /* VRMSceneKit in Frameworks */ = {isa = PBXBuildFile; productRef = 06E116562F333D8700D74CA4 /* VRMSceneKit */; }; 06E116522F277D1800D74CA4 /* AliciaSolid.vrm in Resources */ = {isa = PBXBuildFile; fileRef = 06E116512F277D1700D74CA4 /* AliciaSolid.vrm */; }; 06E116542F277ED600D74CA4 /* AliciaSolid.vrm in Resources */ = {isa = PBXBuildFile; fileRef = 06E116512F277D1700D74CA4 /* AliciaSolid.vrm */; }; 06F0BD792AAD81A40089488C /* WatchExample Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 06F0BD6C2AAD81A30089488C /* WatchExample Watch App.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -106,6 +107,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 06E116552F333D8700D74CA4 /* VRMSceneKit in Frameworks */, 06E116502F277C6D00D74CA4 /* VRMRealityKit in Frameworks */, 06E1164E2F277C6D00D74CA4 /* VRMKit in Frameworks */, ); @@ -212,6 +214,7 @@ packageProductDependencies = ( 06E1164D2F277C6D00D74CA4 /* VRMKit */, 06E1164F2F277C6D00D74CA4 /* VRMRealityKit */, + 06E116562F333D8700D74CA4 /* VRMSceneKit */, ); productName = MacExample; productReference = 06E116422F277AEA00D74CA4 /* MacExample.app */; @@ -866,6 +869,10 @@ isa = XCSwiftPackageProductDependency; productName = VRMRealityKit; }; + 06E116562F333D8700D74CA4 /* VRMSceneKit */ = { + isa = XCSwiftPackageProductDependency; + productName = VRMSceneKit; + }; 06F0BD7E2AAD82120089488C /* VRMKit */ = { isa = XCSwiftPackageProductDependency; package = A1B2C3D4E5F60718293A4B61 /* XCLocalSwiftPackageReference ".." */; diff --git a/Example/Example.xcodeproj/xcshareddata/xcschemes/MacExample.xcscheme b/Example/Example.xcodeproj/xcshareddata/xcschemes/MacExample.xcscheme new file mode 100644 index 0000000..3b72f4d --- /dev/null +++ b/Example/Example.xcodeproj/xcshareddata/xcschemes/MacExample.xcscheme @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/Example/ExampleModel.swift b/Example/Example/ExampleModel.swift new file mode 100644 index 0000000..041d257 --- /dev/null +++ b/Example/Example/ExampleModel.swift @@ -0,0 +1,97 @@ +import CoreGraphics +import Foundation +internal import VRMKit +internal import VRMSceneKit + +#if canImport(RealityKit) +internal import VRMRealityKit +#endif + +enum VRMExampleModel: String, CaseIterable, Identifiable { + case alicia = "AliciaSolid.vrm" + case vrm1 = "VRM1_Constraint_Twist_Sample.vrm" + + var id: String { rawValue } + + var displayName: String { + switch self { + case .alicia: return "Alicia" + case .vrm1: return "VRM 1.0" + } + } + + var initialRotation: Float { + switch self { + case .alicia: return 0 + case .vrm1: return .pi + } + } +} + +enum ExampleExpression: String, CaseIterable { + case neutral + case joy + case angry + case sorrow + case fun + + var blendShapePreset: BlendShapePreset { + switch self { + case .neutral: return .neutral + case .joy: return .joy + case .angry: return .angry + case .sorrow: return .sorrow + case .fun: return .fun + } + } + + var expressionPreset: ExpressionPreset { + switch self { + case .neutral: return .neutral + case .joy: return .happy + case .angry: return .angry + case .sorrow: return .sad + case .fun: return .relaxed + } + } + + func displayName(for model: VRMExampleModel) -> String { + switch model { + case .alicia: + return rawValue.capitalized + case .vrm1: + switch self { + case .neutral: return "Neutral" + case .joy: return "Happy" + case .angry: return "Angry" + case .sorrow: return "Sad" + case .fun: return "Relaxed" + } + } + } +} + +extension VRMNode { + func setExampleExpression(_ expression: ExampleExpression, value: CGFloat) { + switch vrm { + case .v0: + setBlendShape(value: value, for: .preset(expression.blendShapePreset)) + case .v1: + setExpression(value: value, for: .preset(expression.expressionPreset)) + } + } +} + +#if canImport(RealityKit) +@available(iOS 18.0, *) +extension VRMEntity { + func setExampleExpression(_ expression: ExampleExpression, value: CGFloat) { + switch vrm { + case .v0: + setBlendShape(value: value, for: .preset(expression.blendShapePreset)) + case .v1: + setExpression(value: value, for: .preset(expression.expressionPreset)) + } + } +} +#endif diff --git a/Example/Example/RealityKitViewController.swift b/Example/Example/RealityKitViewController.swift index 9d09091..5439d6b 100644 --- a/Example/Example/RealityKitViewController.swift +++ b/Example/Example/RealityKitViewController.swift @@ -11,11 +11,13 @@ final class RealityKitViewController: UIViewController, UIGestureRecognizerDeleg private var loadedEntity: VRMEntity? private var cameraAnchor: AnchorEntity? private var cameraEntity: PerspectiveCamera? + private var expressionSegmentedControl: UISegmentedControl? private var orbitYaw: Float = 0 private var orbitPitch: Float = -0.1 private var orbitDistance: Float = 2 private var orbitTarget = SIMD3(0, 0.8, 0) - private var currentExpression: Expression = .neutral + private var currentModel: VRMExampleModel = .alicia + private var currentExpression: ExampleExpression = .neutral override func viewDidLoad() { super.viewDidLoad() @@ -52,12 +54,13 @@ final class RealityKitViewController: UIViewController, UIGestureRecognizerDeleg segmentedControl.translatesAutoresizingMaskIntoConstraints = false view.addSubview(segmentedControl) - let expressionItems = Expression.allCases.map { $0.displayName } + let expressionItems = ExampleExpression.allCases.map { $0.displayName(for: currentModel) } let expressionSegmentedControl = UISegmentedControl(items: expressionItems) expressionSegmentedControl.selectedSegmentIndex = 0 expressionSegmentedControl.addTarget(self, action: #selector(expressionSegmentChanged(_:)), for: .valueChanged) expressionSegmentedControl.translatesAutoresizingMaskIntoConstraints = false view.addSubview(expressionSegmentedControl) + self.expressionSegmentedControl = expressionSegmentedControl NSLayoutConstraint.activate([ segmentedControl.centerXAnchor.constraint(equalTo: view.centerXAnchor), @@ -73,15 +76,18 @@ final class RealityKitViewController: UIViewController, UIGestureRecognizerDeleg } @objc private func expressionSegmentChanged(_ sender: UISegmentedControl) { - let expression = Expression.allCases[sender.selectedSegmentIndex] - loadedEntity?.setBlendShape(value: 0.0, for: .preset(currentExpression.preset)) + let expression = ExampleExpression.allCases[sender.selectedSegmentIndex] + loadedEntity?.setExampleExpression(currentExpression, value: 0.0) currentExpression = expression - loadedEntity?.setBlendShape(value: 1.0, for: .preset(currentExpression.preset)) + loadedEntity?.setExampleExpression(currentExpression, value: 1.0) } private func loadVRM(model: VRMExampleModel) { guard let arView = arView else { return } + currentModel = model + updateExpressionLabels() + if let loadedEntity = loadedEntity { loadedEntity.entity.removeFromParent() self.loadedEntity = nil @@ -122,7 +128,7 @@ final class RealityKitViewController: UIViewController, UIGestureRecognizerDeleg if let rightArm { rightArm.transform.rotation = rightArm.transform.rotation * armRotation } - vrmEntity.setBlendShape(value: 1.0, for: .preset(currentExpression.preset)) + vrmEntity.setExampleExpression(currentExpression, value: 1.0) loadedEntity = vrmEntity @@ -153,6 +159,18 @@ final class RealityKitViewController: UIViewController, UIGestureRecognizerDeleg } } + private func updateExpressionLabels() { + guard let expressionSegmentedControl else { return } + let selectedIndex = expressionSegmentedControl.selectedSegmentIndex + expressionSegmentedControl.removeAllSegments() + for (index, expression) in ExampleExpression.allCases.enumerated() { + expressionSegmentedControl.insertSegment(withTitle: expression.displayName(for: currentModel), + at: index, + animated: false) + } + expressionSegmentedControl.selectedSegmentIndex = selectedIndex >= 0 ? selectedIndex : 0 + } + private func setUpCamera() { guard let arView = arView else { return } let cameraAnchor = AnchorEntity(world: .zero) diff --git a/Example/Example/ViewController.swift b/Example/Example/ViewController.swift index c3375cb..519f4bc 100644 --- a/Example/Example/ViewController.swift +++ b/Example/Example/ViewController.swift @@ -1,47 +1,7 @@ import UIKit import SceneKit -internal import VRMKit internal import VRMSceneKit -enum VRMExampleModel: String, CaseIterable, Identifiable { - case alicia = "AliciaSolid.vrm" - case vrm1 = "VRM1_Constraint_Twist_Sample.vrm" - - var id: String { rawValue } - - var displayName: String { - switch self { - case .alicia: return "Alicia" - case .vrm1: return "VRM 1.0" - } - } - - var initialRotation: Float { - switch self { - case .alicia: return 0 - case .vrm1: return .pi - } - } -} - -enum Expression: String, CaseIterable { - case neutral, joy, angry, sorrow, fun - - var preset: BlendShapePreset { - switch self { - case .neutral: return .neutral - case .joy: return .joy - case .angry: return .angry - case .sorrow: return .sorrow - case .fun: return .fun - } - } - - var displayName: String { - return rawValue.capitalized - } -} - class ViewController: UIViewController { @IBOutlet private weak var scnView: SCNView! { @@ -54,7 +14,9 @@ class ViewController: UIViewController { } private var vrmNode: VRMNode? - private var currentExpression: Expression = .neutral + private var expressionSegmentedControl: UISegmentedControl? + private var currentModel: VRMExampleModel = .alicia + private var currentExpression: ExampleExpression = .neutral override func viewDidLoad() { super.viewDidLoad() @@ -70,12 +32,13 @@ class ViewController: UIViewController { segmentedControl.translatesAutoresizingMaskIntoConstraints = false view.addSubview(segmentedControl) - let expressionItems = Expression.allCases.map { $0.displayName } + let expressionItems = ExampleExpression.allCases.map { $0.displayName(for: currentModel) } let expressionSegmentedControl = UISegmentedControl(items: expressionItems) expressionSegmentedControl.selectedSegmentIndex = 0 expressionSegmentedControl.addTarget(self, action: #selector(expressionSegmentChanged(_:)), for: .valueChanged) expressionSegmentedControl.translatesAutoresizingMaskIntoConstraints = false view.addSubview(expressionSegmentedControl) + self.expressionSegmentedControl = expressionSegmentedControl NSLayoutConstraint.activate([ segmentedControl.centerXAnchor.constraint(equalTo: view.centerXAnchor), @@ -92,14 +55,16 @@ class ViewController: UIViewController { } @objc private func expressionSegmentChanged(_ sender: UISegmentedControl) { - let expression = Expression.allCases[sender.selectedSegmentIndex] - vrmNode?.setBlendShape(value: 0.0, for: .preset(currentExpression.preset)) + let expression = ExampleExpression.allCases[sender.selectedSegmentIndex] + vrmNode?.setExampleExpression(currentExpression, value: 0.0) currentExpression = expression - vrmNode?.setBlendShape(value: 1.0, for: .preset(currentExpression.preset)) + vrmNode?.setExampleExpression(currentExpression, value: 1.0) } private func loadVRM(model: VRMExampleModel) { do { + currentModel = model + updateExpressionLabels() let loader = try VRMSceneLoader(named: model.rawValue) let scene = try loader.loadScene() setupScene(scene) @@ -111,7 +76,7 @@ class ViewController: UIViewController { let rotationOffset = CGFloat(model.initialRotation) node.eulerAngles = SCNVector3(0, rotationOffset, 0) - node.setBlendShape(value: 1.0, for: .preset(currentExpression.preset)) + node.setExampleExpression(currentExpression, value: 1.0) node.humanoid.node(for: .neck)?.eulerAngles = SCNVector3(0, 0, 20 * CGFloat.pi / 180) let leftArm: SCNNode? @@ -136,6 +101,18 @@ class ViewController: UIViewController { } } + private func updateExpressionLabels() { + guard let expressionSegmentedControl else { return } + let selectedIndex = expressionSegmentedControl.selectedSegmentIndex + expressionSegmentedControl.removeAllSegments() + for (index, expression) in ExampleExpression.allCases.enumerated() { + expressionSegmentedControl.insertSegment(withTitle: expression.displayName(for: currentModel), + at: index, + animated: false) + } + expressionSegmentedControl.selectedSegmentIndex = selectedIndex >= 0 ? selectedIndex : 0 + } + private func setupScene(_ scene: SCNScene) { let cameraNode = SCNNode() cameraNode.camera = SCNCamera() diff --git a/Example/MacExample/ContentView.swift b/Example/MacExample/ContentView.swift index 6e722f5..1e15c2a 100644 --- a/Example/MacExample/ContentView.swift +++ b/Example/MacExample/ContentView.swift @@ -7,59 +7,139 @@ // import SwiftUI +import SceneKit import RealityKit +internal import VRMSceneKit internal import VRMRealityKit internal import Combine internal import VRMKit struct ContentView: View { - @State private var viewModel = ContentViewModel() + @State private var realityKitViewModel = RealityKitContentViewModel() + @State private var sceneKitViewModel = SceneKitContentViewModel() + @State private var selectedRenderer: MacExampleRenderer = .realityKit @State private var selectedModel: MacExampleModel = .alicia + @State private var selectedExpression: MacExampleExpression = .neutral var body: some View { VStack { - Picker("Model", selection: $selectedModel) { - ForEach(MacExampleModel.allCases) { model in - Text(model.displayName).tag(model) + HStack { + Picker("Renderer", selection: $selectedRenderer) { + ForEach(MacExampleRenderer.allCases) { renderer in + Text(renderer.displayName).tag(renderer) + } } + .pickerStyle(.segmented) + + Picker("Model", selection: $selectedModel) { + ForEach(MacExampleModel.allCases) { model in + Text(model.displayName).tag(model) + } + } + .pickerStyle(.segmented) + + Picker("Expression", selection: $selectedExpression) { + ForEach(MacExampleExpression.allCases) { expression in + Text(expression.displayName(for: selectedModel)).tag(expression) + } + } + .pickerStyle(.segmented) } - .pickerStyle(.segmented) .padding([.top, .horizontal]) - RealityView { content in - content.add(viewModel.rootEntity) + switch selectedRenderer { + case .sceneKit: + SceneKitRendererView(viewModel: sceneKitViewModel, + selectedModel: selectedModel, + selectedExpression: selectedExpression) + case .realityKit: + RealityKitRendererView(viewModel: realityKitViewModel, + selectedModel: selectedModel, + selectedExpression: selectedExpression) } + } + .frame(minWidth: 800, minHeight: 600) + } +} + +private struct RealityKitRendererView: View { + let viewModel: RealityKitContentViewModel + let selectedModel: MacExampleModel + let selectedExpression: MacExampleExpression + + var body: some View { + RealityView { content in + content.add(viewModel.rootEntity) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .task(id: selectedModel) { + await viewModel.loadEntity(model: selectedModel, expression: selectedExpression) + } + .onChange(of: selectedExpression) { _, expression in + viewModel.setExpression(expression) + } + .onReceive(viewModel.updateTimer) { _ in + viewModel.update() + } + .overlay(alignment: .bottomLeading) { + if let errorMessage = viewModel.errorMessage { + ErrorMessageView(message: errorMessage) + } + } + } +} + +private struct SceneKitRendererView: View { + let viewModel: SceneKitContentViewModel + let selectedModel: MacExampleModel + let selectedExpression: MacExampleExpression + + var body: some View { + SceneKitView(scene: viewModel.scene) + .frame(maxWidth: .infinity, maxHeight: .infinity) .task(id: selectedModel) { - await viewModel.loadEntity(model: selectedModel) + await viewModel.loadScene(model: selectedModel, expression: selectedExpression) + } + .onChange(of: selectedExpression) { _, expression in + viewModel.setExpression(expression) } .onReceive(viewModel.updateTimer) { _ in viewModel.update() } - - if let errorMessage = viewModel.errorMessage { - Text("Error: \(errorMessage)") - .foregroundColor(.red) - .padding() + .overlay(alignment: .bottomLeading) { + if let errorMessage = viewModel.errorMessage { + ErrorMessageView(message: errorMessage) + } } - } - .frame(minWidth: 800, minHeight: 600) + } +} + +private struct ErrorMessageView: View { + let message: String + + var body: some View { + Text("Error: \(message)") + .foregroundStyle(.red) + .padding() } } @MainActor @Observable -final class ContentViewModel { +final class RealityKitContentViewModel { let rootEntity = Entity() private(set) var errorMessage: String? private var vrmEntity: VRMEntity? private var time: TimeInterval = 0 private var lastUpdateTime: Date? private var currentModel: MacExampleModel = .alicia + private var currentExpression: MacExampleExpression = .neutral let updateTimer = Timer.publish(every: 1.0 / 60.0, on: .main, in: .common).autoconnect() - func loadEntity(model: MacExampleModel) async { + func loadEntity(model: MacExampleModel, expression: MacExampleExpression) async { do { + errorMessage = nil if let vrmEntity { vrmEntity.entity.removeFromParent() self.vrmEntity = nil @@ -96,16 +176,24 @@ final class ContentViewModel { if let rightArm { rightArm.transform.rotation = rightArm.transform.rotation * armRotation } - vrmEntity.setBlendShape(value: 1.0, for: .custom("><")) + vrmEntity.setExampleExpression(expression, value: 1.0) self.vrmEntity = vrmEntity self.currentModel = model + self.currentExpression = expression self.lastUpdateTime = Date() } catch { errorMessage = error.localizedDescription print("VRM Load Error: \(error)") } } + + func setExpression(_ expression: MacExampleExpression) { + guard expression != currentExpression else { return } + vrmEntity?.setExampleExpression(currentExpression, value: 0.0) + currentExpression = expression + vrmEntity?.setExampleExpression(expression, value: 1.0) + } func update() { guard let vrmEntity else { return } @@ -133,27 +221,120 @@ final class ContentViewModel { } } -enum MacExampleModel: String, CaseIterable, Identifiable { - case alicia = "AliciaSolid.vrm" - case vrm1 = "VRM1_Constraint_Twist_Sample.vrm" +private struct SceneKitView: NSViewRepresentable { + let scene: SCNScene? + + func makeNSView(context: Context) -> SCNView { + let sceneView = SCNView() + sceneView.autoenablesDefaultLighting = true + sceneView.allowsCameraControl = true + sceneView.showsStatistics = true + sceneView.backgroundColor = .black + return sceneView + } + + func updateNSView(_ sceneView: SCNView, context: Context) { + sceneView.scene = scene + } +} + +@MainActor +@Observable +final class SceneKitContentViewModel { + private(set) var scene: VRMScene? + private(set) var errorMessage: String? + private var vrmNode: VRMNode? + private var time: TimeInterval = 0 + private var lastUpdateTime: Date? + private var currentModel: MacExampleModel = .alicia + private var currentExpression: MacExampleExpression = .neutral + + let updateTimer = Timer.publish(every: 1.0 / 60.0, on: .main, in: .common).autoconnect() + + func loadScene(model: MacExampleModel, expression: MacExampleExpression) async { + do { + errorMessage = nil + scene = nil + vrmNode = nil + time = 0 + + let loader = try VRMSceneLoader(named: model.rawValue) + let scene = try loader.loadScene() + setUpCamera(in: scene) + + let node = scene.vrmNode + node.eulerAngles = SCNVector3(0, CGFloat(model.sceneKitInitialRotation), 0) + applyPose(to: node) + node.setExampleExpression(expression, value: 1.0) + + self.scene = scene + self.vrmNode = node + self.currentModel = model + self.currentExpression = expression + self.lastUpdateTime = Date() + } catch { + errorMessage = error.localizedDescription + print("VRM Load Error: \(error)") + } + } + + func setExpression(_ expression: MacExampleExpression) { + guard expression != currentExpression else { return } + vrmNode?.setExampleExpression(currentExpression, value: 0.0) + currentExpression = expression + vrmNode?.setExampleExpression(expression, value: 1.0) + } + + func update() { + guard let vrmNode else { return } - var id: String { rawValue } + let now = Date() + let deltaTime = lastUpdateTime.map { now.timeIntervalSince($0) } ?? (1.0 / 60.0) + lastUpdateTime = now - var displayName: String { - switch self { - case .alicia: return "Alicia" - case .vrm1: return "VRM 1.0" + time += deltaTime + + let cycle = time.truncatingRemainder(dividingBy: 1.0) + let angle: Float + if cycle < 0.5 { + let progress = Float(cycle) / 0.5 + angle = -0.5 * progress + } else { + let progress = Float(cycle - 0.5) / 0.5 + angle = -0.5 + 0.5 * progress } + + vrmNode.eulerAngles = SCNVector3(0, CGFloat(currentModel.sceneKitInitialRotation + angle), 0) + vrmNode.update(at: time) } - var initialRotation: Float { - switch self { - case .alicia: return .pi - case .vrm1: return 0 + private func applyPose(to node: VRMNode) { + node.humanoid.node(for: .neck)?.eulerAngles = SCNVector3(0, 0, 20 * CGFloat.pi / 180) + + let leftArm: SCNNode? + let rightArm: SCNNode? + switch node.vrm { + case .v1: + leftArm = node.humanoid.node(for: .leftShoulder) + rightArm = node.humanoid.node(for: .rightShoulder) + case .v0: + leftArm = node.humanoid.node(for: .leftUpperArm) + rightArm = node.humanoid.node(for: .rightUpperArm) } + leftArm?.eulerAngles = SCNVector3(0, 0, 40 * CGFloat.pi / 180) + rightArm?.eulerAngles = SCNVector3(0, 0, 40 * CGFloat.pi / 180) + } + + private func setUpCamera(in scene: SCNScene) { + let cameraNode = SCNNode() + cameraNode.camera = SCNCamera() + cameraNode.position = SCNVector3(0, 0.8, -1.6) + cameraNode.rotation = SCNVector4(0, 1, 0, Float.pi) + scene.rootNode.addChildNode(cameraNode) } } + #Preview { ContentView() } diff --git a/Example/MacExample/MacExampleModel.swift b/Example/MacExample/MacExampleModel.swift new file mode 100644 index 0000000..0c3ba82 --- /dev/null +++ b/Example/MacExample/MacExampleModel.swift @@ -0,0 +1,114 @@ +import CoreGraphics +import Foundation +internal import VRMKit +internal import VRMSceneKit +internal import VRMRealityKit + +enum MacExampleRenderer: String, CaseIterable, Identifiable { + case sceneKit + case realityKit + + var id: String { rawValue } + + var displayName: String { + switch self { + case .sceneKit: return "SceneKit" + case .realityKit: return "RealityKit" + } + } +} + +enum MacExampleModel: String, CaseIterable, Identifiable { + case alicia = "AliciaSolid.vrm" + case vrm1 = "VRM1_Constraint_Twist_Sample.vrm" + + var id: String { rawValue } + + var displayName: String { + switch self { + case .alicia: return "Alicia" + case .vrm1: return "VRM 1.0" + } + } + + var initialRotation: Float { + switch self { + case .alicia: return .pi + case .vrm1: return 0 + } + } + + var sceneKitInitialRotation: Float { + switch self { + case .alicia: return 0 + case .vrm1: return .pi + } + } +} + +enum MacExampleExpression: String, CaseIterable, Identifiable { + case neutral + case joy + case angry + case sorrow + case fun + + var id: String { rawValue } + + var blendShapePreset: BlendShapePreset { + switch self { + case .neutral: return .neutral + case .joy: return .joy + case .angry: return .angry + case .sorrow: return .sorrow + case .fun: return .fun + } + } + + var expressionPreset: ExpressionPreset { + switch self { + case .neutral: return .neutral + case .joy: return .happy + case .angry: return .angry + case .sorrow: return .sad + case .fun: return .relaxed + } + } + + func displayName(for model: MacExampleModel) -> String { + switch model { + case .alicia: + return rawValue.capitalized + case .vrm1: + switch self { + case .neutral: return "Neutral" + case .joy: return "Happy" + case .angry: return "Angry" + case .sorrow: return "Sad" + case .fun: return "Relaxed" + } + } + } +} + +extension VRMEntity { + func setExampleExpression(_ expression: MacExampleExpression, value: CGFloat) { + switch vrm { + case .v0: + setBlendShape(value: value, for: .preset(expression.blendShapePreset)) + case .v1: + setExpression(value: value, for: .preset(expression.expressionPreset)) + } + } +} + +extension VRMNode { + func setExampleExpression(_ expression: MacExampleExpression, value: CGFloat) { + switch vrm { + case .v0: + setBlendShape(value: value, for: .preset(expression.blendShapePreset)) + case .v1: + setExpression(value: value, for: .preset(expression.expressionPreset)) + } + } +} diff --git a/README.md b/README.md index a277112..c6d69d4 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,9 @@ sceneView.scene = scene -### Blend shapes +### Blend shapes / expressions + +VRM 0.x uses blend shapes: joy @@ -142,12 +144,26 @@ vrmEntity.setBlendShape(value: 1.0, for: .preset(.angry)) vrmEntity.setBlendShape(value: 1.0, for: .custom("><")) ``` +VRM 1.0 uses expressions: + +```swift +vrmEntity.setExpression(value: 1.0, for: .preset(.happy)) +vrmEntity.setExpression(value: 1.0, for: .preset(.aa)) +vrmEntity.setExpression(value: 1.0, for: .custom("customExpressionName")) +``` + ### Bone animation Humanoid ```swift -vrmEntity.setBlendShape(value: 1.0, for: .preset(.fun)) +switch vrmEntity.vrm { +case .v0: + vrmEntity.setBlendShape(value: 1.0, for: .preset(.fun)) +case .v1: + vrmEntity.setExpression(value: 1.0, for: .preset(.relaxed)) +} + let neckRotation = simd_quatf(angle: 20 * .pi / 180, axis: SIMD3(0, 0, 1)) let armRotation = simd_quatf(angle: 40 * .pi / 180, axis: SIMD3(0, 0, 1)) let (leftArm, rightArm): (Entity?, Entity?) diff --git a/Sources/VRMKit/Extensions/VRMColor+SIMD.swift b/Sources/VRMKit/Extensions/VRMColor+SIMD.swift new file mode 100644 index 0000000..2fe6153 --- /dev/null +++ b/Sources/VRMKit/Extensions/VRMColor+SIMD.swift @@ -0,0 +1,28 @@ +import CoreGraphics +import simd + +extension VRMColor { + package convenience init(simd color: SIMD4) { + self.init(red: CGFloat(color.x), + green: CGFloat(color.y), + blue: CGFloat(color.z), + alpha: CGFloat(color.w)) + } + + package var simd: SIMD4 { + #if os(macOS) + let color = usingColorSpace(.deviceRGB) ?? self + return SIMD4(Float(color.redComponent), + Float(color.greenComponent), + Float(color.blueComponent), + Float(color.alphaComponent)) + #else + var red: CGFloat = 0 + var green: CGFloat = 0 + var blue: CGFloat = 0 + var alpha: CGFloat = 0 + getRed(&red, green: &green, blue: &blue, alpha: &alpha) + return SIMD4(Float(red), Float(green), Float(blue), Float(alpha)) + #endif + } +} diff --git a/Sources/VRMKit/VRM/Material.swift b/Sources/VRMKit/VRM/Material.swift index 0d46895..618f574 100644 --- a/Sources/VRMKit/VRM/Material.swift +++ b/Sources/VRMKit/VRM/Material.swift @@ -35,7 +35,7 @@ extension GLTF { public struct PbrMetallicRoughness: Codable { let _baseColorFactor: Color4? - public var baseColorFactor: Color4 { return _baseColorFactor ?? .init(r: 0, g: 0, b: 0, a: 0) } + public var baseColorFactor: Color4 { return _baseColorFactor ?? .init(r: 1, g: 1, b: 1, a: 1) } public let baseColorTexture: TextureInfo? let _metallicFactor: Float? public var metallicFactor: Float { return _metallicFactor ?? 1 } diff --git a/Sources/VRMKit/VRM/VRM+Thumbnail.swift b/Sources/VRMKit/VRM/VRM+Thumbnail.swift new file mode 100644 index 0000000..3c15137 --- /dev/null +++ b/Sources/VRMKit/VRM/VRM+Thumbnail.swift @@ -0,0 +1,47 @@ +package extension VRM { + var thumbnailImageIndex: Int { + get throws { + switch self { + case .v0(let vrm0): + return try vrm0.thumbnailImageIndex + case .v1(let vrm1): + return try vrm1.thumbnailImageIndex + } + } + } +} + +package extension VRM0 { + var thumbnailImageIndex: Int { + get throws { + guard let textureIndex = meta.texture, textureIndex >= 0 else { + throw VRMError.thumbnailNotFound + } + let textures = try gltf.jsonData.load(\.textures) + guard textures.indices.contains(textureIndex) else { + throw VRMError.thumbnailNotFound + } + let imageIndex = textures[textureIndex].source + let images = try gltf.jsonData.load(\.images) + guard images.indices.contains(imageIndex) else { + throw VRMError.thumbnailNotFound + } + return imageIndex + } + } +} + +package extension VRM1 { + var thumbnailImageIndex: Int { + get throws { + guard let imageIndex = meta.thumbnailImage, imageIndex >= 0 else { + throw VRMError.thumbnailNotFound + } + let images = try gltf.jsonData.load(\.images) + guard images.indices.contains(imageIndex) else { + throw VRMError.thumbnailNotFound + } + return imageIndex + } + } +} diff --git a/Sources/VRMKit/VRM/VRM1.swift b/Sources/VRMKit/VRM/VRM1.swift index 3824b12..7cd7d02 100644 --- a/Sources/VRMKit/VRM/VRM1.swift +++ b/Sources/VRMKit/VRM/VRM1.swift @@ -195,30 +195,30 @@ public extension VRM1 { } struct Expressions: Codable { - public let preset: Preset + public let preset: Preset? public let custom: CodableAny? public let extensions: CodableAny? public let extras: CodableAny? public struct Preset: Codable { - public let happy: Expression - public let angry: Expression - public let sad: Expression - public let relaxed: Expression - public let surprised: Expression - public let aa: Expression - public let ih: Expression - public let ou: Expression - public let ee: Expression - public let oh: Expression - public let blink: Expression - public let blinkLeft: Expression - public let blinkRight: Expression - public let lookUp: Expression - public let lookDown: Expression - public let lookLeft: Expression - public let lookRight: Expression - public let neutral: Expression + public let happy: Expression? + public let angry: Expression? + public let sad: Expression? + public let relaxed: Expression? + public let surprised: Expression? + public let aa: Expression? + public let ih: Expression? + public let ou: Expression? + public let ee: Expression? + public let oh: Expression? + public let blink: Expression? + public let blinkLeft: Expression? + public let blinkRight: Expression? + public let lookUp: Expression? + public let lookDown: Expression? + public let lookLeft: Expression? + public let lookRight: Expression? + public let neutral: Expression? } public struct Expression: Codable { diff --git a/Sources/VRMKit/VRM/VRMMigration.swift b/Sources/VRMKit/VRM/VRMMigration.swift index 48dd9d6..80a9715 100644 --- a/Sources/VRMKit/VRM/VRMMigration.swift +++ b/Sources/VRMKit/VRM/VRMMigration.swift @@ -68,7 +68,8 @@ public extension VRM0.BlendShapeMaster { var groups: [BlendShapeGroup] = [] let decoder = DictionaryDecoder() - func addGroup(name: String, presetName: String, expression: VRM1.Expressions.Expression) { + func addGroup(name: String, presetName: String, expression: VRM1.Expressions.Expression?) { + guard let expression else { return } let binds: [VRM0.BlendShapeMaster.BlendShapeGroup.Bind] = (expression.morphTargetBinds?.compactMap { (bind) -> VRM0.BlendShapeMaster.BlendShapeGroup.Bind? in let meshIndex: Int? if let nodes = gltf.jsonData.nodes, @@ -134,27 +135,27 @@ public extension VRM0.BlendShapeMaster { // VRM1 preset expressions -> VRM0 BlendShapeGroup presetName mapping. let preset = expressions.preset - addGroup(name: "Happy", presetName: "joy", expression: preset.happy) - addGroup(name: "Angry", presetName: "angry", expression: preset.angry) - addGroup(name: "Sad", presetName: "sorrow", expression: preset.sad) - addGroup(name: "Relaxed", presetName: "fun", expression: preset.relaxed) - addGroup(name: "Surprised", presetName: "unknown", expression: preset.surprised) // VRM0 doesn't have surprised + addGroup(name: "Happy", presetName: "joy", expression: preset?.happy) + addGroup(name: "Angry", presetName: "angry", expression: preset?.angry) + addGroup(name: "Sad", presetName: "sorrow", expression: preset?.sad) + addGroup(name: "Relaxed", presetName: "fun", expression: preset?.relaxed) + addGroup(name: "Surprised", presetName: "unknown", expression: preset?.surprised) // VRM0 doesn't have surprised // VRM0 presets: neutral, a, i, u, e, o, blink, joy, angry, sorrow, fun, lookup, lookdown, lookleft, lookright, blink_l, blink_r. - addGroup(name: "A", presetName: "a", expression: preset.aa) - addGroup(name: "I", presetName: "i", expression: preset.ih) - addGroup(name: "U", presetName: "u", expression: preset.ou) - addGroup(name: "E", presetName: "e", expression: preset.ee) - addGroup(name: "O", presetName: "o", expression: preset.oh) - addGroup(name: "Blink", presetName: "blink", expression: preset.blink) - addGroup(name: "Blink_L", presetName: "blink_l", expression: preset.blinkLeft) - addGroup(name: "Blink_R", presetName: "blink_r", expression: preset.blinkRight) - - addGroup(name: "LookUp", presetName: "lookup", expression: preset.lookUp) - addGroup(name: "LookDown", presetName: "lookdown", expression: preset.lookDown) - addGroup(name: "LookLeft", presetName: "lookleft", expression: preset.lookLeft) - addGroup(name: "LookRight", presetName: "lookright", expression: preset.lookRight) - addGroup(name: "Neutral", presetName: "neutral", expression: preset.neutral) + addGroup(name: "A", presetName: "a", expression: preset?.aa) + addGroup(name: "I", presetName: "i", expression: preset?.ih) + addGroup(name: "U", presetName: "u", expression: preset?.ou) + addGroup(name: "E", presetName: "e", expression: preset?.ee) + addGroup(name: "O", presetName: "o", expression: preset?.oh) + addGroup(name: "Blink", presetName: "blink", expression: preset?.blink) + addGroup(name: "Blink_L", presetName: "blink_l", expression: preset?.blinkLeft) + addGroup(name: "Blink_R", presetName: "blink_r", expression: preset?.blinkRight) + + addGroup(name: "LookUp", presetName: "lookup", expression: preset?.lookUp) + addGroup(name: "LookDown", presetName: "lookdown", expression: preset?.lookDown) + addGroup(name: "LookLeft", presetName: "lookleft", expression: preset?.lookLeft) + addGroup(name: "LookRight", presetName: "lookright", expression: preset?.lookRight) + addGroup(name: "Neutral", presetName: "neutral", expression: preset?.neutral) // VRM1 custom expressions if let customMap = expressions.custom?.value as? [String: Any] { diff --git a/Sources/VRMKit/VRMLoader.swift b/Sources/VRMKit/VRMLoader.swift index 6f41c54..aff32a3 100644 --- a/Sources/VRMKit/VRMLoader.swift +++ b/Sources/VRMKit/VRMLoader.swift @@ -23,28 +23,28 @@ open class VRMLoader { } open func loadThumbnail(from vrm: VRM) throws -> VRMImage { - guard let textureIndex = vrm.meta.texture, textureIndex >= 0 else { - throw VRMError.thumbnailNotFound + switch vrm { + case .v0(let vrm0): + return try loadThumbnail(from: vrm0) + case .v1(let vrm1): + return try loadThumbnail(from: vrm1) } - return try loadImage(from: vrm.gltf, at: textureIndex) } open func loadThumbnail(from vrm0: VRM0) throws -> VRMImage { - guard let textureIndex = vrm0.meta.texture, textureIndex >= 0 else { - throw VRMError.thumbnailNotFound - } - return try loadImage(from: vrm0.gltf, at: textureIndex) + try loadImage(from: vrm0.gltf, at: vrm0.thumbnailImageIndex) } open func loadThumbnail(from vrm1: VRM1) throws -> VRMImage { - guard let imageIndex = vrm1.meta.thumbnailImage, imageIndex >= 0 else { - throw VRMError.thumbnailNotFound - } - return try loadImage(from: vrm1.gltf, at: imageIndex) + try loadImage(from: vrm1.gltf, at: vrm1.thumbnailImageIndex) } private func loadImage(from gltf: BinaryGLTF, at index: Int, relativeTo rootDirectory: URL? = nil) throws -> VRMImage { - let gltfImage = try gltf.jsonData.load(\.images)[index] + let images = try gltf.jsonData.load(\.images) + guard images.indices.contains(index) else { + throw VRMError.thumbnailNotFound + } + let gltfImage = images[index] let imageData: Data if let uri = gltfImage.uri { imageData = try Data(gltfUrlString: uri, relativeTo: rootDirectory) diff --git a/Sources/VRMKitRuntime/BlendShapeBindings.swift b/Sources/VRMKitRuntime/BlendShapeBindings.swift index 8c9ea1f..cd988f8 100644 --- a/Sources/VRMKitRuntime/BlendShapeBindings.swift +++ b/Sources/VRMKitRuntime/BlendShapeBindings.swift @@ -1,5 +1,6 @@ import simd +/// Morph target binding shared by VRM 0.x BlendShape and VRM 1.0 Expression runtime clips. package struct BlendShapeBinding { package let mesh: Mesh package let index: Int @@ -12,6 +13,7 @@ package struct BlendShapeBinding { } } +/// Runtime clip for VRM 0.x BlendShape groups. package struct BlendShapeClip { package let name: String package let preset: BlendShapePreset @@ -33,6 +35,29 @@ package struct BlendShapeClip { } } +/// Runtime clip for VRM 1.0 Expressions. +package struct ExpressionClip { + package let name: String + package let preset: ExpressionPreset? + package let values: [BlendShapeBinding] + package let isBinary: Bool + + package var key: ExpressionKey { + return preset.map(ExpressionKey.preset) ?? .custom(name) + } + + package init(name: String, + preset: ExpressionPreset?, + values: [BlendShapeBinding], + isBinary: Bool) { + self.name = name + self.preset = preset + self.values = values + self.isBinary = isBinary + } +} + +/// Material value binding used by VRM 0.x BlendShape material values. package struct MaterialValueBinding { package let materialName: String package let valueName: String diff --git a/Sources/VRMKitRuntime/BlendShapeTypes.swift b/Sources/VRMKitRuntime/BlendShapeTypes.swift index ec775c5..22e567f 100644 --- a/Sources/VRMKitRuntime/BlendShapeTypes.swift +++ b/Sources/VRMKitRuntime/BlendShapeTypes.swift @@ -1,3 +1,7 @@ +/// VRM 0.x BlendShape key. +/// +/// VRM 1.0 expressions should use `ExpressionKey`; this type is kept for +/// VRM 0.x models and source compatibility with the older public API. public enum BlendShapeKey: Hashable { case preset(BlendShapePreset) case custom(String) @@ -10,7 +14,7 @@ public enum BlendShapeKey: Hashable { } } -/// VRM 0.x Blend Shape Preset +/// VRM 0.x Blend Shape Preset. public enum BlendShapePreset: String { case unknown case neutral @@ -35,3 +39,124 @@ public enum BlendShapePreset: String { self = BlendShapePreset(rawValue: name.lowercased()) ?? .unknown } } + +/// VRM 1.0 Expression Preset. +public enum ExpressionPreset: String { + case neutral + case happy + case angry + case sad + case relaxed + case surprised + case aa + case ih + case ou + case ee + case oh + case blink + case blinkLeft + case blinkRight + case lookUp + case lookDown + case lookLeft + case lookRight + + public init?(name: String) { + self.init(rawValue: name) + } +} + +/// VRM 1.0 Expression key. +/// +/// Use this with `setExpression(value:for:)` / `expression(for:)` when working +/// with native VRM 1.0 expression presets or custom expressions. +public enum ExpressionKey: Hashable { + case preset(ExpressionPreset) + case custom(String) + + public var isPreset: Bool { + switch self { + case .preset: return true + case .custom: return false + } + } +} + +package extension BlendShapePreset { + /// Compatibility bridge from VRM 0.x blend shape presets to VRM 1.0 expressions. + var expressionPreset: ExpressionPreset? { + switch self { + case .neutral: return .neutral + case .a: return .aa + case .i: return .ih + case .u: return .ou + case .e: return .ee + case .o: return .oh + case .blink: return .blink + case .joy: return .happy + case .angry: return .angry + case .sorrow: return .sad + case .fun: return .relaxed + case .lookUp: return .lookUp + case .lookDown: return .lookDown + case .lookLeft: return .lookLeft + case .lookRight: return .lookRight + case .blinkL: return .blinkLeft + case .blinkR: return .blinkRight + case .unknown: return nil + } + } +} + +package extension BlendShapeKey { + /// Compatibility bridge for older `setBlendShape` calls on VRM 1.0 models. + var expressionKey: ExpressionKey? { + switch self { + case .preset(let preset): + return preset.expressionPreset.map(ExpressionKey.preset) + case .custom(let name): + if let preset = ExpressionPreset(name: name) { + return .preset(preset) + } + return .custom(name) + } + } +} + +package extension ExpressionPreset { + /// Compatibility bridge from VRM 1.0 expression presets to legacy VRM 0.x names. + var legacyBlendShapePreset: BlendShapePreset? { + switch self { + case .neutral: return .neutral + case .aa: return .a + case .ih: return .i + case .ou: return .u + case .ee: return .e + case .oh: return .o + case .blink: return .blink + case .happy: return .joy + case .angry: return .angry + case .sad: return .sorrow + case .relaxed: return .fun + case .lookUp: return .lookUp + case .lookDown: return .lookDown + case .lookLeft: return .lookLeft + case .lookRight: return .lookRight + case .blinkLeft: return .blinkL + case .blinkRight: return .blinkR + case .surprised: return nil + } + } +} + +package extension ExpressionKey { + /// Compatibility bridge for code paths that still expose VRM 0.x blend shape keys. + var legacyBlendShapeKey: BlendShapeKey? { + switch self { + case .preset(let preset): + return preset.legacyBlendShapePreset.map(BlendShapeKey.preset) + case .custom(let name): + return .custom(name) + } + } +} diff --git a/Sources/VRMKitRuntime/Extensions/SIMD+.swift b/Sources/VRMKitRuntime/Extensions/SIMD+.swift index caee6e1..e19644c 100644 --- a/Sources/VRMKitRuntime/Extensions/SIMD+.swift +++ b/Sources/VRMKitRuntime/Extensions/SIMD+.swift @@ -1,6 +1,16 @@ import simd package extension SIMD3 where Scalar == Float { + init(_ values: [Double]?, `default` defaultValue: SIMD3) { + self.init(Float(values?[safe: 0] ?? Double(defaultValue.x)), + Float(values?[safe: 1] ?? Double(defaultValue.y)), + Float(values?[safe: 2] ?? Double(defaultValue.z))) + } + + init(_ values: [Double], `default` defaultValue: SIMD3) { + self.init(Optional(values), default: defaultValue) + } + var normalized: SIMD3 { simd_normalize(self) } @@ -18,6 +28,22 @@ package extension SIMD3 where Scalar == Float { } } +package extension SIMD4 where Scalar == Float { + init(_ values: [Double], `default` defaultAlpha: Float) { + self.init(Float(values[safe: 0] ?? 0), + Float(values[safe: 1] ?? 0), + Float(values[safe: 2] ?? 0), + Float(values[safe: 3] ?? Double(defaultAlpha))) + } +} + +package extension SIMD2 where Scalar == Float { + init(_ values: [Double]?, `default` defaultValue: Float) { + self.init(Float(values?[safe: 0] ?? Double(defaultValue)), + Float(values?[safe: 1] ?? Double(defaultValue))) + } +} + package extension simd_quatf { static func * (_ left: simd_quatf, _ right: SIMD3) -> SIMD3 { simd_act(left, right) diff --git a/Sources/VRMKitRuntime/FirstPersonRenderMode.swift b/Sources/VRMKitRuntime/FirstPersonRenderMode.swift new file mode 100644 index 0000000..dc0e7b7 --- /dev/null +++ b/Sources/VRMKitRuntime/FirstPersonRenderMode.swift @@ -0,0 +1,49 @@ +import VRMKit + +public enum FirstPersonRenderMode { + case firstPerson + case thirdPerson +} + +package enum FirstPersonAnnotationType { + case auto + case both + case thirdPersonOnly + case firstPersonOnly + + package init(vrm1Type: VRM1.FirstPerson.FirstPersonType) { + switch vrm1Type { + case .auto: self = .auto + case .both: self = .both + case .thirdPersonOnly: self = .thirdPersonOnly + case .firstPersonOnly: self = .firstPersonOnly + } + } + + package init?(vrm0Flag: String) { + switch vrm0Flag.lowercased() { + case "auto": + self = .auto + case "both": + self = .both + case "thirdpersononly": + self = .thirdPersonOnly + case "firstpersononly": + self = .firstPersonOnly + default: + return nil + } + } + + package func isHidden(in mode: FirstPersonRenderMode, hidesAutoInFirstPerson: Bool) -> Bool { + switch (self, mode) { + case (.auto, .firstPerson): + return hidesAutoInFirstPerson + case (.firstPersonOnly, .thirdPerson), + (.thirdPersonOnly, .firstPerson): + return true + default: + return false + } + } +} diff --git a/Sources/VRMKitRuntime/Humanoid.swift b/Sources/VRMKitRuntime/Humanoid.swift index fe1cf57..4b1f4d8 100644 --- a/Sources/VRMKitRuntime/Humanoid.swift +++ b/Sources/VRMKitRuntime/Humanoid.swift @@ -14,6 +14,73 @@ public final class Humanoid { } } + package func setUp(humanoid: VRM1.Humanoid, nodes: [Node?]) { + let humanBones = humanoid.humanBones + let mappings: [(Bones, VRM1.Humanoid.HumanBones.HumanBone?)] = [ + (.hips, humanBones.hips), + (.spine, humanBones.spine), + (.chest, humanBones.chest), + (.upperChest, humanBones.upperChest), + (.neck, humanBones.neck), + (.head, humanBones.head), + (.leftEye, humanBones.leftEye), + (.rightEye, humanBones.rightEye), + (.jaw, humanBones.jaw), + (.leftUpperLeg, humanBones.leftUpperLeg), + (.leftLowerLeg, humanBones.leftLowerLeg), + (.leftFoot, humanBones.leftFoot), + (.leftToes, humanBones.leftToes), + (.rightUpperLeg, humanBones.rightUpperLeg), + (.rightLowerLeg, humanBones.rightLowerLeg), + (.rightFoot, humanBones.rightFoot), + (.rightToes, humanBones.rightToes), + (.leftShoulder, humanBones.leftShoulder), + (.leftUpperArm, humanBones.leftUpperArm), + (.leftLowerArm, humanBones.leftLowerArm), + (.leftHand, humanBones.leftHand), + (.rightShoulder, humanBones.rightShoulder), + (.rightUpperArm, humanBones.rightUpperArm), + (.rightLowerArm, humanBones.rightLowerArm), + (.rightHand, humanBones.rightHand), + (.leftThumbMetacarpal, humanBones.leftThumbMetacarpal), + (.leftThumbProximal, humanBones.leftThumbProximal), + (.leftThumbDistal, humanBones.leftThumbDistal), + (.leftIndexProximal, humanBones.leftIndexProximal), + (.leftIndexIntermediate, humanBones.leftIndexIntermediate), + (.leftIndexDistal, humanBones.leftIndexDistal), + (.leftMiddleProximal, humanBones.leftMiddleProximal), + (.leftMiddleIntermediate, humanBones.leftMiddleIntermediate), + (.leftMiddleDistal, humanBones.leftMiddleDistal), + (.leftRingProximal, humanBones.leftRingProximal), + (.leftRingIntermediate, humanBones.leftRingIntermediate), + (.leftRingDistal, humanBones.leftRingDistal), + (.leftLittleProximal, humanBones.leftLittleProximal), + (.leftLittleIntermediate, humanBones.leftLittleIntermediate), + (.leftLittleDistal, humanBones.leftLittleDistal), + (.rightThumbMetacarpal, humanBones.rightThumbMetacarpal), + (.rightThumbProximal, humanBones.rightThumbProximal), + (.rightThumbDistal, humanBones.rightThumbDistal), + (.rightIndexProximal, humanBones.rightIndexProximal), + (.rightIndexIntermediate, humanBones.rightIndexIntermediate), + (.rightIndexDistal, humanBones.rightIndexDistal), + (.rightMiddleProximal, humanBones.rightMiddleProximal), + (.rightMiddleIntermediate, humanBones.rightMiddleIntermediate), + (.rightMiddleDistal, humanBones.rightMiddleDistal), + (.rightRingProximal, humanBones.rightRingProximal), + (.rightRingIntermediate, humanBones.rightRingIntermediate), + (.rightRingDistal, humanBones.rightRingDistal), + (.rightLittleProximal, humanBones.rightLittleProximal), + (.rightLittleIntermediate, humanBones.rightLittleIntermediate), + (.rightLittleDistal, humanBones.rightLittleDistal) + ] + bones = mappings.reduce(into: [:]) { result, mapping in + guard let humanBone = mapping.1, + nodes.indices.contains(humanBone.node), + let node = nodes[humanBone.node] else { return } + result[mapping.0] = node + } + } + public func node(for bone: Bones) -> Node? { return bones[bone] } @@ -27,6 +94,7 @@ public final class Humanoid { case leftFoot case rightFoot case spine + case chest case neck case head case leftShoulder @@ -43,6 +111,9 @@ public final class Humanoid { case rightEye case jaw case leftThumbProximal + /// VRM 1.0 + case leftThumbMetacarpal + /// VRM 0.x case leftThumbIntermediate case leftThumbDistal case leftIndexProximal @@ -58,6 +129,9 @@ public final class Humanoid { case leftLittleIntermediate case leftLittleDistal case rightThumbProximal + /// VRM 1.0 + case rightThumbMetacarpal + /// VRM 0.x case rightThumbIntermediate case rightThumbDistal case rightIndexProximal diff --git a/Sources/VRMKitRuntime/VRM1Expressions+Runtime.swift b/Sources/VRMKitRuntime/VRM1Expressions+Runtime.swift new file mode 100644 index 0000000..557c86a --- /dev/null +++ b/Sources/VRMKitRuntime/VRM1Expressions+Runtime.swift @@ -0,0 +1,44 @@ +import VRMKit + +package extension VRM1.Expressions { + var runtimeClips: [(name: String, preset: ExpressionPreset?, expression: VRM1.Expressions.Expression)] { + let presetClips: [(ExpressionPreset, VRM1.Expressions.Expression?)] = [ + (.happy, preset?.happy), + (.angry, preset?.angry), + (.sad, preset?.sad), + (.relaxed, preset?.relaxed), + (.surprised, preset?.surprised), + (.aa, preset?.aa), + (.ih, preset?.ih), + (.ou, preset?.ou), + (.ee, preset?.ee), + (.oh, preset?.oh), + (.blink, preset?.blink), + (.blinkLeft, preset?.blinkLeft), + (.blinkRight, preset?.blinkRight), + (.lookUp, preset?.lookUp), + (.lookDown, preset?.lookDown), + (.lookLeft, preset?.lookLeft), + (.lookRight, preset?.lookRight), + (.neutral, preset?.neutral) + ] + var clips: [(String, ExpressionPreset?, VRM1.Expressions.Expression)] = presetClips.compactMap { expressionPreset, expression in + guard let expression else { return nil } + return (expressionPreset.rawValue, expressionPreset, expression) + } + + guard let customMap = custom?.value as? [String: Any] else { + return clips + } + + let decoder = DictionaryDecoder() + for name in customMap.keys.sorted() { + guard let raw = customMap[name] as? [String: Any], + let expression = try? decoder.decode(VRM1.Expressions.Expression.self, from: raw) else { + continue + } + clips.append((name, nil, expression)) + } + return clips + } +} diff --git a/Sources/VRMKitRuntime/VRM1NodeConstraintRuntime.swift b/Sources/VRMKitRuntime/VRM1NodeConstraintRuntime.swift new file mode 100644 index 0000000..64e37ef --- /dev/null +++ b/Sources/VRMKitRuntime/VRM1NodeConstraintRuntime.swift @@ -0,0 +1,165 @@ +import simd +import VRMKit + +package enum VRMNodeConstraintDescriptor { + case roll(source: Int, axis: SIMD3, weight: Float) + case aim(source: Int, axis: SIMD3, weight: Float) + case rotation(source: Int, weight: Float) + + package init?(_ constraint: GLTF.Node.NodeExtensions.NodeConstraint.Constraint) { + if let roll = constraint.roll { + self = .roll(source: roll.source, + axis: roll.rollAxis.vector, + weight: Float(roll.weight ?? 1.0)) + } else if let aim = constraint.aim { + self = .aim(source: aim.source, + axis: aim.aimAxis.vector, + weight: Float(aim.weight ?? 1.0)) + } else if let rotation = constraint.rotation { + self = .rotation(source: rotation.source, + weight: Float(rotation.weight ?? 1.0)) + } else { + return nil + } + } + + package var source: Int { + switch self { + case .roll(let source, _, _), + .aim(let source, _, _), + .rotation(let source, _): + return source + } + } +} + +package enum VRMNodeConstraintRuntime { + package static func evaluate(_ descriptor: VRMNodeConstraintDescriptor, + sourceRestRotation: simd_quatf, + sourceLocalRotation: simd_quatf, + sourceWorldPosition: SIMD3, + destinationRestRotation: simd_quatf, + destinationParentWorldRotation: simd_quatf, + destinationWorldPosition: SIMD3) -> simd_quatf { + switch descriptor { + case .roll(_, let axis, let weight): + return evaluateRoll(axis: axis, + weight: weight, + sourceRestRotation: sourceRestRotation, + sourceLocalRotation: sourceLocalRotation, + destinationRestRotation: destinationRestRotation) + case .aim(_, let axis, let weight): + return evaluateAim(axis: axis, + weight: weight, + sourceWorldPosition: sourceWorldPosition, + destinationRestRotation: destinationRestRotation, + destinationParentWorldRotation: destinationParentWorldRotation, + destinationWorldPosition: destinationWorldPosition) + case .rotation(_, let weight): + return evaluateRotation(weight: weight, + sourceRestRotation: sourceRestRotation, + sourceLocalRotation: sourceLocalRotation, + destinationRestRotation: destinationRestRotation) + } + } + + private static func evaluateRoll(axis: SIMD3, + weight: Float, + sourceRestRotation: simd_quatf, + sourceLocalRotation: simd_quatf, + destinationRestRotation: simd_quatf) -> simd_quatf { + let deltaSource = simd_inverse(sourceRestRotation) * sourceLocalRotation + let deltaSourceInParent = sourceRestRotation * deltaSource * simd_inverse(sourceRestRotation) + let deltaSourceInDestination = simd_inverse(destinationRestRotation) * deltaSourceInParent * destinationRestRotation + + let toVector = deltaSourceInDestination * axis + let fromToRotation = Self.fromToRotation(from: axis, to: toVector) + let constrained = destinationRestRotation * simd_inverse(fromToRotation) * deltaSourceInDestination + return slerpRest(destinationRestRotation, constrained, weight: weight) + } + + private static func evaluateAim(axis: SIMD3, + weight: Float, + sourceWorldPosition: SIMD3, + destinationRestRotation: simd_quatf, + destinationParentWorldRotation: simd_quatf, + destinationWorldPosition: SIMD3) -> simd_quatf { + let fromVector = destinationParentWorldRotation * destinationRestRotation * axis + let toVector = sourceWorldPosition - destinationWorldPosition + let fromToRotation = Self.fromToRotation(from: fromVector, to: toVector) + let constrained = simd_inverse(destinationParentWorldRotation) * + fromToRotation * + destinationParentWorldRotation * + destinationRestRotation + return slerpRest(destinationRestRotation, constrained, weight: weight) + } + + private static func evaluateRotation(weight: Float, + sourceRestRotation: simd_quatf, + sourceLocalRotation: simd_quatf, + destinationRestRotation: simd_quatf) -> simd_quatf { + let deltaSource = simd_inverse(sourceRestRotation) * sourceLocalRotation + return slerpRest(destinationRestRotation, + destinationRestRotation * deltaSource, + weight: weight) + } + + private static func slerpRest(_ rest: simd_quatf, + _ constrained: simd_quatf, + weight: Float) -> simd_quatf { + simd_slerp(normalized(rest), normalized(constrained), simd_clamp(weight, 0.0, 1.0)) + } + + private static func fromToRotation(from rawFrom: SIMD3, + to rawTo: SIMD3) -> simd_quatf { + guard simd_length_squared(rawFrom) > Float.ulpOfOne, + simd_length_squared(rawTo) > Float.ulpOfOne else { + return quat_identity_float + } + + let from = simd_normalize(rawFrom) + let to = simd_normalize(rawTo) + let dotValue = simd_clamp(simd_dot(from, to), -1.0, 1.0) + if dotValue > 1.0 - 0.000001 { + return quat_identity_float + } + if dotValue < -1.0 + 0.000001 { + let fallback = abs(from.x) < 0.9 ? SIMD3(1, 0, 0) : SIMD3(0, 1, 0) + let axis = simd_normalize(simd_cross(from, fallback)) + return simd_quatf(angle: .pi, axis: axis) + } + + let axis = simd_normalize(simd_cross(from, to)) + return simd_quatf(angle: acos(dotValue), axis: axis) + } + + private static func normalized(_ quaternion: simd_quatf) -> simd_quatf { + let lengthSquared = simd_dot(quaternion.vector, quaternion.vector) + guard lengthSquared > Float.ulpOfOne else { return quat_identity_float } + return simd_quatf(vector: quaternion.vector / sqrt(lengthSquared)) + } + +} + +private extension GLTF.Node.NodeExtensions.NodeConstraint.Constraint.RollConstraint.RollAxis { + var vector: SIMD3 { + switch self { + case .x: return SIMD3(1, 0, 0) + case .y: return SIMD3(0, 1, 0) + case .z: return SIMD3(0, 0, 1) + } + } +} + +private extension GLTF.Node.NodeExtensions.NodeConstraint.Constraint.AimConstraint.AimAxis { + var vector: SIMD3 { + switch self { + case .positiveX: return SIMD3(1, 0, 0) + case .negativeX: return SIMD3(-1, 0, 0) + case .positiveY: return SIMD3(0, 1, 0) + case .negativeY: return SIMD3(0, -1, 0) + case .positiveZ: return SIMD3(0, 0, 1) + case .negativeZ: return SIMD3(0, 0, -1) + } + } +} diff --git a/Sources/VRMRealityKit/CustomType/VRMEntity.swift b/Sources/VRMRealityKit/CustomType/VRMEntity.swift index 6cac717..017b33d 100644 --- a/Sources/VRMRealityKit/CustomType/VRMEntity.swift +++ b/Sources/VRMRealityKit/CustomType/VRMEntity.swift @@ -2,6 +2,7 @@ import CoreGraphics import Foundation import RealityKit +import simd import VRMKit import VRMKitRuntime @@ -13,6 +14,11 @@ struct BlendShapeNormalTangentComponent: Component { let tangentOffsets: [[SIMD3]] } +@available(iOS 18.0, macOS 15.0, visionOS 2.0, *) +struct VRMMaterialIndexComponent: Component { + let materialIndex: Int +} + @available(iOS 18.0, macOS 15.0, visionOS 2.0, *) @MainActor public final class VRMEntity { @@ -23,8 +29,14 @@ public final class VRMEntity { private let enableNormalTangentBlendShape = false var blendShapeClips: [BlendShapeKey: BlendShapeClip] = [:] + var expressionClips: [ExpressionKey: ExpressionClip] = [:] + private var materialColorClips: [ExpressionKey: [MaterialColorBinding]] = [:] + private var textureTransformClips: [ExpressionKey: [TextureTransformBinding]] = [:] + private var firstPersonAnnotations: [FirstPersonAnnotation] = [] private var skinBindings: [SkinBinding] = [] + private var modelEntitiesByMaterialIndex: [Int: [ModelEntity]] = [:] private var springBones: [VRMEntitySpringBone] = [] + private var nodeConstraints: [NodeConstraintBinding] = [] struct SkinBinding { let modelEntity: ModelEntity @@ -38,49 +50,199 @@ public final class VRMEntity { } func setUpHumanoid(nodes: [Entity?]) { - humanoid.setUp(humanoid: vrm.humanoid, nodes: nodes) + switch vrm { + case .v0: + humanoid.setUp(humanoid: vrm.humanoid, nodes: nodes) + case .v1(let vrm1): + humanoid.setUp(humanoid: vrm1.humanoid, nodes: nodes) + } } - func setUpBlendShapes(meshes: [Entity?]) { - blendShapeClips = vrm.blendShapeMaster.blendShapeGroups - .map { group in - let blendShapeBinding: [BlendShapeBinding] = group.binds? - .compactMap { - guard let mesh = meshes[$0.mesh] else { + func setUpBlendShapes(nodes: [Entity?], meshes: [Entity?], loader: VRMEntityLoader) throws { + blendShapeClips = [:] + expressionClips = [:] + materialColorClips = [:] + textureTransformClips = [:] + + switch vrm { + case .v0: + blendShapeClips = vrm.blendShapeMaster.blendShapeGroups + .map { group in + let blendShapeBinding: [BlendShapeBinding] = group.binds? + .compactMap { + guard meshes.indices.contains($0.mesh), + let mesh = meshes[$0.mesh] else { + return nil + } + return BlendShapeBinding(mesh: mesh, index: $0.index, weight: $0.weight) + } ?? [] + return BlendShapeClip(name: group.name, + preset: BlendShapePreset(name: group.presetName), + values: blendShapeBinding, + isBinary: group.isBinary) + } + .reduce(into: [:]) { result, clip in + result[clip.key] = clip + } + case .v1(let vrm1): + guard let expressions = vrm1.expressions else { return } + for expressionClip in expressions.runtimeClips { + let morphBindings: [BlendShapeBinding] = expressionClip.expression.morphTargetBinds? + .compactMap { bind in + guard nodes.indices.contains(bind.node), + let node = nodes[bind.node] else { return nil } - return BlendShapeBinding(mesh: mesh, index: $0.index, weight: $0.weight) + return BlendShapeBinding(mesh: node, index: bind.index, weight: bind.weight * 100.0) + } ?? [] + let runtimeClip = ExpressionClip(name: expressionClip.name, + preset: expressionClip.preset, + values: morphBindings, + isBinary: expressionClip.expression.isBinary ?? false) + expressionClips[runtimeClip.key] = runtimeClip + + let colorBindings: [MaterialColorBinding] = expressionClip.expression.materialColorBinds? + .compactMap { bind in + guard bind.targetValue.count >= 3 else { return nil } + guard let material = try? loader.material(withMaterialIndex: bind.material) else { return nil } + return MaterialColorBinding(materialIndex: bind.material, + type: bind.type, + targetValue: SIMD4(bind.targetValue, default: 1.0), + baseValue: material.currentColor(for: bind.type)) + } ?? [] + if !colorBindings.isEmpty { + materialColorClips[runtimeClip.key] = colorBindings + } + + let transformBindings: [TextureTransformBinding] = expressionClip.expression.textureTransformBinds? + .compactMap { bind in + guard let material = try? loader.material(withMaterialIndex: bind.material) else { return nil } + let base = material.currentTextureTransform + return TextureTransformBinding(materialIndex: bind.material, + baseScale: base.scale, + baseOffset: base.offset, + targetScale: SIMD2(bind.scale, default: 1.0), + targetOffset: SIMD2(bind.offset, default: 0.0)) } ?? [] - return BlendShapeClip(name: group.name, - preset: BlendShapePreset(name: group.presetName), - values: blendShapeBinding, - isBinary: group.isBinary) + if !transformBindings.isEmpty { + textureTransformClips[runtimeClip.key] = transformBindings + } } - .reduce(into: [:]) { result, clip in - result[clip.key] = clip + } + } + + func setUpFirstPerson(nodes: [Entity?], meshes: [Entity?]) { + switch vrm { + case .v0: + firstPersonAnnotations = vrm.firstPerson.meshAnnotations.compactMap { annotation in + guard meshes.indices.contains(annotation.mesh), + let mesh = meshes[annotation.mesh], + let type = FirstPersonAnnotationType(vrm0Flag: annotation.firstPersonFlag) else { + return nil + } + return FirstPersonAnnotation(entity: mesh, + type: type, + hidesAutoInFirstPerson: false) } + case .v1(let vrm1): + let head = humanoid.node(for: .head) + firstPersonAnnotations = vrm1.firstPerson?.meshAnnotations.compactMap { annotation in + guard nodes.indices.contains(annotation.node), + let node = nodes[annotation.node] else { + return nil + } + let type = FirstPersonAnnotationType(vrm1Type: annotation.type) + return FirstPersonAnnotation(entity: node, + type: type, + hidesAutoInFirstPerson: type == .auto && node.isSameOrDescendant(of: head)) + } ?? [] + } + setFirstPersonRenderMode(.thirdPerson) + } + + func setUpNodeConstraints(gltfNodes: [GLTF.Node], loader: VRMEntityLoader) throws { + guard case .v1 = vrm else { + nodeConstraints = [] + return + } + + var bindings: [NodeConstraintBinding] = [] + for (targetIndex, gltfNode) in gltfNodes.enumerated() { + guard let constraint = gltfNode.extensions?.nodeConstraint?.constraint, + let descriptor = VRMNodeConstraintDescriptor(constraint) else { + continue + } + let sourceIndex = descriptor.source + guard sourceIndex != targetIndex else { + throw VRMError._dataInconsistent("VRMC_node_constraint source must not be destination: \(targetIndex)") + } + guard gltfNodes.indices.contains(sourceIndex) else { + throw VRMError._dataInconsistent("VRMC_node_constraint source index is out of range: \(sourceIndex)") + } + + let target = try loader.node(withNodeIndex: targetIndex) + let source = try loader.node(withNodeIndex: sourceIndex) + bindings.append(NodeConstraintBinding(targetIndex: targetIndex, + sourceIndex: sourceIndex, + descriptor: descriptor, + target: target, + source: source)) + } + nodeConstraints = try NodeConstraintBinding.ordered(bindings) } func setUpSpringBones(loader: VRMEntityLoader) throws { var springBones: [VRMEntitySpringBone] = [] - let secondaryAnimation = vrm.secondaryAnimation - for boneGroup in secondaryAnimation.boneGroups { - guard !boneGroup.bones.isEmpty else { continue } - let rootBones: [Entity] = try boneGroup.bones.compactMap { try loader.node(withNodeIndex: $0) } - let centerNode = try? loader.node(withNodeIndex: boneGroup.center) - let colliderGroups = try secondaryAnimation.colliderGroups.map { + switch vrm { + case .v0: + let secondaryAnimation = vrm.secondaryAnimation + let allColliderGroups = try secondaryAnimation.colliderGroups.map { try VRMEntitySpringBoneColliderGroup(colliderGroup: $0, loader: loader) } - let springBone = VRMEntitySpringBone(center: centerNode, - rootBones: rootBones, - comment: boneGroup.comment, - stiffnessForce: Float(boneGroup.stiffiness), - gravityPower: Float(boneGroup.gravityPower), - gravityDir: SIMD3(Float(boneGroup.gravityDir.x), Float(boneGroup.gravityDir.y), Float(boneGroup.gravityDir.z)), - dragForce: Float(boneGroup.dragForce), - hitRadius: Float(boneGroup.hitRadius), + for boneGroup in secondaryAnimation.boneGroups { + guard !boneGroup.bones.isEmpty else { continue } + let rootBones: [Entity] = try boneGroup.bones.compactMap { try loader.node(withNodeIndex: $0) } + let centerNode = try? loader.node(withNodeIndex: boneGroup.center) + let colliderGroups = boneGroup.colliderGroups.compactMap { index in + allColliderGroups.indices.contains(index) ? allColliderGroups[index] : nil + } + let springBone = VRMEntitySpringBone(center: centerNode, + rootBones: rootBones, + comment: boneGroup.comment, + stiffnessForce: Float(boneGroup.stiffiness), + gravityPower: Float(boneGroup.gravityPower), + gravityDir: SIMD3(Float(boneGroup.gravityDir.x), Float(boneGroup.gravityDir.y), Float(boneGroup.gravityDir.z)), + dragForce: Float(boneGroup.dragForce), + hitRadius: Float(boneGroup.hitRadius), + colliderGroups: colliderGroups) + springBones.append(springBone) + } + case .v1(let vrm1): + guard let springBone = vrm1.springBone else { break } + for spring in springBone.springs ?? [] { + let jointEntities = try spring.joints.compactMap { try loader.node(withNodeIndex: $0.node) } + guard !jointEntities.isEmpty else { continue } + let centerEntity = try spring.center.map { try loader.node(withNodeIndex: $0) } + let colliderGroups = try spring.colliderGroups?.compactMap { groupIndex -> VRMEntitySpringBoneColliderGroup? in + guard let groups = springBone.colliderGroups, + groups.indices.contains(groupIndex) else { + return nil + } + return try VRMEntitySpringBoneColliderGroup(colliderGroup: groups[groupIndex], + springBone: springBone, + loader: loader) + } ?? [] + let settings = Dictionary(uniqueKeysWithValues: zip(jointEntities, spring.joints).map { entity, joint in + (ObjectIdentifier(entity), VRMEntitySpringBone.JointSetting(joint: joint)) + }) + let springBone = VRMEntitySpringBone(center: centerEntity, + rootBones: [jointEntities[0]], + comment: spring.name, + jointChain: jointEntities, + jointSettings: settings, colliderGroups: colliderGroups) - springBones.append(springBone) + springBones.append(springBone) + } } self.springBones = springBones } @@ -95,7 +257,12 @@ public final class VRMEntity { initializeSkinPose(for: binding) } + func registerMaterialBinding(modelEntity: ModelEntity, materialIndex: Int) { + modelEntitiesByMaterialIndex[materialIndex, default: []].append(modelEntity) + } + public func update(at time: TimeInterval) { + nodeConstraints.forEach { $0.apply() } updateSkinning() springBones.forEach { $0.update(deltaTime: time) } } @@ -164,6 +331,10 @@ public final class VRMEntity { } public func setBlendShape(value: CGFloat, for key: BlendShapeKey) { + if case .v1 = vrm, let expressionKey = key.expressionKey { + setExpression(value: value, for: expressionKey) + return + } guard let clip = blendShapeClips[key] else { return } let normalized = max(0.0, min(1.0, clip.isBinary ? round(value) : value)) for binding in clip.values { @@ -186,11 +357,97 @@ public final class VRMEntity { } public func blendShape(for key: BlendShapeKey) -> CGFloat { + if case .v1 = vrm, let expressionKey = key.expressionKey { + return expression(for: expressionKey) + } guard let clip = blendShapeClips[key], let binding = clip.values.first else { return 0 } return CGFloat(readBlendShapeWeight(targetIndex: binding.index, on: binding.mesh)) } + public func setExpression(value: CGFloat, for key: ExpressionKey) { + guard let clip = expressionClip(for: key) else { return } + let normalized = max(0.0, min(1.0, clip.isBinary ? round(value) : value)) + for binding in clip.values { + let weight = Float(binding.weight / 100.0) * Float(normalized) + applyBlendShapeWeight(weight, targetIndex: binding.index, on: binding.mesh) + } + for binding in materialColorClip(for: key) { + binding.apply(value: Float(normalized), on: self) + } + for binding in textureTransformClip(for: key) { + binding.apply(value: Float(normalized), on: self) + } + } + + public func expression(for key: ExpressionKey) -> CGFloat { + guard let clip = expressionClip(for: key), + let binding = clip.values.first else { return 0 } + return CGFloat(readBlendShapeWeight(targetIndex: binding.index, on: binding.mesh)) + } + + public func setFirstPersonRenderMode(_ mode: FirstPersonRenderMode) { + for annotation in firstPersonAnnotations { + annotation.entity.isEnabled = !annotation.type.isHidden(in: mode, + hidesAutoInFirstPerson: annotation.hidesAutoInFirstPerson) + } + } + + fileprivate func applyMaterialColor(_ color: SIMD4, + type: VRM1.Expressions.Expression.MaterialColorBind.MaterialColorType, + materialIndex: Int) { + guard let models = modelEntitiesByMaterialIndex[materialIndex] else { return } + let vrmColor = VRMColor(simd: color) + for modelEntity in models { + guard var component = modelEntity.components[ModelComponent.self] else { continue } + component.materials = component.materials.map { material in + material.settingColor(vrmColor, for: type) + } + modelEntity.components.set(component) + } + } + + fileprivate func applyTextureTransform(scale: SIMD2, + offset: SIMD2, + materialIndex: Int) { + guard let models = modelEntitiesByMaterialIndex[materialIndex] else { return } + let transform = MaterialParameterTypes.TextureCoordinateTransform(offset: offset, scale: scale) + for modelEntity in models { + guard var component = modelEntity.components[ModelComponent.self] else { continue } + component.materials = component.materials.map { material in + material.settingTextureTransform(transform) + } + modelEntity.components.set(component) + } + } + + private func expressionClip(for key: ExpressionKey) -> ExpressionClip? { + if let clip = expressionClips[key] { return clip } + if let legacyKey = key.legacyBlendShapeKey, + let expressionKey = legacyKey.expressionKey { + return expressionClips[expressionKey] + } + return nil + } + + private func materialColorClip(for key: ExpressionKey) -> [MaterialColorBinding] { + if let clip = materialColorClips[key] { return clip } + if let legacyKey = key.legacyBlendShapeKey, + let expressionKey = legacyKey.expressionKey { + return materialColorClips[expressionKey] ?? [] + } + return [] + } + + private func textureTransformClip(for key: ExpressionKey) -> [TextureTransformBinding] { + if let clip = textureTransformClips[key] { return clip } + if let legacyKey = key.legacyBlendShapeKey, + let expressionKey = legacyKey.expressionKey { + return textureTransformClips[expressionKey] ?? [] + } + return [] + } + private func modelEntities(in root: Entity) -> [ModelEntity] { var result: [ModelEntity] = [] var stack: [Entity] = [root] @@ -360,4 +617,209 @@ public final class VRMEntity { modelEntity.components.set(BlendShapeWeightsComponent(weightsMapping: mapping)) } } + +@available(iOS 18.0, macOS 15.0, visionOS 2.0, *) +private struct NodeConstraintBinding { + let targetIndex: Int + let sourceIndex: Int + let descriptor: VRMNodeConstraintDescriptor + let target: Entity + let source: Entity + let targetRestRotation: simd_quatf + let sourceRestRotation: simd_quatf + + @MainActor + init(targetIndex: Int, + sourceIndex: Int, + descriptor: VRMNodeConstraintDescriptor, + target: Entity, + source: Entity) { + self.targetIndex = targetIndex + self.sourceIndex = sourceIndex + self.descriptor = descriptor + self.target = target + self.source = source + self.targetRestRotation = target.utx.localRotation + self.sourceRestRotation = source.utx.localRotation + } + + @MainActor + func apply() { + target.utx.localRotation = VRMNodeConstraintRuntime.evaluate( + descriptor, + sourceRestRotation: sourceRestRotation, + sourceLocalRotation: source.utx.localRotation, + sourceWorldPosition: source.utx.position, + destinationRestRotation: targetRestRotation, + destinationParentWorldRotation: target.parent?.utx.rotation ?? quat_identity_float, + destinationWorldPosition: target.utx.position + ) + } + + static func ordered(_ bindings: [NodeConstraintBinding]) throws -> [NodeConstraintBinding] { + var byTargetIndex: [Int: NodeConstraintBinding] = [:] + for binding in bindings { + if byTargetIndex[binding.targetIndex] != nil { + throw VRMError._dataInconsistent("Multiple constraints targeting the same node \(binding.targetIndex)") + } + byTargetIndex[binding.targetIndex] = binding + } + var states: [Int: VisitState] = [:] + var result: [NodeConstraintBinding] = [] + + func visit(_ binding: NodeConstraintBinding) throws { + switch states[binding.targetIndex] { + case .done: + return + case .visiting: + throw VRMError._dataInconsistent("VRMC_node_constraint circular dependency detected at node \(binding.targetIndex)") + case .none: + break + } + + states[binding.targetIndex] = .visiting + if let dependency = byTargetIndex[binding.sourceIndex] { + try visit(dependency) + } + states[binding.targetIndex] = .done + result.append(binding) + } + + for binding in bindings { + try visit(binding) + } + return result + } + + private enum VisitState { + case visiting + case done + } +} + +@available(iOS 18.0, macOS 15.0, visionOS 2.0, *) +private struct MaterialColorBinding { + let materialIndex: Int + let type: VRM1.Expressions.Expression.MaterialColorBind.MaterialColorType + let targetValue: SIMD4 + let baseValue: SIMD4 + + @MainActor + func apply(value: Float, on entity: VRMEntity) { + entity.applyMaterialColor(baseValue + (targetValue - baseValue) * value, + type: type, + materialIndex: materialIndex) + } +} + +@available(iOS 18.0, macOS 15.0, visionOS 2.0, *) +private struct TextureTransformBinding { + let materialIndex: Int + let baseScale: SIMD2 + let baseOffset: SIMD2 + let targetScale: SIMD2 + let targetOffset: SIMD2 + + @MainActor + func apply(value: Float, on entity: VRMEntity) { + let scale = baseScale + (targetScale - baseScale) * value + let offset = baseOffset + (targetOffset - baseOffset) * value + entity.applyTextureTransform(scale: scale, + offset: offset, + materialIndex: materialIndex) + } +} + +@available(iOS 18.0, macOS 15.0, visionOS 2.0, *) +private struct FirstPersonAnnotation { + let entity: Entity + let type: FirstPersonAnnotationType + let hidesAutoInFirstPerson: Bool +} + +@available(iOS 18.0, macOS 15.0, visionOS 2.0, *) +private extension Entity { + func isSameOrDescendant(of ancestor: Entity?) -> Bool { + guard let ancestor else { return false } + var entity: Entity? = self + while let current = entity { + if current === ancestor { + return true + } + entity = current.parent + } + return false + } +} + +@available(iOS 18.0, macOS 15.0, visionOS 2.0, *) +private extension Material { + var currentTextureTransform: MaterialParameterTypes.TextureCoordinateTransform { + switch self { + case let material as UnlitMaterial: + return material.textureCoordinateTransform + case let material as PhysicallyBasedMaterial: + return material.textureCoordinateTransform + default: + return MaterialParameterTypes.TextureCoordinateTransform() + } + } + + func currentColor(for type: VRM1.Expressions.Expression.MaterialColorBind.MaterialColorType) -> SIMD4 { + switch self { + case let material as UnlitMaterial: + return material.color.tint.simd + case let material as PhysicallyBasedMaterial: + switch type { + case .color: + return material.baseColor.tint.simd + case .emissionColor: + return material.emissiveColor.color.simd + case .shadeColor: + return material.baseColor.tint.simd + case .matcapColor, .rimColor: + return material.emissiveColor.color.simd + case .outlineColor: + return material.baseColor.tint.simd + } + default: + return SIMD4(1, 1, 1, 1) + } + } + + func settingColor(_ color: VRMColor, + for type: VRM1.Expressions.Expression.MaterialColorBind.MaterialColorType) -> Material { + switch self { + case var material as UnlitMaterial: + material.color.tint = color + return material + case var material as PhysicallyBasedMaterial: + switch type { + case .color, .shadeColor, .outlineColor: + material.baseColor.tint = color + case .emissionColor: + material.emissiveColor.color = color + case .matcapColor, .rimColor: + material.emissiveColor.color = color + } + return material + default: + return self + } + } + + func settingTextureTransform(_ transform: MaterialParameterTypes.TextureCoordinateTransform) -> Material { + switch self { + case var material as UnlitMaterial: + material.textureCoordinateTransform = transform + return material + case var material as PhysicallyBasedMaterial: + material.textureCoordinateTransform = transform + return material + default: + return self + } + } +} + #endif diff --git a/Sources/VRMRealityKit/CustomType/VRMEntitySpringBone.swift b/Sources/VRMRealityKit/CustomType/VRMEntitySpringBone.swift index 844a3d0..4a827d7 100644 --- a/Sources/VRMRealityKit/CustomType/VRMEntitySpringBone.swift +++ b/Sources/VRMRealityKit/CustomType/VRMEntitySpringBone.swift @@ -7,9 +7,47 @@ import Foundation @available(iOS 18.0, macOS 15.0, visionOS 2.0, *) @MainActor final class VRMEntitySpringBone { - struct SphereCollider { - let position: SIMD3 + struct Collider { + let head: SIMD3 + let tail: SIMD3? let radius: Float + + func closestPoint(to point: SIMD3) -> SIMD3 { + guard let tail else { return head } + let segment = tail - head + let lengthSquared = segment.length_squared + guard lengthSquared > Float.ulpOfOne else { return head } + let t = max(0, min(1, simd_dot(point - head, segment) / lengthSquared)) + return head + segment * t + } + } + + struct JointSetting { + let stiffnessForce: Float + let gravityPower: Float + let gravityDir: SIMD3 + let dragForce: Float + let hitRadius: Float + + init(stiffnessForce: Float = 1.0, + gravityPower: Float = 0.0, + gravityDir: SIMD3 = .init(0, -1, 0), + dragForce: Float = 0.4, + hitRadius: Float = 0.02) { + self.stiffnessForce = stiffnessForce + self.gravityPower = gravityPower + self.gravityDir = gravityDir + self.dragForce = dragForce + self.hitRadius = hitRadius + } + + init(joint: VRM1.SpringBone.Spring.Joint) { + self.init(stiffnessForce: Float(joint.stiffness ?? 1.0), + gravityPower: Float(joint.gravityPower ?? 0.0), + gravityDir: SIMD3(joint.gravityDir, default: SIMD3(0, -1, 0)), + dragForce: Float(joint.dragForce ?? 0.5), + hitRadius: Float(joint.hitRadius ?? 0.02)) + } } public let comment: String? @@ -24,7 +62,9 @@ final class VRMEntitySpringBone { private var initialLocalRotations: [(Entity, simd_quatf)] = [] private let colliderGroups: [VRMEntitySpringBoneColliderGroup] private var verlet: [VRMEntitySpringBoneLogic] = [] - private var colliderList: [SphereCollider] = [] + private var colliderList: [Collider] = [] + private let jointChain: [Entity]? + private let jointSettings: [ObjectIdentifier: JointSetting] init(center: Entity?, rootBones: [Entity], @@ -34,6 +74,8 @@ final class VRMEntitySpringBone { gravityDir: SIMD3 = .init(0, -1, 0), dragForce: Float = 0.4, hitRadius: Float = 0.02, + jointChain: [Entity]? = nil, + jointSettings: [ObjectIdentifier: JointSetting] = [:], colliderGroups: [VRMEntitySpringBoneColliderGroup] = []) { self.center = center self.rootBones = rootBones @@ -43,6 +85,8 @@ final class VRMEntitySpringBone { self.gravityDir = gravityDir self.dragForce = dragForce self.hitRadius = hitRadius + self.jointChain = jointChain + self.jointSettings = jointSettings self.colliderGroups = colliderGroups setup() } @@ -54,11 +98,18 @@ final class VRMEntitySpringBone { initialLocalRotations = [] verlet = [] - for root in rootBones { - enumerateHierarchy(root) { node in + if let jointChain, !jointChain.isEmpty { + for node in jointChain { initialLocalRotations.append((node, node.utx.localRotation)) } - setupRecursive(center, root) + setupChain(center, jointChain) + } else { + for root in rootBones { + enumerateHierarchy(root) { node in + initialLocalRotations.append((node, node.utx.localRotation)) + } + setupRecursive(center, root) + } } } @@ -73,22 +124,18 @@ final class VRMEntitySpringBone { if parent.utx.childCount == 0 { guard let parentNode = parent.parent else { return } let delta = parent.utx.position - parentNode.utx.position - let childPosition = parent.utx.position + delta.normalized * 0.07 + let direction = delta.length_squared > Float.ulpOfOne ? delta.normalized : SIMD3(0, -1, 0) + let childPosition = parent.utx.position + direction * 0.07 let localChild = parent.utx.worldToLocalMatrix.multiplyPoint(childPosition) let logic = VRMEntitySpringBoneLogic(center: center, node: parent, localChildPosition: localChild) verlet.append(logic) } else if let firstChild = parent.children.first { - let localPosition = firstChild.utx.localPosition - let scale = firstChild.utx.lossyScale + let localChildPosition = parent.utx.worldToLocalMatrix.multiplyPoint(firstChild.utx.position) let logic = VRMEntitySpringBoneLogic(center: center, node: parent, - localChildPosition: SIMD3( - localPosition.x * scale.x, - localPosition.y * scale.y, - localPosition.z * scale.z - )) + localChildPosition: localChildPosition) verlet.append(logic) } @@ -97,6 +144,29 @@ final class VRMEntitySpringBone { } } + private func setupChain(_ center: Entity?, _ joints: [Entity]) { + for index in joints.indices { + let joint = joints[index] + let localChildPosition: SIMD3 + if joints.indices.contains(index + 1) { + localChildPosition = joint.utx.worldToLocalMatrix.multiplyPoint(joints[index + 1].utx.position) + } else if let firstChild = joint.children.first { + localChildPosition = joint.utx.worldToLocalMatrix.multiplyPoint(firstChild.utx.position) + } else if let parent = joint.parent { + let delta = joint.utx.position - parent.utx.position + let direction = delta.length_squared > Float.ulpOfOne ? delta.normalized : SIMD3(0, -1, 0) + let childPosition = joint.utx.position + direction * 0.07 + localChildPosition = joint.utx.worldToLocalMatrix.multiplyPoint(childPosition) + } else { + continue + } + let logic = VRMEntitySpringBoneLogic(center: center, + node: joint, + localChildPosition: localChildPosition) + verlet.append(logic) + } + } + func update(deltaTime: TimeInterval) { if verlet.isEmpty { if rootBones.isEmpty { @@ -108,21 +178,22 @@ final class VRMEntitySpringBone { colliderList = [] for group in colliderGroups { for collider in group.colliders { - colliderList.append(SphereCollider( - position: group.node.utx.transformPoint(collider.offset), - radius: collider.radius - )) + colliderList.append(collider.worldCollider) } } - let stiffness = stiffnessForce * Float(deltaTime) - let external = gravityDir * (gravityPower * Float(deltaTime)) - for logic in verlet { - logic.radius = hitRadius + let setting = jointSettings[ObjectIdentifier(logic.head)] ?? JointSetting(stiffnessForce: stiffnessForce, + gravityPower: gravityPower, + gravityDir: gravityDir, + dragForce: dragForce, + hitRadius: hitRadius) + let stiffness = setting.stiffnessForce * Float(deltaTime) + let external = setting.gravityDir * (setting.gravityPower * Float(deltaTime)) + logic.radius = setting.hitRadius logic.update(center: center, stiffnessForce: stiffness, - dragForce: dragForce, + dragForce: setting.dragForce, external: external, colliders: colliderList) } @@ -159,7 +230,7 @@ extension VRMEntitySpringBone { stiffnessForce: Float, dragForce: Float, external: SIMD3, - colliders: [SphereCollider]) { + colliders: [Collider]) { let currentTail: SIMD3 = center?.utx.transformPoint(self.currentTail) ?? self.currentTail let prevTail: SIMD3 = center?.utx.transformPoint(self.prevTail) ?? self.prevTail @@ -185,13 +256,15 @@ extension VRMEntitySpringBone { return simd_quatf(from: rotation * boneAxis, to: nextTail - node.utx.position) * rotation } - private func collision(_ colliders: [SphereCollider], _ nextTail: SIMD3) -> SIMD3 { + private func collision(_ colliders: [Collider], _ nextTail: SIMD3) -> SIMD3 { var nextTail = nextTail for collider in colliders { + let colliderPosition = collider.closestPoint(to: nextTail) let r = radius + collider.radius - if (nextTail - collider.position).length_squared <= (r * r) { - let normal = (nextTail - collider.position).normalized - let posFromCollider = collider.position + normal * (radius + collider.radius) + let delta = nextTail - colliderPosition + if delta.length_squared <= (r * r) { + let normal = delta.length_squared > Float.ulpOfOne ? delta.normalized : SIMD3(0, 1, 0) + let posFromCollider = colliderPosition + normal * (radius + collider.radius) nextTail = node.utx.position + (posFromCollider - node.utx.position).normalized * length } } diff --git a/Sources/VRMRealityKit/CustomType/VRMEntitySpringBoneColliderGroup.swift b/Sources/VRMRealityKit/CustomType/VRMEntitySpringBoneColliderGroup.swift index 082d8c8..2deac61 100644 --- a/Sources/VRMRealityKit/CustomType/VRMEntitySpringBoneColliderGroup.swift +++ b/Sources/VRMRealityKit/CustomType/VRMEntitySpringBoneColliderGroup.swift @@ -5,23 +5,59 @@ import VRMKit @available(iOS 18.0, macOS 15.0, visionOS 2.0, *) @MainActor final class VRMEntitySpringBoneColliderGroup { - let node: Entity - let colliders: [SphereCollider] + let colliders: [Collider] init(colliderGroup: VRM0.SecondaryAnimation.ColliderGroup, loader: VRMEntityLoader) throws { - self.node = try loader.node(withNodeIndex: colliderGroup.node) - self.colliders = colliderGroup.colliders.map(SphereCollider.init) + let node = try loader.node(withNodeIndex: colliderGroup.node) + self.colliders = colliderGroup.colliders.map { Collider(node: node, collider: $0) } } - final class SphereCollider { + init(colliderGroup: VRM1.SpringBone.ColliderGroup, + springBone: VRM1.SpringBone, + loader: VRMEntityLoader) throws { + let sourceColliders = springBone.colliders ?? [] + self.colliders = try colliderGroup.colliders.compactMap { colliderIndex in + guard sourceColliders.indices.contains(colliderIndex) else { return nil } + return try Collider(collider: sourceColliders[colliderIndex], loader: loader) + } + } + + @MainActor + final class Collider { + let node: Entity let offset: SIMD3 + let tail: SIMD3? let radius: Float - init(collider: VRM0.SecondaryAnimation.ColliderGroup.Collider) { + var worldCollider: VRMEntitySpringBone.Collider { + VRMEntitySpringBone.Collider(head: node.utx.transformPoint(offset), + tail: tail.map(node.utx.transformPoint), + radius: radius) + } + + init(node: Entity, collider: VRM0.SecondaryAnimation.ColliderGroup.Collider) { + self.node = node self.offset = SIMD3(Float(collider.offset.x), Float(collider.offset.y), Float(collider.offset.z)) + self.tail = nil self.radius = Float(collider.radius) } + + init(collider: VRM1.SpringBone.Collider, loader: VRMEntityLoader) throws { + self.node = try loader.node(withNodeIndex: collider.node) + if let sphere = collider.shape.sphere { + self.offset = SIMD3(sphere.offset, default: .zero) + self.tail = nil + self.radius = Float(sphere.radius) + } else if let capsule = collider.shape.capsule { + self.offset = SIMD3(capsule.offset, default: .zero) + self.tail = SIMD3(capsule.tail, default: .zero) + self.radius = Float(capsule.radius) + } else { + self.offset = .zero + self.tail = nil + self.radius = 0 + } + } } } #endif - diff --git a/Sources/VRMRealityKit/RuntimeExports.swift b/Sources/VRMRealityKit/RuntimeExports.swift index a640c28..acec06b 100644 --- a/Sources/VRMRealityKit/RuntimeExports.swift +++ b/Sources/VRMRealityKit/RuntimeExports.swift @@ -7,6 +7,7 @@ import RealityKit typealias BlendShapeBinding = VRMKitRuntime.BlendShapeBinding typealias BlendShapeClip = VRMKitRuntime.BlendShapeClip +typealias ExpressionClip = VRMKitRuntime.ExpressionClip typealias MaterialValueBinding = VRMKitRuntime.MaterialValueBinding // MARK: - Humanoid diff --git a/Sources/VRMRealityKit/VRMEntityLoader.swift b/Sources/VRMRealityKit/VRMEntityLoader.swift index 58f0ff9..cd362e3 100644 --- a/Sources/VRMRealityKit/VRMEntityLoader.swift +++ b/Sources/VRMRealityKit/VRMEntityLoader.swift @@ -46,20 +46,20 @@ open class VRMEntityLoader { vrmEntity.entity.addChild(try self.node(withNodeIndex: node)) } vrmEntity.setUpHumanoid(nodes: entityData.nodes) - vrmEntity.setUpBlendShapes(meshes: entityData.meshes) + try vrmEntity.setUpBlendShapes(nodes: entityData.nodes, meshes: entityData.meshes, loader: self) + vrmEntity.setUpFirstPerson(nodes: entityData.nodes, meshes: entityData.meshes) + try vrmEntity.setUpNodeConstraints(gltfNodes: try gltf.load(\.nodes), loader: self) try vrmEntity.setUpSpringBones(loader: self) - // TODO: Constraints, animations. + // TODO: animations. entityData.entities[index] = vrmEntity return vrmEntity } public func loadThumbnail() throws -> VRMImage { - guard let textureIndex = vrm.meta.texture, textureIndex >= 0 else { - throw VRMError.thumbnailNotFound - } - if let cache = try entityData.load(\.images, index: textureIndex) { return cache } - return try image(withImageIndex: textureIndex) + let imageIndex = try vrm.thumbnailImageIndex + if let cache = try entityData.load(\.images, index: imageIndex) { return cache } + return try image(withImageIndex: imageIndex) } func node(withNodeIndex index: Int) throws -> Entity { @@ -128,7 +128,9 @@ open class VRMEntityLoader { func mesh(withMeshIndex index: Int, skinIndex: Int?) throws -> Entity { if skinIndex == nil, let cache = try entityData.load(\.meshes, index: index) { - return cache.clone(recursive: true) + let clone = cache.clone(recursive: true) + registerMaterialBindings(in: clone) + return clone } let gltfMesh = try gltf.load(\.meshes)[index] @@ -163,11 +165,14 @@ open class VRMEntityLoader { if skinIndex == nil { entityData.meshes[index] = meshEntity - return meshEntity.clone(recursive: true) + let clone = meshEntity.clone(recursive: true) + registerMaterialBindings(in: clone) + return clone } if entityData.meshes.indices.contains(index), entityData.meshes[index] == nil { entityData.meshes[index] = meshEntity } + registerMaterialBindings(in: meshEntity) return meshEntity } @@ -329,6 +334,9 @@ open class VRMEntityLoader { } let modelEntity = ModelEntity(mesh: mesh, materials: [material]) + if let materialIndex = primitive.material { + modelEntity.components.set(VRMMaterialIndexComponent(materialIndex: materialIndex)) + } if hasBlendShapes { let mapping = BlendShapeWeightsMapping(meshResource: mesh) modelEntity.components.set(BlendShapeWeightsComponent(weightsMapping: mapping)) @@ -393,11 +401,16 @@ open class VRMEntityLoader { func material(withMaterialIndex index: Int) throws -> Material { if let cache = try entityData.load(\.materials, index: index) { return cache } - let gltfMaterial = try gltf.load(\.materials)[index] + let materials = try gltf.load(\.materials) + guard materials.indices.contains(index) else { + throw VRMError._dataInconsistent("Material index \(index) out of bounds") + } + let gltfMaterial = materials[index] let materialProperty: VRM0.MaterialProperty? = { - guard let name = gltfMaterial.name else { return nil } - return vrm.materialPropertyNameMap[name] + guard case .v0(let vrm0) = vrm, + let name = gltfMaterial.name else { return nil } + return vrm0.materialPropertyNameMap[name] }() let shaderName = materialProperty?.shader.lowercased() // MToon / Unlit variants are not PBR, so use UnlitMaterial for consistent rendering @@ -432,10 +445,6 @@ open class VRMEntityLoader { return .white } let factor = pbr.baseColorFactor - let hasExplicitFactor = !(factor.r == 0 && factor.g == 0 && factor.b == 0 && factor.a == 0) - if !hasExplicitFactor { - return .white - } return VRMColor(red: CGFloat(factor.r), green: CGFloat(factor.g), blue: CGFloat(factor.b), @@ -1346,6 +1355,18 @@ open class VRMEntityLoader { jointEntities: jointEntities) } + private func registerMaterialBindings(in root: Entity) { + guard let vrmEntity = currentEntity else { return } + var stack: [Entity] = [root] + while let entity = stack.popLast() { + if let modelEntity = entity as? ModelEntity, + let materialIndex = modelEntity.components[VRMMaterialIndexComponent.self]?.materialIndex { + vrmEntity.registerMaterialBinding(modelEntity: modelEntity, materialIndex: materialIndex) + } + stack.append(contentsOf: entity.children) + } + } + private func transform(from node: GLTF.Node) -> Transform { if let matrix = node._matrix { return Transform(matrix: matrix.simdMatrix) diff --git a/Sources/VRMSceneKit/CustomType/VRMNode.swift b/Sources/VRMSceneKit/CustomType/VRMNode.swift index 4cc769e..ef71e32 100644 --- a/Sources/VRMSceneKit/CustomType/VRMNode.swift +++ b/Sources/VRMSceneKit/CustomType/VRMNode.swift @@ -1,4 +1,5 @@ import SceneKit +import simd import VRMKit import VRMKitRuntime @@ -10,6 +11,11 @@ open class VRMNode: SCNNode { private var springBones: [VRMSpringBone] = [] var blendShapeClips: [BlendShapeKey: BlendShapeClip] = [:] + var expressionClips: [ExpressionKey: ExpressionClip] = [:] + private var materialColorClips: [ExpressionKey: [MaterialColorBinding]] = [:] + private var textureTransformClips: [ExpressionKey: [TextureTransformBinding]] = [:] + private var firstPersonAnnotations: [FirstPersonAnnotation] = [] + private var nodeConstraints: [NodeConstraintBinding] = [] public init(vrm: VRM) { self.vrm = vrm @@ -21,47 +27,199 @@ open class VRMNode: SCNNode { } func setUpHumanoid(nodes: [SCNNode?]) { - humanoid.setUp(humanoid: vrm.humanoid, nodes: nodes) + switch vrm { + case .v0: + humanoid.setUp(humanoid: vrm.humanoid, nodes: nodes) + case .v1(let vrm1): + humanoid.setUp(humanoid: vrm1.humanoid, nodes: nodes) + } } - func setUpBlendShapes(meshes: [SCNNode?]) { - blendShapeClips = vrm.blendShapeMaster.blendShapeGroups - .map { group in - let blendShapeBinding: [BlendShapeBinding] = group.binds? - .compactMap { - guard let mesh = meshes[$0.mesh] else { + func setUpBlendShapes(nodes: [SCNNode?], meshes: [SCNNode?], loader: VRMSceneLoader) throws { + blendShapeClips = [:] + expressionClips = [:] + materialColorClips = [:] + textureTransformClips = [:] + + switch vrm { + case .v0: + blendShapeClips = vrm.blendShapeMaster.blendShapeGroups + .map { group in + let blendShapeBinding: [BlendShapeBinding] = group.binds? + .compactMap { + guard meshes.indices.contains($0.mesh), + let mesh = meshes[$0.mesh] else { + return nil + } + return BlendShapeBinding(mesh: mesh, index: $0.index, weight: $0.weight) + } ?? [] + return BlendShapeClip(name: group.name, + preset: BlendShapePreset(name: group.presetName), + values: blendShapeBinding, + isBinary: group.isBinary) + } + .reduce(into: [:]) { result, clip in + result[clip.key] = clip + } + case .v1(let vrm1): + guard let expressions = vrm1.expressions else { return } + for expressionClip in expressions.runtimeClips { + let morphBindings: [BlendShapeBinding] = expressionClip.expression.morphTargetBinds? + .compactMap { bind in + guard nodes.indices.contains(bind.node), + let node = nodes[bind.node] else { return nil } - return BlendShapeBinding(mesh: mesh, index: $0.index, weight: $0.weight) + return BlendShapeBinding(mesh: node, index: bind.index, weight: bind.weight * 100.0) + } ?? [] + let runtimeClip = ExpressionClip(name: expressionClip.name, + preset: expressionClip.preset, + values: morphBindings, + isBinary: expressionClip.expression.isBinary ?? false) + expressionClips[runtimeClip.key] = runtimeClip + + let colorBindings: [MaterialColorBinding] = expressionClip.expression.materialColorBinds? + .compactMap { bind in + guard bind.targetValue.count >= 3 else { return nil } + guard let material = try? loader.material(withMaterialIndex: bind.material) else { return nil } + return MaterialColorBinding(material: material, + type: bind.type, + targetValue: SIMD4(bind.targetValue, default: 1.0), + baseValue: material.currentColor(for: bind.type)) } ?? [] - return BlendShapeClip(name: group.name, - preset: BlendShapePreset(name: group.presetName), - values: blendShapeBinding, - isBinary: group.isBinary) + if !colorBindings.isEmpty { + materialColorClips[runtimeClip.key] = colorBindings + } + + let transformBindings: [TextureTransformBinding] = expressionClip.expression.textureTransformBinds? + .compactMap { bind in + guard let material = try? loader.material(withMaterialIndex: bind.material) else { return nil } + let base = material.diffuse.scaleOffset + return TextureTransformBinding(material: material, + baseScale: base.scale, + baseOffset: base.offset, + targetScale: SIMD2(bind.scale, default: 1.0), + targetOffset: SIMD2(bind.offset, default: 0.0)) + } ?? [] + if !transformBindings.isEmpty { + textureTransformClips[runtimeClip.key] = transformBindings + } + } + } + } + + func setUpFirstPerson(nodes: [SCNNode?], meshes: [SCNNode?]) { + switch vrm { + case .v0: + firstPersonAnnotations = vrm.firstPerson.meshAnnotations.compactMap { annotation in + guard meshes.indices.contains(annotation.mesh), + let mesh = meshes[annotation.mesh], + let type = FirstPersonAnnotationType(vrm0Flag: annotation.firstPersonFlag) else { + return nil + } + return FirstPersonAnnotation(node: mesh, + type: type, + hidesAutoInFirstPerson: false) } - .reduce(into: [:]) { result, clip in - result[clip.key] = clip + case .v1(let vrm1): + let head = humanoid.node(for: .head) + firstPersonAnnotations = vrm1.firstPerson?.meshAnnotations.compactMap { annotation in + guard nodes.indices.contains(annotation.node), + let node = nodes[annotation.node] else { + return nil + } + let type = FirstPersonAnnotationType(vrm1Type: annotation.type) + return FirstPersonAnnotation(node: node, + type: type, + hidesAutoInFirstPerson: type == .auto && node.isSameOrDescendant(of: head)) + } ?? [] } + setFirstPersonRenderMode(.thirdPerson) + } + + func setUpNodeConstraints(gltfNodes: [GLTF.Node], loader: VRMSceneLoader) throws { + guard case .v1 = vrm else { + nodeConstraints = [] + return + } + + var bindings: [NodeConstraintBinding] = [] + for (targetIndex, gltfNode) in gltfNodes.enumerated() { + guard let constraint = gltfNode.extensions?.nodeConstraint?.constraint, + let descriptor = VRMNodeConstraintDescriptor(constraint) else { + continue + } + let sourceIndex = descriptor.source + guard sourceIndex != targetIndex else { + throw VRMError._dataInconsistent("VRMC_node_constraint source must not be destination: \(targetIndex)") + } + guard gltfNodes.indices.contains(sourceIndex) else { + throw VRMError._dataInconsistent("VRMC_node_constraint source index is out of range: \(sourceIndex)") + } + + let target = try loader.node(withNodeIndex: targetIndex) + let source = try loader.node(withNodeIndex: sourceIndex) + bindings.append(NodeConstraintBinding(targetIndex: targetIndex, + sourceIndex: sourceIndex, + descriptor: descriptor, + target: target, + source: source)) + } + nodeConstraints = try NodeConstraintBinding.ordered(bindings) } func setUpSpringBones(loader: VRMSceneLoader) throws { var springBones: [VRMSpringBone] = [] - let secondaryAnimation = vrm.secondaryAnimation - for boneGroup in secondaryAnimation.boneGroups { - guard !boneGroup.bones.isEmpty else { return } - let rootBones: [SCNNode] = try boneGroup.bones.compactMap({ try loader.node(withNodeIndex: $0) }).compactMap({ $0 }) - let centerNode = try? loader.node(withNodeIndex: boneGroup.center) - let colliderGroups = try secondaryAnimation.colliderGroups.map({ try VRMSpringBoneColliderGroup(colliderGroup: $0, loader: loader) }) - let springBone = VRMSpringBone(center: centerNode, - rootBones: rootBones, - comment: boneGroup.comment, - stiffnessForce: Float(boneGroup.stiffiness), - gravityPower: Float(boneGroup.gravityPower), - gravityDir: boneGroup.gravityDir.simd, - dragForce: Float(boneGroup.dragForce), - hitRadius: Float(boneGroup.hitRadius), - colliderGroups: colliderGroups) - springBones.append(springBone) + switch vrm { + case .v0: + let secondaryAnimation = vrm.secondaryAnimation + let allColliderGroups = try secondaryAnimation.colliderGroups.map { + try VRMSpringBoneColliderGroup(colliderGroup: $0, loader: loader) + } + for boneGroup in secondaryAnimation.boneGroups { + guard !boneGroup.bones.isEmpty else { continue } + let rootBones: [SCNNode] = try boneGroup.bones.compactMap { try loader.node(withNodeIndex: $0) } + let centerNode = try? loader.node(withNodeIndex: boneGroup.center) + let colliderGroups = boneGroup.colliderGroups.compactMap { index in + allColliderGroups.indices.contains(index) ? allColliderGroups[index] : nil + } + let springBone = VRMSpringBone(center: centerNode, + rootBones: rootBones, + comment: boneGroup.comment, + stiffnessForce: Float(boneGroup.stiffiness), + gravityPower: Float(boneGroup.gravityPower), + gravityDir: boneGroup.gravityDir.simd, + dragForce: Float(boneGroup.dragForce), + hitRadius: Float(boneGroup.hitRadius), + colliderGroups: colliderGroups) + springBones.append(springBone) + } + case .v1(let vrm1): + guard let springBone = vrm1.springBone else { break } + for spring in springBone.springs ?? [] { + let jointNodes = try spring.joints.compactMap { try loader.node(withNodeIndex: $0.node) } + guard !jointNodes.isEmpty else { continue } + let centerNode = try spring.center.map { try loader.node(withNodeIndex: $0) } + let colliderGroups = try spring.colliderGroups?.compactMap { groupIndex -> VRMSpringBoneColliderGroup? in + guard let groups = springBone.colliderGroups, + groups.indices.contains(groupIndex) else { + return nil + } + return try VRMSpringBoneColliderGroup(colliderGroup: groups[groupIndex], + springBone: springBone, + loader: loader) + } ?? [] + let settings = Dictionary(uniqueKeysWithValues: zip(jointNodes, spring.joints).map { node, joint in + (ObjectIdentifier(node), VRMSpringBone.JointSetting(joint: joint)) + }) + let springBone = VRMSpringBone(center: centerNode, + rootBones: [jointNodes[0]], + comment: spring.name, + jointChain: jointNodes, + jointSettings: settings, + colliderGroups: colliderGroups) + springBones.append(springBone) + } } self.springBones = springBones } @@ -72,33 +230,282 @@ open class VRMNode: SCNNode { /// - value: a weight of the blend shape (0.0 <= value <= 1.0) /// - key: a key of the blend shape public func setBlendShape(value: CGFloat, for key: BlendShapeKey) { + if case .v1 = vrm, let expressionKey = key.expressionKey { + setExpression(value: value, for: expressionKey) + return + } guard let clip = blendShapeClips[key] else { return } let value: CGFloat = clip.isBinary ? round(value) : value for binding in clip.values { let weight = CGFloat(binding.weight / 100.0) - for primitive in binding.mesh.childNodes { - guard let morpher = primitive.morpher else { continue } + for morpher in binding.mesh.allMorphers { morpher.setWeight(weight * value, forTargetAt: binding.index) } } } + public func setExpression(value: CGFloat, for key: ExpressionKey) { + guard let clip = expressionClip(for: key) else { return } + let value = max(0.0, min(1.0, clip.isBinary ? round(value) : value)) + for binding in clip.values { + let weight = CGFloat(binding.weight / 100.0) + for morpher in binding.mesh.allMorphers { + morpher.setWeight(weight * value, forTargetAt: binding.index) + } + } + for binding in materialColorClip(for: key) { + binding.apply(value: Float(value)) + } + for binding in textureTransformClip(for: key) { + binding.apply(value: Float(value)) + } + } + /// Get a weight of the blend shape /// /// - Parameter key: a key of the blend shape /// - Returns: a weight of the blend shape public func blendShape(for key: BlendShapeKey) -> CGFloat { + if case .v1 = vrm, let expressionKey = key.expressionKey { + return expression(for: expressionKey) + } guard let clip = blendShapeClips[key], let binding = clip.values.first, - let morpher = binding.mesh.childNodes.lazy.compactMap({ $0.morpher }).first else { return 0 } + let morpher = binding.mesh.allMorphers.first else { return 0 } return morpher.weight(forTargetAt: binding.index) } + + public func expression(for key: ExpressionKey) -> CGFloat { + guard let clip = expressionClip(for: key), + let binding = clip.values.first, + let morpher = binding.mesh.allMorphers.first else { return 0 } + return morpher.weight(forTargetAt: binding.index) + } + + public func setFirstPersonRenderMode(_ mode: FirstPersonRenderMode) { + for annotation in firstPersonAnnotations { + annotation.node.isHidden = annotation.type.isHidden(in: mode, + hidesAutoInFirstPerson: annotation.hidesAutoInFirstPerson) + } + } + + private func expressionClip(for key: ExpressionKey) -> ExpressionClip? { + if let clip = expressionClips[key] { return clip } + if let legacyKey = key.legacyBlendShapeKey, + let expressionKey = legacyKey.expressionKey { + return expressionClips[expressionKey] + } + return nil + } + + private func materialColorClip(for key: ExpressionKey) -> [MaterialColorBinding] { + if let clip = materialColorClips[key] { return clip } + if let legacyKey = key.legacyBlendShapeKey, + let expressionKey = legacyKey.expressionKey { + return materialColorClips[expressionKey] ?? [] + } + return [] + } + + private func textureTransformClip(for key: ExpressionKey) -> [TextureTransformBinding] { + if let clip = textureTransformClips[key] { return clip } + if let legacyKey = key.legacyBlendShapeKey, + let expressionKey = legacyKey.expressionKey { + return textureTransformClips[expressionKey] ?? [] + } + return [] + } } @available(*, deprecated, message: "Deprecated. Use VRMRealityKit instead.") extension VRMNode: RenderUpdatable { public func update(at time: TimeInterval) { let seconds = timer.deltaTime(updateAtTime: time) + nodeConstraints.forEach { $0.apply() } springBones.forEach({ $0.update(deltaTime: seconds) }) } } + +@available(*, deprecated, message: "Deprecated. Use VRMRealityKit instead.") +private struct NodeConstraintBinding { + let targetIndex: Int + let sourceIndex: Int + let descriptor: VRMNodeConstraintDescriptor + let target: SCNNode + let source: SCNNode + let targetRestRotation: simd_quatf + let sourceRestRotation: simd_quatf + + init(targetIndex: Int, + sourceIndex: Int, + descriptor: VRMNodeConstraintDescriptor, + target: SCNNode, + source: SCNNode) { + self.targetIndex = targetIndex + self.sourceIndex = sourceIndex + self.descriptor = descriptor + self.target = target + self.source = source + self.targetRestRotation = target.utx.localRotation + self.sourceRestRotation = source.utx.localRotation + } + + func apply() { + target.utx.localRotation = VRMNodeConstraintRuntime.evaluate( + descriptor, + sourceRestRotation: sourceRestRotation, + sourceLocalRotation: source.utx.localRotation, + sourceWorldPosition: source.utx.position, + destinationRestRotation: targetRestRotation, + destinationParentWorldRotation: target.parent?.utx.rotation ?? quat_identity_float, + destinationWorldPosition: target.utx.position + ) + } + + static func ordered(_ bindings: [NodeConstraintBinding]) throws -> [NodeConstraintBinding] { + var byTargetIndex: [Int: NodeConstraintBinding] = [:] + for binding in bindings { + if byTargetIndex[binding.targetIndex] != nil { + throw VRMError._dataInconsistent("Multiple constraints targeting the same node \(binding.targetIndex)") + } + byTargetIndex[binding.targetIndex] = binding + } + var states: [Int: VisitState] = [:] + var result: [NodeConstraintBinding] = [] + + func visit(_ binding: NodeConstraintBinding) throws { + switch states[binding.targetIndex] { + case .done: + return + case .visiting: + throw VRMError._dataInconsistent("VRMC_node_constraint circular dependency detected at node \(binding.targetIndex)") + case .none: + break + } + + states[binding.targetIndex] = .visiting + if let dependency = byTargetIndex[binding.sourceIndex] { + try visit(dependency) + } + states[binding.targetIndex] = .done + result.append(binding) + } + + for binding in bindings { + try visit(binding) + } + return result + } + + private enum VisitState { + case visiting + case done + } +} + +private struct MaterialColorBinding { + let material: SCNMaterial + let type: VRM1.Expressions.Expression.MaterialColorBind.MaterialColorType + let targetValue: SIMD4 + let baseValue: SIMD4 + + func apply(value: Float) { + material.setColor(baseValue + (targetValue - baseValue) * value, for: type) + } +} + +private struct TextureTransformBinding { + let material: SCNMaterial + let baseScale: SIMD2 + let baseOffset: SIMD2 + let targetScale: SIMD2 + let targetOffset: SIMD2 + + func apply(value: Float) { + let scale = baseScale + (targetScale - baseScale) * value + let offset = baseOffset + (targetOffset - baseOffset) * value + material.diffuse.contentsTransform = SCNMatrix4(scale: scale, offset: offset) + } +} + +private struct FirstPersonAnnotation { + let node: SCNNode + let type: FirstPersonAnnotationType + let hidesAutoInFirstPerson: Bool +} + +private extension SCNNode { + var allMorphers: [SCNMorpher] { + var result: [SCNMorpher] = [] + enumerateHierarchy { node, _ in + if let morpher = node.morpher { + result.append(morpher) + } + } + return result + } + + func isSameOrDescendant(of ancestor: SCNNode?) -> Bool { + guard let ancestor else { return false } + var node: SCNNode? = self + while let current = node { + if current === ancestor { + return true + } + node = current.parent + } + return false + } +} + +private extension SCNMaterial { + func currentColor(for type: VRM1.Expressions.Expression.MaterialColorBind.MaterialColorType) -> SIMD4 { + colorProperty(for: type).simdColor + } + + func setColor(_ color: SIMD4, for type: VRM1.Expressions.Expression.MaterialColorBind.MaterialColorType) { + colorProperty(for: type).contents = VRMColor(simd: color) + } + + private func colorProperty(for type: VRM1.Expressions.Expression.MaterialColorBind.MaterialColorType) -> SCNMaterialProperty { + switch type { + case .color: + return diffuse.contents is VRMImage ? multiply : diffuse + case .emissionColor: + return emission + case .shadeColor: + return multiply + case .matcapColor: + return reflective + case .rimColor: + return selfIllumination + case .outlineColor: + return transparent + } + } +} + +private extension SCNMaterialProperty { + var simdColor: SIMD4 { + guard let color = contents as? VRMColor else { + return SIMD4(1, 1, 1, 1) + } + return color.simd + } + + var scaleOffset: (scale: SIMD2, offset: SIMD2) { + let transform = contentsTransform + return (SIMD2(Float(transform.m11), Float(transform.m22)), + SIMD2(Float(transform.m41), Float(transform.m42))) + } +} + +private extension SCNMatrix4 { + init(scale: SIMD2, offset: SIMD2) { + self = SCNMatrix4Identity + m11 = SCNFloat(scale.x) + m22 = SCNFloat(scale.y) + m41 = SCNFloat(offset.x) + m42 = SCNFloat(offset.y) + } +} diff --git a/Sources/VRMSceneKit/CustomType/VRMSpringBone.swift b/Sources/VRMSceneKit/CustomType/VRMSpringBone.swift index 62a6dfe..290a0c4 100644 --- a/Sources/VRMSceneKit/CustomType/VRMSpringBone.swift +++ b/Sources/VRMSceneKit/CustomType/VRMSpringBone.swift @@ -6,9 +6,47 @@ import VRMKitRuntime @available(*, deprecated, message: "Deprecated. Use VRMRealityKit instead.") final class VRMSpringBone { @available(*, deprecated, message: "Deprecated. Use VRMRealityKit instead.") - struct SphereCollider { - let position: SIMD3 + struct Collider { + let head: SIMD3 + let tail: SIMD3? let radius: Float + + func closestPoint(to point: SIMD3) -> SIMD3 { + guard let tail else { return head } + let segment = tail - head + let lengthSquared = segment.length_squared + guard lengthSquared > Float.ulpOfOne else { return head } + let t = max(0, min(1, simd_dot(point - head, segment) / lengthSquared)) + return head + segment * t + } + } + + struct JointSetting { + let stiffnessForce: Float + let gravityPower: Float + let gravityDir: SIMD3 + let dragForce: Float + let hitRadius: Float + + init(stiffnessForce: Float = 1.0, + gravityPower: Float = 0.0, + gravityDir: SIMD3 = .init(0, -1, 0), + dragForce: Float = 0.4, + hitRadius: Float = 0.02) { + self.stiffnessForce = stiffnessForce + self.gravityPower = gravityPower + self.gravityDir = gravityDir + self.dragForce = dragForce + self.hitRadius = hitRadius + } + + init(joint: VRM1.SpringBone.Spring.Joint) { + self.init(stiffnessForce: Float(joint.stiffness ?? 1.0), + gravityPower: Float(joint.gravityPower ?? 0.0), + gravityDir: SIMD3(joint.gravityDir, default: SIMD3(0, -1, 0)), + dragForce: Float(joint.dragForce ?? 0.5), + hitRadius: Float(joint.hitRadius ?? 0.02)) + } } public let comment: String? @@ -23,7 +61,9 @@ final class VRMSpringBone { private var initialLocalRotationMap: [SCNNode : simd_quatf] = [:] private let colliderGroups: [VRMSpringBoneColliderGroup] private var verlet: [VRMSpringBoneLogic] = [] - private var colliderList: [SphereCollider] = [] + private var colliderList: [Collider] = [] + private let jointChain: [SCNNode]? + private let jointSettings: [ObjectIdentifier: JointSetting] private let isDrawGizmo: Bool @@ -35,6 +75,8 @@ final class VRMSpringBone { gravityDir: SIMD3 = .init(0, -1, 0), dragForce: Float = 0.4, hitRadius: Float = 0.02, + jointChain: [SCNNode]? = nil, + jointSettings: [ObjectIdentifier: JointSetting] = [:], colliderGroups: [VRMSpringBoneColliderGroup] = [], isDrawGizmo: Bool = false) { self.center = center @@ -46,6 +88,8 @@ final class VRMSpringBone { self.dragForce = dragForce self.hitRadius = hitRadius self.colliderGroups = colliderGroups + self.jointChain = jointChain + self.jointSettings = jointSettings self.isDrawGizmo = isDrawGizmo setup() } @@ -57,29 +101,54 @@ final class VRMSpringBone { self.initialLocalRotationMap = [:] self.verlet = [] - for go in self.rootBones { - go.enumerateHierarchy { (x, _) in - self.initialLocalRotationMap[x] = x.utx.localRotation + if let jointChain, !jointChain.isEmpty { + for node in jointChain { + initialLocalRotationMap[node] = node.utx.localRotation } - setupRecursive(self.center, go) + setupChain(center, jointChain) + } else { + for go in self.rootBones { + go.enumerateHierarchy { (x, _) in + self.initialLocalRotationMap[x] = x.utx.localRotation + } + setupRecursive(self.center, go) + } + } + } + + private func setupChain(_ center: SCNNode?, _ joints: [SCNNode]) { + for index in joints.indices { + let joint = joints[index] + let localChildPosition: SIMD3 + if joints.indices.contains(index + 1) { + localChildPosition = joint.utx.worldToLocalMatrix.multiplyPoint(joints[index + 1].utx.position) + } else if let firstChild = joint.childNodes.first { + localChildPosition = joint.utx.worldToLocalMatrix.multiplyPoint(firstChild.utx.position) + } else if let parent = joint.parent { + let delta = joint.utx.position - parent.utx.position + let direction = delta.length_squared > Float.ulpOfOne ? delta.normalized : SIMD3(0, -1, 0) + let childPosition = joint.utx.position + direction * 0.07 + localChildPosition = joint.utx.worldToLocalMatrix.multiplyPoint(childPosition) + } else { + continue + } + let logic = VRMSpringBoneLogic(center: center, node: joint, localChildPosition: localChildPosition) + verlet.append(logic) } } private func setupRecursive(_ center: SCNNode?, _ parent: SCNNode) { if parent.utx.childCount == 0 { - let delta = parent.utx.position - parent.parent!.utx.position - let childPosition = parent.utx.position + delta.normalized * 0.07 + guard let parentNode = parent.parent else { return } + let delta = parent.utx.position - parentNode.utx.position + let direction = delta.length_squared > Float.ulpOfOne ? delta.normalized : SIMD3(0, -1, 0) + let childPosition = parent.utx.position + direction * 0.07 let logic = VRMSpringBoneLogic(center: center, node: parent, localChildPosition: parent.utx.worldToLocalMatrix.multiplyPoint(childPosition)) self.verlet.append(logic) } else { let firstChild = parent.childNodes.first! - let localPosition = firstChild.utx.localPosition - let scale = firstChild.utx.lossyScale - let logic = VRMSpringBoneLogic(center: center, node: parent, localChildPosition: SIMD3( - localPosition.x * scale.x, - localPosition.y * scale.y, - localPosition.z * scale.z - )) + let localChildPosition = parent.utx.worldToLocalMatrix.multiplyPoint(firstChild.utx.position) + let logic = VRMSpringBoneLogic(center: center, node: parent, localChildPosition: localChildPosition) self.verlet.append(logic) } @@ -105,22 +174,23 @@ final class VRMSpringBone { self.colliderList = [] for group in self.colliderGroups { for collider in group.colliders { - self.colliderList.append(SphereCollider( - position: group.node.utx.transformPoint(collider.offset), - radius: collider.radius - )) + self.colliderList.append(collider.worldCollider) } } - let stiffness = self.stiffnessForce * Float(deltaTime) - let external = self.gravityDir * (self.gravityPower * Float(deltaTime)) - for verlet in self.verlet { - verlet.radius = self.hitRadius + let setting = jointSettings[ObjectIdentifier(verlet.head)] ?? JointSetting(stiffnessForce: stiffnessForce, + gravityPower: gravityPower, + gravityDir: gravityDir, + dragForce: dragForce, + hitRadius: hitRadius) + let stiffness = setting.stiffnessForce * Float(deltaTime) + let external = setting.gravityDir * (setting.gravityPower * Float(deltaTime)) + verlet.radius = setting.hitRadius verlet.update( center: self.center, stiffnessForce: stiffness, - dragForce: self.dragForce, + dragForce: setting.dragForce, external: external, colliders: self.colliderList) } @@ -171,7 +241,7 @@ extension VRMSpringBone { self.length = localChildPosition.length } - func update(center: SCNNode?, stiffnessForce: Float, dragForce: Float, external: SIMD3, colliders: [SphereCollider]) { + func update(center: SCNNode?, stiffnessForce: Float, dragForce: Float, external: SIMD3, colliders: [Collider]) { let currentTail: SIMD3 = center?.utx.transformPoint(self.currentTail) ?? self.currentTail let prevTail: SIMD3 = center?.utx.transformPoint(self.prevTail) ?? self.prevTail @@ -202,14 +272,16 @@ extension VRMSpringBone { return simd_quatf(from: rotation * self.boneAxis, to: nextTail - self.node.utx.position) * rotation } - private func collision(_ colliders: [SphereCollider], _ nextTail: SIMD3) -> SIMD3 { + private func collision(_ colliders: [Collider], _ nextTail: SIMD3) -> SIMD3 { var nextTail = nextTail for collider in colliders { + let colliderPosition = collider.closestPoint(to: nextTail) let r = self.radius + collider.radius - if (nextTail - collider.position).length_squared <= (r * r) { + let delta = nextTail - colliderPosition + if delta.length_squared <= (r * r) { // ヒット。Colliderの半径方向に押し出す - let normal = (nextTail - collider.position).normalized - let posFromCollider = collider.position + normal * (self.radius + collider.radius) + let normal = delta.length_squared > Float.ulpOfOne ? delta.normalized : SIMD3(0, 1, 0) + let posFromCollider = colliderPosition + normal * (self.radius + collider.radius) // 長さをboneLengthに強制 nextTail = self.node.utx.position + (posFromCollider - self.node.utx.position).normalized * self.length } diff --git a/Sources/VRMSceneKit/CustomType/VRMSpringBoneColliderGroup.swift b/Sources/VRMSceneKit/CustomType/VRMSpringBoneColliderGroup.swift index cc13b5f..e514633 100644 --- a/Sources/VRMSceneKit/CustomType/VRMSpringBoneColliderGroup.swift +++ b/Sources/VRMSceneKit/CustomType/VRMSpringBoneColliderGroup.swift @@ -4,22 +4,58 @@ import SceneKit @available(*, deprecated, message: "Deprecated. Use VRMRealityKit instead.") final class VRMSpringBoneColliderGroup { - let node: SCNNode - let colliders: [SphereCollider] + let colliders: [Collider] init(colliderGroup: VRM0.SecondaryAnimation.ColliderGroup, loader: VRMSceneLoader) throws { - self.node = try loader.node(withNodeIndex: colliderGroup.node) - self.colliders = colliderGroup.colliders.map(SphereCollider.init) + let node = try loader.node(withNodeIndex: colliderGroup.node) + self.colliders = colliderGroup.colliders.map { Collider(node: node, collider: $0) } + } + + init(colliderGroup: VRM1.SpringBone.ColliderGroup, + springBone: VRM1.SpringBone, + loader: VRMSceneLoader) throws { + let sourceColliders = springBone.colliders ?? [] + self.colliders = try colliderGroup.colliders.compactMap { colliderIndex in + guard sourceColliders.indices.contains(colliderIndex) else { return nil } + return try Collider(collider: sourceColliders[colliderIndex], loader: loader) + } } @available(*, deprecated, message: "Deprecated. Use VRMRealityKit instead.") - final class SphereCollider { + final class Collider { + let node: SCNNode let offset: SIMD3 + let tail: SIMD3? let radius: Float + + var worldCollider: VRMSpringBone.Collider { + VRMSpringBone.Collider(head: node.utx.transformPoint(offset), + tail: tail.map(node.utx.transformPoint), + radius: radius) + } - init(collider: VRM0.SecondaryAnimation.ColliderGroup.Collider) { + init(node: SCNNode, collider: VRM0.SecondaryAnimation.ColliderGroup.Collider) { + self.node = node self.offset = collider.offset.simd + self.tail = nil self.radius = Float(collider.radius) } + + init(collider: VRM1.SpringBone.Collider, loader: VRMSceneLoader) throws { + self.node = try loader.node(withNodeIndex: collider.node) + if let sphere = collider.shape.sphere { + self.offset = SIMD3(sphere.offset, default: .zero) + self.tail = nil + self.radius = Float(sphere.radius) + } else if let capsule = collider.shape.capsule { + self.offset = SIMD3(capsule.offset, default: .zero) + self.tail = SIMD3(capsule.tail, default: .zero) + self.radius = Float(capsule.radius) + } else { + self.offset = .zero + self.tail = nil + self.radius = 0 + } + } } } diff --git a/Sources/VRMSceneKit/GLTF2SCN/GLTF2SCN.swift b/Sources/VRMSceneKit/GLTF2SCN/GLTF2SCN.swift index 6874e4a..72d6cd9 100644 --- a/Sources/VRMSceneKit/GLTF2SCN/GLTF2SCN.swift +++ b/Sources/VRMSceneKit/GLTF2SCN/GLTF2SCN.swift @@ -89,3 +89,12 @@ extension GLTF.Color4 { return .init(red: CGFloat(r), green: CGFloat(g), blue: CGFloat(b), alpha: CGFloat(a)) } } + +extension SKColor { + convenience init(color3 values: [Double], alpha: CGFloat) { + self.init(red: CGFloat(values[safe: 0] ?? 0), + green: CGFloat(values[safe: 1] ?? 0), + blue: CGFloat(values[safe: 2] ?? 0), + alpha: alpha) + } +} diff --git a/Sources/VRMSceneKit/GLTF2SCN/SCNMaterial+GLTF.swift b/Sources/VRMSceneKit/GLTF2SCN/SCNMaterial+GLTF.swift index 859d52d..ff61f96 100644 --- a/Sources/VRMSceneKit/GLTF2SCN/SCNMaterial+GLTF.swift +++ b/Sources/VRMSceneKit/GLTF2SCN/SCNMaterial+GLTF.swift @@ -7,15 +7,21 @@ extension SCNMaterial { convenience init(material: GLTF.Material, loader: VRMSceneLoader) throws { self.init() name = material.name -// lightingModel = .physicallyBased - lightingModel = .constant // FIXME: - isDoubleSided = material.doubleSided - isLitPerPixel = false - writesToDepthBuffer = material.alphaMode != .BLEND + let mtoon = material.extensions?.materialsMToon + let isMToon = mtoon != nil + let isUnlit = material.extensions?.materialsUnlit != nil + let isVRM0: Bool + switch loader.vrm { + case .v0: + isVRM0 = true + case .v1: + isVRM0 = false + } var shader: VRM0.MaterialProperty.Shader? + writesToDepthBuffer = mtoon?.transparentWithZWrite == true || material.alphaMode != .BLEND - if let name = name, let property = loader.vrm.materialPropertyNameMap[name] { + if let name = name, let property = loader.vrm0MaterialProperty(named: name) { shader = property.vrmShader // FIXME/TODO: https://dwango.github.io/vrm/vrm_spec/#vrm%E3%81%8C%E6%8F%90%E4%BE%9B%E3%81%99%E3%82%8B%E3%82%B7%E3%82%A7%E3%83%BC%E3%83%80%E3%83%BC if shader == .unlitTransparent { @@ -30,12 +36,17 @@ extension SCNMaterial { blendMode = blendMode(of: material.alphaMode) } + let usesConstantLighting = isVRM0 || shader == .mToon || shader == .unlitTransparent || isMToon || isUnlit + lightingModel = usesConstantLighting ? .constant : .physicallyBased + isDoubleSided = material.doubleSided + isLitPerPixel = !usesConstantLighting + if let pbr = material.pbrMetallicRoughness { // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#metallic-roughness-material if let baseTexture = pbr.baseColorTexture { try diffuse.setTextureInfo(baseTexture, loader: loader) - if shader == .mToon { + if shader == .mToon || isMToon { multiply.contents = pbr.baseColorFactor.createSKColor() } } else { @@ -68,6 +79,43 @@ extension SCNMaterial { if let emissiveTexture = material.emissiveTexture { try emission.setTextureInfo(emissiveTexture, loader: loader) } + + if let mtoon { + applyMToon(mtoon, material: material, loader: loader) + } + } + + private func applyMToon(_ mtoon: GLTF.Material.MaterialExtensions.MaterialsMToon, + material: GLTF.Material, + loader: VRMSceneLoader) { + if let shadeColor = mtoon.shadeColorFactor { + multiply.contents = SKColor(color3: shadeColor, alpha: 1.0) + } + if let shadeTexture = mtoon.shadeMultiplyTexture { + try? multiply.setMToonTextureInfo(shadeTexture, loader: loader) + } + if let matcapTexture = mtoon.matcapTexture { + try? reflective.setMToonTextureInfo(matcapTexture, loader: loader) + } + if let rimColor = mtoon.parametricRimColorFactor { + selfIllumination.contents = SKColor(color3: rimColor, alpha: 1.0) + selfIllumination.intensity = CGFloat(mtoon.parametricRimLiftFactor ?? 0) + } + if let rimTexture = mtoon.rimMultiplyTexture { + try? selfIllumination.setMToonTextureInfo(rimTexture, loader: loader) + } + if let outlineColor = mtoon.outlineColorFactor { + transparent.contents = SKColor(color3: outlineColor, alpha: 1.0) + } + if let uvMask = mtoon.uvAnimationMaskTexture { + try? ambient.setMToonTextureInfo(uvMask, loader: loader) + } + if material.alphaMode == .BLEND || mtoon.transparentWithZWrite == true { + blendMode = .alpha + } + if material.alphaMode == .MASK { + transparencyMode = .aOne + } } private func createMetallicRoughnessTexture(from uiImage: VRMImage) throws -> (metal: VRMImage, rough: VRMImage) { diff --git a/Sources/VRMSceneKit/GLTF2SCN/SCNMaterialProperty+GLTF.swift b/Sources/VRMSceneKit/GLTF2SCN/SCNMaterialProperty+GLTF.swift index 302e8db..5f4dee0 100644 --- a/Sources/VRMSceneKit/GLTF2SCN/SCNMaterialProperty+GLTF.swift +++ b/Sources/VRMSceneKit/GLTF2SCN/SCNMaterialProperty+GLTF.swift @@ -30,6 +30,20 @@ extension SCNMaterialProperty { mappingChannel = textureInfo.texCoord } + func setMToonTextureInfo(_ textureInfo: GLTF.Material.MaterialExtensions.MaterialsMToon.MaterialsMToonTextureInfo, + loader: VRMSceneLoader) throws { + let texture = try loader.texture(withTextureIndex: textureInfo.index) + contents = texture.contents + magnificationFilter = texture.magnificationFilter + minificationFilter = texture.minificationFilter + mipFilter = texture.mipFilter + wrapS = texture.wrapS + wrapT = texture.wrapT + intensity = texture.intensity + + mappingChannel = textureInfo.texCoord ?? 0 + } + private func filterMode(of filter: GLTF.Sampler.MagFilter) -> SCNFilterMode { switch filter { case .NEAREST: return .nearest diff --git a/Sources/VRMSceneKit/GLTF2SCN/SCNNode+GLTF.swift b/Sources/VRMSceneKit/GLTF2SCN/SCNNode+GLTF.swift index 27fa559..c91ed43 100644 --- a/Sources/VRMSceneKit/GLTF2SCN/SCNNode+GLTF.swift +++ b/Sources/VRMSceneKit/GLTF2SCN/SCNNode+GLTF.swift @@ -73,11 +73,10 @@ extension SCNNode { node.geometry = geometry // FIXME/TODO: - if let name = geometry.materials[0].name, - let property = loader.vrm.materialPropertyNameMap[name], - property.renderQueue != -1 { + if let renderQueue = try loader.renderQueue(forMaterialNamed: geometry.materials[0].name), + renderQueue != -1 { let lastRenderingOrder = childNodes.last?.renderingOrder ?? 0 - node.renderingOrder = lastRenderingOrder == 0 ? property.renderQueue : property.renderQueue + 1 + node.renderingOrder = lastRenderingOrder == 0 ? renderQueue : renderQueue + 1 } } diff --git a/Sources/VRMSceneKit/RuntimeExports.swift b/Sources/VRMSceneKit/RuntimeExports.swift index fe1f6d7..11d968c 100644 --- a/Sources/VRMSceneKit/RuntimeExports.swift +++ b/Sources/VRMSceneKit/RuntimeExports.swift @@ -8,6 +8,8 @@ typealias BlendShapeBinding = VRMKitRuntime.BlendShapeBinding @available(*, deprecated, message: "Deprecated. Use VRMRealityKit instead.") typealias BlendShapeClip = VRMKitRuntime.BlendShapeClip @available(*, deprecated, message: "Deprecated. Use VRMRealityKit instead.") +typealias ExpressionClip = VRMKitRuntime.ExpressionClip +@available(*, deprecated, message: "Deprecated. Use VRMRealityKit instead.") typealias MaterialValueBinding = VRMKitRuntime.MaterialValueBinding // MARK: - Humanoid diff --git a/Sources/VRMSceneKit/VRMSceneLoader.swift b/Sources/VRMSceneKit/VRMSceneLoader.swift index 026a1c4..8840942 100644 --- a/Sources/VRMSceneKit/VRMSceneLoader.swift +++ b/Sources/VRMSceneKit/VRMSceneLoader.swift @@ -32,7 +32,9 @@ open class VRMSceneLoader { vrmNode.addChildNode(try self.node(withNodeIndex: node)) } vrmNode.setUpHumanoid(nodes: sceneData.nodes) - vrmNode.setUpBlendShapes(meshes: sceneData.meshes) + try vrmNode.setUpBlendShapes(nodes: sceneData.nodes, meshes: sceneData.meshes, loader: self) + vrmNode.setUpFirstPerson(nodes: sceneData.nodes, meshes: sceneData.meshes) + try vrmNode.setUpNodeConstraints(gltfNodes: try gltf.load(\.nodes), loader: self) try vrmNode.setUpSpringBones(loader: self) let scnScene = VRMScene(node: vrmNode) @@ -41,11 +43,9 @@ open class VRMSceneLoader { } public func loadThumbnail() throws -> VRMImage { - guard let textureIndex = vrm.meta.texture, textureIndex >= 0 else { - throw VRMError.thumbnailNotFound - } - if let cache = try sceneData.load(\.images, index: textureIndex) { return cache } - return try image(withImageIndex: textureIndex) + let imageIndex = try vrm.thumbnailImageIndex + if let cache = try sceneData.load(\.images, index: imageIndex) { return cache } + return try image(withImageIndex: imageIndex) } func node(withNodeIndex index: Int) throws -> SCNNode { @@ -122,12 +122,43 @@ open class VRMSceneLoader { func material(withMaterialIndex index: Int) throws -> SCNMaterial { if let cache = try sceneData.load(\.materials, index: index) { return cache } - let gltfMaterial = try gltf.load(\.materials)[index] + let materials = try gltf.load(\.materials) + guard materials.indices.contains(index) else { + throw VRMError._dataInconsistent("Material index \(index) out of bounds") + } + let gltfMaterial = materials[index] let material = try SCNMaterial(material: gltfMaterial, loader: self) sceneData.materials[index] = material return material } + func vrm0MaterialProperty(named name: String) -> VRM0.MaterialProperty? { + guard case .v0(let vrm0) = vrm else { return nil } + return vrm0.materialPropertyNameMap[name] + } + + func renderQueue(forMaterialNamed name: String?) throws -> Int? { + guard let name else { return nil } + switch vrm { + case .v0(let vrm0): + return vrm0.materialPropertyNameMap[name]?.renderQueue + case .v1: + guard let material = try gltf.load(\.materials).first(where: { $0.name == name }) else { + return nil + } + let baseQueue: Int + switch material.alphaMode { + case .OPAQUE: + baseQueue = 2000 + case .MASK: + baseQueue = 2450 + case .BLEND: + baseQueue = material.extensions?.materialsMToon?.transparentWithZWrite == true ? 2501 : 3000 + } + return baseQueue + (material.extensions?.materialsMToon?.renderQueueOffsetNumber ?? 0) + } + } + func texture(withTextureIndex index: Int) throws -> SCNMaterialProperty { if let cache = try sceneData.load(\.textures, index: index) { return cache } let gltfTexture = try gltf.load(\.textures)[index] diff --git a/Tests/VRMKitTests/VRM1Tests.swift b/Tests/VRMKitTests/VRM1Tests.swift index 55e97a7..0947fb3 100644 --- a/Tests/VRMKitTests/VRM1Tests.swift +++ b/Tests/VRMKitTests/VRM1Tests.swift @@ -124,238 +124,276 @@ class VRM1Tests: XCTestCase { XCTAssertEqual(vrm.humanoid.humanBones.rightLittleDistal?.node, 129) } + func testExpressionsPresetAllowsMissingEntries() throws { + let json = """ + { + "preset": { + "happy": { + "morphTargetBinds": [ + { "node": 1, "index": 2, "weight": 0.5 } + ] + } + } + } + """.data(using: .utf8)! + + let expressions = try JSONDecoder().decode(VRM1.Expressions.self, from: json) + + XCTAssertEqual(expressions.preset?.happy?.morphTargetBinds?.first?.node, 1) + XCTAssertNil(expressions.preset?.angry) + } + + func testExpressionsAllowsCustomOnly() throws { + let json = """ + { + "custom": { + "smile": { + "morphTargetBinds": [ + { "node": 1, "index": 2, "weight": 1.0 } + ] + } + } + } + """.data(using: .utf8)! + + let expressions = try JSONDecoder().decode(VRM1.Expressions.self, from: json) + + XCTAssertNil(expressions.preset) + XCTAssertNotNil(expressions.custom) + } + func testExpressions() { - XCTAssertEqual(vrm.expressions?.preset.happy.morphTargetBinds?.count, 1) - XCTAssertEqual(vrm.expressions?.preset.happy.morphTargetBinds?[0].node, 2) - XCTAssertEqual(vrm.expressions?.preset.happy.morphTargetBinds?[0].index, 33) - XCTAssertEqual(vrm.expressions?.preset.happy.morphTargetBinds?[0].weight, 1) - XCTAssertEqual(vrm.expressions?.preset.happy.materialColorBinds?.count, nil) - XCTAssertEqual(vrm.expressions?.preset.happy.textureTransformBinds?.count, 1) - XCTAssertEqual(vrm.expressions?.preset.happy.textureTransformBinds?[0].material, 11) - XCTAssertEqual(vrm.expressions?.preset.happy.textureTransformBinds?[0].offset?.count, 2) - XCTAssertEqual(vrm.expressions?.preset.happy.textureTransformBinds?[0].offset?[0], 0.25) - XCTAssertEqual(vrm.expressions?.preset.happy.textureTransformBinds?[0].offset?[1], 0) - XCTAssertEqual(vrm.expressions?.preset.happy.textureTransformBinds?[0].scale?.count, 2) - XCTAssertEqual(vrm.expressions?.preset.happy.textureTransformBinds?[0].scale?[0], 1) - XCTAssertEqual(vrm.expressions?.preset.happy.textureTransformBinds?[0].scale?[1], 1) - XCTAssertEqual(vrm.expressions?.preset.happy.isBinary, true) - XCTAssertEqual(vrm.expressions?.preset.happy.overrideBlink, .blend) - XCTAssertEqual(vrm.expressions?.preset.happy.overrideLookAt, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.happy.overrideMouth, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.happy?.morphTargetBinds?.count, 1) + XCTAssertEqual(vrm.expressions?.preset?.happy?.morphTargetBinds?[0].node, 2) + XCTAssertEqual(vrm.expressions?.preset?.happy?.morphTargetBinds?[0].index, 33) + XCTAssertEqual(vrm.expressions?.preset?.happy?.morphTargetBinds?[0].weight, 1) + XCTAssertEqual(vrm.expressions?.preset?.happy?.materialColorBinds?.count, nil) + XCTAssertEqual(vrm.expressions?.preset?.happy?.textureTransformBinds?.count, 1) + XCTAssertEqual(vrm.expressions?.preset?.happy?.textureTransformBinds?[0].material, 11) + XCTAssertEqual(vrm.expressions?.preset?.happy?.textureTransformBinds?[0].offset?.count, 2) + XCTAssertEqual(vrm.expressions?.preset?.happy?.textureTransformBinds?[0].offset?[0], 0.25) + XCTAssertEqual(vrm.expressions?.preset?.happy?.textureTransformBinds?[0].offset?[1], 0) + XCTAssertEqual(vrm.expressions?.preset?.happy?.textureTransformBinds?[0].scale?.count, 2) + XCTAssertEqual(vrm.expressions?.preset?.happy?.textureTransformBinds?[0].scale?[0], 1) + XCTAssertEqual(vrm.expressions?.preset?.happy?.textureTransformBinds?[0].scale?[1], 1) + XCTAssertEqual(vrm.expressions?.preset?.happy?.isBinary, true) + XCTAssertEqual(vrm.expressions?.preset?.happy?.overrideBlink, .blend) + XCTAssertEqual(vrm.expressions?.preset?.happy?.overrideLookAt, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.happy?.overrideMouth, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.angry.morphTargetBinds?.count, 1) - XCTAssertEqual(vrm.expressions?.preset.angry.morphTargetBinds?[0].node, 2) - XCTAssertEqual(vrm.expressions?.preset.angry.morphTargetBinds?[0].index, 34) - XCTAssertEqual(vrm.expressions?.preset.angry.morphTargetBinds?[0].weight, 1) - XCTAssertEqual(vrm.expressions?.preset.angry.materialColorBinds?.count, nil) - XCTAssertEqual(vrm.expressions?.preset.angry.textureTransformBinds?.count, 1) - XCTAssertEqual(vrm.expressions?.preset.angry.textureTransformBinds?[0].material, 11) - XCTAssertEqual(vrm.expressions?.preset.angry.textureTransformBinds?[0].offset?.count, 2) - XCTAssertEqual(vrm.expressions?.preset.angry.textureTransformBinds?[0].offset?[0], 0.5) - XCTAssertEqual(vrm.expressions?.preset.angry.textureTransformBinds?[0].offset?[1], 0) - XCTAssertEqual(vrm.expressions?.preset.angry.textureTransformBinds?[0].scale?.count, 2) - XCTAssertEqual(vrm.expressions?.preset.angry.textureTransformBinds?[0].scale?[0], 1) - XCTAssertEqual(vrm.expressions?.preset.angry.textureTransformBinds?[0].scale?[1], 1) - XCTAssertEqual(vrm.expressions?.preset.angry.isBinary, true) - XCTAssertEqual(vrm.expressions?.preset.angry.overrideBlink, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.angry.overrideLookAt, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.angry.overrideMouth, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.angry?.morphTargetBinds?.count, 1) + XCTAssertEqual(vrm.expressions?.preset?.angry?.morphTargetBinds?[0].node, 2) + XCTAssertEqual(vrm.expressions?.preset?.angry?.morphTargetBinds?[0].index, 34) + XCTAssertEqual(vrm.expressions?.preset?.angry?.morphTargetBinds?[0].weight, 1) + XCTAssertEqual(vrm.expressions?.preset?.angry?.materialColorBinds?.count, nil) + XCTAssertEqual(vrm.expressions?.preset?.angry?.textureTransformBinds?.count, 1) + XCTAssertEqual(vrm.expressions?.preset?.angry?.textureTransformBinds?[0].material, 11) + XCTAssertEqual(vrm.expressions?.preset?.angry?.textureTransformBinds?[0].offset?.count, 2) + XCTAssertEqual(vrm.expressions?.preset?.angry?.textureTransformBinds?[0].offset?[0], 0.5) + XCTAssertEqual(vrm.expressions?.preset?.angry?.textureTransformBinds?[0].offset?[1], 0) + XCTAssertEqual(vrm.expressions?.preset?.angry?.textureTransformBinds?[0].scale?.count, 2) + XCTAssertEqual(vrm.expressions?.preset?.angry?.textureTransformBinds?[0].scale?[0], 1) + XCTAssertEqual(vrm.expressions?.preset?.angry?.textureTransformBinds?[0].scale?[1], 1) + XCTAssertEqual(vrm.expressions?.preset?.angry?.isBinary, true) + XCTAssertEqual(vrm.expressions?.preset?.angry?.overrideBlink, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.angry?.overrideLookAt, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.angry?.overrideMouth, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.sad.morphTargetBinds?.count, 1) - XCTAssertEqual(vrm.expressions?.preset.sad.morphTargetBinds?[0].node, 2) - XCTAssertEqual(vrm.expressions?.preset.sad.morphTargetBinds?[0].index, 35) - XCTAssertEqual(vrm.expressions?.preset.sad.morphTargetBinds?[0].weight, 1) - XCTAssertEqual(vrm.expressions?.preset.sad.materialColorBinds?.count, nil) - XCTAssertEqual(vrm.expressions?.preset.sad.textureTransformBinds?.count, 1) - XCTAssertEqual(vrm.expressions?.preset.sad.textureTransformBinds?[0].material, 11) - XCTAssertEqual(vrm.expressions?.preset.sad.textureTransformBinds?[0].offset?.count, 2) - XCTAssertEqual(vrm.expressions?.preset.sad.textureTransformBinds?[0].offset?[0], 0.75) - XCTAssertEqual(vrm.expressions?.preset.sad.textureTransformBinds?[0].offset?[1], 0) - XCTAssertEqual(vrm.expressions?.preset.sad.textureTransformBinds?[0].scale?.count, 2) - XCTAssertEqual(vrm.expressions?.preset.sad.textureTransformBinds?[0].scale?[0], 1) - XCTAssertEqual(vrm.expressions?.preset.sad.textureTransformBinds?[0].scale?[1], 1) - XCTAssertEqual(vrm.expressions?.preset.sad.isBinary, true) - XCTAssertEqual(vrm.expressions?.preset.sad.overrideBlink, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.sad.overrideLookAt, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.sad.overrideMouth, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.sad?.morphTargetBinds?.count, 1) + XCTAssertEqual(vrm.expressions?.preset?.sad?.morphTargetBinds?[0].node, 2) + XCTAssertEqual(vrm.expressions?.preset?.sad?.morphTargetBinds?[0].index, 35) + XCTAssertEqual(vrm.expressions?.preset?.sad?.morphTargetBinds?[0].weight, 1) + XCTAssertEqual(vrm.expressions?.preset?.sad?.materialColorBinds?.count, nil) + XCTAssertEqual(vrm.expressions?.preset?.sad?.textureTransformBinds?.count, 1) + XCTAssertEqual(vrm.expressions?.preset?.sad?.textureTransformBinds?[0].material, 11) + XCTAssertEqual(vrm.expressions?.preset?.sad?.textureTransformBinds?[0].offset?.count, 2) + XCTAssertEqual(vrm.expressions?.preset?.sad?.textureTransformBinds?[0].offset?[0], 0.75) + XCTAssertEqual(vrm.expressions?.preset?.sad?.textureTransformBinds?[0].offset?[1], 0) + XCTAssertEqual(vrm.expressions?.preset?.sad?.textureTransformBinds?[0].scale?.count, 2) + XCTAssertEqual(vrm.expressions?.preset?.sad?.textureTransformBinds?[0].scale?[0], 1) + XCTAssertEqual(vrm.expressions?.preset?.sad?.textureTransformBinds?[0].scale?[1], 1) + XCTAssertEqual(vrm.expressions?.preset?.sad?.isBinary, true) + XCTAssertEqual(vrm.expressions?.preset?.sad?.overrideBlink, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.sad?.overrideLookAt, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.sad?.overrideMouth, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.relaxed.morphTargetBinds?.count, 1) - XCTAssertEqual(vrm.expressions?.preset.relaxed.morphTargetBinds?[0].node, 2) - XCTAssertEqual(vrm.expressions?.preset.relaxed.morphTargetBinds?[0].index, 36) - XCTAssertEqual(vrm.expressions?.preset.relaxed.morphTargetBinds?[0].weight, 1) - XCTAssertEqual(vrm.expressions?.preset.relaxed.materialColorBinds?.count, nil) - XCTAssertEqual(vrm.expressions?.preset.relaxed.textureTransformBinds?.count, 1) - XCTAssertEqual(vrm.expressions?.preset.relaxed.textureTransformBinds?[0].material, 11) - XCTAssertEqual(vrm.expressions?.preset.relaxed.textureTransformBinds?[0].offset?.count, 2) - XCTAssertEqual(vrm.expressions?.preset.relaxed.textureTransformBinds?[0].offset?[0], 0.5) - XCTAssertEqual(vrm.expressions?.preset.relaxed.textureTransformBinds?[0].offset?[1], 0.25) - XCTAssertEqual(vrm.expressions?.preset.relaxed.textureTransformBinds?[0].scale?.count, 2) - XCTAssertEqual(vrm.expressions?.preset.relaxed.textureTransformBinds?[0].scale?[0], 1) - XCTAssertEqual(vrm.expressions?.preset.relaxed.textureTransformBinds?[0].scale?[1], 1) - XCTAssertEqual(vrm.expressions?.preset.relaxed.isBinary, true) - XCTAssertEqual(vrm.expressions?.preset.relaxed.overrideBlink, .some(.block)) - XCTAssertEqual(vrm.expressions?.preset.relaxed.overrideLookAt, .some(.block)) - XCTAssertEqual(vrm.expressions?.preset.relaxed.overrideMouth, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.relaxed?.morphTargetBinds?.count, 1) + XCTAssertEqual(vrm.expressions?.preset?.relaxed?.morphTargetBinds?[0].node, 2) + XCTAssertEqual(vrm.expressions?.preset?.relaxed?.morphTargetBinds?[0].index, 36) + XCTAssertEqual(vrm.expressions?.preset?.relaxed?.morphTargetBinds?[0].weight, 1) + XCTAssertEqual(vrm.expressions?.preset?.relaxed?.materialColorBinds?.count, nil) + XCTAssertEqual(vrm.expressions?.preset?.relaxed?.textureTransformBinds?.count, 1) + XCTAssertEqual(vrm.expressions?.preset?.relaxed?.textureTransformBinds?[0].material, 11) + XCTAssertEqual(vrm.expressions?.preset?.relaxed?.textureTransformBinds?[0].offset?.count, 2) + XCTAssertEqual(vrm.expressions?.preset?.relaxed?.textureTransformBinds?[0].offset?[0], 0.5) + XCTAssertEqual(vrm.expressions?.preset?.relaxed?.textureTransformBinds?[0].offset?[1], 0.25) + XCTAssertEqual(vrm.expressions?.preset?.relaxed?.textureTransformBinds?[0].scale?.count, 2) + XCTAssertEqual(vrm.expressions?.preset?.relaxed?.textureTransformBinds?[0].scale?[0], 1) + XCTAssertEqual(vrm.expressions?.preset?.relaxed?.textureTransformBinds?[0].scale?[1], 1) + XCTAssertEqual(vrm.expressions?.preset?.relaxed?.isBinary, true) + XCTAssertEqual(vrm.expressions?.preset?.relaxed?.overrideBlink, .some(.block)) + XCTAssertEqual(vrm.expressions?.preset?.relaxed?.overrideLookAt, .some(.block)) + XCTAssertEqual(vrm.expressions?.preset?.relaxed?.overrideMouth, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.surprised.morphTargetBinds?.count, 1) - XCTAssertEqual(vrm.expressions?.preset.surprised.morphTargetBinds?[0].node, 2) - XCTAssertEqual(vrm.expressions?.preset.surprised.morphTargetBinds?[0].index, 38) - XCTAssertEqual(vrm.expressions?.preset.surprised.morphTargetBinds?[0].weight, 1) - XCTAssertEqual(vrm.expressions?.preset.surprised.materialColorBinds?.count, nil) - XCTAssertEqual(vrm.expressions?.preset.surprised.textureTransformBinds?.count, 1) - XCTAssertEqual(vrm.expressions?.preset.surprised.textureTransformBinds?[0].material, 11) - XCTAssertEqual(vrm.expressions?.preset.surprised.textureTransformBinds?[0].offset?.count, 2) - XCTAssertEqual(vrm.expressions?.preset.surprised.textureTransformBinds?[0].offset?[0], 0) - XCTAssertEqual(vrm.expressions?.preset.surprised.textureTransformBinds?[0].offset?[1], 0.25) - XCTAssertEqual(vrm.expressions?.preset.surprised.textureTransformBinds?[0].scale?.count, 2) - XCTAssertEqual(vrm.expressions?.preset.surprised.textureTransformBinds?[0].scale?[0], 1) - XCTAssertEqual(vrm.expressions?.preset.surprised.textureTransformBinds?[0].scale?[1], 1) - XCTAssertEqual(vrm.expressions?.preset.surprised.isBinary, true) - XCTAssertEqual(vrm.expressions?.preset.surprised.overrideBlink, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.surprised.overrideLookAt, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.surprised.overrideMouth, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.surprised?.morphTargetBinds?.count, 1) + XCTAssertEqual(vrm.expressions?.preset?.surprised?.morphTargetBinds?[0].node, 2) + XCTAssertEqual(vrm.expressions?.preset?.surprised?.morphTargetBinds?[0].index, 38) + XCTAssertEqual(vrm.expressions?.preset?.surprised?.morphTargetBinds?[0].weight, 1) + XCTAssertEqual(vrm.expressions?.preset?.surprised?.materialColorBinds?.count, nil) + XCTAssertEqual(vrm.expressions?.preset?.surprised?.textureTransformBinds?.count, 1) + XCTAssertEqual(vrm.expressions?.preset?.surprised?.textureTransformBinds?[0].material, 11) + XCTAssertEqual(vrm.expressions?.preset?.surprised?.textureTransformBinds?[0].offset?.count, 2) + XCTAssertEqual(vrm.expressions?.preset?.surprised?.textureTransformBinds?[0].offset?[0], 0) + XCTAssertEqual(vrm.expressions?.preset?.surprised?.textureTransformBinds?[0].offset?[1], 0.25) + XCTAssertEqual(vrm.expressions?.preset?.surprised?.textureTransformBinds?[0].scale?.count, 2) + XCTAssertEqual(vrm.expressions?.preset?.surprised?.textureTransformBinds?[0].scale?[0], 1) + XCTAssertEqual(vrm.expressions?.preset?.surprised?.textureTransformBinds?[0].scale?[1], 1) + XCTAssertEqual(vrm.expressions?.preset?.surprised?.isBinary, true) + XCTAssertEqual(vrm.expressions?.preset?.surprised?.overrideBlink, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.surprised?.overrideLookAt, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.surprised?.overrideMouth, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.aa.morphTargetBinds?.count, 1) - XCTAssertEqual(vrm.expressions?.preset.aa.morphTargetBinds?[0].node, 2) - XCTAssertEqual(vrm.expressions?.preset.aa.morphTargetBinds?[0].index, 25) - XCTAssertEqual(vrm.expressions?.preset.aa.morphTargetBinds?[0].weight, 1) - XCTAssertEqual(vrm.expressions?.preset.aa.materialColorBinds?.count, nil) - XCTAssertEqual(vrm.expressions?.preset.aa.textureTransformBinds?.count, nil) - XCTAssertEqual(vrm.expressions?.preset.aa.isBinary, false) - XCTAssertEqual(vrm.expressions?.preset.aa.overrideBlink, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.aa.overrideLookAt, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.aa.overrideMouth, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.ih.morphTargetBinds?.count, 1) - XCTAssertEqual(vrm.expressions?.preset.ih.morphTargetBinds?[0].node, 2) - XCTAssertEqual(vrm.expressions?.preset.ih.morphTargetBinds?[0].index, 26) - XCTAssertEqual(vrm.expressions?.preset.ih.morphTargetBinds?[0].weight, 1) - XCTAssertEqual(vrm.expressions?.preset.ih.materialColorBinds?.count, nil) - XCTAssertEqual(vrm.expressions?.preset.ih.textureTransformBinds?.count, nil) - XCTAssertEqual(vrm.expressions?.preset.ih.isBinary, false) - XCTAssertEqual(vrm.expressions?.preset.ih.overrideBlink, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.ih.overrideLookAt, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.ih.overrideMouth, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.aa?.morphTargetBinds?.count, 1) + XCTAssertEqual(vrm.expressions?.preset?.aa?.morphTargetBinds?[0].node, 2) + XCTAssertEqual(vrm.expressions?.preset?.aa?.morphTargetBinds?[0].index, 25) + XCTAssertEqual(vrm.expressions?.preset?.aa?.morphTargetBinds?[0].weight, 1) + XCTAssertEqual(vrm.expressions?.preset?.aa?.materialColorBinds?.count, nil) + XCTAssertEqual(vrm.expressions?.preset?.aa?.textureTransformBinds?.count, nil) + XCTAssertEqual(vrm.expressions?.preset?.aa?.isBinary, false) + XCTAssertEqual(vrm.expressions?.preset?.aa?.overrideBlink, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.aa?.overrideLookAt, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.aa?.overrideMouth, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.ih?.morphTargetBinds?.count, 1) + XCTAssertEqual(vrm.expressions?.preset?.ih?.morphTargetBinds?[0].node, 2) + XCTAssertEqual(vrm.expressions?.preset?.ih?.morphTargetBinds?[0].index, 26) + XCTAssertEqual(vrm.expressions?.preset?.ih?.morphTargetBinds?[0].weight, 1) + XCTAssertEqual(vrm.expressions?.preset?.ih?.materialColorBinds?.count, nil) + XCTAssertEqual(vrm.expressions?.preset?.ih?.textureTransformBinds?.count, nil) + XCTAssertEqual(vrm.expressions?.preset?.ih?.isBinary, false) + XCTAssertEqual(vrm.expressions?.preset?.ih?.overrideBlink, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.ih?.overrideLookAt, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.ih?.overrideMouth, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.ou.morphTargetBinds?.count, 1) - XCTAssertEqual(vrm.expressions?.preset.ou.morphTargetBinds?[0].node, 2) - XCTAssertEqual(vrm.expressions?.preset.ou.morphTargetBinds?[0].index, 27) - XCTAssertEqual(vrm.expressions?.preset.ou.morphTargetBinds?[0].weight, 1) - XCTAssertEqual(vrm.expressions?.preset.ou.materialColorBinds?.count, nil) - XCTAssertEqual(vrm.expressions?.preset.ou.textureTransformBinds?.count, nil) - XCTAssertEqual(vrm.expressions?.preset.ou.isBinary, false) - XCTAssertEqual(vrm.expressions?.preset.ou.overrideBlink, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.ou.overrideLookAt, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.ou.overrideMouth, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.ou?.morphTargetBinds?.count, 1) + XCTAssertEqual(vrm.expressions?.preset?.ou?.morphTargetBinds?[0].node, 2) + XCTAssertEqual(vrm.expressions?.preset?.ou?.morphTargetBinds?[0].index, 27) + XCTAssertEqual(vrm.expressions?.preset?.ou?.morphTargetBinds?[0].weight, 1) + XCTAssertEqual(vrm.expressions?.preset?.ou?.materialColorBinds?.count, nil) + XCTAssertEqual(vrm.expressions?.preset?.ou?.textureTransformBinds?.count, nil) + XCTAssertEqual(vrm.expressions?.preset?.ou?.isBinary, false) + XCTAssertEqual(vrm.expressions?.preset?.ou?.overrideBlink, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.ou?.overrideLookAt, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.ou?.overrideMouth, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.ee.morphTargetBinds?.count, 1) - XCTAssertEqual(vrm.expressions?.preset.ee.morphTargetBinds?[0].node, 2) - XCTAssertEqual(vrm.expressions?.preset.ee.morphTargetBinds?[0].index, 28) - XCTAssertEqual(vrm.expressions?.preset.ee.morphTargetBinds?[0].weight, 1) - XCTAssertEqual(vrm.expressions?.preset.ee.materialColorBinds?.count, nil) - XCTAssertEqual(vrm.expressions?.preset.ee.textureTransformBinds?.count, nil) - XCTAssertEqual(vrm.expressions?.preset.ee.isBinary, false) - XCTAssertEqual(vrm.expressions?.preset.ee.overrideBlink, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.ee.overrideLookAt, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.ee.overrideMouth, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.ee?.morphTargetBinds?.count, 1) + XCTAssertEqual(vrm.expressions?.preset?.ee?.morphTargetBinds?[0].node, 2) + XCTAssertEqual(vrm.expressions?.preset?.ee?.morphTargetBinds?[0].index, 28) + XCTAssertEqual(vrm.expressions?.preset?.ee?.morphTargetBinds?[0].weight, 1) + XCTAssertEqual(vrm.expressions?.preset?.ee?.materialColorBinds?.count, nil) + XCTAssertEqual(vrm.expressions?.preset?.ee?.textureTransformBinds?.count, nil) + XCTAssertEqual(vrm.expressions?.preset?.ee?.isBinary, false) + XCTAssertEqual(vrm.expressions?.preset?.ee?.overrideBlink, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.ee?.overrideLookAt, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.ee?.overrideMouth, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.oh.morphTargetBinds?.count, 1) - XCTAssertEqual(vrm.expressions?.preset.oh.morphTargetBinds?[0].node, 2) - XCTAssertEqual(vrm.expressions?.preset.oh.morphTargetBinds?[0].index, 29) - XCTAssertEqual(vrm.expressions?.preset.oh.morphTargetBinds?[0].weight, 1) - XCTAssertEqual(vrm.expressions?.preset.oh.materialColorBinds?.count, nil) - XCTAssertEqual(vrm.expressions?.preset.oh.textureTransformBinds?.count, nil) - XCTAssertEqual(vrm.expressions?.preset.oh.isBinary, false) - XCTAssertEqual(vrm.expressions?.preset.oh.overrideBlink, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.oh.overrideLookAt, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.oh.overrideMouth, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.oh?.morphTargetBinds?.count, 1) + XCTAssertEqual(vrm.expressions?.preset?.oh?.morphTargetBinds?[0].node, 2) + XCTAssertEqual(vrm.expressions?.preset?.oh?.morphTargetBinds?[0].index, 29) + XCTAssertEqual(vrm.expressions?.preset?.oh?.morphTargetBinds?[0].weight, 1) + XCTAssertEqual(vrm.expressions?.preset?.oh?.materialColorBinds?.count, nil) + XCTAssertEqual(vrm.expressions?.preset?.oh?.textureTransformBinds?.count, nil) + XCTAssertEqual(vrm.expressions?.preset?.oh?.isBinary, false) + XCTAssertEqual(vrm.expressions?.preset?.oh?.overrideBlink, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.oh?.overrideLookAt, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.oh?.overrideMouth, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.blink.morphTargetBinds?.count, 2) - XCTAssertEqual(vrm.expressions?.preset.blink.morphTargetBinds?[0].node, 2) - XCTAssertEqual(vrm.expressions?.preset.blink.morphTargetBinds?[0].index, 1) - XCTAssertEqual(vrm.expressions?.preset.blink.morphTargetBinds?[0].weight, 1) - XCTAssertEqual(vrm.expressions?.preset.blink.morphTargetBinds?[1].node, 2) - XCTAssertEqual(vrm.expressions?.preset.blink.morphTargetBinds?[1].index, 2) - XCTAssertEqual(vrm.expressions?.preset.blink.morphTargetBinds?[1].weight, 1) - XCTAssertEqual(vrm.expressions?.preset.blink.materialColorBinds?.count, nil) - XCTAssertEqual(vrm.expressions?.preset.blink.textureTransformBinds?.count, nil) - XCTAssertEqual(vrm.expressions?.preset.blink.isBinary, false) - XCTAssertEqual(vrm.expressions?.preset.blink.overrideBlink, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.blink.overrideLookAt, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.blink.overrideMouth, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.blink?.morphTargetBinds?.count, 2) + XCTAssertEqual(vrm.expressions?.preset?.blink?.morphTargetBinds?[0].node, 2) + XCTAssertEqual(vrm.expressions?.preset?.blink?.morphTargetBinds?[0].index, 1) + XCTAssertEqual(vrm.expressions?.preset?.blink?.morphTargetBinds?[0].weight, 1) + XCTAssertEqual(vrm.expressions?.preset?.blink?.morphTargetBinds?[1].node, 2) + XCTAssertEqual(vrm.expressions?.preset?.blink?.morphTargetBinds?[1].index, 2) + XCTAssertEqual(vrm.expressions?.preset?.blink?.morphTargetBinds?[1].weight, 1) + XCTAssertEqual(vrm.expressions?.preset?.blink?.materialColorBinds?.count, nil) + XCTAssertEqual(vrm.expressions?.preset?.blink?.textureTransformBinds?.count, nil) + XCTAssertEqual(vrm.expressions?.preset?.blink?.isBinary, false) + XCTAssertEqual(vrm.expressions?.preset?.blink?.overrideBlink, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.blink?.overrideLookAt, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.blink?.overrideMouth, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.blinkLeft.morphTargetBinds?.count, 1) - XCTAssertEqual(vrm.expressions?.preset.blinkLeft.morphTargetBinds?[0].node, 2) - XCTAssertEqual(vrm.expressions?.preset.blinkLeft.morphTargetBinds?[0].index, 1) - XCTAssertEqual(vrm.expressions?.preset.blinkLeft.morphTargetBinds?[0].weight, 1) - XCTAssertEqual(vrm.expressions?.preset.blinkLeft.materialColorBinds?.count, nil) - XCTAssertEqual(vrm.expressions?.preset.blinkLeft.textureTransformBinds?.count, nil) - XCTAssertEqual(vrm.expressions?.preset.blinkLeft.isBinary, false) - XCTAssertEqual(vrm.expressions?.preset.blinkLeft.overrideBlink, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.blinkLeft.overrideLookAt, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.blinkLeft.overrideMouth, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.blinkLeft?.morphTargetBinds?.count, 1) + XCTAssertEqual(vrm.expressions?.preset?.blinkLeft?.morphTargetBinds?[0].node, 2) + XCTAssertEqual(vrm.expressions?.preset?.blinkLeft?.morphTargetBinds?[0].index, 1) + XCTAssertEqual(vrm.expressions?.preset?.blinkLeft?.morphTargetBinds?[0].weight, 1) + XCTAssertEqual(vrm.expressions?.preset?.blinkLeft?.materialColorBinds?.count, nil) + XCTAssertEqual(vrm.expressions?.preset?.blinkLeft?.textureTransformBinds?.count, nil) + XCTAssertEqual(vrm.expressions?.preset?.blinkLeft?.isBinary, false) + XCTAssertEqual(vrm.expressions?.preset?.blinkLeft?.overrideBlink, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.blinkLeft?.overrideLookAt, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.blinkLeft?.overrideMouth, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.blinkRight.morphTargetBinds?.count, 1) - XCTAssertEqual(vrm.expressions?.preset.blinkRight.morphTargetBinds?[0].node, 2) - XCTAssertEqual(vrm.expressions?.preset.blinkRight.morphTargetBinds?[0].index, 2) - XCTAssertEqual(vrm.expressions?.preset.blinkRight.morphTargetBinds?[0].weight, 1) - XCTAssertEqual(vrm.expressions?.preset.blinkRight.materialColorBinds?.count, nil) - XCTAssertEqual(vrm.expressions?.preset.blinkRight.textureTransformBinds?.count, nil) - XCTAssertEqual(vrm.expressions?.preset.blinkRight.isBinary, false) - XCTAssertEqual(vrm.expressions?.preset.blinkRight.overrideBlink, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.blinkRight.overrideLookAt, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.blinkRight.overrideMouth, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.blinkRight?.morphTargetBinds?.count, 1) + XCTAssertEqual(vrm.expressions?.preset?.blinkRight?.morphTargetBinds?[0].node, 2) + XCTAssertEqual(vrm.expressions?.preset?.blinkRight?.morphTargetBinds?[0].index, 2) + XCTAssertEqual(vrm.expressions?.preset?.blinkRight?.morphTargetBinds?[0].weight, 1) + XCTAssertEqual(vrm.expressions?.preset?.blinkRight?.materialColorBinds?.count, nil) + XCTAssertEqual(vrm.expressions?.preset?.blinkRight?.textureTransformBinds?.count, nil) + XCTAssertEqual(vrm.expressions?.preset?.blinkRight?.isBinary, false) + XCTAssertEqual(vrm.expressions?.preset?.blinkRight?.overrideBlink, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.blinkRight?.overrideLookAt, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.blinkRight?.overrideMouth, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.lookUp.morphTargetBinds?.count, 1) - XCTAssertEqual(vrm.expressions?.preset.lookUp.morphTargetBinds?[0].node, 2) - XCTAssertEqual(vrm.expressions?.preset.lookUp.morphTargetBinds?[0].index, 39) - XCTAssertEqual(vrm.expressions?.preset.lookUp.morphTargetBinds?[0].weight, 1) - XCTAssertEqual(vrm.expressions?.preset.lookUp.materialColorBinds?.count, nil) - XCTAssertEqual(vrm.expressions?.preset.lookUp.textureTransformBinds?.count, nil) - XCTAssertEqual(vrm.expressions?.preset.lookUp.isBinary, false) - XCTAssertEqual(vrm.expressions?.preset.lookUp.overrideBlink, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.lookUp.overrideLookAt, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.lookUp.overrideMouth, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.lookUp?.morphTargetBinds?.count, 1) + XCTAssertEqual(vrm.expressions?.preset?.lookUp?.morphTargetBinds?[0].node, 2) + XCTAssertEqual(vrm.expressions?.preset?.lookUp?.morphTargetBinds?[0].index, 39) + XCTAssertEqual(vrm.expressions?.preset?.lookUp?.morphTargetBinds?[0].weight, 1) + XCTAssertEqual(vrm.expressions?.preset?.lookUp?.materialColorBinds?.count, nil) + XCTAssertEqual(vrm.expressions?.preset?.lookUp?.textureTransformBinds?.count, nil) + XCTAssertEqual(vrm.expressions?.preset?.lookUp?.isBinary, false) + XCTAssertEqual(vrm.expressions?.preset?.lookUp?.overrideBlink, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.lookUp?.overrideLookAt, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.lookUp?.overrideMouth, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.lookDown.morphTargetBinds?.count, 1) - XCTAssertEqual(vrm.expressions?.preset.lookDown.morphTargetBinds?[0].node, 2) - XCTAssertEqual(vrm.expressions?.preset.lookDown.morphTargetBinds?[0].index, 40) - XCTAssertEqual(vrm.expressions?.preset.lookDown.morphTargetBinds?[0].weight, 1) - XCTAssertEqual(vrm.expressions?.preset.lookDown.materialColorBinds?.count, nil) - XCTAssertEqual(vrm.expressions?.preset.lookDown.textureTransformBinds?.count, nil) - XCTAssertEqual(vrm.expressions?.preset.lookDown.isBinary, false) - XCTAssertEqual(vrm.expressions?.preset.lookDown.overrideBlink, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.lookDown.overrideLookAt, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.lookDown.overrideMouth, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.lookDown?.morphTargetBinds?.count, 1) + XCTAssertEqual(vrm.expressions?.preset?.lookDown?.morphTargetBinds?[0].node, 2) + XCTAssertEqual(vrm.expressions?.preset?.lookDown?.morphTargetBinds?[0].index, 40) + XCTAssertEqual(vrm.expressions?.preset?.lookDown?.morphTargetBinds?[0].weight, 1) + XCTAssertEqual(vrm.expressions?.preset?.lookDown?.materialColorBinds?.count, nil) + XCTAssertEqual(vrm.expressions?.preset?.lookDown?.textureTransformBinds?.count, nil) + XCTAssertEqual(vrm.expressions?.preset?.lookDown?.isBinary, false) + XCTAssertEqual(vrm.expressions?.preset?.lookDown?.overrideBlink, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.lookDown?.overrideLookAt, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.lookDown?.overrideMouth, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.lookLeft.morphTargetBinds?.count, 1) - XCTAssertEqual(vrm.expressions?.preset.lookLeft.morphTargetBinds?[0].node, 2) - XCTAssertEqual(vrm.expressions?.preset.lookLeft.morphTargetBinds?[0].index, 41) - XCTAssertEqual(vrm.expressions?.preset.lookLeft.morphTargetBinds?[0].weight, 1) - XCTAssertEqual(vrm.expressions?.preset.lookLeft.materialColorBinds?.count, nil) - XCTAssertEqual(vrm.expressions?.preset.lookLeft.textureTransformBinds?.count, nil) - XCTAssertEqual(vrm.expressions?.preset.lookLeft.isBinary, false) - XCTAssertEqual(vrm.expressions?.preset.lookLeft.overrideBlink, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.lookLeft.overrideLookAt, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.lookLeft.overrideMouth, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.lookLeft?.morphTargetBinds?.count, 1) + XCTAssertEqual(vrm.expressions?.preset?.lookLeft?.morphTargetBinds?[0].node, 2) + XCTAssertEqual(vrm.expressions?.preset?.lookLeft?.morphTargetBinds?[0].index, 41) + XCTAssertEqual(vrm.expressions?.preset?.lookLeft?.morphTargetBinds?[0].weight, 1) + XCTAssertEqual(vrm.expressions?.preset?.lookLeft?.materialColorBinds?.count, nil) + XCTAssertEqual(vrm.expressions?.preset?.lookLeft?.textureTransformBinds?.count, nil) + XCTAssertEqual(vrm.expressions?.preset?.lookLeft?.isBinary, false) + XCTAssertEqual(vrm.expressions?.preset?.lookLeft?.overrideBlink, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.lookLeft?.overrideLookAt, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.lookLeft?.overrideMouth, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.lookRight.morphTargetBinds?.count, 1) - XCTAssertEqual(vrm.expressions?.preset.lookRight.morphTargetBinds?[0].node, 2) - XCTAssertEqual(vrm.expressions?.preset.lookRight.morphTargetBinds?[0].index, 42) - XCTAssertEqual(vrm.expressions?.preset.lookRight.morphTargetBinds?[0].weight, 1) - XCTAssertEqual(vrm.expressions?.preset.lookRight.materialColorBinds?.count, nil) - XCTAssertEqual(vrm.expressions?.preset.lookRight.textureTransformBinds?.count, nil) - XCTAssertEqual(vrm.expressions?.preset.lookRight.isBinary, false) - XCTAssertEqual(vrm.expressions?.preset.lookRight.overrideBlink, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.lookRight.overrideLookAt, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.lookRight.overrideMouth, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.lookRight?.morphTargetBinds?.count, 1) + XCTAssertEqual(vrm.expressions?.preset?.lookRight?.morphTargetBinds?[0].node, 2) + XCTAssertEqual(vrm.expressions?.preset?.lookRight?.morphTargetBinds?[0].index, 42) + XCTAssertEqual(vrm.expressions?.preset?.lookRight?.morphTargetBinds?[0].weight, 1) + XCTAssertEqual(vrm.expressions?.preset?.lookRight?.materialColorBinds?.count, nil) + XCTAssertEqual(vrm.expressions?.preset?.lookRight?.textureTransformBinds?.count, nil) + XCTAssertEqual(vrm.expressions?.preset?.lookRight?.isBinary, false) + XCTAssertEqual(vrm.expressions?.preset?.lookRight?.overrideBlink, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.lookRight?.overrideLookAt, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.lookRight?.overrideMouth, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.neutral.morphTargetBinds?.count, nil) - XCTAssertEqual(vrm.expressions?.preset.neutral.materialColorBinds?.count, nil) - XCTAssertEqual(vrm.expressions?.preset.neutral.textureTransformBinds?.count, nil) - XCTAssertEqual(vrm.expressions?.preset.neutral.isBinary, false) - XCTAssertEqual(vrm.expressions?.preset.neutral.overrideBlink, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.neutral.overrideLookAt, .some(.none)) - XCTAssertEqual(vrm.expressions?.preset.neutral.overrideMouth, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.neutral?.morphTargetBinds?.count, nil) + XCTAssertEqual(vrm.expressions?.preset?.neutral?.materialColorBinds?.count, nil) + XCTAssertEqual(vrm.expressions?.preset?.neutral?.textureTransformBinds?.count, nil) + XCTAssertEqual(vrm.expressions?.preset?.neutral?.isBinary, false) + XCTAssertEqual(vrm.expressions?.preset?.neutral?.overrideBlink, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.neutral?.overrideLookAt, .some(.none)) + XCTAssertEqual(vrm.expressions?.preset?.neutral?.overrideMouth, .some(.none)) } func testSpringBone() { diff --git a/Tests/VRMSceneKitTests/VRM1SceneKitTests.swift b/Tests/VRMSceneKitTests/VRM1SceneKitTests.swift index b882251..dedc24c 100644 --- a/Tests/VRMSceneKitTests/VRM1SceneKitTests.swift +++ b/Tests/VRMSceneKitTests/VRM1SceneKitTests.swift @@ -1,6 +1,7 @@ import VRMKit @testable import VRMSceneKit import SceneKit +import simd import Testing @Suite @@ -42,4 +43,69 @@ struct VRM1SceneLoaderTests { #expect(result.stride == nil) #expect(result.bufferView.count == 93840) } + + @Test + func testVRM1NativeExpressionBindingsUseNodes() throws { + let vrmLoader = try vrmLoader() + let scene = try vrmLoader.loadScene() + let vrmNode = scene.vrmNode + + #expect(vrmNode.expressionClips.count == 18) + let happyBinding = try #require(vrmNode.expressionClips[.preset(.happy)]?.values.first) + #expect(happyBinding.mesh === (try vrmLoader.node(withNodeIndex: 2))) + #expect(vrmNode.expressionClips[.preset(.aa)]?.values.first?.index == 25) + + vrmNode.setExpression(value: 0.42, for: .preset(.aa)) + #expect(abs(vrmNode.expression(for: .preset(.aa)) - 0.42) < 0.001) + #expect(abs(vrmNode.blendShape(for: .preset(.a)) - 0.42) < 0.001) + } + + @Test + func testVRM1FirstPersonAnnotationsUseNodes() throws { + let vrmLoader = try vrmLoader() + let scene = try vrmLoader.loadScene() + let annotatedNode = try vrmLoader.node(withNodeIndex: 0) + + #expect(annotatedNode.isHidden == false) + scene.vrmNode.setFirstPersonRenderMode(.firstPerson) + #expect(annotatedNode.isHidden == true) + scene.vrmNode.setFirstPersonRenderMode(.thirdPerson) + #expect(annotatedNode.isHidden == false) + } + + @Test + func testVRM1MToonMaterialIsLoadedFromExtension() throws { + let vrmLoader = try vrmLoader() + let material = try vrmLoader.material(withMaterialIndex: 0) + let gltfMaterial = try #require(vrmLoader.vrm.gltf.jsonData.materials?[0]) + + #expect(material.name == gltfMaterial.name) + #expect(material.lightingModel == .constant) + #expect(material.isLitPerPixel == false) + #expect(material.writesToDepthBuffer == true) + } + + @Test + func testVRM1NodeConstraintRotationIsApplied() throws { + let vrmLoader = try vrmLoader() + let scene = try vrmLoader.loadScene() + let target = try vrmLoader.node(withNodeIndex: 14) + let source = try vrmLoader.node(withNodeIndex: 82) + + let targetRest = target.simdOrientation + let sourceRest = source.simdOrientation + let sourceDelta = simd_quatf(angle: 0.35, axis: simd_normalize(SIMD3(0.2, 0.9, 0.3))) + source.simdOrientation = sourceRest * sourceDelta + + scene.vrmNode.update(at: 0) + + let expected = targetRest * (simd_inverse(sourceRest) * source.simdOrientation) + #expect(target.simdOrientation.isApproximatelyEqual(to: expected)) + } +} + +private extension simd_quatf { + func isApproximatelyEqual(to other: simd_quatf, tolerance: Float = 0.0001) -> Bool { + abs(simd_dot(vector, other.vector)) > 1.0 - tolerance + } } diff --git a/Tests/VRMSceneKitTests/VRMSceneKitTests.swift b/Tests/VRMSceneKitTests/VRMSceneKitTests.swift index 9adf009..b93d73a 100644 --- a/Tests/VRMSceneKitTests/VRMSceneKitTests.swift +++ b/Tests/VRMSceneKitTests/VRMSceneKitTests.swift @@ -43,10 +43,26 @@ class VRMSceneKitTests: XCTestCase { XCTAssertEqual(round(node.blendShape(for: .preset(.joy)) * 100), 85) } + func testVRM0MaterialsKeepConstantLighting() { + let loader = loadVRMLoader() + let materialCount = loader.vrm.gltf.jsonData.materials?.count ?? 0 + XCTAssertGreaterThan(materialCount, 0) + + for index in 0.. VRMNode { + let loader = loadVRMLoader() + return try! loader.loadScene().vrmNode + } + + func loadVRMLoader() -> VRMSceneLoader { let url = Bundle.module.url(forResource: "AliciaSolid", withExtension: "vrm")! let data = try! Data(contentsOf: url) - let loader = try! VRMSceneLoader(withData: data) - return try! loader.loadScene().vrmNode + return try! VRMSceneLoader(withData: data) } }