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

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

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

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

Q&A

解決済

4回答

4216閲覧

unity 切り取り

tttai

総合スコア5

Unity3D

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

0グッド

0クリップ

投稿2020/04/07 11:02

編集2020/04/09 13:30

前提・実現したいこと

unity 3Dで四角からほかのオブジェクトの重なっている部分を切り取ったメッシュを作成したい

スニッパーズのように四角がほかのオブジェクトと重なっている部分を切り取って、切り取られたメッシュを生成したいのですが、ブーリアン演算を使って切り取った場合にコリジョンが変更されるかわからないので教えていただきたいです。(ゲームは3Dモデルを使った2Dパズルを作ろうとしています)

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

エラーメッセージ

該当のソースコード

ソースコード

試したこと

ここに問題に対して試したことを記載してください。

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

![イメージ説明]イメージ説明![イメージ説明]

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

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

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

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

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

Bongo

2020/04/07 22:29

「ブーリアン演算を使って切り取った場合に」とのことですが、もっと具体的にはどのようなアルゴリズムで切り取るプランなのかご説明いただけるでしょうか。作りかけのコードがありましたらご提示いただけると参考になりそうです。あるいは「このWebサイトで紹介されている方法が利用できないかと思っている」みたいなアテがありましたら、それをご紹介いただいてもいいと思います。 コリジョンに関しては、以前「Unity2D:オブジェクトを二つに自由切断する方法」(https://teratail.com/questions/204249 )という件で2D形状の切断方法を検討してみたことがあるのですが、あちらの時のように切断された形の外周パスを求められるような切断方法ならPolygonCollider2Dが使えるだろうと思います。ですが画像処理に近い方法で切断されたように見せる場合...たとえば「SpriteをSpriteでくり抜いて、くり抜かれた部分は別の場所に描画させたい」(https://teratail.com/questions/125072 )のようなやり方ですと、コリジョンはちょっと工夫が必要そうですね。
tttai

2020/04/08 08:08

「Unity2D:オブジェクトを二つに自由切断する方法」(https://teratail.com/questions/204249 )を拝見させてもらったのですが、この方法を3Dオブジェクトにも適用できるんでしょうか?unity初心者なので分からないところだらけで申し訳ないです。
Bongo

2020/04/08 13:11

そうですね、ゲーム画面上の映像は3Dオブジェクトで構成されているとのことですが、ゲームロジック部分は2Dということでしたら「Unity2D:オブジェクトを二つに自由切断する方法」のPolygonCollider2Dのやり方に近い方法が使えるんじゃないかと思います。 もし本当に3D空間上の衝突判定を取りたいとなると、「unityでgameobjectを部分的に消去したい」(https://teratail.com/questions/210980 )で参考情報として挙げさせていただいた「Simple and Robust Boolean Operations for Triangulated Surfaces」(https://arxiv.org/pdf/1308.4434.pdf )のような方法で3Dソフトのブーリアンモデリング機能のようなものを実装する必要があるでしょうが、衝突判定だけでも2Dで済ましてかまわなければ見た目だけくりぬくのでも十分に思われます。 見た目をくり抜く候補としては「Unity でスクリーンスペースのブーリアン演算をやってみた - 凹みTips」(http://tips.hecomi.com/entry/2016/09/10/191006 )のようなやり方があるでしょうし、視点が真っ正面に固定されているのなら、もっとシンプルにステンシル機能を使って済ませることができるかもしれません。 面白そうなテーマですので(参考元のスニッパーズの紹介ムービーも見てみましたが、こちらも独創的で面白そうですね)何かしらサンプルコードでもお出ししてご協力したいところではありますが、やるにしても時間の取れる休日に取り組む必要がありそうです。 欲を言えばですが、ご質問者さんがやりたいことをもっと具体的に想像できるようなイメージ図...たとえばUnityなり他の3Dソフトなりで3Dオブジェクトを配置して、映像を画像処理ソフトで加工したりして模擬的にくり抜いた様子を再現したものをお見せいただけると参考になりそうです。
tttai

2020/04/08 13:49

視点は正面に固定でZ軸の移動などはないので当たり判定などは2DでOKです!! ゲームの見栄えをよくするために3D オブジェクトを使用しています! 映像はすぐに作るのででき次第、質問に追加させていただきます。 一人では全く見当がつかず悩んでいたので答えていただいてものすごくありがたいです!
guest

回答4

0

ベストアンサー

パート3...シェーダーコードとその他のスクリプト、および実行結果

そして、マスキング処理を担当する下記2つのシェーダーを用意し、これらをインスペクター上で上記スクリプトのcarverMaskShadercarverMaskCleanerShaderにセットしておきます。セットしておかなくても名前で検索してセットするようにはしましたが、その時はビルドして実行する場合Graphics設定の「Always Included Shaders」にこれらを追加しておかないと見つからなくなるかと思います。

CarverMaskは、描画されるたびにステンシルバッファ上の値の下1桁を0から1に、1から0に切り替えます。Carver上での処理によって、マスキング用メッシュの穴の開いた部分は外周パスの内側に内包される形になっているので、穴はメッシュが偶数回重なって0となります。実際にオブジェクトを描画する際は1の部分だけに描画することで、マスクの形にオブジェクトがくり抜かれるだろうという目論見です。

ShaderLab

1Shader "Hidden/CarverMask" 2{ 3 Properties 4 { 5 } 6 SubShader 7 { 8 Pass 9 { 10 ColorMask 0 11 Cull Back 12 ZTest Always 13 ZWrite Off 14 Stencil 15 { 16 Ref 1 17 WriteMask 1 18 Pass IncrWrap 19 } 20 21 CGPROGRAM 22 #pragma vertex vert 23 #pragma fragment frag 24 #include "UnityCG.cginc" 25 26 float4 vert(float3 vertex : POSITION) : SV_POSITION 27 { 28 return UnityObjectToClipPos(vertex); 29 } 30 31 fixed4 frag() : SV_Target 32 { 33 return 0; 34 } 35 ENDCG 36 } 37 } 38}

CarverMaskCleanerはステンシルバッファ上の値の下1桁を0に書き換えるもので、一つのオブジェクトの描画が終わったら画面全体をこれで塗りつぶし、次のオブジェクトの描画に備えます。

ShaderLab

1Shader "Hidden/CarverMaskCleaner" 2{ 3 Properties 4 { 5 } 6 SubShader 7 { 8 Pass 9 { 10 ColorMask 0 11 Cull Back 12 ZTest Always 13 ZWrite Off 14 Stencil 15 { 16 Ref 0 17 WriteMask 1 18 Pass Replace 19 } 20 21 CGPROGRAM 22 #pragma vertex vert 23 #pragma fragment frag 24 #include "UnityCG.cginc" 25 26 float4 vert(float3 vertex : POSITION) : SV_POSITION 27 { 28 return UnityObjectToClipPos(vertex); 29 } 30 31 fixed4 frag() : SV_Target 32 { 33 return 0; 34 } 35 ENDCG 36 } 37 } 38}

そしてこれもまた不便な点で申しわけないですが、先に申し上げたように、くり抜き処理に参加させたいオブジェクトはステンシルバッファの値の下1桁が1の時だけ描画するようなシェーダーを使う必要があります。あとで図示しますCapsule、Torus、Teapotには下記のシェーダーを使ったマテリアルを割り当てています。

ShaderLab

1Shader "Custom/CarverStandard" 2{ 3 Properties 4 { 5 _Color ("Color", Color) = (1,1,1,1) 6 _MainTex ("Albedo (RGB)", 2D) = "white" {} 7 _Glossiness ("Smoothness", Range(0,1)) = 0.5 8 _Metallic ("Metallic", Range(0,1)) = 0.0 9 } 10 SubShader 11 { 12 Tags { "RenderType"="Opaque" } 13 Stencil 14 { 15 Ref 1 16 ReadMask 1 17 Comp Equal 18 } 19 CGPROGRAM 20 #pragma surface surf Standard fullforwardshadows 21 #pragma target 3.0 22 23 sampler2D _MainTex; 24 25 struct Input 26 { 27 float2 uv_MainTex; 28 }; 29 30 half _Glossiness; 31 half _Metallic; 32 fixed4 _Color; 33 34 void surf(Input IN, inout SurfaceOutputStandard o) 35 { 36 fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color; 37 o.Albedo = c.rgb; 38 o.Metallic = _Metallic; 39 o.Smoothness = _Glossiness; 40 o.Alpha = c.a; 41 } 42 ENDCG 43 } 44 FallBack "Diffuse" 45}

ユニティちゃんのマテリアルについては省略しますが、変更点はそれぞれ単に

ShaderLab

1 Stencil 2 { 3 Ref 1 4 ReadMask 1 5 Comp Equal 6 }

を追加しただけです。

また、下記のスクリプトをアタッチした空オブジェクトをシーン上に配置しています。
ステンシル機能を使って描画するかどうかを選択している都合上、非プレイモードだとオブジェクトが常に表示されなくなってしまい不便なので、各カメラがオブジェクトのレンダリングを行う前にステンシルバッファ全面を1で塗りつぶします。
プレイモードならばAwake時に自分自身を削除し、その機能を無効化するようにしました。

このスクリプトが有効化された時に一度だけ塗りつぶし処理の差し込みを行う仕様ですので、スクリプトを編集したりして再読み込みが発生した場合にタイミング的な問題で表示が消えてしまったり、2枚目のシーンビューを開いたりするとそちらには表示されなかったり...という風に不親切な点があります。一旦このスクリプトのチェックボックスを外して無効化し、再度チェックして有効化すれば復活するかと思います。

C#

1using System.Linq; 2using UnityEngine; 3using UnityEngine.Rendering; 4#if UNITY_EDITOR 5using UnityEditor; 6#endif 7 8[ExecuteAlways] 9[DefaultExecutionOrder(1)] 10public class EditorModeStencilWriter : MonoBehaviour 11{ 12 #if UNITY_EDITOR 13 [SerializeField] private Shader maskShader; 14 private static Material maskMaterial; 15 private static CommandBuffer commands; 16 private static Camera[] cameras; 17 18 private void Awake() 19 { 20 if (EditorApplication.isPlayingOrWillChangePlaymode) 21 { 22 Destroy(this); 23 } 24 } 25 26 private void OnEnable() 27 { 28 if (maskMaterial == null) 29 { 30 if (this.maskShader == null) 31 { 32 this.maskShader = Shader.Find("Hidden/CarverMask"); 33 } 34 35 if (this.maskShader != null) 36 { 37 maskMaterial = new Material(this.maskShader); 38 } 39 } 40 41 if (commands != null) 42 { 43 return; 44 } 45 46 commands = new CommandBuffer {name = "Fill Stencil Buffer"}; 47 commands.Blit(EditorGUIUtility.whiteTexture, BuiltinRenderTextureType.CameraTarget, maskMaterial); 48 cameras = SceneView.GetAllSceneCameras().Concat(Camera.allCameras).Select( 49 cam => 50 { 51 cam.AddCommandBuffer(CameraEvent.BeforeForwardOpaque, commands); 52 return cam; 53 }).ToArray(); 54 } 55 56 private void OnDisable() 57 { 58 if (cameras == null) 59 { 60 return; 61 } 62 63 foreach (var cam in cameras) 64 { 65 if (cam == null) 66 { 67 continue; 68 } 69 70 cam.RemoveCommandBuffer(CameraEvent.BeforeForwardOpaque, commands); 71 } 72 73 cameras = null; 74 commands = null; 75 } 76 77 #else 78 private void Awake() 79 { 80 Destroy(this); 81 } 82 #endif 83}

そして、動作確認のために下記スクリプトをアタッチしたオブジェクトをシーン上に配置しています。
ドラッグで各オブジェクトを移動できるようにするとともに、スペースバーを押しながらドラッグを開始した場合、衝突判定を無効化して他のオブジェクトと重ね合わせることができるようになります。
その状態でドロップしたときに、そのオブジェクトを周りのオブジェクトでくり抜くようにしてみました。

C#

1using System.Collections.Generic; 2using System.Linq; 3using UnityEngine; 4 5public class MouseAction : MonoBehaviour 6{ 7 private Camera mainCamera; 8 private Transform dragTarget; 9 private Vector3 dragTargetOrigin; 10 private Vector3 dragOrigin; 11 private bool carveWhenDrop; 12 13 private void Start() 14 { 15 this.mainCamera = Camera.main; 16 } 17 18 private void Update() 19 { 20 if (this.dragTarget == null) 21 { 22 if (Input.GetMouseButtonDown(0)) 23 { 24 var mouseRay = this.mainCamera.ScreenPointToRay(Input.mousePosition); 25 var hit = Physics2D.Raycast(mouseRay.origin, mouseRay.direction); 26 if (hit.collider != null) 27 { 28 this.dragTarget = hit.transform; 29 this.dragTargetOrigin = this.dragTarget.position; 30 this.dragOrigin = hit.point; 31 if (Input.GetKey(KeyCode.Space)) 32 { 33 hit.collider.isTrigger = true; 34 if (hit.rigidbody != null) 35 { 36 hit.rigidbody.bodyType = RigidbodyType2D.Kinematic; 37 } 38 39 this.carveWhenDrop = true; 40 } 41 } 42 } 43 } 44 else 45 { 46 if (Input.GetMouseButton(0)) 47 { 48 var mouseRay = this.mainCamera.ScreenPointToRay(Input.mousePosition); 49 var xyPlane = new Plane(Vector3.back, Vector3.zero); 50 if (xyPlane.Raycast(mouseRay, out var enter)) 51 { 52 var deltaPosition = mouseRay.GetPoint(enter) - this.dragOrigin; 53 this.dragTarget.position = this.dragTargetOrigin + deltaPosition; 54 } 55 } 56 else if (Input.GetMouseButtonUp(0)) 57 { 58 var targetRigidbody = this.dragTarget.GetComponent<Rigidbody2D>(); 59 if (targetRigidbody != null) 60 { 61 targetRigidbody.velocity = Vector2.zero; 62 if (this.carveWhenDrop) 63 { 64 targetRigidbody.bodyType = RigidbodyType2D.Dynamic; 65 } 66 } 67 68 if (this.carveWhenDrop) 69 { 70 var targetCollider = this.dragTarget.GetComponent<Collider2D>(); 71 if (targetCollider != null) 72 { 73 targetCollider.isTrigger = false; 74 var overlappingColliders = new List<Collider2D>(); 75 targetCollider.OverlapCollider(new ContactFilter2D(), overlappingColliders); 76 var carvers = overlappingColliders.Select(c => c.GetComponentInChildren<Carver>()) 77 .Where(c => c != null); 78 var thisCarver = targetCollider.GetComponentInChildren<Carver>(); 79 if (thisCarver != null) 80 { 81 Debug.Log( 82 $"Carve {targetCollider.name} with {string.Join(", ", carvers.Select(c => c.Collider2D.name))}."); 83 Carver.Carve(thisCarver, carvers); 84 } 85 } 86 87 this.carveWhenDrop = false; 88 } 89 90 this.dragTarget = null; 91 } 92 } 93 } 94 95 private void OnGUI() 96 { 97 if (this.carveWhenDrop) 98 { 99 using (new GUILayout.AreaScope(new Rect(0, 0, Screen.width, Screen.height))) 100 { 101 using (new GUILayout.HorizontalScope()) 102 { 103 GUILayout.FlexibleSpace(); 104 GUILayout.Label("Let's Carve!"); 105 GUILayout.FlexibleSpace(); 106 } 107 } 108 } 109 } 110}

シーンの準備が終わると、非プレイモードでは下図のような状態になっています。

図1

そこからプレイモードに移行すると下図のような状態に変わります。くり抜きに参加するオブジェクトにそれぞれ親オブジェクトが追加され、それにコライダーが付けられています。
見た目上の特徴として、特にユニティちゃんで顕著ですが、オブジェクトの上に落ちる影が消失しています。先ほどオブジェクトが落とす影については手抜きしたと申し上げましたが、そのせいでオブジェクト上の影が正しく表現されず見苦しかったため、影は受けないようにしてしまいました。

図2

動かすと下図のようになりました。SkinnedMeshRendererをいくつも持つオブジェクトに対してもくり抜けるか確認したかったためユニティちゃんを参加させましたが、ちょっとした残虐表現になってしまって彼女には悪く思います...

図3

なるべく簡潔にしようと思ったものの、何だかんだで長くなってしまいすみません。また、実験は2019.3.8f1でやったのですが、Unityのバージョンが古いと使えない記述が混じっている可能性があります。たとえば描画の際にForwardBaseパスだけを選ぶのに使っているFindPassTagValueは、2018以前のバージョンには搭載されていないんじゃないかと思います。

投稿2020/04/13 15:42

Bongo

総合スコア10811

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

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

0

パート2...Carverスクリプトの続き

C#

1 private static IEnumerable<(Transform, Vector3)> EnumerateScaledTransformsInParent(Transform fromTransform) 2 { 3 do 4 { 5 var localScale = fromTransform.localScale; 6 if (localScale != Vector3.one) 7 { 8 yield return (fromTransform, localScale); 9 } 10 11 fromTransform = fromTransform.parent; 12 } while (fromTransform != null); 13 } 14 15 // 自身の階層下からコライダーの原型とするメッシュを収集する 16 // オリジナルのメッシュをそのまま返すのではなく、念のため複製を返すことにした 17 // SkinnedMeshRendererについてもBakeMeshで現在の形をキャプチャーして返すようにしたが、 18 // BakeMeshはモデルが拡大縮小されていると正しく見た目通りの形をキャプチャーしてくれないようで 19 // 苦肉の策として一時的になるべくスケールが等倍になるようにしてからキャプチャーし 20 // 後で元に戻すことにした 21 private (Transform, Mesh)[] GatherMeshes() 22 { 23 var meshFilters = this.GetComponentsInChildren<MeshFilter>(); 24 var skinnedMeshRenderers = this.GetComponentsInChildren<SkinnedMeshRenderer>(); 25 if (skinnedMeshRenderers.Select(smr => smr.transform.lossyScale).Distinct().Skip(1).Any()) 26 { 27 Debug.LogWarning($"{this.gameObject.name} has complexly scaled transform hierarchy! It may cause wrong mesh generation."); 28 } 29 30 var scaledTransforms = EnumerateScaledTransformsInParent(this.transform).ToArray(); 31 foreach (var (scaledTransform, _) in scaledTransforms) 32 { 33 scaledTransform.localScale = Vector3.one; 34 } 35 36 var result = meshFilters.Select(mf => (mf.transform, Instantiate(mf.sharedMesh))).Concat( 37 skinnedMeshRenderers.Select( 38 smr => 39 { 40 var mesh = new Mesh(); 41 smr.BakeMesh(mesh); 42 return (smr.transform, mesh); 43 })).ToArray(); 44 foreach (var (scaledTransform, localScale) in scaledTransforms) 45 { 46 scaledTransform.localScale = localScale; 47 } 48 49 return result; 50 } 51 52 // メッシュの頂点をワールド空間に移し、Z方向に圧縮したものをローカル空間に戻し、Triangle配列として返す 53 private Triangle[] GetTrianglesFromMesh((Transform, Mesh)[] meshes) 54 { 55 var worldToLocal = this.Collider2D.transform.worldToLocalMatrix; 56 var worldZ = this.Collider2D.transform.position.z; 57 return meshes.SelectMany( 58 pair => 59 { 60 var (t, m) = pair; 61 var localToWorld = t.localToWorldMatrix; 62 var vertices = m.vertices; 63 var indices = m.triangles; 64 return Enumerable.Range(0, indices.Length / 3).Select( 65 i => 66 { 67 var j = i * 3; 68 var pw0 = localToWorld.MultiplyPoint3x4(vertices[indices[j]]); 69 var pw1 = localToWorld.MultiplyPoint3x4(vertices[indices[j + 1]]); 70 var pw2 = localToWorld.MultiplyPoint3x4(vertices[indices[j + 2]]); 71 var crossZ = CrossZ(pw1 - pw0, pw2 - pw0); 72 return (pw0, pw1, pw2, crossZ); 73 }).Where(face => Mathf.Abs(face.crossZ) > 0.0f).Select( 74 face => 75 { 76 face.pw0.z = worldZ; 77 face.pw1.z = worldZ; 78 face.pw2.z = worldZ; 79 if (face.crossZ < 0.0f) 80 { 81 return new Triangle 82 { 83 Vertex0 = worldToLocal.MultiplyPoint3x4(face.pw0), 84 Vertex1 = worldToLocal.MultiplyPoint3x4(face.pw1), 85 Vertex2 = worldToLocal.MultiplyPoint3x4(face.pw2) 86 }; 87 } 88 89 return new Triangle 90 { 91 Vertex0 = worldToLocal.MultiplyPoint3x4(face.pw0), 92 Vertex1 = worldToLocal.MultiplyPoint3x4(face.pw2), 93 Vertex2 = worldToLocal.MultiplyPoint3x4(face.pw1) 94 }; 95 }); 96 }).ToArray(); 97 } 98 99 private static float CrossZ(Vector3 lhs, Vector3 rhs) 100 { 101 return (lhs.x * rhs.y) - (lhs.y * rhs.x); 102 } 103 104 // このオブジェクトに親オブジェクトを作ってPolygonCollider2Dを取り付け、 105 // さらに描画処理を担当するRenderingHelperもアタッチする 106 private PolygonCollider2D CreateCollider() 107 { 108 var siblingIndex = this.transform.GetSiblingIndex(); 109 var colliderObject = new GameObject(this.gameObject.name); 110 colliderObject.AddComponent<RenderingHelper>().Carver = this; 111 colliderObject.transform.SetParent(this.transform.parent, false); 112 colliderObject.transform.SetSiblingIndex(siblingIndex); 113 colliderObject.transform.position = this.transform.position; 114 colliderObject.transform.rotation = Quaternion.identity; 115 this.transform.SetParent(colliderObject.transform); 116 var collider = colliderObject.AddComponent<PolygonCollider2D>(); 117 if (this.attachRigidbodyOnCreateCollider) 118 { 119 colliderObject.AddComponent<Rigidbody2D>(); 120 } 121 122 collider.isTrigger = this.makeColliderTriggerOnCreateCollider; 123 return collider; 124 } 125 126 // 実際に目に見えるオブジェクトの姿を描画するのはこれが担当する 127 // OnWillRenderObjectタイミングで今このオブジェクトを描画しようとしているカメラを取得し、 128 // まだCommandBufferが挿入されていなければ追加する 129 // 実行中にオブジェクトの数が変動する可能性を考慮し、CommandBufferは毎フレーム再構築する 130 private class RenderingHelper : MonoBehaviour 131 { 132 private static readonly int DummyTexture = Shader.PropertyToID("_DummyTex"); 133 private static readonly HashSet<RenderingHelper> RenderingHelpers = new HashSet<RenderingHelper>(); 134 private static CommandBuffer opaqueCommands; 135 private static CommandBuffer transparentCommands; 136 private static readonly HashSet<Camera> Cameras = new HashSet<Camera>(); 137 private static bool NeedsUpdateCommands; 138 [NonSerialized] public Carver Carver; 139 private MeshRenderer maskRenderer; 140 141 private void Update() 142 { 143 NeedsUpdateCommands = true; 144 } 145 146 private void OnWillRenderObject() 147 { 148 var currentCamera = Camera.current; 149 if (currentCamera == null) 150 { 151 return; 152 } 153 154 CreateCommandsIfNeeded(); 155 if (!Cameras.Contains(currentCamera)) 156 { 157 AddCommands(currentCamera); 158 } 159 160 if (!NeedsUpdateCommands) 161 { 162 return; 163 } 164 165 UpdateCommands(); 166 NeedsUpdateCommands = false; 167 } 168 169 private static void CreateCommandsIfNeeded() 170 { 171 if (opaqueCommands == null) 172 { 173 opaqueCommands = new CommandBuffer {name = "RenderCarversOpaque"}; 174 } 175 176 if (transparentCommands == null) 177 { 178 transparentCommands = new CommandBuffer {name = "RenderCarversTransparent"}; 179 } 180 } 181 182 private static void AddCommands(Camera cam) 183 { 184 cam.AddCommandBuffer(CameraEvent.AfterForwardOpaque, opaqueCommands); 185 cam.AddCommandBuffer(CameraEvent.BeforeForwardAlpha, transparentCommands); 186 Cameras.Add(cam); 187 } 188 189 // レンダリングはForwardBaseを前提とし、邪魔なパスをスキップするようにした 190 // 透明オブジェクトや不透明オブジェクトが複雑に入り組んでいる可能性を考慮するなら 191 // もっと細かく描画順を制御する必要があるだろうが、そこまでやるとややこしくなるため 192 // 妥協して大ざっぱなCarver単位での制御にとどめた 193 private static void UpdateCommands() 194 { 195 var lightModeTag = new ShaderTagId("LightMode"); 196 var forwardBaseTag = new ShaderTagId("ForwardBase"); 197 198 void DrawForwardBasePass(CommandBuffer commands, Material m, Renderer r, int i) 199 { 200 var shader = m.shader; 201 var passCount = m.passCount; 202 for (var passIndex = 0; passIndex < passCount; passIndex++) 203 { 204 if (shader.FindPassTagValue(passIndex, lightModeTag) == forwardBaseTag) 205 { 206 commands.DrawRenderer(r, m, i, passIndex); 207 } 208 } 209 } 210 211 opaqueCommands.Clear(); 212 transparentCommands.Clear(); 213 opaqueCommands.GetTemporaryRT(DummyTexture, 1, 1); 214 transparentCommands.GetTemporaryRT(DummyTexture, 1, 1); 215 opaqueCommands.EnableShaderKeyword("LIGHTPROBE_SH"); 216 transparentCommands.EnableShaderKeyword("LIGHTPROBE_SH"); 217 foreach (var helper in RenderingHelpers) 218 { 219 if (helper.maskRenderer == null) 220 { 221 helper.maskRenderer = helper.GetComponent<MeshRenderer>(); 222 } 223 224 opaqueCommands.DrawRenderer(helper.maskRenderer, maskMaterial); 225 transparentCommands.DrawRenderer(helper.maskRenderer, maskMaterial); 226 foreach (var (renderer, materials) in helper.Carver.renderers) 227 { 228 foreach (var (material, subMeshIndex) in materials) 229 { 230 DrawForwardBasePass( 231 material.renderQueue <= 2500 ? opaqueCommands : transparentCommands, 232 material, 233 renderer, 234 subMeshIndex); 235 } 236 } 237 238 opaqueCommands.Blit(DummyTexture, BuiltinRenderTextureType.CurrentActive, maskCleanerMaterial); 239 transparentCommands.Blit(DummyTexture, BuiltinRenderTextureType.CurrentActive, maskCleanerMaterial); 240 } 241 242 opaqueCommands.ReleaseTemporaryRT(DummyTexture); 243 transparentCommands.ReleaseTemporaryRT(DummyTexture); 244 } 245 246 private void OnEnable() 247 { 248 RenderingHelpers.Add(this); 249 } 250 251 private void OnDisable() 252 { 253 RenderingHelpers.Remove(this); 254 if (RenderingHelpers.Count != 0) 255 { 256 return; 257 } 258 259 foreach (var cam in Cameras) 260 { 261 if (cam != null) 262 { 263 cam.RemoveCommandBuffer(CameraEvent.AfterForwardOpaque, opaqueCommands); 264 cam.RemoveCommandBuffer(CameraEvent.BeforeForwardAlpha, transparentCommands); 265 } 266 } 267 268 Cameras.Clear(); 269 opaqueCommands = null; 270 transparentCommands = null; 271 } 272 } 273 274 private struct Triangle 275 { 276 public Vector3 Vertex0; 277 public Vector3 Vertex1; 278 public Vector3 Vertex2; 279 280 public IEnumerable<Vector3> Vertices 281 { 282 get 283 { 284 yield return this.Vertex0; 285 yield return this.Vertex1; 286 yield return this.Vertex2; 287 } 288 } 289 } 290}

Carverスクリプトはここまでです。すみませんがまた文字数上限が迫ってきたため、別回答に引き継ぎます...

投稿2020/04/13 15:38

Bongo

総合スコア10811

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

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

0

オブジェクトのアウトラインを求めたり、オブジェクトをくり抜くのにはAngus JohnsonさんによるClipperを使用してみました。C#版のコードをプロジェクトにインポートしてみたところ、特に問題なく使用できるようでした。ドキュメントも図入りでわかりやすく、便利なプログラムだと思います。

また、アウトラインから三角形の集合体を生成するのにはUnity2D:オブジェクトを二つに自由切断する方法の時と同じくrunevisionさんのTriangulatorを使いました。今回はTriangulatorのコードをスクリプト内に埋め込むのではなく、単独のスクリプトとしてプロジェクト内に追加しています。

  1. 3Dオブジェクトの形に添ったPolygonCollider2Dを作る
  2. くり抜き操作によってPolygonCollider2Dのアウトラインを変更する
  3. アウトラインの形に合わせて、3Dオブジェクトをくり抜かれたかのように表示する

の3問題のうち、1と2に関してはClipperのおかげで比較的滞りなく作ることができましたが、3に関しては「シンプルにステンシル機能を使って...」などといいかげんなことを申し上げておきながら、ちょっとやっかいでした。

個々のオブジェクトをそれぞれ異なるステンシルマスクで抜きたかったのですが、通常のレンダリングパイプラインではそこまで細かい描画順コントロールは困難そうでした。代替案として、通常の描画タイミングではオブジェクトが描画されないようにした上でCommandBufferを描画過程の途中に挿入し、そこで実際に画面に映る姿を描かせることにしました。

ですがまだ手抜き部分が多く不完全で、本来のUnityのレンダリングパイプラインと同じ見た目で陰影付けできるかは保証されず、さらにオブジェクトが落とす影についてはノータッチですので、くり抜きによって欠落したはずの部分が影を落としてしまったりしますがご容赦ください。この際、ライトの設定を変更して影をオフにしてしまってもいいかもしれません。

また、くり抜き処理のロジックを検討することが主眼となっており、速度やメモリの効率は大して考慮していません(派手にLINQを使ったり、配列やらをポンポン作ったり...)。ですが、毎フレームくり抜き処理を行おうとするとか、かなりのハイポリゴンを処理しようとするとか、多数のオブジェクトをくり抜きに参加させようとしなければ、実用できないほどに遅くはならないだろうとは思います。

まず、くり抜き処理に参加させたいオブジェクト(くり抜く側、くり抜かれる側のどちらも)には下記Carverスクリプトをアタッチしました。

C#

1using System; 2using System.Collections.Generic; 3using System.Linq; 4using ClipperLib; 5using UnityEngine; 6using UnityEngine.Rendering; 7 8public class Carver : MonoBehaviour 9{ 10 private const float Precision = 1024.0f; 11 private static Material maskMaterial; 12 private static Material maskCleanerMaterial; 13 private static readonly Clipper clipper = new Clipper(Clipper.ioStrictlySimple); 14 [SerializeField] private Shader maskShader; 15 [SerializeField] private Shader maskCleanerShader; 16 [SerializeField] private bool attachRigidbodyOnCreateCollider; 17 [SerializeField] private bool makeColliderTriggerOnCreateCollider; 18 private readonly List<List<IntPoint>> outlines = new List<List<IntPoint>>(); 19 private Mesh pathMesh; 20 private (Renderer, (Material, int)[])[] renderers = new (Renderer, (Material, int)[])[0]; 21 public PolygonCollider2D Collider2D { get; private set; } 22 23 private void Start() 24 { 25 this.FitColliderIntoMeshes(); 26 } 27 28 // PolygonCollider2Dを現在の3Dモデルの見た目に合わせて更新する 29 // まずStartで一度実行されるが、後で再度実行すれば欠損部分が復活することになる 30 // その他、モデルの三次元的な回転などによりモデルのアウトラインが変化した 31 // 場合にもこのメソッドでアウトラインを更新するべきだが、実行速度は 32 // 大して考慮していないため、あまり頻繁に行うのはおすすめできない 33 // なお、モデルの「Read/Write Enabled」をオンにしておかないと失敗すると思われる 34 public void FitColliderIntoMeshes() 35 { 36 // マスク処理用マテリアルが未作成なら作っておく 37 this.CreateMaskMaterialsIfNeeded(); 38 39 // 3Dオブジェクトを使っているということなのでオブジェクトが三次元的に 40 // 回転している可能性があるが、2Dコライダーはtransform.forwardが 41 // ワールドZ+を向いていた方が好都合なため、親オブジェクトを追加して 42 // そこにコライダーを付けることにした 43 // 併せてそれにアタッチされるRenderingHelperが、CommandBufferを使って 44 // 独自にレンダリングを行うことになる 45 if (this.Collider2D == null) 46 { 47 this.Collider2D = this.CreateCollider(); 48 } 49 50 // MeshFilter、またはSkinnedMeshRendererからメッシュを集めて 51 // ワールドZ方向に押し潰し、三角形の集合を得てコライダーの原型とする 52 // 裏向きの三角形も向きを反転した上で列挙しているが、ポリゴンの裏面は 53 // 考慮しなくてもいいのなら、裏は除外してもいいかもしれない 54 this.renderers = this.GetComponentsInChildren<Renderer>().Select( 55 r => (r, r.materials.Select((m, i) => (m, i)).OrderBy(pair => pair.m.renderQueue).ToArray())).ToArray(); 56 var meshes = this.GatherMeshes(); 57 var triangles = this.GetTrianglesFromMesh(meshes); 58 DeleteMeshes(meshes); 59 60 // Clipperを使ってアウトラインを作る 61 // Clipperは右手系の慣習に従うようなので、入力三角形は巡回方向を逆転させて反時計回りを表とする 62 // また、計算上のロバスト性のためClipperは座標を整数として扱うそうなので、まずUnity上の座標値を 63 // Precision倍したものをClipperに与え、得られたアウトラインからコライダー形状をセットする際には 64 // 逆にPrecisionで割るようにした 65 clipper.Clear(); 66 var sourcePaths = GetPathsFromTriangles(triangles, Precision); 67 clipper.AddPaths(sourcePaths, PolyType.ptSubject, true); 68 if (clipper.Execute(ClipType.ctUnion, this.outlines, PolyFillType.pftPositive)) 69 { 70 // また、アウトライン生成後にTriangulatorを使ってアウトラインをメッシュ化しておく 71 // これは見た目をくり抜くためのマスクとして使われる 72 // まずpathMeshを新しいoutlinesに合わせて更新、引き続きコライダーオブジェクトに 73 // メッシュレンダラーを付け、それにpathMeshをセットしてマスキングを行わせる 74 this.UpdatePathMesh(); 75 this.UpdateMask(); 76 } 77 else 78 { 79 Debug.LogError($"Path generation for {this.name} failed."); 80 } 81 } 82 83 // Clipperを使ってオブジェクトをくり抜く 84 public static void Carve(Carver subject, IEnumerable<Carver> withCarvers) 85 { 86 if (subject == null) 87 { 88 throw new ArgumentNullException(nameof(subject)); 89 } 90 91 if (withCarvers == null) 92 { 93 throw new ArgumentNullException(nameof(withCarvers)); 94 } 95 96 var solution = new List<List<IntPoint>>(); 97 clipper.Clear(); 98 clipper.AddPaths(subject.outlines, PolyType.ptSubject, true); 99 var worldToSubject = subject.Collider2D.transform.worldToLocalMatrix; 100 101 void TransformPath(List<List<IntPoint>> input, List<List<IntPoint>> output, Matrix4x4 matrix) 102 { 103 output.Clear(); 104 output.AddRange( 105 input.Select( 106 path => path.Select( 107 point => 108 { 109 var p = matrix.MultiplyPoint3x4(new Vector3(point.X / Precision, point.Y / Precision, 0.0f)); 110 return new IntPoint(p.x * Precision, p.y * Precision); 111 }).ToList())); 112 } 113 114 var transformedPath = new List<List<IntPoint>>(); 115 foreach (var withCarver in withCarvers.Where(c => (c != null) && (c != subject))) 116 { 117 TransformPath( 118 withCarver.outlines, 119 transformedPath, 120 worldToSubject * withCarver.Collider2D.transform.localToWorldMatrix); 121 clipper.AddPaths(transformedPath, PolyType.ptClip, true); 122 } 123 124 if (clipper.Execute(ClipType.ctDifference, solution, PolyFillType.pftNonZero)) 125 { 126 subject.outlines.Clear(); 127 subject.outlines.AddRange(solution); 128 subject.UpdatePathMesh(); 129 subject.UpdateMask(); 130 } 131 else 132 { 133 Debug.LogError($"Path generation for {subject.name} failed."); 134 } 135 } 136 137 private void CreateMaskMaterialsIfNeeded() 138 { 139 if (maskMaterial == null) 140 { 141 if (this.maskShader == null) 142 { 143 this.maskShader = Shader.Find("Hidden/CarverMask"); 144 } 145 146 if (this.maskShader != null) 147 { 148 maskMaterial = new Material(this.maskShader); 149 } 150 else 151 { 152 Debug.LogError($"{nameof(this.maskShader)} not found."); 153 } 154 } 155 156 if (maskCleanerMaterial == null) 157 { 158 if (this.maskCleanerShader == null) 159 { 160 this.maskCleanerShader = Shader.Find("Hidden/CarverMaskCleaner"); 161 } 162 163 if (this.maskCleanerShader != null) 164 { 165 maskCleanerMaterial = new Material(this.maskCleanerShader); 166 } 167 else 168 { 169 Debug.LogError($"{nameof(this.maskCleanerShader)} not found."); 170 } 171 } 172 } 173 174 private void UpdatePathMesh() 175 { 176 var pathCount = this.outlines.Count; 177 this.Collider2D.pathCount = pathCount; 178 if (this.pathMesh != null) 179 { 180 Destroy(this.pathMesh); 181 } 182 183 var points = this.outlines.Select( 184 path => (Clipper.Orientation(path) ? ((IEnumerable<IntPoint>)path).Reverse() : path) 185 .Select(p => new Vector2(p.X / Precision, p.Y / Precision)).ToArray() 186 ).ToArray(); 187 for (var i = 0; i < pathCount; i++) 188 { 189 this.Collider2D.SetPath(i, points[i]); 190 } 191 192 var pathMeshVertices = points.SelectMany(path => path).Select(p => (Vector3)p).ToArray(); 193 var pathMeshIndexOffsets = new int[pathCount]; 194 for (var i = 1; i < pathCount; i++) 195 { 196 pathMeshIndexOffsets[i] = pathMeshIndexOffsets[i - 1] + this.outlines[i - 1].Count; 197 } 198 199 var pathMeshIndices = points.Zip(pathMeshIndexOffsets, (path, indexOffset) => (path, indexOffset)) 200 .SelectMany( 201 pathAndOffset => new Triangulator(pathAndOffset.path).Triangulate() 202 .Select(i => i + pathAndOffset.indexOffset)).ToArray(); 203 var mesh = new Mesh 204 { 205 vertices = pathMeshVertices, triangles = pathMeshIndices 206 }; 207 mesh.RecalculateBounds(); 208 this.pathMesh = mesh; 209 } 210 211 private void UpdateMask() 212 { 213 var meshFilter = this.Collider2D.GetComponent<MeshFilter>(); 214 var meshRenderer = this.Collider2D.GetComponent<MeshRenderer>(); 215 if (this.pathMesh == null) 216 { 217 if (meshRenderer != null) 218 { 219 meshRenderer.enabled = false; 220 } 221 } 222 else 223 { 224 if (meshFilter == null) 225 { 226 meshFilter = this.Collider2D.gameObject.AddComponent<MeshFilter>(); 227 } 228 229 if (meshRenderer == null) 230 { 231 meshRenderer = this.Collider2D.gameObject.AddComponent<MeshRenderer>(); 232 } 233 234 meshFilter.sharedMesh = this.pathMesh; 235 meshRenderer.enabled = true; 236 meshRenderer.sharedMaterial = maskCleanerMaterial; 237 } 238 } 239 240 private static List<List<IntPoint>> GetPathsFromTriangles(Triangle[] triangles, float precision) 241 { 242 return triangles.Select( 243 t => 244 { 245 return t.Vertices.Reverse().Select(p => new IntPoint(p.x * precision, p.y * precision)) 246 .ToList(); 247 }) 248 .ToList(); 249 } 250 251 // 製作過程でtrianglesが正しく生成されているか視覚的に確認する際に使った 252 // メッシュ生成メソッドだが、現状のコードではどこからも使われていない 253 private static Mesh GetMeshFromTriangles(Triangle[] triangles) 254 { 255 var mesh = new Mesh(); 256 var vertices = triangles.SelectMany(t => t.Vertices).ToArray(); 257 mesh.vertices = vertices; 258 mesh.triangles = Enumerable.Range(0, vertices.Length).ToArray(); 259 mesh.RecalculateNormals(); 260 return mesh; 261 } 262 263 private static void DeleteMeshes((Transform, Mesh)[] meshes) 264 { 265 foreach (var (_, mesh) in meshes) 266 { 267 Destroy(mesh); 268 } 269 }

スクリプトの途中ですが、文字数が上限に達してしまったので別回答に引き継ぎます...

投稿2020/04/13 15:38

Bongo

総合スコア10811

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

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

tttai

2020/04/15 06:38

やりたいことが完璧にできていてすごく助かりました!! わからないところも多いので調べながら少しづつ理解していけるようにします!! 本当にありがとうございました!
guest

0

あああああああああああああああああああああああ

投稿2020/04/15 06:37

編集2020/04/15 06:38
tttai

総合スコア5

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

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

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.35%

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

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

質問する

関連した質問