Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,13 @@ jobs:
RUNTIME_PLATFORM="${{ matrix.platform }}" \
DEVICE_NAME="${{ matrix.device_name }}" | xcbeautify
fi

build-examples:
name: Build examples (iOS/visionOS/WatchOS/macOS)
runs-on: macos-26
steps:
- uses: actions/checkout@v4
- name: Build All Examples
run: |
set -o pipefail
make build-examples | xcbeautify
14 changes: 14 additions & 0 deletions Example/Example.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
objects = {

/* Begin PBXBuildFile section */
062780292F320E6B00FFBDAC /* AliciaSolid.vrm in Resources */ = {isa = PBXBuildFile; fileRef = 06E116512F277D1700D74CA4 /* AliciaSolid.vrm */; };
0627802A2F320F4800FFBDAC /* VRM1_Constraint_Twist_Sample.vrm in Resources */ = {isa = PBXBuildFile; fileRef = AF000001AF000001AF000001 /* VRM1_Constraint_Twist_Sample.vrm */; };
0627802B2F320F6D00FFBDAC /* AliciaSolid.vrm in Resources */ = {isa = PBXBuildFile; fileRef = 06E116512F277D1700D74CA4 /* AliciaSolid.vrm */; };
0627802C2F320F6F00FFBDAC /* VRM1_Constraint_Twist_Sample.vrm in Resources */ = {isa = PBXBuildFile; fileRef = AF000001AF000001AF000001 /* VRM1_Constraint_Twist_Sample.vrm */; };
067C5C502AAD6D8B00F8FBB3 /* VRMKit in Frameworks */ = {isa = PBXBuildFile; productRef = 067C5C4F2AAD6D8B00F8FBB3 /* VRMKit */; };
067C5C522AAD6D8B00F8FBB3 /* VRMSceneKit in Frameworks */ = {isa = PBXBuildFile; productRef = 067C5C512AAD6D8B00F8FBB3 /* VRMSceneKit */; };
06E1164E2F277C6D00D74CA4 /* VRMKit in Frameworks */ = {isa = PBXBuildFile; productRef = 06E1164D2F277C6D00D74CA4 /* VRMKit */; };
Expand All @@ -18,6 +22,8 @@
06F0BD812AAD82120089488C /* VRMSceneKit in Frameworks */ = {isa = PBXBuildFile; productRef = 06F0BD802AAD82120089488C /* VRMSceneKit */; };
9FD10CB0B1951449373631D5 /* VRMRealityKit in Frameworks */ = {isa = PBXBuildFile; productRef = A1B2C3D4E5F60718293A4B5F /* VRMRealityKit */; };
A1B2C3D4E5F60718293A4B60 /* VRMRealityKit in Frameworks */ = {isa = PBXBuildFile; productRef = A1B2C3D4E5F60718293A4B5F /* VRMRealityKit */; };
AF000002AF000002AF000002 /* VRM1_Constraint_Twist_Sample.vrm in Resources */ = {isa = PBXBuildFile; fileRef = AF000001AF000001AF000001 /* VRM1_Constraint_Twist_Sample.vrm */; };
AF000003AF000003AF000003 /* VRM1_Constraint_Twist_Sample.vrm in Resources */ = {isa = PBXBuildFile; fileRef = AF000001AF000001AF000001 /* VRM1_Constraint_Twist_Sample.vrm */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -61,6 +67,7 @@
06F0BD6C2AAD81A30089488C /* WatchExample Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "WatchExample Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; };
245725D72146F47A003AA5D7 /* VRMExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VRMExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
96FABF60E748F3EF7D574461 /* VisionExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VisionExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
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>"; };
/* End PBXFileReference section */

/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
Expand Down Expand Up @@ -146,6 +153,7 @@
isa = PBXGroup;
children = (
06E116512F277D1700D74CA4 /* AliciaSolid.vrm */,
AF000001AF000001AF000001 /* VRM1_Constraint_Twist_Sample.vrm */,
);
path = Models;
sourceTree = "<group>";
Expand Down Expand Up @@ -340,13 +348,16 @@
buildActionMask = 2147483647;
files = (
06E116522F277D1800D74CA4 /* AliciaSolid.vrm in Resources */,
AF000002AF000002AF000002 /* VRM1_Constraint_Twist_Sample.vrm in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
06F0BD6A2AAD81A30089488C /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
062780292F320E6B00FFBDAC /* AliciaSolid.vrm in Resources */,
0627802A2F320F4800FFBDAC /* VRM1_Constraint_Twist_Sample.vrm in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -355,13 +366,16 @@
buildActionMask = 2147483647;
files = (
06E116542F277ED600D74CA4 /* AliciaSolid.vrm in Resources */,
AF000003AF000003AF000003 /* VRM1_Constraint_Twist_Sample.vrm in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
A9A0A8D44BAC027EB35025C8 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0627802B2F320F6D00FFBDAC /* AliciaSolid.vrm in Resources */,
0627802C2F320F6F00FFBDAC /* VRM1_Constraint_Twist_Sample.vrm in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
2 changes: 2 additions & 0 deletions Example/Example/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,7 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIUserInterfaceStyle</key>
<string>Dark</string>
</dict>
</plist>
73 changes: 68 additions & 5 deletions Example/Example/RealityKitViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ final class RealityKitViewController: UIViewController, UIGestureRecognizerDeleg
private var orbitPitch: Float = -0.1
private var orbitDistance: Float = 2
private var orbitTarget = SIMD3<Float>(0, 0.8, 0)
private var currentExpression: RKExpression = .neutral

override func viewDidLoad() {
super.viewDidLoad()
title = "RealityKit"
view.backgroundColor = .black
setUpARView()
loadVRM()
setUpUI()
loadVRM(model: .alicia)
}

private func setUpARView() {
Expand All @@ -42,11 +44,51 @@ final class RealityKitViewController: UIViewController, UIGestureRecognizerDeleg
setUpGestures()
}

private func loadVRM() {
private func setUpUI() {
let items = VRMExampleModel.allCases.map { $0.displayName }
let segmentedControl = UISegmentedControl(items: items)
segmentedControl.selectedSegmentIndex = 0
segmentedControl.addTarget(self, action: #selector(segmentChanged(_:)), for: .valueChanged)
segmentedControl.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(segmentedControl)

let expressionItems = RKExpression.allCases.map { $0.displayName }
let expressionSegmentedControl = UISegmentedControl(items: expressionItems)
expressionSegmentedControl.selectedSegmentIndex = 0
expressionSegmentedControl.addTarget(self, action: #selector(expressionSegmentChanged(_:)), for: .valueChanged)
expressionSegmentedControl.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(expressionSegmentedControl)

NSLayoutConstraint.activate([
segmentedControl.centerXAnchor.constraint(equalTo: view.centerXAnchor),
segmentedControl.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -50),
expressionSegmentedControl.centerXAnchor.constraint(equalTo: view.centerXAnchor),
expressionSegmentedControl.bottomAnchor.constraint(equalTo: segmentedControl.topAnchor, constant: -20)
])
}

@objc private func segmentChanged(_ sender: UISegmentedControl) {
let model = VRMExampleModel.allCases[sender.selectedSegmentIndex]
loadVRM(model: model)
}

@objc private func expressionSegmentChanged(_ sender: UISegmentedControl) {
let expression = RKExpression.allCases[sender.selectedSegmentIndex]
loadedEntity?.setBlendShape(value: 0.0, for: .preset(currentExpression.preset))
currentExpression = expression
loadedEntity?.setBlendShape(value: 1.0, for: .preset(currentExpression.preset))
}

private func loadVRM(model: VRMExampleModel) {
guard let arView = arView else { return }

if let loadedEntity = loadedEntity {
loadedEntity.entity.removeFromParent()
self.loadedEntity = nil
}

do {
let loader = try VRMEntityLoader(named: "AliciaSolid.vrm")
let loader = try VRMEntityLoader(named: model.rawValue)
let vrmEntity = try loader.loadEntity()

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

loadedEntity = vrmEntity

let rotationOffset = model.initialRotation

var time: TimeInterval = 0
updateSubscription = arView.scene.subscribe(to: SceneEvents.Update.self) { [weak self] event in
guard let loadedEntity = self?.loadedEntity else { return }
Expand All @@ -92,7 +136,7 @@ final class RealityKitViewController: UIViewController, UIGestureRecognizerDeleg
angle = -0.5 + 0.5 * progress
}

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

loadedEntity.update(at: event.deltaTime)
}
Expand Down Expand Up @@ -205,3 +249,22 @@ final class RealityKitViewController: UIViewController, UIGestureRecognizerDeleg
return true
}
}

@available(iOS 18.0, *)
private enum RKExpression: 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
}
}
93 changes: 90 additions & 3 deletions Example/Example/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,45 @@ import SceneKit
internal import VRMKit
internal import VRMSceneKit

@available(*, deprecated, message: "Deprecated. Use VRMRealityKit instead.")
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! {
Expand All @@ -15,17 +53,66 @@ class ViewController: UIViewController {
}
}

private var vrmNode: VRMNode?
private var currentExpression: Expression = .neutral

override func viewDidLoad() {
super.viewDidLoad()
setupUI()
loadVRM(model: .alicia)
}

private func setupUI() {
let items = VRMExampleModel.allCases.map { $0.displayName }
let segmentedControl = UISegmentedControl(items: items)
segmentedControl.selectedSegmentIndex = 0
segmentedControl.addTarget(self, action: #selector(segmentChanged(_:)), for: .valueChanged)
segmentedControl.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(segmentedControl)

let expressionItems = Expression.allCases.map { $0.displayName }
let expressionSegmentedControl = UISegmentedControl(items: expressionItems)
expressionSegmentedControl.selectedSegmentIndex = 0
expressionSegmentedControl.addTarget(self, action: #selector(expressionSegmentChanged(_:)), for: .valueChanged)
expressionSegmentedControl.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(expressionSegmentedControl)

NSLayoutConstraint.activate([
segmentedControl.centerXAnchor.constraint(equalTo: view.centerXAnchor),
segmentedControl.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -50),

expressionSegmentedControl.centerXAnchor.constraint(equalTo: view.centerXAnchor),
expressionSegmentedControl.bottomAnchor.constraint(equalTo: segmentedControl.topAnchor, constant: -20)
])
}

@objc private func segmentChanged(_ sender: UISegmentedControl) {
let model = VRMExampleModel.allCases[sender.selectedSegmentIndex]
loadVRM(model: model)
}

@objc private func expressionSegmentChanged(_ sender: UISegmentedControl) {
let expression = Expression.allCases[sender.selectedSegmentIndex]
vrmNode?.setBlendShape(value: 0.0, for: .preset(currentExpression.preset))
currentExpression = expression
vrmNode?.setBlendShape(value: 1.0, for: .preset(currentExpression.preset))
}

private func loadVRM(model: VRMExampleModel) {
do {
let loader = try VRMSceneLoader(named: "AliciaSolid.vrm")
let loader = try VRMSceneLoader(named: model.rawValue)
let scene = try loader.loadScene()
setupScene(scene)
scnView.scene = scene
scnView.delegate = self
let node = scene.vrmNode
node.setBlendShape(value: 1.0, for: .custom("><"))
self.vrmNode = node

let rotationOffset = CGFloat(model.initialRotation)
node.eulerAngles = SCNVector3(0, rotationOffset, 0)

node.setBlendShape(value: 1.0, for: .preset(currentExpression.preset))

node.humanoid.node(for: .neck)?.eulerAngles = SCNVector3(0, 0, 20 * CGFloat.pi / 180)
node.humanoid.node(for: .leftShoulder)?.eulerAngles = SCNVector3(0, 0, 40 * CGFloat.pi / 180)
node.humanoid.node(for: .rightShoulder)?.eulerAngles = SCNVector3(0, 0, 40 * CGFloat.pi / 180)
Expand Down
Loading
Loading