--information:パスブラー v1.1 by teruyoshi | オブジェクトをベジェ曲線の形に沿って流す方向ブラー --label:teruyoshi --group:パス設定,false -- ベジェ曲線の始点はオブジェクトの中心。各座標はオブジェクト中心からの相対座標 --track@p2x:始点ハンドルX,-4000,4000,100,0.01 --track@p2y:始点ハンドルY,-4000,4000,0,0.01 --trackgroup@p2x,p2y:始点ハンドル --track@p3x:終点ハンドルX,-4000,4000,250,0.01 --track@p3y:終点ハンドルY,-4000,4000,50,0.01 --trackgroup@p3x,p3y:終点ハンドル --track@p4x:終点X,-4000,4000,300,0.01 --track@p4y:終点Y,-4000,4000,100,0.01 --trackgroup@p4x,p4y:終点 --group:ブラー設定,true --track@str:強さ,0,100,100,0.01 --track@length:長さ,0,100,100,0.01 --track@curve:🕒️減衰曲線0→100,0,100,0,0.01 --track@samples:サンプル数,0,500,0,1,自動 -- サンプル数が0の場合はパスの長さから自動で決める --select@colorMode:重なりの計算=0,平均=0,加算=1,通常=2,スクリーン=3,オーバーレイ=4,乗算=5,ソフトライト=6,比較(明)=7,比較(暗)=8,差分=9 --track@overlapGain:重なり強度,0,1000,100,0.01 --hide@overlapGain:colorMode==0 --group:色設定(未指定の場合は元オブジェクトの色),false --color@startColor:開始色,nil --track@startAlpha:開始色透明度,0,100,0,0.01 --color@endColor:終了色,nil --track@endAlpha:終了色透明度,0,100,0,0.01 --track@gradCurve:🕒️グラデ曲線0→100,0,100,0,0.01 --group:その他の設定,false --check@keepOriginal:元オブジェクトを上に重ねる,false --check@fixSize:サイズ固定,false --check@showBezier:ベジェ曲線を描画,false --color@bezierColor:ベジェ曲線色,0xff0000 --hide@bezierColor:showBezier==0 local MaxSamples = 500 -- サンプル数の上限(4の倍数。下のシェーダー側のMaxSamplesも同じ値にしてください) local ArcDivision = 1000 -- 曲線の長さを測る際の分割数 local BezierWidth = 2 -- ベジェ曲線を描画する時の線の太さ(px) local AutoSampleSpacing = 1 -- サンプル数を自動で決める時のサンプル間隔(px) local OverlapBaseLength = 50 -- 重なり強度100%のとき、パス方向にこの長さ(px)の厚みがある部分が元の明るさ(不透明)になる obj.setanchor("p2x,p2y", 0, "small") obj.setanchor("p3x,p3y", 0, "small") obj.setanchor("p4x,p4y", 0) obj.setanchor({obj.cx, obj.cy, p2x, p2y}, 2, "line") -- 始点と始点ハンドルを結ぶ線(表示のみ) obj.setanchor({p3x, p3y, p4x, p4y}, 2, "line") -- 終点ハンドルと終点を結ぶ線(表示のみ) if obj.getoption("track_mode", "p2x") ~= 0 or obj.getoption("track_mode", "p2y") ~= 0 then obj.setanchor({obj.cx, obj.cy, obj.getvalue("track.p2x", 0), obj.getvalue("track.p2y", 0)}, 2, "line", "color", 0xa0ffa0) obj.setanchor({obj.cx, obj.cy, obj.getvalue("track.p2x", 0,-1), obj.getvalue("track.p2y", 0,-1)}, 2, "line", "color", 0xffa0a0) end if obj.getoption("track_mode", "p3x") ~= 0 or obj.getoption("track_mode", "p3y") ~= 0 or obj.getoption("track_mode", "p4x") ~= 0 or obj.getoption("track_mode", "p4y") ~= 0 then obj.setanchor({obj.getvalue("track.p3x", 0), obj.getvalue("track.p3y", 0), obj.getvalue("track.p4x", 0), obj.getvalue("track.p4y", 0)}, 2, "line", "color", 0xa0ffa0) obj.setanchor({obj.getvalue("track.p3x", 0,-1), obj.getvalue("track.p3y", 0,-1), obj.getvalue("track.p4x", 0,-1), obj.getvalue("track.p4y", 0,-1)}, 2, "line", "color", 0xffa0a0) end --[[pixelshader@PathBlur: static const uint MaxSamples = 500; // 上のLua側のMaxSamplesと同じ値にすること(4の倍数) Texture2D tex : register(t0); SamplerState samLinear : register(s0); cbuffer constant0 : register(b0) { float2 size; // バッファのサイズ(px) float count; // サンプル数 float str; // 強さ(0.0~1.0) float2 origin; // オブジェクト中心のバッファ内座標(px) float keepOriginal; // 元オブジェクトを上に重ねるか float showBezier; // ベジェ曲線を描画するか float2 p1; float2 p2; float2 p3; float2 p4; float3 bezierColor; float bezierWidth; float colorMode; // 0=平均, 1=加算, 2以降=合成モードで順に重ねる float useStart; // 開始色の適用率(0=元オブジェクトの色, 1=開始色) float useEnd; // 終了色の適用率(0=元オブジェクトの色, 1=終了色) float padding; float3 startColor; float padding2; float3 endColor; float padding3; float4 gradPos[MaxSamples / 4]; // 各コピーのグラデーション位置(0=開始色, 1=終了色)。floatを4個ずつ詰めて格納 float4 offs[MaxSamples]; // xy=ずらし量(px), z=重み, w=加算・合成モード時の重み }; // 合成モードの色計算(cb=下側の色, cs=上側の色, どちらも乗算済みでない色) float3 blendColor(float3 cb, float3 cs, int mode) { switch (mode) { case 3: // スクリーン return cb + cs - cb * cs; case 4: // オーバーレイ return lerp(2.0 * cb * cs, 1.0 - 2.0 * (1.0 - cb) * (1.0 - cs), step(0.5, cb)); case 5: // 乗算 return cb * cs; case 6: // ソフトライト return (1.0 - 2.0 * cs) * cb * cb + 2.0 * cs * cb; case 7: // 比較(明) return max(cb, cs); case 8: // 比較(暗) return min(cb, cs); case 9: // 差分 return abs(cb - cs); default: // 通常 return cs; } } // i番目のコピーの色をグラデーション位置に応じて開始色→終了色で置き換える(c=乗算済みアルファの色) // (開始色・終了色の透明度の分だけ元オブジェクトの色を混ぜる) float4 tintColor(float4 c, uint i) { if (useStart == 0 && useEnd == 0) return c; float g = gradPos[i / 4][i % 4]; float3 cs = c.rgb / max(c.a, 1e-6); float3 s = lerp(cs, startColor, useStart); float3 e = lerp(cs, endColor, useEnd); return float4(lerp(s, e, g) * c.a, c.a); } float4 PathBlur(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target { float4 original = tex.SampleLevel(samLinear, uv, 0); // 曲線の形でずらしたコピーを重ねる float4 blur = 0.0; int mode = (int)colorMode; if (mode >= 2) { // 合成モード: 尾の先から根元へ順に、コピーを指定の合成モードで重ねていく(根元が一番上) [loop] for (int i = (int)count - 1; i >= 0; i--) { float4 o = offs[i]; float4 c = tintColor(tex.SampleLevel(samLinear, (pos.xy - o.xy) / size, 0), (uint)i); float srcA = saturate(c.a * o.w); if (srcA <= 0.0) continue; float3 cs = c.rgb / max(c.a, 1e-6); float3 cb = blur.rgb / max(blur.a, 1e-6); // 下側が透明な所では合成モードを使わず上側の色をそのまま使う float3 mixed = lerp(cs, saturate(blendColor(cb, cs, mode)), blur.a); blur = float4(srcA * mixed + (1.0 - srcA) * blur.rgb, srcA + (1.0 - srcA) * blur.a); } } else { // 平均: 単純平均だと尾が長いほど重なるコピーの割合が減って根元が薄くなるため、 // 透明度は「重み×透明度」の最大値、色は重なったコピーの重み付き平均とする float3 rgbSum = 0.0; // 乗算済みアルファの色×重みの合計 float aSum = 0.0; // 透明度×重みの合計 float aMax = 0.0; float4 addSum = 0.0; // 加算用(乗算済みアルファの色×加算時の重みの合計) [loop] for (uint i = 0; i < (uint)count; i++) { float4 o = offs[i]; float4 c = tintColor(tex.SampleLevel(samLinear, (pos.xy - o.xy) / size, 0), i); rgbSum += c.rgb * o.z; aSum += c.a * o.z; aMax = max(aMax, c.a * o.z); addSum += c * o.w; } if (mode == 1) { // 加算: 重なるほど明るくなる。色が透明度を超えないよう透明度を引き上げる float3 rgb = saturate(addSum.rgb); blur = float4(rgb, saturate(max(addSum.a, max(rgb.r, max(rgb.g, rgb.b))))); } else { blur = float4(rgbSum / max(aSum, 1e-6) * aMax, aMax); } } float4 result = lerp(original, blur, str); // 元オブジェクトを上に重ねる(乗算済みアルファの通常合成) if (keepOriginal == 1) { result = original + result * (1.0 - original.a); } // 位置合わせ用のベジェ曲線 if (showBezier == 1) { float2 p = pos.xy - origin; // 曲線を折れ線に分割して各線分との距離を求める。 // 点との距離だけで判定すると両端が丸く膨らみ、始点・終点より先まで線がはみ出すため、 // 最初と最後の線分では端より外側を除外して両端を平らに切る static const uint BezierDivision = 500; float minDist = 10000.0; float2 b0 = p1; [loop] for (uint k = 1; k <= BezierDivision; k++) { float t = (float)k / BezierDivision; float u = 1.0 - t; float2 b1 = u*u*u*p1 + 3.0*u*u*t*p2 + 3.0*u*t*t*p3 + t*t*t*p4; float2 d = b1 - b0; float h = dot(p - b0, d) / max(dot(d, d), 1e-6); if (!((k == 1 && h < 0.0) || (k == BezierDivision && h > 1.0))) { minDist = min(minDist, length(p - (b0 + d * saturate(h)))); } b0 = b1; } if (minDist < bezierWidth) { return float4(bezierColor, 1.0); } } return result; } ]] local P1 = {0, 0} local P2 = {p2x, p2y} local P3 = {p3x, p3y} local P4 = {p4x, p4y} local function bezier(t) local u = 1 - t local a, b, c, d = u*u*u, 3*u*u*t, 3*u*t*t, t*t*t return a*P1[1] + b*P2[1] + c*P3[1] + d*P4[1], a*P1[2] + b*P2[2] + c*P3[2] + d*P4[2] end -- 曲線を細かく分割して累積の長さを求める(長さ基準で等間隔にサンプリングするため) local px, py = {}, {} local cumLen = {} px[0], py[0] = bezier(0) cumLen[0] = 0 for i = 1, ArcDivision do px[i], py[i] = bezier(i / ArcDivision) cumLen[i] = cumLen[i-1] + math.sqrt((px[i]-px[i-1])^2 + (py[i]-py[i-1])^2) end local totalLen = cumLen[ArcDivision] -- 曲線の始点から長さsだけ進んだ位置を返す local seg = 1 local function pointAtLength(s) while seg < ArcDivision and cumLen[seg] < s do seg = seg + 1 end local l0, l1 = cumLen[seg-1], cumLen[seg] local r = (l1 > l0) and (s - l0) / (l1 - l0) or 0 return px[seg-1] + (px[seg]-px[seg-1]) * r, py[seg-1] + (py[seg]-py[seg-1]) * r end -- 時間制御のカーブを、0~1の入力に対して0~1の値を返す関数として作成する(時間制御が設定されていない場合は直線とみなす) local function makeCurve(name) if obj.getoption("track_mode", name) == 0 then return function(u) return u end end local duration = obj.totaltime - 1/obj.framerate return function(u) return obj.getvalue("track." .. name, duration * u) / 100 end end local fadeCurve = makeCurve("curve") local gradientCurve = makeCurve("gradCurve") -- 各サンプルのずらし量と重みを作成 local trailLen = totalLen * length / 100 local n = math.floor(samples) if n == 0 then n = math.ceil(trailLen / AutoSampleSpacing) + 1 end n = math.max(2, math.min(n, MaxSamples)) -- 加算・合成モード時の重みはサンプル間隔に比例させ、サンプル数や長さを変えても明るさが変わらないようにする -- (長さが短い時は全コピーがほぼ重なるため、OverlapBaseLength分の長さがあるものとして扱う) local overlapScale = math.max(trailLen, OverlapBaseLength) / (n - 1) / OverlapBaseLength * overlapGain / 100 local offs = {} local gradPos = {} local minDX, maxDX, minDY, maxDY = 0, 0, 0, 0 for i = 0, n - 1 do local u = i / (n - 1) local x, y = pointAtLength(trailLen * u) local dx, dy = x - px[0], y - py[0] gradPos[i] = gradientCurve(u) local w = math.max(1 - fadeCurve(u), 0) offs[i*4], offs[i*4+1], offs[i*4+2], offs[i*4+3] = dx, dy, w, w * overlapScale minDX, maxDX = math.min(minDX, dx), math.max(maxDX, dx) minDY, maxDY = math.min(minDY, dy), math.max(maxDY, dy) end -- 描画範囲(オブジェクト中心を原点とした座標)を求める local minX, maxX = -obj.w/2, obj.w/2 local minY, maxY = -obj.h/2, obj.h/2 if not fixSize then minX, maxX = minX + minDX, maxX + maxDX minY, maxY = minY + minDY, maxY + maxDY if showBezier then for i = 0, ArcDivision, 10 do minX, maxX = math.min(minX, px[i] - BezierWidth), math.max(maxX, px[i] + BezierWidth) minY, maxY = math.min(minY, py[i] - BezierWidth), math.max(maxY, py[i] + BezierWidth) end end end local maxW, maxH = obj.getinfo("image_max") local bufW = math.min(math.ceil(maxX - minX), maxW) local bufH = math.min(math.ceil(maxY - minY), maxH) local shiftX = (maxX + minX) / 2 local shiftY = (maxY + minY) / 2 obj.setoption("drawtarget", "tempbuffer", bufW, bufH) obj.draw(-shiftX, -shiftY) -- 色を0~1のRGBに分解する(nilの場合は0) local function rgb01(color) if not color then return 0, 0, 0 end local r, g, b = RGB(color) return r/255, g/255, b/255 end local br, bg, bb = rgb01(bezierColor) local sr, sg, sb = rgb01(startColor) local er, eg, eb = rgb01(endColor) local consts = { bufW, bufH, n, str / 100, bufW/2 - shiftX, bufH/2 - shiftY, keepOriginal and 1 or 0, showBezier and 1 or 0, P1[1], P1[2], P2[1], P2[2], P3[1], P3[2], P4[1], P4[2], br, bg, bb, BezierWidth, -- 色が未指定なら適用率0(元オブジェクトの色)。透明度の分だけ元オブジェクトの色を混ぜる colorMode, startColor and (1 - startAlpha / 100) or 0, endColor and (1 - endAlpha / 100) or 0, 0, sr, sg, sb, 0, er, eg, eb, 0, } -- gradPosは固定長の配列なので、offsの位置がずれないよう使わない分も0で埋めて渡す for i = 0, MaxSamples - 1 do consts[#consts + 1] = gradPos[i] or 0 end for i = 0, n*4 - 1 do consts[#consts + 1] = offs[i] end obj.pixelshader("PathBlur", "tempbuffer", "tempbuffer", consts, "copy", "clip") obj.copybuffer("obj", "tmp") obj.cx = obj.cx - shiftX obj.cy = obj.cy - shiftY