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 (<VRMAvatarblendshapes={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 VRMconst 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 AStackconst client = new AStackCSRClient({workerUrl: 'wss://your-worker-url',sessionToken: token,});await client.connect();// 3. Apply blendshapes from server to your VRMclient.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.comAccess-Control-Allow-Methods: GET, HEADAccess-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 nameconst myMapping = {jawOpen: 'mouth_open',eyeBlinkLeft: 'left_eye_close',eyeBlinkRight: 'right_eye_close',mouthSmileLeft: 'smile_L',mouthSmileRight: 'smile_R',// ... add more as needed};<VRMAvatarblendshapes={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';<VRMAvatarblendshapes={blendshapes}modelUrl="/vroid-character.vrm"blendshapeMap={VROID_BLENDSHAPE_MAP}/>
Error Handling
<VRMAvatarblendshapes={blendshapes}modelUrl={userModelUrl}maxModelSize={15 * 1024 * 1024} // 15MB limitonModelLoad={(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
maxModelSizeto 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
onModelLoadcallback won't fire on load failure — check the error state
Model loads but doesn't animate
- Check
onModelLoadreport — ifsupportedis 0, the model has no matching blendshapes - Try providing a
blendshapeMapif 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