自由研究

【Unity】GPUを使ってパンツを隠すスクリプトができた

投稿日:

かわいいスカートを履きたい!でもパンチラしたくない!
それは誰もが抱く夢です。もちろん、3Dモデルだってパンチラしたくないと思っているハズです。

というわけで偉大なる先人がいます。
Unityでパンツが見えそうなら「見せられないよ!」する

しかしもっとやれるはずだ!我々にはGPUがある
というわけで、パンツ判定をGPUにやらせる手法のご説明です。

完成デモ


こんな感じですね。

注目してほしいポイント。

  • それなりの処理速度で動く(うちのグラボはGeForce GTX 750です)
  • 見えている範囲を厳密に判定できる
  • モデルの動きに追随する

先人へのイチャモン

MeshColiderって重いんですよ。いや、判定は良いんですけど、MeshColiderにメッシュを設定するのが重い
なんで重いかっていうと、どうも設定時にメッシュを最適化する処理が走るらしく、Update関数で毎フレームMeshColiderを更新してはいけない。

衝突判定

というわけで、衝突判定を自前でやります。

「パンツを構成する全頂点」と「カメラ位置」を結ぶ線分を引いて、その線分が他のポリゴンに遮られているかチェックします。
具体的には以下の画像のような感じ。カメラから見えている部分だけ線を引いています。

この衝突判定ですが、真面目にやると「パンツの頂点数」×「ポリゴン数」やることになって非常に回数が多い。
じゃあGPUにやらせよう、というわけ。

Compute Shader

UnityにはCompute Shaderっていう仕組みがあって、GPUに任意の計算をさせることができます。
で、GPUは大量のスレッドで一気に処理するのが得意なので、GPUで衝突判定をさせれば毎フレームごとに判定させても性能が出せる。

というわけで書いたシェーダーがコレ。戻り値が負の値なら、衝突しています。線分ごとに戻り値がありますが、具体的にどの三角形と衝突したかはわかりません。

#pragma kernel LsTsHit

struct Line
{
    float3 start;
    float3 end;
};

struct Triangle
{
    float3 p;
    float3 q;
    float3 r;
};

StructuredBuffer<Line> Lines;
StructuredBuffer<Triangle> Triangles;
RWBuffer<int> Results;
uniform int _ymax;

[numthreads(32,32,1)]
void LsTsHit (uint3 id : SV_DispatchThreadID)
{
    float3 s = Lines[id.x].start;
    float3 e = Lines[id.x].end;
    float3 p = Triangles[id.y].p;
    float3 q = Triangles[id.y].q;
    float3 r = Triangles[id.y].r;
    float3 l = e - s;
    float3 n = cross(q - p, r - p);
    float  dists = dot(s - p, n);
    float  diste = dot(e - p, n);
    
    int isPlane = -sign(length(n));
    
    float3 crossPoint = dists / (dists - diste) * l + s;
    
    int r0 = -sign(dists * diste);
    int r1 = sign(dot(n, cross(q - p, p - crossPoint)));
    int r2 = sign(dot(n, cross(r - q, q - crossPoint)));
    int r3 = sign(dot(n, cross(p - r, r - crossPoint)));
    
    int ymask = id.y - _ymax;
    
    InterlockedOr(Results[id.x], isPlane & ymask & ~(r0 | ((r1 | r2 | r3) ^ (r1 & r2 & r3))));
}

で、対応するC#側のスクリプトがこれ。

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

using System.Runtime.InteropServices;

public class LineMeshHit : MonoBehaviour
{
    public ComputeShader lineTriangleHitShader;
    private ComputeBuffer linesBuffer;
    private ComputeBuffer trianglesBuffer;
    private ComputeBuffer resultsBuffer;

    public struct Line
    {
        public Vector3 lineStart;
        public Vector3 lineEnd;

        public Line(Vector3 lineStart, Vector3 lineEnd)
        {
            this.lineStart = lineStart;
            this.lineEnd = lineEnd;
        }
    }

    public struct Triangle
    {
        public Vector3 triP;
        public Vector3 triQ;
        public Vector3 triR;

        public Triangle(Vector3 triP, Vector3 triQ, Vector3 triR)
        {
            this.triP = triP;
            this.triQ = triQ;
            this.triR = triR;
        }
    }

    public void initBuffer(int linenum, int trianglenum)
    {
        linesBuffer = new ComputeBuffer(linenum, Marshal.SizeOf(typeof(Line)));
        trianglesBuffer = new ComputeBuffer(trianglenum, Marshal.SizeOf(typeof(Triangle)));
        resultsBuffer = new ComputeBuffer(linenum, Marshal.SizeOf(typeof(int)));
    }

    void OnDisable()
    {
        linesBuffer.Release();
        trianglesBuffer.Release();
        resultsBuffer.Release();
    }

    public int[] isHitLinesTriangles(Line[] lines, Triangle[] triangles)
    {
        int linesgroup = (lines.Length + 31) / 32;
        int trianglesgroup = (triangles.Length + 31) / 32;

        int[] results = new int[linesgroup * 32];

        linesBuffer.SetData(lines);
        trianglesBuffer.SetData(triangles);
        resultsBuffer.SetData(results);
        lineTriangleHitShader.SetBuffer(0, "Lines", linesBuffer);
        lineTriangleHitShader.SetBuffer(0, "Triangles", trianglesBuffer);
        lineTriangleHitShader.SetBuffer(0, "Results", resultsBuffer);
        lineTriangleHitShader.SetInt("_ymax", triangles.Length);
        lineTriangleHitShader.Dispatch(0, linesgroup, trianglesgroup, 1);
        resultsBuffer.GetData(results);
        return results;
    }
}

別の記事で書いたときからちょっと進化していて、GPUのスレッドの使い方を工夫してあります。

たとえば線5本と三角形6個の当たり判定を取るとして、これを下の図のように処理させます。

CPUは線分の配列と三角形の配列を与えてあげて、戻り値は「線分に対する当たり判定」の配列。

Compute ShaderのGPUスレッドはx, y, zという3次元でIDを割り振ることができて、今回はx, yだけを活用します。
この仕組みはつまり、「GPUは5×6スレッド動くけど、その入出力のためにCPUに5×6回の処理をさせていたらイミがないよね?」ということです。GPUに計算させるうえでココが一番性能ネックになったところでした。

で、当たり判定を通過した線が、「カメラに写っているパンツ」となるわけです。

あとはパンツの座標をスクリーン座標に置き換えて画像を表示させれば完成、という感じ。

相変わらず性能面については真面目に調査をしておらず、「きっとif文が無ければCompute Shaderは高速に動くだろう」ぐらいのノリで書いています。
HSLS初めて書いているので許してください。

蛇足

細かいところで、「パンツかどうかをどう判断するの?」という問題があって、ぶっちゃけ3Dモデルに依存します。
パンツが独立したサブメッシュであることがキーポイントです。特定のサブメッシュのポリゴンを抜いてくるのは比較的カンタンなので。詳しくは以前の記事参照です。
これを満たすモデルは中々なくて、今回デモに出したのはセシル変身アプリで作成したVRMモデルです。
他にもアイドル部の八重沢なとりとかがこれを満たしていていい感じに風紀を正すことができます。

配布

seisoGenerator
とりあえずMITライセンスでいいです。使うときは「©ぎんしゃり」って書いてくれてもいいし、書かなくてもいい。

seisoCanvasっていうprefabを用意したので使ってください。

seisoCanvasのRender Cameraにメインカメラを設定して、

seisoImageのSorce ImageとTarget Objectを設定して、メッシュを適切に選択すれば動くはずです。

風紀を正す画像は添付していないので、淡井先生のツイートから拾ってください。

なお清楚な画像もあります。瑠璃姉に許可を得て使ってください。

-自由研究

執筆者:


comment

メールアドレスが公開されることはありません。

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください

関連記事

no image

ロリポップサーバーで独自ドメイン+SSL対応したらちょお楽だった

三連休になにかやろうかな、と思ったので、とりあえず独自ドメイン+SSL対応しました。 元々ロリポップのサーバーだったので系列のサービスを使いました。 ちょお楽だったので作業をまとめておきます。 ドメイ …

no image

コラボカフェとコメントと私

こんにちは!バーチャルYoutuber大好きぎんしゃりさんです! きょう、以前書いたキズナアイコラボカフェの記事にコメントが来たのでご紹介して、それに関わるお話をしていきますね。 元記事の要約 前回の …

no image

UnityのCompute Shaderに線分と三角形の衝突判定をさせる

はじめてCompute Shader使ってみた。でもあまり性能は出ないのでただの失敗の記録です。 まず元のアルゴリズムはコレ。 線分と三角形の当たり判定 – 富士見研究所 で、これをまず三角形の表裏問 …

まんがタイムきらら展のイラストボード一覧

まんがタイムきらら展面白かったですね! みなさんはイラストボード見ましたか? 撮影可だったので全部写真に撮りました!どうぞ!!!! 展示での配置は公式のツイートをご参照下さい。 せっかくですので、スペ …

no image

GW自由研究 – なぜ日本の路面電車は廃れたのか

路面電車と言えば古き良き昭和の乗り物、というイメージですよね。 広島や函館など、一部ではまだ運行が続いている路線もあるものの、それらは「昭和の臭いが残っている」というような扱いです。 以前ブラタモリで …