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

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

新規登録して質問してみよう
ただいま回答率
85.47%
C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

Unity3D

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

Unity

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

Q&A

4回答

887閲覧

Unity 爆風の射線が通っているかどうかの判定

korokorokeke

総合スコア0

C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

Unity3D

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

Unity

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

0グッド

1クリップ

投稿2023/10/31 09:51

実現したいこと

Unityで3Dシューティングゲームを作ってます。
爆風の射線が通っているかどうかの判定を取りたいです。

前提

Unityのバージョンは2021.3.16f1です。
C#を使ってます。

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

現在Physics.OverlapCapsuleを用いて爆風の判定を取っています。以下の画像で黒い円が爆風範囲、緑の四角がプレイヤーのコライダーです。
イメージ説明
上記の場合ではプレイヤーが爆風範囲内に居るので、爆風が当たったという判定で問題ありません。

しかし、爆風地点とプレイヤーの間に壁(画像の赤色の線)があった場合は爆風が壁に遮られるので、爆風の判定は以下の画像の青い範囲になるべきです。
イメージ説明
上記の場合ではPhysics.OverlapCapsuleだけでは当たった判定になってしまうので、爆風地点からプレイヤーの座標までPhysics.Raycastで壁があるかどうかを調べれば解決します。

ですが、壁が複雑な形をしている場合はそうも行きません。以下の画像の場合、最終的には当たった判定になるべきです。ですが、爆風範囲にプレイヤーが居るのでPhysics.Raycastの判定をしますが、爆風とプレイヤーの座標の間に壁があるので当たってない判定になります。また、より正確に判定する為にコライダーの角に対してもPhysics.Raycastの判定をしようと考えましたが、この場合だと当たってない判定になってしまいます。
イメージ説明
判定するコライダーの座標を増やせばかなり正確に判定できますが、爆風の範囲が広がる程、そして座標を増やす程重い処理になってしまいます。また、プレイヤーだけではなく複雑な形のオブジェクトにも適用したいので、汎用的な対処方法を教えて頂きたいです。
宜しくお願いします。

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

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

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

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

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

fana

2023/11/01 01:44

Unityのことは全くしりませんが, 正確さと処理速度を天秤にかけて「どこで妥協するか」という話が必要なのではないでしょうか? 例えば,爆心からの方向をサンプリングするような方法(いろんな方向にレイを飛ばすよな)では隙間を正確に扱えない場合が生じるでしょうけど,そもそも本当に「どこまでも小さな隙間でも正確にやれる」を達成せねばならないのか? という.
korokorokeke

2023/11/01 03:48

正確性はある程度あれば大丈夫なので、プレイヤーは座標を元にしたいくつかの定点に対するRayで済むでしょう。 しかし、複雑な形のオブジェクトの場合は「座標+Vector3に飛ばせば良い」というものが無いです。イメージとしては爆心にカメラを置いてRenderer.isVisibleやOnWillRenderObjectで判定するというのが近いのですが、影だけが映っててもtrueになったり、見た目とコライダーが違うものもあるのでどうしたものかなぁといった次第です。 https://teratail.com/questions/307597 ↑似たような感じの質問はありましたが、数が増えるとこの解決法も重くなりそうです。 質問では内容が分かりやすい様に+より良い解決法があったら知りたいという事でプレイヤーを例にしました。
fana

2023/11/01 04:52 編集

> しかし、複雑な形のオブジェクトの場合は「座標+Vector3に飛ばせば良い」というものが無いです どういう意味かよくわかりませんが,爆心から見て対象物を包括するだけの立体角範囲内を適当にサンプリングするのではダメなのですか? その立体角範囲というのは対象物の形状に見合う形にした方が無駄なレイは減るのでしょうけど,処理の簡単さ/単純さ を考えるならば,「十分広い(=対象を包括する),且つ,簡単に扱える形状な範囲」で良いのでは? 例えば,対象の外接球とかから定める等. 例えるなら,「十分な角度範囲に弾が散らばるショットガンを対象に向けてぶっ放したときに,その散弾のうちの一発でも対象にヒットするか?」みたいな. 各弾の軌道(レイ)に関する判定さえできるならば,残る問題は レイの密度 vs 処理速度 のトレードオフかと思うのですが.
guest

回答4

0

円とその他の当たり判定

追記

LayerMaskで特定のLayerと当たり判定をとる
扇状の当たり判定の取り方
扇状に当たり判定を取れば3枚目の画像でも判定を取ることができるのではないでしょうか?
↓プレイヤーにつける疑似的なコード

C#

1public class Player : MonoBehaviour 2{ 3 private void Start() 4 { 5 LayerMask layerMask = 1 << LayerMask.NameToLayer("ObstacleLayer"); 6 7 if(Physics.Raycast( 8 transform.position, 9 new Vector3(-1f,0,0), 10 out RaycastHit hit, 11 Mathf.Infinity, 12 layerMask)) 13 { 14 Debug.Log("視認"); 15 } 16 } 17}

投稿2023/10/31 20:18

編集2023/11/02 04:16
isimasa

総合スコア295

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

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

korokorokeke

2023/10/31 23:47

Raycastを飛ばす適切な方向はどうやって求めるのでしょうか? その場その場で壁の隙間の幅、位置、そもそも隙間があるかどうかも変わります。
korokorokeke

2023/11/01 00:21

編集ありがとうございます。 ですが、そのスクリプトだと画像3枚目の場合当たってないと判断されてしまうと思います。
guest

0

fanaさんの案は適切かと思います。レイをばらまく範囲を決める目安としてはboundsを使うのはどうでしょうか。

C#

1using System; 2using System.Collections.Generic; 3using UnityEngine; 4 5public static class BlastAreaRaycaster 6{ 7 private static readonly Vector2[] Corners = new Vector2[8]; 8 private static readonly List<Vector2Int> Points = new List<Vector2Int>(); 9 10 /// <summary> 11 /// <paramref name="collider" />が爆風の範囲内にあるかを判定します。 12 /// </summary> 13 /// <param name="collider">判定対象の<c>Collider</c>。</param> 14 /// <param name="center">爆風の中心。</param> 15 /// <param name="radius">爆風の半径。</param> 16 /// <param name="ray"><paramref name="collider" />が爆風の範囲内にある場合、<paramref name="collider" />に衝突した<c>Ray</c>。</param> 17 /// <param name="hitInfo"><paramref name="collider" />が爆風の範囲内にある場合、<paramref name="collider" />と<paramref name="ray" />の衝突情報。 </param> 18 /// <param name="rayResolution"><c>Ray</c>の密度。</param> 19 /// <returns><paramref name="collider" />が爆風の範囲内にある場合は<c>true</c>、それ以外の場合は<c>false</c>。</returns> 20 /// <exception cref="ArgumentNullException" /> 21 public static bool Raycast( 22 Collider collider, 23 Vector3 center, 24 float radius, 25 out Ray ray, 26 out RaycastHit hitInfo, 27 int rayResolution = 8) 28 { 29 if (collider == null) 30 { 31 throw new ArgumentNullException(nameof(collider)); 32 } 33 34 radius = Mathf.Max(radius, 0.0f); 35 ray = new Ray(); 36 hitInfo = new RaycastHit(); 37 38 // 対象が非アクティブならヒットしない 39 if (!collider.enabled || !collider.gameObject.activeInHierarchy) 40 { 41 return false; 42 } 43 44 // コライダーと爆風中心の距離が爆風の半径以上ならヒットしない 45 if ((collider.ClosestPoint(center) - center).sqrMagnitude > (radius * radius)) 46 { 47 return false; 48 } 49 50 // レイを発射する位置を決める 51 // 縦横に走査する代わりに、ちょっと変則的にして範囲の中心付近を優先してみました 52 // おそらく中心付近の方がレイがヒットする確率が高く、レイキャストをより早めに 53 // 切り上げられるだろうと予想してのことです 54 rayResolution = Mathf.Max(rayResolution, 1); 55 var levelCount = Mathf.CeilToInt(rayResolution * 0.5f); 56 var origin = rayResolution >> 1; 57 var edgeLength = 2 - (rayResolution % 2); 58 Points.Clear(); 59 for (var l = 0; l < levelCount; l++, origin++, edgeLength += 2) 60 { 61 var p = new Vector2Int(origin, origin); 62 if (edgeLength < 2) 63 { 64 Points.Add(p); 65 } 66 else 67 { 68 var pa = p; 69 var antipode = rayResolution - 1 - origin; 70 var pb = new Vector2Int(antipode, antipode); 71 for (var i = 0; i < edgeLength; i++) 72 { 73 Points.Add(pa); 74 Points.Add(pb); 75 if (i == (edgeLength - 1)) 76 { 77 pa.y--; 78 pb.y++; 79 break; 80 } 81 82 pa.x--; 83 pb.x++; 84 } 85 86 var trimmedEdgeLength = edgeLength - 2; 87 for (var i = 0; i < trimmedEdgeLength; i++) 88 { 89 Points.Add(pa); 90 Points.Add(pb); 91 pa.y--; 92 pb.y++; 93 } 94 } 95 } 96 97 // バウンディングボックス中心と爆風中心を結ぶ直線を基準とした、ボックスの角8点の位置を求める 98 var bounds = collider.bounds; 99 var viewToWorld = Matrix4x4.TRS(center, Quaternion.LookRotation(bounds.center - center), Vector3.one); 100 var worldToView = viewToWorld.inverse; 101 { 102 var min = bounds.min; 103 var max = bounds.max; 104 for (var z = 0; z < 2; z++) 105 { 106 for (var y = 0; y < 2; y++) 107 { 108 for (var x = 0; x < 2; x++) 109 { 110 var v = worldToView.MultiplyPoint3x4( 111 new Vector3( 112 x == 0 ? min.x : max.x, 113 y == 0 ? min.y : max.y, 114 z == 0 ? min.z : max.z)); 115 Corners[(z << 2) | (y << 1) | x] = (Vector2)v / v.z; 116 } 117 } 118 } 119 } 120 121 // 走査範囲の上下左右を決める 122 var left = float.PositiveInfinity; 123 var right = float.NegativeInfinity; 124 var bottom = float.PositiveInfinity; 125 var top = float.NegativeInfinity; 126 var near = radius * 0.5f; 127 foreach (var c in Corners) 128 { 129 var p = c * near; 130 left = Mathf.Min(left, p.x); 131 right = Mathf.Max(right, p.x); 132 bottom = Mathf.Min(bottom, p.y); 133 top = Mathf.Max(top, p.y); 134 } 135 136 // 走査範囲空間からワールド空間への変換行列を作成する 137 var projectionToWorld = 138 viewToWorld * 139 Matrix4x4.Scale(new Vector3(1.0f, 1.0f, -1.0f)) * 140 Matrix4x4.Frustum(left, right, bottom, top, near, radius).inverse; 141 142 // レイキャストを行う 143 foreach (var p in Points) 144 { 145 var r = new Ray( 146 center, 147 projectionToWorld.MultiplyPoint( 148 new Vector3( 149 (((p.x * 2.0f) + 1.0f) / rayResolution) - 1.0f, 150 (((p.y * 2.0f) + 1.0f) / rayResolution) - 1.0f, 151 0.0f)) - center); 152 153 // レイを視覚的に確認するため、シーンビューにレイを描画する 154 Debug.DrawRay(r.origin, r.direction * radius, Color.green); 155 156 // レイがヒットし、かつそれが判定対象のコライダーなら爆風が当たったと判断して処理を打ち切る 157 if (Physics.Raycast(r, out hitInfo, radius) && (hitInfo.collider == collider)) 158 { 159 ray = r; 160 return true; 161 } 162 } 163 164 return false; 165 } 166}

下図はrayResolutionを8にセットした場合で、爆心地の周りを移動するオブジェクトに対して最大64本のレイキャストを行っています。ヒットした場合はオブジェクトを赤色に変えました。

図1

爆心地にカメラを設置してオブジェクトを追跡したところ、このレイ密度だとかすかにカメラから見える状況では判定に落ちてしまうようでした。ですが爆風を受けるのがプレイヤー側なら、わずかに爆風がかすめただけでヒット判定が出てしまうのはむしろ理不尽に感じるかもしれませんので、これはこれでありなんじゃないかと思いました。

また、「Unityでオブジェクトの全体、もしくは一部がカメラに映っているかを判定したい」のikadzuchiさんの案も妥当なアプローチに感じましたので試してみました。こちらはシェーダー3つ、スクリプト1つの構成になっています。

障害物のマッピングを行うシェーダー

ShaderLab

1Shader "Hidden/BlastAreaObstacleMapping" 2{ 3 Properties 4 { 5 _MainTex ("Texture", 2D) = "white" {} 6 _Color ("Main Color", Color) = (1.0, 1.0, 1.0, 1.0) 7 _Cutoff ("Alpha cutoff", Range(0.0, 1.0)) = 0.5 8 } 9 10 SubShader 11 { 12 Tags { "Queue"="Geometry" "RenderType"="Opaque" } 13 14 Pass 15 { 16 CGPROGRAM 17 #pragma vertex vert 18 #pragma fragment frag 19 #include "UnityCG.cginc" 20 21 struct appdata 22 { 23 float4 vertex : POSITION; 24 }; 25 26 struct v2f 27 { 28 float3 viewPos : TEXCOORD0; 29 float4 vertex : SV_POSITION; 30 }; 31 32 v2f vert(appdata v) 33 { 34 v2f o; 35 o.vertex = UnityObjectToClipPos(v.vertex); 36 o.viewPos = UnityObjectToViewPos(v.vertex); 37 return o; 38 } 39 40 float4 frag(v2f i) : SV_Target 41 { 42 return dot(i.viewPos, i.viewPos); 43 } 44 ENDCG 45 } 46 } 47 48 SubShader 49 { 50 Tags { "Queue"="AlphaTest" "RenderType"="TransparentCutout" } 51 52 Pass 53 { 54 CGPROGRAM 55 #pragma vertex vert 56 #pragma fragment frag 57 #include "UnityCG.cginc" 58 59 struct appdata 60 { 61 float4 vertex : POSITION; 62 float2 uv : TEXCOORD0; 63 }; 64 65 struct v2f 66 { 67 float3 viewPos : TEXCOORD0; 68 float2 uv : TEXCOORD1; 69 float4 vertex : SV_POSITION; 70 }; 71 72 sampler2D _MainTex; 73 float4 _MainTex_ST; 74 fixed4 _Color; 75 fixed _Cutoff; 76 77 v2f vert(appdata v) 78 { 79 v2f o; 80 o.vertex = UnityObjectToClipPos(v.vertex); 81 o.viewPos = UnityObjectToViewPos(v.vertex); 82 o.uv = TRANSFORM_TEX(v.uv, _MainTex); 83 return o; 84 } 85 86 float4 frag (v2f i) : SV_Target 87 { 88 clip(tex2D(_MainTex, i.uv).a * _Color.a - _Cutoff); 89 return dot(i.viewPos, i.viewPos); 90 } 91 ENDCG 92 } 93 } 94}

遮蔽判定を行うシェーダー

ShaderLab

1Shader "Hidden/BlastAreaCheck" 2{ 3 Properties {} 4 5 SubShader 6 { 7 Pass 8 { 9 Name "Opaque" 10 11 CGPROGRAM 12 #pragma vertex vert 13 #pragma fragment frag 14 #include "UnityCG.cginc" 15 16 struct appdata 17 { 18 float4 vertex : POSITION; 19 }; 20 21 struct v2f 22 { 23 float3 worldPos : TEXCOORD0; 24 float4 vertex : SV_POSITION; 25 }; 26 27 v2f vert(appdata v) 28 { 29 v2f o; 30 o.vertex = UnityObjectToClipPos(v.vertex); 31 o.worldPos = mul(unity_ObjectToWorld, v.vertex); 32 return o; 33 } 34 35 samplerCUBE _ObstacleMap; 36 float4 _CenterSqrRadius; 37 38 float4 frag(v2f i) : SV_Target 39 { 40 float3 relativePos = i.worldPos - _CenterSqrRadius.xyz; 41 float sqrDist = dot(relativePos, relativePos); 42 clip(_CenterSqrRadius.w - sqrDist); 43 clip(texCUBE(_ObstacleMap, relativePos).r - sqrDist); 44 return 1.0; 45 } 46 ENDCG 47 } 48 49 Pass 50 { 51 Name "TransparentCutout" 52 53 CGPROGRAM 54 #pragma vertex vert 55 #pragma fragment frag 56 #include "UnityCG.cginc" 57 58 struct appdata 59 { 60 float4 vertex : POSITION; 61 float2 uv : TEXCOORD0; 62 }; 63 64 struct v2f 65 { 66 float3 worldPos : TEXCOORD0; 67 float2 uv : TEXCOORD1; 68 float4 vertex : SV_POSITION; 69 }; 70 71 sampler2D _MainTex; 72 float4 _MainTex_ST; 73 fixed4 _Color; 74 fixed _Cutoff; 75 samplerCUBE _ObstacleMap; 76 float4 _CenterSqrRadius; 77 78 v2f vert(appdata v) 79 { 80 v2f o; 81 o.vertex = UnityObjectToClipPos(v.vertex); 82 o.worldPos = mul(unity_ObjectToWorld, v.vertex); 83 o.uv = TRANSFORM_TEX(v.uv, _MainTex); 84 return o; 85 } 86 87 float4 frag (v2f i) : SV_Target 88 { 89 clip(_CenterSqrRadius.w - sqrDist); 90 clip(tex2D(_MainTex, i.uv).a * _Color.a - _Cutoff); 91 92 float3 relativePos = i.worldPos - _CenterSqrRadius.xyz; 93 float sqrDist = dot(relativePos, relativePos); 94 clip(texCUBE(_ObstacleMap, relativePos).r - sqrDist); 95 return 1.0; 96 } 97 ENDCG 98 } 99 } 100}

判定結果を集約するシェーダー

ShaderLab

1Shader "Hidden/BlastAreaReduction" 2{ 3 Properties 4 { 5 _MainTex ("Texture", 2D) = "white" {} 6 } 7 8 SubShader 9 { 10 Cull Off 11 ZWrite Off 12 ZTest Always 13 14 Pass 15 { 16 CGPROGRAM 17 #pragma vertex vert 18 #pragma fragment frag 19 20 #include "UnityCG.cginc" 21 22 struct appdata 23 { 24 float4 vertex : POSITION; 25 float2 uv : TEXCOORD0; 26 }; 27 28 struct v2f 29 { 30 float2 uv : TEXCOORD0; 31 float4 vertex : SV_POSITION; 32 }; 33 34 v2f vert(appdata v) 35 { 36 v2f o; 37 o.vertex = UnityObjectToClipPos(v.vertex); 38 o.uv = v.uv; 39 return o; 40 } 41 42 sampler2D _MainTex; 43 float4 _MainTex_TexelSize; 44 45 fixed4 frag(v2f i) : SV_Target 46 { 47 float2 d = _MainTex_TexelSize.xy; 48 return sign(dot(float4( 49 tex2D(_MainTex, i.uv + d).r, 50 tex2D(_MainTex, i.uv + float2(-d.x, d.y)).r, 51 tex2D(_MainTex, i.uv + float2(d.x, -d.y)).r, 52 tex2D(_MainTex, i.uv - d).r), 1.0)); 53 } 54 ENDCG 55 } 56 } 57}

(字数が尽きてしまったため、パート2に移ります...)

投稿2023/11/12 12:03

Bongo

総合スコア10807

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

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

0

パート2

C#

1using System; 2using System.Collections.Generic; 3using UnityEngine; 4using UnityEngine.Rendering; 5using Object = UnityEngine.Object; 6 7public static class BlastAreaChecker 8{ 9 public enum TextureResolution 10 { 11 Low, 12 Medium, 13 High 14 } 15 16 /// <summary> 17 /// 障害物マップ作成時のニアプレーン。これより中心に近い障害物はマップに書き込まれません。 18 /// </summary> 19 public const float Near = 0.3f; 20 21 private const string ObstacleMapShaderName = "Hidden/BlastAreaObstacleMapping"; 22 private const string CheckShaderName = "Hidden/BlastAreaCheck"; 23 private const string ReductionShaderName = "Hidden/BlastAreaReduction"; 24 private static Shader obstacleMapShader; 25 private static Camera obstacleMapCamera; 26 private static Transform obstacleMapCameraTransform; 27 private static Vector4 centerAndSqrRadius; 28 private static readonly List<RenderTexture> IntermediateBuffers = new List<RenderTexture>(); 29 private static Texture2D resultBuffer; 30 private static readonly List<Renderer> TargetRenderers = new List<Renderer>(); 31 private static readonly List<Material> TargetMaterials = new List<Material>(); 32 private static readonly CommandBuffer CheckCommands = new CommandBuffer { name = "BlastAreaCheck" }; 33 private static readonly List<RenderingCommand> RenderingCommands = new List<RenderingCommand>(); 34 private static Material checkMaterial; 35 private static Material reductionMaterial; 36 private static readonly int MainTexProperty = Shader.PropertyToID("_MainTex"); 37 private static readonly int MainTexSTProperty = Shader.PropertyToID("_MainTex_ST"); 38 private static readonly int ColorProperty = Shader.PropertyToID("_Color"); 39 private static readonly int CutoffProperty = Shader.PropertyToID("_Cutoff"); 40 private static readonly int ObstacleMapProperty = Shader.PropertyToID("_ObstacleMap"); 41 private static readonly int CenterSqrRadiusProperty = Shader.PropertyToID("_CenterSqrRadius"); 42 private static readonly Vector3[] Corners = new Vector3[8]; 43 44 public static RenderTexture ObstacleMap { get; private set; } 45 46 /// <summary> 47 /// 障害物マップを更新します。 48 /// </summary> 49 /// <param name="center">爆風の中心。</param> 50 /// <param name="radius">爆風の半径。</param> 51 /// <param name="obstacleLayerMask">障害物として扱うオブジェクトのレイヤーマスク。該当するレイヤーであっても、マテリアルが不透明やアルファカットアウトでないものは対象外となります。</param> 52 /// <param name="resolution">マップの各面の解像度。<c>Low</c>は64x64、<c>Medium</c>は256x256、<c>High</c>は1024x1024です。</param> 53 public static void CaptureObstacles( 54 Vector3 center, 55 float radius, 56 int obstacleLayerMask = -5, 57 TextureResolution resolution = TextureResolution.Medium) 58 { 59 if (obstacleMapShader == null) 60 { 61 obstacleMapShader = FindShader(ObstacleMapShaderName); 62 } 63 64 // マップが未作成なら新規作成、または解像度設定が変更されたら再作成する 65 var resolutionInPixels = 1 << ((3 + (int)resolution) * 2); 66 if ((ObstacleMap == null) || (ObstacleMap.width != resolutionInPixels)) 67 { 68 Object.Destroy(ObstacleMap); 69 ObstacleMap = new RenderTexture(resolutionInPixels, resolutionInPixels, 24, RenderTextureFormat.RFloat) 70 { 71 dimension = TextureDimension.Cube 72 }; 73 ObstacleMap.Create(); 74 } 75 76 // マップキャプチャー用のカメラが未作成なら作成する 77 if (obstacleMapCamera == null) 78 { 79 var cameraObject = new GameObject("Obstacle Map Camera"); 80 var camera = cameraObject.AddComponent<Camera>(); 81 camera.clearFlags = CameraClearFlags.SolidColor; 82 camera.enabled = false; 83 cameraObject.hideFlags = HideFlags.NotEditable; 84 obstacleMapCamera = camera; 85 obstacleMapCameraTransform = camera.transform; 86 } 87 88 // カメラの周囲を撮影し、マップ上にカメラからの2乗距離をレンダリングする 89 // 解像度設定によっては、なぜか背景がうまくクリアされないことがあったが 90 // 背景だけを一度レンダリングしてからあらためてオブジェクトをレンダリングすると 91 // 意図通りになったため、RenderToCubemapを2回実行している 92 radius = Mathf.Max(radius, Near); 93 var sqrRadius = radius * radius; 94 obstacleMapCamera.backgroundColor = new Color(sqrRadius, sqrRadius, sqrRadius); 95 obstacleMapCamera.farClipPlane = radius; 96 obstacleMapCamera.nearClipPlane = Near; 97 obstacleMapCamera.SetReplacementShader(obstacleMapShader, "RenderType"); 98 obstacleMapCameraTransform.position = center; 99 obstacleMapCamera.cullingMask = 0; 100 obstacleMapCamera.RenderToCubemap(ObstacleMap); 101 obstacleMapCamera.cullingMask = obstacleLayerMask; 102 obstacleMapCamera.RenderToCubemap(ObstacleMap); 103 centerAndSqrRadius = center; 104 centerAndSqrRadius.w = sqrRadius; 105 } 106 107 /// <summary> 108 /// 現在の障害物マップを使って、<paramref name="gameObject" />が爆風の範囲内にあるかを判定します。 109 /// </summary> 110 /// <param name="gameObject">判定対象のオブジェクト。マテリアルが不透明やアルファカットアウトでないものは対象外となります。</param> 111 /// <param name="resolution">判定用テクスチャの解像度。<c>Low</c>は64x64、<c>Medium</c>は256x256、<c>High</c>は1024x1024です。</param> 112 /// <returns><paramref name="gameObject" />が爆風の範囲内にある場合は<c>true</c>、それ以外の場合は<c>false</c>。</returns> 113 public static bool Check(GameObject gameObject, TextureResolution resolution = TextureResolution.Medium) 114 { 115 if (gameObject == null) 116 { 117 throw new ArgumentNullException(nameof(gameObject)); 118 } 119 120 if (ObstacleMap == null) 121 { 122 Debug.LogError($"Execute {nameof(CaptureObstacles)} to create obstacle map."); 123 return false; 124 } 125 126 // 非アクティブならヒットしない 127 if (!gameObject.activeInHierarchy) 128 { 129 return false; 130 } 131 132 CreateMaterialIfNeeded(CheckShaderName, ref checkMaterial); 133 CreateMaterialIfNeeded(ReductionShaderName, ref reductionMaterial); 134 135 // 判定結果を段階的に縮小して集約するためのテクスチャを用意する 136 var requiredBufferCount = 4 + (int)resolution; 137 var currentBufferCount = IntermediateBuffers.Count; 138 for (var i = currentBufferCount; i < requiredBufferCount; i++) 139 { 140 var size = 1 << (i * 2); 141 var buffer = new RenderTexture(size, size, 0, RenderTextureFormat.R8); 142 buffer.autoGenerateMips = false; 143 buffer.Create(); 144 IntermediateBuffers.Add(buffer); 145 } 146 147 // 1ピクセルに集約された判定結果を受け取るためのテクスチャを用意する 148 if (resultBuffer == null) 149 { 150 resultBuffer = new Texture2D(1, 1, TextureFormat.R8, false, true, true); 151 } 152 153 // 判定対象の条件に合致するレンダラーを抜き出して描画コマンドを構築する 154 // 併せて、描画範囲を決めるためにレンダラーのバウンディングボックスを求める 155 var totalBounds = new Bounds(new Vector3(float.NaN, float.NaN, float.NaN), Vector3.zero); 156 RenderingCommands.Clear(); 157 gameObject.GetComponentsInChildren(TargetRenderers); 158 foreach (var targetRenderer in TargetRenderers) 159 { 160 if (!targetRenderer.enabled) 161 { 162 continue; 163 } 164 165 var mesh = (targetRenderer as SkinnedMeshRenderer)?.sharedMesh; 166 if ((mesh == null) && targetRenderer is MeshRenderer) 167 { 168 mesh = targetRenderer.GetComponent<MeshFilter>().sharedMesh; 169 } 170 171 var subMeshCount = mesh == null ? 1 : mesh.subMeshCount; 172 targetRenderer.GetSharedMaterials(TargetMaterials); 173 var materialCount = TargetMaterials.Count; 174 var willBeRendered = false; 175 for (var i = 0; i < materialCount; i++) 176 { 177 var command = new RenderingCommand(); 178 var material = TargetMaterials[i]; 179 var renderType = material.GetTag("RenderType", false); 180 if (renderType == "TransparentCutout") 181 { 182 var textureScale = material.GetTextureScale(MainTexProperty); 183 var textureOffset = material.GetTextureOffset(MainTexProperty); 184 command.IsCutout = true; 185 command.Texture = material.mainTexture; 186 command.ScaleOffset = new Vector4(textureScale.x, textureScale.y, textureOffset.x, textureOffset.y); 187 command.Color = material.color; 188 command.Cutoff = material.GetFloat(CutoffProperty); 189 } 190 else if (renderType != "Opaque") 191 { 192 continue; 193 } 194 195 command.Renderer = targetRenderer; 196 command.SubmeshIndex = i % subMeshCount; 197 RenderingCommands.Add(command); 198 willBeRendered = true; 199 } 200 201 if (willBeRendered) 202 { 203 var bounds = targetRenderer.bounds; 204 if (float.IsNaN(totalBounds.center.x)) 205 { 206 totalBounds = bounds; 207 } 208 else 209 { 210 totalBounds.Encapsulate(bounds); 211 } 212 } 213 } 214 215 // バウンディングボックスが得られなければ中断する 216 if (float.IsNaN(totalBounds.center.x)) 217 { 218 return false; 219 } 220 221 // 求めたバウンディングボックスを狙う形の座標変換行列を作る 222 // バウンディングボックス中心と爆風中心を結ぶ直線を基準とした、ボックスの角8点の位置を求める 223 var center = (Vector3)centerAndSqrRadius; 224 var viewToWorld = Matrix4x4.TRS(center, Quaternion.LookRotation(totalBounds.center - center), Vector3.one); 225 var worldToView = viewToWorld.inverse; 226 { 227 var min = totalBounds.min; 228 var max = totalBounds.max; 229 for (var z = 0; z < 2; z++) 230 { 231 for (var y = 0; y < 2; y++) 232 { 233 for (var x = 0; x < 2; x++) 234 { 235 var p = worldToView.MultiplyPoint3x4( 236 new Vector3( 237 x == 0 ? min.x : max.x, 238 y == 0 ? min.y : max.y, 239 z == 0 ? min.z : max.z)); 240 p.z = Mathf.Max(p.z, Near); 241 Corners[(z << 2) | (y << 1) | x] = p; 242 } 243 } 244 } 245 } 246 247 // プロジェクション行列の前後を決める 248 var near = float.PositiveInfinity; 249 var far = Near; 250 foreach (var c in Corners) 251 { 252 near = Mathf.Min(near, c.z); 253 far = Mathf.Max(far, c.z); 254 } 255 256 var radius = Mathf.Max(Mathf.Sqrt(centerAndSqrRadius.w), Near); 257 near = Mathf.Max(near, Near); 258 far = Mathf.Min(far, radius); 259 260 // プロジェクション行列の上下左右を決める 261 var left = float.PositiveInfinity; 262 var right = float.NegativeInfinity; 263 var bottom = float.PositiveInfinity; 264 var top = float.NegativeInfinity; 265 foreach (var c in Corners) 266 { 267 var p = (Vector2)c * (near / c.z); 268 left = Mathf.Min(left, p.x); 269 right = Mathf.Max(right, p.x); 270 bottom = Mathf.Min(bottom, p.y); 271 top = Mathf.Max(top, p.y); 272 }

(スクリプトの途中ですが、また字数が尽きたためパート3に移ります...)

投稿2023/11/12 12:03

Bongo

総合スコア10807

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

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

0

パート3

C#

1 // 判定用レンダリングを行うCommandBufferを構築する 2 var bufferIndex = requiredBufferCount - 1; 3 CheckCommands.Clear(); 4 CheckCommands.SetRenderTarget(IntermediateBuffers[bufferIndex]); 5 CheckCommands.ClearRenderTarget(true, true, Color.black); 6 CheckCommands.SetGlobalTexture(ObstacleMapProperty, ObstacleMap); 7 CheckCommands.SetGlobalVector(CenterSqrRadiusProperty, centerAndSqrRadius); 8 CheckCommands.SetViewProjectionMatrices( 9 Matrix4x4.Scale(new Vector3(1.0f, 1.0f, -1.0f)) * worldToView, 10 Matrix4x4.Frustum(left, right, bottom, top, near, far)); 11 foreach (var command in RenderingCommands) 12 { 13 var pass = 0; 14 if (command.IsCutout) 15 { 16 pass = 1; 17 CheckCommands.SetGlobalTexture(MainTexProperty, command.Texture); 18 CheckCommands.SetGlobalVector(MainTexSTProperty, command.ScaleOffset); 19 CheckCommands.SetGlobalColor(ColorProperty, command.Color); 20 CheckCommands.SetGlobalFloat(CutoffProperty, command.Cutoff); 21 } 22 23 CheckCommands.DrawRenderer(command.Renderer, checkMaterial, command.SubmeshIndex, pass); 24 } 25 26 for (var i = bufferIndex; i > 0; i--) 27 { 28 CheckCommands.Blit(IntermediateBuffers[i], IntermediateBuffers[i - 1], reductionMaterial); 29 } 30 31 // 判定用レンダリングを実行し、結果を取得する 32 var currentTarget = RenderTexture.active; 33 Graphics.ExecuteCommandBuffer(CheckCommands); 34 RenderTexture.active = IntermediateBuffers[0]; 35 resultBuffer.ReadPixels(new Rect(0, 0, 1, 1), 0, 0); 36 RenderTexture.active = currentTarget; 37 var result = resultBuffer.GetPixel(0, 0); 38 TargetRenderers.Clear(); 39 TargetMaterials.Clear(); 40 return result.r > 0.5f; 41 } 42 43 private static void CreateMaterialIfNeeded(string shaderName, ref Material material) 44 { 45 if (material != null) 46 { 47 return; 48 } 49 50 material = new Material(FindShader(shaderName)); 51 } 52 53 private static Shader FindShader(string shaderName) 54 { 55 var shader = Shader.Find(shaderName); 56 if (shader == null) 57 { 58 throw new ShaderNotFoundException($"{shaderName} not found."); 59 } 60 61 return shader; 62 } 63 64 private static void DestroyResources() 65 { 66 if (obstacleMapCameraTransform != null) 67 { 68 Object.Destroy(obstacleMapCameraTransform.gameObject); 69 } 70 71 Object.Destroy(ObstacleMap); 72 foreach (var buffer in IntermediateBuffers) 73 { 74 Object.Destroy(buffer); 75 } 76 77 Object.Destroy(resultBuffer); 78 Object.Destroy(checkMaterial); 79 Object.Destroy(reductionMaterial); 80 Application.quitting -= DestroyResources; 81 } 82 83 [RuntimeInitializeOnLoadMethod] 84 private static void RegisterDestroyResources() 85 { 86 Application.quitting += DestroyResources; 87 } 88 89 public class ShaderNotFoundException : Exception 90 { 91 public ShaderNotFoundException(string message) : base(message) 92 { 93 } 94 } 95 96 private struct RenderingCommand 97 { 98 public Renderer Renderer; 99 public int SubmeshIndex; 100 public bool IsCutout; 101 public Texture Texture; 102 public Vector4 ScaleOffset; 103 public Color Color; 104 public float Cutoff; 105 } 106}

今回の実験では、判定メソッドをCaptureObstaclesCheckの2段階に分けました。まずCaptureObstaclesで爆心地から見た周囲の状況をキャプチャーします。結果をスカイボックスとして可視化すると下図のように見えます。

図2

その後、CheckではオブジェクトのレンダラーのバウンディングボックスをBlastAreaRaycasterのときと同様に狙って、判定用のレンダリングを行います。描画された判定結果は、ikadzuchiさんの案を採用して1x1に縮小してから読み取りました。

図3

メソッドを2段階に分けたのは、複数個のオブジェクトを判定する場合の効率化を狙ってのことです。障害物マップは爆心地の位置と半径、および半径内の障害物が変化しない限りは再利用できますので、CaptureObstaclesを1回行ってから爆風が当たる可能性のあるオブジェクトにそれぞれCheckを行うことにすれば、やや高コストであるキューブマップレンダリングの回数を抑えられるだろうと考えました。

実行時の様子は下図のようになりました。CaptureObstaclesはリフレクションプローブを更新するのと同じぐらいの、Checkはオブジェクトをレンダリングするのと同じぐらいの描画コストがかかるだろうと思いますが、判定の解像度はレイ数万~100万本に相当するレベルが期待できますので、精密な判定が必要なら手段の候補に入れてみてもいいんじゃないかと思います。

図4

ただし判定はコライダーではなくレンダラーに基づいていますので、ご質問者さんがおっしゃるようなレンダラーとコライダーの形が違うケースでは使いづらいかもしれません。コライダーの形で判定するには、コライダーの形の判定用メッシュを生成するなどのさらなる改造が必要になるかと思います。また、判定結果を取得するコストを削減するため1x1ピクセルに縮小する処理を加えましたので、どの位置に当たったかを特定する情報は失われてしまうのも弱点と言えそうです。

投稿2023/11/12 12:02

Bongo

総合スコア10807

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

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

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

まだベストアンサーが選ばれていません

会員登録して回答してみよう

アカウントをお持ちの方は

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

ただいまの回答率
85.47%

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

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

質問する

関連した質問