Skip to content

Conversation

@tatsuya-ogawa
Copy link
Contributor

@tatsuya-ogawa tatsuya-ogawa commented Jan 26, 2026

VRM 1.0 Support & Migration Logic

Description

This PR introduces initial support for VRM 1.0 models, including a migration layer to bridge VRM 1.0 data to the existing VRM 0.x based internal architecture. It also includes a sample VRM 1.0 model and updates the example apps to demonstrate loading.

Changes

VRMKit Core

  • VRM 1.0 Migration: Added VRMMigration.swift to map VRM 1.0 data (meta, expressions, materials) to the existing internal VRM structure.
  • Material Support: Implemented extensive mapping for VRM 1.0 MToon and PBR materials to Unity-compatible shader properties (handling textures, colors, rendering modes).
  • Node & Data Structures: Updated Node.swift, VRM.swift, and VRM1.swift to accommodate VRM 1.0 specific fields.

Example Apps

  • New Sample Model: Added VRM1_Constraint_Twist_Sample.vrm to resources and build phases.
  • UI Updates: Updated VisionExample, MacExample, and VRMExample to support loading the new VRM 1.0 model (likely via a toggle or updated loader logic).

Project Configuration

  • Updated project.pbxproj to include the new VRM 1.0 sample file.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @tatsuya-ogawa, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the library by introducing full support for VRM 1.0 models. It includes a sophisticated migration mechanism that translates the newer VRM 1.0 data into the library's established VRM 0.x internal structures, ensuring seamless integration with existing rendering and animation pipelines. The accompanying example applications have been updated to showcase this new capability, providing a clear demonstration of loading and interacting with both VRM 0.x and VRM 1.0 models.

Highlights

  • VRM 1.0 Support: Added comprehensive support for parsing and loading VRM 1.0 models, enabling the library to handle the latest specification.
  • Data Migration Layer: Implemented a robust migration layer that translates VRM 1.0 data structures (meta, humanoid, blend shapes, first-person, secondary animation, materials) into the existing VRM 0.x internal representation for compatibility.
  • Example Application Updates: Updated example applications across RealityKit, SceneKit, VisionOS, and WatchOS to demonstrate VRM 1.0 model loading, including a new sample model and a UI for switching between VRM 0.x and VRM 1.0 models.
  • Enhanced Material Handling: Improved material parsing to support the KHR_materials_unlit glTF extension and refined MToon material property mapping for accurate rendering.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for VRM 1.0 models by adding new model assets, updating project configurations, and implementing migration logic from VRM 1.0 to VRM 0.x structures. The example applications have been updated to allow switching between VRM 0.x and VRM 1.0 models, demonstrating the new functionality. The changes also include necessary adjustments to material handling in RealityKit to ensure consistent rendering of MToon and unlit materials.

Comment on lines +44 to +45
case .alicia: return .pi
case .vrm1: return 0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There's an inconsistency in the initialRotation values across different example apps. In VisionExampleApp.swift, alicia has an initialRotation of .pi and vrm1 has 0. However, in Example/Example/ViewController.swift and Example/Example/RealityKitViewController.swift, alicia has 0 and vrm1 has .pi. This discrepancy will cause models to be oriented differently depending on which example app is used, which could be confusing or lead to unexpected behavior.

Suggested change
case .alicia: return .pi
case .vrm1: return 0
case .alicia: return 0
case .vrm1: return .pi

Comment on lines +89 to +99
let materialValues = expression.materialColorBinds?.map {
VRM.BlendShapeMaster.BlendShapeGroup.MaterialValueBind(materialName: "", // We need material NAME, but VRM1 bind has material INDEX.
// This is a problem. Material migration needs access to GLTF to resolve names.
// For now, we arguably can't populate this correctly without looking up GLTF.
// We will leave it empty or try to resolve later/outside?
// `MaterialValueBind` expects `materialName`.
// We can map it if we have access to GLTF.
// But this init extension only sees `vrm1`.
propertyName: $0.type.rawValue,
targetValue: $0.targetValue)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The materialName for MaterialValueBind cannot be correctly populated within this init extension because it lacks access to the gltf object, which is needed to resolve material names from their indices. This means that material-related blend shapes will not be migrated accurately from VRM 1.0 to VRM 0.x, potentially leading to incorrect visual effects.

To fix this, consider passing the gltf object to this initializer or refactoring the blend shape migration into a static function similar to migrateMaterials that has access to both vrm1 and gltf.

radius: sphere.radius
)
} else if let capsule = collider.shape.capsule {
// Approximate capsule as sphere (head)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Approximating capsule colliders as spheres might lead to less accurate collision detection for spring bones. While this might be a pragmatic choice due to API limitations, it's important to be aware that the physics simulation might not be as precise as intended by the VRM 1.0 specification.

// VRM 0.x: bones: [Int] (indices of root bones)
// VRM 0.x assumes uniform physics parameters for the group.
// VRM 1.0 has per-joint parameters.
// Migration: Take average or first joint's parameters?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When migrating spring bones, the current implementation uses only the parameters of the firstJoint for the entire BoneGroup. If individual joints within a VRM 1.0 spring have varying dragForce, gravityPower, hitRadius, or stiffness values, this simplification might not accurately represent the intended physics for all joints in the group.

Comment on lines +32 to +36
let mirror = Mirror(reflecting: vrm1.humanBones)
for child in mirror.children {
guard let label = child.label,
let humanBone1 = child.value as? VRM1.Humanoid.HumanBones.HumanBone?,
let node = humanBone1?.node else { continue }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using Mirror for iterating over vrm1.humanBones can be less performant and more fragile than an explicit mapping, especially if the structure of HumanBones changes in future VRM 1.0 revisions. While it works for direct property name to bone name mapping, it's generally not considered a best practice for production code due to its dynamic nature and potential for runtime errors if property names don't match expectations.


private func loadVRM() {
private func setUpUI() {
let items = VRMExampleModel.allCases.map { $0.rawValue == "AliciaSolid.vrm" ? "Alicia" : "VRM 1.0" }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The display names for the segmented control are currently hardcoded using a ternary operator. It would be more maintainable and scalable if the VRMExampleModel enum provided a dedicated displayName property. This would centralize the naming logic and make it easier to add or change model names in the future without modifying UI code.

Suggested change
let items = VRMExampleModel.allCases.map { $0.rawValue == "AliciaSolid.vrm" ? "Alicia" : "VRM 1.0" }
let items = VRMExampleModel.allCases.map { $0.displayName }

Comment on lines +39 to +40
let items = VRMExampleModel.allCases.map { $0.rawValue == "AliciaSolid.vrm" ? "Alicia" : "VRM 1.0" }
// Simplification: We could map names better, but sticking to existing UI labels
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The display names for the segmented control are currently hardcoded using a ternary operator. It would be more maintainable and scalable if the VRMExampleModel enum provided a dedicated displayName property. This would centralize the naming logic and make it easier to add or change model names in the future without modifying UI code.

Suggested change
let items = VRMExampleModel.allCases.map { $0.rawValue == "AliciaSolid.vrm" ? "Alicia" : "VRM 1.0" }
// Simplification: We could map names better, but sticking to existing UI labels
let items = VRMExampleModel.allCases.map { $0.displayName }

Comment on lines +17 to +18
ForEach(AppModel.ModelName.allCases) { model in
Text(model.rawValue).tag(model)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The display names for the picker are currently derived directly from rawValue. It would be more user-friendly and maintainable if AppModel.ModelName provided a dedicated displayName property. This would allow for more descriptive names in the UI without relying on raw string values, and make it easier to change display names without affecting the underlying model identifiers.

Suggested change
ForEach(AppModel.ModelName.allCases) { model in
Text(model.rawValue).tag(model)
ForEach(AppModel.ModelName.allCases) { model in
Text(model.displayName).tag(model)
}

Comment on lines +18 to +19
Text("Alicia").tag(ViewModel.ModelName.alicia)
Text("VRM 1.0").tag(ViewModel.ModelName.vrm1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The display names for the picker are currently hardcoded. It would be more maintainable and consistent if ViewModel.ModelName provided a dedicated displayName property. This would centralize the naming logic and make it easier to add or change model names in the future without modifying UI code.

                Picker("Model", selection: $viewModel.selectedModelName) {
                    ForEach(ViewModel.ModelName.allCases) { model in
                        Text(model.displayName).tag(model)
                    }
                }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant