🎄teratailクリスマスプレゼントキャンペーン2024🎄』開催中!

\teratail特別グッズやAmazonギフトカード最大2,000円分が当たる!/

詳細はこちら
Unity3D

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

Unity

Unityは、Unity Technologiesが開発・販売している、IDEを内蔵するゲームエンジンです。主にC#を用いたプログラミングでコンテンツの開発が可能です。

Q&A

解決済

5回答

3584閲覧

2D Spriteを立体に表示したい

Yukirr4_

総合スコア728

Unity3D

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

Unity

Unityは、Unity Technologiesが開発・販売している、IDEを内蔵するゲームエンジンです。主にC#を用いたプログラミングでコンテンツの開発が可能です。

1グッド

1クリップ

投稿2020/05/19 07:12

Image

上の画像のような2Dの透過されたスプライトを右のように立体にさせたいです。
これを3Dのゲームオブジェクトとして扱いたいのですが、どうやって実装させればいいのかわかりません。

どういうワードで検索をかければいいのかもわかりません。
キーワードだけでもいいので教えていただければ嬉しいです。

Hamburger_Man👍を押しています

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

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

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

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

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

sakura_hana

2020/05/19 08:23 編集

私も良いアイデアが浮かんでいませんが、とりあえずまずはスクリプトで任意の形のオブジェクトを作る方法を調べてみてはどうでしょうか?(「unity スクリプト メッシュ作成」等で検索してみてください) ちなみに「メッシュを作成する」の他に「Cubeを組み合わせる(図形が箱の組み合わせであることが保証されている場合)」「オブジェクトを作らずシェーダーでそのように見せる(見た目だけでいい場合)」などのアプローチも一応存在します。
guest

回答5

0

パート4

Sweep.cgincにメインの部分が記述されています。

ShaderLab

1#ifndef SWEEP_INCLUDED 2#define SWEEP_INCLUDED 3 4#include "SweepUtils.cginc" 5 6struct appdata 7{ 8 float4 vertex : POSITION; 9 float3 normal : NORMAL; 10}; 11 12struct v2f 13{ 14 float4 vertex : SV_POSITION; 15 float3 localPos : TEXCOORD1; 16 float3 localViewDir : TEXCOORD2; 17 float3 localLightDir : TEXCOORD3; 18 float3 worldPos : TEXCOORD4; 19 float3 worldViewDir : TEXCOORD5; 20 float3 worldLightDir : TEXCOORD6; 21 float4 screenPos : TEXCOORD7; 22}; 23 24struct v2fShadow 25{ 26 V2F_SHADOW_CASTER_NOPOS 27 float3 localPos : TEXCOORD1; 28 float3 localViewDir : TEXCOORD2; 29 float3 worldPos : TEXCOORD4; 30 float3 worldViewDir : TEXCOORD5; 31}; 32 33v2f vert(float4 vertex : POSITION) 34{ 35 v2f o; 36 o.vertex = UnityObjectToClipPos(vertex); 37 o.worldPos = mul(unity_ObjectToWorld, vertex).xyz; 38 o.worldViewDir = isPerspective() ? -UnityWorldSpaceViewDir(o.worldPos) : getWorldCameraDir(); 39 o.worldLightDir = UnityWorldSpaceLightDir(o.worldPos); 40 o.localPos = vertex.xyz; 41 o.localViewDir = worldToObjectVec(o.worldViewDir); 42 o.localLightDir = worldToObjectVec(o.worldLightDir); 43 o.screenPos = ComputeScreenPos(o.vertex); 44 return o; 45} 46 47float4 frag(v2f i) : SV_Target 48{ 49 // カメラのニアプレーンから他の不透明オブジェクト表面までの範囲で、かつキューブ内である範囲を求める 50 float3 worldViewDir = normalize(i.worldViewDir); 51 float3 localViewDir = normalize(i.localViewDir); 52 float peripheralFactor = 1.0 / dot(getWorldCameraDir(), worldViewDir); 53 float3 worldCameraOrigin = getWorldCameraOrigin(i.worldPos); 54 float3 localCameraOrigin = worldToObjectPos(worldCameraOrigin); 55 float worldFrontFace = dot(objectToWorldVec(getLocalBoundsFrontFace(localCameraOrigin, localViewDir) * localViewDir), worldViewDir); 56 float worldBackFace = dot(i.worldPos - worldCameraOrigin, worldViewDir); 57 float worldCameraNear = getWorldCameraNear(); 58 float worldOpaque = sampleOpaqueZ(i.screenPos) * peripheralFactor; 59 float worldEnter = max(worldFrontFace, worldCameraNear); 60 float worldExit = min(worldBackFace, worldOpaque); 61 62 // 不要な領域をクリッピングする 63 clip(worldExit - worldEnter); 64 65 // 視線方向の色積分の開始・終了ステップ番号を求める 66 float worldBoundsDepth = getWorldBoundsDepth(getWorldCameraDir()); 67 float2 worldBoundsNear = getWorldBoundsNearFar(worldBoundsDepth).x; 68 float worldLineOrigin = worldBoundsNear * peripheralFactor; 69 float worldLineLength = worldBoundsDepth * peripheralFactor; 70 float worldStepLength = worldLineLength / VIEW_INTEGRATION_STEPS; 71 float worldLineOriginToEnter = worldEnter - worldLineOrigin; 72 float worldLineOriginToExit = worldExit - worldLineOrigin; 73 int startingIndex = (int)ceil(worldLineOriginToEnter / worldStepLength); 74 int terminalIndex = (int)(worldLineOriginToExit / worldStepLength); 75 76 // 1ステップごとの、および最後の1ステップのローカル位置進行量を求める 77 float worldLastStepLength = worldLineOriginToExit % worldStepLength; 78 float3 localStep = worldToObjectVec(worldStepLength * worldViewDir); 79 float localStepLength = length(localStep); 80 float3 localLastStep = localStep * worldLastStepLength / worldStepLength; 81 82 // 光源の方角を求める 83 float3 worldLightDir = normalize(i.worldLightDir); 84 float3 localLightDir = normalize(i.localLightDir); 85 86 // 1ステップ進行時の透過率を求めるための指数を決める 87 // DENSITY_SCALEが1だと距離1mで本来のアルファ合成と同等の透明度になるようにしたつもりだが、 88 // それだと薄いようなら密度を上げる 89 float exponent = worldStepLength * DENSITY_SCALE; 90 float lastExponent = worldLastStepLength * DENSITY_SCALE; 91 92 // 視線方向に各点を調べていき、最終的にカメラに届く色を決定する 93 float3 localLineOriginPos = worldToObjectPos(worldCameraOrigin + worldViewDir * worldLineOrigin); 94 float3 integratedColor = 0.0; 95 float transmittance = 1.0; 96 for (int j = startingIndex; j <= terminalIndex; j++) 97 { 98 float3 samplingPoint = localLineOriginPos + j * localStep; 99 float4 color = sampleTexture(samplingPoint); 100 if (color.a < ALPHA_THRESHOLD) 101 { 102 // アルファがかなり小さい場合、この点に由来する光は最終的な見た目に 103 // ほとんど寄与しないと予想されるので、計算量節約のためスキップする 104 continue; 105 } 106 107 // この小空間の透過率および不透明性を求める 108 float transparency = pow(1.0 - color.a, exponent); 109 float opacity = 1.0 - transparency; 110 111 // この点に入射する光の強度を求める 112 float3 light = _LightColor0.rgb * integrateTransmittance(samplingPoint - localViewDir * localStepLength * LIGHT_INTEGRATION_ORIGIN_OFFSET, localLightDir, localStepLength, exponent); 113 114 // 環境光も上乗せして出射光とする 115 integratedColor += (light + getAmbient(samplingPoint)) * color.rgb * (opacity * transmittance); 116 117 // 次のステップでは、最終的な見た目への影響率はこの地点の不透明性の分だけ減衰する 118 // 特に、不透明度の高い領域を通過すると影響率が大幅に低下する 119 transmittance *= transparency; 120 if (transmittance < INTEGRATION_THRESHOLD) 121 { 122 // 影響率が十分小さくなったら、それ以降の点は見た目に大きな影響はないと見なしてループを中断する 123 break; 124 } 125 } 126 127 // 最後にステップ端数分を埋める追加の1ステップを進める 128 if (transmittance >= INTEGRATION_THRESHOLD) 129 { 130 float3 samplingPoint = localLineOriginPos + terminalIndex * localStep + localLastStep; 131 float4 color = sampleTexture(samplingPoint); 132 if (color.a >= ALPHA_THRESHOLD) 133 { 134 float transparency = pow(1.0 - color.a, lastExponent); 135 float opacity = 1.0 - transparency; 136 float3 light = _LightColor0.rgb * integrateTransmittance(samplingPoint, localLightDir, localStepLength, exponent); 137 integratedColor += (light + getAmbient(samplingPoint)) * color.rgb * (opacity * transmittance); 138 transmittance *= transparency; 139 } 140 } 141 return float4(integratedColor, 1.0 - transmittance); 142} 143 144v2fShadow vertShadow(appdata v, out float4 pos : SV_POSITION) 145{ 146 v2fShadow o; 147 TRANSFER_SHADOW_CASTER_NOPOS(o,pos) 148 o.localPos = v.vertex.xyz; 149 o.worldPos = objectToWorldPos(o.localPos); 150 if (isPerspective()) 151 { 152 float3 worldCameraPos = getWorldCameraPos(); 153 float3 localCameraPos = worldToObjectPos(worldCameraPos); 154 o.localViewDir = o.localPos - localCameraPos; 155 o.worldViewDir = o.worldPos - worldCameraPos; 156 } 157 else 158 { 159 o.worldViewDir = getWorldCameraDir(); 160 o.localViewDir = worldToObjectVec(o.worldViewDir); 161 } 162 return o; 163} 164 165// 通常のレンダリング時の流れを踏襲しつつ、シャドウマッピングではそれほど正確な 166// 描画が要求されないため、大幅に簡略化した処理を行う 167float4 fragShadow(v2fShadow i, UNITY_VPOS_TYPE vpos : VPOS 168#if !defined(SHADOWS_CUBE) || defined(SHADOWS_CUBE_IN_DEPTH_TEX) 169 , out float depth : SV_Depth 170#endif 171) : SV_Target 172{ 173 float3 worldViewDir = normalize(i.worldViewDir); 174 float3 localViewDir = normalize(i.localViewDir); 175 float peripheralFactor = 1.0 / dot(getWorldCameraDir(), worldViewDir); 176 float worldBoundsDepth = getWorldBoundsDepth(getWorldCameraDir()); 177 float worldLineLength = worldBoundsDepth * peripheralFactor; 178 float worldStepLength = worldLineLength / VIEW_INTEGRATION_STEPS; 179 float3 localStep = worldToObjectVec(worldStepLength * worldViewDir); 180 float3 worldCameraPos = isPerspective() ? getWorldCameraPos() : mul(UNITY_MATRIX_I_V, float4(UnityObjectToViewPos(i.localPos) * float3(1.0, 1.0, 0.0), 1.0)).xyz; 181 float3 localCameraPos = worldToObjectPos(worldCameraPos); 182 float worldFrontFace = dot(i.worldPos - worldCameraPos, worldViewDir); 183 float worldBackFace = dot(objectToWorldVec(getLocalBoundsBackFace(localCameraPos, localViewDir) * localViewDir), worldViewDir); 184 int terminalIndex = int((worldBackFace - worldFrontFace) / worldStepLength); 185 float exponent = worldStepLength * DENSITY_SCALE; 186 float transmittance = 1.0; 187 float3 samplingPoint; 188 float dither = 0.0; 189 for (int j = 0; j <= terminalIndex; j++) 190 { 191 samplingPoint = i.localPos + j * localStep; 192 float alpha = sampleTexture(samplingPoint).a; 193 if (alpha < ALPHA_THRESHOLD) 194 { 195 dither = 0.0; 196 continue; 197 } 198 transmittance *= pow(1.0 - alpha, exponent); 199 dither = tex3Dlod(_DitherMaskLOD, float4(vpos.xy * 0.25, (1.0 - transmittance) * 0.9375, 0.0)).a; 200 if (dither > 0.5) 201 { 202 break; 203 } 204 } 205 206 clip(dither - 0.01); 207 208 float4 clipPos = UnityApplyLinearShadowBias(UnityObjectToClipPos(samplingPoint)); 209 #if defined(SHADOWS_CUBE) && !defined(SHADOWS_CUBE_IN_DEPTH_TEX) 210 return UnityEncodeCubeShadowDepth(clipPos.z / clipPos.w); 211 #else 212 depth = clipPos.z / clipPos.w; 213 return 0.0; 214 #endif 215} 216#endif

結果として、下図のように側面から見ても大丈夫な描画ができました。
棒が突き刺さっているのは、不透明オブジェクトが貫入していても前後関係がそれらしく見えるかどうかを確認するためです。

Sweep

斜めに延びているもやの部分は良好に表現できましたが、最初に例示しましたメッシュ方式とは対照的に、こちらの方法だと境界がくっきりしている部分に粗が目立つようです。
下図のように、本来くっきりしているべき表面やエッジに縞々のアーティファクトが生じています。

Moire

空間を飛び飛びでサンプリングしている都合上、ある程度は妥協しなければならないのかもしれません。ステップ幅を小さくすれば改善するはずですが、ただでさえ高コストな描画方法なのにこれ以上ループを増やすのは気が引けます。
今回は実装しませんでしたが、Shader Bitsさんの記事の「Temporal Jitter」の節のようにジタリングを行うのもよさそうです。境界をくっきりさせることはできないでしょうが、縞々模様が解消できれば不自然さは大きく低減しそうな気がします。

あるいは、メッシュ方式とのハイブリッドにしてやる手もあるかもしれません。下図は単純にこのボリューメトリック方式のオブジェクトとImageSweeperで作ったメッシュ方式のオブジェクトを重ね合わせただけなのですが、くっきり部分ともやもや部分が共存していて、なかなか悪くない結果じゃないでしょうか?

Hybrid

投稿2020/05/30 08:52

Bongo

総合スコア10811

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

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

0

パート3

SweepUtils.cgincでは各種定数、ユニフォーム変数、下請け関数類を記述しています。
定数類は何だかごちゃごちゃした小数が並んでいますが、なんとなく2の冪乗にしたくなっただけです。あの値でなければならないような理由はありませんので、てきとうでかまいません。

ShaderLab

1#ifndef SWEEP_UTILS_INCLUDED 2#define SWEEP_UTILS_INCLUDED 3 4#include "UnityCG.cginc" 5 6#define LIGHT_INTEGRATION_STEPS 8 7#define VIEW_INTEGRATION_STEPS 256 8#define INTEGRATION_THRESHOLD 0.00390625 9#define DIRECTIONAL_EPSILON 0.0009765625 10#define ALPHA_SCALE 0.999996185302734375 11#define ALPHA_THRESHOLD 0.00390625 12#define DENSITY_SCALE 4.0 13#define LIGHT_INTEGRATION_ORIGIN_OFFSET 2.0 14#define AMBIENT_STEP_LENGTH 0.015625 15#define AMBIENT_BOUNDS_WIDTH 0.0625 16 17sampler2D _MainTex; 18sampler2D _CameraDepthTexture; 19sampler3D _DitherMaskLOD; 20float4 _LightColor0; 21 22// キューブに関するtransform.InverseTransformPoint 23float3 worldToObjectPos(float3 worldPos) {return mul(unity_WorldToObject, float4(worldPos, 1.0)).xyz;} 24 25// キューブに関するtransform.TransformPoint 26float3 objectToWorldPos(float3 localPos) {return mul(unity_ObjectToWorld, float4(localPos, 1.0)).xyz;} 27 28// キューブに関するtransform.InverseTransformVector 29float3 worldToObjectVec(float3 worldVec) {return mul((float3x3)unity_WorldToObject, worldVec);} 30 31// キューブに関するtransform.TransformVector 32float3 objectToWorldVec(float3 localVec) {return mul((float3x3)unity_ObjectToWorld, localVec);} 33 34// カメラのtransform.position 35float3 getWorldCameraPos() {return UNITY_MATRIX_I_V._14_24_34;} 36 37// カメラのtransform.forward 38float3 getWorldCameraDir() {return -UNITY_MATRIX_V._31_32_33;} 39 40// カメラのnearClipPlane 41float getWorldCameraNear() {return _ProjectionParams.y;} 42 43// カメラ投影法が透視投影かどうか 44bool isPerspective() {return any(UNITY_MATRIX_P._41_42_43);} 45 46// カメラが透視投影ならカメラの位置、平行投影ならカメラ位置を通る平面上のworldPosを正面にとらえる位置 47float3 getWorldCameraOrigin(float3 worldPos) 48{ 49 return isPerspective() 50 ? getWorldCameraPos() 51 : worldPos + dot(getWorldCameraPos() - worldPos, getWorldCameraDir()) * getWorldCameraDir(); 52} 53 54// キューブのtransform.position 55float3 getWorldBoundsCenter() {return unity_ObjectToWorld._14_24_34;} 56 57// カメラの位置からキューブを見て、2枚の平面で前後にキューブを挟んだ時の長さを求める 58float getWorldBoundsDepth(float3 worldCameraDir) 59{ 60 float3 worldCornerVec1 = objectToWorldVec(float3(0.5, 0.5, 0.5)); 61 float3 worldCornerVec2 = objectToWorldVec(float3(-0.5, 0.5, 0.5)); 62 float3 worldCornerVec3 = objectToWorldVec(float3(0.5, -0.5, 0.5)); 63 float3 worldCornerVec4 = objectToWorldVec(float3(-0.5, -0.5, 0.5)); 64 float2 lengths1 = abs(float2(dot(worldCornerVec1, worldCameraDir), dot(worldCornerVec2, worldCameraDir))); 65 float2 lengths2 = abs(float2(dot(worldCornerVec3, worldCameraDir), dot(worldCornerVec4, worldCameraDir))); 66 float2 lengths = max(lengths1, lengths2); 67 return max(lengths.x, lengths.y) * 2.0; 68} 69 70// 上記のように挟んだ時の平面の位置を求める 71float2 getWorldBoundsNearFar(float worldBoundsDepth) 72{ 73 float center = isPerspective() 74 ? distance(getWorldBoundsCenter(), getWorldCameraPos()) 75 : dot(getWorldBoundsCenter() - getWorldCameraPos(), getWorldCameraDir()); 76 return float2(-0.5, 0.5) * worldBoundsDepth + center; 77} 78 79// キューブの面に関するPlane.Raycastを3方向まとめて行う 80float3 getLocalBoundsFaces(float3 localPos, float3 localViewDir, float faceOffset) 81{ 82 float3 signs = sign(localViewDir); 83 return -(signs * localPos + faceOffset) / (abs(localViewDir) + (1.0 - abs(signs)) * DIRECTIONAL_EPSILON); 84} 85 86// キューブ前面までの距離を求める 87float getLocalBoundsFrontFace(float3 localPos, float3 localViewDir) 88{ 89 float3 lengths = getLocalBoundsFaces(localPos, localViewDir, 0.5); 90 return max(max(max(lengths.x, lengths.y), lengths.z), 0.0); 91} 92 93// キューブ背面までの距離を求める 94float getLocalBoundsBackFace(float3 localPos, float3 localViewDir) 95{ 96 float3 lengths = getLocalBoundsFaces(localPos, localViewDir, -0.5); 97 return max(min(min(lengths.x, lengths.y), lengths.z), 0.0); 98} 99 100// キューブのZ方向に平面投影マッピングすることを前提としてテクスチャをサンプリングする 101// ただし、アルファ1.0のとき密度が無限大になるのを防止するため、アルファをちょっとだけ減らす 102float4 sampleTexture(float3 localPos) 103{ 104 float4 color = tex2Dlod(_MainTex, float4(localPos.xy + 0.5, 0.0, 0.0)); 105 color.a *= ALPHA_SCALE; 106 return color; 107} 108 109// デプステクスチャをもとに他の不透明オブジェクトのZ位置を算出する 110// キューブ内に他の不透明オブジェクトが貫入している場合に対応するため使用する 111float sampleOpaqueZ(float4 screenPos) 112{ 113 float rawDepth = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(screenPos)); 114 return isPerspective() 115 ? LinearEyeDepth(rawDepth) 116 : -dot(unity_CameraInvProjection._33_34, float2(_ProjectionParams.x * (rawDepth * 2.0 - 1.0), 1.0)); 117} 118 119// 環境光っぽい何かを求める(ライティング時に使用し、陰が真っ黒になるのを防止するために上乗せする) 120float3 getAmbient(float3 localPos) 121{ 122 float2 delta = float2(AMBIENT_STEP_LENGTH, 0.0); 123 float3 gradient = float3( 124 sampleTexture(localPos - delta.xyy).a - sampleTexture(localPos + delta.xyy).a, 125 sampleTexture(localPos - delta.yxy).a - sampleTexture(localPos + delta.yxy).a, 126 sign(localPos.z) * smoothstep(0.5 - AMBIENT_BOUNDS_WIDTH, 0.5, abs(localPos.z))); 127 float gradientLength = length(gradient); 128 float3 ambient = SHEvalLinearL0L1(float4(gradient, gradientLength)); 129 ambient += SHEvalLinearL2(float4(gradient / sqrt(gradientLength), 0.0)); 130 #ifdef UNITY_COLORSPACE_GAMMA 131 ambient = LinearToGammaSpace(ambient); 132 #endif 133 return ambient; 134} 135 136// キューブ内のある点からある方向を見た時、どれだけ遮られずにキューブの外を見通すことができるかを算出する 137float integrateTransmittance(float3 localPos, float3 localDir, float localStepLength, float exponent) 138{ 139 float length = getLocalBoundsBackFace(localPos, localDir); 140 if (length < localStepLength) 141 { 142 return 1.0; 143 } 144 float transmittance = 1.0; 145 for (int j = 1; j <= LIGHT_INTEGRATION_STEPS; j++) 146 { 147 float localTravel = localStepLength * j; 148 bool breaks = localTravel > length; 149 localTravel = min(localTravel, length); 150 float3 samplingPoint = localPos + localTravel * localDir; 151 float alpha = sampleTexture(samplingPoint).a; 152 if (alpha < ALPHA_THRESHOLD) 153 { 154 if (breaks) 155 { 156 break; 157 } 158 continue; 159 } 160 float transparency = pow(1.0 - alpha, exponent); 161 transmittance *= transparency; 162 if (breaks || transmittance < INTEGRATION_THRESHOLD) 163 { 164 break; 165 } 166 } 167 return transmittance; 168} 169#endif

(パート4へ続く...)

投稿2020/05/30 08:47

Bongo

総合スコア10811

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

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

0

パート2

今回のスクリプトでは、テクスチャのアウトラインを取るのにPeter SelingerさんによるPotraceをもとにしたWolfgang NaglさんによるVectorizationを使用しました。自前で実装するのは少々面倒そうだったのでありがたく使わせてもらったのですが、念のため申し上げますと、もしご質問者さんが作品を公開するおつもりでしたらライセンス形態にご注意ください。
PotraceはGNU GPLを採用しているようですので、普通にUnityプロジェクトに組み込んだのでは(たとえプラグインとしてインポートしたとしても)おそらくできあがったUnityプログラムもGPLに従う必要がありそうです。
サイトの文章によれば、プロプライエタリなソフトに組み込みたい場合には有償でライセンスを購入することもできるようですね。

その他、できあがった曲線を整理するために(本来の用途とはちょっとずれていますが...)Angus JohnsonさんによるClipperを、アウトラインを三角形分割してメッシュに使用できるようにするためにMason GreenさんらによるPoly2TriのMartin EvansさんによるC#版を使用しました。

実験材料として下図テクスチャを使ったところ、

RGB

いい感じに立体的なメッシュになりました。

ImageSweeper

ですが、この方式ですと透明・不透明がくっきり分かれた画像でないと対応しづらいかと思います。
先のテクスチャに代わって下図テクスチャを使うと...

RGBTailing

こんな風になってしまいます。

ImageSweeperTailing

境界が曖昧なテクスチャを立体化すると、できあがる立体もはっきりした表面を持たない塊になるはずです。そういうものは普通のメッシュでは表現困難な部類になるかと思います。
一案としてはunsoluble_sugarさんのおっしゃる「透過画像をObjectとして配置し、Z軸に引き伸ばす」が使えるかもしれません。
「毛の断面を積層し、毛が密生した毛皮のように見せる」なんてテクニックがありますが(参考:3Dグラフィックス・マニアックス(32) ジオメトリシェーダ(3)~ジオメトリシェーダのアクセラレーション的活用(3))、試しに下記のようなスクリプトで画像を重ねてみました。

C#

1using System.Collections.Generic; 2using UnityEngine; 3 4public class LatticeImageSweeper : MonoBehaviour 5{ 6 [SerializeField] private Texture2D textureToSweep; 7 [SerializeField] private float sweepLength = 0.25f; 8 [SerializeField] private float pixelsPerUnit = 100.0f; 9 [SerializeField][Range(1, 128)] private int divisions = 32; 10 [Header("Arrange")] 11 [SerializeField] private bool alongX = false; 12 [SerializeField] private bool alongY = false; 13 [SerializeField] private bool alongZ = true; 14 15 private void Start() 16 { 17 // X、Y、Z軸に沿って正方形を並べていく 18 // 全体を一つのメッシュにしないで、いくつもオブジェクトを並べることで 19 // 距離ソートが行われるようにし、描画順が正しくなるようにした 20 var material = new Material(Shader.Find("Mobile/Particles/Alpha Blended")) 21 { 22 mainTexture = this.textureToSweep 23 }; 24 var planeTemplate = GameObject.CreatePrimitive(PrimitiveType.Quad); 25 DestroyImmediate(planeTemplate.GetComponent<Collider>()); 26 planeTemplate.GetComponent<Renderer>().sharedMaterial = material; 27 var vertices = new[] 28 { 29 new Vector3(0.0f, -0.5f, -0.5f), 30 new Vector3(0.0f, 0.5f, -0.5f), 31 new Vector3(0.0f, -0.5f, 0.5f), 32 new Vector3(0.0f, 0.5f, 0.5f) 33 }; 34 var uvs = new Vector2[4]; 35 var indices = new[] {0, 1, 2, 3, 2, 1}; 36 var directions = new List<(Vector3,Vector3,Vector3,Vector3,string)>(); 37 if (this.alongX) 38 { 39 directions.Add((Vector3.right, Vector3.up, Vector3.forward, -Vector3.right * 0.5f, "X")); 40 } 41 if (this.alongY) 42 { 43 directions.Add((Vector3.up, Vector3.forward, Vector3.right, -Vector3.up * 0.5f, "Y")); 44 } 45 if (this.alongZ) 46 { 47 directions.Add((Vector3.forward, Vector3.right, Vector3.up, -Vector3.forward * 0.5f, "Z")); 48 } 49 foreach (var (r, u, f, o, axisName) in directions) 50 { 51 for (var i = 0; i <= this.divisions; i++) 52 { 53 var center = o + (r * ((float)i / this.divisions)); 54 var v0 = center + ((-f - u) * 0.5f); 55 var v1 = center + ((-f + u) * 0.5f); 56 var v2 = center + ((f - u) * 0.5f); 57 var v3 = center + ((f + u) * 0.5f); 58 uvs[0].Set(v0.x + 0.5f, v0.y + 0.5f); 59 uvs[1].Set(v1.x + 0.5f, v1.y + 0.5f); 60 uvs[2].Set(v2.x + 0.5f, v2.y + 0.5f); 61 uvs[3].Set(v3.x + 0.5f, v3.y + 0.5f); 62 var planeName = $"{axisName}{i}"; 63 var mesh = new Mesh {name = planeName, vertices = vertices, uv = uvs, triangles = indices}; 64 mesh.RecalculateNormals(); 65 var plane = Instantiate(planeTemplate, this.transform); 66 plane.name = planeName; 67 var planeTransform = plane.transform; 68 planeTransform.localPosition = center; 69 planeTransform.localRotation = Quaternion.LookRotation(f, u); 70 plane.GetComponent<MeshFilter>().sharedMesh = mesh; 71 } 72 } 73 Destroy(planeTemplate); 74 75 // テクスチャサイズ、pixelsPerUnit、sweepLengthに応じてスケールを調整する 76 this.transform.localScale = new Vector3( 77 this.textureToSweep.width / this.pixelsPerUnit, 78 this.textureToSweep.height / this.pixelsPerUnit, 79 this.sweepLength); 80 } 81}

最初のスクリプトと比べてかなりシンプルです。Z方向に積層したところ下図のような見た目になりました。

LatticeZ

何層も重なったせいでずいぶん濃密に見えますが、その辺はアルファの調整で何とかなるでしょう。
オブジェクト数は多少かさむものの比較的低コストで、正面に近い向きで見るかぎりなかなか高品質な見た目になりそうです。
ただし、側面から見ると...

Aircon

ラジエーター状態なのがばれてしまいます。
X、Y軸にも積層してカバーする手もあるかもしれませんが、今回のスクリプトでそれをやると...

LatticeXYZ

視点の移動にともない面の前後関係がパキッと変わって見苦しくなってしまいます。オブジェクト単位の距離ソートではこうなってしまうのは仕方ないところがあり、解消するには各面をもっと小さい小片として構成する必要があるでしょう。ですがそれをやるとオブジェクト数が許容しがたいレベルに膨れ上がってしまいそうです。

別のアプローチとして、sakura_hanaさんの言及された「オブジェクトを作らずシェーダーでそのように見せる」も候補になるかと思います。
ShaderBits : Creating a Volumetric Ray Marcher - Ryan Brucksの方法を参考に検討してみました。

シェーダーファイル(Sweep.shader)は下記のようにしました。
主体となるコードは整理のため後述の別ファイル(Sweep.cginc、SweepUtils.cginc)に分離しています。

ShaderLab

1Shader "Volumetric/Sweep" 2{ 3 Properties 4 { 5 [NoScaleOffset] _MainTex ("Texture", 2D) = "white" {} 6 } 7 SubShader 8 { 9 Tags { "Queue" = "Transparent" "RenderType" = "Transparent" } 10 // ForwardAddは未実装につき、メインディレクショナルライトでしか描画できない 11 Pass 12 { 13 Tags { "LightMode" = "ForwardBase" } 14 Blend One OneMinusSrcAlpha 15 Cull Front 16 ZWrite Off 17 ZTest Always 18 19 CGPROGRAM 20 #pragma vertex vert 21 #pragma fragment frag 22 #pragma multi_compile_fwdbase 23 #include "Sweep.cginc" 24 ENDCG 25 } 26 Pass 27 { 28 Tags { "LightMode" = "ShadowCaster" } 29 ZWrite On 30 ZTest LEqual 31 32 CGPROGRAM 33 #pragma vertex vertShadow 34 #pragma fragment fragShadow 35 #pragma multi_compile_shadowcaster 36 #include "Sweep.cginc" 37 ENDCG 38 } 39 } 40}

(パート3へ続く...)

投稿2020/05/30 08:46

Bongo

総合スコア10811

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

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

0

ベストアンサー

ドット絵めいた表現でしたらunsoluble_sugarさんからご紹介いただいたようなアセットが有効そうですね。
もっと高解像度で曲線があるような画像だったらどうするか...ですが、メッシュとして立体化する方針で行くとすると、何かしらの方法でテクスチャのアウトラインを取る必要がありそうです。ペイントソフトで扱うようなビットマップデータであるテクスチャをドローソフトで扱うようなベクターデータに変えて、それをベースにメッシュを構築することになるんじゃないでしょうか。

まず、下記のようなスクリプトで実験してみました。

C#

1using System.Collections.Generic; 2using System.Linq; 3using ClipperLib; 4using CsPotrace; 5using Poly2Tri; 6using Poly2Tri.Triangulation.Polygon; 7using UnityEngine; 8 9public class ImageSweeper : MonoBehaviour 10{ 11 [SerializeField] private Texture2D textureToSweep; 12 [SerializeField] private float sweepLength = 0.25f; 13 [SerializeField] private float alphaThreshold = 0.5f; 14 [SerializeField] private float pixelsPerUnit = 100.0f; 15 [SerializeField][Range(1, 16)] private int divisions = 4; 16 [SerializeField] private double clipperResolution = 1024.0; 17 18 private void Start() 19 { 20 if (this.textureToSweep == null) 21 { 22 return; 23 } 24 25 // まず、テクスチャが読み取り不許可かもしれないので複製を作る 26 var renderTexture = RenderTexture.GetTemporary(this.textureToSweep.width, this.textureToSweep.height); 27 var currentActive = RenderTexture.active; 28 Graphics.Blit(this.textureToSweep, renderTexture); 29 var readableTexture = new Texture2D(this.textureToSweep.width, this.textureToSweep.height); 30 RenderTexture.active = renderTexture; 31 readableTexture.ReadPixels(new Rect(0, 0, this.textureToSweep.width, this.textureToSweep.height), 0, 0); 32 RenderTexture.active = currentActive; 33 RenderTexture.ReleaseTemporary(renderTexture); 34 35 // Potraceでアウトラインを求める 36 var curves = new List<List<Curve>>(); 37 Potrace.Treshold = this.alphaThreshold; 38 Potrace.Potrace_Trace(readableTexture, curves); 39 Potrace.Clear(); 40 41 // Clipperでパスを処理し外周と穴を識別する 42 var clipper = new Clipper(); 43 var intPoints = GetIntPoints(GetPolygonPoints(curves, this.divisions), this.clipperResolution); 44 clipper.AddPaths(intPoints, PolyType.ptSubject, true); 45 var clipperSolution = new List<List<IntPoint>>(); 46 clipper.Execute(ClipType.ctXor, clipperSolution); 47 48 // パスの巡回方向を調べて外周と穴に分ける 49 var outlineIntPoints = new List<List<IntPoint>>(); 50 var holeIntPoints = new List<List<IntPoint>>(); 51 foreach (var path in clipperSolution) 52 { 53 var shiftedPath = path.Skip(1).Concat(path.Take(1)); 54 var crossSum = path.Zip(shiftedPath, (a, b) => (a.X * b.Y) - (a.Y * b.X)).Sum(); 55 (crossSum < 0 ? holeIntPoints : outlineIntPoints).Add(path); 56 } 57 58 // 各外周パスから穴パスをくり抜くことで、穴パスを所属する外周パスごとに整理する 59 var outlinePoints = new List<List<PolygonPoint>>(); 60 var allHolePoints = new List<List<List<PolygonPoint>>>(); 61 foreach (var outlinePath in outlineIntPoints) 62 { 63 clipper.Clear(); 64 clipperSolution.Clear(); 65 clipper.AddPath(outlinePath, PolyType.ptSubject, true); 66 clipper.AddPaths(holeIntPoints, PolyType.ptClip, true); 67 clipper.Execute(ClipType.ctDifference, clipperSolution); 68 var holePoints = new List<List<PolygonPoint>>(); 69 foreach (var path in clipperSolution) 70 { 71 var shiftedPath = path.Skip(1).Concat(path.Take(1)); 72 var crossSum = path.Zip(shiftedPath, (a, b) => (a.X * b.Y) - (a.Y * b.X)).Sum(); 73 (crossSum < 0 ? holePoints : outlinePoints).Add(path.Select(p => new PolygonPoint(p.X / this.clipperResolution, p.Y / this.clipperResolution)).ToList()); 74 } 75 allHolePoints.Add(holePoints); 76 } 77 78 // Poly2Triで三角形化する 79 var outlinePolygons = outlinePoints.Select(points => new Polygon(points)).ToArray(); 80 var allHolePolygons = allHolePoints.Select(holePoints => holePoints.Select(points => new Polygon(points)).ToArray()).ToArray(); 81 foreach (var (outlinePolygon, holePolygons) in outlinePolygons.Zip(allHolePolygons, (outlinePolygon, holePolygons) => (outlinePolygon, holePolygons))) 82 { 83 foreach (var holePolygon in holePolygons) 84 { 85 outlinePolygon.AddHole(holePolygon); 86 } 87 P2T.Triangulate(outlinePolygon); 88 } 89 90 // メッシュを作成する 91 var topTransform = Matrix4x4.TRS( 92 new Vector3(-0.5f, -0.5f, -0.5f), 93 Quaternion.identity, 94 new Vector3(1.0f / this.textureToSweep.width, 1.0f / this.textureToSweep.height, 1.0f)); 95 var bottomTransform = Matrix4x4.TRS( 96 new Vector3(-0.5f, -0.5f, 0.5f), 97 Quaternion.identity, 98 new Vector3(1.0f / this.textureToSweep.width, 1.0f / this.textureToSweep.height, 1.0f)); 99 var uvScale = new Vector2(1.0f / this.textureToSweep.width, 1.0f / this.textureToSweep.height); 100 var topMesh = GetTopMesh(outlinePolygons, topTransform, uvScale); 101 var bottomMesh = Instantiate(topMesh); 102 bottomMesh.vertices = bottomMesh.vertices.Select(p => new Vector3(p.x, p.y, -p.z)).ToArray(); 103 bottomMesh.triangles = bottomMesh.triangles.Reverse().ToArray(); 104 bottomMesh.RecalculateNormals(); 105 var sideMesh = GetSideMesh(outlinePoints.Concat(allHolePoints.SelectMany(holePoints => holePoints)), topTransform, bottomTransform, uvScale); 106 var mesh = new Mesh {name = this.textureToSweep.name}; 107 mesh.CombineMeshes(new[] {topMesh, bottomMesh, sideMesh}.Select(m => new CombineInstance {mesh = m, transform = Matrix4x4.identity}).ToArray()); 108 109 // メッシュをオブジェクトに割り付ける 110 var meshFilter = this.GetComponent<MeshFilter>(); 111 if (meshFilter == null) 112 { 113 meshFilter = this.gameObject.AddComponent<MeshFilter>(); 114 } 115 meshFilter.sharedMesh = mesh; 116 var meshRenderer = this.GetComponent<MeshRenderer>(); 117 if (meshRenderer == null) 118 { 119 meshRenderer = this.gameObject.AddComponent<MeshRenderer>(); 120 } 121 var material = meshRenderer.material; 122 if (material == null) 123 { 124 var dummyObject = GameObject.CreatePrimitive(PrimitiveType.Cube); 125 material = dummyObject.GetComponent<Renderer>().material; 126 meshRenderer.material = material; 127 Destroy(dummyObject); 128 } 129 130 // テクスチャサイズ、pixelsPerUnit、sweepLengthに応じてスケールを調整する 131 material.mainTexture = this.textureToSweep; 132 this.transform.localScale = new Vector3( 133 this.textureToSweep.width / this.pixelsPerUnit, 134 this.textureToSweep.height / this.pixelsPerUnit, 135 this.sweepLength); 136 } 137 138 private static Mesh GetSideMesh( 139 IEnumerable<IEnumerable<PolygonPoint>> paths, 140 Matrix4x4 topTransform, 141 Matrix4x4 bottomTransform, 142 Vector2 uvScale, 143 bool reverseIndices = true) 144 { 145 var allVertices = new List<Vector3>(); 146 var allUvs = new List<Vector2>(); 147 var allIndices = new List<int>(); 148 var indexOffsets = new[] {0, 1, 2, 3, 2, 1}; 149 var indexOffset = 0; 150 foreach (var path in paths) 151 { 152 var vertices = path.SelectMany( 153 p => 154 { 155 var v = new Vector3((float)p.X, (float)p.Y, 0.0f); 156 return Enumerable.Repeat(topTransform.MultiplyPoint3x4(v), 1).Concat(Enumerable.Repeat(bottomTransform.MultiplyPoint3x4(v), 1)); 157 }).ToList(); 158 var uvs = path.SelectMany(p => Enumerable.Repeat(new Vector2((float)(p.X * uvScale.x), (float)(p.Y * uvScale.y)), 2)); 159 var vertexCount = vertices.Count; 160 var offset = indexOffset; 161 var indices = Enumerable.Range(0, vertexCount).SelectMany( 162 i => 163 { 164 var i2 = i * 2; 165 return indexOffsets.Select(j => ((i2 + j) % vertexCount) + offset); 166 }); 167 indexOffset += vertexCount; 168 allVertices.AddRange(vertices); 169 allUvs.AddRange(uvs); 170 allIndices.AddRange(indices); 171 } 172 if (reverseIndices) 173 { 174 allIndices.Reverse(); 175 } 176 var mesh = new Mesh {vertices = allVertices.ToArray(), uv = allUvs.ToArray(), triangles = allIndices.ToArray()}; 177 mesh.RecalculateNormals(); 178 return mesh; 179 } 180 181 private static Mesh GetTopMesh( 182 IEnumerable<Polygon> polygons, 183 Matrix4x4 positionTransform, 184 Vector2 uvScale, 185 bool reverseIndices = true) 186 { 187 var vertices = new List<Vector3>(); 188 var indices = new List<int>(); 189 var uvs = new List<Vector2>(); 190 foreach (var polygon in polygons) 191 { 192 vertices.AddRange(polygon.Triangles.SelectMany(t => t.Points.Select(p => positionTransform.MultiplyPoint3x4(new Vector3((float)p.X, (float)p.Y, 0.0f))))); 193 uvs.AddRange(polygon.Triangles.SelectMany(t => t.Points.Select(p => new Vector2((float)(p.X * uvScale.x), (float)(p.Y * uvScale.y))))); 194 } 195 var indexEnumerable = Enumerable.Range(0, vertices.Count); 196 if (reverseIndices) 197 { 198 indexEnumerable = indexEnumerable.Reverse(); 199 } 200 indices.AddRange(indexEnumerable); 201 var mesh = new Mesh {vertices = vertices.ToArray(), uv = uvs.ToArray(), triangles = indices.ToArray()}; 202 mesh.RecalculateNormals(); 203 return mesh; 204 } 205 206 private static List<List<IntPoint>> GetIntPoints(IEnumerable<IEnumerable<PolygonPoint>> paths, double resolution) 207 { 208 return paths.Select(path => path.Select(p => new IntPoint(p.X * resolution, p.Y * resolution)).ToList()).ToList(); 209 } 210 211 private static IEnumerable<IEnumerable<PolygonPoint>> GetPolygonPoints(IEnumerable<IEnumerable<Curve>> paths, int divisions = 16) 212 { 213 return paths.Select(path => GetPolygonPoints(path, divisions)); 214 } 215 216 private static IEnumerable<PolygonPoint> GetPolygonPoints(IEnumerable<Curve> path, int divisions = 16) 217 { 218 return path.SelectMany(curve => GetPolygonPoints(curve, divisions)); 219 } 220 221 private static IEnumerable<PolygonPoint> GetPolygonPoints(Curve curve, int divisions = 16) 222 { 223 divisions = Mathf.Max(divisions, 1); 224 yield return GetPolygonPoint(curve.A); 225 for (var i = 1; i < divisions; i++) 226 { 227 yield return GetPolygonPoint(curve, (double)i / divisions); 228 } 229 } 230 231 private static PolygonPoint GetPolygonPoint(Curve curve, double t) 232 { 233 if (t <= 0.0) 234 { 235 return GetPolygonPoint(curve.A); 236 } 237 if (t >= 1.0) 238 { 239 return GetPolygonPoint(curve.B); 240 } 241 var it = 1.0 - t; 242 var t2 = t * t; 243 var it2 = it * it; 244 var f0 = it2 * it; 245 var f1 = 3.0 * it2 * t; 246 var f2 = 3.0 * it * t2; 247 var f3 = t * t2; 248 var x = (f0 * curve.A.x) + (f1 * curve.ControlPointA.x) + (f2 * curve.ControlPointB.x) + (f3 * curve.B.x); 249 var y = (f0 * curve.A.y) + (f1 * curve.ControlPointA.y) + (f2 * curve.ControlPointB.y) + (f3 * curve.B.y); 250 return new PolygonPoint(x, y); 251 } 252 253 private static PolygonPoint GetPolygonPoint(dPoint point) 254 { 255 return new PolygonPoint(point.x, point.y); 256 } 257}

(パート2へ続く...)

投稿2020/05/30 08:40

Bongo

総合スコア10811

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

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

Yukirr4_

2020/05/30 10:01

回答ありがとうございます。! ここまで細かく検証していただき本当にありがとうございます。助かりました! 今後はShaderの勉強もしようと思います。
guest

0

実現したいこととしては「Qubicle」や「Voxel」などが近そうに感じました。
検索キーワードとしては、例えば以下のようなものになるかと思います

  • 「2d sprite convert 3D」
  • 「convert 2D sprites into 3D voxel」

参考までに関連ソフトやAssetsを載せておきます。
jantepya/Unity-Sprite-Voxelizer: Unity utility for converting a 2D sprite to a 3D voxel mesh
Qubicle - Professional Voxel Editor for Design and Development

例えば手書きのSpriteを立体化させるケースであれば、前述のAssetsなどとは少し要件がズレてしまうかもしれません。

自前で実装するとしたら

  • 3D空間にオブジェクトを配置し、片面に画像貼り付けて切り抜く
  • 透過画像をObjectとして配置し、Z軸に引き伸ばす

などといったアプローチが考えられるのではないでしょうか。
実際に色々試してみると他にも良いやり方があるかもしれませんが、自分がパッと思いついたのはこのくらいです。

ご参考いただければと思います。

投稿2020/05/27 02:34

編集2020/05/27 15:42
unsoluble_sugar

総合スコア222

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

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

Yukirr4_

2020/05/30 09:57

コメント遅れてすみません。 低解像度の画像を扱うときはこちらの回答を参考に実装したいと思います。 本当にありがとうございます!
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.36%

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

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

質問する

関連した質問