teratail header banner
teratail header banner
質問するログイン新規登録

回答編集履歴

1

変数名変更、ループ展開版を追記

2018/10/02 21:55

投稿

Bongo
Bongo

スコア10816

answer CHANGED
@@ -12,12 +12,172 @@
12
12
 
13
13
  _Range("Range", Float) = 0.025
14
14
  // _Blur("Blur", Float) = 0.005 // Blurは廃止、サンプリング位置ずらしはテクセルサイズに基づいた形に変更
15
- _Sigma("Sigma", Range(0.01, 8.0)) = 1.0 // σを追加
15
+ _Sigma("Sigma", Range(0.01, 8.0)) = 1.0 // σを追加
16
16
  }
17
17
 
18
18
  SubShader
19
19
  {
20
- // スカイボックスも含めてグラブするため、キューをTransparentに変更
20
+ // スカイボックスも含めてグラブするため、キューをTransparentに変更
21
+ Tags{ "Queue" = "Transparent" "RenderType" = "Transparent" }
22
+
23
+ Cull Off
24
+
25
+ GrabPass{ "_Frost" }
26
+
27
+ CGINCLUDE
28
+ #include "UnityCG.cginc"
29
+
30
+ half4 _Color;
31
+
32
+ sampler2D _MainTex;
33
+ float4 _MainTex_ST;
34
+
35
+ sampler2D _Frost;
36
+
37
+ // グラブテクスチャのテクセルサイズを追加
38
+ float4 _Frost_TexelSize;
39
+
40
+ sampler2D _Noise;
41
+ float4 _Noise_ST;
42
+
43
+ half _Range;
44
+ // half _Blur; // _Blurは廃止
45
+ float _Sigma; // _Sigmaを追加
46
+
47
+ // 重み計算用関数
48
+ // ご質問者さんの重み関数 1/(2*π*σ)*exp(-(x^2+y^2)/(2*σ^2)) と同様ですが、係数は1に変更しました
49
+ // 係数を付けて正規化した重みで有限の範囲をたたみ込むと、重みの総和が1より小さくなってしまうため
50
+ // できあがったぼかし画像は、もとの画像より暗くなってしまうと思われます
51
+ // そこで重み関数には係数を付けず、たたみ込みの際に重みの総和を求めて、最後にたたみ込み結果を
52
+ // 重みの総和で割ることで明るさが維持されるようにしました
53
+ inline float getWeight(float2 xy)
54
+ {
55
+ return exp(-dot(xy, xy) / (2.0 * _Sigma * _Sigma));
56
+ }
57
+
58
+ // カーネルサイズ計算用関数
59
+ // たたみ込み範囲の片側幅...カーネルの一辺の長さ2*n+1のnをいくつにするかですが、さしあたり
60
+ // 中心から最も遠い点(カーネルの角)における重みが十分小さくなる(0.0001を切る)大きさにしました
61
+ // σ=1でn=4となり、サンプリング回数は(2*4+1)^2=81回になります(多分...)
62
+ // σ=2で225回、σ=4で729回、σ=8で2601回...といった具合に、2乗のオーダーでサンプリング回数が
63
+ // 増大しますので、あんまりσを大きくしすぎるのは控えた方がいいでしょう
64
+ // ぼかしを縦方向と横方向の二段階に分けることで、サンプリング回数の増大を1乗のオーダーに
65
+ // 抑えるテクニックもありますので、負荷を軽減したい場合は採用を検討してみてもいいでしょう
66
+ inline int getKernelN()
67
+ {
68
+ return (int)ceil(_Sigma * sqrt(-log(0.0001)));
69
+ }
70
+
71
+ ENDCG
72
+
73
+ Pass
74
+ {
75
+ CGPROGRAM
76
+ #pragma target 3.0
77
+ #pragma vertex vert
78
+ #pragma fragment frag
79
+
80
+ struct v2f
81
+ {
82
+ float4 pos : SV_POSITION;
83
+
84
+ float3 uv : TEXCOORD;
85
+ float4 screenPos : TEXCOORD1;
86
+ float3 ray : TEXCOORD2;
87
+ };
88
+
89
+ v2f vert(appdata_full v)
90
+ {
91
+ v2f o;
92
+ o.pos = UnityObjectToClipPos(v.vertex);
93
+ o.uv = v.texcoord;
94
+ o.screenPos = ComputeGrabScreenPos(o.pos);
95
+ o.ray = UnityObjectToViewPos(v.vertex).xyz * float3(-1, -1, 1);
96
+ return o;
97
+ }
98
+
99
+ half4 frag(v2f i) : SV_Target
100
+ {
101
+ // rayは使用していないようですが、一応残しておきました
102
+ i.ray = i.ray * (_ProjectionParams.z / i.ray.z);
103
+
104
+ float2 uv = i.screenPos.xy / i.screenPos.w;
105
+
106
+ float2 frostUV = tex2D(_Noise, i.uv * _Noise_ST.xy + _Noise_ST.zw).xy;
107
+
108
+ frostUV -= 0.5;
109
+ frostUV *= _Range;
110
+ frostUV += uv;
111
+
112
+ // 霜のついたガラスを表現するためサンプリング位置にノイズを加えているようですが、
113
+ // もし純粋にぼかし効果だけをかけたい場合は、uvをそのまま使うといいでしょう
114
+ // frostUV = uv;
115
+
116
+ int kernelN = getKernelN();
117
+ float weightSum = 0.0;
118
+ float4 frost = 0.0;
119
+
120
+ // 注目しているピクセルを中心に、-kernelN ~ +kernelNの範囲をたたみ込む
121
+ for (int m = -kernelN; m <= kernelN; m++)
122
+ {
123
+ for (int n = -kernelN; n <= kernelN; n++)
124
+ {
125
+ float2 texelOffset = float2(n, m);
126
+ float weight = getWeight(texelOffset);
127
+ weightSum += weight;
128
+ frost += weight * tex2D(_Frost, frostUV + texelOffset * _Frost_TexelSize.xy);
129
+ }
130
+ }
131
+
132
+ // 最後に、重みの総和で割る
133
+ frost /= weightSum;
134
+
135
+ half4 diffuse = tex2D(_MainTex, i.uv * _MainTex_ST.xy + _MainTex_ST.zw);
136
+
137
+ half alpha = _Color.a * diffuse.a;
138
+
139
+ return half4(frost.xyz + (diffuse.rgb * _Color.rgb * alpha), 1);
140
+ }
141
+
142
+ ENDCG
143
+ }
144
+ }
145
+
146
+ Fallback Off
147
+ }
148
+ ```
149
+
150
+ コード中のコメントでもちょっと言及しましたが、このままではσを大きくすると、毎フレーム描画では重さが気になるかもしれません。
151
+ 追加のレンダーテクスチャが必要になるためコードはちょっとややこしくなるでしょうが、[wgld.org | WebGL: gaussian フィルタ |](https://wgld.org/d/webgl/w057.html)で紹介されているような縦横二段掛け方式とか、[品質とパフォーマンスを両立する独自の技術|YEBIS 3](https://www.siliconstudio.co.jp/middleware/yebis/jp/features/processing_algorithms/)で解説されているような低解像度化してぼかし処理を行うテクニックを用いれば、もっと軽量化できるかもしれませんね。
152
+
153
+ σ = 1、ノイズ効果なし
154
+ ![1](ccbf520945b052ca6f82bca2fec63008.png)
155
+ σ = 1、ノイズ効果あり
156
+ ![F1](f97d3d3faa656cc5a8db3702cc13ff2d.png)
157
+ σ = 8、ノイズ効果なし
158
+ ![8](59f729096d3d242d5c5e10ae710db25b.png)
159
+ σ = 8、ノイズ効果あり
160
+ ![F8](edd8232aac26f3a693c9d12261d30d8d.png)
161
+
162
+ ### ループ展開版
163
+ 非展開版と比べると、シェーダーのインポートに時間がかかるかもしれません。
164
+ `KERNEL_N_MAX`をもっと小さくすると、対応可能なぼかし幅は小さくなってしまうものの、インポート速度・動作速度は改善するかと思います。
165
+ ```ShaderLab
166
+ Shader "Custom/Frost"
167
+ {
168
+ Properties
169
+ {
170
+ _Color("Color", Color) = (1, 1, 1, 1)
171
+
172
+ _MainTex("Diffuse", 2D) = "white" {}
173
+ _Noise("Noise", 2D) = "black" {}
174
+
175
+ _Range("Range", Float) = 0.025
176
+ _Sigma("Sigma", Range(0.01, 8.0)) = 1.0
177
+ }
178
+
179
+ SubShader
180
+ {
21
181
  Tags{ "Queue" = "Transparent" "RenderType" = "Transparent" }
22
182
 
23
183
  Cull Off
@@ -33,41 +193,29 @@
33
193
  float4 _MainTex_ST;
34
194
 
35
195
  sampler2D _Frost;
36
-
37
- // グラブテクスチャのテクセルサイズを追加
38
196
  float4 _Frost_TexelSize;
39
197
 
40
198
  sampler2D _Noise;
41
199
  float4 _Noise_ST;
42
200
 
43
201
  half _Range;
44
- // half _Blur; // _Blurは廃止
45
- float _Sigma; // _Sigmaを追加
202
+ float _Sigma;
46
203
 
47
204
  // 重み計算用関数
48
- // ご質問者さんの重み関数 1/(2*π*σ)*exp(-(x^2+y^2)/(2*σ^2)) と同様ですが、係数は1に変更しました
49
- // 係数を付けて正規化した重みで有限の範囲をたたみ込むと、重みの総和が1より小さくなってしまうため
50
- // できあがったぼかし画像は、もとの画像より暗くなってしまうと思われます
51
- // そこで重み関数には係数を付けず、たたみ込みの際に重みの総和を求めて、最後にたたみ込み結果を
52
- // 重みの総和で割ることで明るさが維持されるようにしました
53
205
  inline float getWeight(float2 xy)
54
206
  {
55
207
  return exp(-dot(xy, xy) / (2.0 * _Sigma * _Sigma));
56
208
  }
57
209
 
58
210
  // カーネルサイズ計算用関数
59
- // たたみ込み範囲の片側幅...カーネルの一辺の長さ2*n+1のnをいくつにするかですが、さしあたり
60
- // 中心から最も遠い点(カーネルの角)における重みが十分小さくなる(0.0001を切る)大きさにしました
61
- // σ=1でn=4となり、サンプリング回数は(2*4+1)^2=81回になります(多分...)
62
- // σ=2で225回、σ=4で729回、σ=8で2601回...といった具合に、2乗のオーダーでサンプリング回数が
63
- // 増大しますので、あんまりσを大きくしすぎるのは控えた方がいいでしょう
64
- // ぼかしを縦方向と横方向の二段階に分けることで、サンプリング回数の増大を1乗のオーダーに
65
- // 抑えるテクニックもありますので、負荷を軽減したい場合は採用を検討してみてもいいでしょう
66
211
  inline int getKernelN()
67
212
  {
68
213
  return (int)ceil(_Sigma * sqrt(-log(0.0001)));
69
214
  }
70
215
 
216
+ // 最大kernelN...(int)ceil(8 * sqrt(-ln(0.0001)))
217
+ #define KERNEL_N_MAX 25
218
+
71
219
  ENDCG
72
220
 
73
221
  Pass
@@ -98,7 +246,6 @@
98
246
 
99
247
  half4 frag(v2f i) : SV_Target
100
248
  {
101
- // rayは使用していないようですが、一応残しておきました
102
249
  i.ray = i.ray * (_ProjectionParams.z / i.ray.z);
103
250
 
104
251
  float2 uv = i.screenPos.xy / i.screenPos.w;
@@ -108,28 +255,59 @@
108
255
  frostUV -= 0.5;
109
256
  frostUV *= _Range;
110
257
  frostUV += uv;
111
-
112
- // 霜のついたガラスを表現するためサンプリング位置にノイズを加えているようですが、
113
- // もし純粋にぼかし効果だけをかけたい場合は、uvをそのまま使うといいでしょう
114
258
  // frostUV = uv;
115
259
 
116
260
  int kernelN = getKernelN();
117
- float weightSum = 0.0;
118
- float4 frost = 0.0;
119
261
 
262
+ // ループ展開版
263
+ float2 texelOffset = float2(0, 0);
264
+ float weight = getWeight(texelOffset);
265
+ float weightSum = weight;
120
- // 注目しているピクセルを中心に、-kernelN +kernelNの範囲をたたみ込む
266
+ float4 frost = weight * tex2D(_Frost, frostUV);
267
+
268
+ [unroll(KERNEL_N_MAX)]
121
- for (int j = -kernelN; j <= kernelN; j++)
269
+ for (int n = 0; n < kernelN; n++)
270
+ {
271
+ int x = n + 1;
272
+
273
+ texelOffset = float2(x, 0);
274
+ weight = getWeight(texelOffset);
275
+ weightSum += 2 * weight;
276
+ frost += weight * tex2D(_Frost, frostUV + texelOffset * _Frost_TexelSize.xy);
277
+ texelOffset = float2(-x, 0);
278
+ frost += weight * tex2D(_Frost, frostUV + texelOffset * _Frost_TexelSize.xy);
279
+ }
280
+
281
+ [unroll(KERNEL_N_MAX)]
282
+ for (int m = 0; m < kernelN; m++)
122
283
  {
284
+ int y = m + 1;
285
+
286
+ texelOffset = float2(0, y);
287
+ weight = getWeight(texelOffset);
288
+ weightSum += 2 * weight;
289
+ frost += weight * tex2D(_Frost, frostUV + texelOffset * _Frost_TexelSize.xy);
290
+ texelOffset = float2(0, -y);
291
+ frost += weight * tex2D(_Frost, frostUV + texelOffset * _Frost_TexelSize.xy);
292
+
293
+ [unroll(KERNEL_N_MAX)]
123
- for (int i = -kernelN; i <= kernelN; i++)
294
+ for (int n = 0; n < kernelN; n++)
124
295
  {
296
+ int x = n + 1;
297
+
125
- float2 texelOffset = float2(i, j);
298
+ texelOffset = float2(x, y);
126
- float weight = getWeight(texelOffset);
299
+ weight = getWeight(texelOffset);
127
- weightSum += weight;
300
+ weightSum += 4 * weight;
128
301
  frost += weight * tex2D(_Frost, frostUV + texelOffset * _Frost_TexelSize.xy);
302
+ texelOffset = float2(-x, y);
303
+ frost += weight * tex2D(_Frost, frostUV + texelOffset * _Frost_TexelSize.xy);
304
+ texelOffset = float2(x, -y);
305
+ frost += weight * tex2D(_Frost, frostUV + texelOffset * _Frost_TexelSize.xy);
306
+ texelOffset = float2(-x, -y);
307
+ frost += weight * tex2D(_Frost, frostUV + texelOffset * _Frost_TexelSize.xy);
129
308
  }
130
309
  }
131
310
 
132
- // 最後に、重みの総和で割る
133
311
  frost /= weightSum;
134
312
 
135
313
  half4 diffuse = tex2D(_MainTex, i.uv * _MainTex_ST.xy + _MainTex_ST.zw);
@@ -145,16 +323,4 @@
145
323
 
146
324
  Fallback Off
147
325
  }
148
- ```
149
-
150
- コード中のコメントでもちょっと言及しましたが、このままではσを大きくすると、毎フレーム描画では重さが気になるかもしれません。
151
- 追加のレンダーテクスチャが必要になるためコードはちょっとややこしくなるでしょうが、[wgld.org | WebGL: gaussian フィルタ |](https://wgld.org/d/webgl/w057.html)で紹介されているような縦横二段掛け方式とか、[品質とパフォーマンスを両立する独自の技術|YEBIS 3](https://www.siliconstudio.co.jp/middleware/yebis/jp/features/processing_algorithms/)で解説されているような低解像度化してぼかし処理を行うテクニックを用いれば、もっと軽量化できるかもしれませんね。
152
-
153
- σ = 1、ノイズ効果なし
154
- ![1](ccbf520945b052ca6f82bca2fec63008.png)
155
- σ = 1、ノイズ効果あり
156
- ![F1](f97d3d3faa656cc5a8db3702cc13ff2d.png)
157
- σ = 8、ノイズ効果なし
158
- ![8](59f729096d3d242d5c5e10ae710db25b.png)
159
- σ = 8、ノイズ効果あり
160
- ![F8](edd8232aac26f3a693c9d12261d30d8d.png)
326
+ ```