はじめに

// 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)));
ざっくりいうと
- 入力されたモデル座標を中心寄りな分布にする。
- X、Z軸方向に潰す。(このとき中心をモッコリさせる)
- 中心から外側にかけてネジネジ&回転させる。
短いコードですがそれなりに絵になりました。
上から順に解説していきます。

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軸方向にも潰します

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をかけた際にちらつきが発生するためです。
参考資料