From Ready Player Me Shutdown to Building Our Own Avatar Renderer 🚀
How I replaced Ready Player Me with a fully modular GLB renderer — with zero Three.js experience.
On January 31, 2026, Ready Player Me shut down.
We were warned at the end of December 2025.
That gave us roughly 3–4 weeks to replace a system that powered avatars across our entire application.
Two things had to be rebuilt immediately:
- Avatar creation
- Avatar appearance across the app
But creation came first — because without avatars, appearance didn’t matter.
And the responsibility landed completely on me — a core frontend developer with zero experience in Three.js, React Three Fiber, or Drei.
It felt like a do-or-die penalty in the last minute of a match.
But as developers, learning under pressure is part of the job.
As Master Shifu said:
"If you only do what you can do, then you will never be better than what you are."
So I started.
The Old System (Before Ready Player Me)
Before using Ready Player Me, our avatar system was simple:
- A single Base64 GLB
- Fully combined avatar
- All assets embedded
- Easy to render
- Easy to store
- Easy to preview
But it was not flexible.
Ready Player Me introduced:
- Modular assets
- Morph targets
- Animations
- Texture customization
- Live preview
When RPM shut down — we lost everything.
Step 1 — First Three.js Success
The first milestone was simple:
Render a GLB.
Using React Three Fiber, I loaded a dummy Wolf3D model.
And it worked.
Seeing a 3D avatar render inside our app was the first real sign:
This might actually be possible.
The First Real Problem — Skeleton Collisions
Then reality hit.
Ready Player Me assets had their own armature.
Our custom assets had a different armature.
Nothing aligned.
Meshes broke.
Animations failed.
Bones mismatched.
Our graphics team stepped in and produced:
- Custom body GLB
- Custom head GLB
- Matching armature
Once those worked together:
We finally had:
- Head
- Body
- 2–3 test assets
And everything rendered correctly.
First real victory. 🎉
The Real Challenge — 37 Modular Assets
Then came the real problem.
We had:
- 18 assets for female
- 19 assets for male
Different categories:
Body Assets
- Top
- Bottom
- Footwear
- Outfit
- Skin color
Head Assets
- Hair
- Hair color
- Eyes
- Beard
- Beard color
- Face shapes
- Eyebrows
- Nose
- Lips and many more.
Global Assets
- Animations
- Poses
Each category needed different rendering logic.
Apparel Was Easy
Clothing assets were simple:
Just attach GLB meshes to the body.
addBodyPart(getGLTFScene(shirtGLTF), 'shirt');
addBodyPart(getGLTFScene(pantsGLTF), 'pants');
addBodyPart(getGLTFScene(shoesGLTF), 'shoes');
That part worked almost immediately.
Asset Loading Strategy
Assets could come from multiple sources:
- Redux preview state
- API data
- Defaults
Preview needed priority.
const assets = {
...defaultAssets,
...glbAssets,
bodyColorTexture: glbAssets?.bodyColorTexture || defaultAssets.bodyColorTexture,
eyeColorTexture: glbAssets?.eyeColorTexture || defaultAssets.eyeColorTexture,
};
const shirtPath = hasOutfitPreview
? defaultAssets.shirt
: (apparelPreview.top || assets.shirt || defaultAssets.shirt);
const hairPath = hasOutfitPreview && gender === 'female'
? defaultAssets.hair
: (apparelPreview.hair || assets.hair || defaultAssets.hair);
This ensured:
Live preview always wins.
Runtime Texture Baking (Eyebrows & Beard)
Head assets were harder.
Beards and eyebrows were PNG overlays.
Instead of adding extra meshes, I baked them directly onto the face texture.
function createFaceWithEyebrowsTexture(baseTexture, eyebrowsTexture, fallback, options) {
if (eyebrowColor) {
// tint only eyebrow pixels
}
const baked = new THREE.CanvasTexture(canvas);
baked.flipY = false;
baked.colorSpace = THREE.SRGBColorSpace;
return baked;
}
Result:
- No extra draw calls
- Better performance
- Realistic blending
The Core Innovation — Skeleton Rebinding
The biggest technical challenge was equipping assets.
Every asset needed to use the same skeleton.
Solution:
Skeleton rebinding.
const addBodyPart = (gltfScene: THREE.Group, partName: string) => {
if (child instanceof THREE.SkinnedMesh) {
const newBones = skinnedChild.skeleton.bones
.map(bone => bonesByName[bone.name])
.filter(Boolean);
const newSkeleton = new THREE.Skeleton(
newBones,
skinnedChild.skeleton.boneInverses.map(m => m.clone())
);
skinnedClone.skeleton = newSkeleton;
skinnedClone.bind(newSkeleton, skinnedChild.bindMatrix.clone());
skinnedClone.pose();
skinnedClone.visible = true;
combined.add(skinnedClone);
}
};
This allowed:
- Any asset to attach dynamically
- Animations to work
- No broken meshes
- No T-pose issues
Static Attachments (Head Parts)
Some assets didn't need skeleton binding.
They could attach directly to bones.
if (['skin','eyes','teeth','beard','hair'].includes(resolvedPartName) && headBone) {
headBone.add(clonedMesh);
}
This worked perfectly for:
- Hair
- Eyes
- Beard
- Accessories
Costume Mode vs Modular Mode
Two rendering paths were required.
Costume Mode
Single full-body GLB:
if (isCostumeMode) {
const costumeScene = SkeletonUtils.clone(getGLTFScene(outfitGLTF));
return costumeScene;
}
Modular Mode
Dynamic assembly:
addBodyPart(getGLTFScene(bodyGLTF), 'skin');
addBodyPart(getGLTFScene(headGLTF), 'head');
addBodyPart(getGLTFScene(shirtGLTF), 'shirt');
addBodyPart(getGLTFScene(pantsGLTF), 'pants');
addBodyPart(getGLTFScene(shoesGLTF), 'shoes');
Animations and Morph Targets
Animations had to survive scene rebuilds.
<AnimatedCombinedScene key={combinedScene.uuid} combinedScene={combinedScene} />
Morph targets enabled facial customization:
Object.entries(apparelPreview.morphTargets || {}).forEach(([name, value]) => {
const idx = child.morphTargetDictionary?.[name];
if (idx !== undefined)
child.morphTargetInfluences[idx] = value;
});
Automatic Eye Blink
Even blinking was implemented.
Wolf3D style.
useFrame((_, delta) => {
// eyeBlinkLeft / eyeBlinkRight logic
});
Small detail.
Huge realism boost.
The Final Renderer Architecture
Key ideas:
Modular Equipping
- Every asset is a GLB
- Loaded via
useGLTF - Preview has priority
Skeleton System
- One template skeleton
- Bone-name matching
- Rebinding
Runtime Baking
- Eyebrows
- Beard
- Face details
Dual Render Paths
- Costume mode
- Modular mode
Stable Rendering
- No T-pose
- No disappearing meshes
skinnedClone.frustumCulled = false;
skinnedClone.pose();
Live Updates
Redux drives everything.
key={combinedScene.uuid}
Forces clean rebuild.
Avatar Appearance Was Easy
Once creation worked:
Appearance across the app was trivial.
The renderer already supported props.
So avatar panels and previews worked instantly.
Sometimes the hardest part unlocks everything else.
Performance Tradeoffs
The old system:
- Single GLB
- Fast
- Simple
The new system:
- Modular GLBs
- Slightly slower
- Much more flexible
Creation flow became far smoother, even if asset loading takes slightly longer.
Optimization is ongoing.
What I Learned
1 — Research Is Development
Learning is part of the job.
Not optional.
Essential.
2 — AI Helps Real Engineers
AI helped me:
- Learn Three.js
- Solve skeleton issues
- Debug rendering
- Speed up development
AI didn't replace engineering.
It accelerated learning.
Building My Own GLB Renderer
After finishing the system, I got inspired to go further.
I started building a custom GLB renderer.
Features:
- Load any GLB
- Test assets on provided bodies
- Male + female models
- Asset testing
Currently supports:
- Tops
- Bottoms
- Footwear
- Hair
Live Demo: glb-viewer
GitHub: glb-viewer
Final Thoughts
Ready Player Me shutting down could have been a disaster.
Instead, it became one of the biggest growth experiences of my career.
In less than a month:
- Learned Three.js
- Built a modular avatar system
- Implemented skeleton rebinding
- Created runtime texture baking
- Supported animations and morph targets
And most importantly:
We didn't depend on a third-party avatar system anymore.
We built our own.

Top comments (2)
Wow! This is a really cool 3D project. Thank you for sharing. I’ve also been trying 3D recently, and I hope to reach your level soon. 😄
Thank you @webdeveloperhyper 🙌.