LoginSignup
15
15

More than 1 year has passed since last update.

VRChatterのためのシェーダーの基本、完全に理解した

Last updated at Posted at 2021-12-16

はじめに

こんにちは、ひら吉です。

最近、noteに一般的な記事ばかり書いていて
エンジニア系の記事は久々の投稿になります。
※本業はインフラ系ITエンジニア(Linux/Cisco/Juniperを使ったサーバ・ネットワーク設計)。

さて、今日は趣味のVRChatに関係する記事を書きますよ。

記事の対象者は、私のような野良プログラマー向けです。
(軽めのプログラムを書けるくらいの方)

VRChatって?

最近、有名な「メタバース」の一種です。

メタバース (英: Metaverse) は、コンピュータやコンピュータネットワークの中に構築された現実世界とは異なる3次元の仮想空間やそのサービスのことを指す[1]。将来的にインターネット環境が到達するであろうコンセプトで、利用者はオンライン上に構築された3DCGの仮想空間に世界中から思い思いのアバターと呼ばれる自分の分身で参加し、相互にコミュニケーションしながら買い物やサービス内での商品の制作・販売といった経済活動を行なったり、そこをもう一つの「現実」として新たな生活を送ったりすることが想定されている[2]。(Wikipediaより)

つまり「インターネットのアバターを使ったコミュニケーション(雑談・遊びなど)のための場所!」です。

VRChatをもっと楽しむならUnityとBlender必須!

コミュニケーションのためにUnityとBlender?と思うかもしれないですね。
Unityはゲーム開発ツール、Blenderは3Dモデリングツールです。

でも実は、Unityがあると、自分の好きなアバターをVRChatで使うことができます。

  • そのアバターかっこいいね/かわいいね
  • どこで買ったの?

そう言われたら嬉しいですよね。だから、ますます自分のアバターをアップしようとするんです!

さらに!

Blenderを使えば、アバターをゼロから作ることすらできます。
実はBoothというサイトでアバターを購入する人がほとんどですが、
中には自分でゼロから作ってしまうつわものも稀にいます。

モデリングは、芸術的センス・経験が必要なので、経験がないと難しいと思いますが、
特に絵画創作経験があると比較的容易にアバターを作ってしまえるように見受けられます。

そう、そして彼らは神として崇められるのです…(半分冗談・半分本気)

そんな世界で一体のアバターを作れたら最高ですよね。

※私は絵画創作経験もなく、アバターのような曲線系の複雑なモデルは作れなかったので、
 単純な直線主体の温泉モデルくらいは作っていまして、Boothで公開しています。

[温泉街の猫の店]
https://hirakichi.booth.pm/items/1765812

そしてシェーダー入門

そして、VRChatで更に楽しむ要素として「シェーダー」があります。
シェーダーとは、物体表面の模様の描画方法を記述する方法の事です。

なんのこっちゃと思うかもしれませんが、
・アバターに色を付けたり
・反射させたり
・もしくは色が動いたり
するのがシェーダーの役割なんです。

見た目はプログラミングとそっくりなのですが、考え方が全然違うのでさっぱりでした。
普通にUnityやBlenderを使うのに比べて、別次元の難しさがありますね。

そこで今回、改めて学ぶことにしたのでその軌跡をまとめておきます。
完全に理解したと書いておきながら、どこまで理解できたのやら(笑)

このページが一番分かりやすかった

こちらのページが有名だと思います。

「7日間でマスターするUnityシェーダ入門」
https://nn-hokuson.hatenablog.com/entry/2018/02/15/140037

image.png

こんな感じでリンクが張られているので
「上から順に全部理解していくこと」
が大事だと思います。

そうすると理解しやすかったです。
これが私の勉強法。

なお、他にもいろいろと参考書などはありますが、私は上記ページが一番分かりやすかったです。
手っ取り早く理解するにはお勧めのページです。
(と言っても、全然手っ取り早く理解できないですけど(笑)積み重ねが大事。)

とりあえずSurfaceシェーダーだけ書こう

最初からいろいろやろうとしても混乱するだけです。
Surfaceシェーダーだけ書くことを意識しましょう。

後段のLightingなどはまた慣れてきたら勉強すればよいと思います。

その辺りの基本原則は以下のページに書かれています。

【Unityシェーダ入門】Unityのシェーダで遊んでみよう
https://nn-hokuson.hatenablog.com/entry/2016/09/12/202320

簡単にまとめると、書き方の基本は、

  • Unityのシェーダーのプロパティから値やテクスチャを設定したいなら、Propertiesタグを使う
  • struct Inputに、Surfaceシェーダー内で使いたい値の変数(例えばワールド座標、テクスチャなど)を用意する
  • void surf関数の中で、Surfaceシェーダーの挙動を記述する

です。

まず基本を作ってみましょう

とりあえずStandard Surface Shaderを作成して、Shader01という名前を付けます。

image.png

image.png

そしてMaterialを作ってMat01と命名します。

image.png

image.png

そして適当な平面(Plane)を設置し、そこにMat01をドラッグ&ドロップします。

image.png

この段階だと、Mat01のシェーダーがStandardになっているはずなので、
先ほど作成したShader01に差し替えましょう。
※自分で作ったシェーダーはCustomの下にあります。

image.png

image.png

次はShader01の中身を触るために、Shader01をダブルクリックしてください。Visual Studioが立ち上がるはずです。

image.png

うえぇ~プログラミング?と思ったあなた。
きっとこの時点で嫌になったと思います(笑)

そう見た目はプログラミングそのものなのです。
この言語は「HLSL言語」と呼ばれるそうです。

Shader "Custom/Shader01"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _Metallic ("Metallic", Range(0,1)) = 0.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        // Physically based Standard lighting model, and enable shadows on all light types
        #pragma surface surf Standard fullforwardshadows

        // Use shader model 3.0 target, to get nicer looking lighting
        #pragma target 3.0

        sampler2D _MainTex;

        struct Input
        {
            float2 uv_MainTex;
        };

        half _Glossiness;
        half _Metallic;
        fixed4 _Color;

        // Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
        // See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
        // #pragma instancing_options assumeuniformscaling
        UNITY_INSTANCING_BUFFER_START(Props)
            // put more per-instance properties here
        UNITY_INSTANCING_BUFFER_END(Props)

        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            // Albedo comes from a texture tinted by color
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            // Metallic and smoothness come from slider variables
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = c.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

でも、それでも基本を理解すればそんなに難しくはないので、堪え忍びましょう…。

さて、構造的には下図のイメージでそこまで難しくないと思います。

image.png

SurfaceシェーダーのInput構造体で使える値については、
Unityの以下のマニュアルに記載されています。
※VRChatで利用可能なUnity2018.4向けのマニュアル。
https://docs.unity3d.com/ja/2018.4/Manual/SL-SurfaceShaders.html

この辺りはよく使うと思われる値です。

  • テクスチャ座標の名前は、uv の後にテクスチャ名が来る形にする必要があります
  • float3 worldPos - ワールド座標
  • float3 viewDir - ビュー方向

各パラメータの意味合いについては、以下のページも
分かりやすかったので、必要に応じてご参照ください。
https://atmarkit.itmedia.co.jp/ait/articles/1801/05/news010.html

何が難しいかというとテクニックが随所に登場する点

透明

例えば、以下のページ。

【Unityシェーダ入門】氷のような半透明シェーダを作る
https://nn-hokuson.hatenablog.com/entry/2016/10/07/221724

この半透明を実現するために、このページの著者は次のアルゴリズムで実装しています。

ドラゴンの輪郭部分の透明度が低くなっているのに対して、中央部分の透明度は高くなって

つまり輪郭ほど濃く、中央部ほど薄くなる計算にすればよいのです。
それを実現するために、

viewDirとworldNormalの内積

を使っています。これがまさにテクニックです。

内積は、数学的には、viewDir・worldNormal = |viewDir|*|worldNormal|*cos(viewDir方向からworldNormal方向の角度)になりますが、なんのこっちゃと思うと思います。

  • 視線方向と法線方向が同じ場合は(中心に近いから)薄くする = cosの値が1に近くなる = Alphaを0に近づける
  • 視線方向と法線方向が違うほど(輪郭に近いから)濃くする = cosの値が0に近くなる = Alphaを1に近づける

だから、色の薄さを表すパラメータ o.Alpha に 1-|viewDir・worldNormal|
の結果を設定すれば実現できます。
(※cosはマイナスにもなるが、ここでは内積の大きさを使いたいので絶対値を使っている)

o.Alpha = 1-(abs(dot(IN.viewDir, IN.worldNormal)));

…数学が得意な方であれば理解できたと思います。
でもなかなかこういうのは理解が難しいです。

だから「暗記」で良いんです。
透明にするときはこの式を使う。

o.Alpha = 1-(abs(dot(IN.viewDir, IN.worldNormal)));

それで良いんです。これがまさにテクニック。

ゲームのお仕事でシェーダー作成をしてきた知人から言わせると、
シェーダーは「他の人が公開しているシェーダーをコピペして使うもの」だそうです。

だから、きっとそういうものなのだろうと思います。

波打つ表現

【Unityシェーダ入門】円やリングをかっこよく動かす方法
https://nn-hokuson.hatenablog.com/entry/2016/11/14/203745

こちらのページから抜粋したコードは下記のとおりです。

        void surf (Input IN, inout SurfaceOutputStandard o) {
            float dist = distance( fixed3(0,0,0), IN.worldPos);
            float val = abs(sin(dist*3.0));
            if( val > 0.98 ){
                o.Albedo = fixed4(1, 1, 1, 1);
            } else {
                o.Albedo = fixed4(110/255.0, 87/255.0, 139/255.0, 1);
            }
        }

このコードを読み解くと

  • 原点(0,0,0)から各座標IN.worldPosの距離distを計算する
  • distの値をsinに入れて、sinカーブの0.98-1の範囲の場合だけ、fixed4(1,1,1,1)の色=白で描画、
  • それ以外はfixed4(110/255.0,87/255.0,139/255.0,1)=紫で描画する

という形でコードを記述しているのが分かります。

この

  • sinカーブの0.98-1の範囲の場合だけ

というのがキーになっていて、これが同心円の表現部分になります。

なお、上の状態だと、円が同心円状に描かれるだけなので、_Timeを使うと波打たせることができます。

float val = abs(sin(dist*3.0-_Time*100));

_TimeはUnityで定義された関数で、ステージがロードされてからの経過時間だそうです。
https://kurotorimkdocs.gitlab.io/kurotorimemo/030-Programming/Shader/UnityShaderSnippet/

ちなみに、-_Time*100とするのか、+_Time*100とするのかで、波の動く方向が反対になります。
(マイナスの場合、中心から外側へ波が出ていく。プラスの場合、外から中心に集まる。)
こんな感じで表現されます(この動画はマイナスの場合(-_Time*100))。

おまけ「シェーダーは保存しないと反映されないよう」

シェーダーを変更しても反映されねえなと思っていたのですが、
Visual Studio側でセーブしないとダメなんですね。

UdonSharpを書くときはAttachしていたので、Attachすれば反映されると思っていましたが、
シェーダーの場合は、Attachではなく保存が必要だそうです。

ランダム性の実装

【Unityシェーダ入門】シェーダで作るノイズ5種盛り
https://nn-hokuson.hatenablog.com/entry/2017/01/27/195659

おもしろいですね。
まさかランダム関数が用意されていないので自分で用意する必要があるとは。

        float random (fixed2 p) { 
            return frac(sin(dot(p, fixed2(12.9898,78.233))) * 43758.5453);
        }

UV座標の値を元に、Alberoの値(色)を設定すれば色にノイズを乗せられるようです。

        void surf (Input IN, inout SurfaceOutputStandard o) {
            float c = random(IN.uv_MainTex);
            o.Albedo = fixed4(c,c,c,1);
        }

次にライティングも触ってみよう

【Unityシェーダ入門】トゥーンシェーダを自作してみる
https://nn-hokuson.hatenablog.com/entry/2017/03/27/204255

このページを見ると、ライティングの記述方法が解説されています。
ライティングになると一段難しくなりますね…。

とりあえずシェーダーを書きたい場合は、
Surfaceシェーダーまでで良いとは思いますが、
アバター向けのシェーダはライティングも
考慮した方が映えそうです。

頂点シェーダー(Vertex Shader)も使ってみる

頂点シェーダーを使うと

  • 平面が波打つ表現

のような海面のようなものを作るときに使えるようです。

そのためにVert関数をハックして、そこに独自関数を記述します。

【Unityシェーダ入門】シェーダで旗や水面をなびかせる
https://nn-hokuson.hatenablog.com/entry/2017/04/04/201537

考え方としては、上下方向(y方向)の座標をsin関数で上下させるイメージです。
そうすると表示として上下しているように見えます。

上のSurfaceシェーダーにこの頂点シェーダーの処理を入れるときの注意点は
まず「頂点シェーダーをハックするよ(上書きするよ)」という以下の一文が必要になるそうです。

  #pragma vertex:vert

その上で、vert関数の実装です。

        void vert(inout appdata_full v, out Input o )
        {
            UNITY_INITIALIZE_OUTPUT(Input, o);
            float amp = 0.5*sin(_Time*100 + v.vertex.x * 100);
            v.vertex.xyz = float3(v.vertex.x, v.vertex.y+amp, v.vertex.z);            
        }

こちら、上記ページよりそのまま引っ張ってきたコードですが、
これで波打ちますね。こんな感じになりました。

Dissolveシェーダとtex2D

「【Unityシェーダ入門】Dissolve(溶けるような)シェーダをつくる」
https://nn-hokuson.hatenablog.com/entry/2017/04/14/204822

このページの溶けるシェーダーは比較的簡単でした。

  • ノイズとなるテクスチャを用意する
  • ノイズテクスチャの濃淡に応じて、実際にモデルに張り付けるテクスチャの表示部分を決める(ノイズテクスチャの濃い部分だけを表示するイメージ。濃いかどうかの閾値はThreshold(スレッショルド、閾値)で調整可能。)
            fixed4 m = tex2D (_DisolveTex, IN.uv_MainTex);
            half g = m.r * 0.2 + m.g * 0.7 + m.b * 0.1;

しかし読み解くうえでなんじゃこりゃ?というのがtex2Dという関数。

調べてみると

tex2D関数は、UV座標(uv_MainTex)からテクスチャ(_MainTex)上のピクセルの色を計算して返します。
らしいです。
(引用元 https://qiita.com/kaiware007/items/ffe7c546bc71136cf8da)
ということで、単純に色を取得する関数のようです。

また half の式はどうも「グレースケール変換」を行っているそうです。
こういうのもテクニックですよね。
RGB値それぞれに定数をかけている式を見ても、それだけだと意味は分からないので
シェーダー特有のノウハウのような気がしました。

結局数学必要じゃない?そんなことはないよ。

はい、私もシェーダーを書いていて、数学の知識が必要だなーと思いました。
(ちゃんと理解して完全に未知のシェーダーを生み出すなら、数学の知識が必要だとは思いますが、
普通に使う分には必要ないと思いました。)

例えば、

  • sinやcosが-1~1の値を取ること
  • sinやcosのグラフを書くとカーブを描くこと(横軸x, 縦軸y=sin(x)としたときにカーブのグラフになる)
  • ベクトルの内積の計算式と、内積をとって内積ゼロのときは直交ベクトルになっている

等。この辺りの知識がないと理解は難しいと思います。

でも問題ないと思います。

上にも書いた通り「他の人のシェーダープログラムを参考(パクる?)」にすれば良いんです。

シェーダーに限った話ではないですが、プログラミングは他のより良質なプログラムを書く人を
真似ることで成長します。

だから、思い描いたシェーダーに近いシェーダーを見つけて、
それを真似ることこそが、シェーダーを上手に使いこなすコツだと思いました。

勉強の仕方

まず基本的なシェーダーの構造(HLSL言語の構造)を理解したうえで、
上で紹介した通り「透明」「波打つ表現」のような表現のテクニックを学ぶのが近道だと思います。

そのテクニックは、Webで「シェーダー サンプル」のような形で検索すると出てくると思いますし、
以下のshadertoyのページもいろいろとサンプルがあって参考になりそうです。

慣れてきたら、自身で数式とグラフを見ながら、こんなものが作れそうだーという発想をしていくと、
新しいシェーダーを作れるんでしょうね~と思いました。

シェーダーの参考ページ

例えば粘性を表現する数式が紹介されていたりします。
まぁ正直これを数式で書くって神業ですよね。
数式をここまで感覚的に操れるのはすごい。

他にもいろいろとシェーダーの書き方があるのでこのページを参考に、
期待する表現に近づけると良さそうです。

最後に

私がぜんぜんシェーダーが分からなかったので少しまじめに取り組んでみました。
知っている人にとっては当たり前でも、
当たり前の知識がないのが初学者には辛いところなので、
まとめてみたところです。

もし参考になれば幸いです。

ご意見等あれば頂けば幸いです。

というわけで記事を書きながら焼いていたパンが焼けたので
食べたいと思います。

IMG_20211127_231615.jpg

それではまたーひら吉でした。

15
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
15