— phi16 (@phi16_) 2021年8月14日
かわいい浴衣になんか入れたいなと思って扇子をつくりました。ついでになんか入れたいなと思って風を送れるようにしました。
最近は記事を全然書いてないのでひさびさに。
メッシュ
まず扇子をつくります。
はい。束ねてるのはシェーダです。ぶいちゃでシェーダ無効化してる人が居ると多分よくわからない板を持つ人になっていたのかなと思いますが、考えるのをやめました。
理由はまぁ「開く」ためです。shape keyもboneもうまくいかないような気がした (単純には)。なんか変な方法はあるかも、bone2個用意してX軸とY軸作るとか (ちゃんと回転を表現してあげる?)。
void vert(inout appdata_full v, out Input o) { UNITY_INITIALIZE_OUTPUT(Input, o); float partAngle = 0.11 * _R; if (v.vertex.x < -0.09) { int i = floor(v.vertex.x / 0.2 + 0.5); float a = - (i+1) * partAngle; if (v.vertex.z > -0.05) { v.vertex.z -= 0.1; float flip = abs(abs(v.vertex.y) - 1) < 0.5 ? 1 : -1; v.normal.xyz = normalize(float3(0.1*flip,0,1)); v.vertex.y = v.vertex.y > -0.1 ? 4 : 1.2; o.paperUV = (v.vertex.xy - float2(-0.1, 1.2)) / float2(-4.8, 2.8); int i = floor(v.vertex.x / 0.1 + 0.5); a = - (i/2.0+0.75) * partAngle; a = min(max(a, 0), 2.538); v.vertex.x -= i * 0.1; if (i % 2 == 0) { v.vertex.z += 0.02 * _R; v.vertex.x += 0.032 * (1 - _R); } else v.vertex.x -= 0.032 * (1 - _R); } else v.vertex.x -= i * 0.2; v.vertex.z += 0.005 * (i + 1); float2x2 m = float2x2(cos(a), -sin(a), sin(a), cos(a)); v.vertex.xy = mul(m, v.vertex.xy); v.normal.xy = mul(m, v.normal.xy); } else { if (v.vertex.z < -0.2) { float a = 23 * partAngle; float2x2 m = float2x2(cos(a),-sin(a),sin(a),cos(a)); v.vertex.xy = mul(m, v.vertex.xy); v.normal.xy = mul(m, v.normal.xy); } } }
毎度のad-hoc実装ですね。しょうがないね。扇子の紙が微妙に傾いたりしてるのも面倒ポイントですよね。というか紙が一番調節が大変でした。それはそう。
折れた紙の法線は正確ではなく適当に回してるだけです。それっぽいのでOK。
あ、そういえば折れた紙の各頂点をシェーダ側で識別するために (同じ位置の頂点でも違う方向の法線を吐く為に) 上下にずらしたんでした。
UVを使ってないのは普通にサブペ製のテクスチャを突っ込むためです。
回転用のパラメータは適当にMaterial経由で拾えるようにしつつ、Animationを作ってExpression Menuからいじるようにします。良い時代ね。
いろいろあるのはいろいろです。これだけを置いたLayer作ってMotionTimeをパラメータ制御にと。
風っぽいの
今回やりたかったのは「平面と空間の接続」です。扇子の表面に描かれたパーティクルが、振ると扇子の外に立体的に出てくる、っていう。
途中までは扇子のローカル空間で計算して、ある程度外に出るとワールド空間に書き換えることにしました。ローカル空間に居る間は、扇子のシェーダ側でも直接色を計算してあげます。
いろいろ算数をやってるんですが解説がめんどくさくなったので省きます。
float3 pos = unpack(uint2(0, i)); float3 up = mul(UNITY_MATRIX_M, float4(pos / 0.06, 1)) - orig; up -= dot(up, axis) * axis; float3 a0 = normalize(up); float3 a1 = cross(axis, a0); float x = dot(a0, cp - up); float y = dot(a1, cp - up); float d = abs(x) + abs(y); d /= state.y; d /= state.x < 0 ? -1 - state.x : 1 - pow(state.x - 1, 3); d /= _R + 0.0001; md = min(md, d);
四角形一個つくるやつ (四角形の軸方向の計算がアレ)。128回もループ回ってるのでちょっと重かったかも。
元々扇子側もadditiveで表示してたんですが、そうするともはや扇子側の描画いらんのではという感じだったので、多少意味があるように「全体の輪郭線」を描画するようにしています。
解説の順序がアレで申し訳ないんですが内部構造はこんな感じ。
Captureはカメラで、16x128の板ポリを眺めるやつです。この板ポリが計算機です、毎度のごとく。
一番下が「1F前のUNITY_MATRIX_M
」。
他は各行が「position, velocity, rotation.xyz, (rotation.w, size + state, wait time)」です。
sizeは ] の数だけどstateは離散的な数 (しかも有限) なので、直積の結果を に埋め込める
あとはこう。がーっとやる感じで。
遠心力で力が働くようにしてるのですが、座標系をあわせるのが大変でした。いつも適当にやってるので。
// i.t0: previous frame rotation matrix (inverted) // i.t1: current frame rotation matrix (inverted) // i.w: angular velocity // i.loc0: previous frame base position // i.loc1: current frame base position float3 d = mul(pos, i.t1 - i.t0) + i.loc1 - i.loc0; vel *= 0.99; vel += mul(i.t1, cross(i.w, d)) / dt / (1 + state.y); // WHAT pos += vel * unity_DeltaTime.z;
あとは外に出て消失したら再度スポーンするようにするとまぁ無限に出てくるわけで。今回は速度を与えるのが自分の腕の動きのみなので、「何もしなければ溜まって、一気にたくさん出せる」「ずっと振っとくと少量がずっと出せる」といい感じにcontrollableになってくれました。
あとサイズ違いで速度が違ったりスポーン位置が実は固定だったりいろいろあるんですがまぁ小さな話ね。
おわり
アバターギミックを作らなくなったのはカメラを仕込むのがだるいからで、さらに仕込んでもアレ (フレンド外にみえない) っていう理由だったと思います。安定させる方法もわからんかったし。
ひさびさになんか作りましたが、特にぶいちゃの仕様に詰まることもなく出来たので安心しました。また当分作らないと思います。
たまに自分が何が出来るかを再認識できる機会があると嬉しいですね。