4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

行き当たりばったりでVRCに0からアバターをアップロードしてみる

4
Posted at

はじめに

こんにちは、Aetorizです。
今日はバーチャルケモミミ Advent Calendar 2025の25日目の記事です!

この記事では
「Unity Editor で 0 からアバターを組み立てて、最終的に VRChat にアップロードするまで」
を、先のことを考えずにそのまま進めていった記録です。
記事を書きながらその時思ったことを垂れ流しで書いています。

さっそく作ってみる

ますはプロジェクトの作成。
liltoonとか入れといた方が良い気がするけどまぁ後で考えよう。
image.png

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

とりあえず作業用フォルダと新規スクリプトを用意する。
image.png

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

一旦ボタンを用意する。

CreateAvatar.cs
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()
        {
            // 実装
        }
    }
}

image.png

Avatar(Componentの方)を作る

まず人を作ろう

人の関節とかの位置決めるの面倒くさいし、Unity内から適当に探してくるかー。
それっぽいのあるじゃーん。
image.png

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
            }
        };
    }
}

image.png
だめでした。

image.png
Object型でもとれないし名前違うかなー?

そもそもunity default resourcesじゃなくてunity editor resourcesから持ってくるならEditorResources.Loadを使うべきなのでは?
image.png
だめそう。

仕方ないので一旦GameObjectを先に取得してAvatarを引っ張ってくる。

var defAvatarObject = UnityEditor.Experimental.EditorResources.Load("DefaultAvatar", typeof(GameObject)) as GameObject;
var defAvatar = defAvatarObject?.GetComponentInChildren<Animator>()?.avatar;
Debug.Log(defAvatar);

image.png
よさげ

戻して確認。

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;
            }
        }

image.png
あー、格納されてるのLocalPositionだったっけ、親子関係復元しないとだ。
まぁ値は入ってるっぽいし次の処理入っちゃおう。

いや待てよ……?

だったら元のGameObjectそのまま参照したらよくないか?
というか元からあるAvatarを参照するのは0からと言えるのか?

やっぱやめやめ

気合で1から並べよう。
一旦親子関係とVisual用のCubeを並べる。
UnityのHumanoidBodyBoneからLastBoneとUpperChest、Jawを抜いて処理する。

こんなことばっかりしてるから完成しないんだよなー。

image.png

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);
            }
        }

生成されたものを人っぽく並び替え、
image.png
反対側にコピーする機能を作っていい感じに。

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;
        }

image.png

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
image.png
よさげ

口が無いことに気づいたので追加、
揺れものも入れたいし頭に何かつけるか。

image.png

折角なら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;
            }
            
        }

インポート設定ちゃんと設定できてないけど、直すほどでもないのでとりあえず手動で設定
image.png
image.png

できてそうなのでアップロード。
image.png

できた!
VRChat_2025-12-24_22-54-08.913_1920x1080.png

実は記事を書いている12/24は誕生日。なので誕生日インスタンスでこれを書いています。よければ祝ってください。

とりあえず完成!

BlendShapeとかAnimationとかも入れたいけどとりあえずアップロードはできたので完成!
続きを書く機会があったらもっと完璧な自作アバターを作りたいですね。
1つのSkinnedMeshRendererにもう纏めちゃったので頭のWeightとX座標値からいい感じに判定して作る感じになるかな?

良い感じにやる気が尽きて完成もしたので今回はここまでです。
気づいたら2025年もあと少し、来年もいい年になりますように。

VRChat_2025-12-24_23-14-30.522_1920x1080.png

メリークリスマス!!

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?