unityblenderavatar3ddeveloper-guide

Driving Unity and Blender Avatars from Anthropometric Data

· 6 min read · Martin Hejda

3D avatar systems in games, VR applications, and virtual fitting rooms need to represent a range of human body shapes. The standard approach is blendshapes (Unity) or shape keys (Blender) — morphing targets that deform a base mesh toward pre-sculpted extremes. Anthropometric measurements map naturally to these parameters: chest circumference controls the chest blendshape weight, waist circumference controls the waist weight, and so on.

Here’s how to get predicted body dimensions and translate them into avatar parameters.


Getting body dimensions for avatar customization

The dimensions most relevant for body shape representation are FLESH dimensions — circumferences that control the visual silhouette of the avatar:

import requests

def get_avatar_dimensions(
    gender: str,
    height_cm: float,
    weight_kg: float,
    region: str = "GLOBAL",
    body_build: str = "CIVILIAN"
) -> dict:
    """
    Predict body dimensions for avatar shape parameters.
    Returns both FLESH (circumferences) and BONE (skeletal) dimensions.
    """
    response = requests.post(
        "https://dimensionspot-bodysize-engine.p.rapidapi.com/v1/predict",
        json={
            "input_data": {
                "input_unit_system": "metric",
                "subject": {"gender": gender, "input_origin_region": region},
                "anchors": {
                    "body_height": int(height_cm * 10),  # cm → mm
                    "body_mass": weight_kg
                }
            },
            "output_settings": {
                "calculation": {
                    "target_region": region,
                    "body_build_type": body_build  # CIVILIAN, ATHLETIC, OVERWEIGHT
                },
                "requested_dimensions": {
                    "bundle": "FULL_BODY"
                },
                "output_format": {
                    "include_range_95": False,
                    "confidence_score_threshold": 50
                }
            }
        },
        headers={
            "X-RapidAPI-Key": "YOUR_API_KEY",
            "X-RapidAPI-Host": "dimensionspot-bodysize-engine.p.rapidapi.com"
        }
    )
    
    data = response.json()
    return {
        dim_id: d.get("value")
        for dim_id, d in data.get("body_dimensions", {}).items()
        if d.get("value") is not None
    }

Normalization: mm values to blendshape weights

Blendshape weights in Unity are typically in the range [0, 1] where 0 is the base mesh and 1 is full morphing toward the blendshape target. Some engines use [0, 100]. You need to normalize raw measurement values to this range.

The normalization requires knowing the expected population range for each dimension:

# Population range for normalization
# min_mm: corresponds to blendshape weight 0 (thin/small end)
# max_mm: corresponds to blendshape weight 1 (large/full end)
BLENDSHAPE_RANGES = {
    "chest_circumference": {
        "male":   {"min_mm": 800, "max_mm": 1300},
        "female": {"min_mm": 720, "max_mm": 1250}
    },
    "waist_circumference_natural": {
        "male":   {"min_mm": 650, "max_mm": 1300},
        "female": {"min_mm": 550, "max_mm": 1250}
    },
    "hip_circumference": {
        "male":   {"min_mm": 800, "max_mm": 1300},
        "female": {"min_mm": 800, "max_mm": 1350}
    },
    "thigh_circumference": {
        "male":   {"min_mm": 450, "max_mm": 750},
        "female": {"min_mm": 450, "max_mm": 780}
    },
    "neck_circumference": {
        "male":   {"min_mm": 320, "max_mm": 500},
        "female": {"min_mm": 280, "max_mm": 450}
    },
    "upper_arm_circumference": {
        "male":   {"min_mm": 250, "max_mm": 500},
        "female": {"min_mm": 230, "max_mm": 460}
    }
}

def dimension_to_blendshape_weight(
    dimension_id: str,
    value_mm: float,
    gender: str
) -> float:
    """
    Convert a predicted body dimension (mm) to a normalized blendshape weight [0, 1].
    """
    ranges = BLENDSHAPE_RANGES.get(dimension_id, {}).get(gender)
    
    if not ranges:
        return 0.5  # Default to midpoint if no range defined
    
    min_mm = ranges["min_mm"]
    max_mm = ranges["max_mm"]
    
    weight = (value_mm - min_mm) / (max_mm - min_mm)
    return max(0.0, min(1.0, weight))

def build_blendshape_params(
    dimensions: dict,
    gender: str,
    base_height_cm: float = 170.0,
    target_height_cm: float | None = None
) -> dict:
    """
    Build a complete set of blendshape parameters from predicted dimensions.
    
    base_height_cm: the height of the base mesh (the unscaled avatar)
    target_height_cm: the actual user's height (used for uniform scale)
    """
    params = {}
    
    # Body shape blendshape weights
    for dim_id, value_mm in dimensions.items():
        if dim_id in BLENDSHAPE_RANGES:
            weight = dimension_to_blendshape_weight(dim_id, value_mm, gender)
            params[dim_id] = weight
    
    # Height scaling (uniform scale factor)
    if target_height_cm and base_height_cm:
        params["uniform_scale"] = target_height_cm / base_height_cm
    
    return params

Unity C# implementation

// AvatarCustomizer.cs
using UnityEngine;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine.Networking;

[System.Serializable]
public class AvatarDimensions
{
    public float chest_circumference;
    public float waist_circumference_natural;
    public float hip_circumference;
    public float thigh_circumference;
    public float neck_circumference;
    public float upper_arm_circumference;
    public float body_height;  // in mm
}

public class AvatarCustomizer : MonoBehaviour
{
    [Header("Avatar References")]
    [SerializeField] private SkinnedMeshRenderer bodyRenderer;
    [SerializeField] private Transform avatarRoot;
    
    [Header("Base Mesh Settings")]
    [SerializeField] private float baseMeshHeightCm = 170f;
    
    // Blendshape index mapping (must match your avatar's blendshape names)
    private readonly Dictionary<string, int> blendshapeIndices = new()
    {
        { "chest_circumference", 0 },
        { "waist_circumference_natural", 1 },
        { "hip_circumference", 2 },
        { "thigh_circumference", 3 },
        { "neck_circumference", 4 },
        { "upper_arm_circumference", 5 }
    };
    
    public async Task ApplyMeasurements(
        string gender,
        float heightCm,
        float weightKg,
        string region = "GLOBAL")
    {
        // Call your backend proxy (never call the prediction API directly from Unity)
        AvatarDimensions dims = await FetchDimensionsFromBackend(gender, heightCm, weightKg, region);
        
        if (dims == null)
        {
            Debug.LogError("Failed to fetch avatar dimensions");
            return;
        }
        
        ApplyBlendshapes(dims, gender, heightCm);
    }
    
    private void ApplyBlendshapes(AvatarDimensions dims, string gender, float heightCm)
    {
        // Map each dimension to its blendshape weight
        var dimensionValues = new Dictionary<string, float>
        {
            { "chest_circumference", dims.chest_circumference },
            { "waist_circumference_natural", dims.waist_circumference_natural },
            { "hip_circumference", dims.hip_circumference },
            { "thigh_circumference", dims.thigh_circumference },
            { "neck_circumference", dims.neck_circumference },
            { "upper_arm_circumference", dims.upper_arm_circumference }
        };
        
        foreach (var pair in dimensionValues)
        {
            if (!blendshapeIndices.TryGetValue(pair.Key, out int idx))
                continue;
            
            float weight = NormalizeDimension(pair.Key, pair.Value, gender);
            // Unity blendshapes: 0 = base mesh, 100 = full morph
            bodyRenderer.SetBlendShapeWeight(idx, weight * 100f);
        }
        
        // Apply height scaling
        if (heightCm > 0 && baseMeshHeightCm > 0)
        {
            float scale = heightCm / baseMeshHeightCm;
            avatarRoot.localScale = new Vector3(scale, scale, scale);
        }
    }
    
    private float NormalizeDimension(string dimensionId, float valueMm, string gender)
    {
        // Simplified normalization ranges — expand to match BLENDSHAPE_RANGES above
        var ranges = dimensionId switch
        {
            "chest_circumference" when gender == "female"  => (720f, 1250f),
            "chest_circumference"                          => (800f, 1300f),
            "waist_circumference_natural" when gender == "female"  => (550f, 1250f),
            "waist_circumference_natural"                          => (650f, 1300f),
            "hip_circumference"   when gender == "female"  => (800f, 1350f),
            "hip_circumference"                            => (800f, 1300f),
            _ => (500f, 1200f)
        };
        
        float weight = (valueMm - ranges.Item1) / (ranges.Item2 - ranges.Item1);
        return Mathf.Clamp01(weight);
    }
    
    private async Task<AvatarDimensions> FetchDimensionsFromBackend(
        string gender, float heightCm, float weightKg, string region)
    {
        string url = $"https://api.yourapp.com/avatar/dimensions" +
                     $"?gender={gender}&height_cm={heightCm}&weight_kg={weightKg}&region={region}";
        
        using var request = UnityWebRequest.Get(url);
        request.SetRequestHeader("Authorization", $"Bearer {GetAuthToken()}");
        
        await request.SendWebRequest();
        
        if (request.result != UnityWebRequest.Result.Success)
        {
            Debug.LogError($"Avatar dimension request failed: {request.error}");
            return null;
        }
        
        return JsonUtility.FromJson<AvatarDimensions>(request.downloadHandler.text);
    }
    
    private string GetAuthToken()
    {
        // Return your app's current authentication token
        return PlayerPrefs.GetString("auth_token", "");
    }
}

Blender Python implementation (shape keys)

# blender_avatar_customizer.py
# Run inside Blender's scripting environment
import bpy
import math

# Shape key mapping — must match names in your Blender avatar
SHAPE_KEY_MAP = {
    "chest_circumference":    "Body.Chest",
    "waist_circumference_natural":    "Body.Waist",
    "hip_circumference":      "Body.Hips",
    "thigh_circumference":    "Body.Thighs",
    "upper_arm_circumference": "Body.Arms",
    "neck_circumference":     "Body.Neck"
}

BLENDSHAPE_RANGES_FEMALE = {
    "chest_circumference":    (720, 1250),
    "waist_circumference_natural":    (550, 1250),
    "hip_circumference":      (800, 1350),
    "thigh_circumference":    (450, 780),
    "upper_arm_circumference": (230, 460),
    "neck_circumference":     (280, 450)
}

def apply_dimensions_to_avatar(
    avatar_object_name: str,
    dimensions: dict,  # {dimension_id: value_mm}
    gender: str,
    height_cm: float,
    base_mesh_height_cm: float = 170.0
):
    """
    Apply body dimension predictions to a Blender avatar object.
    
    avatar_object_name: name of the mesh object in the Blender scene
    dimensions: dict of {dimension_id: value_mm} from prediction API
    """
    avatar = bpy.data.objects.get(avatar_object_name)
    if not avatar or not avatar.data.shape_keys:
        print(f"Avatar '{avatar_object_name}' not found or has no shape keys")
        return
    
    shape_keys = avatar.data.shape_keys.key_blocks
    ranges = BLENDSHAPE_RANGES_FEMALE if gender == "female" else {
        k: (v[0] + 80, v[1] + 50) for k, v in BLENDSHAPE_RANGES_FEMALE.items()
    }
    
    for dim_id, value_mm in dimensions.items():
        shape_key_name = SHAPE_KEY_MAP.get(dim_id)
        if not shape_key_name:
            continue
        
        shape_key = shape_keys.get(shape_key_name)
        if not shape_key:
            print(f"Shape key '{shape_key_name}' not found in avatar")
            continue
        
        # Normalize to [0, 1]
        r = ranges.get(dim_id, (500, 1200))
        weight = (value_mm - r[0]) / (r[1] - r[0])
        weight = max(0.0, min(1.0, weight))
        
        shape_key.value = weight
        print(f"Applied {dim_id}: {value_mm}mm → {shape_key_name} = {weight:.3f}")
    
    # Height scaling
    if height_cm and base_mesh_height_cm:
        scale = height_cm / base_mesh_height_cm
        avatar.scale = (scale, scale, scale)
        print(f"Applied height scale: {scale:.3f}")

# Example usage (call with actual dimension data from your backend):
# apply_dimensions_to_avatar(
#     "Avatar_Female",
#     {"chest_circumference": 924, "waist_circumference_natural": 726, "hip_circumference": 965},
#     "female",
#     168.0
# )

Body build archetypes

The prediction API supports body_build_type parameter with CIVILIAN, ATHLETIC, and OVERWEIGHT values. For games where players choose a character archetype, map these to corresponding API calls:

ARCHETYPE_TO_BUILD = {
    "warrior":  "ATHLETIC",
    "default":  "CIVILIAN",
    "merchant": "OVERWEIGHT",
    "scholar":  "CIVILIAN",
    "elder":    "CIVILIAN"
}

def get_archetype_dimensions(
    archetype: str,
    gender: str,
    height_cm: float,
    region: str = "GLOBAL"
) -> dict:
    """
    Get dimensions for a character archetype.
    Weight is estimated from archetype and height.
    """
    # Archetype → BMI approximation
    archetype_bmi = {
        "ATHLETIC": 22.0,
        "CIVILIAN": 24.0,
        "OVERWEIGHT": 30.0
    }
    
    build = ARCHETYPE_TO_BUILD.get(archetype.lower(), "CIVILIAN")
    bmi = archetype_bmi[build]
    
    # Estimate weight from archetype BMI and height
    height_m = height_cm / 100
    estimated_weight_kg = bmi * (height_m ** 2)
    
    return get_avatar_dimensions(gender, height_cm, estimated_weight_kg, region, build)

The key principle: body dimension prediction handles the measurement science; your avatar system handles the visual representation. The normalization step is where the two connect, and it requires knowing your specific avatar mesh’s range of deformation. Build that normalization table based on your actual mesh, not on generic values.

Try DimensionsPot

Free tier — 100 requests/month, no credit card required.

Get API on RapidAPI