Risebox_VRブログ

VRやAR等のアプリ開発の過程を発信するブログ

unity キャラクターのボーンを表示する方法

キャラクターのボーンを表示させたいなと思ったときに
色々調べてみましたが、自分の中でピンとくるものがなかったので、
どうやって簡単にキャラのボーンを表示させようか悩んでいましたが、
すごく簡単にボーンを表示させることができる方法がありましたので共有したいと思います。

またVRキャラ操作の際にボーンの表示があると便利なのでぜひ使ってみてください。

準備

今回の環境
・windows10
・unity 2019.4.13

まずはボーンを表示させたいキャラを用意してください。
今回の例では無料アセットのunitychanを使わせていただいています。
Space Robot Kyleもボーンを確認するのにいいと思います。

assetstore.unity.com

assetstore.unity.com
unity asset storeより引用

次にunityのpackage managerより本命のanimation rigging をimportします。
unityの2019.4以降が正式版?のようです。なのでできれば2019.4以降のバージョンを用意してください。

f:id:Risebox:20201129230811p:plain

animation riggingパッケージ

animation riggingパッケージはもともとアニメーションを作るための仕組みで、
本来スクリプトでアニメーションを作っていたところを簡潔にしてくれるパッケージになります。
ここに実はキャラのボーンを表示させてくれるスクリプトがあるんです!
それが「bone renderer」と呼ばれるスクリプト

「bone renrderer」で キャラのボーンを表示

f:id:Risebox:20201129231155j:plain

animation riggingをimportしたらadd Componentで「bone renderer」と検索して
ボーンを表示させたいキャラにスクリプトをアタッチしましょう。

次にボーンを表示させたい部位を選択していくのですがひとまず全部位を見ていきます。
unitychanのオブジェクトを右クリックしてselect childrenをクリックします。

f:id:Risebox:20201129232611p:plain

選択されたオブジェクトをtransformにドラッグアンドドロップする
(※すでにelementにオブジェクトを入れていたのでボーンが表示されています。)

f:id:Risebox:20201129235313j:plain

すると下記の画面になります。

f:id:Risebox:20201130000508p:plain
element

elementに選択されたオブジェクトが入り、そのボーンが表示されます。

f:id:Risebox:20201130001659p:plain


unitychan結構いろんなボーンが入っていますね。。
こんなにたくさんボーンが表示されても使いずらいので、
elementには体の基本的な部位のみ表示させました。
f:id:Risebox:20201130002228p:plain

自分は以下のオブジェクトを入れました。
f:id:Risebox:20201130003811p:plainf:id:Risebox:20201130003833p:plain

ボーンオブジェクトの表示については以上です。

VRゲームで運動不足解消

インドア派の運動不足に

こんにちは
私は内向的な性格もあってインドア派で
平日の仕事の日以外ほとんど家で過ごしています。

そこで問題になってくるのが、体を動かさないことによる運動不足。。
室内でだらだら過ごすことが多いので、いざ外に出て体を動かしたときは
息切れになりやすいです。。

恥ずかしながら、100m全力ダッシュしたときなんかは具合が悪くなる時もありました、、

あまりにも運動不足で情けなかったので、
ウォーキングやジョギングとか始めようと思いましたが、
内向的な自分は外に出て自分が走っている姿を見られるのは嫌だったので、
結局やってみたものの長続きしませんでした。。

気にしすぎと思われるかもしれませんが、
自分が何かを必死に頑張っている姿を見られるのが
無理みたいです。。

そこで室内でできることを考えてみたところ
筋トレなどが思い当たりましたが、やり方が悪かったこともあり
きつくて続けられませんでした。

VRゲームで楽しく運動不足を解消

室内できる無理なく運動を続けられるものはないか…
たどり着いたのがVRゲームでした!

元々ゲームが好き、playstationVRでVRゲームをプレイして感動して
一日ぶっ通しでゲームをしたことがあり、これなら運動を続けられそうと思い、
VR機器のOculus Quest2を購入しました。



VRゲームにはいろいろなジャンルがありましたが、
その中でリズムゲームがありこれは楽しみながら運動できそうと思い、即購入しました。
そのゲームがVRゲームの中では有名な「Beat Saber」です。
以下oculus公式サイトのurlです。
Oculus Questの「Beat Saber」 | Oculus

紹介動画にもあるように結構体を動かしてライトセイバーを振り回していますね。
運動量はそこそこありそうな感じです。

実際に自分もやってみて感じましたが、
熱中していると体が熱くなってくるくらいの運動ができます。
特に上下左右に体を大きく動かされる譜面をやったときは
足も動かすので、初めてすぐの時はちょっと筋肉痛になりました笑
(軟弱な体過ぎました。。)

今も続けていて曲レベル(Expert…最高レベルの一つ下)までは何とかクリアできるようになりました。
Expert+をクリアできる人は別次元の世界にいるのでしょうか。。
自分には全くクリアできる気がしません。

またBeatSaberの曲の種類ですが、
公式で配信されている以外に、非公式で発布されているMODを含めるとかなりの数になるようです。
アニソンなどもあり、アニソン好きな私にはたまらないのでいつかMOD導入してプレイしてみたいですね。
その時はまた記事にするかもしれません。

oculus questアップデートでフィットネス機能追加

2020年11月17日にoculus questにアップデートがありました。
以下、アップデート詳細が書かれた記事の引用です。

www.moguravr.com
'MoguLifeより引用

そこで追加されたのが「oculus move」と呼ばれるフィットネス機能で
これはゲームでどのくらいの量と時間体を動かしたかを教えてくれる機能になります。
また一日にどのくらい体を動かしたいか目標値を決定できるため
毎日目標に向けてVRゲームで体を動かすことができます。
以下の画像は自分の今日の運動量と時間の表示です。

f:id:Risebox:20201122155711j:plain
oculus move

・・・ゲームのやりすぎですね。。
目標値の倍の時間やっちゃってます笑

ですが、やはりゲームに夢中になっているということで
運動不足を忘れさせてくれるので、VRゲームで運動不足解消はおすすめです。

VRゲームはその他たくさんあり

Beat Saber 以外にもフィットネスに特化したVRゲームなどもあるので、
気になった方はそちらもプレイしてみるといいかもしれません。


以上VRで運動不足解消についての記事でした!
最後まで読んでいただきありがとうございます。

VR用の鏡を実装する方法~oculusquest2~

はじめに

oculus quest2でVRでのキャラクターの操作を行いたかったため、

キャラが動いている様子を確認するための鏡を用意したかったのですが、

VRの鏡を実装している記事を見ていると

  1. SteamVR Plugin + Vive Stereo Rendering Toolkitを利用した鏡の実装
  2. VRChat SDK
  3. magic mirrorアセットの有料版

上記の方法で鏡を用意している方が多い印象でした。

ただこれらのやり方だとセットアップがあったり、unityのバージョンによって動かなかったり、
お金がかかったりするので、できる限りこれらを避けて鏡が実装できないものかといろんな記事を探しました。

そこで見つけたのが以下の記事

forum.unity.com

unity公式のフォーラムに鏡の実装方法について様々な投稿がされており、

その中でVR用の鏡の実装についての投稿もありました!

 

以下、VR用の鏡の実装方法を書いていきます。

今回の実行環境

・windows10
・unity 2019.4.13f1
・oculus quest2

VR用鏡の実装方法

フォーラムに載っていた鏡用のシェーダーとスクリプトを使用します。

以下、ソースコードです。

(CC BY-SA 3.0ライセンスでフォーラムから引用しています。)

Mirror.shader
// original source from: http://wiki.unity3d.com/index.php/MirrorReflection4
// taken from: https://forum.unity.com/threads/mirror-reflections-in-vr.416728/#post-3594431

Shader "FX/MirrorReflection"
{
    Properties
    {
        _MainTex ("_MainTex", 2D) = "white" {}
        _ReflectionTexLeft ("_ReflectionTexLeft", 2D) = "white" {}
        _ReflectionTexRight ("_ReflectionTexRight", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100
        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 refl : TEXCOORD1;
                float4 pos : SV_POSITION;
            };
            float4 _MainTex_ST;
            v2f vert(float4 pos : POSITION, float2 uv : TEXCOORD0)
            {
                v2f o;
                o.pos = UnityObjectToClipPos (pos);
                o.uv = TRANSFORM_TEX(uv, _MainTex);
                o.refl = ComputeScreenPos (o.pos);
                return o;
            }
            sampler2D _MainTex;
            sampler2D _ReflectionTexLeft;
            sampler2D _ReflectionTexRight;
            fixed4 frag(v2f i) : SV_Target
            {
                fixed4 tex = tex2D(_MainTex, i.uv);
                fixed4 refl;
                if (unity_StereoEyeIndex == 0) refl = tex2Dproj(_ReflectionTexLeft, UNITY_PROJ_COORD(i.refl));
                else refl = tex2Dproj(_ReflectionTexRight, UNITY_PROJ_COORD(i.refl));
                return tex * refl;
            }
            ENDCG
        }
    }
}
MirrorReflection.cs
 // original source from: http://wiki.unity3d.com/index.php/MirrorReflection4
// taken from: https://forum.unity.com/threads/mirror-reflections-in-vr.416728/#post-3691759

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using UnityEngine.XR;
 
 
// This is in fact just the Water script from Pro Standard Assets,
// just with refraction stuff removed.
 
[ExecuteInEditMode] // Make mirror live-update even when not in play mode
public class MirrorReflection : MonoBehaviour
{
    public bool m_DisablePixelLights = true;
    public int m_TextureSize = 256;
    public float m_ClipPlaneOffset = 0.07f;
    public int m_framesNeededToUpdate = 0;
 
    public LayerMask m_ReflectLayers = -1;
 
    private Dictionary<Camera, Camera> m_ReflectionCameras = new Dictionary<Camera, Camera>();
 
    private RenderTexture m_ReflectionTextureLeft = null;
    private RenderTexture m_ReflectionTextureRight = null;
    private int m_OldReflectionTextureSize = 0;
 
    private int m_frameCounter = 0;
 
    private static bool s_InsideRendering = false;
 
    // This is called when it's known that the object will be rendered by some
    // camera. We render reflections and do other updates here.
    // Because the script executes in edit mode, reflections for the scene view
    // camera will just work!
    public void OnWillRenderObject()
    {
        if (m_frameCounter > 0)
        {
            m_frameCounter--;
            return;
        }
 
        var rend = GetComponent<Renderer>();
        if (!enabled || !rend || !rend.sharedMaterial || !rend.enabled)
            return;
 
        Camera cam = Camera.current;
        if (!cam)
            return;
 
        // Safeguard from recursive reflections.  
        if (s_InsideRendering)
            return;
        s_InsideRendering = true;
 
        m_frameCounter = m_framesNeededToUpdate;
 
        RenderCamera(cam, rend, Camera.StereoscopicEye.Left, ref m_ReflectionTextureLeft);
        if (cam.stereoEnabled)
            RenderCamera(cam, rend, Camera.StereoscopicEye.Right, ref m_ReflectionTextureRight);
    }
 
    private void RenderCamera(Camera cam, Renderer rend, Camera.StereoscopicEye eye, ref RenderTexture reflectionTexture)
    {
        Camera reflectionCamera;
        CreateMirrorObjects(cam, eye, out reflectionCamera, ref reflectionTexture);
 
        // find out the reflection plane: position and normal in world space
        Vector3 pos = transform.position;
        Vector3 normal = transform.up;
 
        // Optionally disable pixel lights for reflection
        int oldPixelLightCount = QualitySettings.pixelLightCount;
        if (m_DisablePixelLights)
            QualitySettings.pixelLightCount = 0;
 
        CopyCameraProperties(cam, reflectionCamera);
 
        // Render reflection
        // Reflect camera around reflection plane
        float d = -Vector3.Dot(normal, pos) - m_ClipPlaneOffset;
        Vector4 reflectionPlane = new Vector4(normal.x, normal.y, normal.z, d);
 
        Matrix4x4 reflection = Matrix4x4.zero;
        CalculateReflectionMatrix(ref reflection, reflectionPlane);
 
        Vector3 oldEyePos;
        Matrix4x4 worldToCameraMatrix;
        if (cam.stereoEnabled)
        {
            worldToCameraMatrix = cam.GetStereoViewMatrix(eye) * reflection;
            Vector3 eyeOffset;
            if (eye == Camera.StereoscopicEye.Left)
                eyeOffset = InputTracking.GetLocalPosition(XRNode.LeftEye);
            else
                eyeOffset = InputTracking.GetLocalPosition(XRNode.RightEye);
            eyeOffset.z = 0.0f;
            oldEyePos = cam.transform.position + cam.transform.TransformVector(eyeOffset);
        }
        else
        {
            worldToCameraMatrix = cam.worldToCameraMatrix * reflection;
            oldEyePos = cam.transform.position;
        }
 
        Vector3 newEyePos = reflection.MultiplyPoint(oldEyePos);
        reflectionCamera.transform.position = newEyePos;
 
        reflectionCamera.worldToCameraMatrix = worldToCameraMatrix;
 
        // Setup oblique projection matrix so that near plane is our reflection
        // plane. This way we clip everything below/above it for free.
        Vector4 clipPlane = CameraSpacePlane(worldToCameraMatrix, pos, normal, 1.0f);
 
        Matrix4x4 projectionMatrix;
 
 
 
        //if (cam.stereoEnabled) projectionMatrix = HMDMatrix4x4ToMatrix4x4(cam.GetStereoProjectionMatrix(Camera.StereoscopicEye.Left));
        //else
        //if (cam.stereoEnabled)
        //    projectionMatrix = HMDMatrix4x4ToMatrix4x4(SteamVR.instance.hmd.GetProjectionMatrix((Valve.VR.EVREye)eye, cam.nearClipPlane, cam.farClipPlane));
        //else
        if (cam.stereoEnabled)
            projectionMatrix = cam.GetStereoProjectionMatrix(eye);
        else
            projectionMatrix = cam.projectionMatrix;
        //projectionMatrix = cam.CalculateObliqueMatrix(clipPlane);
        MakeProjectionMatrixOblique(ref projectionMatrix, clipPlane);
 
        reflectionCamera.projectionMatrix = projectionMatrix;
        reflectionCamera.cullingMask = m_ReflectLayers.value;
        reflectionCamera.targetTexture = reflectionTexture;
        GL.invertCulling = true;
        //Vector3 euler = cam.transform.eulerAngles;
        //reflectionCamera.transform.eulerAngles = new Vector3(0, euler.y, euler.z);
        reflectionCamera.transform.rotation = cam.transform.rotation;
        reflectionCamera.Render();
        //reflectionCamera.transform.position = oldEyePos;
        GL.invertCulling = false;
        Material[] materials = rend.sharedMaterials;
        string property = "_ReflectionTex" + eye.ToString();
        foreach (Material mat in materials)
        {
            if (mat.HasProperty(property))
                mat.SetTexture(property, reflectionTexture);
        }
 
        // Restore pixel light count
        if (m_DisablePixelLights)
            QualitySettings.pixelLightCount = oldPixelLightCount;
 
        s_InsideRendering = false;
    }
 
 
    // Cleanup all the objects we possibly have created
    void OnDisable()
    {
        if (m_ReflectionTextureLeft)
        {
            DestroyImmediate(m_ReflectionTextureLeft);
            m_ReflectionTextureLeft = null;
        }
        if (m_ReflectionTextureRight)
        {
            DestroyImmediate(m_ReflectionTextureRight);
            m_ReflectionTextureRight = null;
        }
        foreach (var kvp in m_ReflectionCameras)
            DestroyImmediate(((Camera)kvp.Value).gameObject);
        m_ReflectionCameras.Clear();
    }
 
 
    private void CopyCameraProperties(Camera src, Camera dest)
    {
        if (dest == null)
            return;
        // set camera to clear the same way as current camera
        dest.clearFlags = src.clearFlags;
        dest.backgroundColor = src.backgroundColor;
        if (src.clearFlags == CameraClearFlags.Skybox)
        {
            Skybox sky = src.GetComponent(typeof(Skybox)) as Skybox;
            Skybox mysky = dest.GetComponent(typeof(Skybox)) as Skybox;
            if (!sky || !sky.material)
            {
                mysky.enabled = false;
            }
            else
            {
                mysky.enabled = true;
                mysky.material = sky.material;
            }
        }
        // update other values to match current camera.
        // even if we are supplying custom camera&projection matrices,
        // some of values are used elsewhere (e.g. skybox uses far plane)
        dest.farClipPlane = src.farClipPlane;
        dest.nearClipPlane = src.nearClipPlane;
        dest.orthographic = src.orthographic;
        dest.fieldOfView = src.fieldOfView;
        dest.aspect = src.aspect;
        dest.orthographicSize = src.orthographicSize;
    }
 
    // On-demand create any objects we need
    private void CreateMirrorObjects(Camera currentCamera, Camera.StereoscopicEye eye, out Camera reflectionCamera, ref RenderTexture reflectionTexture)
    {
        reflectionCamera = null;
 
 
        // Reflection render texture
        if (!reflectionTexture || m_OldReflectionTextureSize != m_TextureSize)
        {
            if (reflectionTexture)
                DestroyImmediate(reflectionTexture);
            reflectionTexture = new RenderTexture(m_TextureSize, m_TextureSize, 16);
            reflectionTexture.name = "__MirrorReflection" + eye.ToString() + GetInstanceID();
            reflectionTexture.isPowerOfTwo = true;
            reflectionTexture.hideFlags = HideFlags.DontSave;
            m_OldReflectionTextureSize = m_TextureSize;
        }
 
        // Camera for reflection
        if (!m_ReflectionCameras.TryGetValue(currentCamera, out reflectionCamera)) // catch both not-in-dictionary and in-dictionary-but-deleted-GO
        {
            GameObject go = new GameObject("Mirror Reflection Camera id" + GetInstanceID() + " for " + currentCamera.GetInstanceID(), typeof(Camera), typeof(Skybox));
            reflectionCamera = go.GetComponent<Camera>();
            reflectionCamera.enabled = false;
            reflectionCamera.transform.position = transform.position;
            reflectionCamera.transform.rotation = transform.rotation;
            reflectionCamera.gameObject.AddComponent<FlareLayer>();
            go.hideFlags = HideFlags.DontSave;
            m_ReflectionCameras.Add(currentCamera, reflectionCamera);
        }
    }
 
    // Extended sign: returns -1, 0 or 1 based on sign of a
    private static float sgn(float a)
    {
        if (a > 0.0f) return 1.0f;
        if (a < 0.0f) return -1.0f;
        return 0.0f;
    }
 
    // Given position/normal of the plane, calculates plane in camera space.
    private Vector4 CameraSpacePlane(Matrix4x4 worldToCameraMatrix, Vector3 pos, Vector3 normal, float sideSign)
    {
        Vector3 offsetPos = pos + normal * m_ClipPlaneOffset;
        Vector3 cpos = worldToCameraMatrix.MultiplyPoint(offsetPos);
        Vector3 cnormal = worldToCameraMatrix.MultiplyVector(normal).normalized * sideSign;
        return new Vector4(cnormal.x, cnormal.y, cnormal.z, -Vector3.Dot(cpos, cnormal));
    }
 
    // Calculates reflection matrix around the given plane
    private static void CalculateReflectionMatrix(ref Matrix4x4 reflectionMat, Vector4 plane)
    {
        reflectionMat.m00 = (1F - 2F * plane[0] * plane[0]);
        reflectionMat.m01 = (-2F * plane[0] * plane[1]);
        reflectionMat.m02 = (-2F * plane[0] * plane[2]);
        reflectionMat.m03 = (-2F * plane[3] * plane[0]);
 
        reflectionMat.m10 = (-2F * plane[1] * plane[0]);
        reflectionMat.m11 = (1F - 2F * plane[1] * plane[1]);
        reflectionMat.m12 = (-2F * plane[1] * plane[2]);
        reflectionMat.m13 = (-2F * plane[3] * plane[1]);
 
        reflectionMat.m20 = (-2F * plane[2] * plane[0]);
        reflectionMat.m21 = (-2F * plane[2] * plane[1]);
        reflectionMat.m22 = (1F - 2F * plane[2] * plane[2]);
        reflectionMat.m23 = (-2F * plane[3] * plane[2]);
 
        reflectionMat.m30 = 0F;
        reflectionMat.m31 = 0F;
        reflectionMat.m32 = 0F;
        reflectionMat.m33 = 1F;
    }
 
    // taken from http://www.terathon.com/code/oblique.html
    private static void MakeProjectionMatrixOblique(ref Matrix4x4 matrix, Vector4 clipPlane)
    {
        Vector4 q;
 
        // Calculate the clip-space corner point opposite the clipping plane
        // as (sgn(clipPlane.x), sgn(clipPlane.y), 1, 1) and
        // transform it into camera space by multiplying it
        // by the inverse of the projection matrix
 
        q.x = (sgn(clipPlane.x) + matrix[8]) / matrix[0];
        q.y = (sgn(clipPlane.y) + matrix[9]) / matrix[5];
        q.z = -1.0F;
        q.w = (1.0F + matrix[10]) / matrix[14];
 
        // Calculate the scaled plane vector
        Vector4 c = clipPlane * (2.0F / Vector3.Dot(clipPlane, q));
 
        // Replace the third row of the projection matrix
        matrix[2] = c.x;
        matrix[6] = c.y;
        matrix[10] = c.z + 1.0F;
        matrix[14] = c.w;
    }
 
}


①まずはシェーダーとスクリプトをそれぞれ用意します。
シェーダーについては詳しくないのですが、
Standard Surface Shaderで作成して問題ないと思います。
f:id:Risebox:20201122030602p:plain

②シェーダーを適用させるマテリアルを用意し、シェーダーを適用します。

③鏡にするオブジェクトを用意する。
この時のオブジェクトはPlaneにしてください。
Plane以外では正しく動作しない?かもしれません。
RotationをX:0,Y:0,Z:-90としてください。

④オブジェクトにシェーダーを適用させたマテリアルとスクリプトをアタッチします。
スクリプトをアタッチしたときにMirror Reflection Camera id-○○ for -○○
が生成されますがこれはそのままにしてください。
消してもunity実行時に再生成されます。

⑤鏡に映したいオブジェクトを配置し、鏡ができていることを確認する。
f:id:Risebox:20201122032400p:plain


これで鏡を置くことができます。

鏡の映りが粗い場合はアタッチしたスクリプトコンポーネント
Texture Sizeの値を大きくしていくと映りが良くなっていきます。
1000くらいでunityエディタ上ではきれいに映りますが、VR越しだと2000くらい必要になりそうです。
この値が大きくなるほど描画の処理が重くなるので気を付けましょう。


以下はunityちゃんのキャラ操作を行っているときのものです。

f:id:Risebox:20201122035333j:plain
VR映像

補足

その他スクリプトコンポーネントで変更できる項目について
・Disable Pixel Light…ミラーがピクセルではなく頂点モードでライトをレンダリングするかどうか
・Clip Plane Offset…鏡が映す物体の距離間を補正するための値(鏡と物体との距離感が合わないと思ったらここで調整する)
・Frames Needed To Update…鏡の描画の更新間隔を設定する値(0で固定のままで)
・Reflect Layers…鏡に映すレイヤーを設定する。




以上、VR用の鏡の実装でした。


次回は、VRでのキャラ操作について記事を書いていきます。