rn.log

備忘録など

【Compute Shader メモ 2】中心差分法を利用してノーマルマップを作る

はじめに

Compute Shader で何かを作るシリーズ 第二弾!

今回は、Compute Shader で ノーマルマップ(法線マップ)を作ってみたいと思います。

f:id:r-ngtm:20210120000231p:plain:h240
実行結果 (1024x1024のハイトマップからノーマルマップを計算)

ノーマルマップ(法線マップ)とは

ノーマルマップとは、3Dモデルの法線情報を画像に保存したものです。

f:id:r-ngtm:20210119214419p:plain
ノーマルマップ

平面メッシュにノーマルマップを適用することで、あたかも凹凸が付いているように陰影をつけることができます。

f:id:r-ngtm:20210119214336p:plain
ノーマルマップを適用することで、凹凸が付いて見える

ノーマルマップによって陰影がつく原理

Unityは物体をレンダリングするときに、物体表面の法線を見て凹んでいるかどうかを判断しています。
(法線は黄色の線で表示しています)

f:id:r-ngtm:20210119221045p:plain:w480
凹凸のある面の法線はバラバラになる

ノーマルマップを使うと、物体の法線を再現することができ、陰影をつけることができます。

f:id:r-ngtm:20210119221358p:plain:w480
法線によって陰影がつく

Unityは物体をレンダリングするときに、物体の法線を見て凹んでいるかどうかを判断しています。
そのため、法線がバラつくとあたかもその面には凹凸が付いているかのように陰影が付くわけです。

Heightマップからノーマルマップを作る

今回、以下のような高低マップ(Heightマップ)から法線マップを作りたいと思います。

f:id:r-ngtm:20210119232127p:plain:w240
パーリンノイズ

以下のような法線マップを作ります。 (下記テクスチャはSubstance Designer で作ったノーマルマップになります)

f:id:r-ngtm:20210119232316p:plain:w240
ノーマルマップ

Heightマップからノーマルマップを求める方法

今回は中心差分法を利用して、Heightマップから勾配を求め、そこから法線を計算します。

f:id:r-ngtm:20210119211927p:plain:w320
法線ベクトルnは勾配の外積で計算

求めた法線情報はテクスチャ(ノーマルマップ)に保存します。



中心差分法で勾配を求める

ここで、連続な曲線 f(x) 上の接線(勾配)を求めることを考えます。

f:id:r-ngtm:20210119215214p:plain:w400
ある点での接線の傾きは、両隣の点の座標の差分で近似できる

ある点での接線の傾き(勾配)は、以下の式で近似できます。(中心差分法)
 \dfrac{ \partial f }{ \partial x } = \dfrac{ y_{i + 1} - y_{i - 1} }{ 2 \Delta x } = \dfrac{ f(x_{i + 1}) - f(x_{i - 1}) }{ 2 \Delta x }

参考 : http://www.icehap.chiba-u.jp/activity/SS2016/textbook/SS2016_miyoshi_FD.pdf



勾配ベクトルを求める

Hiehgtマップの座標(x,y)に保存されている高さ情報をf(x,y)とすると、以下のような図になります。

f:id:r-ngtm:20210119210754p:plain:w640
あるピクセルとその隣接ピクセルを可視化

中心差分法を利用すると二つの勾配ベクトル(\Delta x, 0, \Delta f_x ) , (0, \Delta y, \Delta f_y ) を求めることができます。

f:id:r-ngtm:20210119231430p:plain:w640
差分から勾配を求めることができる

外積を利用して法線を求める

法線ベクトル \vec{n} はこれら2つの勾配の外積を利用すると計算することができます。
 \vec{n} (- \Delta y \Delta f_x,  - \Delta x \Delta f_y,  \Delta x \Delta y ) に平行になります。

f:id:r-ngtm:20210119211927p:plain:w640
法線ベクトルnは外積で計算

 \Delta x = 2, \Delta y = 2と置いたとき、
 \vec{n} (- \Delta f_x,  - \Delta f_y,  2 ) に平行になります。

Compute Shader で実装

#pragma kernel CSMain

RWTexture2D<float4> Result; // ノーマルマップの書き込み先
Texture2D<float4> HeightMap; // Heightマップ

[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{    
    // Heightマップの差分
    float dfx = (HeightMap[id.xy + uint2(1, 0)].r - HeightMap[id.xy + uint2(-1, 0)].r);
    float dfy = (HeightMap[id.xy + uint2(0, 1)].r - HeightMap[id.xy + uint2(0, -1)].r);
    
    // 法線
    float3 n = float3(-dfx, -dfy, 2.0);
    n = normalize(n);
    
    // 範囲[-a,a]を[0,1]に変換してからテクスチャに書き込む    
    float a = 0.02;
    n = (n / a + 1.0)  * 0.5;

    Result[id.xy] = float4(n, 1.0);
}

Compute Shader を実行する C#スクリプト

using UnityEngine;
using UnityEngine.UI;

public class ComputeNormal : MonoBehaviour
{
    [SerializeField] private ComputeShader shader; // 実行するComputeShader
    [SerializeField] private Texture _texture; // 入力画像
    [SerializeField] private RawImage targetImage; // RenderTextureを割り当てる対象のRawImage
    private RenderTexture _renderTexture;
    private int kernel;
    
    private void Start()
    {
        // RenderTexture作成
        _renderTexture = new RenderTexture(_texture.width, _texture.height, 0);
        _renderTexture.enableRandomWrite = true;
        _renderTexture.Create();
        
        // 実行したいカーネル(関数のようなもの)
        kernel = shader.FindKernel("CSMain"); 
        
        // ComputeShaderへデータを渡す
        shader.SetTexture(kernel, "HeightMap", _texture); // 元となるテクスチャ
        shader.SetTexture(kernel, "Result", _renderTexture); // 書き込み先のテクスチャ
        
        // スレッドグループサイズの取得
        uint sizeX, sizeY, sizeZ;
        shader.GetKernelThreadGroupSizes(kernel, out sizeX, out sizeY, out sizeZ);
        
        shader.Dispatch(kernel, _texture.width / (int)sizeX, _texture.height / (int)sizeY, 1 / (int)sizeZ);

        targetImage.texture = _renderTexture;
    }
}

実行結果

f:id:r-ngtm:20210120000231p:plain
実行結果 (1024x1024のハイトマップからノーマルマップを計算)