teratail header banner
teratail header banner
質問するログイン新規登録

回答編集履歴

1

軸平行くり抜きの例を追記

2019/09/19 20:32

投稿

Bongo
Bongo

スコア10816

answer CHANGED
@@ -1,4 +1,201 @@
1
1
  見た目がくり抜かれた形になればいいのでしたら、以前[C# - オブジェクトが重なった部分のみを切り取る。|teratail](https://teratail.com/questions/102477)でも挙げさせていただいた[Unity でスクリーンスペースのブーリアン演算をやってみた - 凹みTips](http://tips.hecomi.com/entry/2016/09/10/191006)の手法が使えそうです。レンダリングモードをDeferredにしないといけないという制限はありますが、いい感じにくり抜けるんじゃないかと思います。
2
2
 
3
3
  ご質問者さんのおっしゃる「メッシュを組みなおして重なっていない部分のみのオブジェクトを生成すれば」というアプローチもおそらく可能でしょう(私は実装した経験はないですが...)。
4
- たとえばちょっと検索してみたところ[Simple and Robust Boolean Operations for Triangulated Surfaces](https://arxiv.org/abs/1308.4434)([PDF](https://arxiv.org/pdf/1308.4434.pdf))なんて論文が出てきました。こういった方法ならメッシュが本当にくり抜かれた形になるので、MeshColliderを使えばくり抜かれた形通りに衝突判定を行うなんてことも可能かもしれません。
4
+ たとえばちょっと検索してみたところ[Simple and Robust Boolean Operations for Triangulated Surfaces](https://arxiv.org/abs/1308.4434)([PDF](https://arxiv.org/pdf/1308.4434.pdf))なんて論文が出てきました。こういった方法ならメッシュが本当にくり抜かれた形になるので、MeshColliderを使えばくり抜かれた形通りに衝突判定を行うなんてことも可能かもしれません。
5
+
6
+ **追記**
7
+ 「[unity上で三次元座標の二点を選択して、直方体のブロックを生成したい](https://teratail.com/questions/210981)」を拝見しますに、くり抜く側もくり抜かれる側も軸平行な直方体だという制限を設けてもいいのでしょうかね?
8
+ でしたら問題がだいぶ簡略化でき、最初の回答で例示しました論文のような複雑なことをしなくても済むかもしれません。
9
+
10
+ 今さらながらその条件で実験してみました。軸平行にくり抜いていくなら、くり抜かれた結果は直方体の集まりとして表現できるはずだと思い、くり抜く側の上・下・左・右・前・後の6面でくり抜かれる側を切断して小さな直方体に分割するという方針で行きました。
11
+
12
+ 直方体を最大隅・最小隅の2点で表現する構造体を作り...
13
+
14
+ ```C#
15
+ using System;
16
+ using System.Collections.Generic;
17
+ using UnityEngine;
18
+
19
+ public struct Box
20
+ {
21
+ public readonly Vector3 Max;
22
+ public readonly Vector3 Min;
23
+ public readonly Vector3 Center;
24
+ public readonly Vector3 Size;
25
+ public readonly bool HasVolume;
26
+
27
+ // 対角隅2点を指定してBoxを作る
28
+ public Box(Vector3 cornerA, Vector3 cornerB)
29
+ {
30
+ this.Max = Vector3.Max(cornerA, cornerB);
31
+ this.Min = Vector3.Min(cornerA, cornerB);
32
+ this.Center = (this.Max + this.Min) * 0.5f;
33
+ this.Size = this.Max - this.Min;
34
+ this.HasVolume = (this.Size.x * this.Size.y * this.Size.z) > 0.0f;
35
+ }
36
+
37
+ // Cubeを伸縮させたオブジェクトをもとにBoxを作る
38
+ public Box(Transform cube)
39
+ {
40
+ if (cube == null)
41
+ {
42
+ this = new Box();
43
+ return;
44
+ }
45
+
46
+ var position = cube.position;
47
+ var halfExtents = cube.localScale * 0.5f;
48
+ this = new Box(position + halfExtents, position - halfExtents);
49
+ }
50
+
51
+ // BoxをもとにCubeを伸縮させたオブジェクトを作る
52
+ public Transform ToCube(Transform parent = null, Material sharedMaterial = null)
53
+ {
54
+ if (!this.HasVolume)
55
+ {
56
+ return null;
57
+ }
58
+
59
+ var cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
60
+ var cubeTransform = cube.transform;
61
+ cubeTransform.position = this.Center;
62
+ cubeTransform.localScale = this.Size;
63
+ cubeTransform.SetParent(parent);
64
+ if (sharedMaterial != null)
65
+ {
66
+ cubeTransform.GetComponent<Renderer>().sharedMaterial = sharedMaterial;
67
+ }
68
+
69
+ return cubeTransform;
70
+ }
71
+
72
+ // Boxを特定の軸で切断し2つのBoxに分ける
73
+ // 軸番号はXが0、Yが1、Zが2
74
+ public (Box upper, Box lower) Cut(float at, int axis)
75
+ {
76
+ if (!this.HasVolume)
77
+ {
78
+ return (new Box(), new Box());
79
+ }
80
+
81
+ var sourceMax = this.Max[axis];
82
+ var sourceMin = this.Min[axis];
83
+ if (at <= sourceMin)
84
+ {
85
+ return (this, new Box());
86
+ }
87
+
88
+ if (at >= sourceMax)
89
+ {
90
+ return (new Box(), this);
91
+ }
92
+
93
+ var lowerMin = this.Min;
94
+ var lowerMax = this.Max;
95
+ lowerMax[axis] = at;
96
+ var upperMin = this.Min;
97
+ var upperMax = this.Max;
98
+ upperMin[axis] = at;
99
+ return (new Box(upperMin, upperMax), new Box(lowerMin, lowerMax));
100
+ }
101
+
102
+ // Boxを別のBoxでくり抜き、できあがった断片をnewBoxesに詰める
103
+ public void Carve(Box carver, List<Box> newBoxes)
104
+ {
105
+ if (newBoxes == null)
106
+ {
107
+ throw new InvalidOperationException($"{nameof(newBoxes)} must not be null.");
108
+ }
109
+
110
+ if (!this.HasVolume)
111
+ {
112
+ throw new InvalidOperationException($"carvee must have volume.");
113
+ }
114
+
115
+ newBoxes.Clear();
116
+ var residue = this;
117
+
118
+ // X、Y、Z軸についてそれぞれ...
119
+ for (var i = 0; i < 3; i++)
120
+ {
121
+ // 最小側の面で切断
122
+ var (upper, lower) = residue.Cut(carver.Min[i], i);
123
+ if (lower.HasVolume)
124
+ {
125
+ newBoxes.Add(lower);
126
+ }
127
+
128
+ if (!upper.HasVolume)
129
+ {
130
+ break;
131
+ }
132
+
133
+ residue = upper;
134
+
135
+ // 最大側の面で切断
136
+ (upper, lower) = residue.Cut(carver.Max[i], i);
137
+ if (upper.HasVolume)
138
+ {
139
+ newBoxes.Add(upper);
140
+ }
141
+
142
+ if (!lower.HasVolume)
143
+ {
144
+ break;
145
+ }
146
+
147
+ residue = lower;
148
+ }
149
+ }
150
+ }
151
+ ```
152
+
153
+ くり抜かれる側には下記のようなスクリプトをアタッチして...
154
+ ```C#
155
+ using System.Collections.Generic;
156
+ using System.Linq;
157
+ using UnityEngine;
158
+
159
+ public class Carvee : MonoBehaviour
160
+ {
161
+ public void Carve(Transform carverTransform)
162
+ {
163
+ if (carverTransform == null)
164
+ {
165
+ return;
166
+ }
167
+
168
+ var carver = new Box(carverTransform);
169
+ if (!carver.HasVolume)
170
+ {
171
+ return;
172
+ }
173
+
174
+ // くり抜かれる側自体は空オブジェクトになっており、子オブジェクトとしてCubeオブジェクトを持っている
175
+ // Carveを実行すると子オブジェクトをそれぞれくり抜いて、断片を再び自身の子とする
176
+ var newBoxes = new List<Box>();
177
+ var carveeTransforms = this.transform.Cast<Transform>().ToArray();
178
+ foreach (var carveeTransform in carveeTransforms)
179
+ {
180
+ var carvee = new Box(carveeTransform);
181
+ if (!carvee.HasVolume)
182
+ {
183
+ continue;
184
+ }
185
+
186
+ carvee.Carve(carver, newBoxes);
187
+ var carveeMaterial = carveeTransform.GetComponent<Renderer>().sharedMaterial;
188
+ foreach (var newBox in newBoxes)
189
+ {
190
+ newBox.ToCube(this.transform, carveeMaterial);
191
+ }
192
+
193
+ Destroy(carveeTransform.gameObject);
194
+ }
195
+ }
196
+ }
197
+ ```
198
+
199
+ その他オブジェクト操作用のスクリプト(コードは省略)を付けて実行したところ、下図のような感じになりました。
200
+
201
+ ![図](4e85737ae8b35f380ce91112df18f817.gif)