原点から遠ざかったカメラが単精度浮動小数点(Float型)の精度問題で揺れる現象の解決手法

# 概要

コンピュータグラフィックスにおいて、カメラの視点が原点から遠ざかるにつれて画面の揺れが発生する。これは座標の管理に使われる単精度浮動小数点の精度不足が原因で、原点$(0,0,0)$から遠ざかるにつれ座標の値が精度を失うことによって発生する。本手法は、カメラ座標系を常に原点$(0,0,0)$に設定し、このカメラを基準としてプリミティブなどを相対座標で描画計算することで、精度損失を抑えるものである。

https://x.com/chihirobelmo/status/1864270995479220320

# 提案手法
提案する方法では、以下の手順を採用する：

1. **カメラ位置を座標系の原点へ移動する**
シェーダーに渡すカメラの座標を常に原点$(0,0,0)$として定義する。

2. **各描画対象に相対座標への変換を行う**
各プリミティブのワールド座標から原点へ移動する前のカメラ座標を減算し、カメラ相対座標系に変換する。

この手法により、ワールド座標が極端に大きな値を持つ場合でも、計算精度の損失を最小限に抑えることが可能となる。

# 具体的な変更例

次の例では、カメラ座標およびプリミティブなどのシェーダー上での座標を、カメラを原点とした座標系に変更する。

## シェーダーに渡すビュー行列の変更と、ワールド座標におけるカメラ位置の管理

```diff
    // シェーダーに渡すカメラ情報のバッファ
    CameraBuffer bf;
    // カメラ座標：eyePos_と　注視点：lookAtPos_　はワールド座標
+   // ビュー行列の計算において、カメラ座標を　0　と考えて、注視点をカメラ座標からの相対座標とする：
-   bf.view = XMMatrixTranspose(XMMatrixLookAtLH(eyePos_, lookAtPos_, Up));
+   bf.view = XMMatrixTranspose(XMMatrixLookAtLH(XMVectorSet(0.0, 0.0, 0.0, 1.0), XMVectorSubtract(lookAtPos_, eyePos_), Up));
    // 本論とは関係ないがZバッファを逆転
    bf.projection = XMMatrixTranspose(XMMatrixPerspectiveFovLH(vFov_ * (XM_PI / 180), aspectRatio, far_, near_));
+   // 実際のカメラ座標は別途持っておく
    bf.cameraPosition = eyePos_;
 ```

 ## プリミティブ描画の変更
 ```diff
 struct VS_INPUT {
    float3 Position : POSITION;
    float2 TexCoord : TEXCOORD0;
    float3 Normal : NORMAL;
    float4 Color : COLOR;
};

struct PS_INPUT {
    float4 Position : SV_POSITION;
    float4 Worldpos : POSITION;
    float2 TexCoord : TEXCOORD0;
    float3 Normal : NORMAL;
    float4 Color : COLOR;
    float depth : DEPTH;
};

struct PS_OUTPUT {
    float4 Color : SV_TARGET;
    float Depth : SV_Depth;
};

 PS_INPUT VS(VS_INPUT input) {
    PS_INPUT output;

+  // もともとは実際のワールド座標だが、カメラから見た相対座標に変更する
-  float4 worldPos = float4(input.Position, 1.0f);
+  float4 worldPos = float4(input.Position - cameraPosition.xyz, 1.0f);
    output.Position = mul(mul(worldPos, view), projection);
    output.TexCoord = input.TexCoord;
    output.Worldpos = worldPos;
    output.Normal = input.Normal;
    output.depth = output.Position.z / output.Position.w;
    output.Color = input.Color;
    
    return output;
}

PS_OUTPUT PS(PS_INPUT input) {
    PS_OUTPUT output;

    float4 albedo = input.Color;

    // ビューの方向
-  float3 v = normalize(input.Worldpos.xyz - cameraPosition.xyz);
+  float3 v = normalize(input.Worldpos.xyz - 0);
    float3 n = normalize(input.Normal);
    float3 l = normalize(lightDir.xyz);
    float3 h = normalize(l + v);
    float col = 0.02 + 0.98 * pow(1.0 - max(0.0, dot(n,v)), 5.0);

    float3 fixedLightDir = lightDir.xyz * float3(-1,1,-1);
    float3 lightColor = CalculateSunlightColor(-fixedLightDir);
    lightColor *= col;

    output.Color = float4(lightColor, 1.0);
    output.Depth = input.depth;
    return output;
}
```

## レイマーチング描画の変更

本手法ではレイマーチング時のレイ方向を決定するため、あらかじめプリミティブとしてカメラを囲む箱を描画し、箱に対するワールド空間をノーマライズしてレイ方向を算出している：

```diff
struct VS_INPUT {
    float3 Pos : POSITION;
    float2 TexCoord : TEXCOORD0;
};

struct PS_INPUT {
    float4 Pos : SV_POSITION;
    float4 Worldpos : POSITION;
    float2 TexCoord : TEXCOORD0;
};

struct PS_OUTPUT {
    float4 Color : SV_TARGET;
    float Depth : SV_Depth;
};

PS_INPUT VS(VS_INPUT input) {
    PS_INPUT output;

    // 箱はカメラを常に内部に置くように追従する
-   float4 worldPos = float4(input.Pos + cameraPosition.xyz, 1.0f);
+   float4 worldPos = float4(input.Pos, 1.0f);
    output.Pos = mul(mul(worldPos, view), projection);
	output.TexCoord = input.TexCoord;
    output.Worldpos = worldPos;

    return output;
}

PS_OUTPUT PS(PS_INPUT input) {
    PS_OUTPUT output;

    float2 screenPos = input.Pos.xy;
    float2 pixelPos = screenPos / Resolution;

    // レイ開始地点
	float3 ro = cameraPosition.xyz;
    // レイ方向
-   float3 rd = normalize(input.Worldpos.xyz - cameraPosition.xyz);
+   float3 rd = normalize(input.Worldpos.xyz - 0);
    
    float primDepth = depthTexture.Sample(depthSampler, pixelPos).r;
    float primDepthMeter = DepthToMeter( primDepth );
    float cloudDepth = 0;

    float4 cloud = RayMarch(ro, rd, primDepthMeter, cloudDepth);

    output.Color = cloud;
    output.Depth = cloudDepth;

    return output;
}
```

# 実験と結果
提案手法の性能を評価するため、100平方海里の広大な空間においてレイマーチングによる雲の描画をシミュレートした。カメラ位置を原点から約150,000メートル遠ざけ、原点からの極座標系でアジマスとエレベーションを変化させスムーズな描画を行えるか試験した。

提案手法は、カメラが約150,000メートル離れた場合でも画面の揺れが発生しないことを確認した。対照的に、従来の方式では顕著な揺れが発生した。

https://x.com/chihirobelmo/status/1864273435347853703

今回の実験は次のGitHubリポジトリにて行った

https://github.com/chihirobelmo/volumetric-cloud-for-directx11

今後、本記事で実現した手法を用いて、F-16フライトシミュレータ「FalconBMS」へ新しい雲の描画を実装する。

https://www.falcon-bms.com/

そういえば先行研究は当然ある気がするけど調べてなかったわ