はじめに
Compute Shader で何かを作るシリーズ 第二弾!
今回は、Compute Shader で ノーマルマップ(法線マップ)を作ってみたいと思います。
ノーマルマップ(法線マップ)とは
ノーマルマップとは、3Dモデルの法線情報を画像に保存したものです。
平面メッシュにノーマルマップを適用することで、あたかも凹凸が付いているように陰影をつけることができます。
ノーマルマップによって陰影がつく原理
Unityは物体をレンダリングするときに、物体表面の法線を見て凹んでいるかどうかを判断しています。
(法線は黄色の線で表示しています)
ノーマルマップを使うと、物体の法線を再現することができ、陰影をつけることができます。
Unityは物体をレンダリングするときに、物体の法線を見て凹んでいるかどうかを判断しています。
そのため、法線がバラつくとあたかもその面には凹凸が付いているかのように陰影が付くわけです。
Heightマップからノーマルマップを作る
今回、以下のような高低マップ(Heightマップ)から法線マップを作りたいと思います。
以下のような法線マップを作ります。 (下記テクスチャはSubstance Designer で作ったノーマルマップになります)
Heightマップからノーマルマップを求める方法
今回は中心差分法を利用して、Heightマップから勾配を求め、そこから法線を計算します。
求めた法線情報はテクスチャ(ノーマルマップ)に保存します。
中心差分法で勾配を求める
ここで、連続な曲線 f(x) 上の接線(勾配)を求めることを考えます。
ある点での接線の傾き(勾配)は、以下の式で近似できます。(中心差分法)
参考 : http://www.icehap.chiba-u.jp/activity/SS2016/textbook/SS2016_miyoshi_FD.pdf
勾配ベクトルを求める
Hiehgtマップの座標(x,y)に保存されている高さ情報をf(x,y)とすると、以下のような図になります。
中心差分法を利用すると二つの勾配ベクトル , を求めることができます。
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; } }