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

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

ただいまの
回答率

87.37%

unity 切り取り

解決済

回答 4

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 3,448

score 5

前提・実現したいこと

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

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

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

エラーメッセージ

該当のソースコード

ソースコード

試したこと

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

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

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

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

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

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

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • Bongo

    2020/04/08 07:29

    「ブーリアン演算を使って切り取った場合に」とのことですが、もっと具体的にはどのようなアルゴリズムで切り取るプランなのかご説明いただけるでしょうか。作りかけのコードがありましたらご提示いただけると参考になりそうです。あるいは「このWebサイトで紹介されている方法が利用できないかと思っている」みたいなアテがありましたら、それをご紹介いただいてもいいと思います。

    コリジョンに関しては、以前「Unity2D:オブジェクトを二つに自由切断する方法」(https://teratail.com/questions/204249 )という件で2D形状の切断方法を検討してみたことがあるのですが、あちらの時のように切断された形の外周パスを求められるような切断方法ならPolygonCollider2Dが使えるだろうと思います。ですが画像処理に近い方法で切断されたように見せる場合...たとえば「SpriteをSpriteでくり抜いて、くり抜かれた部分は別の場所に描画させたい」(https://teratail.com/questions/125072 )のようなやり方ですと、コリジョンはちょっと工夫が必要そうですね。

    キャンセル

  • tttai

    2020/04/08 17:08

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

    キャンセル

  • Bongo

    2020/04/08 22: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 22:49

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

    キャンセル

回答 4

checkベストアンサー

+1

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

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

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

Shader "Hidden/CarverMask"
{
    Properties
    {
    }
    SubShader
    {
        Pass
        {
            ColorMask 0
            Cull Back
            ZTest Always
            ZWrite Off
            Stencil
            {
                Ref 1
                WriteMask 1
                Pass IncrWrap
            }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            float4 vert(float3 vertex : POSITION) : SV_POSITION
            {
                return UnityObjectToClipPos(vertex);
            }

            fixed4 frag() : SV_Target
            {
                return 0;
            }
            ENDCG
        }
    }
}

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

Shader "Hidden/CarverMaskCleaner"
{
    Properties
    {
    }
    SubShader
    {
        Pass
        {
            ColorMask 0
            Cull Back
            ZTest Always
            ZWrite Off
            Stencil
            {
                Ref 0
                WriteMask 1
                Pass Replace
            }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            float4 vert(float3 vertex : POSITION) : SV_POSITION
            {
                return UnityObjectToClipPos(vertex);
            }

            fixed4 frag() : SV_Target
            {
                return 0;
            }
            ENDCG
        }
    }
}

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

Shader "Custom/CarverStandard"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _Metallic ("Metallic", Range(0,1)) = 0.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        Stencil
        {
            Ref 1
            ReadMask 1
            Comp Equal
        }
        CGPROGRAM
        #pragma surface surf Standard fullforwardshadows
        #pragma target 3.0

        sampler2D _MainTex;

        struct Input
        {
            float2 uv_MainTex;
        };

        half _Glossiness;
        half _Metallic;
        fixed4 _Color;

        void surf(Input IN, inout SurfaceOutputStandard o)
        {
            fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = c.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

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

        Stencil
        {
            Ref 1
            ReadMask 1
            Comp Equal
        }

を追加しただけです。

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

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

using System.Linq;
using UnityEngine;
using UnityEngine.Rendering;
#if UNITY_EDITOR
using UnityEditor;
#endif

[ExecuteAlways]
[DefaultExecutionOrder(1)]
public class EditorModeStencilWriter : MonoBehaviour
{
    #if UNITY_EDITOR
    [SerializeField] private Shader maskShader;
    private static Material maskMaterial;
    private static CommandBuffer commands;
    private static Camera[] cameras;

    private void Awake()
    {
        if (EditorApplication.isPlayingOrWillChangePlaymode)
        {
            Destroy(this);
        }
    }

    private void OnEnable()
    {
        if (maskMaterial == null)
        {
            if (this.maskShader == null)
            {
                this.maskShader = Shader.Find("Hidden/CarverMask");
            }

            if (this.maskShader != null)
            {
                maskMaterial = new Material(this.maskShader);
            }
        }

        if (commands != null)
        {
            return;
        }

        commands = new CommandBuffer {name = "Fill Stencil Buffer"};
        commands.Blit(EditorGUIUtility.whiteTexture, BuiltinRenderTextureType.CameraTarget, maskMaterial);
        cameras = SceneView.GetAllSceneCameras().Concat(Camera.allCameras).Select(
            cam =>
            {
                cam.AddCommandBuffer(CameraEvent.BeforeForwardOpaque, commands);
                return cam;
            }).ToArray();
    }

    private void OnDisable()
    {
        if (cameras == null)
        {
            return;
        }

        foreach (var cam in cameras)
        {
            if (cam == null)
            {
                continue;
            }

            cam.RemoveCommandBuffer(CameraEvent.BeforeForwardOpaque, commands);
        }

        cameras = null;
        commands = null;
    }

    #else
    private void Awake()
    {
        Destroy(this);
    }
    #endif
}

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

using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public class MouseAction : MonoBehaviour
{
    private Camera mainCamera;
    private Transform dragTarget;
    private Vector3 dragTargetOrigin;
    private Vector3 dragOrigin;
    private bool carveWhenDrop;

    private void Start()
    {
        this.mainCamera = Camera.main;
    }

    private void Update()
    {
        if (this.dragTarget == null)
        {
            if (Input.GetMouseButtonDown(0))
            {
                var mouseRay = this.mainCamera.ScreenPointToRay(Input.mousePosition);
                var hit = Physics2D.Raycast(mouseRay.origin, mouseRay.direction);
                if (hit.collider != null)
                {
                    this.dragTarget = hit.transform;
                    this.dragTargetOrigin = this.dragTarget.position;
                    this.dragOrigin = hit.point;
                    if (Input.GetKey(KeyCode.Space))
                    {
                        hit.collider.isTrigger = true;
                        if (hit.rigidbody != null)
                        {
                            hit.rigidbody.bodyType = RigidbodyType2D.Kinematic;
                        }

                        this.carveWhenDrop = true;
                    }
                }
            }
        }
        else
        {
            if (Input.GetMouseButton(0))
            {
                var mouseRay = this.mainCamera.ScreenPointToRay(Input.mousePosition);
                var xyPlane = new Plane(Vector3.back, Vector3.zero);
                if (xyPlane.Raycast(mouseRay, out var enter))
                {
                    var deltaPosition = mouseRay.GetPoint(enter) - this.dragOrigin;
                    this.dragTarget.position = this.dragTargetOrigin + deltaPosition;
                }
            }
            else if (Input.GetMouseButtonUp(0))
            {
                var targetRigidbody = this.dragTarget.GetComponent<Rigidbody2D>();
                if (targetRigidbody != null)
                {
                    targetRigidbody.velocity = Vector2.zero;
                    if (this.carveWhenDrop)
                    {
                        targetRigidbody.bodyType = RigidbodyType2D.Dynamic;
                    }
                }

                if (this.carveWhenDrop)
                {
                    var targetCollider = this.dragTarget.GetComponent<Collider2D>();
                    if (targetCollider != null)
                    {
                        targetCollider.isTrigger = false;
                        var overlappingColliders = new List<Collider2D>();
                        targetCollider.OverlapCollider(new ContactFilter2D(), overlappingColliders);
                        var carvers = overlappingColliders.Select(c => c.GetComponentInChildren<Carver>())
                            .Where(c => c != null);
                        var thisCarver = targetCollider.GetComponentInChildren<Carver>();
                        if (thisCarver != null)
                        {
                            Debug.Log(
                                $"Carve {targetCollider.name} with {string.Join(", ", carvers.Select(c => c.Collider2D.name))}.");
                            Carver.Carve(thisCarver, carvers);
                        }
                    }

                    this.carveWhenDrop = false;
                }

                this.dragTarget = null;
            }
        }
    }

    private void OnGUI()
    {
        if (this.carveWhenDrop)
        {
            using (new GUILayout.AreaScope(new Rect(0, 0, Screen.width, Screen.height)))
            {
                using (new GUILayout.HorizontalScope())
                {
                    GUILayout.FlexibleSpace();
                    GUILayout.Label("Let's Carve!");
                    GUILayout.FlexibleSpace();
                }
            }
        }
    }
}

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

図1

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

図2

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

図3

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

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

+1

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

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

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

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

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

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

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

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

using System;
using System.Collections.Generic;
using System.Linq;
using ClipperLib;
using UnityEngine;
using UnityEngine.Rendering;

public class Carver : MonoBehaviour
{
    private const float Precision = 1024.0f;
    private static Material maskMaterial;
    private static Material maskCleanerMaterial;
    private static readonly Clipper clipper = new Clipper(Clipper.ioStrictlySimple);
    [SerializeField] private Shader maskShader;
    [SerializeField] private Shader maskCleanerShader;
    [SerializeField] private bool attachRigidbodyOnCreateCollider;
    [SerializeField] private bool makeColliderTriggerOnCreateCollider;
    private readonly List<List<IntPoint>> outlines = new List<List<IntPoint>>();
    private Mesh pathMesh;
    private (Renderer, (Material, int)[])[] renderers = new (Renderer, (Material, int)[])[0];
    public PolygonCollider2D Collider2D { get; private set; }

    private void Start()
    {
        this.FitColliderIntoMeshes();
    }

    // PolygonCollider2Dを現在の3Dモデルの見た目に合わせて更新する
    // まずStartで一度実行されるが、後で再度実行すれば欠損部分が復活することになる
    // その他、モデルの三次元的な回転などによりモデルのアウトラインが変化した
    // 場合にもこのメソッドでアウトラインを更新するべきだが、実行速度は
    // 大して考慮していないため、あまり頻繁に行うのはおすすめできない
    // なお、モデルの「Read/Write Enabled」をオンにしておかないと失敗すると思われる
    public void FitColliderIntoMeshes()
    {
        // マスク処理用マテリアルが未作成なら作っておく
        this.CreateMaskMaterialsIfNeeded();

        // 3Dオブジェクトを使っているということなのでオブジェクトが三次元的に
        // 回転している可能性があるが、2Dコライダーはtransform.forwardが
        // ワールドZ+を向いていた方が好都合なため、親オブジェクトを追加して
        // そこにコライダーを付けることにした
        // 併せてそれにアタッチされるRenderingHelperが、CommandBufferを使って
        // 独自にレンダリングを行うことになる
        if (this.Collider2D == null)
        {
            this.Collider2D = this.CreateCollider();
        }

        // MeshFilter、またはSkinnedMeshRendererからメッシュを集めて
        // ワールドZ方向に押し潰し、三角形の集合を得てコライダーの原型とする
        // 裏向きの三角形も向きを反転した上で列挙しているが、ポリゴンの裏面は
        // 考慮しなくてもいいのなら、裏は除外してもいいかもしれない
        this.renderers = this.GetComponentsInChildren<Renderer>().Select(
            r => (r, r.materials.Select((m, i) => (m, i)).OrderBy(pair => pair.m.renderQueue).ToArray())).ToArray();
        var meshes = this.GatherMeshes();
        var triangles = this.GetTrianglesFromMesh(meshes);
        DeleteMeshes(meshes);

        // Clipperを使ってアウトラインを作る
        // Clipperは右手系の慣習に従うようなので、入力三角形は巡回方向を逆転させて反時計回りを表とする
        // また、計算上のロバスト性のためClipperは座標を整数として扱うそうなので、まずUnity上の座標値を
        // Precision倍したものをClipperに与え、得られたアウトラインからコライダー形状をセットする際には
        // 逆にPrecisionで割るようにした
        clipper.Clear();
        var sourcePaths = GetPathsFromTriangles(triangles, Precision);
        clipper.AddPaths(sourcePaths, PolyType.ptSubject, true);
        if (clipper.Execute(ClipType.ctUnion, this.outlines, PolyFillType.pftPositive))
        {
            // また、アウトライン生成後にTriangulatorを使ってアウトラインをメッシュ化しておく
            // これは見た目をくり抜くためのマスクとして使われる
            // まずpathMeshを新しいoutlinesに合わせて更新、引き続きコライダーオブジェクトに
            // メッシュレンダラーを付け、それにpathMeshをセットしてマスキングを行わせる
            this.UpdatePathMesh();
            this.UpdateMask();
        }
        else
        {
            Debug.LogError($"Path generation for {this.name} failed.");
        }
    }

    // Clipperを使ってオブジェクトをくり抜く
    public static void Carve(Carver subject, IEnumerable<Carver> withCarvers)
    {
        if (subject == null)
        {
            throw new ArgumentNullException(nameof(subject));
        }

        if (withCarvers == null)
        {
            throw new ArgumentNullException(nameof(withCarvers));
        }

        var solution = new List<List<IntPoint>>();
        clipper.Clear();
        clipper.AddPaths(subject.outlines, PolyType.ptSubject, true);
        var worldToSubject = subject.Collider2D.transform.worldToLocalMatrix;

        void TransformPath(List<List<IntPoint>> input, List<List<IntPoint>> output, Matrix4x4 matrix)
        {
            output.Clear();
            output.AddRange(
                input.Select(
                    path => path.Select(
                        point =>
                        {
                            var p = matrix.MultiplyPoint3x4(new Vector3(point.X / Precision, point.Y / Precision, 0.0f));
                            return new IntPoint(p.x * Precision, p.y * Precision);
                        }).ToList()));
        }

        var transformedPath = new List<List<IntPoint>>();
        foreach (var withCarver in withCarvers.Where(c => (c != null) && (c != subject)))
        {
            TransformPath(
                withCarver.outlines,
                transformedPath,
                worldToSubject * withCarver.Collider2D.transform.localToWorldMatrix);
            clipper.AddPaths(transformedPath, PolyType.ptClip, true);
        }

        if (clipper.Execute(ClipType.ctDifference, solution, PolyFillType.pftNonZero))
        {
            subject.outlines.Clear();
            subject.outlines.AddRange(solution);
            subject.UpdatePathMesh();
            subject.UpdateMask();
        }
        else
        {
            Debug.LogError($"Path generation for {subject.name} failed.");
        }
    }

    private void CreateMaskMaterialsIfNeeded()
    {
        if (maskMaterial == null)
        {
            if (this.maskShader == null)
            {
                this.maskShader = Shader.Find("Hidden/CarverMask");
            }

            if (this.maskShader != null)
            {
                maskMaterial = new Material(this.maskShader);
            }
            else
            {
                Debug.LogError($"{nameof(this.maskShader)} not found.");
            }
        }

        if (maskCleanerMaterial == null)
        {
            if (this.maskCleanerShader == null)
            {
                this.maskCleanerShader = Shader.Find("Hidden/CarverMaskCleaner");
            }

            if (this.maskCleanerShader != null)
            {
                maskCleanerMaterial = new Material(this.maskCleanerShader);
            }
            else
            {
                Debug.LogError($"{nameof(this.maskCleanerShader)} not found.");
            }
        }
    }

    private void UpdatePathMesh()
    {
        var pathCount = this.outlines.Count;
        this.Collider2D.pathCount = pathCount;
        if (this.pathMesh != null)
        {
            Destroy(this.pathMesh);
        }

        var points = this.outlines.Select(
            path => (Clipper.Orientation(path) ? ((IEnumerable<IntPoint>)path).Reverse() : path)
                .Select(p => new Vector2(p.X / Precision, p.Y / Precision)).ToArray()
        ).ToArray();
        for (var i = 0; i < pathCount; i++)
        {
            this.Collider2D.SetPath(i, points[i]);
        }

        var pathMeshVertices = points.SelectMany(path => path).Select(p => (Vector3)p).ToArray();
        var pathMeshIndexOffsets = new int[pathCount];
        for (var i = 1; i < pathCount; i++)
        {
            pathMeshIndexOffsets[i] = pathMeshIndexOffsets[i - 1] + this.outlines[i - 1].Count;
        }

        var pathMeshIndices = points.Zip(pathMeshIndexOffsets, (path, indexOffset) => (path, indexOffset))
            .SelectMany(
                pathAndOffset => new Triangulator(pathAndOffset.path).Triangulate()
                    .Select(i => i + pathAndOffset.indexOffset)).ToArray();
        var mesh = new Mesh
        {
            vertices = pathMeshVertices, triangles = pathMeshIndices
        };
        mesh.RecalculateBounds();
        this.pathMesh = mesh;
    }

    private void UpdateMask()
    {
        var meshFilter = this.Collider2D.GetComponent<MeshFilter>();
        var meshRenderer = this.Collider2D.GetComponent<MeshRenderer>();
        if (this.pathMesh == null)
        {
            if (meshRenderer != null)
            {
                meshRenderer.enabled = false;
            }
        }
        else
        {
            if (meshFilter == null)
            {
                meshFilter = this.Collider2D.gameObject.AddComponent<MeshFilter>();
            }

            if (meshRenderer == null)
            {
                meshRenderer = this.Collider2D.gameObject.AddComponent<MeshRenderer>();
            }

            meshFilter.sharedMesh = this.pathMesh;
            meshRenderer.enabled = true;
            meshRenderer.sharedMaterial = maskCleanerMaterial;
        }
    }

    private static List<List<IntPoint>> GetPathsFromTriangles(Triangle[] triangles, float precision)
    {
        return triangles.Select(
                t =>
                {
                    return t.Vertices.Reverse().Select(p => new IntPoint(p.x * precision, p.y * precision))
                        .ToList();
                })
            .ToList();
    }

    // 製作過程でtrianglesが正しく生成されているか視覚的に確認する際に使った
    // メッシュ生成メソッドだが、現状のコードではどこからも使われていない
    private static Mesh GetMeshFromTriangles(Triangle[] triangles)
    {
        var mesh = new Mesh();
        var vertices = triangles.SelectMany(t => t.Vertices).ToArray();
        mesh.vertices = vertices;
        mesh.triangles = Enumerable.Range(0, vertices.Length).ToArray();
        mesh.RecalculateNormals();
        return mesh;
    }

    private static void DeleteMeshes((Transform, Mesh)[] meshes)
    {
        foreach (var (_, mesh) in meshes)
        {
            Destroy(mesh);
        }
    }

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

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/04/15 15:38

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

    キャンセル

+1

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

    private static IEnumerable<(Transform, Vector3)> EnumerateScaledTransformsInParent(Transform fromTransform)
    {
        do
        {
            var localScale = fromTransform.localScale;
            if (localScale != Vector3.one)
            {
                yield return (fromTransform, localScale);
            }

            fromTransform = fromTransform.parent;
        } while (fromTransform != null);
    }

    // 自身の階層下からコライダーの原型とするメッシュを収集する
    // オリジナルのメッシュをそのまま返すのではなく、念のため複製を返すことにした
    // SkinnedMeshRendererについてもBakeMeshで現在の形をキャプチャーして返すようにしたが、
    // BakeMeshはモデルが拡大縮小されていると正しく見た目通りの形をキャプチャーしてくれないようで
    // 苦肉の策として一時的になるべくスケールが等倍になるようにしてからキャプチャーし
    // 後で元に戻すことにした
    private (Transform, Mesh)[] GatherMeshes()
    {
        var meshFilters = this.GetComponentsInChildren<MeshFilter>();
        var skinnedMeshRenderers = this.GetComponentsInChildren<SkinnedMeshRenderer>();
        if (skinnedMeshRenderers.Select(smr => smr.transform.lossyScale).Distinct().Skip(1).Any())
        {
            Debug.LogWarning($"{this.gameObject.name} has complexly scaled transform hierarchy! It may cause wrong mesh generation.");
        }

        var scaledTransforms = EnumerateScaledTransformsInParent(this.transform).ToArray();
        foreach (var (scaledTransform, _) in scaledTransforms)
        {
            scaledTransform.localScale = Vector3.one;
        }

        var result = meshFilters.Select(mf => (mf.transform, Instantiate(mf.sharedMesh))).Concat(
            skinnedMeshRenderers.Select(
                smr =>
                {
                    var mesh = new Mesh();
                    smr.BakeMesh(mesh);
                    return (smr.transform, mesh);
                })).ToArray();
        foreach (var (scaledTransform, localScale) in scaledTransforms)
        {
            scaledTransform.localScale = localScale;
        }

        return result;
    }

    // メッシュの頂点をワールド空間に移し、Z方向に圧縮したものをローカル空間に戻し、Triangle配列として返す
    private Triangle[] GetTrianglesFromMesh((Transform, Mesh)[] meshes)
    {
        var worldToLocal = this.Collider2D.transform.worldToLocalMatrix;
        var worldZ = this.Collider2D.transform.position.z;
        return meshes.SelectMany(
            pair =>
            {
                var (t, m) = pair;
                var localToWorld = t.localToWorldMatrix;
                var vertices = m.vertices;
                var indices = m.triangles;
                return Enumerable.Range(0, indices.Length / 3).Select(
                    i =>
                    {
                        var j = i * 3;
                        var pw0 = localToWorld.MultiplyPoint3x4(vertices[indices[j]]);
                        var pw1 = localToWorld.MultiplyPoint3x4(vertices[indices[j + 1]]);
                        var pw2 = localToWorld.MultiplyPoint3x4(vertices[indices[j + 2]]);
                        var crossZ = CrossZ(pw1 - pw0, pw2 - pw0);
                        return (pw0, pw1, pw2, crossZ);
                    }).Where(face => Mathf.Abs(face.crossZ) > 0.0f).Select(
                    face =>
                    {
                        face.pw0.z = worldZ;
                        face.pw1.z = worldZ;
                        face.pw2.z = worldZ;
                        if (face.crossZ < 0.0f)
                        {
                            return new Triangle
                            {
                                Vertex0 = worldToLocal.MultiplyPoint3x4(face.pw0),
                                Vertex1 = worldToLocal.MultiplyPoint3x4(face.pw1),
                                Vertex2 = worldToLocal.MultiplyPoint3x4(face.pw2)
                            };
                        }

                        return new Triangle
                        {
                            Vertex0 = worldToLocal.MultiplyPoint3x4(face.pw0),
                            Vertex1 = worldToLocal.MultiplyPoint3x4(face.pw2),
                            Vertex2 = worldToLocal.MultiplyPoint3x4(face.pw1)
                        };
                    });
            }).ToArray();
    }

    private static float CrossZ(Vector3 lhs, Vector3 rhs)
    {
        return (lhs.x * rhs.y) - (lhs.y * rhs.x);
    }

    // このオブジェクトに親オブジェクトを作ってPolygonCollider2Dを取り付け、
    // さらに描画処理を担当するRenderingHelperもアタッチする
    private PolygonCollider2D CreateCollider()
    {
        var siblingIndex = this.transform.GetSiblingIndex();
        var colliderObject = new GameObject(this.gameObject.name);
        colliderObject.AddComponent<RenderingHelper>().Carver = this;
        colliderObject.transform.SetParent(this.transform.parent, false);
        colliderObject.transform.SetSiblingIndex(siblingIndex);
        colliderObject.transform.position = this.transform.position;
        colliderObject.transform.rotation = Quaternion.identity;
        this.transform.SetParent(colliderObject.transform);
        var collider = colliderObject.AddComponent<PolygonCollider2D>();
        if (this.attachRigidbodyOnCreateCollider)
        {
            colliderObject.AddComponent<Rigidbody2D>();
        }

        collider.isTrigger = this.makeColliderTriggerOnCreateCollider;
        return collider;
    }

    // 実際に目に見えるオブジェクトの姿を描画するのはこれが担当する
    // OnWillRenderObjectタイミングで今このオブジェクトを描画しようとしているカメラを取得し、
    // まだCommandBufferが挿入されていなければ追加する
    // 実行中にオブジェクトの数が変動する可能性を考慮し、CommandBufferは毎フレーム再構築する
    private class RenderingHelper : MonoBehaviour
    {
        private static readonly int DummyTexture = Shader.PropertyToID("_DummyTex");
        private static readonly HashSet<RenderingHelper> RenderingHelpers = new HashSet<RenderingHelper>();
        private static CommandBuffer opaqueCommands;
        private static CommandBuffer transparentCommands;
        private static readonly HashSet<Camera> Cameras = new HashSet<Camera>();
        private static bool NeedsUpdateCommands;
        [NonSerialized] public Carver Carver;
        private MeshRenderer maskRenderer;

        private void Update()
        {
            NeedsUpdateCommands = true;
        }

        private void OnWillRenderObject()
        {
            var currentCamera = Camera.current;
            if (currentCamera == null)
            {
                return;
            }

            CreateCommandsIfNeeded();
            if (!Cameras.Contains(currentCamera))
            {
                AddCommands(currentCamera);
            }

            if (!NeedsUpdateCommands)
            {
                return;
            }

            UpdateCommands();
            NeedsUpdateCommands = false;
        }

        private static void CreateCommandsIfNeeded()
        {
            if (opaqueCommands == null)
            {
                opaqueCommands = new CommandBuffer {name = "RenderCarversOpaque"};
            }

            if (transparentCommands == null)
            {
                transparentCommands = new CommandBuffer {name = "RenderCarversTransparent"};
            }
        }

        private static void AddCommands(Camera cam)
        {
            cam.AddCommandBuffer(CameraEvent.AfterForwardOpaque, opaqueCommands);
            cam.AddCommandBuffer(CameraEvent.BeforeForwardAlpha, transparentCommands);
            Cameras.Add(cam);
        }

        // レンダリングはForwardBaseを前提とし、邪魔なパスをスキップするようにした
        // 透明オブジェクトや不透明オブジェクトが複雑に入り組んでいる可能性を考慮するなら
        // もっと細かく描画順を制御する必要があるだろうが、そこまでやるとややこしくなるため
        // 妥協して大ざっぱなCarver単位での制御にとどめた
        private static void UpdateCommands()
        {
            var lightModeTag = new ShaderTagId("LightMode");
            var forwardBaseTag = new ShaderTagId("ForwardBase");

            void DrawForwardBasePass(CommandBuffer commands, Material m, Renderer r, int i)
            {
                var shader = m.shader;
                var passCount = m.passCount;
                for (var passIndex = 0; passIndex < passCount; passIndex++)
                {
                    if (shader.FindPassTagValue(passIndex, lightModeTag) == forwardBaseTag)
                    {
                        commands.DrawRenderer(r, m, i, passIndex);
                    }
                }
            }

            opaqueCommands.Clear();
            transparentCommands.Clear();
            opaqueCommands.GetTemporaryRT(DummyTexture, 1, 1);
            transparentCommands.GetTemporaryRT(DummyTexture, 1, 1);
            opaqueCommands.EnableShaderKeyword("LIGHTPROBE_SH");
            transparentCommands.EnableShaderKeyword("LIGHTPROBE_SH");
            foreach (var helper in RenderingHelpers)
            {
                if (helper.maskRenderer == null)
                {
                    helper.maskRenderer = helper.GetComponent<MeshRenderer>();
                }

                opaqueCommands.DrawRenderer(helper.maskRenderer, maskMaterial);
                transparentCommands.DrawRenderer(helper.maskRenderer, maskMaterial);
                foreach (var (renderer, materials) in helper.Carver.renderers)
                {
                    foreach (var (material, subMeshIndex) in materials)
                    {
                        DrawForwardBasePass(
                            material.renderQueue <= 2500 ? opaqueCommands : transparentCommands,
                            material,
                            renderer,
                            subMeshIndex);
                    }
                }

                opaqueCommands.Blit(DummyTexture, BuiltinRenderTextureType.CurrentActive, maskCleanerMaterial);
                transparentCommands.Blit(DummyTexture, BuiltinRenderTextureType.CurrentActive, maskCleanerMaterial);
            }

            opaqueCommands.ReleaseTemporaryRT(DummyTexture);
            transparentCommands.ReleaseTemporaryRT(DummyTexture);
        }

        private void OnEnable()
        {
            RenderingHelpers.Add(this);
        }

        private void OnDisable()
        {
            RenderingHelpers.Remove(this);
            if (RenderingHelpers.Count != 0)
            {
                return;
            }

            foreach (var cam in Cameras)
            {
                if (cam != null)
                {
                    cam.RemoveCommandBuffer(CameraEvent.AfterForwardOpaque, opaqueCommands);
                    cam.RemoveCommandBuffer(CameraEvent.BeforeForwardAlpha, transparentCommands);
                }
            }

            Cameras.Clear();
            opaqueCommands = null;
            transparentCommands = null;
        }
    }

    private struct Triangle
    {
        public Vector3 Vertex0;
        public Vector3 Vertex1;
        public Vector3 Vertex2;

        public IEnumerable<Vector3> Vertices
        {
            get
            {
                yield return this.Vertex0;
                yield return this.Vertex1;
                yield return this.Vertex2;
            }
        }
    }
}

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

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

0

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

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

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

  • ただいまの回答率 87.37%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る