Skip to content

Commit 4e4f1c1

Browse files
tattntatsuya-ogawagemini-code-assist[bot]
authored
Merge feature/vrm1.0 (#42)
* vrm1.0 support (#40) * vrm1.0 support * Update Example/VisionExample/VisionExampleApp.swift Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * displayName * displayName * Update Example/VisionExample/ContentView.swift Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * materialName * SpringBone * revert Sources/VRMKit/VRM/Node.swift * revert Sources/VRMKit/VRM/VRM1.swift * Revert "revert Sources/VRMKit/VRM/VRM1.swift" This reverts commit b66fe1c. * Revert "revert Sources/VRMKit/VRM/Node.swift" This reverts commit 445e5f6. * fix test * fix BlendShape * fix comment * remove comments * fix comment * mac example * add testcase * remove unnecessary comment * SwiftTesting * revert VRM1Tests.swift * Revert "revert VRM1Tests.swift" This reverts commit 6abaf87. * refactor testcase * allowedUserName * texture transform * fix build error * add example build pipeline --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * feat: VRM files to example project resources * Flip the model direction for visionOS example Co-authored-by: tatsuya-ogawa <tatsuya-ogawa@users.noreply.github.com> * New interfaces - Updated VRM.MaterialProperty to VRM0.MaterialProperty for consistency. - Removed VRMFileProtocol as it is no longer needed. - Migrated VRM1 structures to VRM0 equivalents in VRMMigration.swift. - Adjusted VRMLoader to support loading VRM0 thumbnails. - Modified Humanoid and SecondaryAnimation classes to use VRM0 types. - Updated tests to validate VRM0 functionality and migration from VRM1. - Removed deprecated VRM1SceneLoader and related methods. - Added new tests for VRM0 to ensure proper functionality and migration. * feat: Add UIUserInterfaceStyle key for dark mode support in Info.plist * feat: Improve shader handling for MToon and Unlit variants in VRMEntityLoader #42 (comment) --------- Co-authored-by: tatsuya-ogawa <tatsuya-ogawa@users.noreply.github.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent c59c6f6 commit 4e4f1c1

35 files changed

+1627
-455
lines changed

.github/workflows/ci.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,13 @@ jobs:
4646
RUNTIME_PLATFORM="${{ matrix.platform }}" \
4747
DEVICE_NAME="${{ matrix.device_name }}" | xcbeautify
4848
fi
49+
50+
build-examples:
51+
name: Build examples (iOS/visionOS/WatchOS/macOS)
52+
runs-on: macos-26
53+
steps:
54+
- uses: actions/checkout@v4
55+
- name: Build All Examples
56+
run: |
57+
set -o pipefail
58+
make build-examples | xcbeautify

Example/Example.xcodeproj/project.pbxproj

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
062780292F320E6B00FFBDAC /* AliciaSolid.vrm in Resources */ = {isa = PBXBuildFile; fileRef = 06E116512F277D1700D74CA4 /* AliciaSolid.vrm */; };
11+
0627802A2F320F4800FFBDAC /* VRM1_Constraint_Twist_Sample.vrm in Resources */ = {isa = PBXBuildFile; fileRef = AF000001AF000001AF000001 /* VRM1_Constraint_Twist_Sample.vrm */; };
12+
0627802B2F320F6D00FFBDAC /* AliciaSolid.vrm in Resources */ = {isa = PBXBuildFile; fileRef = 06E116512F277D1700D74CA4 /* AliciaSolid.vrm */; };
13+
0627802C2F320F6F00FFBDAC /* VRM1_Constraint_Twist_Sample.vrm in Resources */ = {isa = PBXBuildFile; fileRef = AF000001AF000001AF000001 /* VRM1_Constraint_Twist_Sample.vrm */; };
1014
067C5C502AAD6D8B00F8FBB3 /* VRMKit in Frameworks */ = {isa = PBXBuildFile; productRef = 067C5C4F2AAD6D8B00F8FBB3 /* VRMKit */; };
1115
067C5C522AAD6D8B00F8FBB3 /* VRMSceneKit in Frameworks */ = {isa = PBXBuildFile; productRef = 067C5C512AAD6D8B00F8FBB3 /* VRMSceneKit */; };
1216
06E1164E2F277C6D00D74CA4 /* VRMKit in Frameworks */ = {isa = PBXBuildFile; productRef = 06E1164D2F277C6D00D74CA4 /* VRMKit */; };
@@ -18,6 +22,8 @@
1822
06F0BD812AAD82120089488C /* VRMSceneKit in Frameworks */ = {isa = PBXBuildFile; productRef = 06F0BD802AAD82120089488C /* VRMSceneKit */; };
1923
9FD10CB0B1951449373631D5 /* VRMRealityKit in Frameworks */ = {isa = PBXBuildFile; productRef = A1B2C3D4E5F60718293A4B5F /* VRMRealityKit */; };
2024
A1B2C3D4E5F60718293A4B60 /* VRMRealityKit in Frameworks */ = {isa = PBXBuildFile; productRef = A1B2C3D4E5F60718293A4B5F /* VRMRealityKit */; };
25+
AF000002AF000002AF000002 /* VRM1_Constraint_Twist_Sample.vrm in Resources */ = {isa = PBXBuildFile; fileRef = AF000001AF000001AF000001 /* VRM1_Constraint_Twist_Sample.vrm */; };
26+
AF000003AF000003AF000003 /* VRM1_Constraint_Twist_Sample.vrm in Resources */ = {isa = PBXBuildFile; fileRef = AF000001AF000001AF000001 /* VRM1_Constraint_Twist_Sample.vrm */; };
2127
/* End PBXBuildFile section */
2228

2329
/* Begin PBXContainerItemProxy section */
@@ -61,6 +67,7 @@
6167
06F0BD6C2AAD81A30089488C /* WatchExample Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "WatchExample Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; };
6268
245725D72146F47A003AA5D7 /* VRMExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VRMExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
6369
96FABF60E748F3EF7D574461 /* VisionExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VisionExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
70+
AF000001AF000001AF000001 /* VRM1_Constraint_Twist_Sample.vrm */ = {isa = PBXFileReference; lastKnownFileType = file; name = VRM1_Constraint_Twist_Sample.vrm; path = ../../Tests/VRMKitTests/Assets/VRM1_Constraint_Twist_Sample.vrm; sourceTree = "<group>"; };
6471
/* End PBXFileReference section */
6572

6673
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
@@ -146,6 +153,7 @@
146153
isa = PBXGroup;
147154
children = (
148155
06E116512F277D1700D74CA4 /* AliciaSolid.vrm */,
156+
AF000001AF000001AF000001 /* VRM1_Constraint_Twist_Sample.vrm */,
149157
);
150158
path = Models;
151159
sourceTree = "<group>";
@@ -340,13 +348,16 @@
340348
buildActionMask = 2147483647;
341349
files = (
342350
06E116522F277D1800D74CA4 /* AliciaSolid.vrm in Resources */,
351+
AF000002AF000002AF000002 /* VRM1_Constraint_Twist_Sample.vrm in Resources */,
343352
);
344353
runOnlyForDeploymentPostprocessing = 0;
345354
};
346355
06F0BD6A2AAD81A30089488C /* Resources */ = {
347356
isa = PBXResourcesBuildPhase;
348357
buildActionMask = 2147483647;
349358
files = (
359+
062780292F320E6B00FFBDAC /* AliciaSolid.vrm in Resources */,
360+
0627802A2F320F4800FFBDAC /* VRM1_Constraint_Twist_Sample.vrm in Resources */,
350361
);
351362
runOnlyForDeploymentPostprocessing = 0;
352363
};
@@ -355,13 +366,16 @@
355366
buildActionMask = 2147483647;
356367
files = (
357368
06E116542F277ED600D74CA4 /* AliciaSolid.vrm in Resources */,
369+
AF000003AF000003AF000003 /* VRM1_Constraint_Twist_Sample.vrm in Resources */,
358370
);
359371
runOnlyForDeploymentPostprocessing = 0;
360372
};
361373
A9A0A8D44BAC027EB35025C8 /* Resources */ = {
362374
isa = PBXResourcesBuildPhase;
363375
buildActionMask = 2147483647;
364376
files = (
377+
0627802B2F320F6D00FFBDAC /* AliciaSolid.vrm in Resources */,
378+
0627802C2F320F6F00FFBDAC /* VRM1_Constraint_Twist_Sample.vrm in Resources */,
365379
);
366380
runOnlyForDeploymentPostprocessing = 0;
367381
};

Example/Example/Info.plist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,7 @@
3939
<string>UIInterfaceOrientationLandscapeLeft</string>
4040
<string>UIInterfaceOrientationLandscapeRight</string>
4141
</array>
42+
<key>UIUserInterfaceStyle</key>
43+
<string>Dark</string>
4244
</dict>
4345
</plist>

Example/Example/RealityKitViewController.swift

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@ final class RealityKitViewController: UIViewController, UIGestureRecognizerDeleg
1515
private var orbitPitch: Float = -0.1
1616
private var orbitDistance: Float = 2
1717
private var orbitTarget = SIMD3<Float>(0, 0.8, 0)
18+
private var currentExpression: RKExpression = .neutral
1819

1920
override func viewDidLoad() {
2021
super.viewDidLoad()
2122
title = "RealityKit"
2223
view.backgroundColor = .black
2324
setUpARView()
24-
loadVRM()
25+
setUpUI()
26+
loadVRM(model: .alicia)
2527
}
2628

2729
private func setUpARView() {
@@ -42,11 +44,51 @@ final class RealityKitViewController: UIViewController, UIGestureRecognizerDeleg
4244
setUpGestures()
4345
}
4446

45-
private func loadVRM() {
47+
private func setUpUI() {
48+
let items = VRMExampleModel.allCases.map { $0.displayName }
49+
let segmentedControl = UISegmentedControl(items: items)
50+
segmentedControl.selectedSegmentIndex = 0
51+
segmentedControl.addTarget(self, action: #selector(segmentChanged(_:)), for: .valueChanged)
52+
segmentedControl.translatesAutoresizingMaskIntoConstraints = false
53+
view.addSubview(segmentedControl)
54+
55+
let expressionItems = RKExpression.allCases.map { $0.displayName }
56+
let expressionSegmentedControl = UISegmentedControl(items: expressionItems)
57+
expressionSegmentedControl.selectedSegmentIndex = 0
58+
expressionSegmentedControl.addTarget(self, action: #selector(expressionSegmentChanged(_:)), for: .valueChanged)
59+
expressionSegmentedControl.translatesAutoresizingMaskIntoConstraints = false
60+
view.addSubview(expressionSegmentedControl)
61+
62+
NSLayoutConstraint.activate([
63+
segmentedControl.centerXAnchor.constraint(equalTo: view.centerXAnchor),
64+
segmentedControl.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -50),
65+
expressionSegmentedControl.centerXAnchor.constraint(equalTo: view.centerXAnchor),
66+
expressionSegmentedControl.bottomAnchor.constraint(equalTo: segmentedControl.topAnchor, constant: -20)
67+
])
68+
}
69+
70+
@objc private func segmentChanged(_ sender: UISegmentedControl) {
71+
let model = VRMExampleModel.allCases[sender.selectedSegmentIndex]
72+
loadVRM(model: model)
73+
}
74+
75+
@objc private func expressionSegmentChanged(_ sender: UISegmentedControl) {
76+
let expression = RKExpression.allCases[sender.selectedSegmentIndex]
77+
loadedEntity?.setBlendShape(value: 0.0, for: .preset(currentExpression.preset))
78+
currentExpression = expression
79+
loadedEntity?.setBlendShape(value: 1.0, for: .preset(currentExpression.preset))
80+
}
81+
82+
private func loadVRM(model: VRMExampleModel) {
4683
guard let arView = arView else { return }
4784

85+
if let loadedEntity = loadedEntity {
86+
loadedEntity.entity.removeFromParent()
87+
self.loadedEntity = nil
88+
}
89+
4890
do {
49-
let loader = try VRMEntityLoader(named: "AliciaSolid.vrm")
91+
let loader = try VRMEntityLoader(named: model.rawValue)
5092
let vrmEntity = try loader.loadEntity()
5193

5294
let anchor = AnchorEntity(world: .zero)
@@ -72,10 +114,12 @@ final class RealityKitViewController: UIViewController, UIGestureRecognizerDeleg
72114
if let rightShoulder {
73115
rightShoulder.transform.rotation = rightShoulder.transform.rotation * shoulderRotation
74116
}
75-
vrmEntity.setBlendShape(value: 1.0, for: .custom("><"))
117+
vrmEntity.setBlendShape(value: 1.0, for: .preset(currentExpression.preset))
76118

77119
loadedEntity = vrmEntity
78120

121+
let rotationOffset = model.initialRotation
122+
79123
var time: TimeInterval = 0
80124
updateSubscription = arView.scene.subscribe(to: SceneEvents.Update.self) { [weak self] event in
81125
guard let loadedEntity = self?.loadedEntity else { return }
@@ -92,7 +136,7 @@ final class RealityKitViewController: UIViewController, UIGestureRecognizerDeleg
92136
angle = -0.5 + 0.5 * progress
93137
}
94138

95-
loadedEntity.entity.transform.rotation = simd_quatf(angle: angle, axis: SIMD3<Float>(0, 1, 0))
139+
loadedEntity.entity.transform.rotation = simd_quatf(angle: rotationOffset + angle, axis: SIMD3<Float>(0, 1, 0))
96140

97141
loadedEntity.update(at: event.deltaTime)
98142
}
@@ -205,3 +249,22 @@ final class RealityKitViewController: UIViewController, UIGestureRecognizerDeleg
205249
return true
206250
}
207251
}
252+
253+
@available(iOS 18.0, *)
254+
private enum RKExpression: String, CaseIterable {
255+
case neutral, joy, angry, sorrow, fun
256+
257+
var preset: BlendShapePreset {
258+
switch self {
259+
case .neutral: return .neutral
260+
case .joy: return .joy
261+
case .angry: return .angry
262+
case .sorrow: return .sorrow
263+
case .fun: return .fun
264+
}
265+
}
266+
267+
var displayName: String {
268+
return rawValue.capitalized
269+
}
270+
}

Example/Example/ViewController.swift

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,45 @@ import SceneKit
33
internal import VRMKit
44
internal import VRMSceneKit
55

6-
@available(*, deprecated, message: "Deprecated. Use VRMRealityKit instead.")
6+
enum VRMExampleModel: String, CaseIterable, Identifiable {
7+
case alicia = "AliciaSolid.vrm"
8+
case vrm1 = "VRM1_Constraint_Twist_Sample.vrm"
9+
10+
var id: String { rawValue }
11+
12+
var displayName: String {
13+
switch self {
14+
case .alicia: return "Alicia"
15+
case .vrm1: return "VRM 1.0"
16+
}
17+
}
18+
19+
var initialRotation: Float {
20+
switch self {
21+
case .alicia: return 0
22+
case .vrm1: return .pi
23+
}
24+
}
25+
}
26+
27+
enum Expression: String, CaseIterable {
28+
case neutral, joy, angry, sorrow, fun
29+
30+
var preset: BlendShapePreset {
31+
switch self {
32+
case .neutral: return .neutral
33+
case .joy: return .joy
34+
case .angry: return .angry
35+
case .sorrow: return .sorrow
36+
case .fun: return .fun
37+
}
38+
}
39+
40+
var displayName: String {
41+
return rawValue.capitalized
42+
}
43+
}
44+
745
class ViewController: UIViewController {
846

947
@IBOutlet private weak var scnView: SCNView! {
@@ -15,17 +53,66 @@ class ViewController: UIViewController {
1553
}
1654
}
1755

56+
private var vrmNode: VRMNode?
57+
private var currentExpression: Expression = .neutral
58+
1859
override func viewDidLoad() {
1960
super.viewDidLoad()
61+
setupUI()
62+
loadVRM(model: .alicia)
63+
}
64+
65+
private func setupUI() {
66+
let items = VRMExampleModel.allCases.map { $0.displayName }
67+
let segmentedControl = UISegmentedControl(items: items)
68+
segmentedControl.selectedSegmentIndex = 0
69+
segmentedControl.addTarget(self, action: #selector(segmentChanged(_:)), for: .valueChanged)
70+
segmentedControl.translatesAutoresizingMaskIntoConstraints = false
71+
view.addSubview(segmentedControl)
72+
73+
let expressionItems = Expression.allCases.map { $0.displayName }
74+
let expressionSegmentedControl = UISegmentedControl(items: expressionItems)
75+
expressionSegmentedControl.selectedSegmentIndex = 0
76+
expressionSegmentedControl.addTarget(self, action: #selector(expressionSegmentChanged(_:)), for: .valueChanged)
77+
expressionSegmentedControl.translatesAutoresizingMaskIntoConstraints = false
78+
view.addSubview(expressionSegmentedControl)
79+
80+
NSLayoutConstraint.activate([
81+
segmentedControl.centerXAnchor.constraint(equalTo: view.centerXAnchor),
82+
segmentedControl.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -50),
83+
84+
expressionSegmentedControl.centerXAnchor.constraint(equalTo: view.centerXAnchor),
85+
expressionSegmentedControl.bottomAnchor.constraint(equalTo: segmentedControl.topAnchor, constant: -20)
86+
])
87+
}
2088

89+
@objc private func segmentChanged(_ sender: UISegmentedControl) {
90+
let model = VRMExampleModel.allCases[sender.selectedSegmentIndex]
91+
loadVRM(model: model)
92+
}
93+
94+
@objc private func expressionSegmentChanged(_ sender: UISegmentedControl) {
95+
let expression = Expression.allCases[sender.selectedSegmentIndex]
96+
vrmNode?.setBlendShape(value: 0.0, for: .preset(currentExpression.preset))
97+
currentExpression = expression
98+
vrmNode?.setBlendShape(value: 1.0, for: .preset(currentExpression.preset))
99+
}
100+
101+
private func loadVRM(model: VRMExampleModel) {
21102
do {
22-
let loader = try VRMSceneLoader(named: "AliciaSolid.vrm")
103+
let loader = try VRMSceneLoader(named: model.rawValue)
23104
let scene = try loader.loadScene()
24105
setupScene(scene)
25106
scnView.scene = scene
26107
scnView.delegate = self
27108
let node = scene.vrmNode
28-
node.setBlendShape(value: 1.0, for: .custom("><"))
109+
self.vrmNode = node
110+
111+
let rotationOffset = CGFloat(model.initialRotation)
112+
node.eulerAngles = SCNVector3(0, rotationOffset, 0)
113+
114+
node.setBlendShape(value: 1.0, for: .preset(currentExpression.preset))
115+
29116
node.humanoid.node(for: .neck)?.eulerAngles = SCNVector3(0, 0, 20 * CGFloat.pi / 180)
30117
node.humanoid.node(for: .leftShoulder)?.eulerAngles = SCNVector3(0, 0, 40 * CGFloat.pi / 180)
31118
node.humanoid.node(for: .rightShoulder)?.eulerAngles = SCNVector3(0, 0, 40 * CGFloat.pi / 180)

0 commit comments

Comments
 (0)