のりまき日記

Unityなどの活用リファレンスブログ。「こうしたい時どうする」をまとめたい

unity)映画「ピクセル」みたいにボクセルを大量描画してハデに破壊したい❷ ~スクリーンスペース当たり判定編~

めざすもの

はじめに

meshをvoxel化して大量描画して当たり判定もつけてハデに破壊してみました。いくつかの記事に分けてまとめてみます。(最適化はできていないので、コンセプト的な実装として読んでください。)

できたもの

スクリーンスペース当たり判定とは

GBufferで生成されるDepthテクスチャやNormalsテクスチャを使って軽量に当たり判定を行うものです。画面に映っている情報でしか当たり判定できないのでまったく厳密ではないのですが、結構それっぽく見えます。描画時にDepthテクスチャで奥行きを比較し、奥にあればNormalsテクスチャで反射ベクトルを計算して跳ね返すイメージです。

はじめて見たのは以下の記事でした。かっこいいです。 i-saint.hatenablog.com

実装にあたっては以下の記事を参考にさせていただきました。 tips.hecomi.com

DepthテクスチャやNormalsテクスチャ

Deferredレンダリングを行う際に描画用にいくつかの画像が作成されます(GBuffer)。その中に含まれる画像です。今回のプロジェクトはDeferredレンダリングを使っていなかったので別の方法で取り出します。

private void Start()
{
    Camera.main.depthTextureMode = DepthTextureMode.Depth | DepthTextureMode.DepthNormals;
    ...
}

描画負荷は増えますが上記で似たような画像を生成してくれます。shaderに_CameraDepthTexture_CameraDepthNormalsTextureを定義すると取り出せるので見てみます。

みやすくした画像

memo

複数カメラがあると上記ではうまく取り出せないことがありました。カメラごとに画像を保持したい場合はCommandBufferを使用して以下のようにキープできます。

var buf = new CommandBuffer();
buf.name = "_KeepDepthNormalsTexture";
var depthNormalsTexture = new RenderTexture(Camera.main.pixelWidth, Camera.main.pixelHeight, 24, RenderTextureFormat.ARGB32);
buf.CopyTexture(BuiltinRenderTextureType.DepthNormals, depthNormalsTexture);
Camera.main.AddCommandBuffer(CameraEvent.BeforeForwardAlpha, buf);

「CommandBuffer」や「ComputeBuffer」や「ComputeShader」など、似たような単語がたくさん出てきてこんがらがります。全然違うものです。

memo

デプスバッファとデプステクスチャは同じじゃないよ

いろいろ調べている時に上記文言をよく見かけました。あまり理解できなかったので今度ちゃんと調べてみようと思いました。

計算用shaderに渡す

void DispatchInit()
{
    ...
    m_CubeComputeShader.SetVector("_ScreenSize", new Vector2(Camera.main.pixelWidth, Camera.main.pixelHeight));
}

void DispatchUpdate()
{
    ...
    m_CubeComputeShader.SetTextureFromGlobal(m_UpdateKernel, "_CameraDepthTexture", "_CameraDepthTexture");
    m_CubeComputeShader.SetTextureFromGlobal(m_UpdateKernel, "_CameraDepthNormalsTexture", "_CameraDepthNormalsTexture");
    m_CubeComputeShader.SetMatrix("_MATRIX_VP", GetViewProjectionMatrix(Camera.main));
    ...
}

画像のほかにViewProjectionMatrixも渡しています。ワールド座標からスクリーンポジションに変換してスクリーン上のuvを計算するのに使います。

note

_CameraDepthNormalsTextureから法線もデプスも取り出せるのですが、このデプスはLinear01Depthを通ったのと同様の値になっています。生のデプスはプロジェクション変換時に非線形になるのですが、今回はそっちを使いたいので_CameraDepthTextureも渡しています。

以下の記事にとても詳しく説明されていました。2つの8bitから16bitに戻すDecodeFloatRGの実装が面白かったです。

shiomusubi290.hatenablog.com

計算用shaderに当たり判定を実装する

inline float3 DecodeViewNormalStereo(float4 enc4)
{
    float kScale = 1.7777;
    float3 nn = enc4.xyz*float3(2*kScale,2*kScale,0) + float3(-kScale,-kScale,1);
    float g = 2.0 / dot(nn.xyz,nn.xyz);
    float3 n;
    n.xy = g*nn.xy;
    n.z = g-1;
    return n;
}

...

[numthreads(8,1,1)]
void Update (uint id : SV_DispatchThreadID)
{
    Particle p = _Particles[id];

    if (p.active)
    {
        // 移動先を取得
        float3 nextPos = p.position + p.velocity * _DeltaTime;
        // 移動先のスクリーン座標
        float4 vpPos = mul(_MATRIX_VP, float4(nextPos, 1.0));
        // スクリーン座標からuvにする
        float2 uv = vpPos.xy / vpPos.w * 0.5 + 0.5;
        // uvから画像のインデックスにする
        float2 coord = uv * _ScreenSize;
        
        // ピクセルを取り出す
        float depth = _CameraDepthTexture[coord];
        float3 normal = DecodeViewNormalStereo(_CameraDepthNormalsTexture[coord]) * float3(1.0, 1.0, -1.0);

        // 非線形のデプス
        float pDepth = vpPos.z / vpPos.w;

        // 移動先が奥なら
        if (pDepth < depth) {
            // 反射ベクトルを計算
            p.velocity -= 2 * dot(p.velocity, normal) * normal;
        }

        // 空気抵抗を計算
        float drug = .3;
        p.velocity *= (1 - _DeltaTime * drug);

        // 速度を更新
        p.velocity += _Gravity * _DeltaTime;
        // 位置を更新
        p.position += p.velocity * _DeltaTime;
        // 回転を更新
        p.rotation += p.angularVelocity * _DeltaTime;

        // ライフタイムを更新
        p.time += _DeltaTime;
        p.active = (p.time <= p.lifeTime);

        _ActiveIndexes.Append(id);
    }

    _Particles[id] = p;
}

note

inline float3 DecodeViewNormalStereo(float4 enc4)
{
    float kScale = 1.7777;
    float3 nn = enc4.xyz*float3(2*kScale,2*kScale,0) + float3(-kScale,-kScale,1);
    float g = 2.0 / dot(nn.xyz,nn.xyz);
    float3 n;
    n.xy = g*nn.xy;
    n.z = g-1;
    return n;
}

UnityCG.cgincに定義されているものを持ってきました。

float3 normal = DecodeViewNormalStereo(_CameraDepthNormalsTexture[coord]) * float3(1.0, 1.0, -1.0);

法線を取得する処理です。以下の記事を参考にしました。

light11.hatenadiary.com

// 反射ベクトルを計算
p.velocity -= 2 * dot(p.velocity, normal) * normal;

以下の記事を参考にしました。壁ずりベクトルを使うと粘性のあるインクなどの表現もできそうです。

marupeke296.com

// 空気抵抗を計算
float drag = .3;
p.velocity *= (1 - _DeltaTime * drag);

抵抗がないとずっと同じ速度で跳ね返り続けるので空気抵抗を入れました。以下の記事にunity内での実装が載っていました。

How drag is calculated by Unity engine.? - Unity Answers

ここまでの結果

おわり

つぎはメッシュをボクセル化してみます。