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
3 changes: 2 additions & 1 deletion Example/Example/RealityKitViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ final class RealityKitViewController: UIViewController, UIGestureRecognizerDeleg
loadedEntity = nil

do {
let loader = try VRMEntityLoader(named: model.rawValue, isMToonEnabled: isMToonEnabled)
let loader = try VRMEntityLoader(named: model.rawValue,
shaders: isMToonEnabled ? GLTFEntityLoader.defaultShaders : [])
let vrmEntity = try loader.loadEntity()
vrmEntity.setMToonLightDirection(RealityKitExampleLighting.direction)

Expand Down
3 changes: 2 additions & 1 deletion Example/MacExample/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,8 @@ final class RealityKitContentViewModel {
do {
errorMessage = nil

let loader = try VRMEntityLoader(named: model.rawValue, isMToonEnabled: isMToonEnabled)
let loader = try VRMEntityLoader(named: model.rawValue,
shaders: isMToonEnabled ? GLTFEntityLoader.defaultShaders : [])
let nextVRMEntity = try loader.loadEntity()

nextVRMEntity.transform.translation = SIMD3<Float>(0, -1, 0)
Expand Down
194 changes: 58 additions & 136 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,134 +27,82 @@ For "VRM", please refer to [this page](https://dwango.github.io/en/vrm/).
- [x] Face morphing (blend shape)
- [x] Bone animation (skin / joint)
- [x] Physics (spring bone)
- [x] MToon rendering and custom material shaders
- [x] Render plain glTF / GLB with animations

# Requirements

- Swift 6.0+
- iOS 15.0+
- macOS 12.0+
- visionOS 2.0+
- watchOS 8.0+ (Experimental)

VRMRealityKit requires iOS 18.0+ / macOS 15.0+ / visionOS 2.0+.
- iOS 15.0+ / macOS 12.0+ / visionOS 2.0+ / watchOS 8.0+ (experimental)
- VRMRealityKit: iOS 18.0+ / macOS 15.0+ / visionOS 2.0+

# Installation

## Swift Package Manager

You can install this package with Swift Package Manager.

## Carthage & CocoaPods (Deprecated)

If you want to use these package managers, please use https://github.com/tattn/VRMKit/releases/tag/0.4.2

# Usage

## Load VRM

```swift
import VRMKit

let vrm = try VRMLoader().load(named: "model.vrm")
// let vrm = try VRMLoader().load(withUrl: URL(string: "/path/to/model.vrm")!)
// let vrm = try VRMLoader().load(withData: data)
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
```

## Render VRM

```swift
import RealityKit
import VRMKit
import VRMRealityKit

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

let arView = ARView(frame: .zero, cameraMode: .nonAR, automaticallyConfigureSession: false)
let anchor = AnchorEntity(world: .zero)
anchor.addChild(vrmEntity)
arView.scene.addAnchor(anchor)
// thumbnail
try loader.loadThumbnail(from: vrm)
```

`VRMEntity` is an `Entity`. Once it is in a scene, skinning, constraints and spring bones are updated every frame automatically.

### Render VRM (SwiftUI)
## Render VRM

```swift
import RealityKit
import SwiftUI
import VRMKit
import VRMRealityKit

struct ContentView: View {
var body: some View {
RealityView { content in
guard let loader = try? VRMEntityLoader(named: "model.vrm"),
let vrmEntity = try? loader.loadEntity() else { return }
content.add(vrmEntity)
guard let entity = try? VRMEntityLoader(named: "model.vrm").loadEntity() else { return }
content.add(entity)
}
}
}
```

<details>
<summary>Render VRM (SceneKit) — Deprecated</summary>
`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)`.

> Note: VRMSceneKit is deprecated. Use VRMRealityKit instead.
> VRMSceneKit, the SceneKit renderer, is deprecated. Use VRMRealityKit instead.

```swift
import VRMKit
import VRMSceneKit

@IBOutlet weak var sceneView: SCNView!

let loader = try VRMSceneLoader(named: "model.vrm")
let scene: VRMScene = try loader.loadScene()
let node: VRMNode = scene.vrmNode

sceneView.scene = scene
```

</details>

### Blend shapes / expressions

VRM 0.x uses blend shapes:
## Expressions / blend shapes

<img src="https://github.com/tattn/VRMKit/raw/main/.github/alicia_joy.png" width="100px" alt="joy" />

```swift
vrmEntity.setBlendShape(value: 1.0, for: .preset(.joy))
```

<img src="https://github.com/tattn/VRMKit/raw/main/.github/alicia_angry.png" width="100px" alt="angry" />

```swift
vrmEntity.setBlendShape(value: 1.0, for: .preset(.angry))
```

<img src="https://github.com/tattn/VRMKit/raw/main/.github/alicia_><.png" width="100px" alt="><" />

```swift
vrmEntity.setBlendShape(value: 1.0, for: .custom("><"))
```

VRM 1.0 uses expressions:

```swift
// VRM 1.0
vrmEntity.setExpression(value: 1.0, for: .preset(.happy))
vrmEntity.setExpression(value: 1.0, for: .preset(.aa))
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("><"))
```

### Bone animation
## Bone animation

<img src="https://github.com/tattn/VRMKit/raw/main/.github/alicia_humanoid.png" width="200px" alt="Humanoid" />

Expand All @@ -163,71 +111,53 @@ let neckRotation = simd_quatf(angle: 20 * .pi / 180, axis: SIMD3<Float>(0, 0, 1)
vrmEntity.humanoid.node(for: .neck)?.transform.rotation *= neckRotation
```

### Read the thumbnail image

```swift
let loader = VRMLoader()
let vrm = try loader.load(named: "model.vrm")
let image = try loader.loadThumbnail(from: vrm)
```

## MToon rendering

VRMRealityKit renders MToon materials by default on iOS and macOS. visionOS falls back to Unlit / PBR materials because RealityKit's `CustomMaterial` is unavailable there.
MToon materials render by default on iOS and macOS. visionOS falls back to Unlit / PBR materials, because RealityKit's `CustomMaterial` is unavailable there.

```swift
vrmEntity.setMToonLightDirection(SIMD3<Float>(0, 0, -1))
vrmEntity.setMToonLightColor(SIMD3<Float>(1, 1, 1))
vrmEntity.setMToonAmbientColor(SIMD3<Float>(0.1, 0.1, 0.1))
```

<details>
<summary>Loader options and limitations</summary>
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.

```swift
let loader = try VRMEntityLoader(
named: "model.vrm",
isMToonEnabled: true, // false: disable MToon and use the legacy Unlit / PBR conversion
isOutlineEnabled: true // false: skip MToon outline entities
)
```

Outlines can be skipped while keeping the MToon surface shader. On visionOS both options fall back automatically because the required RealityKit APIs are unavailable.

RealityKit constrains what the MToon renderer can express. Each case below logs a warning once per affected material.

- `renderQueueOffsetNumber` is parsed but ignored, because RealityKit has no material-level draw-order hook. (`transparentWithZWrite` is supported through `CustomMaterial.writesDepth`, so a blended material can still write depth.)
- Textures requesting a UV set other than `TEXCOORD_0` use `TEXCOORD_0`, because custom meshes expose only that one.
- When UV-accessed texture slots specify different `KHR_texture_transform` values, the transform of the first UV-accessed slot — base color when the material has one — is applied to all of them, because `CustomMaterial` has a single material-level UV transform. Expression texture transform binds still update all UV-accessed textures together as required by VRMC_vrm.

</details>

<details>
<summary>Frame updates</summary>

`VRMUpdateSystem` (a RealityKit `System` registered on load) calls `VRMEntity.update(deltaTime:)` on every render frame. To control the timing yourself, opt out and call it manually:

```swift
vrmEntity.isAutomaticUpdateEnabled = false
// 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)
}
}

// Then, once per frame:
vrmEntity.update(deltaTime: deltaTime)
let custom = try VRMEntityLoader(withData: data, shaders: [MyShader(), MToonShader()])
```

To run your own animation code in a guaranteed order relative to the VRM update (e.g. posing joints that the same frame's skinning should reflect), put it in a custom `System` declared with `SystemDependency.before(VRMUpdateSystem.self)`.
`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>

<details>
<summary>Render glTF / GLB</summary>
## Render glTF / GLB

VRMRealityKit can also render plain glTF assets (`.glb` and JSON `.gltf`, including external resources and data URIs).
VRMRealityKit also renders plain glTF assets (`.glb` and JSON `.gltf`, including external resources and data URIs).

```swift
let entity: GLTFEntity = try GLTFEntityLoader(withURL: url).loadEntity()
content.add(entity)

entity.animations // [GLTFAnimation] index, name, duration
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)
Expand All @@ -236,38 +166,30 @@ 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.

### RealityKit renderer limitations
<details>
<summary>Renderer limitations</summary>

The renderer builds RealityKit meshes and materials, so a few parts of glTF have no place to go:
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; `POINTS` and `LINES` primitives are skipped.
- `COLOR_0` vertex colors are ignored: the `MeshResource.Part` buffers this renderer builds carry no vertex-color channel.
- One UV set per material: the first UV-accessed texture decides both the `TEXCOORD_n` set and the single `KHR_texture_transform` every texture of that material is sampled with. An asset that merely lists `KHR_texture_transform` in `extensionsUsed` renders through that approximation and logs it; one that lists it in `extensionsRequired` and gives a material's textures different transforms is rejected instead of drawn wrong.
- `COLOR_0` vertex colors are ignored: the mesh buffers this renderer builds carry no vertex-color channel.
- One UV set and one `KHR_texture_transform` per material: the first UV-accessed texture decides both for every texture of that material. A glTF load rejects a document that lists `KHR_texture_transform` in `extensionsRequired` and needs more than that, instead of drawing it wrong; a VRM load renders the approximation.
- Tangents for a primitive without `TANGENT` are 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 `POSITION` only, since RealityKit blend shapes have no `NORMAL` / `TANGENT` channel.
- Skinning reads `JOINTS_0` / `WEIGHTS_0` only, so a vertex is driven by at most four joints; the further sets a glTF may carry (`JOINTS_1` and up) are ignored, which the spec allows and which can change the result of an animation.
- Skinning reads `JOINTS_0` / `WEIGHTS_0` only, so a vertex is driven by at most four joints; the further sets a glTF may carry are ignored.
- MToon's `renderQueueOffsetNumber` is parsed but ignored, because RealityKit has no material-level draw-order hook. (`transparentWithZWrite` is supported through `CustomMaterial.writesDepth`.)

</details>

# ToDo

- [x] VRM 1.0 support
- [x] Decoding VRM 1.0 file
- [x] Render an avatar by RealityKit (as VRM 0.x)
- [x] Render an avatar by RealityKit (as VRM 1.x)
- [x] VRM shaders support (MToon, RealityKit)
- [ ] Improve rendering quality
- [ ] Animation support (vrma)
- [ ] VRM editing function
- [x] glTF renderer / animation support (RealityKit)

# Contributing

1. Fork it!
2. Create your feature branch: `git checkout -b my-new-feature`
3. Commit your changes: `git commit -am 'Add some feature'`
4. Push to the branch: `git push origin my-new-feature`
5. Submit a pull request :D
Pull requests are welcome. Fork the repository, work on a feature branch, and open a PR :D

## Support this project

Expand All @@ -283,5 +205,5 @@ VRMKit is released under the MIT license. See LICENSE for details.

Tatsuya Tanaka

<a href="https://twitter.com/tattn_dev" target="_blank"><img alt="Twitter" src="https://img.shields.io/twitter/follow/tattn_dev.svg?style=social&label=Follow"></a>
<a href="https://x.com/tattn_dev" target="_blank"><img alt="Twitter" src="https://img.shields.io/twitter/follow/tattn_dev.svg?style=social&label=Follow"></a>
<a href="https://github.com/tattn" target="_blank"><img alt="GitHub" src="https://img.shields.io/github/followers/tattn.svg?style=social"></a>
2 changes: 1 addition & 1 deletion Sources/VRMKit/Extensions/Accessor+Data.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ package extension GLTF.Accessor {
private extension GLTF.Accessor {
/// The element size, rejecting the matrix layouts glTF pads. MAT2 / MAT3
/// columns are aligned to 4 bytes, so with small component types an element
/// is wider than its componentsa layout no VRM asset uses, and one the
/// is wider than its components, a layout no VRM asset uses and one the
/// readers above would mis-slice.
func unpaddedVectorSize() throws -> Int {
let (componentsPerVector, bytesPerComponent, vectorSize) = components()
Expand Down
2 changes: 1 addition & 1 deletion Sources/VRMKit/Extensions/Data+GLTF.swift
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ package extension URL {
///
/// A `uri` is a URI reference, not a file path: its reserved characters are
/// percent-encoded, so `My%20Buffer.bin` names a file with a space in it.
/// Only local files are read a glTF must not fetch resources off the
/// Only local files are read: a glTF must not fetch resources off the
/// network on the caller's behalf.
init(gltfUri: String, relativeTo rootDirectory: URL?) throws {
// A uri that is not a valid URI reference is taken as a literal path,
Expand Down
4 changes: 2 additions & 2 deletions Sources/VRMKit/Extensions/PackedAccessor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ package struct PackedAccessor {
/// Accessors expanded once and reused, so the primitives, skins and animation
/// samplers that share one accessor expand it a single time.
///
/// A cache holds decoded geometry, so it belongs to whatever needs it one
/// scene load, one animation binding and is dropped with it.
/// A cache holds decoded geometry, so it belongs to whatever needs it (one
/// scene load, one animation binding) and is dropped with it.
package final class PackedAccessorCache {
private let document: GLTFDocument
private var packedAccessors: [Int: PackedAccessor] = [:]
Expand Down
4 changes: 2 additions & 2 deletions Sources/VRMKit/GLTFDocument.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation

/// A parsed glTF asset together with the context needed to resolve its binary
/// resources the GLB BIN chunk, external files and data URIs so that they
/// resources (the GLB BIN chunk, external files and data URIs), so that they
/// stay resolvable after loading, e.g. for lazily decoded animation accessors.
public final class GLTFDocument {
public let gltf: GLTF
Expand All @@ -13,7 +13,7 @@ public final class GLTFDocument {
/// Decoded buffers, keyed by buffer index: resolving one re-reads an external
/// file or base64-decodes a whole data URI.
///
/// Buffer *views* are deliberately not cached they are copies out of these
/// Buffer *views* are deliberately not cached: they are copies out of these
/// buffers, and keeping them would hold the file's bytes a second time.
private var buffers: [Int: Data] = [:]

Expand Down
4 changes: 2 additions & 2 deletions Sources/VRMKit/GLTFLoader.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation

/// Loads plain glTF assets GLB containers and JSON `.gltf` files with
/// external or data-URI resources — into a ``GLTFDocument``.
/// Loads plain glTF assets into a ``GLTFDocument``: GLB containers, and JSON
/// `.gltf` files with external or data-URI resources.
public final class GLTFLoader {
public init() {}

Expand Down
2 changes: 1 addition & 1 deletion Sources/VRMKitRuntime/BlendShapeBindings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ package typealias ExpressionOverrideType = VRM1.Expressions.Expression.Expressio

/// Accumulates every active expression's override of one group, following
/// VRMC_vrm: `block` zeroes the group outright, while simultaneous `blend`
/// overrides *add up* before being saturated — they do not compose
/// overrides *add up* before being saturated, rather than composing
/// multiplicatively.
package struct ExpressionOverrideState {
private var isBlocked = false
Expand Down
Loading
Loading