For "VRM", please refer to this page.
- Load VRM file
- Render VRM models on RealityKit (experimental)
- Face morphing (blend shape)
- Bone animation (skin / joint)
- Physics (spring bone)
- MToon rendering and custom material shaders
- Render plain glTF / GLB with animations
- Swift 6.0+
- iOS 15.0+ / macOS 12.0+ / visionOS 2.0+ / watchOS 8.0+ (experimental)
- VRMRealityKit: iOS 18.0+ / macOS 15.0+ / visionOS 2.0+
You can install this package with Swift Package Manager.
import VRMKit
let loader = VRMLoader()
let vrm = try loader.load(named: "model.vrm")
// let vrm = try loader.load(withUrl: URL(string: "/path/to/model.vrm")!)
// let vrm = try loader.load(withData: data)
// VRM meta data
vrm.meta.title
vrm.meta.author
// model data
vrm.gltf.jsonData.nodes[0].name
// thumbnail
try loader.loadThumbnail(from: vrm)import RealityKit
import SwiftUI
import VRMRealityKit
struct ContentView: View {
var body: some View {
RealityView { content in
guard let entity = try? VRMEntityLoader(named: "model.vrm").loadEntity() else { return }
content.add(entity)
}
}
}VRMEntity is an Entity, so it drops into any RealityKit scene, ARView included. Once it is in a scene, skinning, constraints and spring bones update every frame automatically; set isAutomaticUpdateEnabled = false and call update(deltaTime:) to drive the timing yourself. Animation code that must run in a fixed order relative to that update belongs in a System declared with SystemDependency.before(VRMUpdateSystem.self).
VRMSceneKit, the SceneKit renderer, is deprecated. Use VRMRealityKit instead.
// VRM 1.0
vrmEntity.setExpression(value: 1.0, for: .preset(.happy))
vrmEntity.setExpression(value: 1.0, for: .custom("customExpressionName"))
// VRM 0.x
vrmEntity.setBlendShape(value: 1.0, for: .preset(.joy))
vrmEntity.setBlendShape(value: 1.0, for: .custom("><"))let neckRotation = simd_quatf(angle: 20 * .pi / 180, axis: SIMD3<Float>(0, 0, 1))
vrmEntity.humanoid.node(for: .neck)?.transform.rotation *= neckRotationDetails
MToon materials render by default on iOS and macOS. visionOS falls back to Unlit / PBR materials, because RealityKit's CustomMaterial is unavailable there.
vrmEntity.setMToonLightDirection(SIMD3<Float>(0, 0, -1))
vrmEntity.setMToonLightColor(SIMD3<Float>(1, 1, 1))
vrmEntity.setMToonAmbientColor(SIMD3<Float>(0.1, 0.1, 0.1))Both loaders take a material shader chain: each shader is asked in order, and materials no shader claims render through the built-in Unlit / PBR path.
// The default chain is [MToonShader()]: MToon with outlines.
let noOutlines = try VRMEntityLoader(named: "model.vrm", shaders: [MToonShader(isOutlineEnabled: false)])
let noMToon = try VRMEntityLoader(named: "model.vrm", shaders: [])
// Toon-shade a plain glTF, or a VRM whose materials are not MToon.
// Pass .convertAll(MToonConversionStyle(...)) to tune the conversion.
let converted = try GLTFEntityLoader(withURL: url, shaders: [MToonShader(source: .convertAll)])
// Your own shader joins the same chain.
final class MyShader: GLTFMaterialShader {
func makeMaterial(for context: GLTFMaterialShaderContext) throws -> GLTFShadedMaterial? {
// Return nil to pass the material on to the next shader / built-in path,
// or start from try context.standardMaterial() to adjust the standard result.
var material = UnlitMaterial()
if let texture = context.material.pbrMetallicRoughness?.baseColorTexture {
material.color = .init(texture: try context.materialTexture(withTextureIndex: texture.index))
}
return GLTFShadedMaterial(material: material)
}
}
let custom = try VRMEntityLoader(withData: data, shaders: [MyShader(), MToonShader()])GLTFShadedMaterial also carries extra render passes (MToon draws its outline as one) and a makeAnimatableState closure that lets VRM expressions animate a custom material. The GLTFMaterialShader documentation comments cover both, along with what a shader may assume about the mesh it draws.
Details
VRMRealityKit also renders plain glTF assets (.glb and JSON .gltf, including external resources and data URIs).
let entity: GLTFEntity = try GLTFEntityLoader(withURL: url).loadEntity()
entity.animations // [GLTFAnimation]: index, name, duration
let controller = try entity.playAnimation(at: 0, loops: true)
controller.speed = 2 // a negative speed plays backwards
controller.seek(to: 0.5)
controller.stop()loadEntity() renders the asset's default scene, and throws when the glTF names none; pick one with loadEntity(withSceneIndex:). A clone(recursive:) copy shares the loaded meshes and materials but not the animation bindings, so load the scene again for a second animatable instance.
Renderer limitations
RealityKit meshes and materials cannot express every part of glTF and MToon. Each case below logs a warning once per affected material.
- Only triangle primitives are drawn;
POINTSandLINESprimitives are skipped. COLOR_0vertex colors are ignored: the mesh buffers this renderer builds carry no vertex-color channel.- One UV set and one
KHR_texture_transformper material: the first UV-accessed texture decides both for every texture of that material. A glTF load rejects a document that listsKHR_texture_transforminextensionsRequiredand needs more than that, instead of drawing it wrong; a VRM load renders the approximation. - Tangents for a primitive without
TANGENTare averaged from its UV gradients rather than generated with MikkTSpace, which the spec recommends, so a normal map baked against MikkTSpace can differ slightly along UV seams. - Blend shapes morph
POSITIONonly, since RealityKit blend shapes have noNORMAL/TANGENTchannel. - Skinning reads
JOINTS_0/WEIGHTS_0only, so a vertex is driven by at most four joints; the further sets a glTF may carry are ignored. - MToon's
renderQueueOffsetNumberis parsed but ignored, because RealityKit has no material-level draw-order hook. (transparentWithZWriteis supported throughCustomMaterial.writesDepth.)
- Improve rendering quality
- Animation support (vrma)
- VRM editing function
Pull requests are welcome. Fork the repository, work on a feature branch, and open a PR :D
Donating to help me continue working on this project.
VRMKit is released under the MIT license. See LICENSE for details.
Tatsuya Tanaka





