7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

グレンジAdvent Calendar 2024

Day 9

【Unity】シェーダーで世界を曲げる

Last updated at Posted at 2024-12-08

はじめに

この記事では、「世界を曲げる表現」をシェーダーで実現する方法を紹介します。
「世界を曲げる表現」とは、以下のようなものです。

ASTRONEER: Glitchwalkers

Before We Leave

奥にあるオブジェクトほど沈み込み、側面や底面が見えるようになっています。
また、地平線が弧を描くように曲がっています。
この記事では、このような表現をシェーダーで実現する方法を詳しく解説します。

作ったもの

曲がった世界 その1

曲がった世界 その2

曲げる前の平たい世界
スクリーンショット 2024-12-08 4.25.23.png

シェーダーコード全文

Shader "Sample/WorldBending"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Amount ("Amount", Range(-1, 1)) = 0
    }
    SubShader
    {
        Tags
        {
            "RenderType"="Opaque"
            "RenderPipeline"="UniversalPipeline"
        }
        LOD 100

        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            struct appdata
            {
                float4 posOS : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float4 posHCS : SV_POSITION;
                float2 uv : TEXCOORD0;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float _Amount;

            v2f vert(appdata v)
            {
                v2f o;

                // カメラ座標からのオフセットを求める
                float3 posWS = TransformObjectToWorld(v.posOS);
                float3 cameraPosWS = _WorldSpaceCameraPos;
                float3 offsetFromCamera = posWS - cameraPosWS;

                // 曲げた後のワールド座標を求める
                float bendByOffsetX = pow(offsetFromCamera.x, 2) * -_Amount;
                float bendByOffsetZ = pow(offsetFromCamera.z, 2) * -_Amount;
                float bend = bendByOffsetX + bendByOffsetZ;
                posWS.y += bend;

                // ワールド空間からクリップ空間に変換
                float3 bendedPosOS = TransformWorldToObject(posWS);
                o.posHCS = TransformObjectToHClip(bendedPosOS);
                
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            half4 frag(v2f i) : SV_Target
            {
                return tex2D(_MainTex, i.uv);
            }
            ENDHLSL
        }
    }
}

ポイント解説

float bendByOffsetX = pow(offsetFromCamera.x, 2) * -_Amount;
float bendByOffsetZ = pow(offsetFromCamera.z, 2) * -_Amount;
float bend = bendByOffsetX + bendByOffsetZ;

ここでは、カメラからの距離が同じであれば同じ曲げ量(bend)となるように計算を行っています。
これは中学校で習う三平方の定理を利用しています。

また、指数関数の結果に対して_Amountで調整を加えることで、擬似的な球体を表現しています。

オブジェクトが消える問題とその解決策

上述のシェーダーを適用すると、画面奥のオブジェクトがパッと消える現象が起こるようになります。
下のGIFでは、左奥のオブジェクトが消えています。

これはオクルージョンカリングによるものです。
オクルージョンカリングは、GPU側で頂点シェーダが実行されるより前にCPU側で行われます。
頂点移動する前のオブジェクトがカメラの視錐台より外に存在するため、レンダリングされなくなってしまっています。

この問題は、オクルージョンカリングに利用される行列(Camera.cullingMatrix)を調整することで解決できます。
cullingMatrixにはprojectionMatrix * worldToCameraMatrixが格納されているため、任意のプロジェクション行列を使って調整することが可能です。

下記のコードは、Orthographicなプロジェクション行列を利用して調整したものです。

	private void OnEnable() {
		if (!Application.isPlaying) {
			return;
		}

		RenderPipelineManager.beginCameraRendering += OnBeginCameraRendering;
		RenderPipelineManager.endCameraRendering += OnEndCameraRendering;
	}

	private void OnDisable() {
		RenderPipelineManager.beginCameraRendering -= OnBeginCameraRendering;
		RenderPipelineManager.endCameraRendering -= OnEndCameraRendering;
	}

	private static void OnBeginCameraRendering(ScriptableRenderContext ctx, Camera cam) {
		const float orthoSize = 30f; // 要調整
		const float nearClip = 0.001f; // 要調整
		const float farClip = 1000f; // 要調整
		var aspectRatio = (float)Screen.width / Screen.height;
		var orthoWidth = orthoSize * aspectRatio;
		cam.cullingMatrix =
			Matrix4x4.Ortho(-orthoWidth, orthoWidth, -orthoSize, orthoSize, nearClip, farClip) *
			cam.worldToCameraMatrix;
	}

	private static void OnEndCameraRendering(ScriptableRenderContext ctx, Camera cam) {
		cam.ResetCullingMatrix();
	}

調整手順

自分の場合は以下の手順で調整を行いました。

  1. ワールドを曲げた状態で動き回り、表示されないオブジェクトを確認
  2. ワールドを曲げていない状態(MaterialのAmount=0)にし、カメラをOrthographicに切り替え
  3. 表示されなかったオブジェクトがカメラ内に映るようにSizeを調整
  4. ワールドとカメラを元の状態に戻し、手順1から繰り返し

おわりに

シェーダーで「世界を曲げる表現」を実現しました。
ぜひ、あなたのゲームにもこの技術を取り入れてみてください。

参考にしたリンクなど

7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?