A Laravel package for building 3D avatars with GLTF 2.0 (GLB) and VRM (0.x & 1.0) output. Includes a Node.js sidecar for 3D mesh operations and frontend components for Vue, React, and Alpine.js.
- PHP 8.2+
- Laravel 11.x or 12.x
- Node.js 20+
- npm or pnpm
composer require streetmesh/avatarsRun the install command:
php artisan avatars:installThis will:
- Publish the configuration file
- Publish database migrations
- Create storage directories
- Install npm dependencies
- Build the Node.js sidecar
Run migrations:
php artisan migrateThe installer auto-detects your frontend framework. To install manually:
# Vue (Inertia)
npm install @streetmesh/avatar-vue
# React (Inertia)
npm install @streetmesh/avatar-react
# Alpine.js (Livewire)
npm install @streetmesh/avatar-alpinePublish the config file if not already done:
php artisan avatars:publish --configKey configuration options in config/avatars.php:
return [
'storage' => [
'disk' => 'local',
'path' => 'avatars',
],
'sidecar' => [
'node_executable' => 'node',
'timeout' => 120,
],
'export' => [
'default_format' => 'glb',
'draco_compression' => true,
],
'validation' => [
'max_file_size' => 50 * 1024 * 1024,
'max_polygon_count' => 100000,
],
'routes' => [
'enabled' => true,
'prefix' => 'api/avatars',
'middleware' => ['api', 'auth:sanctum'],
],
];use StreetMesh\Avatars\Facades\Avatar;
use StreetMesh\Avatars\Enums\ExportFormat;
use StreetMesh\Avatars\Data\VrmMetadata;
// Part-based assembly
Avatar::assemble()
->base('humanoid-base-v1')
->addPart('hair', 'ponytail-blonde')
->addPart('clothes', 'casual-shirt')
->addPart('accessory', 'glasses-round')
->withMetadata(
VrmMetadata::make()
->title('My Avatar')
->author('User Name')
->version('1.0')
)
->exportAs(ExportFormat::VRM_1)
->forUser($user)
->dispatch();
// Template customization
Avatar::customize('anime-base-template')
->skinColor('#F5D0C5')
->hairColor('#3D2314')
->eyeColor('#4A90A4')
->texture('shirt', $uploadedTexture)
->exportAs(ExportFormat::GLB)
->dispatch();
// Import and convert
Avatar::import($uploadedFile)
->validate()
->autoMapBones()
->addSpringBones(['hair', 'skirt'])
->exportAs(ExportFormat::VRM_1)
->dispatch();Listen for build lifecycle events:
use StreetMesh\Avatars\Events\AvatarBuildCompleted;
use StreetMesh\Avatars\Events\AvatarBuildFailed;
// In EventServiceProvider
protected $listen = [
AvatarBuildCompleted::class => [
SendAvatarReadyNotification::class,
],
AvatarBuildFailed::class => [
LogBuildFailure::class,
],
];The package registers these API routes (configurable prefix):
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/avatars/parts |
List available parts |
| GET | /api/avatars/parts/{type} |
List parts by type |
| GET | /api/avatars/templates |
List templates |
| GET | /api/avatars/templates/{id} |
Get template details |
| POST | /api/avatars/builds |
Start a new build |
| GET | /api/avatars/builds/{id} |
Get build status |
| GET | /api/avatars/{id} |
Get avatar details |
| GET | /api/avatars/{id}/download |
Download avatar file |
<script setup>
import { AvatarBuilder, AvatarViewer } from '@streetmesh/avatar-vue'
function handleBuildComplete(result) {
console.log('Avatar built:', result)
}
</script>
<template>
<!-- Full builder with part selection -->
<AvatarBuilder
api-base="/api/avatars"
:user-id="$page.props.auth.user.id"
@build-complete="handleBuildComplete"
/>
<!-- View-only with animation controls -->
<AvatarViewer
:src="avatar.file_url"
:animations="true"
:controls="true"
:auto-rotate="true"
/>
</template>Using composables:
<script setup>
import { ref, onMounted } from 'vue'
import { useAvatar, useAvatarBuilder } from '@streetmesh/avatar-vue'
const canvasRef = ref(null)
const { init, load, playAnimation, animations } = useAvatar()
onMounted(() => {
init(canvasRef.value)
load('/avatars/my-avatar.vrm')
})
</script>
<template>
<div ref="canvasRef" class="w-full h-96"></div>
<select @change="playAnimation($event.target.value)">
<option v-for="anim in animations" :key="anim" :value="anim">
{{ anim }}
</option>
</select>
</template>import { AvatarBuilder, AvatarViewer, useAvatar } from '@streetmesh/avatar-react'
function AvatarPage({ user, avatar }) {
const handleBuildComplete = (result) => {
console.log('Avatar built:', result)
}
return (
<>
<AvatarBuilder
apiBase="/api/avatars"
userId={user.id}
onBuildComplete={handleBuildComplete}
/>
<AvatarViewer
src={avatar.file_url}
animations
controls
autoRotate
/>
</>
)
}
// Using hooks
function CustomViewer({ src }) {
const canvasRef = useRef(null)
const {
init,
load,
playAnimation,
stopAnimation,
animations,
isLoading,
} = useAvatar()
useEffect(() => {
if (canvasRef.current) {
init(canvasRef.current)
load(src)
}
}, [])
return (
<div>
<div ref={canvasRef} className="w-full h-96" />
{animations.map((anim) => (
<button key={anim} onClick={() => playAnimation(anim)}>
{anim}
</button>
))}
</div>
)
}<!-- Register the plugin in your app.js -->
<script>
import Alpine from 'alpinejs'
import { avatarPlugin } from '@streetmesh/avatar-alpine'
Alpine.plugin(avatarPlugin)
Alpine.start()
</script>
<!-- Avatar Viewer -->
<div x-data="avatarViewer({ src: '{{ $avatar->file_url }}', autoLoad: true })">
<div x-ref="canvas" class="w-full h-96"></div>
<div x-show="isLoading">Loading...</div>
<template x-if="animations.length > 0">
<select x-model="currentAnimation" @change="playAnimation(currentAnimation)">
<option value="">Select animation</option>
<template x-for="anim in animations" :key="anim">
<option :value="anim" x-text="anim"></option>
</template>
</select>
</template>
<button @click="stopAnimation()">Stop</button>
<button @click="resetCamera()">Reset Camera</button>
</div>
<!-- Avatar Builder -->
<div x-data="avatarBuilder({ apiBase: '/api/avatars' })" x-init="init(); loadParts()">
<div x-ref="preview" class="w-full h-96"></div>
<div class="grid grid-cols-4 gap-2">
<template x-for="part in getPartsByType('hair')" :key="part.id">
<button
:class="{ 'ring-2 ring-blue-500': isPartSelected(part.id) }"
@click="selectPart('hair', part.id)"
>
<img :src="part.thumbnailUrl" :alt="part.name">
<span x-text="part.name"></span>
</button>
</template>
</div>
<button
:disabled="!canBuild || isBuilding"
@click="build({ format: 'vrm_1' })"
>
<span x-show="isBuilding">Building...</span>
<span x-show="!isBuilding">Build Avatar</span>
</button>
</div>use StreetMesh\Avatars\Contracts\PartValidatorContract;
use StreetMesh\Avatars\Data\ValidationResult;
use StreetMesh\Avatars\Models\AvatarPart;
class CustomPartValidator implements PartValidatorContract
{
public function handles(): array
{
return ['custom_type'];
}
public function validate(AvatarPart $part, array $options = []): ValidationResult
{
// Your validation logic
return ValidationResult::success();
}
public function validateFile(string $filePath, array $options = []): ValidationResult
{
// File validation logic
return ValidationResult::success();
}
}
// Register in a service provider
use StreetMesh\Avatars\Services\ExtensionManager;
public function boot()
{
$this->app->make(ExtensionManager::class)
->registerValidator(new CustomPartValidator());
}use StreetMesh\Avatars\Contracts\ConverterContract;
use StreetMesh\Avatars\Enums\ExportFormat;
class FbxConverter implements ConverterContract
{
public function inputFormats(): array
{
return ['fbx'];
}
public function outputFormats(): array
{
return [ExportFormat::GLB, ExportFormat::VRM_1];
}
public function canConvert(string $inputFormat, ExportFormat $outputFormat): bool
{
return in_array($inputFormat, $this->inputFormats())
&& in_array($outputFormat, $this->outputFormats());
}
public function convert(
string $inputPath,
string $outputPath,
ExportFormat $outputFormat,
array $options = []
): bool {
// Conversion logic
return true;
}
}
// Register
$this->app->make(ExtensionManager::class)
->registerConverter(new FbxConverter());# Full installation
php artisan avatars:install
# Publish assets
php artisan avatars:publish --config
php artisan avatars:publish --migrations
php artisan avatars:publish --all
# Build sidecar
php artisan avatars:build-sidecar
php artisan avatars:build-sidecar --watch| Format | Extension | Description |
|---|---|---|
GLB |
.glb | Binary glTF 2.0 |
GLTF |
.gltf | JSON glTF 2.0 with separate assets |
VRM_0X |
.vrm | VRM 0.x format |
VRM_1 |
.vrm | VRM 1.0 format (recommended) |
MIT License. See LICENSE for details.