@XR Kaigi 2019
ARグラスで
魅⼒的な絵作り
比留間 和也
2
1. ⾃⼰紹介
2. 会社紹介
3. プロダクト紹介
3-1. PORTAL紹介
4. PORTALをNreal Lightに移植中の話
CONTENTS
3
⾃⼰紹介
Self introduction
4
@edo_m18 @edom18
http://edom18.hateblo.jp/e.blog:
⽐留間 和也
5
MESONの紹介
MESON introduction
6
1. 制作体制
2. テキストのみ
2-1. テキストA
2-2. テキストB
3. テキストと写真
4. 図系
7
Nreal Lightエヴァンジェリスト
Nreal Lightのエヴァンジェリストに
8
プロダクト紹介
9
プロダクト紹介
⾼精細にスキャンしたモデルたちがARランウェイの上を歩き、
それに実際に近づいて⾒てもらうコンテンツ。
PORTAL
CONFIDENCIAL PAGE
PORTAL
X10
11
AUGGIE AWARDSファイナリスト
AUGGIE AWARDSという海外のアワードで
ソフトウェアとしては⽇本初のファイナリストに選出されました。
AR/VRで最⼤のAUGGIE AWARDSにてファイナリストに選出
12
PORTALをNreal Lightに移植中の話
Import the PORTAL to the Nreal Light
13
現在、CES向けにPORTALをNreal Lightに移植中
14
PORTALの移植で⼤事にしたこと
15
Interaction
インタラクション
Graphic
グラフィック
Effect
演出
PORTALの移植で⼤事にした点
iPad版にも劣らないようなグラフィック作り
ARグラスならではのインタラクション設計
PORTALのイメージであるパーティクル演出
16
移植にあたっての課題
17
iPad版に⽐べて
⾊味が明るすぎる
視野⾓の端にモデルを
配置すると枠が気になる
パーティクルを
⼤量に出すための⼯夫
移植にあたっての課題
1 2 3
18
1. iPad版に⽐べて⾊味が明るすぎる
19
iPad版に⽐べて⾊味が明るすぎる
PCで⾒た絵 ARグラス越しの絵
PCディスプレイで⾒た場合の⾊味 Nreal Lightで⾒た場合の⾊味
無加⼯だと⾊味が全然違う
20
課題はコントラスト
全体的に明るすぎてコントラストが低い
⾊に応じてコントラストを調整したい
※単純に黒を⾜して明るさを調整しても輝度が下がるだけ
21
iPad版に⽐べて⾊味が明るすぎる
PCで⾒た絵 ARグラス越しの絵
中⼼が無加⼯。右が⾊を2乗したもの
左は単純に黒を⾜したもの
加⼯したものが⼀番くっきりと⾒える
改善した状態
22
⾊を2乗する(コントラストを上げる)
fixed4 frag(v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
col.rgb = pow(col.rgb, 2.0);
return col;
}
コード的にはシンプルだが効果は絶⼤
23
2. 視野⾓の端にモデルを配置すると枠が気になる
24
視界付近にモデルを配置すると枠が気になる
枠の端でぱっきりと切れると「枠」が意識される
視界の端でぱっきりと切れる
25
視界付近にモデルを配置すると枠が気になる
フェードアウトさせると区切りが意識されなくなる
視界の端をフェードアウトさせる
26
ポストプロセスで上下左右の際をフェードアウトさせる
画⾯端に近づくにつれてフェードアウトさせるポストエフェクトを書く
27
ポストプロセスで上下左右の際をフェードアウトさせる
fixed4 frag(v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
float2 uv = abs(i.uv * 2.0 - 1.0);
float2 u = _Width / _ScreenParams.xy * 0.5;
u = smoothstep(0, u, 1.0 - uv);
col = col * u.x * u.y;
return col;
}
UV値を利⽤して画⾯端に近づくにつれてフェードさせる
(ARグラスは黒=発光しない=透明になっていく)
28
3. パーティクルを⼤量に出すための⼯夫
29
30
31
32
シーン内の⼤量のパーティクルが
様々なオブジェクトの形に姿を変える
33
移植する上での課題
1. 10万弱のパーティクルを描画する
2. 複数モデルグループをパーティクル処理
3. モデル以外の形状にも変化する
4. 常に⽣成済パーティクルを利⽤する
(新規⽣成はしない)
34
処理フロー
35
処理フロー
1. ターゲットのメッシュ情報を集める
2. (1)のデータをグルーピング
3. Compute Shaderで位置計算
4. GPU Instancingで描画
36
Compute Shaderのセットアップ
- その場で、ランウェイに登
場する服の素材感を知るこ
とができる
37
利⽤するカーネルは3つ
Update ExplosionCalculate
01 02 03
パーティクルのターゲットを
更新する
ターゲットがない形状に
変形させる
パーティクルの位置を
計算・更新する
38
パーティクルの位置更新
[numthreads(THREAD_NUM,1,1)]
void Calculate(uint id : SV_DispatchThreadID)
{
Particle p = _Particles[id];
float3 pos = (p.targetPosition - p.position) * _DeltaTime * p.speed;
p.position += pos;
p.useTexture = 1;
_Particles[id] = p;
}
パーティクルの位置を毎フレーム更新
39
パーティクルのターゲット変更
[numthreads(THREAD_NUM,1,1)]
void Update(uint id : SV_DispatchThreadID)
{
Particle p = _Particles[id];
float4x4 mat = _MatrixData[_InitDataList[id].targetId];
p.isActive = _InitDataList[id].isActive;
p.targetPosition = mul(mat, float4(_InitDataList[id].targetPosition,
1.0)).xyz;
p.uv = _InitDataList[id].uv;
p.targetId = _InitDataList[id].targetId;
_Particles[id] = p;
}
パーティクルのターゲット先を変更(更新)する
40
パーティクルのターゲット変更
[numthreads(THREAD_NUM,1,1)]
void Explosion(uint id : SV_DispatchThreadID)
{
Particle p = _Particles[id];
if (_OnCircle == 1)
{
float h = rand(p.id);
float s = sin(p.id + _Time) * 1.5;
float c = cos(p.id + _Time) * 1.5;
float3 pp = float3(s, h * 1.5, c) + noise(p.position);
float3 pos = (pp - p.position) * _DeltaTime * p.speed;
p.position += pos;
p.color = float4(1, 1, 1, 1);
p.useTexture = 0;
}
else
{
float h = rand(p.id);
float3 pos = (p.targetPosition + float3(0, cos(h + _Time) * 0.01, 0) -
p.position) * _DeltaTime * p.speed;
p.position += pos;
}
p.color = float4(1, 1, 1, 1);
p.useTexture = 0;
_Particles[id] = p;
}
別形状のためのカーネル
41
C#側のセットアップ
42
メッシュの情報を集める
public class ParticleTarget : MonoBehaviour
{
private Mesh _mesh = null;
public Mesh Mesh => _mesh ?? (_mesh = GetComponent<MeshFilter>().mesh);
private Renderer _renderer = null;
private Renderer Renderer => _renderer ?? (_renderer =
GetComponent<Renderer>());
public int VertexCount => Mesh.vertexCount;
public Vector3[] Vertices => Mesh.vertices;
public Vector2[] UV => Mesh.uv;
public Matrix4x4 WorldMatrix => transform.localToWorldMatrix;
public Texture2D Texture => Renderer.material.mainTexture as Texture2D;
}
シンタックスシュガー的にデータを渡すだけのコード
43
頂点、UV情報をひとつの配列にまとめる
private void CollectAllData()
{
int count = GetCount();
_allVertices = new Vector3[count];
_allUV = new Vector2[count];
int idx = 0;
foreach (var t in _targets)
{
System.Array.Copy(t.Vertices, 0, _allVertices, idx,
t.Vertices.Length);
System.Array.Copy(t.UV, 0, _allUV, idx, t.UV.Length);
idx += t.Vertices.Length;
}
}
グループの頂点などをメモリアクセスしやすいように配列にまとめる
44
ターゲットモデルのマトリクスをまとめる
private void UpdateMatrices(ParticleTargetGroup group)
{
for (int i = 0; i < group.ParticleTargets.Length; i++)
{
_matrixData[i] = group.ParticleTargets[i].WorldMatrix;
}
_matrixBuffer.SetData(_matrixData);
_computeShader.SetBuffer(_kernelSetup, _propertyDef.MatrixDataID,
_matrixBuffer);
}
各頂点の⾏列計算はシェーダで⾏うためMatrixデータをまとめてGPUに送る
45
テクスチャをTextureArrayにまとめる
private void CreateTextureArray()
{
int count = _targets.Length;
int width = _targets[0].Texture.width;
int height = _targets[0].Texture.height;
_textureArray = new Texture2DArray(width, height, count,
TextureFormat.RGBA32, false, true);
_textureArray.filterMode = FilterMode.Bilinear;
_textureArray.wrapMode = TextureWrapMode.Repeat;
for (int i = 0; i < _targets.Length; i++)
{
_textureArray.SetPixels(_targets[i].Texture.GetPixels(0), i, 0);
}
_textureArray.Apply();
}
Matrix同様、テクスチャもシェーダ側でアクセスするためTextureArrayにまとめる
46
描画
47
GPUインスタンシングで描画
v2f vert (appdata v, uint id : SV_InstanceID)
{
Particle p = _Particles[id];
v2f o;
v.vertex.xyz = (v.vertex.xyz * _BaseScale) +
p.position;
o.vertex = mul(UNITY_MATRIX_VP,
float4(v.vertex.xyz, 1.0));
o.uv.xy = p.uv.xy;
o.uv.z = p.targetId;
o.color = p.color;
o.texid = p.targetId;
o.useTex = p.useTexture;
return o;
}
fixed4 frag(v2f i) : SV_Target
{
fixed4 col;
if (i.useTex == 1)
{
col = UNITY_SAMPLE_TEX2DARRAY(_Textures,
i.uv);
col = pow(col, 1.5);
}
else
{
col = i.color;
}
return col;
}
Compute Shaderの計算に基づいてパーティクルをレンダリングする
頂点シェーダ フラグメントシェーダ
48
GPUインスタンシングで描画
DrawMeshInstancedIndirectメソッドで⼀度に描画
private void DrawParticles()
{
Graphics.DrawMeshInstancedIndirect(
_particleMesh,
0, // submesh index
_particleMat,
new Bounds(Vector3.zero, Vector3.one * 32f),
_argsBuffer,
0,
null,
UnityEngine.Rendering.ShadowCastingMode.Off,
false,
gameObject.layer
);
}
49
50
Nreal Light(Android)でも現実的なレベルで動く!
51
iPad版に⽐べて
⾊味が明るすぎる
視野⾓の端にモデルを
配置すると枠が気になる
パーティクルを
⼤量に出すための⼯夫
まとめ
⾊を2乗する
視界の端を
フェードさせる
Compute Shaderで
がんばる
52
ご静聴ありがとうございました
53
@edo_m18 @edom18
⽐留間 和也
http://edom18.hateblo.jp/e.blog:
MESONではARグラス時代のユースケースを開発する
エンジニアを積極募集中です。
興味がある⽅はぜひご連絡ください!
MESON

ARグラスで 魅力的な絵作り