20191020のUnityに関する記事は8件です。

Vuforiaを使用する際に異なる座標系を合わせる

ARやMRは座標系の扱いが難しい

今回は前提として3つの座標系を扱う必要があって苦労したので忘れないようにメモ

ARで表示したいオブジェクトをImageTargetの子要素にするケースがほとんどだが、親子関係は操作できない制約があったので
「親子関係を作らずに、ImageTargetの移動や回転に合わせてオブジェクト群の表示を更新する」必要がある

Example.png

ImageTarget

Vuforiaに登録した画像マーカー

ARオブジェクト用シーン

Vuforiaを動かすメインのシーンとは別に用意したオブジェクト配置用のシーン
その中でImageTargetの位置を設定

異なる3つの座標系

  • カメラに映る座標系(ImageTarget含む)
  • ARオブジェクト用シーンの座標系 (※Prefab化して配置)
  • ARオブジェクト用シーンに含まれるImageTargetを中心とした座標系

カメラ映像上にARオブジェクト用シーンを配置する

実際にカメラに映るImageTargetの位置にARオブジェクト用シーンを配置する
以下の手順で配置を行う

  1. ImageTargetの位置にARオブジェクト用シーンを移動
  2. ARオブジェクト用シーンに含まれるImageTargetをImageTargetの位置に移動
  3. ARオブジェクト用シーンに含まれるImageTargetをImageTargetと一致するように回転
  4. 以降ImageTargetの回転や移動に合わせて、ARオブジェクト用シーンも更新する

Success.gif

完成したコード

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

public class ARObjectsSetter : MonoBehaviour
{
    [SerializeField]
    private Transform _actualImageTarget;
    [SerializeField]
    private Transform _arObjectsScene;

    [SerializeField]
    private Vector3 _virtualImageTargetPosition;
    [SerializeField]
    private Vector3 _virtualImageTargetRotation;

    private GameObject dummyImageTarget;
    private Vector3 relativePosition;
    private Vector3 parentLocalAxis;
    private float rotationAngle;
    private bool _enableUpdate = false;

    [ContextMenu("Initialize")]
    private void Initialize()
    {
        // ARオブジェクト用シーンをImageTargetに合わせる
        _arObjectsScene.position = _actualImageTarget.position;
        _arObjectsScene.rotation = _actualImageTarget.rotation;

        // ARオブジェクト用シーンのローカル座標からワールド座標でのVirtualImageTargetのPositionを求める
        var offset = _arObjectsScene.TransformPoint(-_virtualImageTargetPosition);
        _arObjectsScene.position = offset;

        // 仮想の親オブジェクトを生成し、ARObjectsSceneを子要素のように扱う
        dummyImageTarget = new GameObject("DummyImageTarget");

        // DummyImageTargetの位置と回転をVirtualImageTargetに合わせる
        var rot = _actualImageTarget.rotation * Quaternion.Euler(_virtualImageTargetRotation);
        dummyImageTarget.transform.SetPositionAndRotation(_actualImageTarget.position, rot);

        // ワールド座標からDummyImageTargetのローカル座標でのPositionを求める
        relativePosition = dummyImageTarget.transform.InverseTransformPoint(_arObjectsScene.position);

        // DummyImageTarget(親)のRotationと回転行列Sの積からARObjectsScene(子)のRotationが求まる
        // ARObjectsScene.rotaion = DummyImageTarget.tranform.rotation * S
        // 両辺にDummyImageTarget.transfrom.rotaionの逆行列をかける
        // ※ 回転の合成はQuaternionの積
        var rotationMatrix = _arObjectsScene.rotation * Quaternion.Inverse(dummyImageTarget.transform.rotation);

        // rotationMatrixがワールド座標での値なので、回転量(rotaitonAngle)と回転軸(globalAxis)を抽出する
        rotationMatrix.ToAngleAxis(out rotationAngle, out Vector3 globalAxis);
        // globalAxisをワールド座標からDummyImageTargetのローカル座標に変換する
        parentLocalAxis = dummyImageTarget.transform.InverseTransformVector(globalAxis);

        // ここまでで親子階層を操作せずに、DummyImageTargetの回転と移動がARObjectsSceneにも影響されるようになったので、
        // DummyImageTargetとImageTargetの回転を一致させる
        dummyImageTarget.transform.rotation = Quaternion.LookRotation(_actualImageTarget.forward);

        _enableUpdate = true;
    }

    private void LateUpdate()
    {
        if (!_enableUpdate) return;

        dummyImageTarget.transform.SetPositionAndRotation(_actualImageTarget.transform.position, _actualImageTarget.transform.rotation);

        // あらかじめ計算しておいたDummyImageTargetのローカル座標でのARObjectsSceneのPositionをワールド座標に変換
        var position = dummyImageTarget.transform.TransformPoint(relativePosition);
        // あらかじめ計算しておいたDummyImageTargetのローカル座標での回転軸をワールド座標に変換
        var globalAxis = dummyImageTarget.transform.TransformVector(parentLocalAxis);
        // あらかじめ計算しておいた回転をDummyImageTargetのRotationに合成する
        var rotation = Quaternion.AngleAxis(rotationAngle, globalAxis) * dummyImageTarget.transform.rotation;

        _arObjectsScene.SetPositionAndRotation(position, rotation);
    }
}

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AR/MRで異なる座標系を合わせる

ARやMRは座標系の扱いが難しい

今回は前提としてVuforiaを使用し、3つの座標系を扱う必要があって苦労したので忘れないようにメモ

ARで表示したいオブジェクトをImageTargetの子要素にするケースがほとんどだが、親子関係は操作できない制約があったので
「親子関係を作らずに、ImageTargetの移動や回転に合わせてオブジェクト群の表示を更新する」必要がある

Example.png

ImageTarget

Vuforiaに登録した画像マーカー

ARオブジェクト用シーン

Vuforiaを動かすメインのシーンとは別に用意したオブジェクト配置用のシーン
その中でImageTargetの位置を設定

異なる3つの座標系

  • カメラに映る座標系(ImageTarget含む)
  • ARオブジェクト用シーンの座標系 (※Prefab化して配置)
  • ARオブジェクト用シーンに含まれるImageTargetを中心とした座標系

カメラ映像上にARオブジェクト用シーンを配置する

実際にカメラに映るImageTargetの位置にARオブジェクト用シーンを配置する
以下の手順で配置を行う

  1. ImageTargetの位置にARオブジェクト用シーンを移動
  2. ARオブジェクト用シーンに含まれるImageTargetをImageTargetの位置に移動
  3. ARオブジェクト用シーンに含まれるImageTargetをImageTargetと一致するように回転
  4. 以降ImageTargetの回転や移動に合わせて、ARオブジェクト用シーンも更新する

Success.gif

完成したコード

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

public class ARObjectsSetter : MonoBehaviour
{
    [SerializeField]
    private Transform _actualImageTarget;
    [SerializeField]
    private Transform _arObjectsScene;

    [SerializeField]
    private Vector3 _virtualImageTargetPosition;
    [SerializeField]
    private Vector3 _virtualImageTargetRotation;

    private GameObject dummyImageTarget;
    private Vector3 relativePosition;
    private Vector3 parentLocalAxis;
    private float rotationAngle;
    private bool _enableUpdate = false;

    [ContextMenu("Initialize")]
    private void Initialize()
    {
        // ARオブジェクト用シーンをImageTargetに合わせる
        _arObjectsScene.position = _actualImageTarget.position;
        _arObjectsScene.rotation = _actualImageTarget.rotation;

        // ARオブジェクト用シーンのローカル座標からワールド座標でのVirtualImageTargetのPositionを求める
        var offset = _arObjectsScene.TransformPoint(-_virtualImageTargetPosition);
        _arObjectsScene.position = offset;

        // 仮想の親オブジェクトを生成し、ARObjectsSceneを子要素のように扱う
        dummyImageTarget = new GameObject("DummyImageTarget");

        // DummyImageTargetの位置と回転をVirtualImageTargetに合わせる
        var rot = _actualImageTarget.rotation * Quaternion.Euler(_virtualImageTargetRotation);
        dummyImageTarget.transform.SetPositionAndRotation(_actualImageTarget.position, rot);

        // ワールド座標からDummyImageTargetのローカル座標でのPositionを求める
        relativePosition = dummyImageTarget.transform.InverseTransformPoint(_arObjectsScene.position);

        // DummyImageTarget(親)のRotationと回転行列Sの積からARObjectsScene(子)のRotationが求まる
        // ARObjectsScene.rotaion = DummyImageTarget.tranform.rotation * S
        // 両辺にDummyImageTarget.transfrom.rotaionの逆行列をかける
        // ※ 回転の合成はQuaternionの積
        var rotationMatrix = _arObjectsScene.rotation * Quaternion.Inverse(dummyImageTarget.transform.rotation);

        // rotationMatrixがワールド座標での値なので、回転量(rotaitonAngle)と回転軸(globalAxis)を抽出する
        rotationMatrix.ToAngleAxis(out rotationAngle, out Vector3 globalAxis);
        // globalAxisをワールド座標からDummyImageTargetのローカル座標に変換する
        parentLocalAxis = dummyImageTarget.transform.InverseTransformVector(globalAxis);

        // ここまでで親子階層を操作せずに、DummyImageTargetの回転と移動がARObjectsSceneにも影響されるようになったので、
        // DummyImageTargetとImageTargetの回転を一致させる
        dummyImageTarget.transform.rotation = Quaternion.LookRotation(_actualImageTarget.forward);

        _enableUpdate = true;
    }

    private void LateUpdate()
    {
        if (!_enableUpdate) return;

        dummyImageTarget.transform.SetPositionAndRotation(_actualImageTarget.transform.position, _actualImageTarget.transform.rotation);

        // あらかじめ計算しておいたDummyImageTargetのローカル座標でのARObjectsSceneのPositionをワールド座標に変換
        var position = dummyImageTarget.transform.TransformPoint(relativePosition);
        // あらかじめ計算しておいたDummyImageTargetのローカル座標での回転軸をワールド座標に変換
        var globalAxis = dummyImageTarget.transform.TransformVector(parentLocalAxis);
        // あらかじめ計算しておいた回転をDummyImageTargetのRotationに合成する
        var rotation = Quaternion.AngleAxis(rotationAngle, globalAxis) * dummyImageTarget.transform.rotation;

        _arObjectsScene.SetPositionAndRotation(position, rotation);
    }
}

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Oculus Store 申請時の 'Enable Android NSC to Prevent Cleartext Traffic' という警告への対処方法

はじめに

Quest アプリを Oculus Store に提出する際に警告が出てきてしまいました。。:weary:
警告内容を見ると、ネットワークのセキュリティ設定に関するものでした :arrow_down:

The app has not enabled Android N's Network Security Configuration(https://developer.android.com/training/articles/security-config.html) feature, which forces the use of encryption (HTTPS) for all of the app's connections. The feature will block most cleartext HTTP traffic initiated by the app, which helps ensure that all data to and from the app has a base level of protection at all times.

対処方法を調べた所、
どうやら network_security_config.xml というファイルをプロジェクトに追加することで対策可能そうでした :thumbsup:

そこで、
今回は network_security_config.xml の追加手順 & 設定方法について書いていきます :pencil:

network_security_config.xml をプロジェクトに追加する

追加するフォルダは /Assets/Plugins/Android/res/xml になります :arrow_down:

/Assets/Plugins/Android/res/xml/network_security_config.xml
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <!-- サブドメイン含む nikaera.com 以外のアクセスには HTTPS 通信が必須とする -->
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">nikaera.com</domain>
    </domain-config>
</network-security-config>

network_security_config.xml に記載する内容はプロジェクトによって異なると思いますが、
私の場合は特定ドメイン以外のアクセスには HTTPS 通信を必須とするよう設定しました :gear:

AndroidManifest.xml に network_security_config.xml を利用することを宣言する

プロジェクトに network_security_config.xml の内容を反映させるには、
AndroidManifest.xml に 1行設定に必要な記述を追加する必要あります :writing_hand:

/Assets/Plugins/Android/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest>
...
    <!-- android:networkSecurityConfig の設定を application タグに追加する -->
    <application
        android:networkSecurityConfig="@xml/network_security_config">
...
    </application>
...
</manifest>

AndroidManifest.xmlapplication タグに android:networkSecurityConfig 属性を追加し、
network_security_config.xml のファイルパスを指定することで設定が有効となります :white_check_mark:

この状態でビルドを行い APK の動作に問題が無いこと確認出来次第、
Oculus Store に再度アップロードしてみると、警告が無くなっていることが確認できるはずです :thumbsup:

おわりに

今回は Enable Android NSC to Prevent Cleartext Traffic の対処法について紹介しました :hand_splayed:

実は他にも警告が出ているのですが、どうやらどれも Android に関わるもののようで、
Oculus Store 申請時は、「ココらへんも気をつけないといけないんだなあ」と思いました。。 :skull:

参考リンク

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Unity] RGB三チャネル同時色相変換シェーダー(明暗グラデーション対応)

経緯

初期のドラ●エみたいな昔のゲームだと、容量削減のためにパレット色変換で同じ元絵から、色違いのキャラ画像出したりしてましたよね。あれの発展形で、R,G,Bの三原色の明暗を維持しつつ、違う色に置き換えるシェーダーを書いてみました。
huetransHouse.jpg

下図は色変換のイメージです(変更後の色はサンプル画像のものとは異なります)。
huetrans.png
通常このような色置換はパレット指定でやると思うんですが、このシェーダーのメリットはパレット管理しなくてよいこと。たった3色シェーダーに設定するだけで同系色の明暗グラデーション置換ができるし、一度覚えて貰えばデザイナーさんに描いてもらう場合のやりとりも楽です。
デメリットとしてはパレット置換に比べると色相の異なる4色以上の変換ができないので自由度が減る場合がありますが、現実的には3系統の明暗グラデーション置換ができれば事足りる場合がほとんどだと思います。

元ネタはこれです。
http://www7b.biglobe.ne.jp/~nao8140/freetrain/patch.htm#rgb_trans

言葉で説明するとどうしても若干ややこしくなるんですが、RGBが (x,0,0),(0,y,0),(0,0,z) となるようなピクセルをそれぞれ、A,B,C の任意のカラーへ、元の明度に比例した明度で置換するわけです。

  赤系:(x,0,0) => (x*A.r, x*A.g, x*A.b);
  緑系:(0,y,0) => (y*B.r, y*B.g, y*B.b);
  青系:(0,0,z) => (z*C.r, z*C.g, z*C.b);
  それ以外:(x,y,z) => (x,y,z); // 変化なし!

シェーダー実装(基本編)

HueTrans.shader
Shader "Custom/HueTrans"
{
    Properties
    {
        [NoScaleOffset] _MainTex ("Day Texture", 2D) = "white" {}

        _RedTransfar ("RED Transfer Color", Color) = (1,0,0,1)
        _GreenTransfar ("GREEN Transfer Color", Color) = (0,1,0,1)
        _BlueTransfar ("BLUE Transfer Color", Color) = (0,0,1,1)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            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;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            fixed3 _RedTransfar;
            fixed3 _GreenTransfar;
            fixed3 _BlueTransfar;


            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            inline fixed4 transferColor(in fixed4 srcCol) {
                // test srcCol if the two of r,g,b are 0.
                float d1 = (1 - srcCol.r) * (1 - srcCol.g) * (1 - srcCol.b);
                float d2 = srcCol.r + srcCol.g + srcCol.b;
                float flag = step(d1 + d2, 1);

                fixed3 rgb = lerp(
                    srcCol.rgb,
                    _RedTransfar * srcCol.r + _GreenTransfar * srcCol.g + _BlueTransfar * srcCol.b,
                    flag );

                return fixed4(rgb,1);
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);

                // stop rendering if it is transpalent.
                if(col.a < 0.0001) discard;

                // apply hue transform.
                col =  transferColor(col);

                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}

色置換で重要なのは transferColor 関数だけです

    inline fixed4 transferColor(in fixed4 srcCol) {
       // test srcCol if the two of r,g,b are 0.
        float d1 = (1 - srcCol.r) * (1 - srcCol.g) * (1 - srcCol.b);
        float d2 = srcCol.r + srcCol.g + srcCol.b;
        float flag = step(d1 + d2, 1);

        fixed3 rgb = lerp(
            srcCol.rgb,
            _RedTransfar * srcCol.r + _GreenTransfar * srcCol.g + _BlueTransfar * srcCol.b,
            flag );

        return fixed4(rgb,1);
    }

ここだけで以下の変換を全て行っていることになります。意外とコンパクトに纏まってますよね(自画自賛)!?

  赤系:(x,0,0) => (x*A.r, x*A.g, x*A.b);
  緑系:(0,y,0) => (y*B.r, y*B.g, y*B.b);
  青系:(0,0,z) => (z*C.r, z*C.g, z*C.b);
  それ以外:(x,y,z) => (x,y,z); // 変化なし!

シェーダーで使用している計算式は、(私が知らないだけでどこかの偉い人が発明済みかもしれませんが)自力で捻りだしたものですので参考資料とかはありません。

rgb の同時計算自体はわりとすぐに思い付いたので、それを利用できるような flag の算出方法を何とか捻りだそうと試行錯誤していて、偶然見つけたようなものです。そんな経緯なので、直感的にわかりやすい説明はなく、なぜこれでいいのか後付けで証明してみました。

一応、以下に詳しく解説しますが、自分でも眠くなりそうな長い説明なんで、興味のない方は読み飛ばしてください。

詳しい説明

最初の三行は r,g,b のうち少なくとも二つが0である条件を求めるためのものです。
d1 + d2 > 1 を展開すると rg + gb + br > rgb という不等式ができますが、もし r,g,b のうち二つ以上が0であれば両辺共に0になってこれは成り立ちません
一方で r,g,b のうち少なくとも二つ以上が0でない場合ですが、一つだけ0がある場合はすぐおわかりでしょう。全て0でない場合は、両辺 rgb で割って 1/b + 1/r + 1/g > 1 と変形するとr,g,b いずれも1以下であることから必ず成り立ちつことがわかります。
ゆえに、r,g,b のうち少なくとも二つが0であるは d1 + d2 == 1 であり、それ以外の場合は d1 + d2 > 1 である、と言えます。これを 0,1 値に変換したのが float flag = step(d1 + d2, 1); の行です。

その次の lerp 関数を使った式は、シェーダーでおなじみの参考演算子的な使い方をしてます。
flag == 0 の時、rgb には元のピクセル値 (srcCol.rgb) が入り、 flag == 1 の時は rgb には _RedTransfar * srcCol.r + _GreenTransfar * srcCol.g + _BlueTransfar * srcCol.b が入ります。
しかし flag == 1 の時は r,g,b のうち少なくとも二つは0であるので、結局のところ _RedTransfar * srcCol.r_GreenTransfar * srcCol.g_BlueTransfar * srcCol.b のどれか1つのうち0でないものが入ります(※厳密には r,g,b 全て0の場合もこっちの計算が適用されるので、全て0になることもあるが、黒が書き出されるだけなので問題ない)。
例えば0でないのが r だったら、_RedTransfar で指定した色に、元絵の r 値を乗じたものが書き出し色になります。

結果

四つ並んだ左上の画像が、色置換のない素の画像です。(元ネタサイトの許可を得て画像を利用させていただいています)ほかの三つはそれぞれRGBを違う色に置換しています。上手くいきました。
huetransHouse.jpg

光沢も出したい(白混色対応)

自分としては、一応ここまでで十分目的は果たしたんですが、せっかく記事にしたのでもう少し発展させてみます。
このシェーダーの弱点は黒との混色だけで白との混色には対応できていません。
どういうことかと言うと、下記のような光沢のある絵で使われる RGB と白との混色(ハイライト)が置換できないということです。

hueTransSprite.png
上の絵を色置換したものが下の画像です。ハイライト部分が置換できていないのがわかります。
huetrance-darken.jpghuetrans-zoom.jpg

シェーダー実装(拡張編)

今まで、黒混色のみ対応していたシェーダーを、同時に白混色にも対応させます。
下記は色変換のイメージ

黒混色のみ 黒混色+白混色
huetrans.png huetrans-ex.png

修正は transferColor 関数だけなので、他は割愛します。

    inline fixed4 transferColor(in fixed4 srcCol) {
        // test srcCol if the two of r,g,b are 0.
        float d1 = (1 - srcCol.r) * (1 - srcCol.g) * (1 - srcCol.b);
        float d2 = srcCol.r + srcCol.g + srcCol.b;
        float flag = step(d1 + d2, 1);

        fixed3 rgb = lerp(
            srcCol.rgb,
            _RedTransfar * srcCol.r + _GreenTransfar * srcCol.g + _BlueTransfar * srcCol.b,
            flag );

        fixed3 white = fixed3(1,1,1);
        flag = (1 - flag) * step(1, srcCol.r) * step(1, 1- abs(srcCol.g - srcCol.b));
        rgb = lerp(
            rgb,
            lerp(_RedTransfar, white, srcCol.g),
            flag);

        flag = (1 - flag) * step(1, srcCol.g) * step(1, 1- abs(srcCol.b - srcCol.r));
        rgb = lerp(
            rgb,
            lerp(_GreenTransfar, white, srcCol.b),
            flag);

        flag = (1 - flag) * step(1, srcCol.b) * step(1, 1- abs(srcCol.r - srcCol.g));
        rgb = lerp(
            rgb,
            lerp(_BlueTransfar, white, srcCol.r),
            flag);

        return fixed4(rgb,1);
    }

今回追加した白混色の置換処理は r,g,bそれぞれに分けて三回計算しています。
flag は (1-flag) で直前の式の else 条件とし、r, g, b のどれか一つが1で、他の二つが等しい場合に 1 になるようになっています。例えば赤系のハイライトなら (r,g,b) == (1,x,x) になるような色が対象です。この時の書き出し色は lerp 関数を使って x * white + (1-x) * _RedTransfar となります。

全体をまとめると、こんな感じの変換対応になります。

  赤系+黒:(x,0,0) => (x*A.r, x*A.g, x*A.b);
  赤系+白:(1,x,x) => (x + (1-x)*A.r, x + (1-x)*A.g, x + (1-x)*A.b);
  緑系+黒:(0,y,0) => (y*B.r, y*B.g, y*B.b);
  緑系+白:(y,1,y) => (y + (1-y)*B.r, y + (1-y)*B.g, y + (1-y)*B.b);
  青系+黒:(0,0,z) => (z*C.r, z*C.g, z*C.b);
  青系+白:(z,z,1) => (z + (1-z)*C.r, z + (1-z)*C.g, z + (1-z)*C.b);
  それ以外:(x,y,z) => (x,y,z); // 変化なし!

結果

ご覧の通り、ハイライト部分も色置換できるようになりました!
黄色、マゼンタ、シアンなどRGB系以外の色は影響を受けていないことも確認できました。
huetrans-advanced.jpg

更なるコード効率化(改良編)

当初は上に挙げたコードで満足していましたが、白混色ももっとスッキリ効率的な書き方ができるんじゃないかと思って検討してみました。

そして思いのほか上手く行ったので下記に紹介します。

    inline fixed4 transferColor(in fixed4 srcCol) {
        float lowest = min(srcCol.r, min(srcCol.g, srcCol.b));
        fixed3 hueCol = srcCol.rgb - lowest;
        fixed3 hilight = fixed3(lowest,lowest,lowest);

        // test srcCol if the two of r,g,b are 0.
        float d1 = (1 - hueCol.r) * (1 - hueCol.g) * (1 - hueCol.b);            
        float d2 = hueCol.r + hueCol.g + hueCol.b;                
        float flag = step(d1 + d2, 1);

        fixed3 rgb = lerp(
            srcCol.rgb,
            _RedTransfar * hueCol.r + _GreenTransfar * hueCol.g + _BlueTransfar * hueCol.b + hilight,
            flag );

        return fixed4(rgb,1);
    }

変数名の変更を除けば、実質的な計算は、黒混色だけの時から3行増えただけ&変換後の色算出に1項増えただけで済みました。

詳しい説明

まず、追加された冒頭の三行について。
lowest は r,g,b のうちで最小値を求めています。最小値ですが、黒混色も白混色も r,g,b のうち二つが最小値になるはずです。
hueCol は元の色から最小値を引いた色です。この操作により、白混色の対象カラーも黒混色と同じ (x,0,0),(0,y,0),(0,0,z) のどれかに変わりますね!
highlight はスカラー値である lowest を fixed3 に変えただけです。RGBが等しいのでグレー系ですね。これは色変換後に足すべきベースの明るさを意味します。

さて、その次の三行は黒混色の時と同じ条件判定用の計算ですね。先ほど述べたように、hueCol は黒変換と白変換で同じ条件式が使えるようになっているはずです。
メインの rgb 計算式は、 lerp の二つ目の引数式の最後に + highlight が追加されていることに注目してください。これで白混色の場合は元色と同じ割合だけ白成分が追加されます。黒混色の場合 highlight は黒(0,0,0)なので何も影響はありません。

ご使用上の注意

今回ご紹介したシェーダーは、RGB値の厳密な比較を行ってますので、オリジナル画像のRGB値が変わるような不可逆圧縮や、隣のピクセルと色が混じるような WrapMode は使えないと思います。要するにPixelPerfectなドット絵前提ってことです。

お勧めは以下のように、WrapMode を Pointにして Format を無圧縮にすることです。
texture.jpg

感想

最近ある程度シェーダーが書けるようになってきた気がします。
普段高級言語で開発してる時は、リーダブルコードを意識して、些細な効率よりも見やすい書き方を心がけてるんですが、たまにこういった、いかに効率的に書けるかっていう挑戦も面白くて、いい刺激になりますね。
ただそれだけだと、半年もすればどうしてこんな式で動くのか自分でも忘れてしまいそうなので、こうやって記事に残すのは備忘録としても役立ちそうです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unity玉転がしチュートリアル 3-1.収集するオブジェクトの作成

この記事の対象者

  • Unity入門したい人
  • 最初の一歩が踏み出せない人

OSとか環境とか

  • Windows 10 Pro
  • macOS Mojave
  • Unity 2019.2.8f1
  • Rider 2019.2.2

補足

  • 公式動画にて利用しているのはMacなので、Windowsユーザーはある程度脳内変換して見る事
  • 筆者はWindows、Macの両方の環境で確認。Ubuntuとかでは検証してない。
  • 基本Unityは英語メニューで利用
  • 間違いがあったらツッコミ大歓迎

公式

https://learn.unity.com/tutorial/collecting-scoring-and-building-the-game#5c7f8529edbc2a002053b788

オブジェクトを作成

プレイヤーが獲得するアイテムを作成
GameObject > 3D Object > Cubeで追加→座標リセットを行う

追加すると既存のプレイヤーのオブジェクトが操作に邪魔なので、非アクティブにする
非表示にする方法はInspectorにて名前の左側にあるチェックボックのON/OFFで切り替わる

image.png

追加したCubeが埋まっているので、PositionのYを0.5上に移動してあげる

image.png

メラミンスポンジみたいな形なったら成功

オブジェクトの外観変化

アイテムっぽくないので見た目を変更する

追加したアイテムの
・ScaleをXYZ全て0.5に
・TransformのRotationのXYZを全て45に
にして、小さく地面から浮いた状態に変更する

これだけでもアイテム感は確かにあるが、アイテムが動いていれば更にアイテムとわからせることが出来るので動きを追加

Cubeを回転させるスクリプトを追加

Pick Upオブジェクトを選択してInspectorからAdd ComponentでNewScriptを選択してスクリプト追加
出来上がったスクリプトはこちら

Rotator.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Rotator : MonoBehaviour
{

    // Update is called once per frame
    void Update()
    {
        transform.Rotate(new Vector3(15,30,45) * Time.deltaTime);   
    }
}

これでPlayを押すと、アイテムがしっかり回転していることがわかる

Pick Up オブジェクトのプレハブ化

プレハブ=GameObjectの設計図
プレハブを更新すると全てのオブジェクトが更新される

プレハブを格納するフォルダを作成する
フォルダ名は「Prefabs」とする
HierarchyのPick UpからProjectのPrefabsの中にドラッグする

ゲームオブジェクトの整理

整理用のオブジェクト(Pick Ups)を作って、その中を整理します。
Pick UPオブジェクトをPick Upsの子に設定する

image.png

このままPick Upオブジェクトを移動すると、斜め(今の角度準拠)に動いてしまう
行いたいことは地面にオブジェクトを動かす事なので、エディタのモードを変更する

Local→Globalに変更

image.png

そしてPick Upオブジェクトを移動すると、グローバルな座標での移動が可能になる

アイテムが1個だと寂しいので、コピーします
Edit > Duplicateからも出来るが、Ctrl+D(Macは⌘+D)のショートカットで複製可能

image.png

今回は12個配置

アイテムの色変更

Materialを複製して紐付ける
マテリアルの色を黄色に変更してプレハブにセット
プレハブにセットするので、画面内のPickUpの色が全て変更される

image.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Substance Painterでテクスチャを作って、Unityで取り込む

Substanceの基礎知識

Substanceは、Allegorithmic社が提供するソフト群で、3D形状にペイントするツールです。
Substanceはいろいろなソフトがありますが、人気の高いのが以下の3つです。

Substance Painter

レイヤーを重ねて、3Dでテクスチャを作成し、画像ファイルを出力するソフト。3D版フォトショップです。今回取り上げるのは、このソフトです。Indie版で月額19.9ドルです。

Substance Designer

ノードをつなぎ合わせて、3Dのテクスチャを作成し、画像ファイルを出力するソフト。加算や乗算などを組み合わせる仕組みのため、難易度は高いです。

Bitmap2Material

1枚のテクスチャ画像をもとに上下左右にシームレスに繋げられるタイリング画像を作成し、画像ファイルを出力するソフト。

Unityに取り込むまでの流れ

  1. 3dsMax、Mayaなどのモデリングソフトを使って、3D形状を作成。fbx、obj、ply、3dsなどの形式で出力。
  2. Substance Painterで取り込んでペイント。Albedo、Metallic、Normal、Emissionの4枚のテクスチャ画像ファイルを出力。
  3. Unityで3D画像とテクスチャ画像ファイルを取り込んで、3D画像にテクスチャ画像を割り当てる。

言語設定

中途半端に日本語が混じっていてわかりにくいため、英語表示に変えます。

image.png
image.png

3Dオブジェクトを取り込む

File→Newの順で選択します。
image.png
Selectボタンを押し、取り込みたい3Dデータを選択します。
image.png

マウス操作

  • ALt + 左ドラッグ: カメラの回転
  • ALt + 右ドラッグ: カメラのズーム
  • Alt + Ctrl + 左ドラッグ: カメラのパン(カメラの上下左右移動)
  • マウスホイール: カメラのズーム

テクスチャのチャンネル

テクスチャをリアルにするために下の5つの要素を設定します。これらの要素をチャンネルといいます。

  • Base Color: 色。RGBの色情報を指定します。
  • Height: 高さ。凹凸を表す情報。-1から+1の範囲で指定します。
  • Roughness: 粗さ。ざらつきを表す情報。値を大きくするとざらつき、反射しにくくなります。0から1の範囲で指定します。Metallicとセットで指定します。
  • Metallic: 金属感。値を大きくすると、鏡のように反射します。0から1の範囲で指定します。 Roughnessを0、Metallicを1にすると鏡のような完全な反射になります。 Roughnessを1、Metallicを0にすると反射せず粘土のような質感になります。
  • Normal: 法線。凹凸を表す情報。法線とは凹凸情報を表した画像のことで、出っ張っている場所は青が強くなります。法線は塗りつぶしレイヤで指定することが多いです。

テクスチャを取り込む

今回、木材のテクスチャとノーマルマップは以下から入手しました。
http://www.texturise.club/2014/02/seamless-wood-fine-sabbia-texture.html
また、以下のサイトを使うと、画像データからノーマルマップを作成できます。
http://cpetry.github.io/NormalMap-Online/

File->Import resourcesを選びます。
image.png
texture、current sessionを選びます。
image.png
image.png
image.png
image.png

テクスチャを割り当てる

Texturesの該当のテクスチャをBase colorにドラッグ&ドロップします。同じようにノーマルマップテクスチャをNormalにドラッグ&ドロップします。(2Dビューの画像のずれの原因不明。なぜずれているのかわかる人がいたら、教えてください。。)
image.png
Roughnessを0.6にしてテカリをおさえました
image.png

木材のリアルなテクスチャが完成しました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

カメラの視線とメッシュとの交点の座標を数学的に求める

概要

メッシュを張ってカメラでそのメッシュを見たときに、カメラの視線とメッシュとの交点の座標を数学的に計算し表示させるプログラムをUnityで作ります。

結果

方法

まずメッシュをスクリプトを使って張ります。メッシュの張り方はこちらのサイトを参考にさせていただきました。

本記事では、メッシュの頂点に球体を配置し、Unityのシーンからメッシュの頂点の位置を自由に動かせるという方針で実装していくので、参考にさせていただいたスクリプトに多少新たなコードを追加しています。

まず空のゲームオブジェクトに"Mesh_front"という名前を付け(後で"Mesh_back"も作ります)、MeshRendererMeshFilter、白いマテリアル、さらに次のスクリプトをアタッチします。

MeshMaker.cs
using System.Collections.Generic;
using UnityEngine;

public class MeshMaker : MonoBehaviour {
    public GameObject[] sphere;
    private Vector3[] point; 
    private Mesh mesh;
    private MeshFilter meshFilter;

    private void Start() {
        point = new Vector3[sphere.Length];
        for (int i = 0; i < sphere.Length; i++) point[i] = sphere[i].gameObject.transform.position;

        mesh = new Mesh();
        List<Vector3> verticles = new List<Vector3>();
        for (int i = 0; i < point.Length; i++) verticles.Add(point[i]);
        mesh.SetVertices(verticles);

        List<int> triangles = new List<int>();
        for (int i = 0; i < point.Length; i++) triangles.Add(i);
        mesh.SetTriangles(triangles, 0);

        meshFilter = GetComponent<MeshFilter>();
        meshFilter.mesh = mesh;
    }

    private void Update() {
    }
}

メッシュの頂点を表す球をsphere、球の頂点の座標をpointとしてあります。ここまでやると、下の図になります。
mesh_front.png
次に、Mesh_frontに球を渡します。とりあえず三角形のメッシュを作っていくので、球を3つ用意します。3つの球の名前をPoint0, Point1, Point2とし、Scaleを(0.1, 0.1, 0.1)としてあります。Mesh_frontのMakeMesh.csにあるSpheresのSizeを"3"に設定し、Point0, 1, 2を順番に入れます。ここまでやると、下の図になります。
three spheres.png
さて、このままだとメッシュは片面にしか張られないので、もう片方の面にも張ります。やり方は簡単です。Mesh_frontを複製し、複製後のものを"Mesh_back"という名前にします。SphereのSizeを"3"にし、球をPoint2, 1, 0の順番に入れます。先ほどとは入れる順番が逆です。
mesh_back.png
次に、Main Cameraにアタッチするスクリプトについて説明します。

FindCoordinateController.cs
using UnityEngine;
using UnityEngine.UI;

public class FindCoordinateOfIntersection : MonoBehaviour
{
    public GameObject[] spheres; //メッシュの頂点の球
    public GameObject intersection_sphere; //交点の球
    public Text coordinate; //画面上に出す座標のテキスト
    private Vector3[] point; //メッシュの頂点の座標
    private Vector3 normal, intersection; //normal: メッシュの法線ベクトル、intersection: 交点の座標
    private MeshMaker meshMaker;
    private float[] abcd; //メッシュが貼られている平面の方程式の係数
    private float parameter; //交点の座標を求めるときに使うパラメータ
    private bool intersectionIsInsidePolygon; //交点がメッシュの中に存在しているかどうか

    private void Start() {
        point = new Vector3[spheres.Length];
        for (int i = 0; i < spheres.Length; i++) point[i] = spheres[i].gameObject.transform.position;
        normal = CalculateOuterProduct(point[0], point[1], point[2]); //3点point[0]~[2]を通る平面の法線ベクトルを求める
    }

    private void Update() {
        abcd = CalculateEquationOfPlane(point[0], point[1], point[2]);

        //gameObject.transform.rotation * Vector3.forward, gameObject.transform.positionはカメラの視線の方向ベクトル
        intersection = CalculateCoordinateOfIntersection(abcd, gameObject.transform.rotation * Vector3.forward, gameObject.transform.position);

        intersectionIsInsidePolygon = WhetherIntersectionIsInsidePolygon(point, intersection, normal);

        //交点がメッシュの内部にあるときだけ交点をアクティブにする
        intersection_sphere.SetActive(intersectionIsInsidePolygon);

        //交点がメッシュの内部にあり、かつ交点がカメラの前側にあるときに、テキストに交点の座標を表示する
        if (intersectionIsInsidePolygon && parameter > 0f) coordinate.text = "(" + intersection.x.ToString("F2") + ", " + intersection.y.ToString("F2") + ", " + intersection.z.ToString("F2") + ")";
        else coordinate.text = "※交点が存在しません※";
    }

    //変数intersectionの値を読み取る、書き込むプロパティ
    public Vector3 Intersection {
        get { return intersection; }
        private set { intersection = value;  }
    }

    //このメソッドは、vec1,vec2,vec3の3点を通る平面の方程式ax+by+cz+d=0のa,b,c,dを配列で返す
    private float[] CalculateEquationOfPlane(Vector3 vec1, Vector3 vec2, Vector3 vec3) {
        float[] ans = new float[]{
            normal.x,
            normal.y,
            normal.z,
            -normal.x * vec1.x - normal.y * vec1.y - normal.z * vec1.z
        };
        return ans;
    }

    //このメソッドでは、カメラの視線とメッシュとの交点の座標が求められる
    private Vector3 CalculateCoordinateOfIntersection(float[] plane, Vector3 angle, Vector3 position) {
        parameter = -(plane[0] * position.x + plane[1] * position.y + plane[2] * position.z + plane[3]) / (plane[0] * angle.x + plane[1] * angle.y + plane[2] * angle.z);
        float x = angle.x * parameter + position.x;
        float y = angle.y * parameter + position.y;
        float z = angle.z * parameter + position.z;
        return new Vector3(x, y, z);
    }

    //このメソッドでは、vec1,vec2,vec3の3点を通る平面の法線ベクトルが求められる
    private Vector3 CalculateOuterProduct(Vector3 vec1, Vector3 vec2, Vector3 vec3) {
        Vector3 tmp1 = vec1 - vec2;
        Vector3 tmp2 = vec1 - vec3;
        return Vector3.Cross(tmp1, tmp2); //Vector3.Crossは外積を求めるメソッド
    }

    //このメソッドは引用させていただきました
    private bool WhetherIntersectionIsInsidePolygon(Vector3[] vertices, Vector3 intersection, Vector3 normal) {
        float angle_sum = 0f;
        for (int i = 0; i < vertices.Length; i++) {
            Vector3 tmp1 = vertices[i] - intersection;
            Vector3 tmp2 = vertices[(i + 1) % vertices.Length] - intersection;
            float angle = Vector3.Angle(tmp1, tmp2);
            Vector3 cross = Vector3.Cross(tmp1, tmp2);
            if (Vector3.Dot(cross, normal) < 0) angle *= -1;
            angle_sum += angle;
        }
        angle_sum /= 360f;
        return Mathf.Abs(angle_sum) >= 0.1f;
    }
}

各メソッドの説明をしていきます。

CalculateEquationOfPlaneメソッド

このメソッドの実装はこちらのサイトの1:外積と法線ベクトルを用いる方法を参考にさせていただきました。

p(x-x_0)+q(y-y_0)+r(z-z_0)=0\\
⇔px+qy+rz-px_0-qy_0-rz_0=0

という変形のもとに実装しました。法線ベクトルの値は、すでにStartメソッドの中で計算済みです。

CalculateCoordinateOfIntersectionメソッド

このメソッドの実装はこちらのサイトを参考にさせていただきました。ここで、カメラの座標を表すベクトルを

(p_x, p_y, p_z)

カメラの視線の方向ベクトルを

(d_x, d_y, d_z)

とします。このときパラメータtを用いて(91)式のように書くとすると

x=d_xt+p_x\\
y=d_yt+p_y\\
z=d_zt+p_z

となります。このx, y, zの値をとる座標がカメラの視線と平面との交点になっていればよいので、平面の方程式を

ax+by+cz+d=0

としたとき

a(d_xt+p_x)+b(d_yt+p_y)+c(d_zt+p_z)+d=0\\
⇔(ad_x+bd_y+cd_z)t=-(ap_x+bp_y+cp_z+d)\\

よって左辺が0でなければ

t=-\frac{ap_x+bp_y+cp_z+d}{ad_x+bd_y+cd_z}

となります。あとはこのtの値をx, y, zに代入すればOKです。

CalculateOuterProductメソッド

このメソッドはVector3.Crossメソッドを用いて、vec1, vec2, vec3の3点を通る平面の法線ベクトルを求めています。

WhetherIntersectionIsInsidePolygonメソッド

このメソッドはこちらのサイトから、ほぼ完全に引用させていただきました。交点がメッシュの内側にあるのか外側にあるのかを判定します。

メインカメラのInspectorは下の図のようになっています。TPS Cameraというスクリプトは、カメラの位置や回転をXboxコントローラーで制御するためのスクリプトです。本記事では説明を割愛します。また、Find Coordinate Of Intersectionに入っているIntersectionCoordinateというオブジェクトは後ほど作ります。
main camera.png

次に、カメラとメッシュとの交点を作ります。本記事では交点が視覚的に分かりやすいように、交点の位置に球を配置することにします。球は紫色にし、IntersectionController.csというスクリプトをつけます。

IntersectionController.cs
using UnityEngine;

public class IntersectionController : MonoBehaviour
{
    private FindCoordinateOfIntersection f;
    public GameObject camera;

    private void Start() {
        f = camera.GetComponent<FindCoordinateOfIntersection>();
    }

    private void Update() {
        gameObject.transform.position = f.Intersection;
    }
}

IntersectionController.csの中では、球の位置を交点の位置に合わせ続けるように実装してあります。Main cameraにFindCoordinateController.csをアタッチしてあるので、Unityでcameraの欄にはMain Cameraを入れておきます。
intersection.png
次に、画面上に交点の座標を表示するためのテキストを作ります。
text.png
上の図のようにテキストを配置し、CoordinateというテキストをMain CameraにアタッチしたFindCoordinateController.csに入れておきます。

最後に、メッシュが空中に浮いていると頂点の前後関係などが視覚的に分かりづらいと思ったので、x,x,z軸をオブジェクトとして作ることにしました。円柱を細く長く引き伸ばし、軸ごとの色を付けて置いておきます。
axis.png

以上です。長い記事を読んでくださりありがとうございました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】音を鳴らそう

やりたいこと

 ・BGMをループ再生。
 ・SEを高頻度で再生。(一度止めてからもう一度再生?)

AudioClip と AudioSource

AudioClip
Unityで扱われる「音声データ」(ex. 音楽CD)
AudioSource
Unityで扱われる「AudioClipの再生状況等を管理するやつ」(ex.音楽プレイヤー)

リファレンス調査

 ・ループ再生はloop = trueにするだけで設定できる。楽だねぇ。

AudioSource audioSource;
audioSource = gameObject.AddComponent<AudioSource>();
audioSource.clip = Resources.Load("Sounds/1_3line") as AudioClip;
audioSource.loop = true;
audioSource.Play();

 ・SEの高頻度再生(シューティングのショットのような)は上記からloopを設定せずPlay()だけで可能。
  一度目の再生が完了するまで二度目の再生が待たされることはない。
  ただし、毎フレームだと音が再生されないため、
  最短でも2フレームに一度として再生する必要があるっぽい。

参考サイト

(AudioSource)[https://docs.unity3d.com/ja/2017.4/ScriptReference/AudioSource.html]

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む