Posted at

Shaderアートのススメ Particle編


はじめに



なんかShaderでエモい感じのできてBOOTHで配ったら興味ある人がいたので適当に解説します。

BOOTHはここからどうぞ→Galaxy Shader - Voxel Gummi - BOOTH

ぶっちゃけShurikenとか使えば良さそうな気がしますがテッセレーションの勉強がてら座標操作の練習がしたかったんでゴニョゴニョした次第です。


まずはコード全文をペタリ

// Copyright (c) 2019 @Feyris77

// Released under the MIT license
// https://opensource.org/licenses/mit-license.php
Shader "Unlit/Galaxy Shader"
{
Properties
{
[IntRange]_Tessellation ("Particle Amount", Range(1, 32)) = 8
_Size("Particle Size", float) = 0.1
_Speed("Speed", float) = 1
}
SubShader
{
Tags { "RenderType"="Opaque" "Queue" = "Transparent-1"}
LOD 100
Blend One One
ZWrite off

Pass
{
CGPROGRAM
#pragma target 5.0
#pragma vertex vert
#pragma hull Hull
#pragma domain Domain
#pragma geometry geom
#pragma fragment frag

#include "UnityCG.cginc"

struct v2h {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};

struct h2d
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};

struct h2dc
{
float Edges[3] : SV_TessFactor;
float Inside : SV_InsideTessFactor;
};

struct d2g
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};

struct g2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float4 col : TEXCOORD1;
};

uniform float _Tessellation, _Size, _Speed;

#define ADD_VERT(u, v) \
o.uv = float2(u, v); \
o.pos = vp + float4(u*ar, v, 0, 0)*_Size; \
TriStream.Append(o);

float2 rot(float2 p, float r)
{
float c = cos(r);
float s = sin(r);
return mul(p, float2x2(c, -s, s, c));
}

float3 rand3D(float3 p)
{
p = float3( dot(p,float3(127.1, 311.7, 74.7)),
dot(p,float3(269.5, 183.3,246.1)),
dot(p,float3(113.5,271.9,124.6)));
return frac(sin(p)*43758.5453123);
}

v2h vert (appdata_base v)
{
v2h o;
o.pos = v.vertex;
o.uv = v.texcoord;
return o;
}

h2dc HullConst(InputPatch<v2h, 3> i)
{
h2dc o;
float3 retf;
float ritf, uitf;
ProcessTriTessFactorsAvg(_Tessellation.xxx, 1, retf, ritf, uitf );
o.Edges[0] = retf.x;
o.Edges[1] = retf.y;
o.Edges[2] = retf.z;
o.Inside = ritf;
return o;
}

[domain("tri")]
[partitioning("integer")]
[outputtopology("triangle_cw")]
[outputcontrolpoints(3)]
[patchconstantfunc("HullConst")]
h2d Hull(InputPatch<v2h, 3> IN, uint id : SV_OutputControlPointID)
{
h2d o;
o.pos = IN[id].pos;
o.uv = IN[id].uv;
return o;
}

[domain("tri")]
d2g Domain(h2dc hs_const_data, OutputPatch<h2d, 3> i, float3 bary: SV_DomainLocation)
{
d2g o;
o.pos = i[0].pos * bary.x + i[1].pos * bary.y + i[2].pos * bary.z;
o.uv = i[0].uv * bary.x + i[1].uv * bary.y + i[2].uv * bary.z;
return o;
}

[maxvertexcount(3)]
void geom(point d2g IN[1],inout TriangleStream<g2f> TriStream)
{
g2f o;
float3 pos = rand3D(IN[0].pos.xyz)*2-1;
float3 rand0 = rand3D(pos.yxz)*2-1;
float3 rand1 = rand3D(pos.zyx)*2-1;
float3 rand3 = rand3D(pos.xyz);
pos = rand0 *rand1;
float t = _Time.y*.05*_Speed;

o.col = float4((sin(abs(pos)*10) * 0.57 + 0.6)*.001, 1);

pos.z *= length(1-abs(pos.x))*.1;
pos.x *= length(1-abs(pos.z))*.4;

pos.xy = rot(pos.xy , t+(1-length(pos))*10);
pos.xy = rot(pos.xy , t*2*smoothstep(.75, 1, rand3.x)*abs(1-length(pos)));

float ar = - UNITY_MATRIX_P[0][0] / UNITY_MATRIX_P[1][1]; //Aspect Ratio
float4 vp = UnityObjectToClipPos(float4(pos, 1));
ADD_VERT( 0.0, 1.0);
ADD_VERT(-0.9, -0.5);
ADD_VERT( 0.9, -0.5);
TriStream.RestartStrip();
}

float4 frag (g2f i) : SV_Target
{
return saturate(.5-length(i.uv)) * clamp(i.col / pow(length(i.uv), 2), 0, 2);
}
ENDCG
}
}
}

~110行目くらいまでは単純にテッセレーションでポリゴン分割しているだけなので特に説明はしません。


パーティクルの座標計算

実際にパーティクルの座標を計算しているところです

float3 pos = rand3D(IN[0].pos.xyz)*2-1;

float3 rand0 = rand3D(pos.yxz)*2-1;
float3 rand1 = rand3D(pos.zyx)*2-1;
float3 rand3 = rand3D(pos.xyz);
pos = rand0 *rand1;
float t = _Time.y*.05*_Speed;

//o.col = float4((sin(abs(pos)*10) * 0.57 + 0.6)*.001, 1);

pos.z *= length(1-abs(pos.x))*.1;
pos.x *= length(1-abs(pos.z))*.4;

pos.xy = rot(pos.xy , t+(1-length(pos))*10);
pos.xy = rot(pos.xy , t*2*smoothstep(.75, 1, rand3.x)*abs(1-length(pos)));

ざっくりいうと

1. 入力されたモデル座標を中心寄りな分布にする。

2. X、Z軸方向に潰す。(このとき中心をモッコリさせる)

3. 中心から外側にかけてネジネジ&回転させる。

短いコードですがそれなりに絵になりました。

上から順に解説していきます。


float3 pos = rand3D(IN[0].pos.xyz)*2-1;

入力されたモデル座標をランダムな±1の範囲に収めます。


上の画像のようにモデルの原点を中心に±1の範囲でランダムに分布します。




次にランダムの分布を原点の近くに集中させます。(中心の密度を上げる)


二つのランダム値を乗算することで分布を変えます。

float3 rand0 = rand3D(pos.yxz)*2-1;

float3 rand1 = rand3D(pos.zyx)*2-1;
pos = rand0 * rand1;




モデル座標のZ軸方向に潰します。このとき中心から離れるほど潰す強さを大きくするのがポイントです。

真横から見ると真ん中がモッコリしています。

pos.z *= length(1-abs(pos.x))*.1;  //Z軸方向に圧縮圧縮ッ!

pos.x *= length(1-abs(pos.z))*.4; //X軸方向に圧縮圧縮ッ!

その後同様にX軸方向にも潰します




最後に中心から外側にかけてねじりつつ回転させます。

t+(1-length(pos))*10で中心からの距離に応じて回転をオフセットしています。

ちなみに、*10というのがねじり具合です。

t*2*smoothstep(.75, 1, rand3.x)*abs(1-length(pos))は一部の粒子を更に回転させてまばらにすることでいい感じの”味付け”を行っています。こちらでも中心からの距離に応じて回転速度を変えています。


ありふれた二次元回転関数

float2 rot(float2 p, float r)

{
float c = cos(r);
float s = sin(r);
return mul(p, float2x2(c, -s, s, c));
}

pos.xy = rot(pos.xy , t+(1-length(pos))*10);  //ネジネジ

pos.xy = rot(pos.xy , t*2*smoothstep(.75, 1, rand3.x)*abs(1-length(pos))); //味付け


パーティクルの表現方法について

パーティクルというと単に小さな粒子なのでその形状や構造について深く考える機会は少ないですがちょっとした工夫で格段に「粒子感」をアップさせることができます。

1-step(.5, length(uv))で円を書いて終わりだとなんだか味気ないです。



今回作ったやつ




パーティクルの一つを拡大表示した様子

中心部(コア)を白飛びさせその周りを色付きのグローで囲うことで人が見たときに発光していると錯覚させます。

グローの幅はコアの大きさよりも大きくするのがポイントです。

saturate(.5-length(i.uv)) * clamp(i.col / pow(length(i.uv), 2), 0, 2);

※このコードはUV座標の原点がコアの中心になることが前提です。

ちなみにclampで0~2の範囲に収めているのはPostEffectをかけた際にちらつきが発生するためです。


参考資料

http://compojigoku.blog.fc2.com/blog-entry-28.html