PodmanとBuildahに周回遅れで入門してみた
ども。nodamushiです。
さて、前々からプロジェクトのビルドにDockerを使うと、Root権限またはdockerグループに所属する必要があったり、デーモンが必要だったり、なんだかんだ結構不満がありました。
とはいえ、忙しいし、困ってはないということでずっとDockerでやってきてたんですが、いい加減 Podman や Buildah も使えない老害がデブい顔してるのはヤバイと思ってはいたのでちょっと勉強してきました。
Podman って何

Podman (Pod Manager) は ダグトリオかウミトリオの近縁種 Dockerと互換性のあるコンテナランタイムで、Linuxシステムでコンテナを操作・管理するためのツール。
Podmanは、Dockerの代替として開発されていて、特徴としては以下のような物があるらしい。
- デーモンレス: Podmanはデーモンプロセスを必要とせず、コンテナ操作はコマンドラインから直接行われる
- ルートレス: Podmanは、ルート権限を持たないユーザーでもコンテナを操作可能。Podmanが気になってたのはだいたいこれ。
- OCI準拠: Podmanは、Open Container Initiative(OCI)に準拠したコンテナイメージをサポートしており、Dockerイメージと互換性があるっていうか、コマンド体型からして大体Docker
あとはその名前からわかるように Pod (複数のコンテナの塊) を管理する機能もついてるみたいで、 Kubernetes を使ってる人とかには刺さるのかもしれない。今のところ私は Pod が必要な場面に出くわしたことがないので今回は無視。
ていうか、今更なんだけど Docker も数年前からルートレスで実行することができるようになってるらしい。
いや、まじで今日まで知らなかった。ホント、とんだ老害だよね………流石に反省。とはいえ、Rootless用のDockerを入れないといけないみたいなので、簡単には使えないのかな?
Ubuntu 20.04 にインストール
とりあえず、サクッと用意できる環境が Ubuntu 20.04 だったので、Ubuntu20.04 でやろうとしたんだけど、 apt install でサクッと入らなかった。どうにも Ubuntu 20.10 以上じゃないと登録されていないらしい。
ひとまずは、リポジトリのGPGキーをインポートし、リポジトリを追加すれば Ubuntu 20.04 でもインストールできるようだ。とはいえ、これ、公式のインストール方法に書いてない(消されたらしい)し、 Docker に比べてこの点はかなりマイナスかな。。
curl -L https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/xUbuntu_20.04/Release.key | sudo apt-key add - sudo apt-add-repository 'deb http://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/xUbuntu_20.04/ /'
そしたらあとは普通にインストール。Ubuntu 22.04 などは以下の手順からだけで行けた。
sudo apt update -qq sudo apt install -y podman
podman --version を実行してバージョン情報が表示されれば成功。
$ podman -v podman version 3.4.2
Buildah って何?

Buildah は、コンテナイメージを作成・変更・ビルドするためのツールで、どうにも Podman と同じ文脈でよく語られてるっぽいけど、なんぞこれ?
まずは読み方。 Buildah と書いて ビルダー と読むらしい……。ボストン訛のbuilder だそうですよ。 あのさぁ、 nginx (んぎっくす と発音する) とかもそうだけど読めない名前つけるのやめてもらっていいですか? なお、私の中では びるだっち になりました。
Buildahの説明を見るとDockerfileの代わりにスクリプトを使ってイメージを作成するものとか、デーモンレスですとか、非rootで実行できますとか、全然意味わからなかった。デーモンレスとか非rootがどうのだったら、前述の Podman でビルドできるんだよね。
あれこれ悩んで、最終的に個人的には以下の理解で納得しました。
| イメージ作成 | Java ビルド | 説明 |
|---|---|---|
| Dockerfile, Containerfile | Ant, Maven | 設定ファイルベースのビルド |
| Buildah | Gradle | スクリプトベースのビルド |
Java がわからない人には意味不明な対比ですまない。しかし、私はこれで納得してしまったんだ。Dockerfileだってスクリプトじゃないかって?いや、まぁ、そりゃそうだけどさ。
BuildahはShellスクリプトのif文やfor文でより柔軟性の高いイメージビルドが可能。一方で、Dockerfileは RUN とか COPY 自体をループすることはできないし、そもそもシンプルに書くことが求められている。そういう意味で、設定ファイルベースに近いと感じたのだ。
むろん、上記の対比は Buildah のすべてを表す対比ではないけど、極端に間違った対比でもないと思います。
Ubuntu 20.04 にインストール
Buildahのインストール方法は以下のコマンドを実行すればOKでした。 Podmanのインストール後だとこのコマンド一発で普通に入ったけど、Ubuntu 20.04の場合はGPGキー登録が必要かも。 その場合はさっきの Podman の GPGキー登録を先にやってください。
sudo apt install -y buildah
Ubuntu 20.04 の問題なのかどうなのかはよくわからないけど、私の環境では WSL でも Ubuntuマシンでも以下の警告が毎回出力されました。
WARN[0000] Failed to decode the keys ["machine"] from "/usr/share/containers/containers.conf".
よくわからないが、このイシュー によると、とりあえず /usr/share/containers/containers.conf の [machine] をコメントアウトすれば消える模様。とりあえず、私はコメントアウトしておいたけど、いいんかね?
コンテナイメージの作成と実行
Podmanを使ってコンテナイメージを取得・実行する方法と、Buildahで作成したイメージをPodmanで実行する方法をやってみた。
Podman を使ったイメージの取得と実行
Podmanはなんと普通に Dockerfile が扱える。びっくりだ。すごいね。
コマンド体型もほとんど Docker と同じ感覚で扱えるし、何なら docker コマンドを podman のエイリアスにしてしまう podman-docker なんてのもある。(私は入れないけど)
以下のrunを実行するとお馴染みの 「Hello from Docker!」が表示される。お馴染みのimage rm でイメージ削除できる。
podman run --rm hello-world podman image rm hello-world
ちなみに、WSLの環境だと以下のようなメッセージが表示されちゃった。
WARN[0000] "/" is not a shared mount, this could cause issues or missing mounts with rootless containers
これはなんだかよくわかんない。このイシューによると、sudo mount --make-rshared / やwsl.exe -u root -e mount --make-rshared / とすれば良いようだけど。
とりあえず、上記のコマンドを実行したあとに、マウントを試してみる。
$ mkdir hoge $ podman run --rm -it -v ./hoge:/hoge alpine / # ls bin etc home media opt root sbin sys usr dev hoge lib mnt proc run srv tmp var / # cd hoge /hoge # touch piyo.txt /hoge # exit $ ls -al hoge total 12 drwxr-xr-x 2 nodamushi nodamushi 4096 Apr 4 11:33 . drwxr-xr-x 3 nodamushi nodamushi 4096 Apr 4 11:32 .. -rw-r--r-- 1 nodamushi nodamushi 5 Apr 4 11:33 piyo.txt
普通にボリュームをマウントできて、ファイルも出力できてるっぽいですね。(mountのコマンドの効果なのかはよくわかんない)
あと、何よりも嬉しいのがこれ。
-rw-r--r-- 1 nodamushi nodamushi 5 Apr 4 11:33 piyo.txt
piyo.txt のファイルの権限が root じゃなくて、 nodamushi なんですよ。Dockerで同じ操作をすると、権限が root になりますよね。ひゃっほい😀
というわけで、ほとんど Docker と変わらない感じでコンテナの実行とかはできるみたい。実際にはポートとかの扱いとか細かいところはやっぱり異なるみたいだけど、プロジェクトをビルドするとかの用途では気にする必要なさそう。
Buildahで作成したイメージをPodmanで実行
Buildah でイメージを作成する手順は主に以下。
- 作業用コンテナを作成する
- Dockerfileでいう RUNやCOPY、ENV などの設定
- Dockerfile でいう CMD や ENTRYPOINT などの設定
- 作業コンテナからイメージの作成
以下のShellスクリプトを適当に保存して実行してみよう。
#!/bin/bash # 作業用コンテナの作成。 alpine イメージから作成 container=$(buildah from alpine) # 環境変数を設定する場合は config env buildah config --env MESSAGE="Hello World!" $container # Dockerfileでいう CMD は config --cmd。 ENTRYPOINT は config --entrypoint buildah config --cmd "/bin/sh -c 'echo \$MESSAGE'" $container # 作業コンテナから Image を作成 buildah commit $container my-hello-world # 作業コンテナがもういらないなら削除 buildah rm $container
上のスクリプトには書く場面がなかったけど Dockerfile でいう COPY と RUN は以下のように書くみたい。
# COPY buildah copy $container コピー元ファイル コピー先 # RUN buildah run $continer -- コマンド
作成したイメージは Podman からすぐに実行できる。Dockerから実行しようとするなら、 Docker Hub か、Private Registory を立ち上げてそこに push するしかないのかな。
$ podman run --rm my-hello-world
Hello World!
まとめ
とりあえず、初 Podman と Buildah (びるだっち) を試してみました。 Podman はデーモンレスで、 コンテナ内で root 権限のままボリュームにファイル生成してもホスト側ではユーザー権限になるのがとっても良い。
Buildah は現状そこまで必要という感じはしないかなぁ。別に Dockerfile(Containerfile)で簡単にかける範囲なら、 Dockerfile を使えばいいと思う。ループや分岐がしたくなったら、 Buildah +Podman という選択になるのかな。
以上、まぁ、ちょっとは老害を脱せたのではないでしょうか。。。
Zynqで Microblaze を動かす方法
ども。nodamushiです。今年こそはブログ頑張って書こうと思いつつも早くも3月が終わろうと………え、終わった!?
さて、今回は Zynq (Cora Z7-07S) でMicroblaze を動かそうとしたら、意外にも苦戦したのでその記録を残しておきます。
Zynq で Microblaze を動かしたい
Zynq で Microblaze ?何?ヘテロジニアスなコアでも開発してんの?
いえいえ、実はシングルCPUアプリケーションの実験なんです。Microblazeを触りたかっただけで、手元にあるのがZynq (Cora Z7-07S) だったんですよね。泣く泣くの選択です。
さて、まずはお馴染みLチカから。というわけで、作ったデザインは以下。単に AXI を介して GPIO に Microblaze が繋がってるだけ。

Lチカのプログラムはこんな感じ。
#include <xparameters.h> #include <cstdint> #define LED ((volatile uint32_t*)(XPAR_GPIO_0_BASEADDR)) #define LEDT ((volatile uint32_t*)(XPAR_GPIO_0_BASEADDR + 4)) int main() { *LEDT = 0; // all out *LED = 1; // LED Red return 0; }
Bitstreamを作成し、Vitisで上記のプログラムを動かそうとしたところ、問題が発生。
ビルドはできるんだけど、デバッグを開始すると、「No ARM processor found in design.Cannot reset entire system」というエラーメッセージが表示されて何も動かん。おい、どういうことだってばよ。

MicroBlaze code running on a Zynq Development Board ZC706 without a Zynq IP implemented - Error: No ARM processor found in design. とか読んでもよくわからない……。私は単に動かしたいだけなんだがな……。
解決法:ZYNQ Processing System のリセットを接続
動かなーい、って実は一回諦めたんだけど、リセットが動きゃいいってことは、単に ARM CPU のリセット繋げばいいんじゃね?ってことで、Zynq7 Processing Systemを配置してリセットを接続してみたら、Runをすることに成功しました。

LEDもちゃんと光ったよ。おぉ、やったね。
というわけで、 Zynq で何故か Microblaze を動かそうとしてる変な人は、とりあえずZynq のリセットを接続すればいいみたいよ。
脱毛の痛みを数値化してみた
私「髭っていらねぇし、剃るの面倒くさいし、いい加減脱毛しようかと思ってんだけど、通うの面倒くさいんだよねぇ」
母「ていうか、あんた全身脱毛したら?」
私「は?」
母「体毛あると介護するのに迷惑なのよ。あんた身寄りのない痴呆老人になるんだから、せめてよそ様の迷惑を減らしておきなさい。」
私「え?マジ?」
というわけで、私事ですが、現在介護脱毛中です。
デブで性悪の上に不潔とか、ウンコ製造機どころか、私がウンコそのものだった様です。人間は諦めても、せめてウンコ製造機ぐらいにはなりたい。
え?痩せろ?………いいですか、そもそも痩せれるなら太らないのです。
さて、この脱毛ですが、痛いです。漏らすぐらいには痛いです。
いや、正直、平時の肩や首の方が痛いので、大した痛みだとは思わなかったのですが、それでもちょっとした覚悟は必要ですね。ちなみに、今のところ人生で一番痛かったのは、長さで言えば歯の矯正、瞬発力で言えば鼻中隔湾曲症の止血してる詰め物(鼻の穴に親指ぐらいの太さのものが入ってたぞ)を抜いた瞬間ですかね。
さて、大したことないとはいえ、痛いことは痛いわけです。この痛み、実際後どのぐらい続くのでしょうか? 説明では完全脱毛まで2年はかかるという話でしたが、本当なの? 数値として示してほしい。
というわけで、ちょっとシミュレーションしてみた。
前提条件
- 毛は個々に固定の毛周期を持つと仮定
- 毛は休止期と成長期の2つと仮定(退行期は考慮しない)
- 毛がどちらの期間にあるかは一本一本ランダムに異なるとする
- 脱毛をすると、成長期の毛を確率で殺せると仮定
- 休止期の毛は殺せないと仮定
- 脱毛をすると、成長期の毛は必ず痛むと仮定
- 脱毛をすると、休止期の毛は75%が痛むと仮定
- 知覚する痛みは、痛みを発生させた毛の本数に比例すると仮定
コード
KillHair.cpp
#include <iostream> #include <cstdint> #include <random> #include <cmath> constexpr size_t size = 1000000; // シミュレートする毛の本数 constexpr uint32_t span = uint32_t(365 * 1.5); // 毛周期の日数 constexpr double rate = 0.7; // 休止期毛の割合 constexpr double killRate = 1; // 成長期の毛を殺害できる確率 constexpr uint32_t killSpan = 60; // 脱毛する周期 constexpr double tdamageRate = 0.75; // 休止期毛が痛む確率 template<class I> I saturation(I min, I max, I value) { return std::min(std::max(min, value), max); } struct Hair { uint32_t lifeSpan; double rateOfTelogen;// 休止期の割合. 退行期は無いものとする uint32_t date; // lifeSpan * rateOfTelogen より小さければ休止 bool telogen; // 休止か否か bool dead; // タヒんだ毛 static bool isTelogen(uint32_t date, uint32_t lifeSpan, double rateOfTelogen) { return date < rateOfTelogen * lifeSpan; } /** * 初期化。若干乱数によって指定値からぶれるようにしている。 * @param span 毛周期 * @param rate 休止期の割合 * @param rand 0~1の一様分布な乱数 */ template<class Random> void init(uint32_t span, double rate, Random& rand) { lifeSpan = uint32_t(span * (1 + (rand() - 0.5) * 0.1)); rateOfTelogen = saturation(0.1, 1.0, rate * (1 + (rand() - 0.5) * 0.1)); date = ((uint32_t)(rand() * lifeSpan)) % lifeSpan;; telogen = isTelogen(date, lifeSpan, rateOfTelogen); dead = false; } /** * 脱毛実行 * @param rate 殺毛率 * @param rand 0~1の一様分布な乱数 * @return 毛がタヒんだかどうか。もともとタヒんでいた場合はfalse */ template<class Random> bool kill(double rate, Random& rand) { if (dead || telogen) return false; if (rand() < rate) { dead = true; return true; } return false; } /** * 殺毛の痛み */ uint32_t killDamage() { if (dead) return 0; if (!telogen) return 1; // 休止期のすべてが痛いのは直観に反する // 休止期のx%が痛むと仮定 if (date < rateOfTelogen * lifeSpan * tdamageRate) return 1; return 0; } bool alive() const { return !dead; } /** * 日にちを更新する。 */ void nextDay() { if (dead) return; date = (date+1) % lifeSpan; telogen = isTelogen(date, lifeSpan, rateOfTelogen); } }; int main(int argc, char const *argv[]) { std::vector<Hair> hairs; hairs.resize(size); std::random_device seed_gen; std::mt19937 engine(seed_gen()); std::uniform_real_distribution<> dist(0, 1.0); auto r = [&]() { return dist(engine); }; for(auto& h: hairs) { h.init(span, rate, r); } size_t alive = size; uint32_t kill = 0; uint32_t killCount = 0; uint32_t firstDamage = 0; while(alive) { if (!kill) { kill = killSpan; uint32_t damage = 0; uint32_t kills = 0; for(auto& h: hairs) { damage += h.killDamage(); if (h.kill(killRate, r)) { alive--; kills++; } else { h.nextDay(); } } if (killCount == 0) firstDamage = damage; double damageRate = std::ceil(((double)damage / firstDamage) * 100); double alives = ((double)alive / (double)size) * 100; killCount++; std::cout << "[" << killCount << "回目] 痛み: " << damageRate << ", 除毛数: " << kills << ", 残存率: " << alives << "[%]" << std::endl; if (kills == 0) break; } else { for(auto& h: hairs) { h.nextDay(); } } kill--; } std::cout << (killCount * killSpan) << " 日, " << ((killCount * killSpan)/365.0) << " 年" << std::endl; return 0; }
以下のパラメータを直接弄って遊ぶ。
constexpr size_t size = 1000000; // シミュレートする毛の本数 constexpr uint32_t span = uint32_t(365 * 1.5); // 毛周期の日数 constexpr double rate = 0.7; // 休止期毛の割合 constexpr double killRate = 0.8; // 成長期の毛を殺害できる確率 constexpr uint32_t killSpan = 60; // 脱毛する周期 constexpr double tdamageRate = 0.75; // 休止期毛が痛む確率
実行は以下。
g++ -O3 KillHair.cpp && ./a.out
結果
まずは、成長期の毛を必ず殺せる(killRate = 1)とした場合の結果を見てみましょう。
他のパラメータは毛周期 1.5年(大体半年~1年半ぐらいらしい)、休止期の割合は70%で、そこまで変な値ではないと思う。(頭髪以外の休止期の割合は6~8割ほどらしい) 陰毛などは毛周期が3年ぐらいらしい。
[1回目] 痛み: 100, 除毛数: 299395, 残存率: 70.0605[%] [2回目] 痛み: 64, 除毛数: 110091, 残存率: 59.0514[%] [3回目] 痛み: 51, 除毛数: 110260, 残存率: 48.0254[%] [4回目] 痛み: 38, 除毛数: 109903, 残存率: 37.0351[%] [5回目] 痛み: 24, 除毛数: 109849, 残存率: 26.0502[%] [6回目] 痛み: 14, 除毛数: 109776, 残存率: 15.0726[%] [7回目] 痛み: 14, 除毛数: 108899, 残存率: 4.1827[%] [8回目] 痛み: 6, 除毛数: 41825, 残存率: 0.0002[%] [9回目] 痛み: 1, 除毛数: 2, 残存率: 0[%] 540 日, 1.47945 年
100% 毛を殺せる技術があったとしても、9回は殺毛しないと除去しきれないらしいですね。この場合、1.5年はどうあがいても脱毛にかかるようです。ちなみに、除毛周期を90にしたら、6回で1年半でした。
毛周期が長い3年ぐらいだと大体15回(2.5年)ぐらいはかかるみたいです。毛周期が1年だと6回ぐらい、毛周期が半年だと除毛周期60日だと収束しなくて30日とかにしないとだめでした。(その場合でも1年)
痛みは、一回目に感じると想定される痛みを100とした相対ダメージで表示しています。この結果だと、2回目のダメージは1回目のダメージの64%という意味です。
次に、毛を殺せる確立を80% (killRate = 0.8)ぐらいにしてみましょう。完全脱毛の定義って脱毛後に生えてくる毛が10%~20%以下のことを言うらしいし、こんなもんかな?
[1回目] 痛み: 100, 除毛数: 240083, 残存率: 75.9917[%] [2回目] 痛み: 71, 除毛数: 118020, 残存率: 64.1897[%] [3回目] 痛み: 57, 除毛数: 107706, 残存率: 53.4191[%] [4回目] 痛み: 44, 除毛数: 107949, 残存率: 42.6242[%] [5回目] 痛み: 31, 除毛数: 107749, 残存率: 31.8493[%] [6回目] 痛み: 20, 除毛数: 107761, 残存率: 21.0732[%] [7回目] 痛み: 18, 除毛数: 107553, 残存率: 10.3179[%] [8回目] 痛み: 11, 除毛数: 64908, 残存率: 3.8271[%] [9回目] 痛み: 5, 除毛数: 20363, 残存率: 1.7908[%] [10回目] 痛み: 2, 除毛数: 4704, 残存率: 1.3204[%] ~省略~ [31回目] 痛み: 1, 除毛数: 0, 残存率: 0.0006[%] 1860 日, 5.09589 年
おっふ、完全除毛するまで5年もかかってしまった。といっても、この完全除毛はシミュレーション本数や乱数に依存するので、これ自体はあまり意味はありません。この結果も0%になったのではなく、除毛できなかったからループが終わってるだけですし。
完全脱毛の定義の10%以下でみると、やっぱり8、9回ぐらいのようですね。毛周期を3年にしても、13回ほどで10%を下回ります。
シミュレーション上でも、やはり2年という期間に嘘はなさそうです……
さて、痛みの項目ですが、見るべきは3回目からの痛みですかね。カウンセラーの方から 「3、4回目からはそんなに痛くないという人が多い」 と言われたのですが、確かに痛みが半減しています。 除毛数自体は2~7回目に大きな差はないので、休止期毛が減ることで痛みが減っているようです。グラフ(↓)でみるとわかりやすいですね。休止期毛が痛む確率を0にすると、2回目以降はしばらく一定になります。

いずれにせよ、2回目までは、あなた……『覚悟して来てる人』………である必要があるということもわかりますね。
まとめ
かなり大雑把な仮定ですが、確かにシミュレーションからも2年ぐらいかかる、回を重ねるほど痛くなくなり、3、4回目からはさほど痛くないということが数値化できました。
人の話や体験とかを聞くより、やっぱりこうして数字を見るほうが実感わきますね。
私はまだ1回目なので、あともう一回は覚悟してきている人となろうと思います。
なお、個人的には太ももが一番痛かったよ。
Zynq をVitisで Lチカするまでの簡単な手順書
Vitis の起動の仕方すらわからないんだけど
おもむろに Vitis を起動してみたらこんな画面が出てきた。

え………っと、え?何語?日本語でおk。読めない。
ていうか、え?何?私はC++をコンパイルして動かしたいだけなんだけど………。
何でこんなにボタンいっぱいあるの?意味わからない。コワイ。
というわけで、当方初 Vitis で Cora Z7 07Sで Lチカするのに6時間も Vitis の起動方法が分からなくて格闘したのでその記録です。
Vitis むつかしすぎる………。世の中の人、これを当たり前に使ってるのすごいよ………。
Git プロジェクト
作ったものはこちらにあります。
1. Vivado のデザインを作る
Cora Z7 07S は Zynq なので、中に ARM CPU が載っています。これを使って L チカをする………といっても、まずはベースとなるデザインが必要らしいです。
なので、単純に ARM CPU と LED (GPIO)を接続しただけのデザインを作ります。
1.1 Git Ignore を作成
まずは .gitignore を作ってコミットします。

build*/ .vscode/
my_first_vitis_prj/.gitignore at master · nodamushi/my_first_vitis_prj · GitHub
1.2 CMakeLists.txt を作成
次に、Vivado のプロジェクトを作成、合成するための CMake を作成していきます。
CMake で Vivado を扱うにはなんかGitHubに転がってたゴミでできるみたいです。
CMakeLists.txt を作成して、おもむろに以下のコードをコピペ
- ※ VIVADO_REQUIRED_VERSION は自分のVivado のバージョンに合わせる
- ※ GIT_TAG のバージョンだけは最新を調べて設定
- ※ project名はご自分のものに変更
cmake_minimum_required(VERSION 3.14) project(my_first_vitis_prj) ################## # Vivado Version # ################## set(VIVADO_REQUIRED_VERSION 2021.1) #### おまじない ########## include(FetchContent) # https://github.com/nodamushi/vivado_cmake_module FetchContent_Declare( vivado_cmake_module GIT_REPOSITORY https://github.com/nodamushi/vivado_cmake_module.git GIT_TAG v0.0.6 # バージョンは最新のを使う ) FetchContent_MakeAvailable(vivado_cmake_module) list(APPEND CMAKE_MODULE_PATH ${vivado_cmake_module_SOURCE_DIR}) find_package(HLS) find_package(Vivado) find_package(Vitis) ############################
したらば、ターミナルを開いて、以下のコマンドを実行してみましょう。
mkdir build cd build cmake ..
エラーが出なければOK。
通常のインストールフォルダとは異なる場所に Vivado をインストールしたなど、 Vivado が見つからないエラーが出る場合は -DVIVADO_ROOT オプションを指定する。
cmake -DVIVADO_ROOT=/hoge/moge/Xilinx/Vivado/2021.1 ..
1.3 CMakeLists に Vivado プロジェクトを追加
続いて、CMakeLists.txt に以下を追加します。
FetchContent_Declare( xilinxboardstore GIT_REPOSITORY https://github.com/Xilinx/XilinxBoardStore.git GIT_TAG d4e7c68d16042b32a88790162d9b2b5642ca476b ) FetchContent_MakeAvailable(xilinxboardstore)
………コワイ。何してるのかよくわからない。
今回、私が使ってるボードは Cora Z7-07S なのですが、これって最近の Vivado からボード定義ファイルが削除されたんですよね。
なので、GitHub - Xilinx/XilinxBoardStore からダウンロードしてくる処理を最初に書いています。
コワイ FetchContent_Declare と FetchContent_MakeAvailable がそのダウンロード処理です。
Vivado に最初から定義されているボードを使う場合は不要です。
次に、ようやく Vivado プロジェクトを追加します。
add_vivado_project ( sample_design # プロジェクト名 DESIGN top.tcl # デザインファイル TOP top_wrapper # トップモジュール名 (デザインファイルから自動生成) BOARD digilentinc.com:cora-z7-07s:part0:1.0 # Cora Z7-07S を使用 BOARD_REPO ${xilinxboardstore_SOURCE_DIR} # ↑でダウンロードした XilinXBoardStoreへのパスを追加 )
add_vivado_project で Vivado のプロジェクトを定義するそうです。
DESIGNとかTOP とかなんのこっちゃか意味がわかりませんが、とりあえずこう書いておけばいいみたいです。
BOARD には使用するボード定義名を記載します。これは一回普通に Vivado でプロジェクトを作成してみて、自分が作りたいボードの定義名をProject Summary とかからコピーするのが一番です(↓)。

BOARD_REPO は私が Vivado に定義されてない変なボードを使ってるから必要になるだけで通常は不要です。
1.4 Vivado を開いて IP Integrator でせっちゃこ
再び shell に移動して、以下のコマンドを実行します。
これで、 Vivado のプロジェクトを作ってるれるそうです。
cmake .. make sample_design
すると、画面にコワイ謎文字列がいっぱい出てきてなんか終わります。
うまくいけば Vivado のプロジェクトが出来てるので、以下のコマンドで Vivado を開きます。
make open_sample_design

………ボタンがいっぱいあってコワイよぉ。
とりあえず、 IP Integrator の Open Block Design をクリックします。

後は Zync のProcessing Systemや Microblaze などを適当に配置して適当にAuto connect で接続します。

接続したら、この
ボタンを押して Validation チェックをします。

成功したら Ctrl + S で Vivado を保存しましょう。まぁ、なんか Warningが出てますがよくわからんので無視します。
1.5 IP Integrator を出力する
ちゃんとVivado 保存しましたか?保存しましたね?Vivado 保存しましたね?
Vivado 保存したら、Vivado が保存されてることを確認して、再度Vivadoが保存されてることを確認してから、以下を実行します。
make export_bd_sample_design
すると、謎の top.tcl というコワイ ファイルが突然生成されます。
どうもこのコマンドを実行すると、作った IP Integrator の画面を復元するための謎ファイルを生成してくれるようです。
つまり、以下のような関係みたいですね。

では、本当に復元されるのか、Vivado をクローズし、一度全部削除してから試してみましょう。
なお、再三の忠告ですが、 Vivadoは保存してから makeコマンド実行しましたね?
make clear_sample_design make sample_design make open_sample_design
clear_sample_design はプロジェクトディレクトリごと全部削除してしまいますので、 Vivado を保存せずに export _bd_sample_design した場合はここで試合終了です。
ちゃんとVivado を保存してから、 export _bd_sample_design を実行した場合は、以下のようにIP Integrator が復元されます。

1.6 合成を試す
もし、合成を試したい場合は以下のコマンドで合成ができます。
make impl_sample_design
2. Vitis プロジェクトの作成とビルド
Vivado のデザインができたので、あとは Vitis でLチカプログラムを書くだけです。頑張りましょう。
2.1 ソースファイル(src/main.cpp)を作成
まずは、 空の src/main.cpp を作成しておきます。

ディレクトリは src にしておくのが良いと思います。
2.2 Vitis プロジェクトを CMake に追加
CMakeLists.txt に以下を追加して、 Vitis プロジェクトを追加します。
add_vitis_hw_project( sample_app # プロジェクト名 XSA sample_design # Vivado プロジェクトを指定 PROC 知らんがな。馬鹿にすんな。 # 知らんがな SOURCES src # ソースコードディレクトリ )
………はぁ~………PROC とかまた頭おかしい知らんもんが出てくるし…。
ホンマ、人を馬鹿にされてるみたいで腹立つわぁ………。
いつになったらプログラムを書けるんだよ…。
何か知らんけど、 PROC というのは、搭載されてるプロセッサのことらしいです。
マジで知らんがな。人を馬鹿にすんなよ、オイ。私はプログラム書きたいだけだっつてんだろ、オォン!?
そこで、以下のコマンドを実行します。
make show_proc_sample_design
このコマンドでデザインに含まれるプロセッサを表示してくれます。
ただ、この時に一回 Vivado の合成が走るので結構時間がかかります。(謝罪:ハードウェアをエクスポートするだけなら合成は要らないという情報も見たのですが、一応合成してます)
しばらくすると、以下の様な文字列が表示されました。
----------------------------------------- Processors: /home/nodamushi/my_first_vitis_prj/build/sample_design.xsa ----------------------------------------- ps7_cortexa9_0 ----------------------------------------- [100%] Built target show_proc_sample_app
ps7_cortexa9_0 というのが搭載されてるプロセッサのようです。
なので、先ほどわからなくてイライラさせられたところは以下のようにします。
add vitis project · nodamushi/my_first_vitis_prj@83b5627 · GitHub
add_vitis_hw_project( sample_app # プロジェクト名 XSA sample_design # Vivado プロジェクトを指定 PROC ps7_cortexa9_0 # Processor SOURCES src # ソースコードディレクトリ )
以上で CMakeLists.txt は完成です。あとはプログラムを書いていくだけです。
2.3 Vitis プロジェクトを一度生成する
プログラムを書き始める前に、一度 Vitis のプロジェクトを作成します。
make create_sample_app
で、コマンドが完了したら、VS Code の「CMake: Delete Cache and Reconfigure」のコマンドを実行します。

よくわからないんですが、これを実行しないと、VS Code での補完が効かない様です。
2.4 Lチカプログラムを作成する
もう VSCode で補完が効くようになっているので、プログラムが書けます!



ほら、ちゃんと補完が効いてるよ!!
作ったコードはこちら。
2.5 ビルドをして実行
プログラムが完成したら、あとはビルドして書き込むだけです。
まずは、ビルドを行います。
make sample_app
これで、アプリケーションがビルドされます。
ビルドに成功したら、Vitis を起動して書き込みましょう。
make open_sample_app
上記のコマンドで Vitis が開きます。

残念なことに、なんかうっとうしい Welcom画面が開きます。×を押して閉じましょう。(これ開かないで済む方法あったら誰か教えてください)

×を押すと、上記のようにアプリケーションプロジェクトと、プラットフォームプロジェクトが表示されています。
既にビルドはされてるので、アプリケーションプロジェクトを右クリックして、Run As...からプログラムを書き込んで実行しましょう!

ひかったぞーいーーー

完!
注意事項:Vitis でファイルを追加した場合
注意事項として、新規にファイルを追加した場合は、一度 clear_sample_app を実行して削除してから、再度プロジェクトの作成から行う必要があります。
これは、 Vitis の💩仕様でフォルダをリンクしてるのではなく、コピーしているためです。
ファイルそのものはシンボリックリンク(ソフトリンク)させているので変更が反映されますが、フォルダの変更は反映されません。したがって、プロジェクトの作成からやり直す必要があります。
また、デバッグ中に Vitis 側でファイルを追加した場合なども、元のソースディレクトリには反映されません。手動でコピーしてください。
まとめ

最終的に作ったファイルはたったのこれだけです。
これだけなのに、6時間もかかってしまいました。
Vitis むつかしい………
ひとまず、本手順に従えば、 IP Integrator とか以外は基本的に GUI を触らなくていいので、初心者でもなんとかなると思います。
Vivadoに入門できない~DFXも分からない~
Vivadoで DFX (PR)をやってみたので、メモ書き。私は何時になったらVivadoがわかるようになるのか………
デザインの用意
まずは、元々作ってた Lチカデザインから、LED0を Lチカをするための RTLとポート を削除して、代わりに必要なクロックとリセットをデザインから外に出した、以下の様なデザインを作りました。

LED1はHLSでチカらせるサンプルコードで、今回は放置。JTAGやらも関係ない。
PRしてる間、 LED が消えないことを確認する(書き込み中もFPGAが動作することを確認する)ためだけに乗っています。
※ RTLにするために分離したけど、2021ではしなくても良いのかな
あと2021.2あたりでBlock Design ContainerのDFX対応入ったので、RTLじゃなくてもいいはず。
— Kenta IDA (@ciniml) March 10, 2022
(ちなみに、私が使う必要があるのは2019なのだっ)
DFXプロジェクトにする
以下のメニューからDFXプロジェクトに変換する。

Enable Dynamic Function eXchange...を押して、プロジェクトをPRに対応させる。何かバージョンによってこのメニューの文字列が違うっぽい。
TCLコンソールに流れるコマンドは以下。
set_property PR_FLOW 1 [current_project]
で、先ほど削除したmyrtlをsourceタブで右クリックして、Create Prtition Definition... を実行。

しかしできなかった。 Topでアクティブになっていないとダメらしい。よく分からないけど、トップモジュールで接続しろって事なんでしょう。

ということで、雑にトップ接続をしたRTLを作ります。(※古いバージョンではRTLじゃないとダメ)
`timescale 1 ps / 1 ps module top ( output wire [2:0] led0, output wire [2:0] led1, input wire reset, input wire sys_clock); wire aclk; wire aresetn; myrtl#(.T(50 * 1000 * 1000)) myrtl( .clk(aclk), .resetn(aresetn), .led(led0) ); design_1_wrapper design_1( .aclk(aclk), .aresetn(aresetn), .led1(led1), .reset(reset), .sys_clock(sys_clock)); endmodule

再度ボタンを押すと………

…え、パラメタがだめ?……制限多いなっコンニャロウ!仕方がないので、myrtlの T を localparam にして再実行。

パーティションの名前を設定する必要があるらしい。とりあえず、ledにしてOKを押しました。
するとなんか、Partition Definitions というタブが追加された。

TCLコンソールには以下が実行されてた。
update_compile_order -fileset sources_1 create_partition_def -name led -module myrtl create_reconfig_module -name myrtl -partition_def [get_partition_defs led ] -define_from myrtl
PR に他のモジュールを追加
同じ領域に別のRTLを差し込む(DFX)には、 Add Reconfiguarable Module からRTLを追加すれば良いらしい。

画面は………よく意味分からないけど、Add Fileでmyrtl2を設定してみた。名前はmyled2にした。(同じだとわかりにくいから)

update_compile_order -fileset sources_1 create_reconfig_module -name myled2 -partition_def [get_partition_defs led ] add_files -norecurse ■■■■■/src/rtl/myrtl2.v -of_objects [get_reconfig_modules myled2]
(■は私のPCのPathなので隠してます。)

む、入れ子構造の中身が見えていない。
なるほど、このmyrtl2っていうのは1つのビルド系になる、みたいなイメージなんだな。ソースファイルや制約ファイルとかも全部定義し直し、らしい。

RTLに必要なファイルを2つともAddしたら、上手く認識できてるっぽい。

ここまでを CMake化
ひとまず、ここまでをCMakeから復元できるか試してみます。デザインファイルを読み込んだ後に、以下のTCLコマンドを実行するように変更しました。
set_property PR_FLOW 1 [current_project] create_partition_def -name led -module myrtl create_reconfig_module -name myrtl -partition_def [get_partition_defs led ] -define_from myrtl create_reconfig_module -name myled2 -partition_def [get_partition_defs led ] add_files ${root}/src/rtl/myrtl2.v \ ${root}/src/rtl/_myrtl2.sv \ -of_objects [get_reconfig_modules myled2] update_compile_order
update_compile_order は実行すべきなのか、いつやるのかよく分からない。
とりあえず、これで再度プロジェクトを生成し直してみると、同様の設定で復元されました。よしよし。
Configuration 定義
ここまで来たら、一度合成を実行する。合成が完了して、Open Synthesized Design を押すと、以下のEdit Configurationsが勝手に表示される。

とりあえず、 automatically create configurations を押すと、以下の様になった。

とりあえず、この設定のままで Next を押すと、今度は Runの設定が出てくる。

良く分からないけど、ここでも取りあえず、 automatically create configuration run を押す。

で、良く分からないまま何か良さそうな気がするので、 Nextを押して完了する。
TCL コンソールには以下が実行されていた。
create_pr_configuration -name config_1 -partitions [list myrtl:myrtl ] create_pr_configuration -name config_2 -partitions [list myrtl:myled2 ] set_property PR_CONFIGURATION config_1 [get_runs impl_1] create_run child_0_impl_1 -parent_run impl_1 -flow {Vivado Implementation 2021} -pr_config config_2
リージョン定義
このまま、PRの場所の定義をする。
PRするモジュールを右クリックして「Floorplanning」→「Draw Pblock」を押す。

とりあえず、全く意味が分からないので X0Y0 を全部選択してみた。


今回は1つしか作らないので、コレで設定は完了です。Ctrl+Sを押して、xdcファイルに書き出します。

中身
startgroup create_pblock pblock_myrtl resize_pblock pblock_myrtl -add CLOCKREGION_X0Y0:CLOCKREGION_X0Y0 add_cells_to_pblock pblock_myrtl [get_cells [list myrtl]] -clear_locs endgroup close [ open F:/Workspace/xilinx/study/study/vivado/dfx.xdc w ] add_files -fileset constrs_1 ■■■■■/vivado/dfx.xdc set_property target_constrs_file ■■■■■//vivado/dfx.xdc [current_fileset -constrset] save_constraints -force
保存した制約ファイルの追加自体は普通に add_files -fileset constrs_1 でいいっぽい
Implementをして見るも失敗
で、implementを走らせると………はい、エラー

………BSCANってなに?
JTAG。ふむ?JTAGの何かを潰しちゃってる?どういうこと?

なんか、ここが怪しい。他の所にはないし。
で、ここを拡大してみると………

あった、BSCAN!あと、上にICAPとかいうのもある。これもDFX用のモジュールで、ロジックからPRを行う場合に使うモジュールらしい。
なるほど、偶然選んじゃいけないところを選んでしまったらしい。(X0Y0だもの………。選んじゃうよ。)
というわけで、となりにお引っ越し。

で、もう一回implementを実行。おぉ、配置できた。

書き込む
プロジェクト.runs ディレクトリには色々と合成の結果とかが格納されていますが、必要なのはこの2つです。

名前は以下から分かります。

それぞれのディレクトリには、以下の様に2つのBitstream が出来ています。

●●_prtial.bit が DFXで使うビットストリームで、もう一方(今回はtop.bit)が全体です。
というわけで、先ずは全体を書き込み。

この段階では、私が作ったRTL は3色LEDが一色ずつ点灯します。
次に、myrtl2の方のPartial Bitstreamを書き込みます。選択するときにディレクトリと、bitを間違えないように注意。

書き込んでみると、LED1側(DFXと無関係)はずっと光ったままでした。OKOK.
で、LED0側が………


あ、あれ?紫色と緑色!?
本当は白(RGB全部)→消灯(RGB消灯)を繰り返すつもりだったのが何故か「RB点灯」→「G点灯」を繰り返しています。
ちなみに、点滅部分のコードは以下。
always_ff @(posedge clk) begin if (!resetn) led <= 0; // 0初期化 else if (counter >= T) led <= ~led; // 分周して、反転するだけ end
これ、PR時にFFの値が初期化されてないな?
後で教えてもらったのですが、7seriese だと PR してもリセットが入らないそうです。 https://japan.xilinx.com/support/documentation/sw_manuals_j/xilinx2020_2/ug909-vivado-partial-reconfiguration.pdf#page=34
リセットをするには、以下の制約を設定しておくみたいです。
set_property RESET_AFTER_RECONFIG true [get_pblocks <reconfig_pblock_name>]
(※ただ、今回は私は、リージョンの範囲を中途半端に指定したため、この設定が出来ませんでした。)
CMake化
ここまでの作業を CMake で復元できるようにしたのが以下です。
Unityに入門したいのにできない~URPでシェーダー書いてみる~
数式扱うようなビジュアル プログラミングなんて死んでもやりたくありません。馬鹿じゃねぇの?
というわけで、なんとか自分でURPのシェーダを書いてみます。
自動生成された Lit シェーダを読んでみた
生成されたコードを見るみたいなボタンがあったので押してみると、2000行にも及ぶシェーダーコード。辛そう………。
泣く泣く読んでみると、沢山の Pass がありました。取りあえずLiteModeでgrepかけると、以下のパスがありました。殆どのパスはtargetが4.5と2.0で2つ作られるようです。
| Pass | 説明 | target |
|---|---|---|
| UniversalForward | 最終的に画面に出すレンダリングパスかな。 | 4.5 / 2.0 |
| UniversalGBuffer | 光の計算はせずにジオメトリを出力するパス。defferdレンダリング用。 | 4.5 |
| ShadowCaster | シャドウマップかデプステクスチャにレンダリングするパス。 | 4.5 / 2.0 |
| DepthOnly | デプステクスチャに書き込むパス。 | 4.5 / 2.0 |
| DepthNormals | URPバージョン10.0.xからの _CameraNormalsTexture を作成するときに使うパス。 | 4.5 / 2.0 |
| Meta | Unity エディターでライトマップをベイク処理するときに使うパス。 | 4.5 / 2.0 |
| Universal2D | 2Dレンダラで使うパス。 | 4.5 / 2.0 |
| SceneSelectionPass | Sceneビューで選択されたオブジェクトの境界を標示するためのパス | 4.5/2.0 |
| Picking | Sceneビューで選択する領域を定義するためのパス | 4.5/ 2.0 |
また、コードの殆どは型変換のコードばかりで、実質的には100行もないのではないでしょうか。
型変換コードが大量にあるのは、たぶん、Shader Graphの表現型をそのままコードに落とし込むためかなぁ。無駄な情報の最適化とかはコンパイラで頑張るのでしょうね。
うん、何となく分かったから、手書きできそうかな?
UnityとしてはShaderGraphがコア機能になってるので、ShaderLabではなく、ShaderGraphを推奨してるのでしょうが、面倒なのでやりたくない。
ただ、DepthNormalsみたく、URPはまだまだ変更が多く加わるので、バージョンという面ではShaderGraphによる自動生成が良いのでしょう。
とりあえずの結論としては、一度手書きでコードを作成した後に ShaderGraph を作れるなら作るというのが良い気がします。(発想が逆ぅ!)
とりあえず、テクスチャだけ表示するシェーダを作る
さっくりできた。
hogeシェーダー
Shader "Unlit/hoge" { Properties { [MainTexture] _BaseMap ("Texture", 2D) = "white" {} } SubShader { Tags { "RenderPipeline"="UniversalPipeline" "RenderType"="Opaque" } Pass { Name "Universal Forward" Tags { "LightMode" = "UniversalForward" } Cull Back ZTest LEqual ZWrite On HLSLPROGRAM // Pragmas #pragma target 4.5 #pragma vertex vert #pragma fragment frag #pragma multi_compile_instancing #pragma multi_compile _ DOTS_INSTANCING_ON // Includes #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" struct appdata { float4 pos: POSITION; float2 uv : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD1; UNITY_VERTEX_INPUT_INSTANCE_ID UNITY_VERTEX_OUTPUT_STEREO }; sampler2D _BaseMap; CBUFFER_START(UnityPerMaterial) float4 _BaseMap_ST; CBUFFER_END v2f vert (appdata i) { v2f o; UNITY_SETUP_INSTANCE_ID(i); UNITY_TRANSFER_INSTANCE_ID(i, o); // ワールド座標、クリップスペース、ビュースペースを全部計算するらしい // 使われない値はコンパイラが無視するとのこと VertexPositionInputs vInput = GetVertexPositionInputs(i.pos.xyz); o.pos = vInput.positionCS; o.uv = TRANSFORM_TEX(i.uv, _BaseMap); return o; } half4 frag (v2f i) : SV_Target { UNITY_SETUP_INSTANCE_ID(i); half4 baseColor = tex2D(_BaseMap, i.uv); return baseColor; } ENDHLSL } } }
URPでは空間の変換は取りあえず GetVertexPositionInputs にぶん投げておけばワールド空間、クリップ空間、ビュー空間の座標に一気に変換してくれるっぽい。
コメントを信じるなら、要らない変数はコンパイラが頑張って削除してくれるので気にしなくて良いよとのこと。
v2f vert (appdata i) { v2f o; UNITY_SETUP_INSTANCE_ID(i); UNITY_TRANSFER_INSTANCE_ID(i, o); // ワールド座標、クリップスペース、ビュースペースを全部計算するらしい // 使われない値はコンパイラが無視するとのこと VertexPositionInputs vInput = GetVertexPositionInputs(i.pos.xyz); o.pos = vInput.positionCS; o.uv = TRANSFORM_TEX(i.uv, _BaseMap); return o; }

影を表示する
シャドウマップから情報持ってきて、影を表示してみたいとおもいます。実装したのが以下。
mogeシェーダ
Shader "Unlit/moge" { Properties { [MainTexture] _BaseMap ("Texture", 2D) = "white" {} } SubShader { Tags { "RenderPipeline"="UniversalPipeline" "RenderType"="Opaque" } Pass { Name "Universal Forward" Tags { "LightMode" = "UniversalForward" } Cull Back ZTest LEqual ZWrite On HLSLPROGRAM // Pragmas #pragma vertex vert #pragma fragment frag // Lightweight Pipeline keywords #pragma multi_compile _ _MAIN_LIGHT_SHADOWS #define _MAIN_LIGHT_SHADOWS_CASCADE #define _SHADOWS_SOFT // #pragma multi_compile _ _ADDITIONAL_LIGHT_SHADOWS // #pragma multi_compile _ LIGHTMAP_SHADOW_MIXING // #pragma multi_compile _ SHADOWS_SHADOWMASK #pragma multi_compile_instancing #pragma multi_compile _ DOTS_INSTANCING_ON // Includes #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl" struct appdata { float4 pos: POSITION; float2 uv : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD1; float4 shadowCoord : TECOORD4; // メインライトの影 UNITY_VERTEX_INPUT_INSTANCE_ID }; sampler2D _BaseMap; CBUFFER_START(UnityPerMaterial) float4 _BaseMap_ST; CBUFFER_END v2f vert (appdata i) { v2f o; UNITY_SETUP_INSTANCE_ID(i); UNITY_TRANSFER_INSTANCE_ID(i, o); // ワールド座標、クリップスペース、ビュースペースを全部計算するらしい // 使われない値はコンパイラが無視するとのこと VertexPositionInputs vInput = GetVertexPositionInputs(i.pos.xyz); o.pos = vInput.positionCS; o.uv = TRANSFORM_TEX(i.uv, _BaseMap); o.shadowCoord = GetShadowCoord(vInput); return o; } half4 frag (v2f i) : SV_Target { UNITY_SETUP_INSTANCE_ID(i); half4 baseColor = tex2D(_BaseMap, i.uv); // Light // directionに_MainLightPosition.xyz // colorに色が格納されているっぽい Light light = GetMainLight(i.shadowCoord); half shadow = light.shadowAttenuation * light.distanceAttenuation; return baseColor * half4(shadow, shadow, shadow, 1); } ENDHLSL } } }
とりあえず、影に関するキーワードは以下ぐらいあるみたいです。
// Lightweight Pipeline keywords #pragma multi_compile _ _MAIN_LIGHT_SHADOWS #define _MAIN_LIGHT_SHADOWS_CASCADE #define _SHADOWS_SOFT // #pragma multi_compile _ _ADDITIONAL_LIGHT_SHADOWS // #pragma multi_compile _ LIGHTMAP_SHADOW_MIXING // #pragma multi_compile _ SHADOWS_SHADOWMASK
_MAIN_LIGHT_SHADOWS のキーワードを定義するとメインライトに対する影の処理が有効になります。以下の参考記事によると、これは固定でdefineしたらダメっぽい。
_MAIN_LIGHT_SHADOWS_CASCADE と _SHADOWS_SOFT はデフォルトONでいいよね?よくわからんけどって感じでdefineで定義してみました。
参考
- Universal Render Pipeline環境でのシェーダー最適化対策 - Qiita
- 【Unity】ソフトシャドウってハードシャドウよりどれだけ重いのか?シェーダを見て考える - LIGHT11
- シャドウカスケード - Unity マニュアル
で、Lightingの為のhlslをインクルード
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
v2f にシャドウマップから取得するための情報を格納。
struct v2f { float4 shadowCoord : TECOORD4; // メインライトの影 }; o.shadowCoord = GetShadowCoord(vInput);
フラグメントシェーダで取りあえず、影の分だけ暗くするようにしました。
Light light = GetMainLight(i.shadowCoord); half shadow = light.shadowAttenuation * light.distanceAttenuation; return baseColor * half4(shadow, shadow, shadow, 1);
で、結果が以下………あれ?なんもかわらんね?

うーん、もしかして ShadowCaster のパスを実装しないとダメなのかな?というわけで、以下の一文を追加。
UsePass "Universal Render Pipeline/Lit/ShadowCaster"

おぉ、影がついた。
ShadowCaster のパスは必須みたいですね。
ShadowCasterも自分で実装してみる
ShadowCasterも自力で実装してみます。
moge2シェーダ
Shader "Unlit/moge2" { Properties { [MainTexture] _BaseMap ("Texture", 2D) = "white" {} } SubShader { Tags { "RenderPipeline"="UniversalPipeline" "RenderType"="Opaque" } Pass { Name "Universal Forward" Tags { "LightMode" = "UniversalForward" } Cull Back ZTest LEqual ZWrite On HLSLPROGRAM // Pragmas #pragma vertex vert #pragma fragment frag // Lightweight Pipeline keywords #pragma multi_compile _ _MAIN_LIGHT_SHADOWS #define _MAIN_LIGHT_SHADOWS_CASCADE #define _SHADOWS_SOFT // #pragma multi_compile _ _ADDITIONAL_LIGHT_SHADOWS // #pragma multi_compile _ LIGHTMAP_SHADOW_MIXING // #pragma multi_compile _ SHADOWS_SHADOWMASK #pragma multi_compile_instancing #pragma multi_compile _ DOTS_INSTANCING_ON // Includes #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl" struct appdata { float4 pos: POSITION; float2 uv : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD1; float4 shadowCoord : TECOORD4; // メインライトの影 UNITY_VERTEX_INPUT_INSTANCE_ID }; sampler2D _BaseMap; CBUFFER_START(UnityPerMaterial) float4 _BaseMap_ST; CBUFFER_END v2f vert (appdata i) { v2f o; UNITY_SETUP_INSTANCE_ID(i); UNITY_TRANSFER_INSTANCE_ID(i, o); // ワールド座標、クリップスペース、ビュースペースを全部計算するらしい // 使われない値はコンパイラが無視するとのこと VertexPositionInputs vInput = GetVertexPositionInputs(i.pos.xyz); o.pos = vInput.positionCS; o.uv = TRANSFORM_TEX(i.uv, _BaseMap); o.shadowCoord = GetShadowCoord(vInput); return o; } half4 frag (v2f i) : SV_Target { UNITY_SETUP_INSTANCE_ID(i); half4 baseColor = tex2D(_BaseMap, i.uv); // Light // directionに_MainLightPosition.xyz // colorに色が格納されているっぽい Light light = GetMainLight(i.shadowCoord); half shadow = light.shadowAttenuation * light.distanceAttenuation; return baseColor * half4(shadow, shadow, shadow, 1); } ENDHLSL } Pass { Name "ShadowCaster" Tags { "LightMode" = "ShadowCaster" } Cull Back ZTest LEqual ZWrite On ColorMask 0 HLSLPROGRAM // Pragmas #pragma vertex vert #pragma fragment frag #pragma multi_compile_instancing #pragma multi_compile _ DOTS_INSTANCING_ON // Includes #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" struct appdata { float4 pos: POSITION; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct v2f { float4 pos : SV_POSITION; }; v2f vert (appdata i) { v2f o; UNITY_SETUP_INSTANCE_ID(i); VertexPositionInputs vInput = GetVertexPositionInputs(i.pos.xyz); o.pos = vInput.positionCS; return o; } half4 frag (v2f i) : SV_Target { return 0; } ENDHLSL } } }
ShadowCasterはめっちゃ単純に入力された頂点をクリップ空間に変換し、色は0を出力してるだけです。
v2f vert (appdata i) { v2f o; UNITY_SETUP_INSTANCE_ID(i); VertexPositionInputs vInput = GetVertexPositionInputs(i.pos.xyz); o.pos = vInput.positionCS; return o; } half4 frag (v2f i) : SV_Target { return 0; }
これで表示すると………

あれー?まだテクスチャ貼ってない段階だけど、何か酷いことになった………。何この縞模様。
で、正常に表示されている Lit シェーダのShadowCasterを覗いてみると、vertexシェーダのクリップ空間の計算が、以下の様になっていました。
float3 positionWS = TransformObjectToWorld(input.positionOS.xyz); float3 normalWS = TransformObjectToWorldNormal(input.normalOS); float4 positionCS = TransformWorldToHClip(ApplyShadowBias(positionWS, normalWS, _LightDirection)); #if UNITY_REVERSED_Z positionCS.z = min(positionCS.z, positionCS.w * UNITY_NEAR_CLIP_VALUE); #else positionCS.z = max(positionCS.z, positionCS.w * UNITY_NEAR_CLIP_VALUE); #endif return positionCS;
ApplyShadowBias というのが重要っぽいケド、なんだろこれ。というわけで、 Shader.hlsl を覗いてみます。
float3 ApplyShadowBias(float3 positionWS, float3 normalWS, float3 lightDirection) { float invNdotL = 1.0 - saturate(dot(lightDirection, normalWS)); float scale = invNdotL * _ShadowBias.y; // normal bias is negative since we want to apply an inset normal offset positionWS = lightDirection * _ShadowBias.xxx + positionWS; positionWS = normalWS * scale.xxx + positionWS; return positionWS; }
うーんと、要するに、ライト方向に移動させて、法線方向に拡大or縮小してるのかな。
あー。そっか、おっさんようやく意味が分かった。
あの縞模様って「自分自身の影」を表示しちゃってるんだ。だから、「若干ライトから見て奥に移動」と「若干縮小」で表面が自分の影に入らない様にしてるんだね。
というわけで、以下のインクルードと、定数を追加。 なお、CommonMaterial.hlsl をインクルードしないと Shadows.hlsl で関数定義不足でエラーになった。(これ、バグじゃない?)
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonMaterial.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl" float3 _LightDirection;
で、頂点シェーダを以下の様に書き換え。
v2f vert (appdata i) { v2f o; UNITY_SETUP_INSTANCE_ID(i); VertexPositionInputs vInput = GetVertexPositionInputs(i.pos.xyz); float3 normalWS = TransformObjectToWorldNormal(i.normal); o.pos = TransformWorldToHClip(ApplyShadowBias(vInput.positionWS, normalWS, _LightDirection)); #if UNITY_REVERSED_Z o.pos.z = min(o.pos.z, o.pos.w * UNITY_NEAR_CLIP_VALUE); #else o.pos.z = max(o.pos.z, o.pos.w * UNITY_NEAR_CLIP_VALUE); #endif return o; }
UNITY_REVERSED_Z は OpenGL系とDirectXでデプスが0~1か、1~0かで違うのを吸収する為っぽい。中の数式(min、max)は良く意味が分からんけど、何かの飽和処理なんでしょう。ライトの画面外にでた場合でも表示するためかな。

上手く影でました。
moge3シェーダー
Shader "Unlit/moge3" { Properties { [MainTexture] _BaseMap ("Texture", 2D) = "white" {} } SubShader { Tags { "RenderPipeline"="UniversalPipeline" "RenderType"="Opaque" } Pass { Name "Universal Forward" Tags { "LightMode" = "UniversalForward" } Cull Back ZTest LEqual ZWrite On HLSLPROGRAM // Pragmas #pragma vertex vert #pragma fragment frag #pragma multi_compile_instancing #pragma multi_compile _ DOTS_INSTANCING_ON // Lightweight Pipeline keywords #pragma multi_compile _ _MAIN_LIGHT_SHADOWS #define _MAIN_LIGHT_SHADOWS_CASCADE #define _SHADOWS_SOFT // #pragma multi_compile _ _ADDITIONAL_LIGHT_SHADOWS // #pragma multi_compile _ LIGHTMAP_SHADOW_MIXING // #pragma multi_compile _ SHADOWS_SHADOWMASK // Includes #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl" struct appdata { float4 pos: POSITION; float2 uv : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD1; float4 shadowCoord : TECOORD4; // メインライトの影 UNITY_VERTEX_INPUT_INSTANCE_ID }; sampler2D _BaseMap; CBUFFER_START(UnityPerMaterial) float4 _BaseMap_ST; CBUFFER_END v2f vert (appdata i) { v2f o; UNITY_SETUP_INSTANCE_ID(i); UNITY_TRANSFER_INSTANCE_ID(i, o); // ワールド座標、クリップスペース、ビュースペースを全部計算するらしい // 使われない値はコンパイラが無視するとのこと VertexPositionInputs vInput = GetVertexPositionInputs(i.pos.xyz); o.pos = vInput.positionCS; o.uv = TRANSFORM_TEX(i.uv, _BaseMap); o.shadowCoord = GetShadowCoord(vInput); return o; } half4 frag (v2f i) : SV_Target { UNITY_SETUP_INSTANCE_ID(i); half4 baseColor = tex2D(_BaseMap, i.uv); // Light // directionに_MainLightPosition.xyz // colorに色が格納されているっぽい Light light = GetMainLight(i.shadowCoord); half shadow = light.shadowAttenuation * light.distanceAttenuation; return baseColor * half4(shadow, shadow, shadow, 1); } ENDHLSL } Pass { Name "ShadowCaster" Tags { "LightMode" = "ShadowCaster" } Cull Back ZTest LEqual ZWrite On ColorMask 0 HLSLPROGRAM // Pragmas #pragma vertex vert #pragma fragment frag #pragma multi_compile_instancing #pragma multi_compile _ DOTS_INSTANCING_ON // Includes #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonMaterial.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl" struct appdata { float4 pos: POSITION; float3 normal: NORMAL; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct v2f { float4 pos : SV_POSITION; }; float3 _LightDirection; v2f vert (appdata i) { v2f o; UNITY_SETUP_INSTANCE_ID(i); VertexPositionInputs vInput = GetVertexPositionInputs(i.pos.xyz); float3 normalWS = TransformObjectToWorldNormal(i.normal); o.pos = TransformWorldToHClip(ApplyShadowBias(vInput.positionWS, normalWS, _LightDirection)); #if UNITY_REVERSED_Z o.pos.z = min(o.pos.z, o.pos.w * UNITY_NEAR_CLIP_VALUE); #else o.pos.z = max(o.pos.z, o.pos.w * UNITY_NEAR_CLIP_VALUE); #endif return o; } half4 frag (v2f i) : SV_Target { return 0; } ENDHLSL } } }
輪郭を表示
URPでも昔ながらの前面カリングで輪郭線を付けれるみたいなので、やってみます。
まずは、 「Edit」→「Project Settings...」を開いて、「Tags and Layers」を表示して、適当な Outlined レイヤーを追加

そうしたら、輪郭を表示したいオブジェクトのLayerをOutlinedに変更。

で、こんな感じのテキトーなアウトライン表示用のシェーダとマテリアルを用意。
outlineシェーダ
Shader "Unlit/outline" { Properties { _BaseMap ("Texture", 2D) = "black" {} _Width ("Width", float) = 0.01 } SubShader { Tags { "RenderPipeline"="UniversalPipeline" "RenderType"="Opaque" "UniversalMaterialType" = "Lit" "Queue"="Geometry" } Pass { Name "Universal Forward" Tags { "LightMode" = "UniversalForward" } Cull Front HLSLPROGRAM // Pragmas #pragma vertex vert #pragma fragment frag // Unity defined keywords #pragma multi_compile_instancing #pragma multi_compile _ DOTS_INSTANCING_ON // Includes #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl" // -------------------------------------------------- struct appdata { float4 pos: POSITION; float3 normal: NORMAL; float2 uv : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; }; sampler2D _BaseMap; CBUFFER_START(UnityPerMaterial) float4 _BaseMap_ST; float _Width; CBUFFER_END v2f vert (appdata i) { v2f o; UNITY_SETUP_INSTANCE_ID(i); VertexPositionInputs vInput = GetVertexPositionInputs(i.pos.xyz + i.normal * _Width); o.pos = vInput.positionCS ; o.uv = TRANSFORM_TEX(i.uv, _BaseMap); return o; } half4 frag (v2f i) : SV_Target { return tex2D(_BaseMap, i.uv); } ENDHLSL } } }
URP の Renderer Data のInspectorを開いて、「Add Rendrer Feature」からRender Objects を追加し、こんな感じで設定。

OverridesのMaterialでoutlineを指定してシェーダーを変更するというやり方らしい。
こんな感じで球体に輪郭線が出ました。

Unityのシェーダーが微粒子レベルぐらいはわかってきたかなぁ。いや、もう調べれば調べるほど色々出てきて収拾つかないね。
Unityに入門したいのにできない~URPを触ってみる~
UnityもVivadoも全然入門できなくて困っています。
まぁ、おっさんが無能なのは置いておいて、今後は URP というのと、 HDRP というのがスタンダードになっていく予定らしいですね。
ということは、新参者のおっさんは取りあえず、 URP と HDRP をやっておけば良いのだと思います。
RP はレンダー パイプラインで、レンダリング処理の流れをスクリプトで変更することが可能(SRP:スクリプタブルレンダーパイプライン )になる模様。シェーダーとかも旧レンダラとは互換性がなく、新規に作る必要があるらしい。ついでに、 URP と HDRP にも互換性がなく、どちらで作るのか最初に決めないとダメらしい。
URP は軽量プラットフォームでも動く物で、HDRPは高性能なマシン(コンピュートシェーダが使えるマシンかな)で動く物。 URP は現在カスタム ポスト エフェクトが作れないらしく、固定の物から選択するしかないらしい。
でも、検索すると公式な手段がまだ提供されてないだけで、URPでもポストエフェクトが作れるらしい。
おっさんがUnity弄ってる目的ってぶっちゃけゲーム作りと言うより絵作りで、おっさんのPCで動けば満足なので HDRP で良い気がするけど。しかし、どうも HDRP は中々難しいらしい(?)ので、先ずは URP を試してみます。
URP のプロジェクトを作る
まずはURP のプロジェクトを作ってみます。新規作成時に「Universal Render Pipelin」を選択しても良いのでしょうけど、何か最初からシーンが入ってて初心者にはわかりにくいので、3Dのプロジェクトから0から作っていきます。

参考: Installing the Universal Render Pipeline into an existing Project
先ずはPackage ManagerからUniversal RP をプロジェクトにインストールするらしい。

Packages: Unity Registry を選択した後、 universal で検索し、 Universal RP が出ていて選択していることを確認後、 Install

インスコしただけだとダメで、URPのレンダー パイプライン アセットを作成する必要があるらしいです。
というわけで、Assetsの下に適当にurpディレクトリを作りました。後は右クリックからメニューを辿って作成します。
「Create」→「Rendering」→「Univarsal Render Pipeline」→「Pipeline Asset(Forward Renderer)」

ふむ?ディレクトリに何か作成された。

作っただけではダメで、これをプロジェクトのグラフィックス設定で指定しないとダメらしい。
Edit →Project Setings... →Graphics→Scriptable Render Pipeline Settingsの◎ボタンと押していって、最後に出てきた画面で作成したアセットを選択。

よくわからないけど、これで完了したらしい。
ポストプロセスを使ってみる
URPだとカスタムポストエフェクトを作れないと書いたけど、そもそもどうやってポストエフェクト適応するんだ?ということでやってみる。
参考: How to configure post-processing effects in URP
まずは、シーンのカメラを選択して、InspectorでPost Processing にチェックを入れる。

次に、ポストエフェクトを適応するために右クリックからGlobal Volumeを作成。Volumeにはグローバルとローカルがあって、特定エリアだけエフェクトを書けるとかしたい場合はLocalを使うっぽいです。
VolumeそのものはあらゆるGemeObjectにアタッチできて、有効なVolumeとWeightから使用する効果を決めるそうです。

効果はProfileというものに設定を保存してるのかな?とりあえず、新規にProfileを作成。

このProfileで適応する効果は、Add Overrideボタンで追加できる。ボタンを押すとポップアップが開いて、その中から効果を選択する。なるほど、この効果を現在は自作できない、ということらしい。

適当にカラー調整のエフェクトを追加して適当に設定。

おぉ、画面が赤くなった。

もう一個Volumeを作成して、こっちは青くする効果にしてみました。で、Weightをぐりぐり弄ると、混色具合が変わります。

なるほどぉ。
これ、複数のカメラ作ったとき、効果を変更するのはどうやるんだろう?
まぁ、必要になったら調べれば良いか。
Shaderを作ってみる
Shaderもこれまで作った物は使えないらしい。具体的に何が違うのか分からないけど、やってみよう。
とりあえず、テンプレートっぽい物がメニューにあったので作ってみる。

で、出てきたのがコレ。

つ、ツライ。なにこれ、こんな分かりにくい物、絶対やりたくない………。ビジュアルプログラミングとか、クソの頂きですよ (現実の私を知る一部の方から「お前が言うな」と言われかねませんが)
………ん?この仮想テクスチャってなんじゃろ。なんかUEの事を調べてたときに読んだ気がする。

調べてみると、やっぱりUEの仮想テクスチャの仕組みと同じっぽい。これが発展すると Nanite になるのですね。
なお、仮想テクスチャはHDRPでしか使えないみたいですね。HDRPも面白そうですねぇ。

まぁ、何にせよ、GUIは難しいし、触りたくないなぁ。
というわけで、次回は0からShaderを書いてみる。