質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

87.48%

[Unity] 3DTextureを付与したオブジェクト内にカメラが侵入してもボリュームレンダリングの結果を描画したい

解決済

回答 1

投稿

  • 評価
  • クリップ 0
  • VIEW 447

score 13

前提・実現したいこと

Unity初心者です.ご了承ください.

Unity内で人間のCT画像データからボリュームレンダリングで3次元的に臓器を見て,仮想的に体内に胃カメラを入れられるようなアプリケーションを作りたいと思っています.

「Unity でボリュームレンダリングをしてみる」の記事を見ながら,紹介されているコードの実装を行っていました.
以下に示すようにサンプルデータである鯉のCT画像から仮想空間内におけるボリュームレンダリングは成功しました.

イメージ説明
Sceneビューにおける位置関係

イメージ説明
カメラの取得風景

しかし,このカメラが3Dテクスチャを持つオブジェクト内に侵入するとカメラの取得風景から鯉の3次元モデルが消えます.

イメージ説明
Sceneビューにおける位置関係

イメージ説明
カメラの取得風景

これを解決するために,Unity内のShaderに関して勉強してみたのですがはっきりとした原因が分かりませんでした.
この問題の原因・解決法などご教授いただければ幸いです.

発生している問題・エラーメッセージ

カメラが3Dテクスチャを持つオブジェクト内に侵入するとカメラの取得風景から鯉の3次元モデルが消える.

該当のソースコード

ボリュームレンダリングのShaderは以下のようなものを使っています.

Shader "VolumeRendering/VolumeRendering"
{

Properties
{
    [Header(Rendering)]
    _Volume("Volume", 3D) = "" {}
    _Color("Color", Color) = (1, 1, 1, 1)
    _Iteration("Iteration", Int) = 100
    _Intensity("Intensity", Range(0.0, 1.0)) = 0.1

    [Header(Ranges)]
    _MinX("MinX", Range(0, 1)) = 0.0
    _MaxX("MaxX", Range(0, 1)) = 1.0
    _MinY("MinY", Range(0, 1)) = 0.0
    _MaxY("MaxY", Range(0, 1)) = 1.0
    _MinZ("MinZ", Range(0, 1)) = 0.0
    _MaxZ("MaxZ", Range(0, 1)) = 1.0
}

CGINCLUDE

#include "UnityCG.cginc"

struct appdata
{
    float4 vertex : POSITION;
};

struct v2f
{
    float4 vertex   : SV_POSITION;
    float4 localPos : TEXCOORD0;
    float4 worldPos : TEXCOORD1;
};

sampler3D _Volume;
fixed4 _Color;
int _Iteration;
fixed _Intensity;
fixed _MinX, _MaxX, _MinY, _MaxY, _MinZ, _MaxZ;

fixed sample(float3 pos)
{
    fixed x = step(pos.x, _MaxX) * step(_MinX, pos.x);
    fixed y = step(pos.y, _MaxY) * step(_MinY, pos.y);
    fixed z = step(pos.z, _MaxZ) * step(_MinZ, pos.z);
    return tex3D(_Volume, pos).a * x * y * z;
}

v2f vert(appdata v)
{
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);
    o.localPos = v.vertex;
    o.worldPos = mul(unity_ObjectToWorld, v.vertex);
    return o;
}

fixed4 frag(v2f i) : SV_Target
{
    float3 wdir = i.worldPos - _WorldSpaceCameraPos;
    float3 ldir = normalize(mul(unity_WorldToObject, wdir));
    float3 lstep = ldir / _Iteration;
    float3 lpos = i.localPos;
    fixed output = 0.0;

    [loop]
    for (int i = 0; i < _Iteration; ++i)
    {
        fixed a = sample(lpos + 0.5);
        output += (1 - output) * a * _Intensity;
        lpos += lstep;
        if (!all(max(0.5 - abs(lpos), 0.0)) || output > 0.99) break;
    }

    return _Color * output;
}

ENDCG

SubShader
{

Tags 
{ 
    "Queue" = "Transparent"
    "RenderType" = "Transparent" 
}

Pass
{
    Cull Back
    ZWrite Off
    ZTest LEqual
    Blend SrcAlpha OneMinusSrcAlpha 
    Lighting Off

    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    ENDCG
}

}

}

試したこと

「Pass」内の[Cull Back]を[Cull Off]に変えたりしましたが,結果は変わりませんでした.

補足情報(FW/ツールのバージョンなど)

OS: Win10 pro
GPU: GeForce RTX 2080
Unity: Unity 2019.3.10f1(64-bit)

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 1

checkベストアンサー

+2

凹みTipsさんの方式は「ポリゴン表面を起点に、視線に沿ってテクスチャをサンプリングしていく」というスタイルですね。ということは積分の起点はキューブの前面であることが暗黙の前提になっていて、単純に両面描画に切り替えただけではうまくいかなそうです(キューブの内部から背面を描画するというシチュエーションでは、背面を起点にキューブの外側へ向かって積分していってしまうはずです)。

対策として、逆にキューブ背面をレンダリングすることを前提としたスタイルはいかがでしょうか。以前別の方の「2D Spriteを立体に表示したい」とのご質問に挑戦したことがあるのですが、その際のコードを流用して下記のように改変してみました。

Shader "VolumeRendering/VolumeRendering"
{
    Properties
    {
        [Header(Rendering)]
        _Volume("Volume", 3D) = "" {}
        _Color("Color", Color) = (1, 1, 1, 1)
        _Iteration("Iteration", Int) = 100
        _Intensity("Intensity", Range(0.0, 1.0)) = 0.1

        [Header(Ranges)]
        _MinX("MinX", Range(0, 1)) = 0.0
        _MaxX("MaxX", Range(0, 1)) = 1.0
        _MinY("MinY", Range(0, 1)) = 0.0
        _MaxY("MaxY", Range(0, 1)) = 1.0
        _MinZ("MinZ", Range(0, 1)) = 0.0
        _MaxZ("MaxZ", Range(0, 1)) = 1.0
    }

    CGINCLUDE
    #include "UnityCG.cginc"

    struct appdata
    {
        float4 vertex : POSITION;
    };

    struct v2f
    {
        float4 vertex : SV_POSITION;
        float3 localPos : TEXCOORD0;
        float3 worldPos : TEXCOORD1;
        float3 localViewDir : TEXCOORD2;
        float3 worldViewDir : TEXCOORD3;
        float4 screenPos : TEXCOORD4;
    };

    sampler3D _Volume;
    fixed4 _Color;
    int _Iteration;
    fixed _Intensity;
    fixed _MinX, _MaxX, _MinY, _MaxY, _MinZ, _MaxZ;

    #define INTEGRATION_THRESHOLD 0.01
    #define DIRECTIONAL_EPSILON 0.0009765625
    #define ALPHA_THRESHOLD 0.01

    sampler2D _CameraDepthTexture;

    // キューブに関するtransform.InverseTransformPoint
    float3 worldToObjectPos(float3 worldPos) {return mul(unity_WorldToObject, float4(worldPos, 1.0)).xyz;}

    // キューブに関するtransform.InverseTransformVector
    float3 worldToObjectVec(float3 worldVec) {return mul((float3x3)unity_WorldToObject, worldVec);}

    // キューブに関するtransform.TransformVector
    float3 objectToWorldVec(float3 localVec) {return mul((float3x3)unity_ObjectToWorld, localVec);}

    // カメラのtransform.position
    float3 getWorldCameraPos() {return UNITY_MATRIX_I_V._14_24_34;}

    // カメラのtransform.forward
    float3 getWorldCameraDir() {return -UNITY_MATRIX_V._31_32_33;}

    // カメラのnearClipPlane
    float getWorldCameraNear() {return _ProjectionParams.y;}

    // カメラ投影法が透視投影かどうか
    bool isPerspective() {return any(UNITY_MATRIX_P._41_42_43);}

    // カメラが透視投影ならカメラの位置、平行投影ならカメラ位置を通る平面上のworldPosを正面にとらえる位置
    float3 getWorldCameraOrigin(float3 worldPos)
    {
        return isPerspective()
            ? getWorldCameraPos()
            : worldPos + dot(getWorldCameraPos() - worldPos, getWorldCameraDir()) * getWorldCameraDir();
    }

    // キューブのtransform.position
    float3 getWorldBoundsCenter() {return unity_ObjectToWorld._14_24_34;}

    // カメラの位置からキューブを見て、2枚の平面で前後にキューブを挟んだ時の長さを求める
    float getWorldBoundsDepth(float3 worldCameraDir)
    {
        float3 worldCornerVec1 = objectToWorldVec(float3(0.5, 0.5, 0.5));
        float3 worldCornerVec2 = objectToWorldVec(float3(-0.5, 0.5, 0.5));
        float3 worldCornerVec3 = objectToWorldVec(float3(0.5, -0.5, 0.5));
        float3 worldCornerVec4 = objectToWorldVec(float3(-0.5, -0.5, 0.5));
        float2 lengths1 = abs(float2(dot(worldCornerVec1, worldCameraDir), dot(worldCornerVec2, worldCameraDir)));
        float2 lengths2 = abs(float2(dot(worldCornerVec3, worldCameraDir), dot(worldCornerVec4, worldCameraDir)));
        float2 lengths = max(lengths1, lengths2);
        return max(lengths.x, lengths.y) * 2.0;
    }

    // 上記のように挟んだ時の平面の位置を求める
    float2 getWorldBoundsNearFar(float worldBoundsDepth)
    {
        float center = isPerspective()
            ? distance(getWorldBoundsCenter(), getWorldCameraPos())
            : dot(getWorldBoundsCenter() - getWorldCameraPos(), getWorldCameraDir());
        return float2(-0.5, 0.5) * worldBoundsDepth + center;
    }

    // キューブの面に関するPlane.Raycastを3方向まとめて行う
    float3 getLocalBoundsFaces(float3 localPos, float3 localViewDir, float faceOffset)
    {
        float3 signs = sign(localViewDir);
        return -(signs * localPos + faceOffset) / (abs(localViewDir) + (1.0 - abs(signs)) * DIRECTIONAL_EPSILON);
    }

    // キューブ前面までの距離を求める
    float getLocalBoundsFrontFace(float3 localPos, float3 localViewDir)
    {
        float3 lengths = getLocalBoundsFaces(localPos, localViewDir, 0.5);
        return max(max(max(lengths.x, lengths.y), lengths.z), 0.0);
    }

    // デプステクスチャをもとに他の不透明オブジェクトのZ位置を算出する
    // キューブ内に他の不透明オブジェクトが貫入している場合に対応するため使用する
    float sampleOpaqueZ(float4 screenPos)
    {
        float rawDepth = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(screenPos));
        return isPerspective()
            ? LinearEyeDepth(rawDepth)
            : -dot(unity_CameraInvProjection._33_34, float2(_ProjectionParams.x * (rawDepth * 2.0 - 1.0), 1.0));
    }

    fixed sample(float3 pos)
    {
        fixed x = step(pos.x, _MaxX) * step(_MinX, pos.x);
        fixed y = step(pos.y, _MaxY) * step(_MinY, pos.y);
        fixed z = step(pos.z, _MaxZ) * step(_MinZ, pos.z);
        return tex3D(_Volume, pos).a * x * y * z;
    }

    v2f vert(appdata v)
    {
        v2f o;
        o.vertex = UnityObjectToClipPos(v.vertex);
        o.worldPos = mul(unity_ObjectToWorld, v.vertex);
        o.localPos = v.vertex.xyz;
        o.worldViewDir = isPerspective() ? -UnityWorldSpaceViewDir(o.worldPos) : getWorldCameraDir();
        o.localViewDir = worldToObjectVec(o.worldViewDir);
        o.screenPos = ComputeScreenPos(o.vertex);
        return o;
    }

    fixed4 frag(v2f i) : SV_Target
    {
        // カメラのニアプレーンから他の不透明オブジェクト表面までの範囲で、かつキューブ内である範囲を求める
        float3 worldViewDir = normalize(i.worldViewDir);
        float3 localViewDir = normalize(i.localViewDir);
        float peripheralFactor = 1.0 / dot(getWorldCameraDir(), worldViewDir);
        float3 worldCameraOrigin = getWorldCameraOrigin(i.worldPos);
        float3 localCameraOrigin = worldToObjectPos(worldCameraOrigin);
        float worldFrontFace = dot(objectToWorldVec(getLocalBoundsFrontFace(localCameraOrigin, localViewDir) * localViewDir), worldViewDir);
        float worldBackFace = dot(i.worldPos - worldCameraOrigin, worldViewDir);
        float worldCameraNear = getWorldCameraNear();
        float worldOpaque = sampleOpaqueZ(i.screenPos) * peripheralFactor;
        float worldEnter = max(worldFrontFace, worldCameraNear);
        float worldExit = min(worldBackFace, worldOpaque);

        // 不要な領域をクリッピングする
        clip(worldExit - worldEnter);

        // 視線方向の色積分の開始・終了ステップ番号を求める
        float worldBoundsDepth = getWorldBoundsDepth(getWorldCameraDir());
        float2 worldBoundsNear = getWorldBoundsNearFar(worldBoundsDepth).x;
        float worldLineOrigin = worldBoundsNear * peripheralFactor;
        float worldLineLength = worldBoundsDepth * peripheralFactor;
        float worldStepLength = worldLineLength / _Iteration;
        float worldLineOriginToEnter = worldEnter - worldLineOrigin;
        float worldLineOriginToExit = worldExit - worldLineOrigin;
        int startingIndex = (int)ceil(worldLineOriginToEnter / worldStepLength);
        int terminalIndex = (int)(worldLineOriginToExit / worldStepLength);

        float3 localStep = worldToObjectVec(worldStepLength * worldViewDir);
        float3 localLineOriginPos = worldToObjectPos(worldCameraOrigin + worldViewDir * worldLineOrigin);
        fixed output = 0.0;

        [loop]
        for (int j = startingIndex; j <= terminalIndex; j++)
        {
            float3 lpos = localLineOriginPos + j * localStep;
            fixed a = sample(lpos + 0.5);
            if (a < ALPHA_THRESHOLD)
            {
                continue;
            }
            output += (1.0 - output) * a * _Intensity;
            if (output > 1.0 - INTEGRATION_THRESHOLD)
            {
                break;
            }
        }

        return _Color * output;
    }
    ENDCG

    SubShader
    {
        Tags 
        { 
            "Queue" = "Transparent"
            "RenderType" = "Transparent" 
        }

        Pass
        {
            Cull Front
            ZWrite Off
            ZTest Always 
            Blend One OneMinusSrcAlpha
            Lighting Off

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            ENDCG
        }
    }
}

図

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2021/03/06 15:19

    ご丁寧に回答していただき,ありがとうございます!

    なぜ私の実装ではCTの内部が見れないのか,またそれを実現するための実装もコメント付きのコードまで提示していただいてとてもありがたく思います.

    これを基に私の方でも発展させていきます.本当にありがとうございました!

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 87.48%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る

  • トップ
  • Unity3Dに関する質問
  • [Unity] 3DTextureを付与したオブジェクト内にカメラが侵入してもボリュームレンダリングの結果を描画したい