Table of Contents

Custom Character Integration

Step-by-step guide for integrating custom VRM characters into your AStack application.

Quick Start (React)

The simplest way to use a custom character — pass modelUrl to the VRMAvatar component:

import { useAStackCSR, VRMAvatar } from '@aether-stack-dev/client-sdk/react';
function MyApp() {
const { blendshapes, connect, startCall } = useAStackCSR({
workerUrl: 'wss://your-worker-url',
sessionToken: token,
});
return (
<VRMAvatar
blendshapes={blendshapes}
modelUrl="https://your-cdn.com/character.vrm"
/>
);
}

Using the Hook Character Config

For cleaner integration, pass character config through useAStackCSR and spread it on VRMAvatar:

import { useAStackCSR, VRMAvatar } from '@aether-stack-dev/client-sdk/react';
function MyApp() {
const { blendshapes, characterConfig, connect, startCall } = useAStackCSR({
workerUrl: 'wss://your-worker-url',
sessionToken: token,
character: {
modelUrl: 'https://your-cdn.com/character.vrm',
onModelLoad: (report) => {
if (report.warnings.length > 0) {
console.warn('Character compatibility issues:', report.warnings);
}
},
},
});
return <VRMAvatar blendshapes={blendshapes} {...characterConfig} />;
}

Vanilla JavaScript

Without React, use the client SDK directly and render VRM with Three.js + @pixiv/three-vrm:

import { AStackCSRClient, ARKIT_BLENDSHAPES } from '@aether-stack-dev/client-sdk';
import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { VRMLoaderPlugin } from '@pixiv/three-vrm';
// 1. Load your custom VRM
const loader = new GLTFLoader();
loader.register((parser) => new VRMLoaderPlugin(parser));
const gltf = await loader.loadAsync('https://your-cdn.com/character.vrm');
const vrm = gltf.userData.vrm;
// 2. Connect to AStack
const client = new AStackCSRClient({
workerUrl: 'wss://your-worker-url',
sessionToken: token,
});
await client.connect();
// 3. Apply blendshapes from server to your VRM
client.on('blendshapeUpdate', (shapes) => {
vrm.scene.traverse((obj) => {
if (obj.isMesh && obj.morphTargetDictionary) {
ARKIT_BLENDSHAPES.forEach((name, i) => {
const idx = obj.morphTargetDictionary[name];
if (idx !== undefined) {
obj.morphTargetInfluences[idx] = shapes[i] || 0;
}
});
}
});
});
await client.startCall();

Hosting Your Model

  • AStack templates: Use any of the 11 built-in characters hosted on Supabase Storage — no bundling required. The SDK defaults to one automatically
  • Same origin: Serve from your app's public directory (e.g., /public/models/)
  • CDN: Host on S3, CloudFront, or any CDN with proper CORS headers
  • External URL: Any HTTPS URL that serves the .vrm file with CORS enabled

CORS Requirements

If hosting your VRM on a different domain, the server must include these CORS headers:

Access-Control-Allow-Origin: https://your-app.com
Access-Control-Allow-Methods: GET, HEAD
Access-Control-Allow-Headers: Content-Type

The HEADmethod is used for size checking before download. If your server doesn't support HEAD requests, the size guard will be skipped and the model will load normally.

Blendshape Mapping

If your model uses custom blendshape names instead of ARKit names, provide a mapping:

// Custom mapping: ARKit name → your model's name
const myMapping = {
jawOpen: 'mouth_open',
eyeBlinkLeft: 'left_eye_close',
eyeBlinkRight: 'right_eye_close',
mouthSmileLeft: 'smile_L',
mouthSmileRight: 'smile_R',
// ... add more as needed
};
<VRMAvatar
blendshapes={blendshapes}
modelUrl="/my-model.vrm"
blendshapeMap={myMapping}
/>

For VRoid Studio models, use the built-in preset:

import { VROID_BLENDSHAPE_MAP } from '@aether-stack-dev/client-sdk';
<VRMAvatar
blendshapes={blendshapes}
modelUrl="/vroid-character.vrm"
blendshapeMap={VROID_BLENDSHAPE_MAP}
/>

Error Handling

<VRMAvatar
blendshapes={blendshapes}
modelUrl={userModelUrl}
maxModelSize={15 * 1024 * 1024} // 15MB limit
onModelLoad={(report) => {
if (report.supported < 10) {
alert('This model has very few compatible blendshapes.');
}
if (report.warnings.length > 0) {
console.warn('Issues found:', report.warnings);
}
console.log('Model stats:', report.modelStats);
}}
/>

Performance Tips

  • Model size: Keep under 15MB. Use maxModelSize to enforce limits
  • Vertex count: Under 100K vertices. Models over this threshold will trigger a warning in onModelLoad
  • Textures: Compress to JPEG/WebP. Avoid uncompressed 4096x4096 textures
  • LOD: For mobile, consider providing a lower-poly variant

Troubleshooting

Model doesn't load

  • Check CORS headers if hosted on a different domain
  • Verify the URL returns a valid .vrm file (not HTML or a redirect)
  • Check browser console for specific error messages
  • The onModelLoad callback won't fire on load failure — check the error state

Model loads but doesn't animate

  • Check onModelLoad report — if supported is 0, the model has no matching blendshapes
  • Try providing a blendshapeMap if your model uses non-standard names
  • Open the model in VRoid Hub or Three.js editor to inspect morph target names

Model looks wrong / T-pose

  • Ensure the VRM has a valid humanoid bone structure
  • VRM 0.x models may need re-export with VRoid Studio or UniVRM

Performance is poor

  • Check modelStats.vertexCount — reduce poly count if over 100K
  • Compress textures and reduce resolution
  • Use a simpler model for mobile devices