20200314のUnityに関する記事は6件です。

DOTween完全に理解するその2 Transformアニメーション編

前回:DOTween完全に理解するその1 コントロールAPI編

今回解説するアニメーション

DOTweenではTransformやUI.Imageなどのオブジェクト毎に
Tweenを生成できる拡張メソッドを提供しています。
今回はその中からTransformに関するものをピックアップして解説していきます。

メソッド名 メモ
DOMove ワールド座標を指定して移動
DOLocalMove ローカル座標を指定して移動
DOMoveX/Y/Z 特定の軸上の座標へ移動
DOBlendableMoveBy 合成可能な移動
DORotate 各軸の回転量を指定して回転
DOBlendableRotateBy 合成可能な回転
DOLockat 特定の方向に注目するように回転
DOScale xyzのスケールを指定
DOScaleX/Y/Z 各軸のスケールを指定
DOBlendableScaleBy 合成可能なスケール

環境は引き続き以下の通りです
Unity:2019.3.0f6
DOTween:v1.2.335

移動

transform.DOMoveX(9, 1);                                   //position.x=9まで移動
transform.DOMove(new Vector3(9,0,0), 1);                   //(9,0,0)まで移動
transform.DOMove(new Vector3(9,0,0), 1).SetRelative(true); //現在の位置から(9,0,0)だけ移動
transform.DOBlendableMove(new Vector3(9,0,0), 1);          //(9,0,0)まで移動(合成可能)
transform.DOLocalMove(new Vector3(9,0,0), 1);              //ローカル座標で(9,0,0)まで移動

1Move.gif

SetRelative()はDOMoveだけの特殊なものではなく大体のTweenに効果があります。
DOMoveの場合trueにすると「現在座標からの相対座標」に移動します

ただ、DOTweenには「同じTweenを同時に実行できないシチュエーション」があります。
以下のgifを見てみてください。
DOTweenを使用してジグザグ移動するアニメーションを作りました。
2movezig.gif
一番上のDOMoveを2つ使用したSphereは一直線に移動しています。
DOMoveが1つのGameObjectに対して複数ある場合、
「先に実行されるTweenが後に実行されるTweenで上書きされてしまう」のです。
これはDOMoveが内部的にtransform.Position = 移動後の座標をしているためです。

複数の軸に対して変更を行いたい場合は以下のいずれかで実装可能です。
1. DOMoveX/Y/Zでそれぞれの軸に対して移動を行う
2. 親子関係のGameObjectそれぞれにDOMoveとDOLocalMoveを使用する
3. DOBlendableMoveByを使用する

またDOBlendableMoveByは相対座標への移動になります
以下は最初のgifの移動完了時の画像です。
1MoveKaisetu.png
SetRelatable(true)したものとDOBlendableMoveを使用したものが同じ座標で停止しています。
ただこれで勘違いしやすいのは私だけかもしれませんが
SetRelative(true)は相対座標へ移動する設定であり、あくまで移動方法はDOMoveである
ということです。(3敗)
複数の移動を同時に1つのGameObjectに対して行う場合はDOBlendableMoveを使用しましょう。
DOBlendableMoveByは内部的にtransform.position += 移動ベクトルです。

回転

transform.DORotate(new Vector3(0,0,360), 1, RotateMode.Fast);          //最短で指定の角度まで回転(360度を超えない)
transform.DORotate(new Vector3(0,0,360), 1, RotateMode.FastBeyond360); //最短で指定の角度まで回転(360度を超える)
transform.DORotate(new Vector3(0,0,360), 1, RotateMode.WorldAxisAdd);  //ワールド軸に対して
transform.DORotate(new Vector3(0,0,360), 1, RotateMode.LocalAxisAdd);  //ローカル軸に対して
transform.BlendableRotateBy(new Vector3(0,0,360), 1 RotateMode.WorldAxisAdd); //合成可能な回転(相対的)
transform.Lookat(Vector3.forward, 1, AxisConstraint.None, Vector3.up); //指定したべjクトルを向くように回転する

3Rotate.gif
回転に関しても移動の時と同様で、
複数の回転を同一GameObjectに行う場合はBlendableRotateByを使用しましょう。

LookatはQuaternion.LookRotationと同様ですが、AxisConstraintを使用することで
回転軸を特定の軸のみに制限をかけることができます。(AxisConstraint.X | AxisConstraint.Yなどで複数軸指定できます)

拡縮

transform.Scale(new Vector3(2,2,2), 1);              //2倍スケール
transform.ScaleX(4, 1);                              //X軸だけ4倍スケール
transform.BlendableScaleBy(new Vector3(3, 0, 0), 1); //合成可能スケール(相対的)

4Scale.gif
スケールに関しても他Tweenと同様ですね。
特に面白みもないのでEaseを設定してみましたが、これだけでも楽しげな動きをしてくれます。

まとめ

今回はTransformに関するTweenのみを紹介でした。
これらを組み合わせるだけでも、かなり気持ちいいそれっぽい動きをつけることができます。
その内最近話題のブログを見つつそれっぽい動きを作るネタみたいな記事を書こうかなと考えています。

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

Unityで子オブジェクトを全部削除する

下手に検索しても出てこないので自分用にメモ。
なおこれはEditor用

GameObject o;//削除対象の親オブジェクト
while(0 < o.transform.childCount)
{
  DestroyImmediate(o.transform.GetChild(0).gameObject) ;

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

Unity Student Plan の登録方法

Unity Student Plan とは

Unityはいまや最もメジャーなゲーム開発環境の一つ。
今年の2月25日、なんとそのUnityから学生向けのStudent Planの提供が始まりました!
image.png

 実際にプロが使用しているツールやワークフローなどを利用できるとしており、最大5人でグループプロジェクトが行なえるクラウドベースの「Collaboration」、クラウドサーバー上でプロジェクトのビルドが行なえる「Cloud Build」、3DやVR開発などの知識を学べる「Learn Premium」、開発用アセット「Snaps Prototype Pack」が利用できる「Student Asset Pack」、目への負担を抑える「Dark UI Theme」などがプランに含まれている。
(出典記事: https://pc.watch.impress.co.jp/docs/news/1237291.html)

どうやら、18歳以上の学生ならProのライセンス無料で手に入れられるようになったみたいです。
素晴らしい。学生万歳!!! (^o^)
様々な機能が使えるのでワクワクしますね。個人的に、Unityを初めて触る私にとっては、学習用の “Learn Premium” は特に嬉しいコンテンツです!

事前準備

Student Planを利用するために必要なものがいくつかあります。
・Unityアカウント
 当然ではありますが、Unityのサービスなので、まだの方は登録しておきましょう!Planを聞かれたらPersonal(無料)で一旦入れておきます。

・Unity Hubのダウンロード
 Unityに登録出来たら、UnityHubをダウンロードしましょう。ライセンスの変更や追加もここで行います。右の方の「UnityHubをダウンロード」をクリック!Unity自体はHubの方から後でダウンロードできるので今はしなくても大丈夫です。(もちろんしても大丈夫です!)
 UnityHubのダウンロード
2020-03-13 (1).png

・GitHubアカウント
 Student Planは GitHub Student Developer Packと連携したサービスのため、GitHubのアカウントが必須です。
 GitHubアカウント作成

・学生証
 上記のGitHub Student Developer Packに申し込むにあたり、自分が学生であることを証明する必要があります。その確認作業に最大1か月程度待たされます。そのため一度PersonalでUnityに登録しておきました。せっかくダウンロードしたのに最大1か月間Unityを使えないことになってしまうからです。
 ちなみに私は2週間しないうちに認証メールが届きました!

学生ライセンスの取得方法

  1. 以下のUntiy Student PlanのページからSignUpする。Unity Student Plan
    この際、Untiyアカウントが無い場合、ご自身の画面に従って、もしくは事前準備からUnityアカウントを作成してください。

  2. GitHubアカウントと連携する。1同様、GitHubアカウントが無い場合はご自身の画面もしくは事前準備から作成しましょう。
    GitHubアカウントと連携.jpg

  3. 上手くSingUp出来れば以下のような画面に到達します。
    Signupした後①.jpg
    「What's next?」の①はUnityHub入れておけば取り敢えず大丈夫だと思いますので、次は②のGitHub Student Developer Packに進んでください。
    SDP.jpg

  4. GitHub Student Developer Packのプランを選びます。今回は個人利用の学生ということでIndividualsのStudentsを選択しましょう。
    individual.jpg2020-03-14.png

次に学生であることの認証を行います。学生証が鮮明に見えるよう写真を撮ってアップロードしましょう。
Qualification.jpg
完了すると、「結果を1か月以内にメールで送るね」というふうにメッセージが現れますので、おとなしく承認完了メールが来るまで待ちましょう。Submitted.jpg・・・
・・・・
・・・・・!
5. 認証メール届いた!
2020-03-14.pngさあいよいよUnity Student Planをゲットしましょう。再びUnity Studentのページを訪れ、Student PlanをActivate(有効化)しましょう!
「What's next?」の①は取り合えず置いといて、②のActivate your licenseを実行しましょう。
2020-03-14 (1).png

以下のようなライセンスキーを伝えるメールがUnityから届きますので、まずはメール記載のキーを控えてください。
image.png

6.UnityHubを開きます。
右上のアカウントアイコンをクリックして、ライセンスの管理をクリック。
2020-03-14 (4).png

新規ライセンスの認証をクリックし、PlusまたはProの項目にチェックを入れ、先ほど控えたライセンスキーを入力して下さい。
(おそらくStudent Planは“無料でProのライセンスをゲット出来る”というサービスみたいなので、チェック項目にStudent Planという欄は無いです。)
2020-03-14 (5).png
これで完了です!あとはUnityHub上で適当なVersionのUnityをダウンロードして、ゲーム開発を体験してみましょう!

おわりに

無事環境を整えられたでしょうか…?
もしもやり方が大幅に変わってしまっていたり、やり方が分からないという方は遠慮せずコメントください。
Unityを使ったことが無い!という学生の皆さんも、これを機にインストールしてみるのもいいかもしれませんね。

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

【Unity超初心者】NullReferenceExceptionというエラーについて知る【初見殺し】

はじめに

amazonで高評価だったのとタイトルが気になり「Unityで神になる本」という本を結構前に買いました。
書籍内に大砲みたいなのをつくれるチャプターがあるのですが、
内容に沿って進めてうまくいかなかったので
どこがおかしかったのかわかるような気づきの記事になるように残したいと思います。

できあがったもの

できあがってうれしかったのでYoutube動画でうpしました
こんなかんじで動きます
https://www.youtube.com/watch?v=mhkqT4d-S40

この書籍の注意点

2015/6/26に発売した本です。載ってる情報が若干古いです。
(紹介されているアセットストアの無料アセットが有料になってたりしています)

つまずいたスクリプト

「Destruction.cs」というスクリプトを作成しました。
「Destruction」は直訳すると「破壊」です。
大砲の玉をとばして車を破壊する処理についてのスクリプトなのでこのような命名になってます。

コメントは勉強不足な自分が読み返して何をしているのかわかるようにつけているので、慣れている人がソースを見れば処理の内容がわかるものについても記述されている状態だと思います。

・エラーメッセージ
NullReferenceException: Object reference not set to an instance of an object
Destruction.Start () (at Assets/Destruction.cs:35)

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

//P.228 玉がボログルマに着弾するとボログルマがだんだん壊れていくスクリプト

public class Destruction : MonoBehaviour
{
    // 玉がぶつかった回数を記憶する int hitCount
    int hitCount = 0;

    // Start後にオブジェクト自身のRigidbodyを参照して情報を格納するためのrb
    public Rigidbody rb;

    // GameObject型のDamageLevels、GameObjectのもの(ボログルマ3形態)のみ配列に入れる
    public GameObject[] DamageLevels;

    // 煙エフェクト Unity側でSmokeをドロップする
    public GameObject SmokePt;

    void Start()
    {
        // 自分のRigidbodyを格納して参照する
        rb = GetComponent<Rigidbody>();
        // 3段階で壊れるボログルマを配列で用意
        DamageLevels = new GameObject[3];
        // ダメージレベルが上がったら各ダメージレベルのオブジェクトを呼ぶくりかえし
        for(var n = 0; n < 3; n++)
        {
            // 1段階壊れたオブジェクトから後+1ずつ呼び出す、3段階目まで
            // 非アクティブ(ディアクティベート)状態の子オブジェクトも取得できるtransform.Find
            /*(子オブジェクトの参照先がみつからない、原因分からず)NullReferenceException: Object reference not set to an 
            instance of an object
            Destruction.Start () (at Assets/Destruction.cs:35)*/
            DamageLevels[n] = transform.Find("damage_level1" + (n + 1)).gameObject;
        }
    }

    // BulletBehaviour.csから送られてきたら反応するAdd_Damageメソッド
    void Add_Damage()
    {
        // 玉がぶつかるたびに煙のsmをinstantiate(インスタンス化する、シーン中に表示させる)
        GameObject sm = Instantiate(SmokePt, transform.position, transform.rotation);
        // smoke出現後はボログルマを親とする
        sm.transform.parent = transform;
        // hitcount2以上はreturn、玉からAdd_Damageが呼ばれるとメソッドの処理は終わり
        if (hitCount > 2)
            return;
        // ボログルマのダメージが0の状態はSmokeを非表示
        DamageLevels[hitCount].SetActive(false);

        // インクリメント hitCountという整数の値に1を足すこと
        hitCount++;

        // ボログルマのダメージ1以上の状態はSmokeを表示
        DamageLevels[hitCount].SetActive(true);
    }

}

ログに表示

Debug.Log(DamageLevels);

たぶんうまくいってるデバッグログ.JPG

画像のログを確認したところ3つ分のボログルマオブジェクトを認識しているっぽい

Unity側のヒエラルキー内

赤枠が対象の1段階壊れたボログルマです
最初はdamage_level1をシーン上に表示してある状態で、
それ以降の状態のオブジェクトはディアクティベート(シーン上から非表示状態)にしています。
つまったとき、ここのオブジェクト名称とスクリプト内35行目の「"damage_level1"」を何度も見返して
名前が間違ってないか見てました。
4章のヒエラルキー.JPG

本をよく読み返したら

for文の部分は同じような構文を繰り返す場合に、ソースコードの見通しがよくなるという意味での参考でした、
短縮しないでひとつずつ配列を用意しても同様のエラーが出るか下記のように修正して試したら
なぜかちゃんと動いちゃいました。

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

//P.228 玉がボログルマに着弾するとボログルマがだんだん壊れていくスクリプト

public class Destruction : MonoBehaviour
{
    // 玉がぶつかった回数を記憶する int hitCount
    int hitCount = 0;

    // Start後にオブジェクト自身のRigidbodyを参照して情報を格納するためのrb
    public Rigidbody rb;

    // GameObject型のDamageLevels、GameObjectのもののみ配列に入れる
    public GameObject[] DamageLevels;

    // 煙エフェクト Unity側でSmokeをドロップする
    public GameObject SmokePt;

    // ビルトイン配列を定義 配列数を指定して子オブジェクトをtransform.Findで探す、damage_levelのGameObject
    void Start()
    {
        rb = GetComponent<Rigidbody>();
        DamageLevels = new GameObject[3];
        DamageLevels[0] = transform.Find("damage_level1").gameObject;
        DamageLevels[1] = transform.Find("damage_level2").gameObject;
        DamageLevels[2] = transform.Find("damage_level3").gameObject;
    }

    /*
    // For文を使うともう少しすっきりする
    // (子オブジェクトの参照先がみつからない、原因分からず)NullReferenceException: Object reference not set to an instance of an object
       Destruction.Start () (at Assets/Destruction.cs:47)
    void Start()
    {
        // 自分のRigidbodyを格納して参照する
        rb = GetComponent<Rigidbody>();
        // 3段階で壊れるボログルマを配列で用意
        DamageLevels = new GameObject[3];
        // ダメージレベルが上がったら各ダメージレベルのオブジェクトを呼ぶくりかえし
        for(var n = 0; n < 3; n++)
        {
            // 1段階壊れたオブジェクトから後+1ずつ呼び出す、3段階目まで
            // 非アクティブ(ディアクティベート)状態の子オブジェクトも取得できるtransform.Find
            DamageLevels[n] = transform.Find("damage_level1" + (n + 1)).gameObject;
        }
    }
    */

    // BulletBehaviour.csから送られてきたら反応するAdd_Damageメソッド
    void Add_Damage()
    {
        // 玉がぶつかるたびに煙のsmをinstantiate(インスタンス化する、シーン中に表示させる)
        GameObject sm = Instantiate(SmokePt, transform.position, transform.rotation);
        // smoke出現後はボログルマを親とする
        sm.transform.parent = transform;
        // hitcount2以上はreturn、玉からAdd_Damageが呼ばれるとメソッドの処理は終わり
        if (hitCount > 2)
            return;
        // ボログルマのダメージが0の状態はSmokeを非表示
        DamageLevels[hitCount].SetActive(false);

        // インクリメント hitCountという整数の値に1を足すこと
        hitCount++;

        // ボログルマのダメージ1以上の状態はSmokeを表示
        DamageLevels[hitCount].SetActive(true);
    }

}

ググる

過去に同じ書籍を進めて質問していたであろう人の質問とそれに対しての回答や、記事を読みました。

情報をざっくり整理すると
・Transform.Findで取得できるのは、アクティブ・非アクティブに関わらず子オブジェクトのみ

つまり、やろうとしていることは間違ってないことがわかりました。
書籍の参考がおかしいことになってるなと思いStartメソッドをを下記のように改修したら
ちゃんと動くようになりました。

Destruction.cs
    void Start()
    {
        // 自分のRigidbodyを格納して参照する
        rb = GetComponent<Rigidbody>();
        // 3段階で壊れるボログルマを配列で用意
        DamageLevels = new GameObject[3];
        // ダメージレベルが上がったら各ダメージレベルのオブジェクトを呼ぶくりかえし
        for (var n = 0; n < 3; n++)
        {
            //アクティブなオブジェクト"damaged_transporter_01"をまず見つけ出し、その子オブジェクトを探索して"damage_level1"を見つける
            DamageLevels[n] = GameObject.Find("damaged_transporter_01").transform.Find("damage_level1").gameObject;
        }
    }

おわりに

去年の11月に購入した書籍なんですが、今回のように詰まってばかりなため
いつ読み終えることができるか心配なところです。
ただじっくり進めているおかげか今のところ挫折せずに楽しみながら進められています。

この記事を書きながら親オブジェクトを参照し子オブジェクトを探索するというやり方に気づけたので、
気づきの記事にするという目標は達成できました。

これにゲームの要素を取り入れるとしたら玉の段数を10発までにして
90秒の間にボログルマを何台破壊できるかとかでしょうか、

これなら1ダウンロード10円くらいで販売できそうですかね

参考サイト

▼Find関数について
http://kimama-up.net/unity-find/

▼Find系関数で注意すべきこと4つ
http://mediamonster.blog.fc2.com/blog-entry-7.html

▼transform.Findでオブジェクトを取得できない
https://teratail.com/questions/53384

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

Houdini Engine for Unity でインスタンスの回転を制御する

rotate_07.jpg

はじめに

前回「Houdini Engine for Unity でインスタンスを配置する」という記事を書きました。
この記事では題名のとおり、Houdini Engine for Unityを使ってUnityの中でプロシージャルにオブジェクトのインスタンスを配置する方法をご紹介しています。記事に載っている手順と講演動画、サンプルプロジェクトを見て頂けば比較的簡単に同じことが出来るようになると思います。

ただ、この手法でインスタンス配置が出来るようになった後、多くの方が同じ悩みに突き当たるのではと予想しています。それは「配置したインスタンスに望んだ向き(回転)を設定するにはどうしたらよいのか」です。実はHoudini Engineを使ったインスタンス配置は、配置とスケール設定はとても簡単に出来るのですが、回転だけは少し難易度が高く、先日の講演でも意図的に触れるのを避けていました。
その方法を今回の記事で書きたいと思います。

サンプルプロジェクトについて

今回も配布用のサンプルプロジェクトを作成しました。
Houdini Engine Instance Orient Sample.zip
※サンプルはUnity 2019.3.0f3、Houdini Core 18.0.348で作成しています
※動作にはHoudini CoreまたはFXのライセンスが必要になります

サンプルは以下4種類の方法でインスタンスをオブジェクトの表面に配置しています。
・ランダムに回転させて配置
・XYZいずれかの一軸で回転させて配置
・オブジェクトのY軸を配置する地面の傾きに合わせ、その軸で回転させて配置
・オブジェクトのZ軸を配置する壁面の傾きに合わせ、その軸で回転させて配置

rotate_10.jpg
PlaneとSphereのそれぞれに配置したものがシーンに置かれていますので、アクティブ/非アクティブを切り替えて結果をご確認ください。またこのサンプルのHDAはTerrainではないメッシュオブジェクトへの配置を行うものですので、Terrain上に配置を行いたい場合はHDAをHoudiniで編集する必要があります。

では、本題に入ります。

Houdini Engine の回転制御はQuaternion

まず、なぜHoudini Engine for Unityでの回転制御が難しいかという理由についてです。答えは見出しに書いたとおりなのですが、回転制御のために設定するアトリビュート@orientQuaternion(四元数)だからです。Quaternionはアーティストが慣れ親しんだ3軸の回転ではなく名前のとおり4つの数字で回転を表現します。そしてこの数字はとても非直感的で、適当な数字を入れてもまず思ったような結果が得られません。

正しい道程としてはQuaternionを理解するところから始めるのでしょうが、(私もそれほどわかっていないので)今回は難しい話は置いてアーティストのための実務的な記事にしようと思います。
Quaternionを理解したい方にはとてもお勧めの動画があるので、最後に紹介します。
結論から書くと、Quaternionはいくつかの別の数値があればHoudiniで簡単に作る事が出来ます。

HoudiniでQuaternionを作る

HoudiniでQuaternionを作る場合はVEXが便利です。VOPでも作れるかもしれませんが、私はまだ試したことがありません。
Quaternionは以下のどちらからの情報があれば作る事ができます。
・1つのベクトル(回転軸)と回転角
・2つのベクトル(UpとNormal)

それぞれ、以下のようにquaternion関数を使う事で値を得る事ができます。

vector axis = {0,0,1}; //回転軸ベクトル
float angle = 360;     //回転角(度)
//Quaternionの作成
vector4 @orient;
@orient = quaternion(radians(angle),axis);

※quaternion関数の回転角は度数ではなくラジアンなのでradians()でその変換を行っています。

vector normal = {0,0,1}; //Normalベクトル
vector up = {0,1,0};     //Upベクトル
//Quaternionの作成
vector4 @orient;
@orient = quaternion(maketransform(normal,up));

※こちらも実際に入力するのは3×3の回転行列なのでmaketransform()を使っています。

このサンプルコードはいずれも下の画像のように、HoudiniでScatterノードなどを使いポイントを生成した後にAttribute Wrangleノードを接続し、ノード内のVEXpressionに記載するという想定です。画像では通常のScatterノードを使っていますが、Terrain上に配置する場合はHeightField Scatterノードを使います
rotate_01.jpg
2種類の作成方法を書きましたが、アーティストから見て直感的なのは後者、2つのベクトルから作成する方法だと思います。回転軸と角度は一見シンプルに見えますが、理解するのに少しコツが要る感じです。
しかし基本的にはこれだけで、Unityで配置されるインスタンスの回転をコントロールする事ができます。

具体的な回転制御のサンプル

回転の制御方法については以上なのですが、せっかくなのでより具体的なシチュエーションを想定したサンプルを作成してみました。以下4種類の手法で多くのパターンは網羅できるのではと思っています。また今回のサンプルでは全て個別にランダム回転させていますが、ここを任意の角度に変える事は難しくないと思います。

少し地味ですが、今回Unityでは下のようなオブジェクトのインスタンスを配置しました。Y方向に背が高く、先端がZ方向を向いている形状です。これで概ねインスタンスがどの方向を向いているかがわかると思います。
rotate_00.jpg
それでは順番にサンプルコードと結果のスクリーンショットを載せます。

ランダムに回転させて配置

//Unityインスタンスアトリビュートを生成
string @unity_instance = "Assets/Prefab/Test Object.prefab";

//Z方向のランダムなベクトル
vector normal;
normal = rand(@ptnum +10);
normal = normalize((normal *2) -1);

//Y方向のランダムなベクトル
vector up;
up = rand(@ptnum);
up = normalize((up *2) -1);

//2つのベクトルからクォータニオンを生成
vector4 @orient;
@orient = quaternion(maketransform(normal,up));

rotate_02.jpg
rotate_03.jpg

まずは全方向にランダムな回転を加えた配置です。サンプルのような棒状のオブジェクトではあまり使いませんが、岩の配置などにはよく使われる方法です。

XYZいずれかの一軸で回転させて配置

//Unityインスタンスアトリビュートを生成
string @unity_instance = "Assets/Prefab/Test Object.prefab";

//回転軸のベクトル
vector axis = {0,1,0}; //Y軸の場合
//vector axis = {1,0,0}; //X軸の場合
//vector axis = {0,0,1}; //Z軸の場合

//ランダムな回転(度)
float angle = rand(@ptnum) *360;

//設定した軸で回転
vector4 @orient;
@orient = quaternion(radians(angle),axis);

rotate_04.jpg
rotate_05.jpg
配置する場所の面の向きに関わらず、任意の軸を作り、その軸に対してランダムな回転をかける配置です。サンプルではY方向を軸としていますが、樹木や建物の配置などにはよくこの方法を使います。また壁面にディティール配置する際などにはこの軸をZ方向に変えたものを使う事もあります。

オブジェクトのY軸を配置する地面の傾きに合わせ、その軸で回転させて配置

//Unityインスタンスアトリビュートを生成
string @unity_instance = "Assets/Prefab/Test Object.prefab";

//ランダムな回転(度)
float angle = rand(@ptnum) *360;

//配置場所の法線をY軸に指定し、最初の回転
vector up = @N;
vector normal = cross(up,{0,1,0});
vector4 firstrot = quaternion(maketransform(normal,up));

//最初の回転に、ランダムの回転を追加
vector4 extrarot = quaternion(radians(angle),{0,1,0});
vector4 @orient;
@orient = qmultiply(firstrot, extrarot);

rotate_06.jpg
rotate_07.jpg
先ほどと似ていますが、これはインスタンスのY方向を配置位置の法線に合わせた上で、それを軸として回転を掛けるというサンプルです。球体に配置した例がわかりやすいと思います。この方法を使うと地形の傾きに合わせてインスタンスが配置出来るので、草や茂み、あるいは瓦礫のような背が低く設置面が広いオブジェクトを配置する際によく使われます。

オブジェクトのZ軸を配置する壁面の傾きに合わせ、その軸で回転させて配置

//Unityインスタンスアトリビュートを生成
string @unity_instance = "Assets/Prefab/Test Object.prefab";

//ランダムな回転(度)
float angle = rand(@ptnum) *360;

//配置場所の法線をZ軸に指定し、最初の回転
vector normal = @N;
vector up = cross(normal,{1,0,0});
vector4 firstrot = quaternion(maketransform(normal,up));

//最初の回転に、ランダムの回転を追加
vector4 extrarot = quaternion(radians(angle),{0,0,1});
vector4 @orient;
@orient = qmultiply(firstrot, extrarot);

rotate_08.jpg
rotate_09.jpg
これは先ほどのバリエーションのようなもので、オブジェクトの背を設置面の合わせるような形で、回転を加えています。これも崖や山肌など、壁面に対してインスタンスを設置したい場合によく使われる手法です。

サンプルは以上です。

最後に

Houdini Engine for Unityでのインスタンスの回転制御はVEXが中心でアーティストには少し難しく感じられるかもしれませんが、出来るだけそのままコピペできるようにコードを書いてみたのでぜひお試しください。Houdini Engine for Unityによる配置で回転制御が出来ると、通常考えうるインスタンスの配置パターンはほぼ実現できるのではないかと思います。

また実務面だけでなくもっとQuaternionを理解したいと思われた方には以下の動画をお勧めします。私もこれまで何度か観ていて、観るたびに少しずつ理解が進んでいるような気がしてます。

【Unity道場 大阪スペシャル in モリサワ 2017】クォータニオン完全マスター

プロシージャルなオブジェクト配置がこれまで以上に普及すればいいなと思っています。
ぜひ皆さんの制作にもお役立てください。

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

UnityShader Shadowの記述備忘録

影の書き込みに必要な記述を追う

Shadowのバッファーへの書き込みと、影を受ける側の処理を把握する為にビルトインシェーダーの記述を追ってみる。2019.3系のビルトインシェーダーを参考にします。
ひとまず記述が簡潔そうなVertexLitの影の書き込みを見てみる。

Mobile-VertexLit.shader
    // Pass to render object as a shadow caster
    Pass
    {
        Name "ShadowCaster"
        Tags { "LightMode" = "ShadowCaster" }

        ZWrite On ZTest LEqual Cull Off

        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag
        #pragma target 2.0
        #pragma multi_compile_shadowcaster
        #include "UnityCG.cginc"

        struct v2f {
            V2F_SHADOW_CASTER;
            UNITY_VERTEX_OUTPUT_STEREO
        };

        v2f vert( appdata_base v )
        {
            v2f o;
            UNITY_SETUP_INSTANCE_ID(v);
            UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
            TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
            return o;
        }

        float4 frag( v2f i ) : SV_Target
        {
            SHADOW_CASTER_FRAGMENT(i)
        }
        ENDCG
    }

適当に整理して重要な部分だけ抜粋するとこう。

        Tags { "LightMode" = "ShadowCaster" }
        ZWrite On ZTest LEqual Cull Off

        #pragma vertex vert
        #pragma fragment frag
        #pragma multi_compile_shadowcaster
        #include "UnityCG.cginc"

        struct v2f {
            V2F_SHADOW_CASTER;
        };

        v2f vert( appdata_base v )
        {
            v2f o;
            TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
            return o;
        }

        float4 frag( v2f i ) : SV_Target
        {
            SHADOW_CASTER_FRAGMENT(i)
        }

把握するべきはV2F_SHADOW_CASTER;TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)SHADOW_CASTER_FRAGMENT(i)っぽい。
一応考えやすい様に併記しておくとappdata_baseはこう定義されている。

UnityCG.cginc
struct appdata_base {
    float4 vertex : POSITION;
    float3 normal : NORMAL;
    float4 texcoord : TEXCOORD0;
    UNITY_VERTEX_INPUT_INSTANCE_ID
};

UNITY_VERTEX_INPUT_INSTANCE_IDはGPUInstancing対応の為の記述なので、頂点座標,法線,UV座標を持っているだけの構造体と思っておけば良い。

前述の3つの記述を詳しく追ってみる。

V2F_SHADOW_CASTER;

結論

展開するとこうなる。

struct v2f {
    //V2F_SHADOW_CASTER;
    float4 pos : SV_POSITION
    float3 vec : TEXCOORD0; //これはグラフィックAPIとSHADOWS_CUBEキーワード次第
};

マクロを読む

こんなマクロになっている。

UnityCG.cginc
// Declare all data needed for shadow caster pass output (any shadow directions/depths/distances as needed),
// plus clip space position.
#define V2F_SHADOW_CASTER V2F_SHADOW_CASTER_NOPOS UNITY_POSITION(pos)

V2F_SHADOW_CASTER_NOPOSUNITY_POSITION(pos)をそれぞれ確認してみる。
まず簡単なUNITY_POSITION(pos)。これはときどき見かける。

HLSLSupport.cginc
// On D3D reading screen space coordinates from fragment shader requires SM3.0
#define UNITY_POSITION(pos) float4 pos : SV_POSITION

単純にクリップ座標を入れる変数を宣言しているだけ。
次にV2F_SHADOW_CASTER_NOPOS

UnityCG.cginc
#if defined(SHADOWS_CUBE) && !defined(SHADOWS_CUBE_IN_DEPTH_TEX)
    // Rendering into point light (cubemap) shadows
    #define V2F_SHADOW_CASTER_NOPOS float3 vec : TEXCOORD0;
    //...省略...
#else
    //...省略...
#endif

コメントを読むとポイントライトのシャドウを表現する場合はこれが構造体に追加される場合があるようですね。
SHADOW_CUBEはCubemapでシャドウマップを表現する時に有効になるkeywordのよう?cginc内で定義されていないようなので、実行時にCPU側からEnableされていそう。
SHADOWS_CUBE_IN_DEPTH_TEXは以下のように定義されているのでプラットフォームが"depth-format cube shadow map"に対応していない場合はにfloat3 vec : TEXCOORD0;が構造体に追加されるという事になりそう。

UnityCG.cginc
#if defined(SHADER_API_D3D11) || defined(SHADER_API_PSSL) || defined(SHADER_API_METAL) || defined(SHADER_API_GLCORE) || defined(SHADER_API_GLES3) || defined(SHADER_API_VULKAN) || defined(SHADER_API_SWITCH) // D3D11, D3D12, XB1, PS4, iOS, macOS, tvOS, glcore, gles3, webgl2.0, Switch
// Real-support for depth-format cube shadow map.
#define SHADOWS_CUBE_IN_DEPTH_TEX
#endif

とはいえ結構一般的なグラフィックAPIがそろっているようなので、ほとんどの場合はfloat3 vec : TEXCOORD0;は構造体に追加されないと思っていても良さそう。こればかりは開発対象次第ですね。
ひとまず、V2F_SHADOW_CASTER;マクロを展開してみるとこうなりますね。

struct v2f {
    //V2F_SHADOW_CASTER;
    float4 pos : SV_POSITION
    float3 vec : TEXCOORD0; //これはグラフィックAPIとSHADOWS_CUBEキーワード次第
};

TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)

結論

こう展開される

        v2f vert( appdata_base v )
        {
            v2f o;
            //TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
            #if defined(SHADOWS_CUBE) && !defined(SHADOWS_CUBE_IN_DEPTH_TEX)
                o.vec = mul(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz; opos = UnityObjectToClipPos(v.vertex);
            #else
                o.pos = UnityClipSpaceShadowCasterPos(v.vertex, v.normal); 
                o.pos = UnityApplyLinearShadowBias(o.pos);
            #endif
            return o;
        }

マクロを読む

マクロはこうなっている

UnityCG.cginc
// Vertex shader part, with support for normal offset shadows. Requires
// position and normal to be present in the vertex input.
#define TRANSFER_SHADOW_CASTER_NORMALOFFSET(o) TRANSFER_SHADOW_CASTER_NOPOS(o,o.pos)

TRANSFER_SHADOW_CASTER_NOPOSを確認してみます。

UnityCG.cginc
#if defined(SHADOWS_CUBE) && !defined(SHADOWS_CUBE_IN_DEPTH_TEX)
    //省略
    #define TRANSFER_SHADOW_CASTER_NOPOS(o,opos) o.vec = mul(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz; opos = UnityObjectToClipPos(v.vertex);
    //省略

#else
    //省略
    #define TRANSFER_SHADOW_CASTER_NOPOS(o,opos) \
        opos = UnityClipSpaceShadowCasterPos(v.vertex, v.normal); \
        opos = UnityApplyLinearShadowBias(opos);
    //省略
#endif

前述の分岐がここでも出てきます。
前者の分岐先では光源から頂点に向くベクトル(ワールド座標系)と、頂点をUnityObjectToClipPos()したクリップ座標を計算していますね。後者の分岐先は、頂点座標と法線を入力にUnityClipSpaceShadowCasterPos()UnityApplyLinearShadowBias()でなにやら計算してますね。このふたつの関数を追ってみます。

UnityCG.cginc
float4 UnityClipSpaceShadowCasterPos(float4 vertex, float3 normal)
{
    float4 wPos = mul(unity_ObjectToWorld, vertex);

    if (unity_LightShadowBias.z != 0.0)
    {
        float3 wNormal = UnityObjectToWorldNormal(normal);
        float3 wLight = normalize(UnityWorldSpaceLightDir(wPos.xyz));

        // apply normal offset bias (inset position along the normal)
        // bias needs to be scaled by sine between normal and light direction
        // (http://the-witness.net/news/2013/09/shadow-mapping-summary-part-1/)
        //
        // unity_LightShadowBias.z contains user-specified normal offset amount
        // scaled by world space texel size.

        float shadowCos = dot(wNormal, wLight);
        float shadowSine = sqrt(1-shadowCos*shadowCos);
        float normalBias = unity_LightShadowBias.z * shadowSine;

        wPos.xyz -= wNormal * normalBias;
    }

    return mul(UNITY_MATRIX_VP, wPos);
}

頂点座標、法線をワールド座標に変換して頂点を法線方向に幾らか動かしてからクリップ座標に変換していますね。幾らかはLightコンポーネントで設定するBias依存っぽい。

UnityApplyLinearShadowBias()も確認します。前述のコードからUnityClipSpaceShadowCasterPos()で出力された法線方向にオフセットされたクリップ座標が引数にくることが分かっています。

UnityCG.cginc
float4 UnityApplyLinearShadowBias(float4 clipPos)

{
    // For point lights that support depth cube map, the bias is applied in the fragment shader sampling the shadow map.
    // This is because the legacy behaviour for point light shadow map cannot be implemented by offseting the vertex position
    // in the vertex shader generating the shadow map.
#if !(defined(SHADOWS_CUBE) && defined(SHADOWS_CUBE_IN_DEPTH_TEX))
    #if defined(UNITY_REVERSED_Z)
        // We use max/min instead of clamp to ensure proper handling of the rare case
        // where both numerator and denominator are zero and the fraction becomes NaN.
        clipPos.z += max(-1, min(unity_LightShadowBias.x / clipPos.w, 0));
    #else
        clipPos.z += saturate(unity_LightShadowBias.x/clipPos.w);
    #endif
#endif

#if defined(UNITY_REVERSED_Z)
    float clamped = min(clipPos.z, clipPos.w*UNITY_NEAR_CLIP_VALUE);
#else
    float clamped = max(clipPos.z, clipPos.w*UNITY_NEAR_CLIP_VALUE);
#endif
    clipPos.z = lerp(clipPos.z, clamped, unity_LightShadowBias.y);
    return clipPos;
}

とりあえずグラフィックAPI差を吸収して深度値の元になる値(clipPos.z)を調整しているっぽい。

なので、展開の結果は以下になる。特別な処理の内容は以下の2点。

  • 法線方向に頂点がバイアスでオフセットされる
  • 深度がバイアスで調整される
        v2f vert( appdata_base v )
        {
            v2f o;
            //TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
            #if defined(SHADOWS_CUBE) && !defined(SHADOWS_CUBE_IN_DEPTH_TEX)
                o.vec = mul(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz; opos = UnityObjectToClipPos(v.vertex);
            #else
                o.pos = UnityClipSpaceShadowCasterPos(v.vertex, v.normal); 
                o.pos = UnityApplyLinearShadowBias(o.pos);
            #endif
            return o;
        }

SHADOW_CASTER_FRAGMENT(i)

結論

こんな感じに展開される。(少し書きかえました)

        float4 frag( v2f i ) : SV_Target
        {
            #if defined(SHADOWS_CUBE) && !defined(SHADOWS_CUBE_IN_DEPTH_TEX)
              //省略
              float depth = length(i.vec) + unity_LightShadowBias.x) * _LightPositionRange.w;
              #ifdef UNITY_USE_RGBA_FOR_POINT_SHADOWS
                return EncodeFloatRGBA (min(depth, 0.999));
              #else
                return depth;
              #endif
            #else
              return 0;
            #endif
        }

マクロを追う

振り返っておくと、SHADOW_CASTER_FRAGMENT(i)はフラグメントシェーダーで実行されているのでこのマクロが最終的な深度値にあたる値を出力していそうな予想はできる。

UnityCG.cginc
#if defined(SHADOWS_CUBE) && !defined(SHADOWS_CUBE_IN_DEPTH_TEX)
    //省略
    #define SHADOW_CASTER_FRAGMENT(i) return UnityEncodeCubeShadowDepth ((length(i.vec) + unity_LightShadowBias.x) * _LightPositionRange.w);
#else
    //省略
    #define SHADOW_CASTER_FRAGMENT(i) return 0;
#endif

後者の分岐先はreturn 0;なので特に何もしていなさそう。これは多分SV_Depthの値をシャドウマップに使用するからSV_Targetがどんな値を出力しても関係ないということなんだと思う。前者の分岐先においてだけSV_Targetが必要。

(length(i.vec) + unity_LightShadowBias.x) * _LightPositionRange.w

の部分ついて考える。
i.vecは光源から頂点に向かうベクトルだったのでlength(i.vec)で光源からの距離を計算して、* _LightPositionRange.wで距離を0~1に正規化しているのかな。+ unity_LightShadowBias.xは距離にバイアスを持たせているだけっぽい。この計算で0~1に収まる深度が計算されているっぽいですね。
UnityEncodeCubeShadowDepth()はfloat精度の0~1の値を必要に応じて8bitRGBAにパックしてる。"必要に応じて"というのはfloat精度のレンダリングターゲットをサポートしているか否か(UNITY_USE_RGBA_FOR_POINT_SHADOWSキーワード)で分岐されていて、defined(SHADER_API_GLES) || defined(SHADER_API_GLES3)ならRGBAにfloatを分割して保存している。
なので以下のように展開される。

        float4 frag( v2f i ) : SV_Target
        {
            #if defined(SHADOWS_CUBE) && !defined(SHADOWS_CUBE_IN_DEPTH_TEX)
                //省略
                float depth = length(i.vec) + unity_LightShadowBias.x) * _LightPositionRange.w;
                #ifdef UNITY_USE_RGBA_FOR_POINT_SHADOWS
                    return EncodeFloatRGBA (min(depth, 0.999));
                #else
                    return depth;
                #endif
            #else
                return 0;
            #endif
        }

全体を展開してみる

長くなるので静的分岐ごとに分けて2パターンに展開します。
#if defined(SHADOWS_CUBE) && !defined(SHADOWS_CUBE_IN_DEPTH_TEX)な場合は以下のように展開されるはず。

        Tags { "LightMode" = "ShadowCaster" }
        ZWrite On ZTest LEqual Cull Off

        #pragma vertex vert
        #pragma fragment frag
        #pragma multi_compile_shadowcaster
        #include "UnityCG.cginc"

        struct v2f {
            float4 pos : SV_POSITION;
            float3 vec : TEXCOORD0;
        };

        v2f vert( appdata_base v )
        {
            v2f o;
            o.vec = mul(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz;
            o.pos = UnityObjectToClipPos(v.vertex);
            return o;
        }

        float4 frag( v2f i ) : SV_Target
        {
            float depth = length(i.vec) + unity_LightShadowBias.x) * _LightPositionRange.w;

            //defined(SHADER_API_GLES) || defined(SHADER_API_GLES3)の場合はEncodeFloatRGBAでエンコード
            #ifdef UNITY_USE_RGBA_FOR_POINT_SHADOWS
                return EncodeFloatRGBA (min(depth, 0.999));
            #else
                return depth;
            #endif
        }

次に、#if defined(SHADOWS_CUBE) && !defined(SHADOWS_CUBE_IN_DEPTH_TEX)ではない場合の展開結果。多分多くの場合はこっちが使われると思う。

        Tags { "LightMode" = "ShadowCaster" }
        ZWrite On ZTest LEqual Cull Off

        #pragma vertex vert
        #pragma fragment frag
        #pragma multi_compile_shadowcaster
        #include "UnityCG.cginc"

        struct v2f {
            float4 pos : SV_POSITION;
        };

        v2f vert( appdata_base v )
        {
            v2f o;
            //バイアス依存で法線方向にオフセット+バイアス依存で深度値を調整?
            o.pos = UnityClipSpaceShadowCasterPos(v.vertex, v.normal); 
            o.pos = UnityApplyLinearShadowBias(o.pos);
            return o;
        }

        float4 frag( v2f i ) : SV_Target
        {
            return 0;
        }

備考

SHADOWS_CUBEキーワードが有効になる条件だけは確かな情報が見当たらなかったのでわからないまま...
(少なくとも以下のAPIを使用している場合は気にしなくてよい)
#if defined(SHADER_API_D3D11) || defined(SHADER_API_PSSL) || defined(SHADER_API_METAL) || defined(SHADER_API_GLCORE) || defined(SHADER_API_GLES3) || defined(SHADER_API_VULKAN) || defined(SHADER_API_SWITCH) // D3D11, D3D12, XB1, PS4, iOS, macOS, tvOS, glcore, gles3, webgl2.0, Switch

と思ったら、以下の記述を見つけたのでpoint lightでrealtime shadowする時にEnableになるのかもしれない。

AutoLight.cginc
        //point realtime shadow
        #if defined (SHADOWS_CUBE)
       //省略

影を受ける為に必要な処理を追う

影を受ける側の処理は適切なサンプルになりそうなシェーダーがみつからなかったので負った結果のサンプルを以下に記載します。静的分岐が複雑なのでマクロを追うのがしんどいです。影を受ける側でトリッキーな事をする場面は少ないと思うので深く入り込まなくても良い気はするので深くは調べませんでした。

Shader "Unlit/ReceiveShadow"
{
  Properties
  {
    _MainTex ("Texture", 2D) = "white" { }
  }
  SubShader
  {
    Name "FORWARD"
    Tags { "RenderType" = "Opaque" "LightMode" = "ForwardBase" }

    Pass
    {
      CGPROGRAM

      #pragma vertex vert
      #pragma fragment frag
      #pragma multi_compile_fwdbase_fullshadows

      #include "UnityCG.cginc"
      #include "AutoLight.cginc"

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

      struct v2f
      {
        float2 uv: TEXCOORD0;
        float4 pos: SV_POSITION;
        float3 worldPos: WORLDPOS;
        UNITY_LIGHTING_COORDS(3, 4)
      };

      sampler2D _MainTex;

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

      fixed4 frag(v2f i): SV_Target
      {
        fixed4 col = tex2D(_MainTex, i.uv);
        UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
        col *= atten;
        return col;
      }
      ENDCG

    }

    // shadowを受けるのにもshadowcasterパスが必要
    // Pass to render object as a shadow caster
    Pass
    {
      Name "ShadowCaster"
      Tags { "LightMode" = "ShadowCaster" }

      ZWrite On ZTest LEqual Cull Off

      CGPROGRAM

      #pragma vertex vert
      #pragma fragment frag
      #pragma multi_compile_shadowcaster
      #include "UnityCG.cginc"

      struct v2f
      {
        float4 pos: SV_POSITION;
      };

      v2f vert(appdata_base v)
      {
        v2f o;
        //バイアス依存で法線方向にオフセット+バイアス依存で深度値を調整?
        o.pos = UnityClipSpaceShadowCasterPos(v.vertex, v.normal);
        o.pos = UnityApplyLinearShadowBias(o.pos);
        return o;
      }

      float4 frag(v2f i): SV_Target
      {
        return 0;
      }
      ENDCG

    }
  }
}

UNITY_LIGHTING_COORDS(n,m)

v2f構造体にシャドウマップをサンプリングする為のUV座標を含める必要があります。UNITY_LIGHTING_COORDS(n,m)マクロはその為のマクロで以下のように定義されている。

AutoLight.cginc
#define UNITY_LIGHTING_COORDS(idx1, idx2) DECLARE_LIGHT_COORDS(idx1) UNITY_SHADOW_COORDS(idx2)

2つのマクロに分かれていてそれぞれライトコードとシャドウコードを構造体内に宣言してる。
DECLARE_LIGHT_COORDS(idx1)UNITY_SHADOW_COORDS(idx2)も、分岐によって2or3or4次元のベクトルを宣言している。変数名は_LightCoord_ShadowCoord

UNITY_TRANSFER_LIGHTING(o, v.uv1);

頂点シェーダー内で_LightCoord_ShadowCoordにUV座標を計算し代入する処理がUNITY_TRANSFER_LIGHTING(o, v.uv1)マクロで提供されています。uv1と書いているのはStandardシェーダーないでそう書かれていたのを持ってきたからなんですが、lightmap uvを使いたいとかそういうことなのかな。あんまりその辺理解していないのでよく分からず...
考えやすい様にマクロで使用されている変数名は書き換えてますが、定義は以下のようになっています。

AutoLight.cginc
#define UNITY_TRANSFER_LIGHTING(o, coord) COMPUTE_LIGHT_COORDS(o) UNITY_TRANSFER_SHADOW(o, coord)

COMPUTE_LIGHT_COORDS(o)はライトの種類で分岐してる。が、基本的にライトをカメラとして考えてカメラから見たクリップ座標のような座標を算出している感じになっている。例えば以下のような感じ。厳密にどういう変換をしているかはunity_WorldToLight行列次第。

AutoLight.cginc
#define COMPUTE_LIGHT_COORDS(o) o._LightCoord = mul(unity_WorldToLight, mul(unity_ObjectToWorld, v.vertex)).xyz;

UNITY_TRANSFER_SHADOW(o, coord)は分岐パターンが多くて追うのが億劫なので中止。とりあえず、o._ShadowCoord変数に2ro3or4次元のベクトルが代入される。計算に使われる特別な変数はunity_WorldToShadow[0],_LightPositionRange.xyz,unity_LightmapST.xyzwくらい。

UNITY_LIGHT_ATTENUATION(atten, i, s.posWorld);

後はフラグメントシェーダーでシャドウマップを参照して影を付けるだけ。
UNITY_LIGHT_ATTENUATIONマクロも分岐が複雑なのでざっくりの理解で進める。例えばこんな分岐がある。
シャドウとかCoolieとかを含めて諸々処理した結果をfixed destName変数に代入している。
```

define UNITY_LIGHT_ATTENUATION(destName, input, worldPos) \

DECLARE_LIGHT_COORD(input, worldPos); \
fixed shadow = UNITY_SHADOW_ATTENUATION(input, worldPos); \
fixed destName = (lightCoord.z > 0) * UnitySpotCookie(lightCoord) * UnitySpotAttenuate(lightCoord.xyz) * shadow;
どの分岐でも`UNITY_SHADOW_ATTENUATION(input, worldPos);`は含まれていて、これは`UnityComputeForwardShadows()`関数を呼ぶか、`SHADOW_ATTENUATION(a)`マクロを実行してる。ほとんどの場合`UnityComputeForwardShadows()`関数が呼ばれている。

```AutoLight.cginc
half UnityComputeForwardShadows(float2 lightmapUV, float3 worldPos, float4 screenPos)
{
    //fade value
    float zDist = dot(_WorldSpaceCameraPos - worldPos, UNITY_MATRIX_V[2].xyz);
    float fadeDist = UnityComputeShadowFadeDistance(worldPos, zDist);
    half  realtimeToBakedShadowFade = UnityComputeShadowFade(fadeDist);

    //baked occlusion if any
    half shadowMaskAttenuation = UnitySampleBakedOcclusion(lightmapUV, worldPos);

    half realtimeShadowAttenuation = 1.0f;
    //directional realtime shadow
    #if defined (SHADOWS_SCREEN)
        #if defined(UNITY_NO_SCREENSPACE_SHADOWS) && !defined(UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS)
            realtimeShadowAttenuation = unitySampleShadow(mul(unity_WorldToShadow[0], unityShadowCoord4(worldPos, 1)));
        #else
            //Only reached when LIGHTMAP_ON is NOT defined (and thus we use interpolator for screenPos rather than lightmap UVs). See HANDLE_SHADOWS_BLENDING_IN_GI below.
            realtimeShadowAttenuation = unitySampleShadow(screenPos);
        #endif
    #endif

    #if defined(UNITY_FAST_COHERENT_DYNAMIC_BRANCHING) && defined(SHADOWS_SOFT) && !defined(LIGHTMAP_SHADOW_MIXING)
    //avoid expensive shadows fetches in the distance where coherency will be good
    UNITY_BRANCH
    if (realtimeToBakedShadowFade < (1.0f - 1e-2f))
    {
    #endif

        //spot realtime shadow
        #if (defined (SHADOWS_DEPTH) && defined (SPOT))
            #if !defined(UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS)
                unityShadowCoord4 spotShadowCoord = mul(unity_WorldToShadow[0], unityShadowCoord4(worldPos, 1));
            #else
                unityShadowCoord4 spotShadowCoord = screenPos;
            #endif
            realtimeShadowAttenuation = UnitySampleShadowmap(spotShadowCoord);
        #endif

        //point realtime shadow
        #if defined (SHADOWS_CUBE)
            realtimeShadowAttenuation = UnitySampleShadowmap(worldPos - _LightPositionRange.xyz);
        #endif

    #if defined(UNITY_FAST_COHERENT_DYNAMIC_BRANCHING) && defined(SHADOWS_SOFT) && !defined(LIGHTMAP_SHADOW_MIXING)
    }
    #endif

    return UnityMixRealtimeAndBakedShadows(realtimeShadowAttenuation, shadowMaskAttenuation, realtimeToBakedShadowFade);
}

return UnityMixRealtimeAndBakedShadows(...)とあるのでこの関数内でシャドウの処理をまとめて行っている。で、フラグメントシェーダーまで巻き戻るとUNITY_LIGHT_ATTENUATION(atten, i, s.posWorld);という記述の中で上記の関数が呼ばれなんやかんや影の処理が行われた結果がフラグメントシェーダー上でfixed atten;変数として宣言された状態になる。このatten変数をフラグメントシェーダーの結果に乗算すれば影が落ちたような状態になる。その後にEmissionを加算すればOK。長ぁ。
余談ですが、StandardシェーダーではFragmentGI()関数にattenを引数として渡していて中でグローバルイルミネーションのライトカラーにattenが乗算されるようになっている。

備考

変数名の制約

UNITY_LIGHT_ATTENUATIONが内部でSV_POSITIONセマンティクスをバインドした変数を使っています。そのコードでは変数名がposとされている前提で書かれているのでそれに合わせる必要があります。

もっと簡潔なReceive Shadow

Standardシェーダーの処理を追って調べていたのでUNITY_LIGHTING_COORDSマクロを使いましたが、ArktoonShaderのコードを追ってみたところ、以下のようにUNITY_LIGHTING_COORDSSHADOW_COORDSに、UNITY_TRANSFER_LIGHTINGTRANSFER_SHADOWにしても影を受けられるようです。

      struct v2f
      {
        float2 uv: TEXCOORD0;
        float4 pos: SV_POSITION;
        float3 worldPos: WORLDPOS;
        //UNITY_LIGHTING_COORDS(3, 4)
        SHADOW_COORDS(3)
      };

      sampler2D _MainTex;

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

たしかに、UNITY_LIGHTING_COORDS_LightCoord_ShadowCoordをまとめて扱っているからシャドウだけに絞ればそれでいいのか。なるほど:bow:

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