シェーダ勉強会
第1回
@nojima
■ 対象
– シェーダを書いたことがない人
■ 目標
– Unity上で簡単なシェーダを書いてレンダリングしてみる
マテリアル
オブジェクトを描画するプログラム
マテリアル
=描画に必要なパラメータ+
オブジェクトを描画するプログラム
マテリアル
=描画に必要なパラメータ+
シェーダ
と呼ぶ
オブジェクトを描画するプログラム
マテリアル
=描画に必要なパラメータ+
シェーダ
と呼ぶ
基本色、滑らかさ、金属っぽさ
など
既存のシェーダを使ってみる
Create → Material
でマテリアルを作る
Standard Shader
Standard Shader の
パラメータ
Albedo = 基本色
Metallic = 金属なら1、
非金属なら0
Smoothness = 表面がどれ
だけつるっとしているか
金の球ができた!
レンダリングパイプライン
頂点シェーダ
クリッピング
カリング
ラスタライズ
フラグメント
シェーダ
アルファテスト
深度テスト
ステンシルテスト
アルファブレンド
各頂点を画面のどこに
描画するかを計算する
画面外のポリゴンや
裏向きのポリゴンを破棄
ポリゴンをピクセルに
分割する
各ピクセルの色を計算する
ピクセルシェーダともいう
ピクセルを出力するか決める
アルファブレンドを行う
入力
出力(画像)
シェーダを書いてみる
Create → Shader → Unlit Shader
ダブルクリック
Shader "Unlit/FirstShader" {
SubShader {
Tags { "RenderType"="Opaque" }
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
float4 vert(float4 pos : POSITION) : SV_POSITION {
return UnityObjectToClipPos(pos); // 座標変換
}
float4 frag() : SV_TARGET {
return float4(0.22, 0.71, 0.55, 1.0); // 翡翠色
}
ENDCG
}
}
}
適用してみる
① Create → Material
② Unlit/FirstShader を選択
① オブジェクトを選択
② FirstMaterial を割り当てる
レンダリングされた!
Shader "Unlit/FirstShader" {
SubShader {
Tags { "RenderType"="Opaque" }
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
float4 vert(float4 pos : POSITION) : SV_POSITION {
return UnityObjectToClipPos(pos); // 座標変換
}
float4 frag() : SV_TARGET {
return float4(0.22, 0.71, 0.55, 1.0); // 翡翠色
}
ENDCG
}
}
}
HLSL
#pragma vertex vert
#pragma fragment frag
float4 vert(float4 pos : POSITION) : SV_POSITION {
return UnityObjectToClipPos(pos); // 座標変換
}
float4 frag() : SV_TARGET {
return float4(0.22, 0.71, 0.55, 1.0); // 翡翠色
}
頂点
シェーダ
フラグメント
シェーダ
頂点シェーダとフラグメントシェーダ
■ 頂点シェーダとフラグメントシェーダはそれぞれひとつの関数として書く
– 関数名は #pragma vertex と #pragma fragment で宣言する
■ 頂点シェーダとフラグメントシェーダの引数と戻り値はセマンティクスを持つ
– セマンティクスはその値の意味を表す
– 例えば、POSITION はローカル座標、SV_POSITION はクリップ空間の座標、
SV_TARGET は出力するピクセルの色
■ 頂点シェーダは少なくとも SV_POSITION を返さないといけない。
■ フラグメントシェーダは少なくとも SV_TARGET を返さないといけない。
ベクトル型
■ float4 は4次元ベクトル(float が4本束ねてあるもの)を表す型
– float2, float3, int3, uint2 など、ベースとなる型 + 次元という
命名規則
■ ベクトルの要素には .x や .y などでアクセスする
– 例: v.x
■ .xyz のような記法で複数の要素をまとめて新しいベクトルを作れる
– 例: v.xyz, v.rgb, v.xyxy
■ .xyzw と .rgba は同じ意味
行列型
■ float4x4 は4x4の行列を表す型
– ベースとなる型 + 行x列 という命名規則
■ 行列と行列の掛け算、行列とベクトルの掛け算は mul() という関数を使う
#pragma vertex vert
#pragma fragment frag
float4 vert(float4 pos : POSITION) : SV_POSITION {
return UnityObjectToClipPos(pos); // 座標変換
}
float4 frag() : SV_TARGET {
return float4(0.22, 0.71, 0.55, 1.0); // 翡翠色
}
頂点
シェーダ
フラグメント
シェーダ
セマンティクス
頂点シェーダの役割
■ 頂点シェーダは頂点ひとつにつき一回呼び出される
■ 頂点シェーダは、以下の3つの座標変換を行う
1. ワールド変換
ローカル空間(モデル固有の座標系)からワールド空間(シーンの座標系)への
変換
2. ビュー変換
ワールド空間からビュー空間(カメラから見た座標系)への変換
3. パースペクティブ変換
ビュー空間からクリッピング空間(画面上の描画位置に対応する座標系)へ
の変換
float4 vert(float4 pos : POSITION) : SV_POSITION {
return UnityObjectToClipPos(pos); // 座標変換
}
頂点
シェーダ
float4 vert(float4 pos : POSITION) : SV_POSITION {
pos = mul(UNITY_MATRIX_M, pos); // ワールド変換
pos = mul(UNITY_MATRIX_V, pos); // ビュー変換
pos = mul(UNITY_MATRIX_P, pos); // パースペクティブ変換
return pos;
}
以下のコードと等価
float4 vert(float4 pos : POSITION) : SV_POSITION {
pos = mul(UNITY_MATRIX_M, pos); // ワールド変換
pos = mul(UNITY_MATRIX_V, pos); // ビュー変換
pos = mul(UNITY_MATRIX_P, pos); // パースペクティブ変換
return pos;
}
ローカル座標系 ワールド座標系
float4 vert(float4 pos : POSITION) : SV_POSITION {
pos = mul(UNITY_MATRIX_M, pos); // ワールド変換
pos = mul(UNITY_MATRIX_V, pos); // ビュー変換
pos = mul(UNITY_MATRIX_P, pos); // パースペクティブ変換
return pos;
}
カメラから見た座標系に変換
(カメラを原点、カメラが向いている方向がz軸になるように変換)
float4 vert(float4 pos : POSITION) : SV_POSITION {
pos = mul(UNITY_MATRIX_M, pos); // ワールド変換
pos = mul(UNITY_MATRIX_V, pos); // ビュー変換
pos = mul(UNITY_MATRIX_P, pos); // パースペクティブ変換
return pos;
}
far plane
near plane
far plane
near plane
camera
ビュー座標系 正規化デバイス座標系(NDC)
※ 実際には、一次変換ではNDCに変換できないため、頂点シェーダはクリップ座標系に変換する。
あとでGPUがクリップ座標系からNDCに変換する。
頂点シェーダの出力
■ 頂点シェーダは、入力された頂点の座標をクリップ座標系に変換した座標を返す
■ 頂点シェーダは、それ以外にもフラグメントシェーダが必要とする情報を
好きな数だけ返すことができる
– 複数の値を返す場合は struct に詰めて返すか、out 引数を使う
– 例えば、頂点の色、法線ベクトル、テクスチャ座標など
– GPUによっては8個までしか返せない場合がある
フラグメントシェーダの役割
■ フラグメントシェーダはポリゴンのピクセルひとつにつき一回呼び出される
■ フラグメントシェーダは、そのピクセルの色を出力する
– 色は4次元ベクトルとして表現される (RGBA)
シェーダのプロパティ
Shader "Unlit/FirstShader" {
Properties {
_Color("Color of the surface", Color) = (0.22, 0.71, 0.55, 1.0)
}
SubShader {
Tags { "RenderType"="Opaque" }
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
float4 _Color;
float4 vert(float4 pos : POSITION) : SV_POSITION {
return UnityObjectToClipPos(pos); // 座標変換
}
float4 frag() : SV_TARGET {
return _Color;
}
ENDCG
}
}
}
色を自由に変えれる!
Properties {
_Color("Color of the surface", Color) = (0.22, 0.71, 0.55, 1.0)
}
変数名 表示名 種類 デフォルト値
陰影をつける
Lambert Model
Lambert 反射モデル
物体の表面に入射した光が
あらゆる方向に同じ強さで反射するとしたモデル
𝑰 𝐷 = 𝝆 max(𝑳 ∙ 𝑵, 0) 𝑰 𝐿
入射光の強度物体の色反射光の強度
入射光の方向 表面の法線
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
float4 _BaseColor;
float4 _LightDir;
struct VertexInput {
float4 pos : POSITION;
float3 normal : NORMAL;
};
struct VertexOutput {
float4 clipPos : SV_POSITION;
float3 worldPos : TEXCOORD0;
float3 normal : TEXCOORD1;
};
VertexOutput vert(VertexInput v) {
VertexOutput o;
o.clipPos = UnityObjectToClipPos(v.pos);
o.worldPos = mul(unity_ObjectToWorld, v.pos);
o.normal = mul(v.normal,
(float3x3)unity_WorldToObject);
return o;
}
float4 frag(VertexOutput i) : SV_TARGET {
float3 lightDir = normalize(
UnityWorldSpaceLightDir(i.worldPos));
float3 normal = normalize(i.normal);
float LN = max(dot(lightDir, normal), 0.0);
float3 color = _BaseColor * LN;
return float4(color, 1.0);
}
VertexOutput vert(VertexInput v) {
VertexOutput o;
o.pos = UnityObjectToClipPos(v.pos);
o.worldPos = mul(unity_ObjectToWorld, v.pos);
o.normal = mul(v.normal, (float3x3)unity_WorldToObject);
return o;
}
ローカル座標系の法線ベクトルをワールド座標系の法線ベクトルに変換する
説明のためにベタ書きしたが、UnityObjectToWorldNormal() という関数が用意されているので、
実際はそれを使ったほうがいい
float4 frag(VertexOutput i) : SV_TARGET {
float3 lightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
float3 normal = normalize(i.normal);
float LN = max(dot(lightDir, normal), 0.0);
float3 color = _BaseColor * LN;
return float4(color, 1.0);
}
𝑰 𝐷 = 𝝆 max(𝑳 ∙ 𝑵, 0) 𝑰 𝐿
入射光の強度物体の色反射光の強度
入射光の方向 表面の法線
下の式にしたがって反射光を計算している(コードの簡略化のために 𝑰 𝐿 = 𝟏 とした)
陰影がついた!

シェーダ勉強会(第1回) Unityでシェーダを書いてみる回