やぎりのブログ

やぎりのブログです。ゲーム制作、プログラミング、エフェクトのことについて書いていきます。

VRChatワールド「TextChat」ギミックざっくり解説

VRChatのワールド、「TextChat」ギミックのざっくりした解説です。
全体的にもっといい方法があるかもしれませんので、ご参考までに。

目次

各ギミック解説

三人称視点

ワールドに三人称用のカメラを置き、その映像をRenderTextureに出力し、プレイヤーカメラに貼り付けることで三人称視点の映像を表示しています。
カメラの位置、姿勢は、Udonで制御しています。Networking.LocalPlayerで取得したVRCPlayerApiの、GetPosition()GetTrackingData(VRCPlayerApi.TrackingDataType.Head).rotationを元に位置、姿勢を算出しています。

テキスト送信

同期されるstring変数をメンバに持ったUdonBehaviorをワールドに入る最大人数分用意し、各クライアントに一つづつ所有権を割り当てます。
テキスト送信時は、自分が所有権を持っているオブジェクトのstring変数を書き換えます。各クライアントは変数が書き換わっているかを常にチェックし、書き換わったらテキストが送信されたとみなします。
書き換わったかどうかは各自のローカルで保持している各stringの履歴との比較で判定します。

所有権割り当て

所有権割当マネージャーオブジェクトを用意し、それを所有しているクライアントが、所有権の割当を行います。
所有権の割当は、他のプレイヤーが入ってきた時に行われます。
割当処理はVRC PrefabsのUdonStringEventsを改変したものを用いており、各プレイヤーJoin時から少し間を開けて所有権を設定する処理はSimple Object Poolを参考にしています。

文字数制限

Udonのstringは一定文字数以上を同期すると後半が欠けるようなので、特定の文字数以上が入力された場合は文字をトリミングしてから送信しています。

UI

UIは全てuGUIを用いています。

Tを押すと入力開始する処理

Tキーを押した時にInputFieldが選択され、入力可能になります。
InputFieldのSelectを呼ぶだけだと入力開始にならない場合があるので、ダミーの選択可能UIを用意しておき、一度そのUIのSelectを呼んでから、InputFieldのSelectを呼んで確実に入力開始にしています。
また、Navigationが張られている場合、WASDを押すと他のUIにフォーカスが移るので、解除してあります。

各プレイヤー頭上のテキスト表示UI

一人称視点の場合はRender ModeWorld Spaceのテキスト表示UIを、
三人称視点の場合はRender ModeScreen Space - Overlayのテキスト表示UIを用いて表示を行っています。
テキスト表示UIは、一、三人称視点用それぞれにワールドに入る最大人数分用意してあります。

UdonSharpコード走り書きメモ

概要

UdonSharpで自分が書いたコード集です。走り書きです。

Merlin-san/UdonSharp

UdonSharpを用いることで、ノード無し、C#コードでUdonを書けます。詳しくは以下。 github.com

オブジェクトのアクティブをオンオフ(ローカル)

using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;

/// <summary>
/// ローカルで対象オブジェクトのアクティブ/非アクティブを切り替える
/// </summary>
public class ToggleBtnLocal : UdonSharpBehaviour
{
    [SerializeField] private GameObject target;
    public override void Interact()
    {
        target.SetActive(!target.activeSelf);
    }
}

オブジェクトを一つづつアクティブ/非アクティブ化(ローカル)

using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;

/// <summary>
/// ローカルで複数のオブジェクトを順番にアクティブ/非アクティブにする
/// </summary>
public class SwitchObjsLocal : UdonSharpBehaviour
{
    [SerializeField] private GameObject[] objs;

    private int activationIndex = 0;

    private void SetActiveObjs()
    {
        for (int i = 0; i < objs.Length; i++)
        {
            objs[i].SetActive(i == activationIndex);
        }
    }

    private void Start()
    {
        SetActiveObjs();
    }

    public override void Interact()
    {
        activationIndex++;

        if (activationIndex >= objs.Length)
        {
            activationIndex = 0;
        }

        SetActiveObjs();
    }
}

UIスライダーの値をUIテキストに反映(ローカル)

using UdonSharp;
using UnityEngine;
using UnityEngine.UI;
using VRC.SDKBase;
using VRC.Udon;

public class SliderValToText : UdonSharpBehaviour
{
    [SerializeField] private Slider slider;
    [SerializeField] private Text targetText;

    public override void Interact()
    {
        targetText.text = slider.value.ToString();
    }
}

オブジェクトの位置をマテリアルのパラメータに反映

using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;

/// <summary>
/// オブジェクトの座標を元に、マテリアルのパラメータを設定するサンプル
/// オブジェクト座標は同期されるので、値も同期される。
/// 同じマテリアル全てで共通のパラメータにしたい場合は、SharedMaterialを用いる。(処理もそちらのほうが軽い)
/// </summary>
public class LocalPosToMaterialParam : UdonSharpBehaviour
{
    [SerializeField] private MeshRenderer meshRenderer;
    [SerializeField] private string paramName = "_Color";
    [SerializeField] private float scale = 4.0f;

    private void Update()
    {
        Vector3 p = transform.localPosition * scale;
        meshRenderer.material.SetColor(paramName, new Color(
            Mathf.Clamp(p.x, 0.0f, 1.0f),
            Mathf.Clamp(p.y, 0.0f, 1.0f),
            Mathf.Clamp(p.z, 0.0f, 1.0f)
            ));
    }
}

Playerの動きのパラメーターを設定

using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;

/// <summary>
/// プレイヤー移動のパラメータをセットする
/// </summary>
public class PlayerMovingParameterSetter : UdonSharpBehaviour
{
    [SerializeField] private float runSpeed = 2.0f;
    [SerializeField] private float walkSpeed = 4.0f;
    [SerializeField] private float strafeSpeed = 2.0f;
    [SerializeField] private float jumpImpulse = 3.0f;
    [SerializeField] private float gravityStrength = 1.0f;

    private void Start()
    {
        VRCPlayerApi localPlayer = Networking.LocalPlayer;

        if(localPlayer == null)
        {
            return;
        }

        localPlayer.SetWalkSpeed(runSpeed);
        localPlayer.SetRunSpeed(walkSpeed);
        localPlayer.SetStrafeSpeed(strafeSpeed);
        localPlayer.SetJumpImpulse(jumpImpulse);
        localPlayer.SetGravityStrength(gravityStrength);
    }
}

TeleportTo 特定の位置にプレイヤー座標を設定

using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;

/// <summary>
/// 特定の位置にプレイヤー座標を設定する
/// 角度も設定できるが、Y軸以外は回転できない?(詳しくは未検証)
/// </summary>

public class TeleportToTest : UdonSharpBehaviour
{
    [SerializeField] private Transform target;
    public override void Interact()
    {
        var player = Networking.LocalPlayer;
        player.TeleportTo(target.position, target.rotation);
    }
}

SetVelocity プレイヤーの速度を設定

using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;

// プレイヤーの速度を設定するテスト
// SetVelocityを用いる。

public class VelocitySetterOnInteract : UdonSharpBehaviour
{
    [SerializeField] private Vector3 vel = Vector3.up * 10.0f;
    public override void Interact()
    {
        var player = Networking.LocalPlayer;
        player.SetVelocity(vel);
    }
}

同SetVelocity ピックアップした状態で押すとピックアップオブジェクトの前方に速度を設定

※オブジェクトにVRCPickupをアタッチ、VRC Pickupのパラメーターを設定する必要あり

using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;

public class VelocitySetterForward : UdonSharpBehaviour
{
    [SerializeField] private float speed = 10.0f;
    private Transform _transform;

    private void Start()
    {
        _transform = GetComponent<Transform>();
    }

    public override void OnPickupUseDown()
    {
        var player = Networking.LocalPlayer;
        player.SetVelocity(speed * _transform.forward);
    }
}

Immobilize プレイヤーの座標更新を行うかどうか設定

using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;

// Immobilize関数を用いて、プレイヤーが動かないようにする

public class ImmobilizeTest : UdonSharpBehaviour
{
    private bool isImmobilize = false;

    public override void Interact()
    {
        var player = Networking.LocalPlayer;
        isImmobilize = !isImmobilize;
        player.Immobilize(isImmobilize);
    }
}

(今は古いやり方)インスタンスにいる各プレイヤーにアクセスするテスト

※今は古いやり方です。現在はPlayer一覧を取得できます。

using UdonSharp;
using UnityEngine;
using UnityEngine.UI;
using VRC.SDKBase;
using VRC.Udon;

/// <summary>
/// インスタンスにいる各プレイヤーにアクセスするテスト
/// プレイヤー名を表示しているが、名前以外にも色々アクセス可能。SetVelocityや、TeleportToなどなど
/// </summary>
public class AllPlayersTest : UdonSharpBehaviour
{
    [SerializeField] private Text textUI;

    private int[] ids = null;

    private void InitializeIdsIfNull()
    {
        if(ids == null)
        {
            ids = new int[80];
            for(int i = 0; i < ids.Length; i++)
            {
                // プレイヤーIDに-1が入らないと仮定し、-1を空白とする。
                ids[i] = -1;
            }
        }
    }
    private void UpdateText()
    {
        textUI.text = "---Players Name Shower---\r\n";

        // 各プレイヤーに対して処理を行う
        // テストとして、プレイヤーID, プレイヤー名を表示
        for (int i = 0; i < ids.Length; i++)
        {
            if (ids[i] != -1)
            {
                var player = VRCPlayerApi.GetPlayerById(ids[i]);
                textUI.text += string.Format("id:{0}, name:{1} \r\n", player.playerId.ToString(), player.displayName);
            }
        }
    }

    public override void OnPlayerJoined(VRCPlayerApi player)
    {
        InitializeIdsIfNull();

        for (int i = 0; i < ids.Length; i++)
        {
            if(ids[i] == -1)
            {
                ids[i] = player.playerId;
                break;
            }
        }

        UpdateText();
    }

    public override void OnPlayerLeft(VRCPlayerApi player)
    {
        InitializeIdsIfNull();

        for (int i = 0; i < ids.Length; i++)
        {
            if (ids[i] == player.playerId)
            {
                ids[i] = -1;
                break;
            }
        }

        UpdateText();
    }
}

SetPlayerTag

using UdonSharp;
using UnityEngine;
using UnityEngine.UI;
using VRC.SDKBase;
using VRC.Udon;

/// <summary>
/// タグ(おそらくUnityのタグではない)を設定するテスト
/// 各プレイヤーごとにstringをキー、stringをバリューとした辞書を設定できる
/// 多分同期はされていない(要検証)
/// </summary>

public class TagTest1 : UdonSharpBehaviour
{
    [SerializeField] private Text textUI;
    public override void Interact()
    {
        var localPlayer = Networking.LocalPlayer;
        localPlayer.SetPlayerTag("a0", Random.Range(0, 10).ToString());

        textUI.text = "---TagTest---\r\n";

        // FIXME: 正しいやり方に修正する
        // 各プレイヤーごとに処理
        for (int i = 0; i < 99; i++)
        {
            var player = VRCPlayerApi.GetPlayerById(i);
            if (player == null)
            {
                continue;
            }
            textUI.text += string.Format("name:{0}, (tag:a0).value:{1} \r\n", player.displayName, player.GetPlayerTag("a0"));
        }
    }
}

VRCPlayerApiから取得できるシステム系の値を表示するテスト

using UdonSharp;
using UnityEngine;
using UnityEngine.UI;
using VRC.SDKBase;
using VRC.Udon;

/// <summary>
/// VRCPlayerApiから取得できるシステム系の値を表示するテスト
/// 他にも座標など取れそうだった
/// </summary>
public class LocalPlayerParam : UdonSharpBehaviour
{
    [SerializeField] private Text textUI;

    public override void Interact()
    {
        var player = Networking.LocalPlayer;
        textUI.text = "---Networking.LocalPlayer Params---\r\n";
        textUI.text += string.Format("displayName: {0} \r\n", player.displayName);
        textUI.text += string.Format("isLocal: {0} \r\n", player.isLocal.ToString());
        textUI.text += string.Format("isMaster: {0} \r\n", player.isMaster.ToString());
        textUI.text += string.Format("playerId: {0} \r\n", player.playerId.ToString());
        textUI.text += string.Format("IsUserInVR: {0} \r\n", player.IsUserInVR().ToString());
        textUI.text += string.Format("IsPlayerGrounded: {0} \r\n", player.IsPlayerGrounded().ToString());
    }
}

プレイヤーのトラッキング情報を取得するテスト

using UdonSharp;
using UnityEngine;
using UnityEngine.UI;
using VRC.SDKBase;
using VRC.Udon;

/// <summary>
/// プレイヤーのトラッキングデータを取得するテスト
/// 頭、右手、左手それぞれの位置の座標、角度を取得できる
/// </summary>

public class TrackingDataTest : UdonSharpBehaviour
{
    [SerializeField] private Text textUI;

    public override void Interact()
    {
        var player = Networking.LocalPlayer;

        textUI.text = "---TrackingDataTest---\r\n";
        {
            var headData = player.GetTrackingData(VRCPlayerApi.TrackingDataType.Head);
            textUI.text += string.Format("Head-Pos: {0}\r\n", headData.position.ToString());
            textUI.text += string.Format("Head-Rot: {0}\r\n", headData.rotation.ToString());
            textUI.text += "\r\n";
        }
        {
            var rightHandData = player.GetTrackingData(VRCPlayerApi.TrackingDataType.RightHand);
            textUI.text += string.Format("RightHand-Pos: {0}\r\n", rightHandData.position.ToString());
            textUI.text += string.Format("RightHand-Rot: {0}\r\n", rightHandData.rotation.ToString());
            textUI.text += "\r\n";
        }
        {
            var leftHandData = player.GetTrackingData(VRCPlayerApi.TrackingDataType.LeftHand);
            textUI.text += string.Format("LeftHand-Pos: {0}\r\n", leftHandData.position.ToString());
            textUI.text += string.Format("LeftHand-Rot: {0}\r\n", leftHandData.rotation.ToString());
            textUI.text += "\r\n";
        }
    }
}

時刻表示

using UdonSharp;
using UnityEngine;
using UnityEngine.UI;
using VRC.SDKBase;
using VRC.Udon;

public class TimeTest : UdonSharpBehaviour
{
    [SerializeField] private Text textUI;

    private void Update()
    {
        textUI.text = string.Format("{0:hh:mm:ss}", System.DateTime.Now);
    }
}

現実の時間に応じて姿勢(transform.rotation)を更新

using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;

/// <summary>
/// 現実の時間に応じて姿勢(transform.rotation)を更新する
/// </summary>
public class RotationOnRealTime : UdonSharpBehaviour
{
    // 一日に何周するか
    [SerializeField] private float frequency = 1.0f;

    private Transform _transform;

    private void Start()
    {
        _transform = GetComponent<Transform>();
    }

    private void Update()
    {
        var now = System.DateTime.Now;
        // NOTE: Ticks使ってもいいかも
        float rate = now.Hour / 24.0f + now.Minute / (24.0f * 60.0f) + now.Second / (24.0f * 60.0f * 60.0f) + now.Millisecond / (24.0f * 60.0f * 60.0f * 1000.0f);

        _transform.rotation = Quaternion.Euler(
            360.0f * rate * frequency,
            0.0f,
            0.0f);
    }
}

VRCInstantiate

※正常に動くのか要検証

using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;

public class VRCInstantiateTest : UdonSharpBehaviour
{
    [SerializeField] private GameObject prefab;

    private Transform cashedTransform;

    private void Start()
    {
        cashedTransform = GetComponent<Transform>();
    }

    public override void OnPickupUseDown()
    {
        var go = VRCInstantiate(prefab);
        go.GetComponent<Transform>().position = cashedTransform.position;
    }
}

RayCast

using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;

/// <summary>
/// RayCastのテスト
/// レイがヒットした場所にオブジェクトを動かす
/// レイがどのレイヤーのオブジェクトに当たるか、LayerMaskを設定する必要あり
/// </summary>
public class RayCastTest : UdonSharpBehaviour
{
    [SerializeField] private Transform hitShower;
    [SerializeField] private LayerMask layerMask;

    private Transform cashedTransform;

    private void Start()
    {
        cashedTransform = GetComponent<Transform>();
    }

    private void Update()
    {
        RaycastHit hit;
        if (Physics.Raycast(cashedTransform.position, cashedTransform.forward, out hit, Mathf.Infinity, layerMask))
        {
            hitShower.position = hit.point;
        }
    }
}

参考になる記事等

github.com

ビルドなしで、Unity上でVRChatクライアントをエミュレートできます。
再生ボタンを押すとほぼ即実行されます。 効率が上がるので非常におすすめです。

hatuxes.hatenablog.jp

hatuxes.hatenablog.jp

github.com UdonSharp公式Wiki

kurotorimkdocs.gitlab.io

phi16.hatenablog.com

github.com

qiita.com

VRChatワールド制作おすすめアセット・ツール

 

 VRChatでワールドを作成する時にオススメのアセット、書籍、ツールです。

 

 

kayanomicha.booth.pm

とても丁寧に書かれた本です。まずはこの本を読むことをオススメします。

    

virtual-boys.booth.pm

 ON/OFFできる鏡をはじめ、ワールド作成に必要なギミックが入っています。
とりあえず入れるのがオススメです。

   

github.com

 Youtubeが再生できるビデオプレイヤーです。

 

65536.booth.pm

同期するペンです。ペンが置いてあるとコミュニケーションしやすくなるのでオススメです。

 

islakioriy.booth.pm

ワールドビルド時やVRCログイン時、デスクトップモードかVRモードのどちらで起動するか選択できます。微調整して動くかどうかを確認する時、いちいちHMDを被らずにデスクトップで確認できるようになるので便利です。

 

lura.booth.pm

 

chiugame.booth.pm

 ワールドでメニューを開いた時にプレイヤーの前に出てくるメニューを作ることができます。

 

 


固定長進行レイマーチングやってみたので簡単なサンプルと解説

※この記事で扱っている、固定長レイマーチングで不透明のオブジェクトを描画する手法はレアな手法です。

通常のUnityでのレイマーチング(スフィアトレーシング)の概要を知りたい方は、以下の記事、書籍がおすすめです。

gurutaka-log.com

scrapbox.io

booth.pm

概要

f:id:yagiri000:20180912095045g:plain

最近レイトレーシング専用回路を持つGPUNVIDIAから出たり,何かとレイトレーシングが流行っていたのでレイトレーシングの一種であるレイマーチングをやってみました.

本記事のサンプルプロジェクトは以下になります.比較的重くなりやすい処理なので,描画負荷には注意してください.

↓サンプルプロジェクト

github.com

目次

主な原理

レイトレーシングは,一般的に使われるラスタライズレンダリングとは異なり,レイを飛ばしてピクセルの色を決める手法です.レイを飛ばし,何かに当たった後も物体の物理特性(光がどう跳ね返るか)に応じてレイを反射,屈折させることを繰り返し,リアルな描画を行います,レイマーチングはその一種です.

f:id:yagiri000:20180911152813j:plain 出典:Chapter1. レイトレーシング法とは何か | The Textbook of RayTracing @TDU

今回のレイマーチングでは,それぞれのピクセルからレイを飛ばして,レイを一定距離ずつ動かして,自分の指定した形状に当たったらレイを反射せず即描画(そのピクセルの色を塗る)します.*1

球を描画

f:id:yagiri000:20180911153121p:plain

サンプルプロジェクトの,RayMarchingSphereシーンについて解説していきます.このシーンでは,レイマーチングによってQuadの表面に球を描画しています.

処理手順は以下です.

  1. Quadのそれぞれのピクセルのワールド座標(インスペクタのPositionで見られる座標系と同じ座標系)を求める.
  2. ピクセルのワールド座標とカメラのワールド座標から,レイが進む方向を決定する.
  3. ピクセルのワールド座標からレイを飛ばし,少し移動させる.
  4. 移動後,その場所の形状関数から,オブジェクトに当たったか判定する.当たってなかったら3. に戻る.
  5. オブジェクトに当たったらそのピクセルの色を決定する.一定回数進んで当たらなかったらそのピクセルを黒とする.

手順2では,図に示すように,ピクセルとカメラの位置からレイの飛ぶ方向を決めます.一般的にはカメラからレイを飛ばしますが,今回ではピクセルの位置からレイを飛ばしています.これにより描画元メッシュ形状に応じた断面の描画を可能にしています.

f:id:yagiri000:20180911151420p:plain

手順3,4では,少しだけレイを進めることを繰り返します.

f:id:yagiri000:20180911152208p:plain

形状関数isInObjectはfloat3(座標)を取り,オブジェクト内かをboolで返す関数です.この関数によって形状が決まります.*2 今回は,原点から一定距離の時trueを返すので,オブジェクトの形は球です.

bool isInObject(float3 pos) {
    return distance(pos, float3(0.0, 0.0, 0.0)) < _Threshold;
}

コード全体は以下になります.

Shader "Custom/RayMarchingSphere"
{
    Properties
    {
        _Threshold("Threshold", Range(0.0,3.0)) = 0.6 // sliders
    }
    SubShader
    {
        Tags{ "Queue" = "Transparent" }
        LOD 100

        Pass
        {
            ZWrite On
            Blend SrcAlpha OneMinusSrcAlpha

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 pos : POSITION1;
                float4 vertex : SV_POSITION;
            };

            v2f vert(appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.pos = mul(unity_ObjectToWorld, v.vertex);
                o.uv = v.uv;
                return o;
            }

            float _Threshold;

            // 座標がオブジェクト内か?を返し,形状を定義する形状関数
            // 形状は原点を中心とした球.
            // 原点から一定の距離内の座標に存在するので球になる.
            bool isInObject(float3 pos) {
                return distance(pos, float3(0.0, 0.0, 0.0)) < _Threshold;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                fixed4 col;

                // 初期の色(黒)を設定
                col.xyz = 0.0;
                col.w = 1.0;

                // レイの初期位置
                float3 pos = i.pos.xyz; 

                // レイの進行方向
                float3 forward = normalize(pos.xyz - _WorldSpaceCameraPos); 

                // レイが進むことを繰り返す.
                // オブジェクト内に到達したら進行距離に応じて色決定
                // 当たらなかったらそのまま(今回は黒)
                const int StepNum = 30;
                const float MarchingDist = 0.03;
                for (int i = 0; i < StepNum; i++) {
                    if (isInObject(pos)) {
                        col.xyz = 1.0 - i * 0.02;
                        break;
                    }
                    pos.xyz += MarchingDist * forward.xyz;
                }

                return col;
            }
            ENDCG
        }
    }
}

o.posはVertex Shader内では頂点の数しか計算されませんが(Quadだったら三角形2つで6回),ピクセルシェーダー内で補間されます.つまり,最終的にスクリーンに映ったピクセルごとにワールド座標o.posが求められます.

描画結果は以下のようになります. f:id:yagiri000:20180911153121p:plain

マテリアルプロパティの,Thresholdを変えると球の大きさが変わることが確認できます.

f:id:yagiri000:20180911154015p:plain

SampleSphereシーンのQuadを横に移動させると,球が同じ位置に描画されていることが確認できます.

f:id:yagiri000:20180911153757p:plain

様々な形状描画

存在関数や,メッシュの形状を工夫すると様々な形状を描画できます.

02_RayMarchingCubes f:id:yagiri000:20180911154242p:plain 等間隔に並ぶ立方体が無限に存在します.xyz各軸に等間隔に存在/非存在するので結果として立方体が並んでいます.

形状関数は以下のようになります.

bool isInObject(float3 pos) {
    const float PI2 = 6.28318530718;
    return 
        sin(PI2 * _Freqency * pos.x) < _Threshold && 
        sin(PI2 * _Freqency * pos.y) < _Threshold && 
        sin(PI2 * _Freqency * pos.z) < _Threshold;
}

alphaの値をデフォルトで0,レイがヒットしたら距離に応じた適当な値にしました.

col.w = pow(col.x, 0.3);

03_RayMarchingDistFromTransformPosition f:id:yagiri000:20180911154526p:plain

一定距離ごとに存在/非存在する形です.

形状関数は以下のようになります.

bool isInObject(float3 pos) {
    const float PI2 = 6.28318530718;
    float dist = length(pos);
    return sin(PI2 * dist * _Freqency + PI2 * _Time * _TimeRate) < _Threshold;
}

メッシュはCubeを使い,どの方向からも見えるようにしました.

_Time.yで時間を取得できます.

pos.x += _Time.y * _TimeRate;

Cubeを移動させても形状の中心をCubeの中心と一致させたかったため,形状関数に渡す座標の原点をCubeの座標にしました.

// Transfrom.positionを形状関数に渡す座標の原点に設定する
if (isInObject(pos-transformPos)) {
    col.xyz = 1.0 - i * 0.01;
    col.w = col.x;
    break;
}

以下のようにすることで,Transfromの座標を取得しています.

o.transformPos = mul(unity_ObjectToWorld, float4(0.0, 0.0, 0.0, 1.0));

04_RayMarchingSinPosPlusSinDist f:id:yagiri000:20180911155053p:plain

名状しがたい形状が無限に存在します. このように形状関数の形を複雑にすると複雑な形を表現できます.

bool isInObject(float3 pos) {
    pos.x += _Time * _TimeRate;
    pos.xyz *= _Scale;
    float dist = sin(pos.x) * sin(pos.y) * sin(pos.z);
    dist = pow(dist, 2);
    float sxsy = sin(pos.x) * sin(pos.y);
    float sxsz = sin(pos.x) * sin(pos.z);
    float szsy = sin(pos.z) * sin(pos.y);
    return sin(dist*_DistScale) + sin(sxsy * _FreqScale) + sin(sxsz * _FreqScale) + sin(szsy * _FreqScale) < _Threshold;
}

描画負荷

画面上のピクセル数 × レイの進行回数(StepNum) × 形状関数isInObjectの複雑さ が大まかな処理の重さになると思われます.つまり,StepNumを大きな数にしたり,「重めの」レイトレオブジェクトにカメラを近づけスクリーンに映る領域(ピクセル数)が大きくなると重くなります.

さいごに

最後まで読んでいただきありがとうございました.もしよく分からない部分があっても,形状関数をいじるといろいろな形状ができると思うので,ぜひオリジナル形状を作ってみてください.皆さんが作ったレイマーチングオブジェクトを見る日を楽しみにしております.

VRChatでレイマーチングを行いたい方向けサンプル

yagiri.booth.pm

参考文献

三葉レイのCG技術チャンネル - YouTube

参考になるレイマーチングの記事

wgld.org | GLSL: シェーダ内でレイを定義する |

Unity でオブジェクトスペースのレイマーチをやってみた - 凹みTips

Unity でオブジェクトスペースの Raymarching をフォワードレンダリングでやってみた - 凹みTips

*1:ちなみに,距離関数を使ってオブジェクトとの距離に応じて進む距離を変えるスフィアトレーシングのほうが一般的です.

*2:形状を定義する関数なのでこの記事では形状関数と呼びます.

DirectX11始めました

以前からシェーダーなど含めて触ってみたかったのと,一応就活用にDirectX11触り始めました.
年内に簡単なTPSもどき作ることを目標にしてます.
触り始めたバージョンが11なのは,12は魔境みを感じたのと,9だと最新のシェーダーを試すのが難しそうだと思ったからです.
これまでのDirectX歴は「DirectX9シェーダープログラミングブック」のサンプルだけ一通り動かしたぐらいで,正直パイプラインとかはよくわからないままシェーダーの計算式の部分だけ追った程度です.*1
こっから1ヶ月ぐらいは,「ゲームプログラマになる前に覚えておきたい技術」の3D計算系の部分を読み返しつつ,「DirectX10/11プログラミングブック」を読んでいって初歩的な部分を把握して,DirectX Tool Kitとか触ってみようかと思っています.

現状の進捗ですが,Visual Studio 2015に付いてるサンプルを改変して各頂点の座標を変えるプログラムを作りました.

f:id:yagiri000:20170916164158p:plain

Visual Studio 2015にももっと最小のサンプルコードつけてほしいなあとか思ったり*2
よってネットでサンプルコードを探したのですが,初歩的でミニマムなサンプルコードを探すのに結構苦労しました.DirectX初心者としては公式のSDKをインストールしてC直下のフォルダに展開されるSamples内のTutorialが現状ネットで見つけた中で一番わかりやすかったです.ここぐらいまでやって,ネットだと情報調べるのに時間がかかりそうと思い,本を買いました.

以下,読んでる本の雑感です.

これまでDirectXを全く触ったことがなく,これから11を初めたい方は,「DirectX 10/11 プログラミング」を読み始め,足りない3D描画関係の知識を「ゲームプログラマになる前に覚えておきたい技術」で補完するといいと思いました.Visual Studio 2017でそのまま動くDirectX11のサンプルがついたシェーダープログラミングブックととかSEGA本とかDirectX10/11の初心者用本が出てくれるといいなあ(詠嘆)

余裕あれば友達にDirectX11を布教して仲間*3を増やしたいと思いつつ...
ではでは.

*1:Imagire is GOD

*2:Windowsユニバーサルアプリケーションのサンプルを含むDirectX11サンプルしかない

*3:ゾンビ

コミケ初参加―ブースであったほうが良さそうなもの

コミケ初参加してきました。サークルとしてのブログ記事はこちら→

冬コミお疲れ様でした!&ブログ始めました!&新年の挨拶 - アリス第一観測所

 

今回自分のサークルとして参加することは初めてだったのでいい経験になりました。

この記事では、今回参加して感じた、同人ゲームサークルとして参加するにあたって、当日ブースであったほうがよさそうなものや、CDとDLカードについて書いていきたいと思います。

 

当日のブース様子

f:id:yagiri000:20170103125530j:plain

  • ゲームの動画を流す用ノートPC or タブレット
    ゲームのPVや、ゲームの見どころとなる部分のプレイ動画を切り貼りしたものを流しました。動画があるとゲームのイメージをつかみやすいので、動画を切り貼りする余裕がない場合でも、普通のステージのプレイ動画でもいいのであったほうがよさそうだと思いました。(当日会場で撮った鮮度の高いゲームプレイ動画を流しました
    サークル前を通っていく人は3~5秒ぐらいしか画面を見ていなかったので、動画では常に見どころとなる部分が流れているといいと思いました。また、コミケには電源が無いので、ノートPC or タブレット端末で動画を流すことになります。今回はmacbook airで動画を流しました。通常のノートPCだと開始~終了まで持たないかもしれないので、可能であれば2台持っていくといいと思います。
    また、端末ですが、できるだけ大きい画面で、視野角が広く、発色がいいもの(ディスプレイ系研究室並の意見)がいいと思います。タブレットは、視野角は広いのですが、画面が小さいので、できればノートPCのほうが良さそうだと思いました。

  • 動画再生用ノートPCを載せるための台
    台にノートPCを載せたほうが、歩いている人から動画が見えやすいので、持っていきました。今回は家で普段は棚として使っているものを持っていきましたが、折りたためるタイプのもののほうが持ち運びやすそうだと思いました。

    Amazon.co.jp : パール金属 キッチンストレージ 積み重ね 棚 (L) H-7271 : ホーム&キッチン

  • ポスタースタンド

    大型ポスターを吊り下げるのに必要です。当日のブース様子の写真には映っていませんが、売り子の後ろのほうにあります。やはりポスターがあると目立つので、あってよかったです。
  • ポスター

    f:id:yagiri000:20170103131540p:plain

    インターネットからIllustratorのデータを入稿して印刷ができるプリントサービスを使って印刷しました。ゲームのタイトルロゴ・サークルロゴ・ブースを載せました。ブースが書いてあると自分がどこにいるかわかるので、一般参加者としてはありがたいです。
    【サイズ】今回はB1(728 × 1030 ミリ)にしましたが、ポスター下1/4ぐらいが売り子の頭で隠れる感じになりました。インパクトはありましたが、B2(515 × 728 ミリ)のほうが取り回しがよさそうだと思いました。
    【紙】色々選べますが、半光沢紙が見栄えがよく光の照り返しも小さいためよいそうです。→

    ポスター印刷の用紙選びに困ったら「半光沢紙」をお勧めします

    また、ブースの後ろから裏面も見えるので、次回は裏面にも印刷をしようと思いました。

  • お品書き

    f:id:yagiri000:20170103131607p:plain

    頒布物の外観と価格を書きました。デザインとの兼ね合いになりますが、ゲームの概要説明、スクリーンショットが書いてあるとなおいいかもしれません(他の紙に分けてもいいかも)。印刷はコンビニで行い、アクリルフレーム(後述)に挟んで展示しました。
  • アクリルフレーム

    www.muji.net

    アクリルフレームです。これにお品書きをはさんで展示しました。
  • 卓上popスタンド

    https://www.amazon.co.jp/gp/product/B00JQ6DOK2/ref=oh_aui_detailpage_o07_s00?ie=UTF8&psc=1

    コンビニでポスターのグラフィックをA3の紙に印刷して吊り、机の上に置きました。
  • テーブルクロス

    Amazon CAPTCHA

    テーブルクロスを自作し、サークルのキャラクターを印刷しているところもあるので、その方向に凝れそうだと思いました。
  • ブックスタンド

    f:id:yagiri000:20170103140858j:plain
    頒布物(CDケース)、色紙を立てるのに使いました。

  • CD-ROM, DLカードについて
    今回はトールケース+CD-ROMとDLカードで迷ったのでどちらも作成しました。トールケース+CD-ROMのほうが早く売れました。

    f:id:yagiri000:20170103142342j:plainf:id:yagiri000:20170103142455j:plain

    今回はCD-ROM自体への印刷は業者に依頼、トールケースに入れるパッケージの印刷はコンビニで行い、トールケースにパッケージを入れる作業は自分で行いました。CDに作品を焼く作業も自分で行いました。
    また、ダウンロードカードはconcaを使用し、カード印刷もconcaのサービスを利用しました。(ダウンロードコードだけconcaで発行してもらい、カード印刷は他のところに頼むという方法もあります)


    CDは焼く必要があり、会場に持っていくのが辛い(搬入という手もあるが前日焼きできない)ですが、「CDで買いたい」という人も結構いました。
    ダウンロードカードはゲームのデータを所定のアドレスにアップロードするだけなので楽ですし、Day1パッチ(クラウンアップデート)ができるため便利です。カードは簡単に持ち運べるので今後もっと普及していくと思います。
    ダウンロードカードのみ頒布する時の注意ですが、当日コミケ側に見本として1部データが入ったCD-ROMを提出しなければいけません。

  • 紙・ペン・小さいホワイトボード・テープ
    当日、「完売!」や、「新刊あります!」など、細かい情報を書くのに便利です。ペンは太いものも持っていくと大きな文字を書きやすいですし、色ペンを持っていくとデコりやすいです。
  • ネームホルダー・名刺
    名刺(というかtwitterアイコン)を首から下げていると誰が誰だか分かりやすいのでみんなハッピーになれると思います。

    f:id:yagiri000:20170103152028j:plain

いろいろ書いてきましたが、ゲームの雰囲気が知れるゲームの動画が一番重要だと思いました。ポスターに関しては作ってて楽しいですし、モチベも上がるので是非作ってみてください。また、頒布形式はconcaなどのダウンロードカードの方が(ゲーム出す側としては)敷居が低いと思いました。

また、今回自サークルでは初参加でしたが、デジゲー博で得られた知見や、毎回参加している先輩からの助言で十分な準備が出来ました。ありがとうございました。

導入系の役に立ったページ

Sublime Text

Sublime Text で Markdown を快適にする3つのパッケージ
http://webmem.hatenablog.com/entry/sublime-text-markdown

恋に落ちる「Sublime Text」のインストール・日本語化
http://webmem.hatenablog.com/entry/sublime-text

プログラミングやマークアップで特に役立つ、Sublime Textの標準機能
http://www.buildinsider.net/small/sublimetext/02

シェルスクリプト

シェルスクリプト入門 書き方のまとめ
http://motw.mods.jp/shellscript/tutorial.html#intro

Boost

Boostライブラリのビルド方法
http://boostjp.github.io/howtobuild.html

Visual Studio

初級者向けVisualStudio便利機能集Vol.1
http://qiita.com/hart_edsf/items/4d1561751a96d145301a

Unity

Visual Studio Tools for Unity ( UnityVS ) で Unity 開発/デバッグの生産性を上げよう
http://yutawatanabe.hatenablog.com/entry/visual-studio-tools-for-unity-unityvs

Tex

簡単LaTeXインストールWindows編(2014年7月版)
http://did2memo.net/2014/03/06/easy-latex-install-windows-8-2014-03/

画像内の文章や数式をTeX・Word形式に変換出来るMoshaが凄い!
http://nasimeya.blog.fc2.com/blog-entry-926.html

手書きの数式を自動認識してLaTeXやMathMLにする「Web Equation」
http://gigazine.net/news/20120203-latex-mathml-web-equation/

OpenGL

Natural Software WindowsGLUT を使う準備をする
http://www.naturalsoftware.jp/blog/2901

GLUTによる「手抜き」OpenGL入門
http://www.wakayama-u.ac.jp/~tokoi/opengl/libglut.html

Lua

tima620のゆらぎ WindowsVisual StudioLua
http://tima620.hatenablog.com/entry/2014/03/07/051052

備忘録
http://my-progress-diary-ako.hatenadiary.com/archive/category/%E3%80%90Lua%E3%80%91

LuaC++組み込み方自分用まとめ
http://qiita.com/hiz_/items/8739c46ddd2563a5603f