プログラミング

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

投稿日:2019年5月12日 更新日:

かわいいスカートを履きたい!でもパンチラしたくない!
それは誰もが抱く夢です。もちろん、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を設定して、メッシュを適切に選択すれば動くはずです。

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

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

-プログラミング

執筆者:

関連記事

no image

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

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

GeoJSONで市町村境界をマージして都道府県境界にしたい(実践編)

前々回と前回で問題を整理して、ようやく実践編です。 まず実物のリンク貼りましょう。GitHubに上げました。 今回はGo言語で書いてますが、ポイントがいくつかあります。 ちなみに言語としてGoを選択し …

no image

[C言語]スペースを大量に入れるとバグが直るコードを書いてみた

↓こういうツイートがバズっていたので、実際に組んでみた。 修士の頃、授業の課題でC言語書いてる時にどうしても謎のエラーが出て困っていた。それを見たSE経験(金融系)がある社会人大学院生の同期の女性が「 …

YouTube Data APIをGoogle Apps Script(GAS)から使おう

YouTubeってAPIから色々な情報を取ることができるんですよ。 APIの情報はリファレンスにまとまってるんですが、APIキーだのOAuth2.0だの、使い始めるまでがまぁまぁ面倒なんですね。 で、 …

no image

M5StickC Plusの明るさ(M5.Axp.ScreenBreath)、0~100かもしれない

M5StickC Plusで画面の明るさを調整するM5.Axp.ScreenBreath()という関数があります。 日本語リファレンスでは指定値7~12ということになっています。 また、公式ドキュメン …