ちょっと前に以下のワールドを作りました。
音もつけました。たのしい!
SEつけた pic.twitter.com/spuQ5QePLO
— シバヅケ (@shiva_duke28) 2024年3月23日
フォグについてはBoothに1500円で売ってますが、Lightsaber2では少し手を加えたものを使ってます。
フォグを使うために深度バッファが必要なのでワールドにめっちゃ薄くSSAOをかけてます。
フォグは線分光源の内散乱に対応していて、その計算には線分の端点のワールド座標が必要です。ClusterScriptのベータAPIでマテリアルにパラメータを渡せるようになりましたが、Lightsaber2ではセーバーの端点をスクリプトで動かして、その位置をカメラを通してRenderTextureに書き込んで、フォグのシェーダーからRenderTextureを読む、という形で対応しています。
セーバーの両端点にすごく小さなQuadを置いて、以下の「画面の指定の位置にモデル行列(4x4行列)をRGBAx4ピクセルとして書き込むシェーダー」をつけています。
部屋にはセーバーが2本あるので、RenderTextureの解像度は4x4で十分です。精度が必要なので各チャンネルが32bit SFLOAT。
部屋の外に画像のようにカメラを置いて、室内が全てレンダリングされるようにしています。Culling MaskはInteractableItemとGrabbingItemにして、Depthは最速であって欲しいので-100にしています。 (書いていて気づいたけど、ライトセーバーのグリップとかもこのカメラに描画されるはずなので、モデル行列を書き込む君はRenderQueueを一番最後にするかZWriteありにしてnearスレスレにするとかした方が良さそう。)
フォグ側は以下のようにして4点の座標を読み込んでいます。
const float SaberThreshold = 0.05; float3 P0 = tex2Dlod(_ParamTex, float4(0.875, 0.125, 0, 0)); float3 P1 = tex2Dlod(_ParamTex, float4(0.875, 0.375, 0, 0)); float3 P01 = P0 - P1; float distSqr01 = dot(P01, P01); Ls += UniformFogSegmentLightInScatter(cameraPos, view, P0, P1, distance, 0.05, 18) * _Saber0 * smoothstep(0, SaberThreshold, distSqr01); float3 P2 = tex2Dlod(_ParamTex, float4(0.875, 0.625, 0, 0)); float3 P3 = tex2Dlod(_ParamTex, float4(0.875, 0.875, 0, 0)); float3 P23 = P2 - P3; float distSqr23 = dot(P23, P23); Ls += UniformFogSegmentLightInScatter(cameraPos, view, P2, P3, distance, 0.05, 18) * _Saber1 * smoothstep(0, SaberThreshold, distSqr23);
このままだとメインカメラに対してもモデル行列の値が描画されてしまうので、ステンシルテストをするようにしています。 カメラの前にある黒い板ポリはステンシルバッファに100を書き込むだけのシェーダーをつけていて、セーバーの両端点につけているシェーダーはステンシルが100のときだけパスするようになっています(本当はレイヤーでやりたい)。
セーバー自体はBlenderで高さ1mのシリンダーで原点を底面の中心にしたものを用意して、以下のように頂点シェーダーでRenderTextureを読んでスケールを変えるようにしています。
v2f vert(appdata v) { v2f o; float y = 0.125 + 0.25 * 2 * _Index; float3 p0 = tex2Dlod(_ParamTex, float4(0.875, y, 0, 0)); float3 p1 = tex2Dlod(_ParamTex, float4(0.875, y + 0.25, 0, 0)); float len = length(p0 - p1); v.vertex.y *= len; o.vertex = UnityObjectToClipPos(v.vertex); return o; }
アウトラインがこういう感じになっちゃうのは諦めました。
SEは以下の音源を利用させていただきました。音が付くとそれっぽさが増して最高です。
効果音工房
- ライトセーバー的な音-03の効果音 https://umipla.com/soundeffect/2310
- ライトセーバー的な音-05の効果音 https://umipla.com/soundeffect/2316
- ライトセーバー的な音-06の効果音 https://umipla.com/soundeffect/2319
きままに
- ライトセーバー風【起動音】https://commons.nicovideo.jp/works/nc154033
- ライトセーバー風【停止音】 https://commons.nicovideo.jp/works/agreement/nc154034
「シュッと振ったら音が鳴る」は前フレームと現フレームの回転が大きく変わっていたら鳴らす、という形にしています。
const rot = $.getRotation(); const prev = $.state.rot ?? rot; $.state.rot = rot; const dot = rot.dot(prev); const dist = 1 - dot * dot; if (dist > 0.01) { se.play(); }
ライトセーバーのItemにつけているスクリプトの全体は以下のような感じです(先端の移動とSEを鳴らすしかしてない)。
const sn1 = $.subNode("Saber1"); const se3 = $.audio("SE3"); const se5 = $.audio("SE5"); const se6 = $.audio("SE6"); const seOn = $.audio("SEOn"); const seOff = $.audio("SEOff"); $.onUse((u, p) => { // mode:0(off),1(on),2(turn off),3(turn on) const mode = $.state.mode ?? 0; if (u) { if (mode === 0 || mode === 2) { seOn.play(); $.state.mode = 3; } else { seOff.play(); $.state.mode = 2; } } else { $.state.mode = (mode === 0 || mode === 2) ? 0 : 1; } }); $.onUpdate(dt => { let t = $.state.t ?? 0; const tse = $.state.tse ?? 0; const rot = $.getRotation(); const prev = $.state.rot ?? rot; $.state.rot = rot; if (t > 0) { if (tse <= 0) { const dot = rot.dot(prev); const dist = 1 - dot * dot; if (dist > 0.01) { const rand = Math.random(); if (rand < 0.333) { se3.play(); } else if (rand < 0.666) { se5.play(); } else { se6.play(); } $.state.tse = 0.6; } } else { $.state.tse = tse - dt; } } const mode = $.state.mode ?? 0; if (mode === 0 || mode === 1) return; if (mode == 2 && t <= 0) { $.state.mode = 0; return; } if (mode == 3 && t >= 1.5) { $.state.mode = 1; return; } t += mode === 2 ? -dt * 2 : dt * 2; t = Math.max(0, Math.min(1.5, t)); $.state.t = t; sn1.setPosition(new Vector3(0, t, 0)); });