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

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

新規登録して質問してみよう
ただいま回答率
85.31%
Unity3D

Unity3Dは、ゲームや対話式の3Dアプリケーション、トレーニングシュミレーション、そして医学的・建築学的な技術を可視化する、商業用の開発プラットフォームです。

Q&A

解決済

1回答

2234閲覧

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

coffee_break00

総合スコア13

Unity3D

Unity3Dは、ゲームや対話式の3Dアプリケーション、トレーニングシュミレーション、そして医学的・建築学的な技術を可視化する、商業用の開発プラットフォームです。

0グッド

0クリップ

投稿2021/03/05 06:42

前提・実現したいこと

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)

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

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

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

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答1

0

ベストアンサー

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

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

ShaderLab

1Shader "VolumeRendering/VolumeRendering" 2{ 3 Properties 4 { 5 [Header(Rendering)] 6 _Volume("Volume", 3D) = "" {} 7 _Color("Color", Color) = (1, 1, 1, 1) 8 _Iteration("Iteration", Int) = 100 9 _Intensity("Intensity", Range(0.0, 1.0)) = 0.1 10 11 [Header(Ranges)] 12 _MinX("MinX", Range(0, 1)) = 0.0 13 _MaxX("MaxX", Range(0, 1)) = 1.0 14 _MinY("MinY", Range(0, 1)) = 0.0 15 _MaxY("MaxY", Range(0, 1)) = 1.0 16 _MinZ("MinZ", Range(0, 1)) = 0.0 17 _MaxZ("MaxZ", Range(0, 1)) = 1.0 18 } 19 20 CGINCLUDE 21 #include "UnityCG.cginc" 22 23 struct appdata 24 { 25 float4 vertex : POSITION; 26 }; 27 28 struct v2f 29 { 30 float4 vertex : SV_POSITION; 31 float3 localPos : TEXCOORD0; 32 float3 worldPos : TEXCOORD1; 33 float3 localViewDir : TEXCOORD2; 34 float3 worldViewDir : TEXCOORD3; 35 float4 screenPos : TEXCOORD4; 36 }; 37 38 sampler3D _Volume; 39 fixed4 _Color; 40 int _Iteration; 41 fixed _Intensity; 42 fixed _MinX, _MaxX, _MinY, _MaxY, _MinZ, _MaxZ; 43 44 #define INTEGRATION_THRESHOLD 0.01 45 #define DIRECTIONAL_EPSILON 0.0009765625 46 #define ALPHA_THRESHOLD 0.01 47 48 sampler2D _CameraDepthTexture; 49 50 // キューブに関するtransform.InverseTransformPoint 51 float3 worldToObjectPos(float3 worldPos) {return mul(unity_WorldToObject, float4(worldPos, 1.0)).xyz;} 52 53 // キューブに関するtransform.InverseTransformVector 54 float3 worldToObjectVec(float3 worldVec) {return mul((float3x3)unity_WorldToObject, worldVec);} 55 56 // キューブに関するtransform.TransformVector 57 float3 objectToWorldVec(float3 localVec) {return mul((float3x3)unity_ObjectToWorld, localVec);} 58 59 // カメラのtransform.position 60 float3 getWorldCameraPos() {return UNITY_MATRIX_I_V._14_24_34;} 61 62 // カメラのtransform.forward 63 float3 getWorldCameraDir() {return -UNITY_MATRIX_V._31_32_33;} 64 65 // カメラのnearClipPlane 66 float getWorldCameraNear() {return _ProjectionParams.y;} 67 68 // カメラ投影法が透視投影かどうか 69 bool isPerspective() {return any(UNITY_MATRIX_P._41_42_43);} 70 71 // カメラが透視投影ならカメラの位置、平行投影ならカメラ位置を通る平面上のworldPosを正面にとらえる位置 72 float3 getWorldCameraOrigin(float3 worldPos) 73 { 74 return isPerspective() 75 ? getWorldCameraPos() 76 : worldPos + dot(getWorldCameraPos() - worldPos, getWorldCameraDir()) * getWorldCameraDir(); 77 } 78 79 // キューブのtransform.position 80 float3 getWorldBoundsCenter() {return unity_ObjectToWorld._14_24_34;} 81 82 // カメラの位置からキューブを見て、2枚の平面で前後にキューブを挟んだ時の長さを求める 83 float getWorldBoundsDepth(float3 worldCameraDir) 84 { 85 float3 worldCornerVec1 = objectToWorldVec(float3(0.5, 0.5, 0.5)); 86 float3 worldCornerVec2 = objectToWorldVec(float3(-0.5, 0.5, 0.5)); 87 float3 worldCornerVec3 = objectToWorldVec(float3(0.5, -0.5, 0.5)); 88 float3 worldCornerVec4 = objectToWorldVec(float3(-0.5, -0.5, 0.5)); 89 float2 lengths1 = abs(float2(dot(worldCornerVec1, worldCameraDir), dot(worldCornerVec2, worldCameraDir))); 90 float2 lengths2 = abs(float2(dot(worldCornerVec3, worldCameraDir), dot(worldCornerVec4, worldCameraDir))); 91 float2 lengths = max(lengths1, lengths2); 92 return max(lengths.x, lengths.y) * 2.0; 93 } 94 95 // 上記のように挟んだ時の平面の位置を求める 96 float2 getWorldBoundsNearFar(float worldBoundsDepth) 97 { 98 float center = isPerspective() 99 ? distance(getWorldBoundsCenter(), getWorldCameraPos()) 100 : dot(getWorldBoundsCenter() - getWorldCameraPos(), getWorldCameraDir()); 101 return float2(-0.5, 0.5) * worldBoundsDepth + center; 102 } 103 104 // キューブの面に関するPlane.Raycastを3方向まとめて行う 105 float3 getLocalBoundsFaces(float3 localPos, float3 localViewDir, float faceOffset) 106 { 107 float3 signs = sign(localViewDir); 108 return -(signs * localPos + faceOffset) / (abs(localViewDir) + (1.0 - abs(signs)) * DIRECTIONAL_EPSILON); 109 } 110 111 // キューブ前面までの距離を求める 112 float getLocalBoundsFrontFace(float3 localPos, float3 localViewDir) 113 { 114 float3 lengths = getLocalBoundsFaces(localPos, localViewDir, 0.5); 115 return max(max(max(lengths.x, lengths.y), lengths.z), 0.0); 116 } 117 118 // デプステクスチャをもとに他の不透明オブジェクトのZ位置を算出する 119 // キューブ内に他の不透明オブジェクトが貫入している場合に対応するため使用する 120 float sampleOpaqueZ(float4 screenPos) 121 { 122 float rawDepth = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(screenPos)); 123 return isPerspective() 124 ? LinearEyeDepth(rawDepth) 125 : -dot(unity_CameraInvProjection._33_34, float2(_ProjectionParams.x * (rawDepth * 2.0 - 1.0), 1.0)); 126 } 127 128 fixed sample(float3 pos) 129 { 130 fixed x = step(pos.x, _MaxX) * step(_MinX, pos.x); 131 fixed y = step(pos.y, _MaxY) * step(_MinY, pos.y); 132 fixed z = step(pos.z, _MaxZ) * step(_MinZ, pos.z); 133 return tex3D(_Volume, pos).a * x * y * z; 134 } 135 136 v2f vert(appdata v) 137 { 138 v2f o; 139 o.vertex = UnityObjectToClipPos(v.vertex); 140 o.worldPos = mul(unity_ObjectToWorld, v.vertex); 141 o.localPos = v.vertex.xyz; 142 o.worldViewDir = isPerspective() ? -UnityWorldSpaceViewDir(o.worldPos) : getWorldCameraDir(); 143 o.localViewDir = worldToObjectVec(o.worldViewDir); 144 o.screenPos = ComputeScreenPos(o.vertex); 145 return o; 146 } 147 148 fixed4 frag(v2f i) : SV_Target 149 { 150 // カメラのニアプレーンから他の不透明オブジェクト表面までの範囲で、かつキューブ内である範囲を求める 151 float3 worldViewDir = normalize(i.worldViewDir); 152 float3 localViewDir = normalize(i.localViewDir); 153 float peripheralFactor = 1.0 / dot(getWorldCameraDir(), worldViewDir); 154 float3 worldCameraOrigin = getWorldCameraOrigin(i.worldPos); 155 float3 localCameraOrigin = worldToObjectPos(worldCameraOrigin); 156 float worldFrontFace = dot(objectToWorldVec(getLocalBoundsFrontFace(localCameraOrigin, localViewDir) * localViewDir), worldViewDir); 157 float worldBackFace = dot(i.worldPos - worldCameraOrigin, worldViewDir); 158 float worldCameraNear = getWorldCameraNear(); 159 float worldOpaque = sampleOpaqueZ(i.screenPos) * peripheralFactor; 160 float worldEnter = max(worldFrontFace, worldCameraNear); 161 float worldExit = min(worldBackFace, worldOpaque); 162 163 // 不要な領域をクリッピングする 164 clip(worldExit - worldEnter); 165 166 // 視線方向の色積分の開始・終了ステップ番号を求める 167 float worldBoundsDepth = getWorldBoundsDepth(getWorldCameraDir()); 168 float2 worldBoundsNear = getWorldBoundsNearFar(worldBoundsDepth).x; 169 float worldLineOrigin = worldBoundsNear * peripheralFactor; 170 float worldLineLength = worldBoundsDepth * peripheralFactor; 171 float worldStepLength = worldLineLength / _Iteration; 172 float worldLineOriginToEnter = worldEnter - worldLineOrigin; 173 float worldLineOriginToExit = worldExit - worldLineOrigin; 174 int startingIndex = (int)ceil(worldLineOriginToEnter / worldStepLength); 175 int terminalIndex = (int)(worldLineOriginToExit / worldStepLength); 176 177 float3 localStep = worldToObjectVec(worldStepLength * worldViewDir); 178 float3 localLineOriginPos = worldToObjectPos(worldCameraOrigin + worldViewDir * worldLineOrigin); 179 fixed output = 0.0; 180 181 [loop] 182 for (int j = startingIndex; j <= terminalIndex; j++) 183 { 184 float3 lpos = localLineOriginPos + j * localStep; 185 fixed a = sample(lpos + 0.5); 186 if (a < ALPHA_THRESHOLD) 187 { 188 continue; 189 } 190 output += (1.0 - output) * a * _Intensity; 191 if (output > 1.0 - INTEGRATION_THRESHOLD) 192 { 193 break; 194 } 195 } 196 197 return _Color * output; 198 } 199 ENDCG 200 201 SubShader 202 { 203 Tags 204 { 205 "Queue" = "Transparent" 206 "RenderType" = "Transparent" 207 } 208 209 Pass 210 { 211 Cull Front 212 ZWrite Off 213 ZTest Always 214 Blend One OneMinusSrcAlpha 215 Lighting Off 216 217 CGPROGRAM 218 #pragma vertex vert 219 #pragma fragment frag 220 ENDCG 221 } 222 } 223}

図

投稿2021/03/05 17:08

Bongo

総合スコア10816

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

coffee_break00

2021/03/06 06:19

ご丁寧に回答していただき,ありがとうございます! なぜ私の実装ではCTの内部が見れないのか,またそれを実現するための実装もコメント付きのコードまで提示していただいてとてもありがたく思います. これを基に私の方でも発展させていきます.本当にありがとうございました!
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.31%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問