Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

StreetMesh Avatars

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.

Requirements

  • PHP 8.2+
  • Laravel 11.x or 12.x
  • Node.js 20+
  • npm or pnpm

Installation

composer require streetmesh/avatars

Run the install command:

php artisan avatars:install

This will:

  • Publish the configuration file
  • Publish database migrations
  • Create storage directories
  • Install npm dependencies
  • Build the Node.js sidecar

Run migrations:

php artisan migrate

Framework-Specific Installation

The 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-alpine

Configuration

Publish the config file if not already done:

php artisan avatars:publish --config

Key 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'],
    ],
];

Backend Usage

Fluent API

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();

Events

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,
    ],
];

REST API

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

Frontend Usage

Vue 3 (Inertia)

<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>

React (Inertia)

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>
    )
}

Alpine.js (Livewire)

<!-- 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>

Extending the Package

Custom Part Validator

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());
}

Custom Converter

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());

Artisan Commands

# 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

Export Formats

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)

License

MIT License. See LICENSE for details.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages