Chuyển đến nội dung chính

レッスン 17: 3D アバターの生成 — 仮想フィッティング アバターの作成

身体パラメータから 3D アバターを生成: SMPL/SMPL-X モデル、テクスチャ マッピング、身体形状モーフィング。ブラウザーレンダリングのための Three.js/WebGL との統合。

🧠 AI と ML — レッスン 16 レッスン 17: 3D アバターの生成 — アバターの作成 バーチャル試着

AI の活用: ファッションとプリント オン デマンド向けの AI プラットフォームの構築

パート 5: 仮想試着とコンピュータ ビジョン

xdev.asia

はじめに

この記事では、前の記事の SMPL パラメータから、ユーザーが仮想アイテムを試着できるように 3D アバターを作成します。リアルなボディ メッシュには SMPL-X モデルを使用し、肌の外観にはテクスチャ マッピングを使用し、ブラウザ レンダリングのために Three.js にエクスポートします。


1. SMPL-X ボディモデル

SMPLとは何ですか?

SMPL (Skinned Multi-Person Linear Model):
- Parametric body model cho 3D human body
- Input: shape β (10 params) + pose θ (72 params)
- Output: 3D mesh (6890 vertices, 13776 faces)

SMPL-X (eXpressive):
- Mở rộng SMPL với hands và face
- 10,475 vertices
- Hỗ trợ: body + hands + face expressions

ボディメッシュの生成

import smplx
import torch
import trimesh

class AvatarGenerator:
    """Tạo 3D avatar từ body parameters"""

    def __init__(self):
        self.body_model = smplx.create(
            model_path="models/smplx",
            model_type="smplx",
            gender="neutral",
            num_betas=10,
            use_pca=True,
            num_pca_comps=12,
        )

    def generate_avatar(
        self, smpl_params: SMPLParams
    ) -> AvatarMesh:
        # Set body shape
        betas = torch.tensor(
            smpl_params.betas, dtype=torch.float32
        ).unsqueeze(0)

        # Default T-pose
        body_pose = torch.zeros(1, 63)  # 21 joints × 3

        # Forward pass
        output = self.body_model(
            betas=betas,
            body_pose=body_pose,
            return_verts=True,
        )

        vertices = output.vertices.detach().numpy()[0]
        faces = self.body_model.faces

        # Create trimesh
        mesh = trimesh.Trimesh(
            vertices=vertices,
            faces=faces,
            process=False,
        )

        return AvatarMesh(
            mesh=mesh,
            vertices=vertices,
            faces=faces,
            joints=output.joints.detach().numpy()[0],
        )

2. テクスチャ マッピング

class AvatarTexture:
    """Apply skin texture lên avatar"""

    DEFAULT_TEXTURES = {
        "light": "textures/skin_light.png",
        "medium": "textures/skin_medium.png",
        "dark": "textures/skin_dark.png",
        "neutral": "textures/skin_neutral.png",
    }

    def apply_texture(
        self,
        mesh: trimesh.Trimesh,
        skin_tone: str = "neutral",
    ) -> trimesh.Trimesh:
        # Load UV coordinates
        uv_coords = self._load_uv_map()

        # Load texture image
        texture_path = self.DEFAULT_TEXTURES[skin_tone]
        texture = Image.open(texture_path)

        # Apply texture
        material = trimesh.visual.texture.SimpleMaterial(
            image=texture
        )
        color_visuals = trimesh.visual.TextureVisuals(
            uv=uv_coords,
            material=material,
        )
        mesh.visual = color_visuals

        return mesh

3. ポーズの調整

class PoseController:
    """Điều chỉnh pose cho avatar"""

    PRESET_POSES = {
        "t_pose": {
            "description": "Arms stretched out horizontally",
            "body_pose": None,  # Default SMPL T-pose
        },
        "standing": {
            "description": "Casual standing pose",
            "body_pose": "poses/standing_casual.npy",
        },
        "walking": {
            "description": "Mid-walk pose",
            "body_pose": "poses/walking.npy",
        },
        "hands_on_hips": {
            "description": "Hands on hips, confident",
            "body_pose": "poses/hands_on_hips.npy",
        },
    }

    def set_pose(
        self,
        avatar: AvatarMesh,
        pose_name: str = "standing",
    ) -> AvatarMesh:
        pose_data = self.PRESET_POSES[pose_name]

        if pose_data["body_pose"] is None:
            body_pose = torch.zeros(1, 63)
        else:
            body_pose = torch.tensor(
                np.load(pose_data["body_pose"]),
                dtype=torch.float32,
            ).unsqueeze(0)

        # Re-generate mesh with pose
        output = self.body_model(
            betas=avatar.betas,
            body_pose=body_pose,
            return_verts=True,
        )

        return AvatarMesh(
            mesh=trimesh.Trimesh(
                vertices=output.vertices.detach().numpy()[0],
                faces=self.body_model.faces,
            ),
            vertices=output.vertices.detach().numpy()[0],
            faces=self.body_model.faces,
            joints=output.joints.detach().numpy()[0],
            pose_name=pose_name,
        )

4. Web 用にエクスポート (Three.js)

class WebExporter:
    """Export avatar mesh cho Three.js rendering"""

    def export_glb(
        self, avatar: AvatarMesh, output_path: str
    ):
        """Export to GLB format (binary glTF)"""
        avatar.mesh.export(output_path, file_type="glb")

    def export_for_threejs(
        self, avatar: AvatarMesh
    ) -> dict:
        """Export as JSON cho Three.js BufferGeometry"""
        vertices = avatar.vertices.flatten().tolist()
        normals = avatar.mesh.vertex_normals.flatten().tolist()
        indices = avatar.faces.flatten().tolist()
        uv = avatar.mesh.visual.uv.flatten().tolist() \
            if hasattr(avatar.mesh.visual, 'uv') else []

        return {
            "vertices": vertices,
            "normals": normals,
            "indices": indices,
            "uv": uv,
            "joints": avatar.joints.tolist(),
        }

Three.js フロントエンド (スケルトン)

// Frontend: Load and render avatar
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

class AvatarViewer {
    constructor(container) {
        this.scene = new THREE.Scene();
        this.camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100);
        this.renderer = new THREE.WebGLRenderer({ antialias: true });
        this.controls = new OrbitControls(
            this.camera, this.renderer.domElement
        );

        container.appendChild(this.renderer.domElement);
        this.setupLighting();
    }

    async loadAvatar(glbUrl) {
        const loader = new GLTFLoader();
        const gltf = await loader.loadAsync(glbUrl);

        this.avatarMesh = gltf.scene;
        this.scene.add(this.avatarMesh);

        // Center camera on avatar
        const box = new THREE.Box3().setFromObject(this.avatarMesh);
        const center = box.getCenter(new THREE.Vector3());
        this.camera.lookAt(center);
    }

    setupLighting() {
        const ambient = new THREE.AmbientLight(0xffffff, 0.5);
        const directional = new THREE.DirectionalLight(0xffffff, 0.8);
        directional.position.set(5, 5, 5);
        this.scene.add(ambient, directional);
    }

    animate() {
        requestAnimationFrame(() => this.animate());
        this.controls.update();
        this.renderer.render(this.scene, this.camera);
    }
}

5. アバター API

from fastapi import FastAPI, UploadFile

app = FastAPI()

@app.post("/api/v1/avatar/generate")
async def generate_avatar(
    height: float,
    weight: float,
    chest: float | None = None,
    waist: float | None = None,
    shoulder: float | None = None,
    skin_tone: str = "neutral",
    pose: str = "standing",
):
    # 1. Estimate body
    body = await body_pipeline.estimate(
        height=height, weight=weight,
        chest=chest, waist=waist, shoulder=shoulder,
    )

    # 2. Generate avatar
    avatar = avatar_generator.generate_avatar(body.smpl_params)

    # 3. Apply texture
    avatar.mesh = texture_mapper.apply_texture(
        avatar.mesh, skin_tone
    )

    # 4. Set pose
    avatar = pose_controller.set_pose(avatar, pose)

    # 5. Export GLB
    glb_path = f"avatars/{uuid4()}.glb"
    exporter.export_glb(avatar, glb_path)

    return {
        "avatar_url": f"/storage/{glb_path}",
        "measurements": body.measurements.to_dict(),
    }

概要

3D アバターの生成:

  1. SMPL-X — パラメトリック ボディ モデル、10,475 頂点
  2. テクスチャ マッピング — 肌色のテクスチャ
  3. ポーズのプリセット — 立ったり、歩いたり、腰に手を当てたり
  4. GLB エクスポート — Web 配信用のバイナリ glTF
  5. Three.js — ブラウザーでのリアルタイム レンダリング

次の記事: 衣服のレンダリング — 布地シミュレーションを使用してアバターにシャツをレンダリングします。