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:
@@ -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
```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)
}
}