gaminggame-devhow-tonpcanthropometryunity

How to Generate a Realistic NPC Population with Diverse Body Proportions

· 6 min read · Martin Hejda

Open-world games and crowd simulations often have a problem that’s easy to overlook until a reviewer points it out: all the NPCs look structurally identical. Same shoulder width. Same hip proportion. Same leg length. Scaled versions of one base mesh. It reads as a crowd, not a population.

Generating statistically realistic body diversity doesn’t require 3D scanning or photogrammetry. Height and weight — two numeric inputs — produce enough structural variation to make a crowd feel like it was drawn from a real population. DimensionsPot returns the full dimensional profile for any combination, and its stateless architecture means batch generation is just a collection of parallel HTTP requests.

This guide covers the generation pipeline: designing archetypes, generating parameter distributions, making batch API calls with proper rate limiting, mapping the returned dimensions to Unity’s humanoid rig, and storing results as game assets.


The generation pipeline

Archetype definition (region, gender, height/weight distribution, build type)

Parameter sampling (Gaussian random per archetype)

Batch API calls (async, batched to respect rate limits)

Dimension extraction (biacromial_breadth, hip_circumference, inseam_length, etc.)

Bone scale calculation (dimension / reference mesh dimension)

Stored as ScriptableObjects or JSON per NPC prefab

You run this pipeline offline — during development or level design — not at runtime. The generated dimension sets become part of your game assets.


Step 1: Define NPC archetypes

Each archetype describes a population: a region, gender, typical height and weight distribution, and body build type. The API’s regional calibration profiles produce culturally specific body proportions — East Asian NPCs will differ from North American ones at the same height and weight, because the underlying population statistics differ.

// NPCArchetype.cs
using UnityEngine;

[System.Serializable]
public class NPCArchetype
{
    public string name;

    // API parameters
    public string gender;           // "male" or "female"
    public string region;           // GLOBAL | EUROPE | ASIA_PACIFIC | AFRICA |
                                    // LATAM | INDIA | MIDDLE_EAST
    public string buildType;        // CIVILIAN | ATHLETIC | OVERWEIGHT

    // Height distribution for this archetype (cm)
    public float heightMeanCm;
    public float heightStdDevCm;

    // Weight distribution (kg)
    public float weightMeanKg;
    public float weightStdDevKg;
}

Example archetypes for a medieval fantasy city:

public static class CityArchetypes
{
    public static readonly NPCArchetype CityGuard = new NPCArchetype {
        name        = "City Guard",
        gender      = "male",
        region      = "EUROPE",
        buildType   = "ATHLETIC",
        heightMeanCm = 182f, heightStdDevCm = 4f,
        weightMeanKg = 90f,  weightStdDevKg = 8f,
    };

    public static readonly NPCArchetype MarketVendor = new NPCArchetype {
        name        = "Market Vendor",
        gender      = "female",
        region      = "EUROPE",
        buildType   = "CIVILIAN",
        heightMeanCm = 163f, heightStdDevCm = 6f,
        weightMeanKg = 66f,  weightStdDevKg = 10f,
    };

    public static readonly NPCArchetype EasternMerchant = new NPCArchetype {
        name        = "Eastern Merchant",
        gender      = "male",
        region      = "ASIA_PACIFIC",
        buildType   = "CIVILIAN",
        heightMeanCm = 172f, heightStdDevCm = 5f,
        weightMeanKg = 70f,  weightStdDevKg = 9f,
    };
}

Step 2: Sample parameters with Gaussian distribution

Uniform random produces unrealistic uniformity. Real populations follow approximately normal distributions for height and weight. Box-Muller transform gives you Gaussian sampling without external libraries:

public static class MathUtils
{
    private static System.Random rng = new System.Random();

    public static float GaussianRandom(float mean, float stdDev)
    {
        // Box-Muller transform
        float u1 = 1f - (float)rng.NextDouble();
        float u2 = 1f - (float)rng.NextDouble();
        float z  = Mathf.Sqrt(-2f * Mathf.Log(u1)) * Mathf.Sin(2f * Mathf.PI * u2);
        return mean + stdDev * z;
    }

    // Clamp to physiologically plausible range
    public static float SampleHeight(NPCArchetype a) =>
        Mathf.Clamp(GaussianRandom(a.heightMeanCm, a.heightStdDevCm), 145f, 215f);

    public static float SampleWeight(NPCArchetype a) =>
        Mathf.Clamp(GaussianRandom(a.weightMeanKg, a.weightStdDevKg), 40f, 160f);
}

Step 3: Batch API calls

DimensionsPot is stateless — every request is independent. This means you can run many calls in parallel, limited only by your rate tier. Batch them in groups of 10 with a brief delay between groups:

// NPCBodyGenerator.cs — attach to an Editor window or build pipeline tool
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using Newtonsoft.Json;

public class NPCBodyGenerator
{
    private static readonly HttpClient Http = new HttpClient();
    private const string API_URL = "https://dimensionspot-bodysize-engine.p.rapidapi.com/v1/predict";
    private const string RAPIDAPI_KEY = ""; // Load from EditorPrefs or env — never hardcode

    [System.Serializable]
    public class DimensionDetail
    {
        public float  value;
        public int    confidence_score;
        public string unit;
        public string label;
    }

    [System.Serializable]
    public class APIResponse
    {
        public Dictionary<string, DimensionDetail> body_dimensions;
    }

    [System.Serializable]
    public class NPCBodyData
    {
        public string archetypeName;
        public float  heightMm;
        public float  weightKg;
        public float  shoulder_width_mm;
        public float  hip_circumference_mm;
        public float  chest_circumference_mm;
        public float  waist_circumference_mm;
        public float  inseam_length_mm;
        public float  body_height_mm;
    }

    public async Task<List<NPCBodyData>> GenerateBatch(NPCArchetype archetype, int count)
    {
        var results  = new List<NPCBodyData>();
        const int batchSize = 10;

        for (int i = 0; i < count; i += batchSize)
        {
            int thisBatch = Math.Min(batchSize, count - i);
            var tasks = new List<Task<NPCBodyData>>(thisBatch);

            for (int j = 0; j < thisBatch; j++)
            {
                float heightCm = MathUtils.SampleHeight(archetype);
                float weightKg = MathUtils.SampleWeight(archetype);
                tasks.Add(FetchDimensions(archetype, heightCm, weightKg));
            }

            var batchResults = await Task.WhenAll(tasks);
            results.AddRange(batchResults);

            // Pause between batches — adjust based on your rate tier
            if (i + batchSize < count)
                await Task.Delay(150);
        }

        return results;
    }

    private async Task<NPCBodyData> FetchDimensions(NPCArchetype a, float heightCm, float weightKg)
    {
        var requestBody = new
        {
            input_data = new {
                input_unit_system = "metric",
                subject = new {
                    gender = a.gender,
                    input_origin_region = a.region,
                },
                anchors = new {
                    body_height = (int)(heightCm * 10), // cm → mm
                    body_mass   = weightKg,
                },
            },
            output_settings = new {
                calculation = new {
                    calculation_model = "AUTO",
                    target_region     = a.region,
                    body_build_type   = a.buildType,
                },
                requested_dimensions = new { bundle = "FULL_BODY" },
                output_format = new {
                    unit_system                = "metric",
                    include_range_95           = false,
                    confidence_score_threshold = 0,
                },
            },
        };

        var json    = JsonConvert.SerializeObject(requestBody);
        var content = new StringContent(json, Encoding.UTF8, "application/json");

        Http.DefaultRequestHeaders.Clear();
        Http.DefaultRequestHeaders.Add("x-rapidapi-key",  RAPIDAPI_KEY);
        Http.DefaultRequestHeaders.Add("x-rapidapi-host", "dimensionspot-bodysize-engine.p.rapidapi.com");

        var response = await Http.PostAsync(API_URL, content);
        response.EnsureSuccessStatusCode();

        var responseJson = await response.Content.ReadAsStringAsync();
        var apiData      = JsonConvert.DeserializeObject<APIResponse>(responseJson);

        var dims = apiData.body_dimensions ?? new Dictionary<string, DimensionDetail>();

        float GetValue(string key, float fallback) =>
            dims.TryGetValue(key, out var d) ? d.value : fallback;

        return new NPCBodyData {
            archetypeName          = a.name,
            heightMm               = heightCm * 10f,
            weightKg               = weightKg,
            body_height_mm         = heightCm * 10f,
            shoulder_width_mm      = GetValue("biacromial_breadth",          400f),
            chest_circumference_mm = GetValue("chest_circumference",         920f),
            waist_circumference_mm = GetValue("waist_circumference_natural", 780f),
            hip_circumference_mm   = GetValue("hip_circumference",           960f),
            inseam_length_mm       = GetValue("inseam_length",               790f),
        };
    }
}

Step 4: Apply dimensions to NPC bone rig

At runtime, assign the pre-generated body data to an NPC and scale its rig:

// NPCBodyScaler.cs — attach to each NPC prefab
using UnityEngine;

public class NPCBodyScaler : MonoBehaviour
{
    // These values match your base mesh's dimensions exactly
    [Header("Reference mesh dimensions (mm)")]
    public float refHeight         = 1750f;
    public float refShoulderWidth  = 400f;
    public float refHipCirc        = 940f;
    public float refInseam         = 790f;

    private Animator _animator;

    void Awake() => _animator = GetComponent<Animator>();

    public void ApplyBodyData(NPCBodyGenerator.NPCBodyData d)
    {
        // 1. Overall height via root scale
        float heightScale = d.body_height_mm / refHeight;
        transform.localScale = new Vector3(1f, heightScale, 1f);

        // 2. Shoulder width via spine bone
        var spine = _animator.GetBoneTransform(HumanBodyBones.Spine);
        if (spine != null)
        {
            float s = d.shoulder_width_mm / refShoulderWidth;
            spine.localScale = new Vector3(s, 1f, s);
        }

        // 3. Hip width via hips bone
        var hips = _animator.GetBoneTransform(HumanBodyBones.Hips);
        if (hips != null)
        {
            float hipWidth    = d.hip_circumference_mm / Mathf.PI;
            float refHipWidth = refHipCirc / Mathf.PI;
            float s = hipWidth / refHipWidth;
            hips.localScale = new Vector3(s, 1f, s);
        }

        // 4. Leg proportions via upper leg bones
        float legScale = d.inseam_length_mm / refInseam;
        var leftLeg  = _animator.GetBoneTransform(HumanBodyBones.LeftUpperLeg);
        var rightLeg = _animator.GetBoneTransform(HumanBodyBones.RightUpperLeg);
        if (leftLeg  != null) leftLeg.localScale  = new Vector3(1f, legScale, 1f);
        if (rightLeg != null) rightLeg.localScale = new Vector3(1f, legScale, 1f);
    }
}

Storing the results

Export the generated body data to JSON or ScriptableObjects during build time. Don’t regenerate at runtime unless you’re doing procedural world generation where NPCs are created on demand.

// Save generated batch to JSON file in project
using System.IO;
using UnityEditor;

void SaveBatch(List<NPCBodyGenerator.NPCBodyData> batch, string filename)
{
    var json = JsonConvert.SerializeObject(batch, Formatting.Indented);
    File.WriteAllText(Path.Combine(Application.dataPath, "GeneratedNPCs", filename), json);
    AssetDatabase.Refresh();
}

At runtime, load from the JSON asset and assign to NPC instances as they spawn. This keeps generation off the critical path and makes your NPC population reproducible across builds.

Try DimensionsPot

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

Get API on RapidAPI