Skip to content

Commit f4e0f8c

Browse files
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>
1 parent a747111 commit f4e0f8c

File tree

20 files changed

+1361
-198
lines changed

20 files changed

+1361
-198
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: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
06F0BD812AAD82120089488C /* VRMSceneKit in Frameworks */ = {isa = PBXBuildFile; productRef = 06F0BD802AAD82120089488C /* VRMSceneKit */; };
1919
9FD10CB0B1951449373631D5 /* VRMRealityKit in Frameworks */ = {isa = PBXBuildFile; productRef = A1B2C3D4E5F60718293A4B5F /* VRMRealityKit */; };
2020
A1B2C3D4E5F60718293A4B60 /* VRMRealityKit in Frameworks */ = {isa = PBXBuildFile; productRef = A1B2C3D4E5F60718293A4B5F /* VRMRealityKit */; };
21+
AF000002AF000002AF000002 /* VRM1_Constraint_Twist_Sample.vrm in Resources */ = {isa = PBXBuildFile; fileRef = AF000001AF000001AF000001 /* VRM1_Constraint_Twist_Sample.vrm */; };
22+
AF000003AF000003AF000003 /* VRM1_Constraint_Twist_Sample.vrm in Resources */ = {isa = PBXBuildFile; fileRef = AF000001AF000001AF000001 /* VRM1_Constraint_Twist_Sample.vrm */; };
2123
/* End PBXBuildFile section */
2224

2325
/* Begin PBXContainerItemProxy section */
@@ -61,6 +63,7 @@
6163
06F0BD6C2AAD81A30089488C /* WatchExample Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "WatchExample Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; };
6264
245725D72146F47A003AA5D7 /* VRMExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VRMExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
6365
96FABF60E748F3EF7D574461 /* VisionExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VisionExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
66+
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>"; };
6467
/* End PBXFileReference section */
6568

6669
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
@@ -146,6 +149,7 @@
146149
isa = PBXGroup;
147150
children = (
148151
06E116512F277D1700D74CA4 /* AliciaSolid.vrm */,
152+
AF000001AF000001AF000001 /* VRM1_Constraint_Twist_Sample.vrm */,
149153
);
150154
path = Models;
151155
sourceTree = "<group>";
@@ -340,6 +344,7 @@
340344
buildActionMask = 2147483647;
341345
files = (
342346
06E116522F277D1800D74CA4 /* AliciaSolid.vrm in Resources */,
347+
AF000002AF000002AF000002 /* VRM1_Constraint_Twist_Sample.vrm in Resources */,
343348
);
344349
runOnlyForDeploymentPostprocessing = 0;
345350
};
@@ -355,6 +360,7 @@
355360
buildActionMask = 2147483647;
356361
files = (
357362
06E116542F277ED600D74CA4 /* AliciaSolid.vrm in Resources */,
363+
AF000003AF000003AF000003 /* VRM1_Constraint_Twist_Sample.vrm in Resources */,
358364
);
359365
runOnlyForDeploymentPostprocessing = 0;
360366
};

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 & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,45 @@ internal import VRMKit
44
internal import VRMSceneKit
55

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

948
@IBOutlet private weak var scnView: SCNView! {
@@ -15,17 +54,66 @@ class ViewController: UIViewController {
1554
}
1655
}
1756

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

90+
@objc private func segmentChanged(_ sender: UISegmentedControl) {
91+
let model = VRMExampleModel.allCases[sender.selectedSegmentIndex]
92+
loadVRM(model: model)
93+
}
94+
95+
@objc private func expressionSegmentChanged(_ sender: UISegmentedControl) {
96+
let expression = Expression.allCases[sender.selectedSegmentIndex]
97+
vrmNode?.setBlendShape(value: 0.0, for: .preset(currentExpression.preset))
98+
currentExpression = expression
99+
vrmNode?.setBlendShape(value: 1.0, for: .preset(currentExpression.preset))
100+
}
101+
102+
private func loadVRM(model: VRMExampleModel) {
21103
do {
22-
let loader = try VRMSceneLoader(named: "AliciaSolid.vrm")
104+
let loader = try VRMSceneLoader(named: model.rawValue)
23105
let scene = try loader.loadScene()
24106
setupScene(scene)
25107
scnView.scene = scene
26108
scnView.delegate = self
27109
let node = scene.vrmNode
28-
node.setBlendShape(value: 1.0, for: .custom("><"))
110+
self.vrmNode = node
111+
112+
let rotationOffset = CGFloat(model.initialRotation)
113+
node.eulerAngles = SCNVector3(0, rotationOffset, 0)
114+
115+
node.setBlendShape(value: 1.0, for: .preset(currentExpression.preset))
116+
29117
node.humanoid.node(for: .neck)?.eulerAngles = SCNVector3(0, 0, 20 * CGFloat.pi / 180)
30118
node.humanoid.node(for: .leftShoulder)?.eulerAngles = SCNVector3(0, 0, 40 * CGFloat.pi / 180)
31119
node.humanoid.node(for: .rightShoulder)?.eulerAngles = SCNVector3(0, 0, 40 * CGFloat.pi / 180)

Example/MacExample/ContentView.swift

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,23 @@ internal import Combine
1313

1414
struct ContentView: View {
1515
@State private var viewModel = ContentViewModel()
16+
@State private var selectedModel: MacExampleModel = .alicia
1617

1718
var body: some View {
1819
VStack {
20+
Picker("Model", selection: $selectedModel) {
21+
ForEach(MacExampleModel.allCases) { model in
22+
Text(model.displayName).tag(model)
23+
}
24+
}
25+
.pickerStyle(.segmented)
26+
.padding([.top, .horizontal])
27+
1928
RealityView { content in
2029
content.add(viewModel.rootEntity)
2130
}
22-
.task {
23-
await viewModel.loadEntity()
31+
.task(id: selectedModel) {
32+
await viewModel.loadEntity(model: selectedModel)
2433
}
2534
.onReceive(viewModel.updateTimer) { _ in
2635
viewModel.update()
@@ -44,16 +53,22 @@ final class ContentViewModel {
4453
private var vrmEntity: VRMEntity?
4554
private var time: TimeInterval = 0
4655
private var lastUpdateTime: Date?
56+
private var currentModel: MacExampleModel = .alicia
4757

4858
let updateTimer = Timer.publish(every: 1.0 / 60.0, on: .main, in: .common).autoconnect()
4959

50-
func loadEntity() async {
60+
func loadEntity(model: MacExampleModel) async {
5161
do {
52-
let loader = try VRMEntityLoader(named: "AliciaSolid.vrm")
62+
if let vrmEntity {
63+
vrmEntity.entity.removeFromParent()
64+
self.vrmEntity = nil
65+
}
66+
67+
let loader = try VRMEntityLoader(named: model.rawValue)
5368
let vrmEntity = try loader.loadEntity()
5469

5570
vrmEntity.entity.transform.translation = SIMD3<Float>(0, -1, 0)
56-
vrmEntity.entity.transform.rotation = simd_quatf(angle: .pi, axis: SIMD3<Float>(0, 1, 0))
71+
vrmEntity.entity.transform.rotation = simd_quatf(angle: model.initialRotation, axis: SIMD3<Float>(0, 1, 0))
5772
rootEntity.addChild(vrmEntity.entity)
5873

5974
// Adjust pose
@@ -75,6 +90,7 @@ final class ContentViewModel {
7590
vrmEntity.setBlendShape(value: 1.0, for: .custom("><"))
7691

7792
self.vrmEntity = vrmEntity
93+
self.currentModel = model
7894
self.lastUpdateTime = Date()
7995
} catch {
8096
errorMessage = error.localizedDescription
@@ -102,11 +118,33 @@ final class ContentViewModel {
102118
angle = -0.5 + 0.5 * progress
103119
}
104120

105-
vrmEntity.entity.transform.rotation = simd_quatf(angle: .pi + angle, axis: SIMD3<Float>(0, 1, 0))
121+
vrmEntity.entity.transform.rotation = simd_quatf(angle: currentModel.initialRotation + angle,
122+
axis: SIMD3<Float>(0, 1, 0))
106123
vrmEntity.update(at: deltaTime)
107124
}
108125
}
109126

127+
enum MacExampleModel: String, CaseIterable, Identifiable {
128+
case alicia = "AliciaSolid.vrm"
129+
case vrm1 = "VRM1_Constraint_Twist_Sample.vrm"
130+
131+
var id: String { rawValue }
132+
133+
var displayName: String {
134+
switch self {
135+
case .alicia: return "Alicia"
136+
case .vrm1: return "VRM 1.0"
137+
}
138+
}
139+
140+
var initialRotation: Float {
141+
switch self {
142+
case .alicia: return .pi
143+
case .vrm1: return 0
144+
}
145+
}
146+
}
147+
110148
#Preview {
111149
ContentView()
112150
}

0 commit comments

Comments
 (0)