かわいいスカートを履きたい!でもパンチラしたくない!
それは誰もが抱く夢です。もちろん、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を設定して、メッシュを適切に選択すれば動くはずです。
風紀を正す画像は添付していないので、淡井先生のツイートから拾ってください。
見せられないよパロですー!ご自由にどうぞ!(੭ु ›ω‹ )੭ु#SiroArt #なとあーと #もちにゃあと pic.twitter.com/El2ASIVP2K
— 淡井フレヴィア🐉 (@awaiflavia) November 24, 2018
なお清楚な画像もあります。瑠璃姉に許可を得て使ってください。
いくつか使えそうなのを皆さんにプレゼントします pic.twitter.com/mkYq1FKiJ8
— 朝ノ瑠璃✪デビュー1周年VRライブ「瑠璃色艶舞」ありがとうございました!! (@asanoruri) January 6, 2019