Player avatars are almost always a compromise. Character creators offer sliders — tall/short, thin/wide, muscular/soft — but the result is rarely the player. Sports games let you import a photo, but the pipeline is fragile and requires camera access. VR games work best with accurate body proportions, but almost none ask for them at onboarding.
The missing piece is simple: height and weight. Every player knows their approximate height and weight. From those two numbers, you can derive shoulder width, hip proportions, leg length, arm reach, torso depth, and 120 more dimensions — all without a photo, a camera, or any biometric capture.
This guide builds the full pipeline: a server endpoint that calls DimensionsPot, a Unity C# avatar scaler, and the data persistence pattern that stores proportions (not biometrics) in the player profile.
What the API provides for avatar work
The FULL_BODY bundle returns all 130 dimensions, which gives you everything needed for a complete humanoid rig. The most impactful for visual avatar fidelity:
| Dimension | Avatar application |
|---|---|
| Height input (mm) | Overall scale |
biacromial_breadth | Shoulder breadth, arm spread |
hip_circumference | Hip bone width and depth |
chest_circumference | Chest/torso shape |
waist_circumference_natural | Waist taper |
inseam_length | Leg length proportion |
arm_length_total | Reach in VR hand placement |
head_circumference | Head scale (if adjustable) |
neck_circumference | Neck thickness |
For VR specifically, arm_length_mm is critical: it determines where virtual hands appear in the world relative to the headset, which directly affects immersion and interaction accuracy.
Step 1: Server endpoint (Node.js)
The API key stays on the server. The endpoint receives gender, height, and weight; calls DimensionsPot; and returns the dimensional profile to the game client. It does not log or store the inputs.
// avatarDimensions.js — Express endpoint
const express = require('express');
const router = express.Router();
const API_URL = 'https://dimensionspot-bodysize-engine.p.rapidapi.com/v1/predict';
const RAPIDAPI_KEY = process.env.RAPIDAPI_KEY;
// Per-player dimension cache — keyed by playerId
// Avoids recalling the API on every login for players who haven't changed their inputs
const dimensionCache = new Map();
router.post('/player/avatar-dimensions', async (req, res) => {
const { playerId, gender, height_cm, weight_kg } = req.body;
if (!playerId || !gender || !height_cm || !weight_kg) {
return res.status(400).json({ error: 'playerId, gender, height_cm, and weight_kg are required' });
}
const cacheKey = `${gender}-${Math.round(height_cm * 10)}-${parseFloat(weight_kg)}`;
if (!dimensionCache.has(cacheKey)) {
const heightMm = Math.round(parseFloat(height_cm) * 10);
const weightKg = parseFloat(weight_kg);
const apiRes = await fetch(API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-rapidapi-key': RAPIDAPI_KEY,
'x-rapidapi-host': 'dimensionspot-bodysize-engine.p.rapidapi.com',
},
body: JSON.stringify({
input_data: {
input_unit_system: 'metric',
subject: { gender, input_origin_region: 'GLOBAL' },
anchors: { body_height: heightMm, body_mass: weightKg },
},
output_settings: {
calculation: {
calculation_model: 'AUTO',
target_region: 'GLOBAL',
body_build_type: 'CIVILIAN',
},
requested_dimensions: { bundle: 'FULL_BODY' },
output_format: {
unit_system: 'metric',
confidence_score_threshold: 0,
include_range_95: false,
include_iso_codes: false,
},
},
}),
});
if (!apiRes.ok) return res.status(502).json({ error: 'Prediction service unavailable' });
const data = await apiRes.json();
const bd = data.body_dimensions;
// Map API dimension keys to the avatar struct fields expected by the game client.
// body_height is the input anchor (not in body_dimensions), so pass heightMm directly.
const dims = {
body_height_mm: heightMm,
shoulder_width_mm: bd.biacromial_breadth?.value ?? 0,
hip_circumference_mm: bd.hip_circumference?.value ?? 0,
chest_circumference_mm: bd.chest_circumference?.value ?? 0,
waist_circumference_mm: bd.waist_circumference_natural?.value ?? 0,
inseam_length_mm: bd.inseam_length?.value ?? 0,
arm_length_mm: bd.arm_length_total?.value ?? 0,
head_circumference_mm: bd.head_circumference?.value ?? 0,
neck_circumference_mm: bd.neck_circumference?.value ?? 0,
};
dimensionCache.set(cacheKey, dims);
}
const dims = dimensionCache.get(cacheKey);
// Persist ONLY the derived dimensions to the player profile — not height, weight, or gender
// await db.players.update(playerId, { avatarDimensions: dims });
res.json({ dimensions: dims });
});
module.exports = router;
Step 2: Player onboarding UI
Keep the measurement form as part of the initial character creation or settings. Frame it as “personalise your avatar”:
┌─────────────────────────────────┐
│ Personalize your avatar │
│ │
│ Height [ 170 ] cm │
│ Weight [ 65 ] kg │
│ Gender [Female ▼] │
│ │
│ [ Create my avatar ] │
│ │
│ Your measurements are used to │
│ shape your avatar. They are │
│ never stored or shared. │
└─────────────────────────────────┘
The privacy note matters — players are more willing to share height and weight when they understand what happens with the data.
Step 3: Unity C# — requesting and applying dimensions
// AvatarPersonalizer.cs
using System;
using System.Collections;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;
using Newtonsoft.Json;
[System.Serializable]
public class AvatarDimensions
{
public float body_height_mm;
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 arm_length_mm;
public float head_circumference_mm;
public float neck_circumference_mm;
}
[System.Serializable]
public class DimensionsResponse
{
public AvatarDimensions dimensions;
}
public class AvatarPersonalizer : MonoBehaviour
{
[Header("Your game server — not the DimensionsPot API directly")]
public string serverEndpoint = "https://your-game-server.com/player/avatar-dimensions";
[Header("Reference mesh dimensions — must match your base character model")]
public float refHeight = 1750f; // mm
public float refShoulderWidth = 410f;
public float refHipCirc = 940f;
public float refChestCirc = 920f;
public float refInseam = 790f;
public float refArmLength = 590f;
public float refHeadCirc = 570f;
[Header("Rig references — assign in Inspector")]
public Transform characterRoot;
public Animator animator;
public void PersonalizeAvatar(string playerId, string gender, float heightCm, float weightKg)
{
StartCoroutine(FetchAndApply(playerId, gender, heightCm, weightKg));
}
private IEnumerator FetchAndApply(string playerId, string gender, float heightCm, float weightKg)
{
var payload = JsonConvert.SerializeObject(new {
playerId, gender,
height_cm = heightCm,
weight_kg = weightKg,
});
using var req = new UnityWebRequest(serverEndpoint, "POST");
req.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(payload));
req.downloadHandler = new DownloadHandlerBuffer();
req.SetRequestHeader("Content-Type", "application/json");
// Add your game's auth header here
// req.SetRequestHeader("Authorization", "Bearer " + GameAuth.Token);
yield return req.SendWebRequest();
if (req.result != UnityWebRequest.Result.Success)
{
Debug.LogWarning($"Avatar personalisation failed: {req.error}");
yield break;
}
var response = JsonConvert.DeserializeObject<DimensionsResponse>(req.downloadHandler.text);
ApplyDimensions(response.dimensions);
}
private void ApplyDimensions(AvatarDimensions d)
{
// 1. Height — scale the root transform vertically
float heightScale = d.body_height_mm / refHeight;
characterRoot.localScale = new Vector3(1f, heightScale, 1f);
// 2. Shoulder width — spine bone drives shoulder breadth
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 — hips bone
var hips = animator.GetBoneTransform(HumanBodyBones.Hips);
if (hips != null)
{
float hipDiam = d.hip_circumference_mm / Mathf.PI;
float refHipDiam = refHipCirc / Mathf.PI;
float s = hipDiam / refHipDiam;
hips.localScale = new Vector3(s, 1f, s);
}
// 4. Leg length — upper leg bones
float legScale = d.inseam_length_mm / refInseam;
SetBoneYScale(HumanBodyBones.LeftUpperLeg, legScale);
SetBoneYScale(HumanBodyBones.RightUpperLeg, legScale);
// 5. Arm length — upper arm bones
float armScale = d.arm_length_mm / refArmLength;
SetBoneYScale(HumanBodyBones.LeftUpperArm, armScale);
SetBoneYScale(HumanBodyBones.RightUpperArm, armScale);
// 6. Head size (if your rig allows it)
var head = animator.GetBoneTransform(HumanBodyBones.Head);
if (head != null)
{
float s = d.head_circumference_mm / refHeadCirc;
head.localScale = new Vector3(s, s, s);
}
}
private void SetBoneYScale(HumanBodyBones bone, float yScale)
{
var t = animator.GetBoneTransform(bone);
if (t != null) t.localScale = new Vector3(1f, yScale, 1f);
}
}
VR-specific: arm length and hand placement
In VR, hand position is determined by controller tracking, but the avatar’s virtual hands need to match the physical controller position. If the arm bones are wrong length, virtual hands float ahead of or behind the real hands — breaking presence immediately.
After applying ApplyDimensions, override hand position to match the tracked controllers:
void Update()
{
if (_vrMode && _dimensionsApplied)
{
// Override hand bone positions to match tracked controller positions
// XR Toolkit:
var leftHandBone = animator.GetBoneTransform(HumanBodyBones.LeftHand);
var rightHandBone = animator.GetBoneTransform(HumanBodyBones.RightHand);
if (leftHandBone != null) leftHandBone.position = leftController.position;
if (rightHandBone != null) rightHandBone.position = rightController.position;
}
}
The bone-length scaling gets the avatar’s proportions right at rest pose. Runtime IK ensures hands match controller tracking during play.
Persisting across sessions
After a player sets their dimensions once, don’t ask again. Persist the AvatarDimensions object in your player profile:
// When dimensions are first set
string json = JsonConvert.SerializeObject(dimensions);
PlayerPrefs.SetString("avatar_dimensions", json);
// On subsequent loads
string stored = PlayerPrefs.GetString("avatar_dimensions", null);
if (!string.IsNullOrEmpty(stored))
{
var dims = JsonConvert.DeserializeObject<AvatarDimensions>(stored);
ApplyDimensions(dims); // No API call needed
}
Store the AvatarDimensions struct — not the height and weight inputs. The struct contains derived body proportions, not the biometric inputs that generated them. If the player updates their measurements, invalidate the cache and regenerate.