イネ科

すっぺらぴっちょん

RenderTextureで実装する簡易鏡

概要

鏡もどきを簡単に実装する知見です。現実の鏡とは違い、どちらかと言えばカメラの映像を映した液晶ディスプレイ。RenderTextureを用いる事で、そのまま写真撮影にも応用できるので、そういった使い方をするのであれば、この手法がおすすめです。カメラのFOVも好きに調整できるので、平面的な写真が撮りやすい鏡も作れます。(FOVが低いほど平面的に写せてよい感じになる。)

実装方法

1.RenderTextureを作成。サイズは任意に変更。

f:id:sesleria:20190503134641p:plain:w400

2.Materialを作成。Albedoに1で作成したRenderTextureを設定し。ShaderStandardからLegacy Shaders/Self-Illumin/VertexLitに変更することで、明るく良い感じに表示される。

参考:フォーラムの情報

f:id:sesleria:20190503134657p:plain:w400

f:id:sesleria:20190413015403p:plain:w200f:id:sesleria:20190503135545p:plain:w200

※StandardとLitの違い

3.鏡に映すためのカメラを作成。TargetTextureに1で作成したRenderTextureをアタッチする。

4.Quadを作成。SizePositionを調整する。2で作成したMaterialを設定。拡大/縮小Xがマイナスになっているのは、鏡のように反転させるため。

f:id:sesleria:20190503134652p:plain:w400

5.Quadの子オブジェクトにCameraを設定、PositionfieldOfViewnearClipPlaneを調整する。Quadにカメラの映像が写っていれば完成。 f:id:sesleria:20190503140859p:plain:w400

カメラのPositionを計算する方法についてはこちらを参照

完成イメージ

f:id:sesleria:20190503144727g:plain:w400

以下はFOV30のカメラを使用してRenderTextureをPNG保存したものです。PNG化についてはRenderTexture png 保存とかでぐぐれば出てくるので割愛します。この鏡の実装について書いたのは、RenderTextureを適用したMaterialのShader設定について記述している記事が殆ど無かった為です。暗くて泣いていたUnity初心者はきっと私だけなのでしょう。

f:id:sesleria:20190503134704p:plain:w400

f:id:sesleria:20190503134707p:plain:w400

VRMのBlendShapeを良い感じに遷移させてみる

表情を変えるだけなら公式を参照すれば解決なのですが、作成中のアダルトアプリにおいて、表情の遷移が重要なエッセンスであると感じ、実装してみました。背景としてBlendShapeをAnimationに組み込むと、スクリプトからの変更が効かなくなるのに加え、同じ体位中(Animation)でも表情だけを変更したく、コードから変更したほうが良さそうだった為です。Mecanimのアバターマスクでの遷移は地獄なので。

前提として、またたきの処理についてはVRM標準のもの(Blinker.cs)をモデルにアタッチしています。
C#の理解度が低いので、かなりゴリ押しです。

KeepChangeBlendshapeEmoteでBlendShapeを5分割して遷移させてあげるだけの、シンプルな実装ですが、引数で細かい制御を可能にしています。emoteがBlendShapeの名前、ratioが表情の遷移分パラメータ、secが表情のキープ時間、lateが遅延時間、transitionが表情の遷移にかける時間、startが表情の初期パラメータです。transitionは実際には最低0.02secほどかかってしまうので、この時間を10倍した値を考慮して、遷移間隔を算出してあげる必要があります。

以下が、実際に動作させたイメージです。かわいいですね。
f:id:sesleria:20190502152409g:plain:w250f:id:sesleria:20190502152414g:plain:w250

ManageFacialExpressions.cs

VRoidで作成したVRMモデルにBlinker.csと、ManageFacialExpressions.csをアタッチすれば動きます。C V B で遷移、Nで中断。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using VRM;

public class ManageFacialExpressions : MonoBehaviour
{

    private Blinker blink;
    private VRMBlendShapeProxy proxy;
    private bool endFlag = true;
    private bool startFlag = true;
    private int newFace = 100;

    void Start()
    {
        proxy = this.GetComponent<VRMBlendShapeProxy>();
        blink = this.GetComponent<Blinker>();
    }

    void Update()
    {
        if(Input.GetKeyDown(KeyCode.C)){
            startFlag = false;
            newFace = 0;
        }

        if(Input.GetKeyDown(KeyCode.V)){
            startFlag = false;
            newFace = 1;
        }

        if(Input.GetKeyDown(KeyCode.B)){
            startFlag = false;
            newFace = 2;
        }

        if(Input.GetKeyDown(KeyCode.N)){
            blink.enabled = true;
            startFlag = false;
            newFace = 100;
        }

        if(endFlag && newFace != 100){
            blink.enabled = false;
            startFlag = true;
            endFlag = false;
            StartCoroutine(FaceAnimationStart(newFace));
        }
    }

    //表情を初期化
    public void ResetBlendShape(){
        proxy.SetValue("NEWTRAL",0.0f);
        proxy.SetValue("A",0.0f);
        proxy.SetValue("I",0.0f);
        proxy.SetValue("U",0.0f);       
        proxy.SetValue("E",0.0f);
        proxy.SetValue("O",0.0f);
        proxy.SetValue("BLINK",0.0f);
        proxy.SetValue("BLINK_L",0.0f);
        proxy.SetValue("BLINK_R",0.0f);
        proxy.SetValue("JOY",0.0f);
        proxy.SetValue("ANGRY",0.0f);
        proxy.SetValue("SORROW",0.0f);
        proxy.SetValue("FUN",0.0f);
        proxy.SetValue("SURPRISED",0.0f);
        proxy.SetValue("EXTRA",0.0f);
    }

    //表情制御(個別)
    public void KeepChangeBlendshapeEmote(string emote, float ratio, float sec, float late = 0.0f, float transition = 0.01f, float start = 0.0f){
        StartCoroutine(ChangingBlendshape(emote, ratio, sec, late, transition, start));
    }

    private IEnumerator ChangingBlendshape(string emote, float ratio, float sec, float late ,float transition, float start){
        
        yield return new WaitForSeconds(late);

        for(int i=5; i>=1; i--){
            proxy.SetValue(emote,start + ratio / i);
            yield return new WaitForSeconds(transition);
        }
        yield return new WaitForSeconds(sec);
        for(int i=1; i<= 5; i++){
            proxy.SetValue(emote,start + ratio / i);
            yield return new WaitForSeconds(transition);
        }

        proxy.SetValue(emote,start);

    }

    //表情制御(指示)
    private IEnumerator FaceAnimationStart(int faceNo){

        int blinkCount = 0;

        if(faceNo == 0){
            KeepChangeBlendshapeEmote("BLINK", 0.0f, 0.0f, 0.0f, 0.0f, 0.0f);
            while(startFlag){
                KeepChangeBlendshapeEmote("FUN", 0.15f, 1.2f, 0.0f, 0.1f, 0.15f);
                KeepChangeBlendshapeEmote("JOY", -0.15f, 1.2f, 0.0f, 0.1f, 0.3f);
                yield return new WaitForSeconds(2.5f);
                blinkCount++;
                if(blinkCount == 3){
                    blinkCount = 0;
                    KeepChangeBlendshapeEmote("BLINK", 0.6f, 0.0f, 0.0f, 0.01f, 0.0f); 
                }
            }
        }

        if(faceNo == 1){
            KeepChangeBlendshapeEmote("BLINK", 0.0f, 0.0f, 0.0f, 0.0f, 0.55f);
            while(startFlag){
                KeepChangeBlendshapeEmote("FUN", 0.15f, 1.2f, 0.0f, 0.1f, 0.15f);
                KeepChangeBlendshapeEmote("JOY", -0.15f, 1.2f, 0.0f, 0.1f, 0.3f);
                KeepChangeBlendshapeEmote("BLINK", 0.0f, 2.5f, 0.0f, 0.0f, 0.55f);
                yield return new WaitForSeconds(2.5f);
            }
        }

        if(faceNo == 2){
            KeepChangeBlendshapeEmote("BLINK", 0.0f, 0.0f, 0.0f, 0.0f, 0.0f);
            while(startFlag){
                KeepChangeBlendshapeEmote("A", 1.0f, 0.0f, 0.0f, 0.02f, 0.0f);
                KeepChangeBlendshapeEmote("I", 1.0f, 0.0f, 0.3f, 0.02f, 0.0f);
                KeepChangeBlendshapeEmote("U", 1.0f, 0.0f, 0.6f, 0.02f, 0.0f);
                KeepChangeBlendshapeEmote("E", 1.0f, 0.0f, 0.9f, 0.02f, 0.0f);
                KeepChangeBlendshapeEmote("O", 1.0f, 0.0f, 1.2f, 0.02f, 0.0f);
                yield return new WaitForSeconds(1.7f);
            }
        }
        
        endFlag = true;
        ResetBlendShape();        

    }
}

課題 : 元のBlendShapeの値をキープしつつ、変更先までのパラメータを加算なり減算してあげれば、一度リセットしてやらなくても済むのかもしれない。

作業進捗風景

VRMモデルになってVRMモデルに憑依するVRアプリを作ったお話

配布先

sesleria.booth.pm

紹介動画

Unity歴について

7年ほど前にアカウントは作成してあった。本格的に触り始めたのは二か月ほど前。最初にやったのはUdemyの講座。非常にわかりやすい。
ユニティちゃんが教える!初心者向けUnity講座 | Udemy

経緯

VRアプリの作成技術を高めたく、一人称視点のゾンビ討伐系VRアプリを作成しようと思っていたが、VR+TSF系のアプリが少なすぎることに気づいてしまい、方針転換。異性のアバターを使うだけならば、他のVRアプリでも同様の体験が可能だが、感情移入するためには元のアバターを表示し続けておくようなギミックが重要なのではないかと感じた(変身と精神の転移は異なるという解釈) 。

VRMの良いところ

VRoid Studioで高品質なモデルを作成可能。VRoid Hubにおいても第三者が使用可能のモデルがユーザーによって提供され続けている。また、ライセンスが明確な為、様々なゲームに採用しやすい。今回のアプリでVRMロード時に注意したポイントは[このアバターを用いて性的表現を演じることの許可][アバターに人格を与えることの許諾範囲][再配布・改変に関する許諾範囲]。VRoidについては脱衣機能を付与した為、改変のライセンスを厳密にチェックしている(Hubと自作のものではライセンスの確認箇所が多少異なる)。VRMファイルについてはStreamingAssetsから決め打ちのファイルパスを起動時に読み込み、Disallowのモデルが含まれていた場合、強制的に終了するようにしている。

VR開発について

タッチコントローラの互換性を持たせたかったので、OpenVRで作成。InputManagerは鬼畜。VR開発は一見ハードルが高そうに見えるが、MainCameraをGameObjectの子に変えて、以下のようにProjectSettingsを変更すれば良い。

f:id:sesleria:20190407032157p:plain
ProjectSettings

一人称視点のこだわり

憑依という設定を大事にしたく、カメラのクリッピング平面パラメータをギリギリのラインで調整している。VRMには頭を非表示にする機能がついているが、あえてそれは使っていない(頭の影が見えなくなる、髪の毛が見えなくなる為)。IKの処理については、Unity標準のものを使用している。失業中なのでFinalIKを買いにくかった・・。コントローラと乳首の位置が大幅にずれる現象の対処等、HMDやコントローラーの位置についての知見は深まったと思う。現実の体と仮想の体についてのサイズ差異については、あくまで自分自身で調整しているので、腕の長さや、首の長さによってはIKの処理がダメだったりするかもしれない。また、モデルの設定次第ではあるが、胸については手で触った時、必ず揺れるように調整している。こちらについてはMtoFにおけるTSFのお約束事項なので、重要視した。自撮り用カメラはトリガーで背面に切り替わるようにしてあり、倒れている自分自身を撮影出来る。

憑依体験について

精神体になり、体を重ねることで憑依できるギミックも考慮していたが、スピード感を大事にしたく、今回のように相手の体に手を入れる、コントローラーが振動する、そこでトリガーを引くと相手に体に入り込めるという体験にした。TSFシチュエーションとしては微妙だが、VR化すると相手の体に入った直後、元の体の意識が抜けるという非現実的な体験が可能になった。また、大型で高画質の鏡を置くことにより、憑依した自分をすぐに確認できるというTSFが好きな人には刺さるアプリになったのではないかと思う。憑依先のモデルについてはブレンドシェイプでまばたきをする程度のアニメーションしかさせていないが、相手が生きているという感じが出てくる。

言いたかったこと

TSF系VRアプリはブルーオーシャン

使用アセット

Very Animation - Asset Store
Abandoned Asylum - Asset Store トイレを借りました

PackageはProBuilderとPost Processing。建物はProBuilderで作成したメッシュにMaterialを貼り付けただけ。

大変参考になったサイト

OpenVR コントローラーの入力 - Unity マニュアル
102 2 レーザーコントローラー · yumemi-inc/vr-studies Wiki · GitHub
Unity標準のVR機能(UnityEngine.XR)メモ - フレームシンセシス

UnityのPlayer設定(解像度関連)変更後、設定が反映されない場合の対処法

Windowsアプリの仕様で、前回起動時の解像度情報がレジストリに保管されるようになっており、このレジストリを削除する必要がある。
例:ディスプレイ解像度ダイアログを無効に変更した後も、全画面表示され続けてしまう。

レジストリ削除手順

  1. [Windows+R]からregeditを起動
  2. 以下のレジストリを削除

\HKEY_CURRENT_USER\Software\企業名\プロダクト名

f:id:sesleria:20190324011046p:plain
Player設定画面

参考リンク:

docs.unity3d.com

Portfolio

writing now....

・Being interested
VR application. VRM format file.

・IT experience
System Engineering Service : 5 years.
(Windows native application develop)
Information system department : 3 years.
(PC kitting etc)

・Skill C(Win32API),C#,PHP,HTML5,JavaScript,CSS3,Git,Office365(Sharepoint,PowerBI,Flow,Teams etc)
(But wide and shallow....)

・Certification
情報セキュリティスペシャリスト(2012)

・Creation
-2019 Hyoui Chan : 憑依ちゃん - イネ科 - BOOTH
It's original application working for Oculus Rift CV1 and S.
You can experience sex in 6DoF view and read VRM format files.
You can create an video from a third person viewpoint.
Used: Unity(OpenVR,SteamVR Plugin)

Oculus Quest Apps: イネ科 Questアプリ 配布所 - イネ科 - BOOTH
It's original mini application working for Oculus Quest.

Android App: https://play.google.com/store/apps/details?id=com.Sesleria.VMascot
Simple VRM viewer.Made with Unity.

-2013
Hatena Blog's Theme. platismのテーマ一覧 - テーマ ストア - はてなブログ
About 10,000 user.
f:id:sesleria:20190610121500p:plain:w400

Tumblr's Theme. Tumblr-Theme/FLATISM at master · sesleria/Tumblr-Theme · GitHub f:id:sesleria:20190609193110p:plain:w400
Used: CSS3