プログラムdeタマゴ

nodamushiの著作物は、文章、画像、プログラムにかかわらず全てUnlicenseです

Vivado/VitisのCMakeを強化した

 一度CMake化してしまうと、GUI が面倒臭いのであれもこれも Make コマンドから実行したくなります。

 というわけで、さらにCMakeを強化したのでメモ

github.com

 

目次

 

追加

  • report_addr_${project} : 設定したアドレスの一覧を出力する。GUI を見ないと確認できないのが馬鹿じゃないのかと常々思ってた。
  • open_project: Vivado のプロジェクトをGUIで開く。Explorer でダブルクリックするのが面倒臭いし、パスを指定するのも面倒臭かった。
  • program_project: BitStream を書き込む。xsdb で connect→target →fpga とかやるのが面倒臭かった。
  • export_bd_project: DesignのTCLファイルを出力する。Fileメニューからパスを指定して保存するのが面倒臭かった。
  • Vivado_HLSでも動作するように修正

 

FindXXXの検索パスの追加

 以下の様にして追加。

set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake)

Vivado

 find_package(Vivado REQUIRED) で読み込む。

 XILINX_VIVADO か、 VIVADO_ROOT 変数からvivadoのパスを検索する。

 CMakeのVIVADO_VERSION が定義されるので、読み込んだVivadoのバージョンが幾つか確認できる。

Vivadoプロジェクト定義

add_vivado_project(
   <project>
   BOARD <board name>
   TOP   <top module>
   [DIR <directory name>]
   [RTL <file/directory>...]
   [CONSTRAINT <file/directory>...]
   [IP <directory>...]
   [DESIGN <tcl file>]
   [DEPENDS <target>...]
)

引数

  • <project> : プロジェクト名
  • BOARD <name> : ボードパートの名前
  • TOP <name> : トップモジュールの名前

オプション引数

  • DIR <dir> : プロジェクトのディレクトリ名。(デフォルトは <projet>.prj)
  • RTL <path>... : RTL ファイル・ディレクトリ。複数のパスを指定可能
  • CONSTRAINT <path>... : 制約ファイル・ディレクトリ。複数のパスを指定可能
  • IP <path>... : IPディレクトリ(HLSで合成した物含む)。複数のパスを指定可能
  • DESIGN <TCL file>: ボードデザインのTCLファイル
  • DEPENDS <target>... : 依存ターゲット。複数指定可能

定義されるターゲット

ターゲット名 説明
<project> Vivadoプロジェクトの作成
open_<project> 作成したVivadoプロジェクトをGUIで開く
clear_<project> プロジェクトディレクトリを削除
impl_<project> ビットストリームを作成する。 ビルドディレクトリ直下のbitディレクトリにコピーされる
program_<project> ビットストリームを書き込む.詳細後述
export_bd_<project> デザインのボードファイルをDESIGNで指定したファイルに出力。
report_addr_<project> アドレスの情報を出力.詳細後述

 

program_<project>

make JTAG=ターゲット番号 [XSDB_URL=URL] program_<project>

  • JTAG: JTAGの番号。未指定の場合は、ターゲット一覧を表示して終了する
  • XSDB_URL: ローカルサーバー以外のhw_serverを指定する

 

report_addr_<project>

make [REPORT_CSV=file] report_addr_<project>

  • REPORT_CSV: 出力するCSVファイル名

現状は以下の様な内容を出力

Offset, Range, Access, Usage, Path, NAME
0x00000000,0x00010000,read-write,register,/JTAG/Data/SEG_axi_gpio_0_Reg,SEG_axi_gpio_0_Reg
0x00020000,0x00010000,read-write,register,/JTAG/Data/SEG_hlsled_0_Reg,SEG_hlsled_0_Reg

 

 

HLS

 find_package(HLS REQUIRED) で読み込む。

 VitisはXILINX_HLS か、 VITIS_ROOT 変数から vitis_hls のパスを検索する。

 VivadoはXILINX_VIVADO か、 VIVADO_ROOT 変数からvivado_hlsのパスを検索する。

 VivadoとVitisではVitisが優先される。

 CMakeのHLS_VERSION が定義されるので、読み込んだVitis/Vivado HLSのバージョンが幾つか確認できる。

 Vitis/Vivadoの確認は HLS_IS_VIVADOHLS_IS_VITIS 変数が TRUEかFALSEかで判断できる。

HLS プロジェクトの定義

add_hls_project(
 <project>
 TOP      <top module>
 PERIOD   <clock period(ns)>
 PART     <board part>
 SOURCES  <C++ source file>...
 [INCDIRS    <include directory>...]
 [LINK       <link library>...]
 [TB_SOURCES <test bench C++ file>...]
 [TB_INCDIRS <include directory>...]
 [TB_LINK    <link libray>...]
 [DEPENDS    <depends target>...]
 [NAME    <display name>]
 [IPNAME  <IP name>]
 [VENDOR  <your name>]
 [TAXONOMY <category>]
 [VERSION  <version(x.y)>]
 [SOLUTION <solution name>]
 [COSIM_LDFLAGS <flag string>]
 [COSIM_TRACE_LEVEL <none|all|port|port_hier>]
)

add_hls_project( TOP PERIOD PART SOURCES <C++ source file>... [INCDIRS ...] [LINK ...] [TB_SOURCES ...] [TB_INCDIRS ...] [TB_LINK ...] [DEPENDS ...] [NAME ] [IPNAME ] [VENDOR ] [TAXONOMY ] [VERSION <version(x.y)>] [SOLUTION ] [COSIM_LDFLAGS ] [COSIM_TRACE_LEVEL <none|all|port|port_hier>] )

 

定義されるターゲット

ターゲット名 説明
create_project_<project> HLS プロジェクトを作成する
open_<project> HLS プロジェクトをGUIで開く
clear_<projec> HLS プロジェクトを削除する
csynth_<project> 高位合成をする
cosim_<project> C/RTL シミュレーションを実行する
lib_<project> C++のコンパイル
test_<project> テストベンチのコンパイル

 

引数

  • project: プロジェクト名
  • TOP <name> : トップモジュール名
  • PERIOD <ns>: クロック周期(ナノ秒)
  • PART <name> : デバイスの part名
  • SOURCES <C++ source file>...: HLSのC++ソースファイル(ヘッダは不要)

 

オプション引数

  • NAME <name> : IP の表示される名前.スペースを使っていい
  • IPNAME <name>: IP名。スペースは使っちゃダメ
  • VENDOR <name>: 作成者の名前
  • TAXONOMY <name>: よくわからんけど、IPのカテゴリ名らしい
  • VERSION <x.y>: IP version(x.y)
  • SOLUTION <name>: ソリューション名
  • TB_SOURCES <C++ source file>... : テストベンチのC++ソースファイル(ヘッダは不要)
  • INCDIRS <directory>... : HLSのインクルードディレクトリ
  • TB_INCDIRS <directory>... : テストベンチの追加インクルードディレクトリ
  • DEPENDS <target>... : プロジェクト生成に必要な依存ターゲット
  • LINK <name>... : HLSの合成に必要なリンクライブラリ
  • TB_LINK <name>...: テストベンチのコンパイルに必要な追加のリンクライブラリ
  • COSIM_LDFLAGS <string> : cosim_designに渡す-ldflags
  • COSIM_TRACE_LEVEL <none, all, port, port_hier>: よくしらん。

Vivadoに入門したいんですけど~Vitis HLSの合成とVivadoのCMake化~

 おっさん、今回はVitis HLS やって来ました。正直に言うと、めっちゃ苦労しました。

 どのぐらい苦労したかって言うと、ここ3日ほどUnityもBlenderもゲームもしてません。

 あぁ、GPGPUもやらなきゃ………やることだけが積み上がっていく。お金も積み上がってくれませんかね。

 

Vitis HLS に入門したいけど出来ない

 おっさんは2021.1を入れているのですが、何やらVivado HLS ではなく、 Vitis HLS と言う物になっているらしい。細々色々違って大変だという話は聞きますが、入門どころか門戸を叩けてすらいない私には何の関係もないので、このままVitis HLS で行きます。

 

 とりあえず、HLSで合成したものでLEDをチカらせます。そこまでの道がだいぶ長いです。ゾルディック家かな?

 

VS Code で開発できるプロジェクト構成を作る

 Vivado や Vitis HLS の エディタはないので、何らかのエディタを選ぶ必要があります。おっさんはEmacs………は最近開いてないので VSCode にしました。

 VSCode で CMake Tools の拡張機能を入れていると、 CMakeLists.txt の情報から勝手にライブラリの検索候補などを引っ張ってきてくれます。マジ便利。

 というわけで、 Vitis HLS はMakefileではなく、 CMake で合成します。

 VSCode でライブラリのパスなどに反応させたければ、適当に add_libraryadd_exec などでターゲットを作って、リンクしてやれば上手くいきます。

 Vitis HLS のインクルード対象ファイルは Vitis HLS のインストールディレクトリのinclude ディレクトリです。

add_libray(foobar  foobar.cpp)
target_link_library(foobar C:/Xilinx/Vitis/2021.1/include)

 上記のようなCMakeLists.txt をディレクトリに置いておけば、割と良い感じに補間候補を見つけてきてくれます。

 ただ、これだと他人が使えないし、自分もバージョンを選べないし、他のプロジェクトに使い回せないしで不便なので FindXXX.cmake を作ります。

 

 むろん、既存の FindVivado.cmake とか、 FindoVitis.cmake は GitHubとかにあるのでそれを使ってもいいんですが、なんというか、わかりにくいんですよね。

 大抵プロジェクト専用の定数やらビルド手順が書き込まれてるので、出発点となるようなFindVitisは見つかりませんでした。

 なので、とにかくシンプルに使えて、後から必要なら付け足せるように作ることを目指しました↓。

github.com

 一見ゴチャゴチャしているように見えますが、実際には引数の値の確認と、相対パスの修正をしているだけで固定値は殆ど持っていません。

 なので、たぶん、各プロジェクト用に付け足ししやすいはずです。たぶん。一応、vivado_hls でも動きました。

 以下の様な感じで Vitis/Vivado HLSプロジェクトを定義 します。

add_hls_project(hlsled
  TOP     HlsLED
  VERSION 1.0
  VENDOR  nodamushi
  NAME    "HLS LED Chika"
  PERIOD  10 # 100MHz
  PART    xc7z007sclg400-1
  SOURCES hlsled.cpp
  # Test
  TB_SOURCES tb.cpp
)

    

 この関数を使うと、以下のターゲットが作られます。

  • create_project_hlsled: Vitis HLSのプロジェクトの作成
  • clear_hlsled: 作成したプロジェクトの削除
  • csynth_hlsled: 高位合成の実行
  • cosim_hlsled: C/RTL シミュレーション
  • test_hlsled: テストベンチのコンパイル

   高位合成したければ、 csynth_hlsled で出来ます。

 

Vivado もCMake化する

 ここまで作っておっさんは気がつきました。この CMake、 MSYS2 の上で動くじゃん。

 おっさんの環境問題かもしれませんが、VivadoはMSYS2のシェル上では動きません↓(左がMSYS2のbash、右がDOS窓)

f:id:nodamushi:20220303114822p:plain

 

 しかし、上で作った CMake は Vitis HLS の合成が MSYS2 の上で出来たのです。(MSYS2で合成したのは無意識)

 あれ? CMake 化すれば DOS 窓要らないんじゃね?

 というわけで、 Vivado を前回は Makefile で合成してましたが、これも同様に CMake 化しました。

github.com

 この FindVivado.cmake には xsdb ターゲットが定義してあります。と、いうことは………

f:id:nodamushi:20220303115630p:plain

 やった、MSYS2でxsdb が動いたよ!脱DOS窓!

 

 Vivadoのプロジェクト定義 も、Makefileの時 より分かりやすくなったんじゃないかな。

project(vivado_study)

add_vivado_project(vivado_study
  TOP        design_1_wrapper
  BOARD      digilentinc.com:cora-z7-07s:part0:1.0
  RTL        ../src/rtl
  CONSTRAINT ../src/constraint
  IP         ../build/src/hls
  DESIGN     design_1.tcl
  DEPENDS    csynth_hlsled
)

 

実はメチャクチャ苦労した

 ました工法でサクッと出来ましたが、裏では実は苦労しました。

  1. VSCode で上手く補完が効かない
  2. Vitis HLS の cosim_design で GoogleTest が使えない

 

Windowsで選択すべき構成について 

 VSCodeではScan for kitsで大凡上手くスキャンしてくれるから、あまり今まで気にしてなかったんだけど、上手くいかないときは構成を選択する必要があります。

f:id:nodamushi:20220303001008p:plain:w320

 

 そして、マジで上手くいかなかって苦労したんよ………。

f:id:nodamushi:20220303001248p:plain

 こんな感じで見つからなかったり………、見つかるようになっても iostream が見つかりませんとか言われたり………。まじなんなの。

 

 環境固有の問題かもしれませんが、私の環境ではどう足掻いても Visual Studio のコンパイラやClang では解析することが出来ませんでした。

f:id:nodamushi:20220303001931p:plain

 

 Windowsの環境において選ぶべきはこのへんのMSYS系。ない場合は、 MSYS2 をインストールして、64bit版のGCCやClangを入れてください。

 ただ、これを選択しても最初は全然安定しなかったんだけど、この3日で何したのかを覚えていないの。同じような症状になったら頑張ってください。1回VSCodeを再起動すると治ることも良くあります。

 また、私が作った FindHLS.cmake はvitisを探すために、環境変数 XILINX_HLS かVITIS_HLS_ROOT を設定する必要があります。でも環境変数は設定したくないので、構成を追加します。

f:id:nodamushi:20220303113710p:plain

 まずは、VSCodeのコマンドでCmake: ユーザーローカル CMake キットの編集を実行します。

 で、なんかJSONファイルが出てくるので、ご自身の上手く動く構成をコピーして追加し、"cmakeSetting"に VITIS_HLS_ROOT を設定します。

 私の場合は以下の様にしています。

  {
    "name": "Vivado 2021.1",
    "compilers": {
      "C": "C:\\msys64\\mingw64\\bin\\gcc.exe",
      "CXX": "C:\\msys64\\mingw64\\bin\\g++.exe"
    },
    "cmakeSettings": {
      "VITIS_HLS_ROOT": "C:\\Xilinx\\Vitis_HLS\\2021.1",
      "VIVADO_ROOT": "C:\\Xilinx\\Vivado\\2021.1"
    }
  }

 あとは、この Vivado構成を選択するだけです。

 

cosim_designでGoogleTestがコンパイルできない

 これは、もう諦めました。

 最初は Cベースのテストを GoogleTest で作って動いたのでヤッタね!とか言ってたんですが、これをcosimで実行しようとするとどうしても コンパイルできません でした………。

 実行できないんじゃないんですよ。 コンパイル できないんですよ。リンクが出来ないんじゃないのですよ。 コンパイル が出来ないんですよ。

 それも、インクルードが見つからないというエラーが出るのなら対処しようもあるのですが、以下の様なエラーが出るんですよ………

  1. # という不正な文字がある
  2. ifndef という不正な文字がある
  3. #endif が出てきたが #if の範囲内にない

 

 むろん、私はテストコードの中でマクロは使っていませんし、ifdefなどもheaderでしか使ってません。まず間違いなく GoogleTest がincludeで展開された結果でしょう。

 もう、どうにもならねぇよ………。なんだ、これ………。エラーで検索すると「 (全角空白)」を間違えて入れてるときによく初心者が遭遇するエラーらしいのですけど、「#」だよ。知らねぇよ。

 誰かタスケロ

まとめ

 色々苦労しましたが、 HLS でLED をチカらせるコードを書いて、 Vivado の IP Integrator でつなぎ、ビルドも書き込みもは全て MSYS2 で行えるようになりました。

 おっさんも、これでようやくVivadoの門戸を叩くことが出来そうです。

Vivadoにいつまでも入門できないの~VivadoでGUIなしで合成~

 おじさん分かった。ブログ書かねぇからいつまで経ってもVivadoがよく分からないんだ。

 世の中はインプットに読む人/聞く人、アウトプットに書く人/話す人がいるというけど、私はインプットに書く人、アウトプットはしない人というゴミ屑みたいなタイプなのだ。書かねばならぬのだ。

 というわけで、 Vivado をポチポチしたいけどプロジェクトの作り方の時点から躓いたから頑張った

目的

 以下の様なプロジェクトを作りたい。

  1. Vivado をGit管理する
  2. Vivado のGUIがなくても合成できるようにする

 

作ったレポジトリ

GitHubに置いてあります。この記事の時点でのコミットは以下。

https://github.com/nodamushi/vivado_init_project/tree/19e58c23419515d0829968369757c7973e2d1b1a

環境

  • Cora Z7
  • Windows
  • Vivado 2021.1
  • VSCode

 

事前準備

 VSCodeでコマンドプロンプトを開く。生DOS窓はツライ。

 Vivadoを通常通りインストールしているなら、以下のコマンドを実行して、Vivadoをコマンドプロンプトで実行できるようにする。

call C:\Xilinx\Vivado\2021.1\settings64.bat

 

まずはボードの名前を調べる

 Cora Z7 のボード名(ID?)を調べる為に先ずは Vivado の GUI で塵プロジェクトを作ります。

 Create Project を押しましてー

f:id:nodamushi:20220228134342p:plain:w320

 ゴミ箱作りましてー

f:id:nodamushi:20220228134455p:plain:w320

 Cora Z7 を選択しましてー

f:id:nodamushi:20220228134531p:plain:w320

 TCL Consoleに表示された digilentinc.com:cora-z7-07s:part0:1.0 の文字列をコピーして、Vivadoを閉じる。

 もうdustboxプロジェクトは要らないのでエクスプローラーから完全削除する。

f:id:nodamushi:20220228134641p:plain

 

プロジェクトを初期化する

適当なプロジェクトディレクトリを作成し、以下のコマンドを実行。環境はMSYS2のbash

git init
touch .gitignore
touch .editorconfig
mkdir -p src/rtl
touch src/rtl/myrtl.v
mkdir -p src/constraint
wget https://raw.githubusercontent.com/Digilent/digilent-xdc/master/Cora-Z7-07S-Master.xdc  -O src/constraint/constraint.xdc
mkdir vivado
touch vivado/Makefile
touch vivado/create_project.tcl

 

  • gitの初期化
  • .gitignoreと.editorconfigの作成
  • ソースコードディレクトリの作成と、適当にRTLファイルを作成
  • Cora z7 7Sの制約ファイル例がGitHubにあるので、src/constraint/constraint.xdcに保存
  • vivadoのプロジェクトディレクトリを作成
  • Makefileとプロジェクト作成用のTCLファイルを作成

f:id:nodamushi:20220228143834p:plain

 

Git Ignoreの設定

取りあえず以下

.Xil/
*.prj/
vivado_*
*.jou
*.log

Editor Config

取りあえず以下

root = true

[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space

[Makefile]
indent_style = tab
indent_size = 2

create_project.tcl

 プロジェクトの復元を行うスクリプトとして、create_project.tclを作成する。

 vivadoのバッチモードでこのスクリプトを実行すると、プロジェクトを復元できる。

 なお、先にコピった digilentinc.com:cora-z7-07s:part0:1.0は以下のboard_name で使う。

 最初に定義してるプロジェクト名とディレクトリは Makefile から渡す。

# Project name and directory are set from Makefile
set project_name      [lindex $argv 0]
set project_directory [lindex $argv 1]

set board_name           "digilentinc.com:cora-z7-07s:part0:1.0"
set rtl_directory        "../src/rtl"
set constraint_directory "../src/constraint"

# Create/Init Project
create_project $project_name $project_directory
set_property board $board_name [current_project]
update_ip_catalog

# Add sources
add_files $rtl_directory
add_files -fileset constrs_1 $constraint_directory/constraint.xdc

close_project

Makefile

先ほど作ったcreate_project.tclを実行するコマンドなどを定義する。

PROJECT_NAME := vivado_study
PROJECT_DIR := $(PROJECT_NAME).prj


$(PROJECT_DIR):
    @vivado -mode batch -source create_project.tcl -tclargs $(PROJECT_NAME) $(PROJECT_DIR)

create_project: $(PROJECT_DIR)

clean:
    @rm -rf $(PROJECT_DIR) .Xil vivado_* *.log *.jou


.PHONY: all create_project clean

 なお、cleanコマンドを rf で定義しているが、これはVivado環境下では Windows でも許される。  

 

myrtl.v

単に3色LEDを順にチカらせるだけのLチカです。

module myrtl#(
  parameter integer T = 1000 * 1000
) (
  input wire clk,
  input wire resetn,
  output wire [2:0] led
);

  reg [31:0] counter;
  reg [2:0] out;
  wire [2:0] out_next_led;
  assign led = out;

  always @(posedge clk) begin
    if (!resetn)
      counter <= 0;
    else
      counter <= counter == T? 0 : counter + 1;
  end

  assign out_next_led = {out[1:0], out[2]};
  always @(posedge clk) begin
    if (!resetn)
      out <= 3'b001;
    else
      out <= counter == T? out_next_led: out;
  end

endmodule

 

プロジェクトの作成

 この作業は、事前準備で Vivado を呼び出せるようにしたコマンドプロンプトで行う。事前準備は以下(再掲)

call C:\Xilinx\Vivado\2021.1\settings64.bat

 makeを呼び出す。

make create_project

 すると、プロジェクトが復元されるので、 vivado/vivado_study.prj/vivado_study.xrpをダブルクリックするなりしてVivadoを起動する。

 起動したら以下の様な感じ。

f:id:nodamushi:20220228143222p:plain:w320

 Create Block Designを押してブロックデザインを作成する。名前は取りあえずそのまま。

f:id:nodamushi:20220228161725p:plain:w320

 先ほど適当に作ったLチカRTLをブロックデザインに突っ込む。右クリックからAdd Module

f:id:nodamushi:20220228181725p:plain:w320

 先ずは led を選択した状態で「Ctrl + K 」でポートを作成する。

f:id:nodamushi:20220228181947p:plain:w320

 で、Run Connection Automaticallyでクロックポートを繋ぐ

f:id:nodamushi:20220228182052p:plain:w320

f:id:nodamushi:20220228182154p:plain:w320

 そうすると何かこうなった。

f:id:nodamushi:20220228182233p:plain

 とりあえず、クロックに関しては、H16ピンから125MHzのクロックが入力されてるので、ピンとして外に出せばいいと思うんだけど、リセット………ってどうするんだ………

 なんか、こう、PLLとかから出てこないの………?わからん………

 というので、散々悩んだんですけど、取りあえず適当なピンにリセットを割り当てておけば良いらしいので、外にピンとして出しておく。

f:id:nodamushi:20220228182609p:plain:w320

 ※Autoconnectを使うとクロックは勝手に繋がった。

 後は、Validate Designを実行して、デザインにエラーがないか確認する。

f:id:nodamushi:20220228182739p:plain:w320

 そして、「File→Export→Export Block Design」を選択して、IP Integratorで構成したデザインを再構築するためのTCLファイルを出力する。

f:id:nodamushi:20220228202819p:plain:w320

 保存場所は、「vivado」ディレクトリの直下。プロジェクト直下がデフォルトでは選択されてるので間違えないこと。

 Automatically create top design にはチェックを入れておく。

f:id:nodamushi:20220228203033p:plain

 保存したらVivadoは終了し、 make clean でプロジェクトを削除。

 

create_projectにdesign_1.tclを読み込む処理を追加

 makeコマンドでIP Integratorを復元するために、 create_project.tclのclose_project の前に以下を追加。

source "design_1.tcl"
regenerate_bd_layout
save_bd_design
set design_bd_name [get_bd_designs]
set bd_files [get_files $design_bd_name.bd]
puts $bd_files
generate_target all $bd_files
make_wrapper -files $bd_files -top -import

close_project

 

  1. 先ほど出力した design_1.tcl を読み込む
  2. レイアウトを綺麗にする
  3. 保存
  4. ボードファイルの一覧を取得して、ターゲットを生成する。ターゲットがなんなのかはよく知らない。
  5. ボードのHDLラッパー作成し、プロジェクトにインポートする

 make create_project を実行して、再々生成したプロジェクトで IP Integrator が復元されていることを確認する。

 ついでに、SettingsのTop Moduleが design_1_wrapper になっていることも確認。

f:id:nodamushi:20220228204212p:plain

   確認したらプロジェクトはもう要らないのでVivadoを閉じて make clean で削除。

 

制約ファイルの修正

 作成したボードのポート定義に合うように、ledreset の設定を変更。

  とりあえず、リセットはボタン0に割り当てた。

  sys_clk はボード定義ファイルにあるので不要らしい。

set_property -dict { PACKAGE_PIN L15   IOSTANDARD LVCMOS33 } [get_ports { led[2] }]; #IO_L22N_T3_AD7N_35 Sch=led0_b
set_property -dict { PACKAGE_PIN G17   IOSTANDARD LVCMOS33 } [get_ports { led[1] }]; #IO_L16P_T2_35 Sch=led0_g
set_property -dict { PACKAGE_PIN N15   IOSTANDARD LVCMOS33 } [get_ports { led[0] }]; #IO_L21P_T3_DQS_AD14P_35 Sch=led0_r
set_property -dict { PACKAGE_PIN D20   IOSTANDARD LVCMOS33 } [get_ports { reset }]; #IO_L4N_T0_35 Sch=btn[0]

 

Makefileから合成する

 合成からビットストリーム作成までをMakefileから行えるようにする。

 これも vivado をバッチモードで起動して、合成用のTCLファイルを実行するだけである。

 以下の implement.tcl を vivado ディレクトリに保存

set project_name      [lindex $argv 0]
set project_directory [lindex $argv 1]

open_project ${project_directory}/${project_name}.xpr

set project_status [get_property STATUS [get_runs impl_1]]
if {$project_status != "write_bitstream Complete!"} {
    launch_runs impl_1 -jobs 8 -to_step write_bitstream
    wait_on_run impl_1
}

close_project

 

  1. プロジェクト(xprファイル)を開く
  2. プロジェクトステータスを見ると「write_bitstream Complete!」だと、合成が完了してるので無視することが出来る
  3. write_bitstreamまでを実行
  4. 完了するまで待機
  5. プロジェクトを閉じて終了

 

 そして、 Makefileに implement ターゲットを追加。 all の対象も implement に変更

all: implement

implement: $(PROJECT_DIR)
    @vivado -mode batch -source implement.tcl -tclargs $(PROJECT_NAME) $(PROJECT_DIR)

 

 で、再々再度 プロジェクトを開くと、合成できてた。

f:id:nodamushi:20220228205703p:plain

 

BitStream をCora Z7 07S に書き込む

 Cora Z7 をUSB経由でPCに接続する。

 Open Hardware Managerから Hardware Manager の画面を開き、Open Targetから Auto Connect を選択して接続。

 自動で接続できない場合は Open New Targetから選択。

f:id:nodamushi:20220228205813p:plain

 で、Program deviceをするとLチカしました。

f:id:nodamushi:20220228210440p:plain

 

まとめ

 とりあえず、VivadoのGUIがなくても合成とかできるようになりました。

Unityでシェーダープログラムに入門3:ディスプレイスメントマップとノーマルマップ

 折角ハルシェーダをやったので、ディスプレイスメントマップをやってみます。ついでにノーマルマップもやってみました。

Blender から Unity にモデルを持っていく

 そもそもの問題として、このレベルのことからわからんゴミ屑です。一歩一歩やっていくしかありません。

 使用している Blender のバージョンは3.0です。

f:id:nodamushi:20220225024200p:plain:w320

 とりあえず、30秒もかけてカワイイキャラクタを作りました。調べると、fbx なるファイルフォーマットで出力すれば Unity で読めるらしい。

f:id:nodamushi:20220225024329p:plain

 保存するときに、直接Unity プロジェクトのAssetに出力すると即座に Asset に反映されました。なるほど、簡単ですね。ドラッグアンドドロップをしてシーンに追加してみるとこんな感じ。

f:id:nodamushi:20220225024522p:plain:w320

 ………?何か面がカクカクしてる。法線がおかしいよね。これってBlenderの設定かな?

 というわけで、 Blender にもどって「編集モード」→「Aで全選択」→「面」→「スムースシェード」で滑らかに表示してから、再度出力してみました。

f:id:nodamushi:20220225050306p:plain

 

 で、Unity で表示してみる。  

f:id:nodamushi:20220225050217p:plain

 おぉ、滑らかになった。ミラーボールみたいな物を出力するわけでもない場合は、スムースシェードを適応してから出力する。おっさん賢くなった。

 

スカルプトでハイポリ作って Unity に持っていく

 Unreal Engine 5の Nanite を使えばハイポリのままゲームに持ち込めるらしいですが、一先ず私がやってるのは Unity なのでハイポリはノーマルマップとかにベイクしてローポリを表示するしかないと思われます。

 折角なのでハルシェーダをやったので、当初の目的通りディスプレイスメントマップを作成してドメインシェーダーで処理したいと思います。

 なので、手順とし以下ですね。

  1. マルチレゾリューションでハイポリとローポリのデータを作成
  2. テクスチャにベイク
  3. テッセレーションに対応したシェーダを作成
  4. マテリアルを割り当てる

ハイポリの作成

 Blender でマルチレゾリューションのモディファイアを追加します。レベルは4ぐらいにしました。

f:id:nodamushi:20220225024951p:plain

 後は、スカルプトモードで丹精込めて10分もかけてカワイイキャラを作ります。

f:id:nodamushi:20220225030640p:plain

 スゴクカワイイ。初めてスカルプティングをして見ましたが、結構難しい………

 ハイポリのキャラクタが完成したら、ベースとなるローポリの球体にも変形を適応します。

f:id:nodamushi:20220225030739p:plain

 元々球体だったのが、何となくハイポリに近い形になりますね。

f:id:nodamushi:20220225030754p:plain

 見やすさのためにフラットシェーディングにしていますが、これもスムースシェーディングに設定することを忘れずに。(一敗)

 

ノーマルマップとディスプレイスメントマップをテクスチャにベイク

 今回はキャラクタをUV球から作ってるので、UV展開とか調整は特にしなくて良いでしょう。

 Blender でShading画面に移動して、レンダープロパティでレンダリングエンジンを Cycles に変更します。(Blender 3.0でもEeveeはベイク出来なかった)

f:id:nodamushi:20220225143103p:plain

 取りあえず適当にマテリアルを作成します。

 で、画像テクスチャノードを追加して、新規にnormal_mapとdisplacement_mapを作成。画像サイズは適当に1024x1024にしました。

f:id:nodamushi:20220225142940p:plain

 で、先ずはnormal_mapのノードを選択してから、レンダープロパティの下の方にあるベイクのランで、「マルチレゾからベイク」をチェックして、ベイクタイプを「ノーマル」を選択肢、「ベイク」ボタンを押す。

f:id:nodamushi:20220225143248p:plain

 次に、displacement_map のノードを選択肢、同様にベイクタイプをディスプレイスメントにしてからベイクボタンを押す。後から理解しましたが、低解像度メッシュにチェックを入れます。

f:id:nodamushi:20220225214845p:plain

 で、適当に保存します。

f:id:nodamushi:20220225143848p:plain

 ノーマルマップはxyzの3次元、ディスプレイスメントマップは距離の1次元なので、RGBAのPNGに納めることが出来ます。RGBにノーマルマップを、Aにディスプレイスメントマップを配置しましょう。

 というわけで、適当に書いたPythonスクリプトで結合してやります。

import sys
import cv2

argv = sys.argv

norm_file = argv[1] # 第1引数 ノーマルマップファイル名
disp_file = argv[2] # 第2引数 ディスプレイスメントマップ
out_file = argv[3] # 出力ファイル

norm_img = cv2.imread(norm_file, -1)
disp_img = cv2.imread(disp_file)
norm_img[:,:,3] = disp_img[:,:,1]

cv2.imwrite(out_file, norm_img)

 pip install opencv-python さえしておけば後は python concat.py ノーマルマップ ディスプレイスメントマップ 出力.png で結合できます。

 ちなみに、Pythonでやる前に、Compositingで結合してみたんだけど、どうも結果がおかしいんだよね………。Blender 力が低すぎてよく分からない。

f:id:nodamushi:20220225144457p:plain:w320

 

Unity で表示

 ディスプレイスメントマップは法線方向にどれだけ点を変動させるのかという情報です。したがってディスプレイスメントマップ適応した点pは元の位置pとマップに記録された値d、法線ベクトルnから以下の様に決まります。

移動後の点の位置
\vec{p}= (d - 0.5) D\vec{n} + \vec{p_0}

 Dはどの程度変位させるかの定数です。0.5を引いているのは、テクスチャは0~1しか保持できないので、中央を0にするため。

 で、この法線なんですが、どう決まるんでしょうか?

  1. 頂点に保存されている法線
  2. ノーマルマップ込みで作る法線

 おっさんはもう頭が腐ってるのでよく分かりません。実際に立方体の辺に膨らみを付けてベイクし、表示してみて確認してみました。

  1. 頂点に保存されている法線

f:id:nodamushi:20220225211706p:plain:w320

 シャープな感じ?

  1. ノーマルマップ込みで作る法線

f:id:nodamushi:20220225211507p:plain:w320

 膨らんでる?

 いやーぶっちゃけ分からん………。Blenderの画面と見比べても視野角とか違うから、細かいことはマジ分からぬ。

 と、思って弄っていたら、ノーマルマップ込みで作る法線で破綻が発生!↓

f:id:nodamushi:20220225212240p:plain

 頂点に保存されている法線では破綻しなかった↓ので、どうやら頂点に保存されている法線nが正解っぽいです。

f:id:nodamushi:20220225212327p:plain

 

 というわけで、答えが分かったので実装しました。

f:id:nodamushi:20220225220111p:plain

 マジでキッメェな、おい、これ、どうすんだよ。夢に出てくるよ。馬鹿じゃねぇのか。これ書いてるの何時だと思ってるんだ、おっさん寝れなくなったらどうするの。

 遠目にはぶっちゃけノーマルマップの所為で、耳の形や足の形が違うかなーぐらいですが、接写するとテッセレーションで頂点数増やしてるため、輪郭線が流石に滑らかですね。ていうか、キッモ。

 

ノーマルマップを使う

 上の画像では、ローポリ状態でもノーマルマップの御陰で言うほど差はわかりませんね。このノーマルマップも今回は自分で実装しました。

 ノーマルマップのデータというのは接空間という直交座標系で保存されているようです。

 UV方向と、法線方向Nからなる座標系のようで、モデルの変形などが起こっても同じように扱うためにこうなっているようです。

 接空間のベクトル方向qから、ローカル空間(メッシュの空間)の方向ベクトルpに変換するには以下の様にします。

qからpへの変換
\begin{aligned}
\vec{p} & =q_u\vec{u} + q_v\vec{v} + q_n\vec{n} \\
 & = q_u\vec{u} \pm q_v(\vec{n}\times\vec{u}) + q_n\vec{n} \\
 & = \vec{q} \matrix M 
\end{aligned}

(※プログラム的には与えられるのはuとnだけなので、vをクロス積から求める。±はソフトごとの座標系の違いによる物………かな。左手系とか右手系とか。)

ここで M は以下の様に定義される。

M
\matrix M = \left(\begin{array}c
u_x & u_y & u_z\\
v_x & v_y & v_z\\
n_x & n_y & n_z
\end{array}\right)
=
\left(\begin{array}c
\vec{u}\\
\vec{v}\\
\vec{n}
\end{array}\right)
=
\left(\begin{array}c
\vec{u}\\
\pm\vec{n}\times\vec{u}\\
\vec{n}
\end{array}\right)

 u,v,nはそれぞれ直交してるので、Mの逆行列はすぐに求まる。

Mの逆行列
\matrix M ^ {-1}  = \left(\begin{array}c \vec{u}^T & \vec{v}^T  &\vec{n}^T \end{array}\right) = M^T

 なので、逆にローカル空間から接空間に変換する際には単純に以下の様になる。

pからqへの変換
\begin{aligned}
\vec{q} & = \vec{p} \matrix M^{-1} = \vec{p} \matrix M^T\\
\vec{q}^T & = M \vec{p}^T
\end{aligned}

 転置の形で最後のqの答えを出しているのは、hlslの関数を使えば以下の様に書けるから。

float3x3 M = float3x3( u.xyz, cross(n, u.xyz) * u.w, n);
float3 q = mul(M, p);

 ±の情報は、u.w にシェーダに渡されるっぽい。ここらへんはエンジンごとに違うっぽいし、実はUnityでもバージョンによって違うのかな。

 後はこの式を使ってドメインシェーダでライト方向と視線方向を接空間に変換してやれば、フラグメントシェーダでは、接空間で光の計算をすれば良いだけになる。この場合、法線方向は単にテクスチャから読み出した値をそのまま使えばいい。

 と、いうわけで、piyoシェーダの全体は後述の様になりました。

 あー………シェーダー全然わからんわぁ

作ったシェーダ

Shader "Unlit/piyo"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _NormalTex ("Normal", 2D) = "bump" {}
        _TessFactor("Tessellation", Range(1, 50)) = 10
        _Displacement("Displacement", Range(0, 1.0)) = 0.3
        _Shininess ("Shininess", Range(0.0, 1.0)) = 0.078125
        _RimColor ("RimColor", Color) = (1,1,1,1)
        _RimPower("RimPower", float) = 0.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            Tags { "LightMode"="ForwardBase" }
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma domain domain
            #pragma hull hullMain

            #include "UnityCG.cginc"
            #include "Tessellation.cginc"
            #include "AutoLight.cginc"
            #define INPUT_PATCH_SIZE 3
            #define OUTPUT_PATCH_SIZE 3



            //---- Define structure ----
            struct appdata {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float4 tangent : TANGENT;
                float2 uv : TEXCOORD0;
            };

            struct v2h {
                float3 pos: POS;
                float3 normal: NORMAL;
                float4 tangent : TANGENT;
                float2 uv : TEXCOORD0;
            };

            struct h2dMain {
                float3 pos: POS;
                float3 normal: NORMAL;
                float4 tangent : TANGENT;
                float2 uv : TEXCOORD0;
            };

            struct h2dConst {
                float edgeFactor[3] : SV_TessFactor;
                float insideFactor : SV_InsideTessFactor;
            };

            struct d2f {
                float4 pos: SV_Position;
                float2 uv : TEXCOORD0;
                half3 lightDir : TEXCOORD1;
                half3 viewDir : TEXCOORD2;
            };

            // --------------------
            sampler2D _MainTex;
            sampler2D _NormalTex;
            float _Displacement;
            float _TessFactor;
            float _LineWidth;
            half4 _LightColor0;
            half _Shininess;
            half4 _RimColor;
            half _RimPower;



            // 頂点シェーダ
            v2h vert(appdata i)
            {
                v2h o;
                o.pos = i.vertex;
                o.normal = i.normal;
                o.uv = i.uv;
                o.tangent = i.tangent;
                return o;
            }


            [domain("tri")]
            [partitioning("integer")]
            [outputtopology("triangle_cw")]
            [outputcontrolpoints(OUTPUT_PATCH_SIZE)]
            [patchconstantfunc("hullConst")]
            h2dMain hullMain(InputPatch<v2h, INPUT_PATCH_SIZE> i, uint id: SV_OutputControlPointID)
            {
                h2dMain o;
                o.pos = i[id].pos.xyz;
                o.normal = i[id].normal;
                o.uv = i[id].uv;
                o.tangent = i[id].tangent;
                return o;
            }

            h2dConst hullConst(InputPatch<v2h, INPUT_PATCH_SIZE> i)
            {
                h2dConst o;
                o.edgeFactor[0] = _TessFactor;
                o.edgeFactor[1] = _TessFactor;
                o.edgeFactor[2] = _TessFactor;
                o.insideFactor = _TessFactor;
                return o;
            }

            // domainシェーダ
            #define NDS_UTIL_DOMAIN_CALC(name) (i[0].name * bary.x + i[1].name * bary.y + i[2].name * bary.z)
            [domain("tri")]
            d2f domain(h2dConst c, const OutputPatch<h2dMain, OUTPUT_PATCH_SIZE> i, float3 bary: SV_DomainLocation) {
                d2f o;
                o.uv = NDS_UTIL_DOMAIN_CALC(uv);

                float3 normal = normalize(NDS_UTIL_DOMAIN_CALC(normal));
                float displace = (tex2Dlod(_NormalTex, float4(o.uv, 0, 0)).a - 0.5) * _Displacement;

                float3 pos = NDS_UTIL_DOMAIN_CALC(pos) + normal * displace;
                o.pos = UnityObjectToClipPos(pos);

                float4 tangent = normalize(NDS_UTIL_DOMAIN_CALC(tangent));
                float3x3 rotation = float3x3(tangent.xyz, cross(normal, tangent.xyz) * tangent.w, normal);
                o.lightDir = mul(rotation, ObjSpaceLightDir(float4(pos, 1)));
                o.viewDir = mul(rotation, ObjSpaceViewDir(float4(pos, 1)));

                return o;
            }

            float4 frag(d2f i): SV_Target {
                i.lightDir = normalize(i.lightDir);
                i.viewDir = normalize(i.viewDir);
                half3 halfDir = normalize(i.lightDir + i.viewDir);
                half3 baseColor = tex2D(_MainTex, i.uv);
                half3 normal = UnpackNormal(tex2D(_NormalTex, i.uv));
                half diff = saturate(dot(normal, i.lightDir));
                half r0 = 1- max(0, dot(normal, i.viewDir));
                half r1 = 1- max(0, dot(normal, -i.lightDir));
                half rim =  pow(r0 * r1, _RimPower);
                half spec = pow(max(0, dot(normal, halfDir)), _Shininess * 128.0);
                half4 col;
                col.rgb  = (diff + spec) * baseColor * _LightColor0 + _RimColor.rgb * rim;
                col.a = 1;
                return col;
            }
            ENDCG
        }
    }
}

Unityでシェーダープログラムに入門2: コンピュートシェーダ

 はい、どうも。流石にシェーダーばっかり弄りすぎて全くゲームを作るという雰囲気じゃないので、いい加減脱線するのをやめて真面目にUnity触っていきたいと思いました。

 なので、今日はコンピュートシェーダをやってみます。

 初心者がどういう思考をして試行錯誤して理解してくのかの過程が見えた方が、他の初心者の人にも良いかなと思って、やったことをそのまま書いていくよ。

とりあえず何も調べないで触ってみた

 前から、ここに何かあるのが気になってました。ポティッとな。

f:id:nodamushi:20220212152701p:plain

 で、出てきたファイルがこれ。

#pragma kernel CSMain
RWTexture2D<float4> Result;

[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    Result[id.xy] = float4(id.x & id.y, (id.x & 15)/15.0, (id.y & 15)/15.0, 0.0);
}

 ………あー。たぶん、 kernel CSMain がどの関数を実行するかを指定してるんだろうね。

 idが3次元(uint3)なのは上の8,8,1が何か関係が?直感的には8×8×1 = 64スレッド走ってるってことかな。3次元ボクセルを計算するときは最後の1を変えるんだろうな。

 でも、ならなんで id.x & 15 なんて必要なんだ?0-7にならないのか?うーん…?

 ふむ…、取りあえず、もっとガッツリ[numthreads(256,256,1)]ぐらいにしてみて………

 お?何かエラー出てる

f:id:nodamushi:20220212162429p:plain

 1024よりも少なくないとダメらしい………1024?少なくね?10万ノードぐらい行こうぜ?

 ということは、もう根本的に何か勘違いしてるっぽい。いい加減真面目に調べよう。

   

スレッドとグループ

 Microsoftのドキュメント見ても全く意味がわからんのはいつものこと。(MicrosoftのDocsってなんであんなに読みにくいんだろうな。どこ読めば良いのか全く分からないんだよなぁ)

f:id:nodamushi:20220212165302p:plain

 どうにも、スレッドはグループによって管理されていて、そのグループ自体も並列に動くのかな。グループは Dispatch によって数を指定するみたい。グループは各次元が65535まで指定可能らしい。なるほど。

 で、void CSMain (uint3 id : SV_DispatchThreadID)id: SV_DispatchThreadID は上の図を見ると、全体を通してのIDになるみたい。

 つまり、 id.x & 15 なんて必要なんだって思ったけど、グループの数によってはid.xは15を超えるって事だね。

 ふむ?何となくシェーダー側は分かってきたかな…?

コンピュートシェーダの結果をテクスチャとして貼り付ける

 作成したシェーダを動かすにはC#から Dispatch を起動しなくちゃいけないと言うことは、さっきのスレッドとグループを調べててわかりました。

 で、それをUnityでどうやって動かすんだ?結果もどうやって見れば良いんだ?うーん………とりあえず、テクスチャとして貼り付ければ良いかな。昔ながらのやり方だね。

 今回はひとまず、256x256のテクスチャを作って、適当な四角形ポリゴンに貼り付けてみることにしました。

 というわけで、適当にquadをシーンに追加して、カメラの目の前に配置。

f:id:nodamushi:20220212172656p:plain

 んで適当にTextureを貼り付けるためのマテリアルを追加。これにC#からテクスチャを設定すれば良いのだと思うけど、どうやるんだ?

f:id:nodamushi:20220212172827p:plain

 どうやら、 _MainTex にテクスチャを設定すればいけそう。

f:id:nodamushi:20220212172947p:plain

 で、なんとかかんとか調べながら書いたC#コードが以下。

using UnityEngine;

public class sample : MonoBehaviour
{
  [SerializeField] ComputeShader shader;
  private RenderTexture texture;

  void Start()
  {
    // テクスチャの作成。256x256。デプスバッファは無し
    texture = new RenderTexture(256, 256, 0);
    texture.enableRandomWrite = true; // テクスチャに書き込むのに必要らしい。
    texture.Create();

    // マテリアルの取得とテクスチャの設定
    var material = GetComponent<Renderer>().material;
    material.SetTexture("_MainTex", texture);

    // カーネルの実行
    var id = shader.FindKernel("puri");
    shader.SetTexture(id, "Output", texture);
    shader.Dispatch(id, 1, 64, 1);// グループ数 1 x 64 x 1
  }

  void OnDestroy()
  {
    texture.Release(); // テクスチャリソース破棄
  }

  void Update()
  {
  }
}

 

 まずは、以下の様にしてComputeShaderを受け取れるようにする。public じゃなくて[SerializeField]が良いらしい。 よく分かってなかったけど、Inspector から設定可能になるみたいです。(後述)

[SerializeField] ComputeShader shader;

 次に以下の様にしてコンピュートシェーダで書き込むテクスチャを作成して、シェーダーの _MainTex に突っ込めばいいっぽい。

private RenderTexture texture;
  void Start()
  {
    // テクスチャの作成。256x256。デプスバッファは無し
    texture = new RenderTexture(256, 256, 0);
    texture.enableRandomWrite = true; // テクスチャに書き込むのに必要らしい。
    texture.Create();

    // マテリアルの取得とテクスチャの設定
    var material = GetComponent<Renderer>().material;
    material.SetTexture("_MainTex", texture);
  }

  void OnDestroy()
  {
    texture.Release(); // テクスチャリソース破棄
  }

 

 コンピュートシェーダの方は取りあえずグラデーションを表示するだけの ぷりカーネルを定義してみました。

#pragma kernel puri
RWTexture2D<float> Output; // 256 x 256の画像

[numthreads(256, 4, 1)]
void puri(uint3 id: SV_DISPATCHTHREADID)
{
  Output[id.xy] = (float)(id.x + id.y) / 512;
}

 カーネル側ではスレッド数を 256x4x1 に指定してるので256x256の画像を処理をするには、1x64x1個のグループを実行すれば良いはずです。

    var id = shader.FindKernel("puri"); // カーネルの取得
    shader.SetTexture(id, "Output", texture); // テクスチャをコンピュートシェーダに結びつける
    shader.Dispatch(id, 1, 64, 1);// グループ数 1 x 64 x 1

 

 で、作成したC#をquadにアタッチしてゲーム実行してみると………

f:id:nodamushi:20220212173638p:plain

 はい、見事に失敗です。動かない。shaderが設定されてない?どうやって設定するの?

 と、調べると、[SerializeField] を指定すると以下の様に Inspector に表示されるので、ここから設定するらしい。そんなことも知らないゴミカスです。

f:id:nodamushi:20220212173738p:plain

f:id:nodamushi:20220212173913p:plain

 気を取り直して実行すると………

f:id:nodamushi:20220212191322p:plain

 おぉ、グラデーションが出てる

 

アニメーションして見る

 今度は2秒周期で色がグラデーションする ぷりぷりカーネルを追加しました。

#pragma kernel puri
RWTexture2D<float> Output; // 256 x 256の画像

[numthreads(256, 4, 1)]
void puri(uint3 id: SV_DISPATCHTHREADID)
{
  Output[id.xy] = (float)(id.x + id.y) / 512;
}

#pragma kernel puripuri
float Time;
[numthreads(256, 4, 1)]
void puripuri(uint3 id: SV_DISPATCHTHREADID)
{
  Output[id.xy] = frac(Time / 2);
}

 これをUpdateでDispatchすればきっとアニメーションするはず。

using UnityEngine;

public class sample : MonoBehaviour
{
  [SerializeField] ComputeShader shader;
  private RenderTexture texture;
  private int id;

  void Start()
  {
    texture = new RenderTexture(256, 256, 0);
    texture.enableRandomWrite = true; 
    texture.Create();
    var material = GetComponent<Renderer>().material;
    material.SetTexture("_MainTex", texture);

    // カーネルIDをフィールドにとって、Outputとtextureを結びつける
    id = shader.FindKernel("puripuri");
    shader.SetTexture(id, "Output", texture);
  }

  void OnDestroy()
  {
    texture.Release();
  }

  void Update()
  {
    // 時間を指定してDispatch
    shader.SetFloat("Time", Time.time);
    shader.Dispatch(id, 1, 64, 1);
  }
}

 で、動かしてみると………

f:id:nodamushi:20220212193249g:plain

 おぉ、動いたっ。

前の結果から次の結果を算出する

 ぷりぷりは時間から色を算出してたけど、良くあるシミュレーションは前の状態から次の状態を取得します。というわけで、前の状態を処理して次を色を計算してみるぷりぷりぷりぷりを作ってみます。

f:id:nodamushi:20220212201622p:plain

 Fixed Timestampは0.02なので、FixedUpdateは基本的には50fpsで呼ばれるハズ(本当はちゃんと時間情報も渡すべきですが)。

 

 なので、先ほどと同じように2秒で色が変化する様にするには0.01だけ前の値から変化すれば良いね。

#pragma kernel puripuripuri
[numthreads(256, 4, 1)]
void puripuripuri(uint3 id: SV_DISPATCHTHREADID)
{
  Output[id.xy] = frac(Output[id.xy] - 1.0 / 100.0 );
}

 C#側は Update は削除して、FixedUpdateを作る。

  void FixedUpdate()
  {
    shader.Dispatch(id, 1, 64, 1);
  }

 動かしてみた。

f:id:nodamushi:20220212201831g:plain

 OKOK………

 

入力と出力でダブルバッファリングすればいいのかね?

 ところで、ぷりぷりぷりぷりのこれ↓って自分で自分を更新する限りは問題ないけど、隣を参照する場合とか困るはずだよね?

Output[id.xy] = frac(Output[id.xy] - 1.0 / 100.0 );

 

 というわけで、以下の様なぷりぷりぷりぷりカーネルを追加しました。C#側は省略。

// 画像の初期化
#pragma kernel init
[numthreads(256, 4, 1)]
void init(uint3 id: SV_DISPATCHTHREADID)
{
  Output[id.xy] = step(1, frac((float)id.x /64) * 2) * step(1, frac((float)id.y /64) * 2);
}

#pragma kernel puripuripuripuri
[numthreads(256, 4, 1)]
void puripuripuripuri(uint3 id: SV_DISPATCHTHREADID)
{
  Output[id.xy] = Output[(id.xy - uint2(1, 1)) & (uint2(255, 255))];
}

f:id:nodamushi:20220212202724g:plain

 予想通り、最初は綺麗な四角形だったのに、徐々に壊れてますね。

f:id:nodamushi:20220212202611p:plain

f:id:nodamushi:20220212202630p:plain

 

 解決方法はたぶん、ダブルバッファリングすれば良いんですかね?

 まずは、ぷりぷりぷりぷりが入力画像を受け取れるようにします。

#pragma kernel puripuripuripuri

RWTexture2D<float> Input; // 256 x 256の画像
[numthreads(256, 4, 1)]
void puripuripuripuri(uint3 id: SV_DISPATCHTHREADID)
{
  Output[id.xy] = Input[(id.xy - uint2(1, 1)) & (uint2(255, 255))];
}

 C#側ではダブルバッファリングするようにします。

using UnityEngine;

public class sample : MonoBehaviour
{
  [SerializeField] ComputeShader shader;

  private RenderTexture[] texture;// 入力用、出力用
  private int id;// kernel id
  private int front;// 表示するtextureのインデックス(0 or 1)
  private Material material;

  void Start()
  {
    texture = new RenderTexture[2];
    front = 0;
    for (var i = 0; i < 2; i++)
    {
      texture[i] = new RenderTexture(256, 256, 0);
      texture[i].enableRandomWrite = true;
      texture[i].Create();
    }

    material = GetComponent<Renderer>().material;
    material.SetTexture("_MainTex", texture[front]);

    // カーネルの取得
    id = shader.FindKernel("puripuripuripuri");

    // 初期画像作成
    int init = shader.FindKernel("init");
    shader.SetTexture(init, "Output", texture[front]);
    shader.Dispatch(init, 1, 64, 1);
  }

  void OnDestroy()
  {
    texture[0].Release();
    texture[1].Release();
  }

  void FixedUpdate()
  {
    front = 1 - front; // 表示するテクスチャの入れ替え

    shader.SetTexture(id, "Output", texture[front]);
    shader.SetTexture(id, "Input", texture[1 - front]);
    shader.Dispatch(id, 1, 64, 1);

    material.SetTexture("_MainTex", texture[front]);
  }
}

 

 で、動かすとこうなった。

f:id:nodamushi:20220212204610g:plain

 いいね。

 ComputeShaderは何となく分かったかなぁ………。

 早く簡単なゲーム作れるぐらいにはなりたいんですが、初心者にはゲーム開発は難しいですね……。

Unityでシェーダープログラムに入門1: テッセレーション

 ゲームが作りたくて最近Unityを触っています。………のハズだったのですが、気がついたら入門書はchapter5で投げ出してずっとシェーダーばっかり弄ってます。どういうことだってばよ。

 特にテッセレーションを数日弄ってました。私がまだおっさんでなかった頃、OpenGLESを弄っていたときには無かったからね。……私がおっさんじゃなかった時期ってあったかな。

 はい、というわけで、初テッセレーションでサッパリワカランので記事にして理解度深めていこうかと。

テッセレーションシェーダ と ジオメトリシェーダ

 テッセレーションというのは、GPU側でポリゴンを分割して、新しい頂点を作ること。

 これをテッセレーションシェーダで行うことが出来る。

 ………ん?それってジオメトリシェーダでもできるんでは?とか思ったけど、以下の様な判断基準らしい。

● テッセレーションシェーダを使った方がいい場合

  1. 組込のテッセレーションパターンで満足できる場合
  2. 表面を定義する頂点数が6以上

● ジオメトリシェーダを使った方がいい場合

  1. 入力プリミティブから異なるプリミティブを出力したい場合(三角形から線とか点とか)

 テッセレーションシェーダのドメインシェーダで頂点レベルよりは細かいけどピクセルレベルよりは荒いシェーディング計算を行って、フラグメンテシェーダでは単に線形補完するだけという利用方法もある模様。

○参考

Tessellation Shaders Oregon State University Mike Bailey 51ページ目

テッセレーションシェーダ

 テッセレーションは「ハルシェーダー」→「テッセレータ(ハードウェア)」→「ドメインシェーダ」の順にデータが処理されます。

f:id:nodamushi:20220211082655p:plain

Graphics Pipelineより引用

 ハルシェーダーではテッセレータがポリゴンを分割するために必要な2つの出力「制御点」と「係数(定数)」の算出を行って、テッセレータに渡します。2つの出力があるので、hullシェーダは制御点を生成するシェーダと係数(定数)を算出するシェーダの2つを実装する必要があります。

f:id:nodamushi:20220211082821p:plain (Tessellation Stagesより引用)

 テッセレータは渡された制御点と係数から、ポリゴンを分割します。生成された新しい頂点は各頂点からの重みという形でドメインシェーダーに渡されるっぽい。

 ドメインシェーダーはハルシェーダとテッセレータから渡された情報を基に頂点の位置を計算します。

f:id:nodamushi:20220211082943p:plainTessellation Stagesより引用)

 とりあえずUnityで簡単にテッセレーションを動かしてみたかったら、以下のコードが手っ取り早いのかな?

Shader "Custom/sample1" {
  Properties {
    _TessFactor("TessFactor", Float) = 3
  }

  SubShader {
    Tags { "RenderType" = "Opaque" }

    CGPROGRAM
    #pragma surface surf Standard tessellate:tess vertex:disp

    // テッセレーション
    #include "Tessellation.cginc"

    float _TessFactor;

    // ハルシェーダの係数算出
    float4 tess(appdata_full v0, appdata_full v1, appdata_full v2) {
      return _TessFactor;
    }

    // ドメインシェーダに相当
    void disp(inout appdata_full v) { }

    // テッセレーション終わり

    struct Input {
      float2 uv_MainTex;
    };

    void surf(Input i, inout SurfaceOutputStandard o) {
      o.Albedo = fixed4(.1f, 1.0f, 1.0f, 1);
    }
    ENDCG
  }
  FallBack "Diffuse"
}

 青い球体のポリゴンが白い球体より細かくなっていますね。

f:id:nodamushi:20220211080633p:plain

 なお、ドメインシェーダ相当の disp で何もしてないので、テッセレーションしてるけど滑らかさに変化はありません。

f:id:nodamushi:20220211081011p:plain

 UnityのサーフェイスシェーダはPhongテッセレーションをしてくれる機能があるので、以下の様に有効化すると滑らかになります。

Shader "Custom/sample1" {
  Properties {
    _TessFactor("TessFactor", Float) = 3
    _Phong("Phong Strength", Range(0, 1)) = 0.5
  }

  SubShader {
    Tags { "RenderType" = "Opaque" }

    CGPROGRAM
    // tessphong:_Phongを追加
    #pragma surface surf Standard tessellate:tess vertex:disp tessphong:_Phong

    // テッセレーション
    #include "Tessellation.cginc"
    float _TessFactor;
    float _Phong; // Phong テッセレーションのための係数

// 以下同じなので省略

f:id:nodamushi:20220211081233p:plain

 おぉ、丸い丸い。

 しかし、サーフェイスシェーダ隠蔽されて hull シェーダとか domain シェーダがよくわからない………。よし、自分でPhong Tessellationを実装しよう。

 

Phong Tessellationを実装する

 Phong Tessellationの論文はこちら

 補正する前の点を各頂点の接平面に投射した点を制御点として補正後の点の位置を算出するという考え方らしい。

 そのままだと補正が効き過ぎるので、「α補正後 + (1-α)補正前」を最終的な結果とするみたい。というわけで、まんま論文の式を実装したのがこちら。

Shader "Custom/sample2" {
  Properties {
    _Phong("Phong Strength", Range(0, 1)) = 0.5
    _EdgeTessFactor("Tess Edge Factor", Range(0, 32)) = 2
    _InsideTessFactor("Tess Inside Factor", Range(0, 32)) = 2
  }

  SubShader {
    Tags { "RenderType" = "Opaque" }

    PASS {

      CGPROGRAM

      #pragma vertex vert
      #pragma hull hullMain
      #pragma domain domain
      #pragma fragment frag

      #include "UnityCG.cginc"
      #include "Tessellation.cginc"
      #define INPUT_PATCH_SIZE 3
      #define OUTPUT_PATCH_SIZE 3

      //---- Define structure ----

      struct appdata {
        //! 頂点
        float3 vertex : POSITION;
        //! 法線
        float3 normal : NORMAL;
      };

      // 頂点シェーダからhullシェーダに渡すデータ
      struct v2h {
        float3 pos: POS;
        float3 normal: NORMAL;
      };

      // コントロールポイント
      struct h2dMain {
        float3 pos: POS;
        float3 normal: NORMAL;
      };

      struct h2dConst {
        // テッセレーションの辺の係数
        float edgeFactor[3] : SV_TessFactor;
        // テッセレーションの内部分割係数
        float insideFactor : SV_InsideTessFactor;

        // Phongテッセレーション用データ
        float3 phongs[3]: POS;
      };

      // domainシェーダからフラグメントシェーダへ
      struct d2f {
        float4 pos: SV_Position;
        float3 normal: NORMAL;
      };

      // --------------------

      // 頂点シェーダ
      v2h vert(appdata i)
      {
        v2h o;
        o.pos = i.vertex;
        o.normal = i.normal;
        return o;
      }

      // テッセレーションシェーダ
      float _Phong; // Phong Tessellationのα
      float _EdgeTessFactor;
      float _InsideTessFactor;

      [domain("tri")]
      [partitioning("integer")]
      [outputtopology("triangle_cw")]
      [outputcontrolpoints(OUTPUT_PATCH_SIZE)]
      [patchconstantfunc("hullConst")]
      h2dMain hullMain(InputPatch<v2h, INPUT_PATCH_SIZE> i, uint id: SV_OutputControlPointID)
      {
        h2dMain o;
        o.pos = i[id].pos.xyz;
        o.normal = i[id].normal;
        return o;
      }

      // Phong Tessellationのπi(Pk) + πk(Pi)
      float3 funcPi(float3 pi, float3 ni, float3 pk, float3 nk)
      {
        float3 pik = pi - pk;
        return pi + pk + dot(pik, ni) * ni - dot(pik, nk) * nk;
      }

      h2dConst hullConst(InputPatch<v2h, INPUT_PATCH_SIZE> i)
      {
        h2dConst o;
        o.edgeFactor[0] = _EdgeTessFactor;
        o.edgeFactor[1] = _EdgeTessFactor;
        o.edgeFactor[2] = _EdgeTessFactor;
        o.insideFactor = _InsideTessFactor;

        o.phongs[0] = funcPi(i[0].pos, i[0].normal, i[1].pos, i[1].normal);
        o.phongs[1] = funcPi(i[0].pos, i[0].normal, i[2].pos, i[2].normal);
        o.phongs[2] = funcPi(i[1].pos, i[1].normal, i[2].pos, i[2].normal);
        return o;
      }

      // domainシェーダ
      [domain("tri")]
      d2f domain(h2dConst c, const OutputPatch<h2dMain, OUTPUT_PATCH_SIZE> i, float3 bary: SV_DomainLocation) {
        d2f o;
        float3 b2 = bary * bary;
        float3 pos_phong = i[0].pos * b2.x + i[1].pos * b2.y + i[2].pos * b2.z
                           + bary.x * bary.y * c.phongs[0]
                           + bary.x * bary.z * c.phongs[1]
                           + bary.y * bary.z * c.phongs[2];
        float3 pos_default = i[0].pos * bary.x + i[1].pos * bary.y + i[2].pos * bary.z;
        float3 pos = pos_phong * _Phong + pos_default * (1 - _Phong);
        o.pos = UnityObjectToClipPos(pos);
        o.normal = normalize(i[0].normal * bary.x + i[1].normal * bary.y + i[2].normal * bary.z);
        return o;
      }

      float4 frag(d2f i): SV_Target {
        return float4(1.0f, 1.0f, 0.1f, 1);
      }
      ENDCG
    }
  }
  FallBack "Diffuse"
}

 適応した結果は黄色い球です。サーフェイスシェーダと同じように輪郭が丸く綺麗になっていますね。よっしゃよっしゃ。

f:id:nodamushi:20220211094239p:plain

適応的テッセレーションを作る前にWireFrameシェーダを付けておく

 現状はテッセレーション係数を固定にしてるので、オブジェクトが遠かろうが近かろうが同じ分割をしてしまいます。

f:id:nodamushi:20220211165353p:plain

 ディスプレースメントマッピングをしてない現状、正直輪郭以外は大して重要じゃないので球の真ん中部分は分割数を減らし、ついでに画面ベースでも点が近い場合は分割を減らすべきですね。

 ついでにやってみましょう………と思ったのですが、UnityのSceneビューで中ボタンをスクロールすると一気にズームするので変化を観察しにくいですね………。良い方法ないんでしょうか?Chapter5で投げ出しただけあって、Unity力が低すぎますね

 わからないので、とりあえず手っ取り早くゲームでカメラを動かすことにしました。カメラの移動には 【Unity】カメラ移動を制御するスクリプト - Qiita を使用しました。

 しかし、GameビューだとWireFrameが表示されないので、分割がどうなってるのかサッパリ見えないですねぇ。困った。

f:id:nodamushi:20220211170313p:plain

 しょーがないので、とりあえず WireFrame を追加で実装しました。参考にしたのはこちら(nvidia White Paper Solid Wireframe)

 全体は省略しますが、以下の様にジオメトリシェーダを追加し、フラグメントシェーダで辺の描画をします。

 ジオメトリシェーダでは、各頂点に対辺までのスクリーンスペースでの距離を付加し、フラグメントシェーダでは距離から線を描画するか、黄色を描画するかを切り替えるだけです。

 なお、判断にstep+lerpではなく、clamp+lerpを使ってるので、アンチエイリアスは勝手にかかります。

      [maxvertexcount(3)]
      void geom(triangle d2g i[3], inout TriangleStream<g2f> triangleStream)
      {
        half2 p0 = ComputeScreenPos(i[0].pos).xy / i[0].pos.w * _ScreenParams.xy;
        half2 p1 = ComputeScreenPos(i[1].pos).xy / i[1].pos.w * _ScreenParams.xy;
        half2 p2 = ComputeScreenPos(i[2].pos).xy / i[2].pos.w * _ScreenParams.xy;

        half2 e0 = p1 - p0;
        half2 e1 = p2 - p1;
        half2 e2 = p0 - p2;

        half2 n0 = normalize(e0);
        half2 n1 = normalize(e1);
        half2 n2 = normalize(e2);

        g2f o;
        o.pos = i[0].pos;
        o.dist = float3(0, distance(e0, dot(e0, n1) * n1), 0);
        o.normal = i[0].normal;
        triangleStream.Append(o);

        o.pos = i[1].pos;
        o.dist = float3(0, 0, distance(e1, dot(e1, n2) * n2));
        o.normal = i[1].normal;
        triangleStream.Append(o);

        o.pos = i[2].pos;
        o.dist = float3(distance(e2, dot(e2, n0) * n0), 0, 0);
        o.normal = i[2].normal;
        triangleStream.Append(o);
      }


      float _LineWidth;

      float4 frag(g2f i): SV_Target {
        float dist = min(i.dist[0], min(i.dist[1], i.dist[2]));
        float d = clamp(dist, 0, _LineWidth) / _LineWidth;
        return lerp(float4(0.0f, 0.0f, 0.1f, 1), float4(1, 1, 0.1, 1), d);
      }

f:id:nodamushi:20220211171045p:plain

 わーい、ゲームビューでもワイヤーフレームが綺麗に表示されたよ。

適応的テッセレーションを作ってみる

 視線ベクトルと、法線ベクトルの内積を用いて係数を操作するだけ。テッセレーション係数に0を渡してしまうと、破棄しまうので、clampで適当な値で0にならないようにしています。

      h2dConst hullConst(InputPatch<v2h, INPUT_PATCH_SIZE> i)
      {
        h2dConst o;
        half3 camLocal = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos, 1));
        half3 cam01 = normalize((i[0].pos + i[1].pos) * 0.5 - camLocal);
        half3 cam12 = normalize((i[1].pos + i[2].pos) * 0.5 - camLocal);
        half3 cam02 = normalize((i[0].pos + i[2].pos) * 0.5 - camLocal);
        half3 n01 = normalize(i[0].normal + i[1].normal);
        half3 n12 = normalize(i[1].normal + i[2].normal);
        half3 n02 = normalize(i[2].normal + i[0].normal);

        half3 d = 1 - abs(half3(dot(n12, cam12), dot(n02, cam02), dot(n01, cam01)));
        half3 f = clamp(d * d, 0.01, 1) * _EdgeTessFactor;

        o.edgeFactor[0] = f[0];
        o.edgeFactor[1] = f[1];
        o.edgeFactor[2] = f[2];
        o.insideFactor = (f[0] + f[1] + f[2]) / 3;

        o.phongs[0] = funcPi(i[0].pos, i[0].normal, i[1].pos, i[1].normal);
        o.phongs[1] = funcPi(i[0].pos, i[0].normal, i[2].pos, i[2].normal);
        o.phongs[2] = funcPi(i[1].pos, i[1].normal, i[2].pos, i[2].normal);

        return o;
      }

f:id:nodamushi:20220211181643g:plain

 (はてなブログWebPに対応してないってどういうことよ。Zennに移行しようかなぁ……)

 出来てみるとたったこれだけなんだけど、実はメチャクチャ苦労した。

half3 camLocal = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos, 1));

 ここを私は最初UnityWorldToObjectDirを使っていて、ちゃんと変換できていなかったのです。

half3 camLocal = UnityWorldToObjectDir(_WorldSpaceCameraPos);

 法線を色として出力したり色々頑張って、やっと気がついたよ………。一度ハマるとプリント デバッグ出来ないのが辛いところだね。

FMP Capsule Update FailとでてWindows Updateが終わらない場合

 ThinkPad X1 Carbon Gen9をWindows 11にした後のWindows Updateで、「FMP Capsule Update Fail」と表示され、1時間経っても全く進まずどう見てもハングアップしてる状況になりました。

追記: 2/15日に再発しました

f:id:nodamushi:20220210053454p:plain:w320

 このエラーで調べると出てきた公式サポートでは「電源を落とせ」とかいうロクでもないことが書かれています。マジかよ。

Workaround

  1. Turn the system off.
  2. Turn the system on.
  3. Issue should no longer be present.

 覚悟を決めて電源ボタンを長押しするも、シャットダウンせず。完全に無反応状態。

 こうなるともはや、電池を完全に放電させ切るか、ノートパソコンには大抵どこかにある強制リセット端子を触る必要があります。せっかちなので私は強制リセット端子を触りました。目の悪い私には穴が見えないで探すのに苦労しましたが………。

 ThinkPad X1 Carbon Gen9の場合はここにあります。ゼムクリップとか何か針金を突っ込んで端子に触りましょう。

f:id:nodamushi:20220210054356p:plain:w320

  1. 電源など全部外す
  2. ひっくり返す
  3. 針金を刺す

 これで再起動すると、ひとまずの所、(突然再起動が起こったり、エクスプローラーがやたら重たくなったり色々ありましたが)動いております。