Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c8d5da7
vrm1.0 support
tatsuya-ogawa Jan 26, 2026
6c880bd
Update Example/VisionExample/VisionExampleApp.swift
tatsuya-ogawa Jan 31, 2026
85a140f
displayName
tatsuya-ogawa Jan 31, 2026
b6a0b7f
displayName
tatsuya-ogawa Jan 31, 2026
04b327f
Update Example/VisionExample/ContentView.swift
tatsuya-ogawa Jan 31, 2026
7e8607f
materialName
tatsuya-ogawa Jan 31, 2026
0ea686a
SpringBone
tatsuya-ogawa Jan 31, 2026
445e5f6
revert Sources/VRMKit/VRM/Node.swift
tatsuya-ogawa Jan 31, 2026
b66fe1c
revert Sources/VRMKit/VRM/VRM1.swift
tatsuya-ogawa Jan 31, 2026
6371170
Revert "revert Sources/VRMKit/VRM/VRM1.swift"
tatsuya-ogawa Jan 31, 2026
51d4ff6
Revert "revert Sources/VRMKit/VRM/Node.swift"
tatsuya-ogawa Jan 31, 2026
4b5e8fd
fix test
tatsuya-ogawa Jan 31, 2026
58c01fa
fix BlendShape
tatsuya-ogawa Feb 1, 2026
c058dd8
Merge branch 'main' of github.com:tatsuya-ogawa/VRMKit into feature/v…
tatsuya-ogawa Feb 2, 2026
d8b23bd
fix comment
tatsuya-ogawa Feb 2, 2026
8b9b966
remove comments
tatsuya-ogawa Feb 2, 2026
d1cb5f3
fix comment
tatsuya-ogawa Feb 2, 2026
9451a52
mac example
tatsuya-ogawa Feb 2, 2026
dee4292
add testcase
tatsuya-ogawa Feb 2, 2026
e51ea67
remove unnecessary comment
tatsuya-ogawa Feb 2, 2026
84d9113
SwiftTesting
tatsuya-ogawa Feb 3, 2026
6abaf87
revert VRM1Tests.swift
tatsuya-ogawa Feb 3, 2026
9dd2db8
Revert "revert VRM1Tests.swift"
tatsuya-ogawa Feb 3, 2026
9876270
refactor testcase
tatsuya-ogawa Feb 3, 2026
4e407c4
allowedUserName
tatsuya-ogawa Feb 3, 2026
775d164
texture transform
tatsuya-ogawa Feb 3, 2026
cfb1b65
fix build error
tatsuya-ogawa Feb 3, 2026
141fb79
add example build pipeline
tatsuya-ogawa Feb 3, 2026
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
6 changes: 6 additions & 0 deletions Example/Example.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,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 +63,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 +149,7 @@
isa = PBXGroup;
children = (
06E116512F277D1700D74CA4 /* AliciaSolid.vrm */,
AF000001AF000001AF000001 /* VRM1_Constraint_Twist_Sample.vrm */,
);
path = Models;
sourceTree = "<group>";
Expand Down Expand Up @@ -340,6 +344,7 @@
buildActionMask = 2147483647;
files = (
06E116522F277D1800D74CA4 /* AliciaSolid.vrm in Resources */,
AF000002AF000002AF000002 /* VRM1_Constraint_Twist_Sample.vrm in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -355,6 +360,7 @@
buildActionMask = 2147483647;
files = (
06E116542F277ED600D74CA4 /* AliciaSolid.vrm in Resources */,
AF000003AF000003AF000003 /* VRM1_Constraint_Twist_Sample.vrm in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
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
}
}
92 changes: 90 additions & 2 deletions Example/Example/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,45 @@ 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 +54,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
50 changes: 44 additions & 6 deletions Example/MacExample/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,23 @@ internal import Combine

struct ContentView: View {
@State private var viewModel = ContentViewModel()
@State private var selectedModel: MacExampleModel = .alicia

var body: some View {
VStack {
Picker("Model", selection: $selectedModel) {
ForEach(MacExampleModel.allCases) { model in
Text(model.displayName).tag(model)
}
}
.pickerStyle(.segmented)
.padding([.top, .horizontal])

RealityView { content in
content.add(viewModel.rootEntity)
}
.task {
await viewModel.loadEntity()
.task(id: selectedModel) {
await viewModel.loadEntity(model: selectedModel)
}
.onReceive(viewModel.updateTimer) { _ in
viewModel.update()
Expand All @@ -44,16 +53,22 @@ final class ContentViewModel {
private var vrmEntity: VRMEntity?
private var time: TimeInterval = 0
private var lastUpdateTime: Date?
private var currentModel: MacExampleModel = .alicia

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

func loadEntity() async {
func loadEntity(model: MacExampleModel) async {
do {
let loader = try VRMEntityLoader(named: "AliciaSolid.vrm")
if let vrmEntity {
vrmEntity.entity.removeFromParent()
self.vrmEntity = nil
}

let loader = try VRMEntityLoader(named: model.rawValue)
let vrmEntity = try loader.loadEntity()

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

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

self.vrmEntity = vrmEntity
self.currentModel = model
self.lastUpdateTime = Date()
} catch {
errorMessage = error.localizedDescription
Expand Down Expand Up @@ -102,11 +118,33 @@ final class ContentViewModel {
angle = -0.5 + 0.5 * progress
}

vrmEntity.entity.transform.rotation = simd_quatf(angle: .pi + angle, axis: SIMD3<Float>(0, 1, 0))
vrmEntity.entity.transform.rotation = simd_quatf(angle: currentModel.initialRotation + angle,
axis: SIMD3<Float>(0, 1, 0))
vrmEntity.update(at: deltaTime)
}
}

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
}
}
}

#Preview {
ContentView()
}
Loading
Loading