はじめに
こんにちは、Aetorizです。
今日はバーチャルケモミミ Advent Calendar 2025の25日目の記事です!
この記事では
「Unity Editor で 0 からアバターを組み立てて、最終的に VRChat にアップロードするまで」
を、先のことを考えずにそのまま進めていった記録です。
記事を書きながらその時思ったことを垂れ流しで書いています。
さっそく作ってみる
ますはプロジェクトの作成。
liltoonとか入れといた方が良い気がするけどまぁ後で考えよう。

綺麗なUnity、人の画面共有とか見ると皆結構レイアウト変えてる気がする。

間違えた、Editorフォルダを作ってその中に入れるのを忘れないように。
対策しないとPlay時compileに失敗して面倒。

一旦ボタンを用意する。
using UnityEditor;
using UnityEngine;
namespace OneAvatar.Editor
{
public class CreateAvatar : EditorWindow
{
// 上のメニューにエディタ表示用のボタンを追加
[MenuItem("OneAvatar/Create Avatar")]
public static void ShowWindow()
{
GetWindow<CreateAvatar>("あばたーをつくろう");
}
private void OnGUI()
{
// GUIにボタンを追加
if (GUILayout.Button("テスト"))
{
CreateNewAvatar();
}
}
private void CreateNewAvatar()
{
// 実装
}
}
}
Avatar(Componentの方)を作る
まず人を作ろう
人の関節とかの位置決めるの面倒くさいし、Unity内から適当に探してくるかー。
それっぽいのあるじゃーん。

EditorResourceから取得する。
private void CreateNewAvatar()
{
// 人型に成形
var avatar = new GameObject("NewAvatar");
var defAvatar = AssetDatabase.GetBuiltinExtraResource<Avatar>("DefaultAvatarAvatar.avatar");
// Humanoidデータか確認
if (defAvatar == null || !defAvatar.isHuman)
{
Debug.Log("ひとじゃないかも");
return;
}
var skeletonBones = defAvatar.humanDescription.skeleton;
foreach (var bone in skeletonBones)
{
// 位置回転が入ってるか確認
var boneObj = new GameObject(bone.name)
{
transform =
{
parent = avatar.transform,
position = bone.position,
rotation = bone.rotation
}
};
}
}
そもそもunity default resourcesじゃなくてunity editor resourcesから持ってくるならEditorResources.Loadを使うべきなのでは?

だめそう。
仕方ないので一旦GameObjectを先に取得してAvatarを引っ張ってくる。
var defAvatarObject = UnityEditor.Experimental.EditorResources.Load("DefaultAvatar", typeof(GameObject)) as GameObject;
var defAvatar = defAvatarObject?.GetComponentInChildren<Animator>()?.avatar;
Debug.Log(defAvatar);
戻して確認。
private void CreateNewAvatar()
{
// 人型に成形
var avatar = new GameObject("NewAvatar");
var defAvatarObject = UnityEditor.Experimental.EditorResources.Load<GameObject>("DefaultAvatar");
var defAvatar = defAvatarObject?.GetComponentInChildren<Animator>()?.avatar;
// Humanoidデータか確認
if (defAvatar == null || !defAvatar.isHuman)
{
Debug.Log("ひとじゃないかも");
return;
}
var skeletonBones = defAvatar.humanDescription.skeleton;
foreach (var bone in skeletonBones)
{
// 位置回転が入ってるか確認
var boneObj = new GameObject(bone.name)
{
transform =
{
parent = avatar.transform,
position = bone.position,
rotation = bone.rotation
}
};
// ボーンの位置に目印をつける
var sp = GameObject.CreatePrimitive(PrimitiveType.Cube);
sp.transform.localScale = Vector3.one * 0.1f;
sp.transform.parent = boneObj.transform;
sp.transform.localPosition = Vector3.zero;
sp.transform.localRotation = Quaternion.identity;
}
}

あー、格納されてるのLocalPositionだったっけ、親子関係復元しないとだ。
まぁ値は入ってるっぽいし次の処理入っちゃおう。
いや待てよ……?
だったら元のGameObjectそのまま参照したらよくないか?
というか元からあるAvatarを参照するのは0からと言えるのか?
やっぱやめやめ
気合で1から並べよう。
一旦親子関係とVisual用のCubeを並べる。
UnityのHumanoidBodyBoneからLastBoneとUpperChest、Jawを抜いて処理する。
こんなことばっかりしてるから完成しないんだよなー。
private void CreateHumanoidAvatar()
{
var avatarRoot = new GameObject("root");
// 管理用の辞書を用意
var boneDictionary = new Dictionary<HumanBodyBones, Transform>();
// すべてのボーン列挙を取得(LastBone,UpperChest,Jawを除く)
var boneEnums =
(Enum.GetValues(typeof(HumanBodyBones)) as IEnumerable<HumanBodyBones>).Where(b =>
b != HumanBodyBones.LastBone &&
b != HumanBodyBones.UpperChest &&
b != HumanBodyBones.Jaw).ToArray();
// GameObjectを全て作成
foreach (var bone in boneEnums)
{
var boneObject = new GameObject(bone.ToString());
boneDictionary[bone] = boneObject.transform;
var visual = GameObject.CreatePrimitive(PrimitiveType.Cube);
visual.transform.SetParent(boneObject.transform, false);
visual.transform.localScale = Vector3.one * 0.1f;
DestroyImmediate(visual.GetComponent<BoxCollider>());
}
// ボーンの親子関係を設定
foreach (var bone in boneEnums)
{
boneDictionary[bone].parent = bone switch
{
HumanBodyBones.Hips => avatarRoot.transform,
HumanBodyBones.LeftUpperLeg => boneDictionary[HumanBodyBones.Hips],
HumanBodyBones.RightUpperLeg => boneDictionary[HumanBodyBones.Hips],
HumanBodyBones.Spine => boneDictionary[HumanBodyBones.Hips],
HumanBodyBones.LeftLowerLeg => boneDictionary[HumanBodyBones.LeftUpperLeg],
HumanBodyBones.RightLowerLeg => boneDictionary[HumanBodyBones.RightUpperLeg],
HumanBodyBones.LeftFoot => boneDictionary[HumanBodyBones.LeftLowerLeg],
HumanBodyBones.RightFoot => boneDictionary[HumanBodyBones.RightLowerLeg],
HumanBodyBones.Chest => boneDictionary[HumanBodyBones.Spine],
HumanBodyBones.Neck => boneDictionary[HumanBodyBones.Chest],
HumanBodyBones.Head => boneDictionary[HumanBodyBones.Neck],
HumanBodyBones.LeftShoulder => boneDictionary[HumanBodyBones.Chest],
HumanBodyBones.RightShoulder => boneDictionary[HumanBodyBones.Chest],
HumanBodyBones.LeftUpperArm => boneDictionary[HumanBodyBones.LeftShoulder],
HumanBodyBones.RightUpperArm => boneDictionary[HumanBodyBones.RightShoulder],
HumanBodyBones.LeftLowerArm => boneDictionary[HumanBodyBones.LeftUpperArm],
HumanBodyBones.RightLowerArm => boneDictionary[HumanBodyBones.RightUpperArm],
HumanBodyBones.LeftHand => boneDictionary[HumanBodyBones.LeftLowerArm],
HumanBodyBones.RightHand => boneDictionary[HumanBodyBones.RightLowerArm],
HumanBodyBones.LeftEye => boneDictionary[HumanBodyBones.Head],
HumanBodyBones.RightEye => boneDictionary[HumanBodyBones.Head],
HumanBodyBones.LeftToes => boneDictionary[HumanBodyBones.LeftFoot],
HumanBodyBones.RightToes => boneDictionary[HumanBodyBones.RightFoot],
HumanBodyBones.LeftIndexDistal => boneDictionary[HumanBodyBones.LeftHand],
HumanBodyBones.LeftIndexIntermediate => boneDictionary[HumanBodyBones.LeftIndexDistal],
HumanBodyBones.LeftIndexProximal => boneDictionary[HumanBodyBones.LeftIndexIntermediate],
HumanBodyBones.LeftMiddleDistal => boneDictionary[HumanBodyBones.LeftHand],
HumanBodyBones.LeftMiddleIntermediate => boneDictionary[HumanBodyBones.LeftMiddleDistal],
HumanBodyBones.LeftMiddleProximal => boneDictionary[HumanBodyBones.LeftMiddleIntermediate],
HumanBodyBones.LeftRingDistal => boneDictionary[HumanBodyBones.LeftHand],
HumanBodyBones.LeftRingIntermediate => boneDictionary[HumanBodyBones.LeftRingDistal],
HumanBodyBones.LeftRingProximal => boneDictionary[HumanBodyBones.LeftRingIntermediate],
HumanBodyBones.LeftLittleDistal => boneDictionary[HumanBodyBones.LeftHand],
HumanBodyBones.LeftLittleIntermediate => boneDictionary[HumanBodyBones.LeftLittleDistal],
HumanBodyBones.LeftLittleProximal => boneDictionary[HumanBodyBones.LeftLittleIntermediate],
HumanBodyBones.LeftThumbDistal => boneDictionary[HumanBodyBones.LeftHand],
HumanBodyBones.LeftThumbIntermediate => boneDictionary[HumanBodyBones.LeftThumbDistal],
HumanBodyBones.LeftThumbProximal => boneDictionary[HumanBodyBones.LeftThumbIntermediate],
HumanBodyBones.RightIndexDistal => boneDictionary[HumanBodyBones.RightHand],
HumanBodyBones.RightIndexIntermediate => boneDictionary[HumanBodyBones.RightIndexDistal],
HumanBodyBones.RightIndexProximal => boneDictionary[HumanBodyBones.RightIndexIntermediate],
HumanBodyBones.RightMiddleDistal => boneDictionary[HumanBodyBones.RightHand],
HumanBodyBones.RightMiddleIntermediate => boneDictionary[HumanBodyBones.RightMiddleDistal],
HumanBodyBones.RightMiddleProximal => boneDictionary[HumanBodyBones.RightMiddleIntermediate],
HumanBodyBones.RightRingDistal => boneDictionary[HumanBodyBones.RightHand],
HumanBodyBones.RightRingIntermediate => boneDictionary[HumanBodyBones.RightRingDistal],
HumanBodyBones.RightRingProximal => boneDictionary[HumanBodyBones.RightRingIntermediate],
HumanBodyBones.RightLittleDistal => boneDictionary[HumanBodyBones.RightHand],
HumanBodyBones.RightLittleIntermediate => boneDictionary[HumanBodyBones.RightLittleDistal],
HumanBodyBones.RightLittleProximal => boneDictionary[HumanBodyBones.RightLittleIntermediate],
HumanBodyBones.RightThumbDistal => boneDictionary[HumanBodyBones.RightHand],
HumanBodyBones.RightThumbIntermediate => boneDictionary[HumanBodyBones.RightThumbDistal],
HumanBodyBones.RightThumbProximal => boneDictionary[HumanBodyBones.RightThumbIntermediate],
_ => null
};
boneDictionary[bone].SetSiblingIndex(0);
}
}
生成されたものを人っぽく並び替え、

反対側にコピーする機能を作っていい感じに。
private void MirrorHumanBone()
{
var boneEnums =
(Enum.GetValues(typeof(HumanBodyBones)) as IEnumerable<HumanBodyBones>).Where(b =>
b != HumanBodyBones.LastBone &&
b != HumanBodyBones.UpperChest &&
b != HumanBodyBones.Jaw).ToArray();
var bones = sourceAvatar.GetComponentsInChildren<Transform>();
foreach (var bone in boneEnums)
{
if (bone.ToString().StartsWith("Right"))
{
var leftBoneName = bone.ToString().Replace("Right", "Left");
// 全ての子から該当ボーンを探す
var sourceBone = bones.First(b => b.name == bone.ToString());
var targetBone = bones.First(b => b.name == leftBoneName);
if (sourceBone != null && targetBone != null)
{
MirrorBoneTransform(sourceBone, targetBone);
targetBone.Find("Cube").localScale = sourceBone.Find("Cube").localScale;
}
}
}
}
private void MirrorBoneTransform(Transform source, Transform target)
{
target.localPosition = new Vector3(-source.localPosition.x, source.localPosition.y, source.localPosition.z);
target.localRotation = new Quaternion(-source.localRotation.x, source.localRotation.y, source.localRotation.z, -source.localRotation.w);
target.localScale = source.localScale;
}
Avatarを生成する処理を作ってAnimatorごと追加。
private void ConvertToHumanoid()
{
// Humanoid用のボーン辞書を作成
var boneEnums =
(Enum.GetValues(typeof(HumanBodyBones)) as IEnumerable<HumanBodyBones>).Where(b =>
b != HumanBodyBones.LastBone &&
b != HumanBodyBones.UpperChest &&
b != HumanBodyBones.Jaw).ToArray();
var bones = sourceAvatar.GetComponentsInChildren<Transform>();
var boneDictionary = new Dictionary<HumanBodyBones, Transform>();
foreach (var bone in boneEnums)
{
var boneTransform = bones.FirstOrDefault(b => b.name == bone.ToString());
if (boneTransform != null)
{
boneDictionary.Add(bone, boneTransform);
}
}
// HumanDescriptionを作成
var humanDesc = new HumanDescription();
humanDesc.human = boneDictionary.Select(kv => new HumanBone
{
humanName = kv.Key.ToString(),
boneName = kv.Value.name,
limit = new HumanLimit
{
useDefaultValues = true
}
}).ToArray();
humanDesc.skeleton = boneDictionary.Select(kv => new SkeletonBone
{
name = kv.Value.name,
position = kv.Value.localPosition,
rotation = kv.Value.localRotation,
scale = kv.Value.localScale
}).ToArray();
// Avatarを作成してAnimatorに設定
var avatar = AvatarBuilder.BuildHumanAvatar(sourceAvatar, humanDesc);
avatar.name = sourceAvatar.name + "_Humanoid";
sourceAvatar.AddComponent<Animator>().avatar = avatar;
// assetとして保存
AssetDatabase.CreateAsset(avatar, "Assets/OneAvatar/" + avatar.name + ".asset");
AssetDatabase.SaveAssets();
}
適当にHumanoidAnimationを入れてテストしてみる。
【ポーズ詰め合わせ】Unity Humanoid AnimationClip - PoseCollection

よさげ
口が無いことに気づいたので追加、
揺れものも入れたいし頭に何かつけるか。
折角ならExcellentにしたいのでSkinnedMeshRendererにする。
三色なので適当にUVを開いてテクスチャも生成。
private void ConvertToSkinnedMeshRenderer()
{
var genObj = new GameObject("renderer");
genObj.transform.SetParent(sourceAvatar.transform, false);
var genRenderer = genObj.AddComponent<SkinnedMeshRenderer>();
var renderers = sourceAvatar.GetComponentsInChildren<MeshRenderer>();
var bones = sourceAvatar.GetComponentsInChildren<Transform>().Where(b => renderers.All(r => r.gameObject != b.gameObject)).ToArray();
// メッシュデータを統合
var vertices = new List<Vector3>();
var uvs = new List<Vector2>();
var triangles = new List<int>();
var boneWeights = new List<BoneWeight>();
var bindPoses = new List<Matrix4x4>();
for (int i = 0; i < bones.Length; i++)
{
bindPoses.Add(bones[i].worldToLocalMatrix * sourceAvatar.transform.localToWorldMatrix);
}
foreach (var renderer in renderers)
{
var meshFilter = renderer.gameObject.GetComponent<MeshFilter>();
if (meshFilter == null) continue;
var mesh = meshFilter.sharedMesh;
var vertexOffset = vertices.Count;
vertices.AddRange(mesh.vertices.Select(v => renderer.transform.localToWorldMatrix.MultiplyPoint3x4(v)));
triangles.AddRange(mesh.triangles.Select(t => t + vertexOffset));
foreach (var vertex in mesh.vertices)
{
var weight = new BoneWeight();
weight.boneIndex0 = Array.IndexOf(bones, renderer.transform.parent);
weight.weight0 = 1.0f;
boneWeights.Add(weight);
}
// 白かデフォなら左、黒なら真ん中、赤なら右にUVを割り当てる
var mat = renderer.sharedMaterial;
if(mat != null)
{
if(mat.name.Contains("red"))
{
uvs.AddRange(mesh.uv.Select(uv => new Vector2(1.0f, uv.y)));
}
else if(mat.name.Contains("black"))
{
uvs.AddRange(mesh.uv.Select(uv => new Vector2(0.5f, uv.y)));
}
else
{
uvs.AddRange(mesh.uv.Select(uv => new Vector2(0f, uv.y)));
}
}else
{
uvs.AddRange(mesh.uv.Select(uv => new Vector2(0f, uv.y)));
}
// 元のレンダラーを削除
DestroyImmediate(renderer.gameObject);
}
var skinnedMesh = new Mesh
{
name = sourceAvatar.name + "_SkinnedMesh",
vertices = vertices.ToArray(),
triangles = triangles.ToArray(),
boneWeights = boneWeights.ToArray(),
bindposes = bindPoses.ToArray(),
uv = uvs.ToArray()
};
skinnedMesh.RecalculateNormals();
genRenderer.sharedMesh = skinnedMesh;
genRenderer.bones = bones;
genRenderer.rootBone = sourceAvatar.transform;
// テクスチャを生成
var texture = new Texture2D(3, 1);
texture.SetPixels(new Color[]
{
Color.white,
Color.black,
Color.red
});
texture.Apply();
var material = new Material(Shader.Find("Standard"));
// assetとして保存
AssetDatabase.CreateAsset(skinnedMesh, "Assets/OneAvatar/" + sourceAvatar.name + "_mesh.asset");
AssetDatabase.CreateAsset(material, "Assets/OneAvatar/" + sourceAvatar.name + "_mat.mat");
AssetDatabase.CreateAsset(texture, "Assets/OneAvatar/" + sourceAvatar.name + "_tex.asset");
AssetDatabase.SaveAssets();
material.mainTexture = texture;
genRenderer.sharedMaterial = material;
// テクスチャのインポート設定を変更
var texturePath = "Assets/OneAvatar/" + sourceAvatar.name + "_tex.asset";
var textureImporter = AssetImporter.GetAtPath(texturePath) as TextureImporter;
if (textureImporter != null)
{
textureImporter.textureType = TextureImporterType.Default;
textureImporter.wrapMode = TextureWrapMode.Clamp;
textureImporter.filterMode = FilterMode.Point;
textureImporter.SaveAndReimport();
textureImporter.mipmapEnabled = true;
}
}
インポート設定ちゃんと設定できてないけど、直すほどでもないのでとりあえず手動で設定
。

実は記事を書いている12/24は誕生日。なので誕生日インスタンスでこれを書いています。よければ祝ってください。
とりあえず完成!
BlendShapeとかAnimationとかも入れたいけどとりあえずアップロードはできたので完成!
続きを書く機会があったらもっと完璧な自作アバターを作りたいですね。
1つのSkinnedMeshRendererにもう纏めちゃったので頭のWeightとX座標値からいい感じに判定して作る感じになるかな?
良い感じにやる気が尽きて完成もしたので今回はここまでです。
気づいたら2025年もあと少し、来年もいい年になりますように。
メリークリスマス!!










