2Dなのに立体が描ける!
コード
学校の課題で作りました
RaymarchingPS.hlsl
#include "Common.hlsl"
// 解像度
static float2 RESOLUTION = float2(540, 540);
// カメラの回転速度
static float CAMERA_SPEED = 0.2;
// カメラの座標
static float3 CAMERA_POS = float3(2.0, 2.0, -2.0);
// カメラの注視点
static float3 CAMERA_TARGET = float3(0.0, 0.0, 0.0);
// 箱のサイズ
static float BOX_SIZE = 1.0;
// 背景色
static float3 BACKGROUND_COLOR = float3(0.2, 0.2, 0.2);
// 箱の距離関数
float box(float3 p, float b)
{
float3 d = abs(p) - b;
return min(max(d.x, max(d.y, d.z)), 0.0) + length(max(d, 0.0));
}
// ビュー行列の計算
float3x3 calcLookAtMatrix(float3 origin, float3 target)
{
float3 w = normalize(target - origin);
float3 u = normalize(cross(float3(0.0, 1.0, 0.0), w));
float3 v = normalize(cross(u, w));
return float3x3(u, v, w);
}
// 法線の計算
float3 calcNormal(float3 pos)
{
float2 e = float2(1.0, -1.0) * 0.5773;
const float eps = 0.0005;
return normalize(
e.xyy * box(pos + e.xyy * eps, BOX_SIZE) +
e.yyx * box(pos + e.yyx * eps, BOX_SIZE) +
e.yxy * box(pos + e.yxy * eps, BOX_SIZE) +
e.xxx * box(pos + e.xxx * eps, BOX_SIZE)
);
}
[earlydepthstencil]
PS_OUTPUT main(PS_INPUT input)
{
PS_OUTPUT output;
// 画面の中心を原点として、-1.0~1.0の範囲の座標を取得
float2 uv;
uv = input.TexCoord * 2.0 - 1.0;
uv.y *= RESOLUTION.y / RESOLUTION.x;
// カメラ
float speed = CTime * CAMERA_SPEED;
float3 rayOrigin = float3(CAMERA_POS.x * sin(speed), CAMERA_POS.y, CAMERA_POS.z * cos(speed));
float3x3 viewMatrix = calcLookAtMatrix(rayOrigin, CAMERA_TARGET);
// レイの向きを計算
float3 rayDirection = normalize(mul(float3(uv, 1.0), viewMatrix));
// レイマーチング
float3 color = BACKGROUND_COLOR;
float3 rayPos = rayOrigin;
for (int i = 0; i < 256; i++)
{
// 距離関数で距離を取得
float distance = box(rayPos, BOX_SIZE);
// 距離が十分小さくなったら終了
if (distance < 0.0001)
{
// 拡散反射
float3 normal = calcNormal(rayPos);
float diffuse = saturate(dot(normal, float3(0.0, 1.0, 0.0)));
color.rgb = saturate(diffuse + 0.5);
break;
}
// 距離を加算
rayPos += rayDirection * distance;
}
output.Color = float4(color, 1.0);
return output;
}
レイマーチングの理解
Shadertoyや記事を参考に(コピペもしつつ)学習を進めました。
アウトプットも兼ねて書いておこうと思います。
今回は1つの形状のみを描画しました。
板ポリ
やってることは2Dの板ポリ1枚を描画するだけです。3Dのポリゴンに対してやってるのかなーと思ってたので驚きました。
レイマーチング
こちらの記事が分かりやすかったです。
- PixelShaderでUV(TEXCOORD)の値から、対象のピクセルを求める
- ピクセルの座標とカメラの座標から、レイの方向ベクトルを求める
- 形状との距離を測り、その距離だけレイを伸ばす
- 3を繰り返して距離が0以下になる、つまり形状に接触したらそのピクセルを塗りつぶす
UV値はピクセルのワールド座標のような扱いをします。3D空間にz=0の板ポリがあり、それにカメラを向けているイメージです。空間をイメージしづらく、ここで少し詰まりました。
カメラ
// カメラの座標
static float3 CAMERA_POS = float3(2.0, 2.0, -2.0);
HLSLは、GLSLと異なり左手座標系です。zがマイナスになるように配置します。
// ビュー行列の計算
float3x3 calcLookAtMatrix(float3 origin, float3 target)
{
float3 w = normalize(target - origin);
float3 u = normalize(cross(float3(0.0, 1.0, 0.0), w));
float3 v = normalize(cross(u, w));
return float3x3(u, v, w);
}
さらにオブジェクトの周りを回転させています。それには、PixelShader内でカメラのビュー行列を求める必要があります。(DirectXのXMMatrixLookAtLH関数と同じことをしました)
// レイの向きを計算
float3 rayDirection = normalize(mul(float3(uv, 1.0), viewMatrix));
レイのベクトルとカメラのビュー行列を乗算することにより、回転させた場所からレイを飛ばしたことになります。
レイマーチングでの回転や移動の操作は、レイの座標に対して行うみたいです。形状を複数描画するならその分必要です。ここも理解に時間がかかりました。
距離関数(SDF)
// 箱の距離関数
float box(float3 p, float b)
{
float3 d = abs(p) - b;
return min(max(d.x, max(d.y, d.z)), 0.0) + length(max(d, 0.0));
}
レイの座標を渡すことで形状との距離が返ってきます。レイと接触する場合は0以下の値が返ってくるので、そのピクセルだけを塗りつぶすと形状が描画できます。形状は(0, 0, 0)に位置しているように見えます。
他の記事でも紹介されているこちらからコピペさせていただきました。
ライティング
法線の計算も他の作品をコピペさせていただきました。
正直まだ完全に理解できてないです。
アレンジ
これでひとまず立方体の描画ができました。
ここからは課題に合わせてアレンジした内容になります。
レイの進め方
今回こちらの作品を真似て作り始めました。
透過部分を描画するには形状の内部までレイを進める必要があります。
そのためレイを進める距離をあらかじめ計算し、それに沿ってレイマーチングを行っています。
ノイズを混ぜる
fbmノイズでレイの座標の値を計算し、一定の値以上でなければ描画しないようにしました。これにより形状の内部がノイズに沿った形で描画されます。
時間での変化
コンスタントバッファで時間を渡し、ノイズにまぜてシードを変えたりカメラを回転させています。
色
最後に唐突な思い付きで球の距離関数と組み合わせ、氷漬けのスイカっぽい色にしてみました。
完成...
Raymarching_2.hlsl
#include "Common.hlsl"
#include "Noise.hlsl"
// 解像度
static float2 RESOLUTION = float2(540, 540);
// カメラの回転速度
static float CAMERA_SPEED = 0.2;
static float3 CAMERA_POS = float3(1.8, 1.8, -1.8);
// カメラの注視点
static float3 CAMERA_TARGET = float3(0.0, 0.0, 0.0);
// 箱の角の丸さ
static float BOX_ROUND = 0.1;
// 箱のサイズ
static float BOX_SIZE = 1.0;
// サンプリング距離
static float SAMPLE_DISTANCE = 4.0;
// サンプリング数
static int SAMPLE_COUNT = 256;
// 背景色
static float3 BACKGROUND_COLOR = float3(0.2, 0.2, 0.2);
// 箱の色
static float3 BOX_COLOR_1 = float3(0.3, 0.6, 0.9);
static float3 BOX_COLOR_2 = float3(0.8, 0.8, 0.9);
// 球の色
static float3 SPHERE_COLOR_1 = float3(0.9, 0.2, 0.1);
static float3 SPHERE_COLOR_2 = float3(0.8, 0.9, 0.8);
static float3 SPHERE_COLOR_3 = float3(0.1, 0.4, 0.1);
static float3 SPHERE_COLOR_4 = float3(0.1, 0.1, 0.1);
// 球の閾値
static float SPHERE_THRESHOLD_1 = 0.8;
static float SPHERE_THRESHOLD_2 = 0.9;
static float SPHERE_THRESHOLD_3 = 0.99;
static float SPHERE_THRESHOLD_4 = 0.3;
// 拡散反射光の色
static float3 DIFFUSE_COLOR = float3(-0.6, 0.6, -0.6);
// 環境光の色
static float3 AMBIENT_COLOR = float3(0.3, 0.2, 0.1);
// 箱の距離関数
float box(float3 p, float b, float r)
{
float3 d = abs(p) - b;
return min(max(d.x, max(d.y, d.z)), 0.0) + length(max(d, 0.0)) - r;
}
// 球の距離関数
float sphere(float3 p, float s)
{
return length(p) - s;
}
// ノイズ入り距離関数
float map(float3 p)
{
const float OFFSET = -10.2;
float noise = fbm3(p + CTime / 5.0, 2.0) + OFFSET;
float distance = box(p, BOX_SIZE, BOX_ROUND);
return (noise > distance) ? noise : distance;
}
// 法線の計算
float3 calcNormal(float3 pos)
{
float2 e = float2(1.0, -1.0) * 0.5773;
const float eps = 0.0005;
return normalize(
e.xyy * map(pos + e.xyy * eps) +
e.yyx * map(pos + e.yyx * eps) +
e.yxy * map(pos + e.yxy * eps) +
e.xxx * map(pos + e.xxx * eps)
);
}
// ビュー行列の計算
float3x3 calcLookAtMatrix(float3 origin, float3 target)
{
float3 w = normalize(target - origin);
float3 u = normalize(cross(float3(0.0, 1.0, 0.0), w));
float3 v = normalize(cross(u, w));
return float3x3(u, v, w);
}
[earlydepthstencil]
PS_OUTPUT main(PS_INPUT input)
{
PS_OUTPUT output;
// 画面の中心を原点として、-1.0~1.0の範囲の座標を取得
float2 uv;
uv = input.TexCoord * 2.0 - 1.0;
uv.y *= RESOLUTION.y / RESOLUTION.x;
// カメラ
float speed = CTime * CAMERA_SPEED;
float3 rayOrigin = float3(CAMERA_POS.x * sin(speed), CAMERA_POS.y, CAMERA_POS.z * cos(speed));
float3x3 viewMatrix = calcLookAtMatrix(rayOrigin, CAMERA_TARGET);
// レイの向きを計算
float3 rayDirection = normalize(mul(float3(uv, 1.0), viewMatrix));
// レイマーチング
float zMax = SAMPLE_DISTANCE;
float zStep = zMax / float(SAMPLE_COUNT);
float z = 0.0;
float3 color = BACKGROUND_COLOR;
// レイの座標
float3 rayPos;
for (int i = 0; i < SAMPLE_COUNT; i++)
{
rayPos = rayOrigin + z * rayDirection;
// 距離関数で距離を取得
float h = map(rayPos);
// 距離が十分小さくなったら終了
if (h < 0.0001 || z > zMax)
{
break;
}
// 距離を加算
z += zStep;
}
// レイの座標が最大値を超えていない場合
if(z < zMax)
{
// 模様
float3 baseColor;
if (sphere(rayPos, SPHERE_THRESHOLD_1) < 0.0)
{
float pattern = perlinNoise3(rayPos * 5) * 0.5 + 0.5;
baseColor = pattern > SPHERE_THRESHOLD_4 ? SPHERE_COLOR_1 : SPHERE_COLOR_4;
}
else if (sphere(rayPos, SPHERE_THRESHOLD_2) < 0.0)
{
baseColor = SPHERE_COLOR_2;
}
else if (sphere(rayPos, SPHERE_THRESHOLD_3) < 0.0)
{
baseColor = SPHERE_COLOR_3;
}
else
{
float pattern = fbm3(rayPos * 5, 3) * 0.5 + 0.5;
baseColor = lerp(BOX_COLOR_1, BOX_COLOR_2, pattern);
}
// 拡散反射光
float3 normal = calcNormal(rayPos);
float diffuse = saturate(dot(normal, DIFFUSE_COLOR));
// 環境光
float ambient = dot(normal, float3(0.0, 1.0, 0.0)) * 0.5 + 0.5;
ambient *= AMBIENT_COLOR;
const float OFFSET = 0.1;
color = baseColor * (diffuse + OFFSET) + ambient;
}
output.Color = float4(color, 1.0);
したのですがこのシェーダー、とっても重いです。具体的には解像度540x540の上、Releaseビルドで実行してもノートPCのGTX1650Tiが使用率100%になってしまいます。if文を減らしたり、アルゴリズムを見直したりと改良の余地が多く、今後も色々試してみたいと思います。