のぐそんブログ

暗いおじさんがシコシコ書くブログです。

TouchDesignerでトゥーンシェーディング

はじめに

こちらはTouchDesigner Study Weekend Vo.010に参加した際に学んだことをまとめています。

運営の方含め、とても良い勉強会でおすすめです。
あくまで自分の復習の為に、まとめ直しています。

実際はもっと、有益な情報を沢山学ぶことができます。
勉強会の内容を撮影した動画も購入できるみたいなのです!
購入はこちらから

考え方

トゥーンシェーディングは、輪郭の検出が重要です。
検出する為に、カメラのviewベクトルと、オブジェクトの法線情報を利用します。

この2つの内積が90度以上であれば輪郭とします。

内積とは

正しく理解するには、私ではむずかしいのですが、以下の説明がわかりやすかったです。

内積は2つのベクトルが、どのくらい同じ向きを向いているかを表す量である」 大人になってからの再学習

ちなみにGLSLでは内積算出用のビルドイン関数があります。

dot(x,y);// xとyの内積(負の値になることもある)

基本の構成

初めの構成は以下です。
glsl MATに色々追加していきます。

f:id:nogson2:20181021163945p:plain

頂点座標、法線情報をフラグメントシェーダーに送る

輪郭などに色をつけるのはフラグメントシェーダーの役割なので、頂点シェーダーは頂点座標と法線情報をわたすだけです。

◎glsl1_vertex

out vec3 worldSpaceNorm;
out vec4 worldSpacePos;

void main() 
{
    worldSpacePos = TDDeform(P);
    gl_Position = TDWorldToProj(worldSpacePos);

    worldSpaceNorm = normalize(TDDeformNorm(N));

}

◎glsl1_pixel

in vec3 worldSpaceNorm;
in vec4 worldSpacePos;

out vec4 fragColor;
void main()
{
    TDCheckDiscard();
    vec4 color = vec4(1.0);
    TDAlphaTest(color.a);
    fragColor = TDOutputSwizzle(color);
}

カメラの位置情報を取得する

ここがちゃんと理解できなかったのですが、公式のドキュメントをみると、↓のような記述があるので、これをつかうのだと思う。

camInverseはTDの行列みたいなのですが、なぜInverseするのかわかりませんでした。

// The last column of the camera transform is it's position
vec4 worldSpaceCamPos = uTDMat.camInverse[3]; 

あと、公式のドキュメントに以下のコメントも書いてあったので、uTDMat ではなくuTDMats[0]を利用する。

Major changes since TouchDesigner088

uTDMat has been removed when lighting in World Space, use the array uTDMats[] instead.

◎glsl1_pixel

in vec3 worldSpaceNorm;
in vec3 worldSpacePos;

out vec4 fragColor;
void main()
{
    TDCheckDiscard();
    
    vec3 viewVec = uTDMats[0].camInverse[3].xyz;

    vec4 color = vec4(1.0);
    TDAlphaTest(color.a);
    fragColor = TDOutputSwizzle(color);
}

out vec4 fragColor;
void main()
{
    TDCheckDiscard();
    
    vec3 viewVec = uTDMats[0].camInverse[3].xyz;

    vec4 color = vec4(1.0);
    TDAlphaTest(color.a);
    fragColor = TDOutputSwizzle(color);
}

ライトの位置を取得

影や、色の濃淡を出すためにはライトの情報も必要です。

◎glsl1_pixel

in vec3 worldSpaceNorm;
in vec3 worldSpacePos;

out vec4 fragColor;
void main()
{
    TDCheckDiscard();
    
    vec3 viewVec = uTDMats[0].camInverse[3].xyz;
    vec3 lightVec = uTDLights[0].position.xyz;

    vec4 color = vec4(1.0);
    TDAlphaTest(color.a);
    fragColor = TDOutputSwizzle(color);
}

単位ベクトルを算出する

取得したカメラ、ライトの位置情報を利用して単位ベクトルを作っていきます。
単位ベクトルを取得するにはnormalize関数を利用します。

◎glsl1_pixel

in vec3 worldSpaceNorm;
in vec4 worldSpacePos;

out vec4 fragColor;
void main()
{
    TDCheckDiscard();

    vec3 viewVec = uTDMats[0].camInverse[3].xyz;
    vec3 lightVec = uTDLights[0].position.xyz;

    vec3 L = normalize(lightVec - worldSpacePos.xyz);
    vec3 V = normalize(viewVec - worldSpacePos.xyz);

    vec4 color = vec4(1.0,);
    TDAlphaTest(color.a);
    fragColor = TDOutputSwizzle(color);
}

色をつけるためのuniform変数を用意する

着色用にuniform変数uDiffuseColorをglsl MATに定義します。

◎glsl1_pixel

uniform vec3 uDiffuseColor;
in vec3 worldSpaceNorm;
in vec4 worldSpacePos;

out vec4 fragColor;
void main()
{
    TDCheckDiscard();
    
    vec3 diffuseColor = uDiffuseColor;
    vec3 viewVec = uTDMats[0].camInverse[3].xyz;
    vec3 lightVec = uTDLights[0].position.xyz;

    vec3 L = normalize(lightVec - worldSpacePos.xyz);
    vec3 V = normalize(viewVec - worldSpacePos.xyz);

    vec4 color = vec4(1.0,);
    TDAlphaTest(color.a);
    fragColor = TDOutputSwizzle(color);
}

ライトの単位ベクトルをつかって色をつくる

難しい概念はわかりませんが、内積が大きい(法線と、ライトのベクトルが同じ方向をみている)場合はより、色は白(0.0,0.0,0.0)になります。

なのでライトと法線の内積の値をdiffuseColorに掛けてあげます。
ただし、内積は負の値になる場合もあるのでmax関数を利用して、負の値は0にします。

//xとyで大きい方を返す。
max(x,y)

◎glsl1_pixel

uniform vec3 uDiffuseColor;
in vec3 worldSpaceNorm;
in vec4 worldSpacePos;

out vec4 fragColor;
void main()
{
    TDCheckDiscard();

    vec3 diffuseColor = uDiffuseColor;
    vec3 viewVec = uTDMats[0].camInverse[3].xyz;
    vec3 lightVec = uTDLights[0].position.xyz;

    vec3 L = normalize(lightVec - worldSpacePos.xyz);
    vec3 V = normalize(viewVec - worldSpacePos.xyz);

    float diffuse = max(0.0,dot(L,worldSpaceNorm));

    diffuseColor = diffuseColor * diffuse;

    vec4 color = vec4(1.0);
    TDAlphaTest(color.a);
    fragColor = TDOutputSwizzle(color);
}

表示は↓の感じになります。

輪郭を黒で塗る

輪郭を描く場合は、頂点座標の単位ベクトルと、法線の単位ベクトルの内積を利用します。
内積が小さければ輪郭とします。

↓では、(dot(V,worldSpaceNorm) > 0.2) ? 1.0:0.0;として、内積の値が0.2未満の場合は輪郭にします。

uniform vec3 uDiffuseColor;
in vec3 worldSpaceNorm;
in vec4 worldSpacePos;

out vec4 fragColor;
void main()
{
    TDCheckDiscard();

    vec3 diffuseColor = uDiffuseColor;
    vec3 viewVec = uTDMats[0].camInverse[3].xyz;
    vec3 lightVec = uTDLights[0].position.xyz;

    vec3 L = normalize(lightVec - worldSpacePos.xyz);
    vec3 V = normalize(viewVec - worldSpacePos.xyz);

    float diffuse = max(0.0,dot(L,worldSpaceNorm));

    diffuseColor = diffuseColor * diffuse;

    float edgeDetection = (dot(V,worldSpaceNorm) > 0.2) ? 1.0:0.0;

    vec4 color = vec4(diffuseColor * edgeDetection,1.0);
    TDAlphaTest(color.a);
    fragColor = TDOutputSwizzle(color);
}

ここまででこんな感じになります。

平面的に塗っていく

ここまでだと、塗がグラデーションになっており、トゥーンシェーディングぽくない。
塗りの階調を間引くことで、トゥーンシェーディングぽい塗にしていく。

階調を間引くためには、floot関数を利用する。

//x以下の最大の整数を返す
floor(x)

floor関数を利用すると、細かい値を間引いた値を取得することができます。

例)
floor(1.5) = 1;
floor(1.9) = 1;
floor(2.0) = 2;

↓の処理で階調化しているところはfloor(diffuse * level) / levelの箇所です。

uniform vec3 uDiffuseColor;
in vec3 worldSpaceNorm;
in vec4 worldSpacePos;

out vec4 fragColor;
void main()
{
    TDCheckDiscard();

    int level = 6;

    vec3 diffuseColor = uDiffuseColor;
    vec3 viewVec = uTDMats[0].camInverse[3].xyz;
    vec3 lightVec = uTDLights[0].position.xyz;

    vec3 L = normalize(lightVec - worldSpacePos.xyz);
    vec3 V = normalize(viewVec - worldSpacePos.xyz);

    float diffuse = max(0.0,dot(L,worldSpaceNorm));

    diffuseColor = diffuseColor * floor(diffuse * level) / level;

    float edgeDetection = (dot(V,worldSpaceNorm) > 0.2) ? 1.0:0.0;

    vec4 color = vec4(diffuseColor * edgeDetection,1.0);
    TDAlphaTest(color.a);
    fragColor = TDOutputSwizzle(color);
}

diffuse だけでは値が小さすぎて0の値ばかりになってしまう為、level変数をかけて値を大きくします。
その上で、floor(diffuse * level)で少数以下の値を省き、大きくなってしまった値を0.0 ~ 1.0の値に戻しくます。

※floorの使い方については、GLSLでモザイク処理がわかりやすいです。

これでトゥーンシェーディングぽい表示になりました。

スペキュラを追加

スペキュラぽい処理をいれて、ハイライトを調整する。

仕組みは理解できなかったのだが、↓の式を利用するらしい。

//L ・・・ライトベクトル
//V ・・・頂点ベクトル
//worldSpaceNorm ・・・法線
pow(max(0,dot(L + V, worldSpaceNorm)),2)

◎glsl1_pixel

uniform vec3 uDiffuseColor;
in vec3 worldSpaceNorm;
in vec4 worldSpacePos;

out vec4 fragColor;
void main()
{
    TDCheckDiscard();

    int level = 6;

    vec3 diffuseColor = uDiffuseColor;
    vec3 viewVec = uTDMats[0].camInverse[3].xyz;
    vec3 lightVec = uTDLights[0].position.xyz;

    vec3 L = normalize(lightVec - worldSpacePos.xyz);
    vec3 V = normalize(viewVec - worldSpacePos.xyz);

    float diffuse = max(0.0,dot(L,worldSpaceNorm));

    diffuseColor = diffuseColor * floor(diffuse * level) * (1.0 / level);

    float specular = 0.0;

    if(dot(L,worldSpaceNorm) > 0.0){
        specular = 0.01 * pow(max(0,dot(L + V, worldSpaceNorm)),2);
    }

    float edgeDetection = (dot(V,worldSpaceNorm) > 0.2) ? 1.0:0.0;

    vec4 color = vec4(diffuseColor * edgeDetection + specular,1.0);
    TDAlphaTest(color.a);
    fragColor = TDOutputSwizzle(color);
}

まとめ

今回の方法の場合は、輪郭がオブジェクトの内側に食い込むので少し小さく見えてしまうようです。
その他の方法だと、オブジェクトの背面に少し大きめのオブジェクトをおいてそれを輪郭として使うなどの方法があるようです。