Imaginantia

思ったことを書きます

一生成一消費原則

何度か考えていることの断片を適当にメモ。

「何かを与えたとき、必ず何かを受け取っている」。

物を買うとき、金銭を与えて物を受け取る。それ以上に、物を買われたという体験を与え、物を買ったという体験を受け取っている。

つまり対価の解釈を広げる話。例えば「無料で公開された商品」を得るとき、それは「対価を払わないという体験」を受け取っていて、それはつまり「知らないところで『不均衡な関係性』を生む責任」を与えている。

何かを作って誰かに見せたら、「見たという体験」を与えていて、「見なかった可能性」を奪っている。

感想を与えたら、「感想を聞かないという選択」を奪っている。

実際に言及が発生するかどうかは関係なく、これは何をやろうとも必ず対応して発生する。全ては表裏一体なので。

 

そして、この不均衡に気づかないことが、状況の不安定さを生んでいるような、気はする。いろんなところでね。

 

制御できることは制御できないことを奪う。絵があることは想像可能性を奪う。各媒体が具体例としてわかりやすいか。

制約が強ければ強いほど、出来ることは多く、実現可能性は小さくなる。

それが能力の代償なんですよね。

 

「わからないもの」は理解を与えていないから「意味を持たない」=「存在しない」。だけれど、そのわからなさを与えている… つまり意味の無さを与えている…

対象を増やすことはそれぞれに対応するということ…

選択もまた必ず代償を持って… 結局それこそが、という話

記録 210605

書きたいことがあるならちゃんと書けばいいと思いました。

久しぶりに諸々の状況について記します。定期的に書くべきなのかも。

 

いろいろ作ったり作っていたものが出た最近ですが、現在は相変わらずしんどい気持ちで過ごしています。

表層的な原因は「作ったものが不具合を出しがち」だから。もうちょっと進むと「不具合を出してしまう自分自身」が。

そして奥に進むと「何かを作ることでどんどん負債が増えていっている」から、ですね…。

GC5.0

現時点での不具合は「入り口」「アバターライティングがたまにバグる」「ライトマップがおかしい」という点です。

それぞれ「場所の変化を気づかせたくない」「アバターにもちゃんと光を載せたい」「動的光源をちゃんと映してあげたい」という願いから来ているものです。

願い、その実現手段がギリギリで目的に届いていないこの感じ、です。もっと安定的な運用ができれば良かったんですけどね…。

まぁ完成?してからそう時間が経ってはいないとはいえ、ね…。ダメなものはダメなので。

他にうまくいってたりする部分がたくさんあっても、それはもう私の手からは離れたものであり。最終的に私の手に残るのは問題点だけなのです。

そして問題点が全て消えたら何も残りません。まぁ、それでいいんです。むしろそれを望んでいます。

でも当分はそうはならなそうだから…。うーん。

何よりも「運用」というのは「永遠」ということですからね…。

 

まぁこんなこと言ってても問題が解決さえすれば身軽にはなるんでしょうけどね。

…っていう性格もまた問題か。後で書こう。

Half Line

久しぶりにワールドを作りました。正確に言うと、久しぶりに「満足の行く」ワールドが出来ました。

Saturata と Single Step はほぼ実験的な空間で、こう、深さがあまりない。写真に近い。それこそ Saturata は「日記」だったし、Single Step は「絵」だと思う。

slice of time と Lights はコンセプトとその返答ですね。一歩くらい足を踏み入れる場所があります。slice of time は「機構 / 前に進むと時間が進む」と「その制御 / 上に登る方法」、Lights は「構造 / 粒の降り注ぐ空間」と「その解釈 / 反射によって光と顕す」です。

Half Line は「コンセプト」「意図」「誘導」「解釈」そして「反応」があります。Amebient には「進行」があったけど、それ以外の抽象成分は結構持ってるんじゃないかしら。

なのでとてもお気に入りなのです。

 

コンセプトは「創造的な破壊」です。

まぁ理由は単純で、「私が破壊衝動に呑まれていたから」です。だからそれが出来る空間がほしかっただけ。

そしてそれをベースとしたことで、「ここに来た人を破壊衝動に巻き込む」という空間としての意図が生まれました。

この動画はお手本みたいなもので、こう、こうやって力を込めてほしい、という気持ちなのです。

さて、「意図」にはそれを実現させる「誘導」が無ければなりません。私はそれをアニメーションだけでやりました。それしか出来ないだけなんですけど。

「溜め」の必要性はぽわんって出てくる円の様子と、同時に膨らんでいく杖の先端のキューブで表しています。引く速度によって変化があるのは、触っていくと「感触」でわかると思います。

キューブが「急に一気に伸びる」のは私の願いがそうだったからで、それはワールド名にそのまま書いてありますね。これは「小さな変化が大きな変化を生む」点でもあって、「破壊」の本質に近いものかなぁとちょっと思いました。今。

あとまぁ音も大事ですよね、減衰の強さは線の長さに、音程は線の太さに対応しています。その辺からも「感触」がつかめるようになる…と想定しています。

 

ちなみにそういえば裏コンセプトがあって、「(おなじ) ふたつ」です。白と黒はわかりやすいですね。地面も二色のチェッカー模様です。

白黒それぞれで出てくる音程は各々ホールトーンスケールになってて、半音ズレてるので一切かぶらないようになってたりします。調性の無さがそのまま空間の平坦さに繋がっていて、かつ複数一気に出したときの五度堆積もまた感情の無さに繋がっています。要は「押し付けない」ということですね。

あと「左手 / orthogonal」と「右手 / perspective」も対比になってるんですが、これは機能性から来たものだったりします。まっすぐ線を引きたかったから。

半直線も「在る側」と「無い側」っていう解釈はありますね。そこまで考えてないけど。

 

で、いろいろ試して適当に線を伸ばしたあとで、自分の出した線群が「破壊としての様相」を示していることを観測し、いっそのこととさらに破壊性を助長する、みたいな。そんな気持ちで遊んでくれたらいいなと思ってました。気づかせずに「押し付け」ようとした感じです。多分。

結構public見るとね、最悪なりに最悪でだいぶいいですね。publicのpublicらしさがそのまま出ている感じがします。

 

まぁ、という、ユーザの制御下に見せかけて、「操作しやすさ」で以てユーザの行動を制約する感じ、とか。

端的に言えば「無意識制御」?

あとまぁ抽象空間であることとか、その辺、こう、「私が一番やりたいこと」だったので、だいぶ気にいりました。うんうん。

 

そういえばこれは VRChat の World Jam に提出したものでもあるんですが、「これになった」理由の一つの側面として「絵を描かせない」というものがありました。

基本的にまぁ Drawing と言われれば各々好きに描くものです。そうですよね。それでいいんです。

私はそうやって「好きに描いてください」というのがものすごく苦手で。原因は「何を描いても自分が出るから」です。それはそのまま表現力による代償です。

唐突に山を描き始めたらその時点で「その人はまず山を思い浮かぶ人」という認識が発生するわけです。そうありたくないので。

まぁ、で、私と似たような人がどれくらい居るかはわかりませんが、そういう人にとって Half Line は「描きやすい」んじゃないかなと思います。

つまり制約され、誘導されている状況が。それでも各々の性格が出るこの感じが。まぁ私は好きなのです。

Amebientもそれに近いですよね、結局あれの自由度は「選択」を上回りません。それでも楽しい空間です。

 

例えば Half Line で作った「建造物」は「作品」にはならないと思うのです。他のワールドではそれが起こりうるかもしれない。

まぁでも、元の思想が「作品を作ってほしい」ではないから、これでいいわけです。自由に「描いて」ほしいだけです。破壊を。

 

そういえば所謂 𝑽𝑹 𝑨𝒓𝒕 をぺちぺちする意味合いも含んでいることを思い出しました。

これくらいの規模で「空間」を作ってほしいですね。まだまだ表現できることはたくさんあるはずですから。

まぁいいやそんなことは。

 

とはいえここもめちゃくちゃ不具合があってね。所有権を移す (杖を渡す) とたまにUdonが死にます。原因は…はて。

今日の昼頃にまた怪しいところの修正をしてみたのだけど、これで何回目だろうか。きっと治ってないです。

イベントが来るタイミングとかの精密な仕様があればまた違うんですけどね…。もう。

まぁこれもあって結構しんどい気持ちだったんですよね。はい。

治すべき箇所がわかれば治すのだけど、そうならない限りは放置かな…。

プログラムを書くのは難しいです。

衣装

さて、話は一気に変わって最近のアバターのことを。

Nellちゃんが来てからだんだん変化が加速している現在ですが、恐くもあり、嬉しくもあり、です。嬉しさは実際強い。

まぁ服の強さも勿論あって、ミリタリードレスは完全にほしかったやつです。長袖・ロングスカートが好きな私ですが、これに関しては機能性が上回ります。それはそう。

…という理由によって「挙動」を変えることが出来ました。こう、荒っぽいというか、なんかそういう感じ。ふとももの主張が大きい (個人的な感想) のでおしとやかには成れないんです。なので。

最近は「自分に合う衣装を選ぶ」よりも「衣装に自分を合わせる」スタイルになっている気がしますね…。メイド服もそうね。

Nellちゃんは穏やかでそれはそれでいいですね。まぁ分裂したと言えばそうだと思う。

今まで諸々「合わない」と思って買ってない衣装とかも今なら割と許容範囲なのではないかと思います。暇があったらまた探してみたいね。

 

そういえばkanonさんち所属については、個人的にはこれはこれで解釈一致なので、いいのではないかと思っています。

浮き沈みの話

あとはおまけで。

私は基本的にめちゃくちゃ安定しているタイプの人間で、何故かというと一定範囲までの能力は確立されているので疑う余地がないからです。

が、その先どこまで行けるかというのはよくわかっていません。まぁ一般にわかるものではない。

そうすると「自分にある程度を期待する」「期待を上回る結果が出るとさらに期待する」「その期待に応えられないと一気に落ちる」「その状態で調子を戻そうといろいろやってみる」「ある程度の成果が出る」というサイクルが完成します。おしまいです。

今がどこに当たるかはよくわかりませんが、事実としては「成果が出ている」「が、期待に完全に沿うわけではない」という状況で、まぁあまり進んでない感じ。不具合の修正が終わったら「期待が上昇するフェーズ」に入るのかもしれない。

身の程を知れという話なんだけれども。でも身の程に収まってたら創作なんて面白くないですよね。

まぁこんなサイクル何度目だよって話でもあるんですけどね…。ぶいちゃは「期待を上回る結果」というものが本当に馬鹿でかいことがあるから感覚がバグる。

そろそろ自分の限界が見えてきたところかと思っていたのがAmebientだったわけだけど、全然違うところに行くとこうなるというのがGC5.0でわかってしまったのが善くないね。

「常に違うことをやりつづける」という私の性格と永遠に戦っていくしかないですね。いやむしろ「違うことをやること」に逆を張ってほしいところだけれど。

はい。

とりあえず次何をするべきなのかはあんまりよくわかっていません。タスクはいろいろあるんだけどね。

もう少し穏やかな気持ちでやっていきたいです。しんどさから逃げたい。

おわり。

そういえば、「わたしはわたしが間違ってないと思う」、というのがやばいな、というのを最近思っている。

バグレポート / PassageTeleport

バグのせいです。

 

6/9 解決済み: 末尾に追記

こんばんはお疲れさまです。

この度多大なる迷惑を現在も継続しておかけしておりますあの入り口について、現状どのような感じかお伝えしようと思います。

仕組み

PassageTeleportというUdonがあの機構全体を管理しています。基本的にはdisableの状態です。

bool open; // 開こうとしてるときのみ true
float openTime; // 開き始めてからの時間
bool closing; // 閉じる遷移のときのみ true
float closeTime; // 閉じ始めてからの時間
bool sleep; // テレポート直後は一度区画から出るまで処理を貫通

public void Update() {
    if (player == null) return;
    Vector3 playerPos = player.GetPosition();
    if (open) {
        openTime += Time.deltaTime * 0.5f;
        if (side0) {
            // (ドア0のアニメーション)
            if (insideClose0.Contains(playerPos) && !sleep) {
                open = false;
                closing = true;
                closeTime = 0;
            } else {
                if ((openTime > 5.0f || sleep) && !outsideClose0.Contains(playerPos)) {
                    open = false;
                    closing = true;
                    closeTime = 0;
                }
            }
        } else {
            // (ドア1のアニメーション)
            if (insideClose1.Contains(playerPos) && !sleep) {
                open = false;
                closing = true;
                closeTime = 0;
            } else {
                if ((openTime > 5.0f || sleep) && !outsideClose1.Contains(playerPos)) {
                    open = false;
                    closing = true;
                    closeTime = 0;
                }
            }
        }
    }
    if (closing) {
        closeTime += Time.deltaTime * 0.5f;
        if (side0) {
            // (ドア0のアニメーション)
        } else {
            // (ドア1のアニメーション)
        }
        if (closeTime > 1f) {
            closing = false;
            enabled = false;
        }
    }
    if (!open && !closing && !sleep) {
        if (bounds0.Contains(playerPos)) {
            Teleport0to1(playerPos);
        } else if (bounds1.Contains(playerPos)) {
            Teleport1to0(playerPos);
        }
    } else {
        if (sleep && !open && !closing) {
            if (!bounds0.Contains(playerPos) && !bounds1.Contains(playerPos)) {
                sleep = false;
            }
        }
    }
}

概ねこんな感じになっています。最悪ですね。

ただまぁconsistencyとしてはいくらかわかりやすい部分があって。closing = true のとき、一定時間経つと closing = false になるはずなんですよ。

何故なら closeTime はだんだん増加していくはずで、closing = true に成るとき同時に closeTime = 0 になるから、そして closeTime > 1 のとき closing = false になるからです。

だから扉が途中で止まるはずないんですよ。

調査報告

enabled = false は怪しいよなぁ、とは思って、enabled への代入を Update 末尾に行うようにしたんですが、変わりませんでした。

仮説としては「Update内でenabledをfalseにすると、Udonの実行時間チェックとかのコンテキストスイッチによって以降不活性化する」みたいなことを考えていたんですが、これは「enabledじゃないのにメソッドが呼べる」ことに反するのできっと違います。

ちなみに enabled = false にして直後 10000 回のループを回すテストをしてみましたが、ちゃんとラグは発生するけど別に途中で止まったりはしませんでした。

 

不思議なのは、一度死ぬとあのボタンが一切動かなくなることです。

public void Open0() {
    if (open || closing) return;
    open = true;
    openTime = 0;
    side0 = true;
}

ボタンを押す方のUdonではPassageTeleportのenableをしているので、Open0 がちゃんと最後まで呼ばれていれば次の瞬間扉は初期位置に戻るはずです。

つまり open || closing が成立してそうな雰囲気がします。

そして「閉じかけの扉」については open は成立しません。つまり「閉じかけで固まる状態」は、 closing = true ではないかと推測されます。

しかしこれは先の話と矛盾します。何故進行しないのか。

そうすると残る不審箇所は Time.deltaTime しかないのです。はて。

 

あと強いて言うなら、上の記述では省略したんですが、何故か Open0Open1 の呼び出し時についでに player = Networking.LocalPlayer をやっているらしいです。

それが何故か null になって Update が走らなくなるのはありうるのかな…とは思いましたが、別にボタンを押してないのに急に扉が途中で止まるのはやっぱり変だと思うのです。

結論

ちなみにこの不具合、人数が多くないとなかなか発現しません。まぁ。

なんだろう。なんなんでしょうね。ほんとご迷惑おかけしております。

enabled が死んだときの対策として EmergencyButton を追加したんですが、でも普通に enabled = true っぽいので多分効果は出てないのです。これ。

player か、Time.deltaTime か…。前者説はありそうだけどじゃあなんで Networking.LocalPlayer が気まぐれで null になるんかっていう話なんだよな…

もうちょっと調査はしようと思います。そしてまぁ最悪ケースの為のセーフティは入れようと思います、よろしくおねがいします。

 

お急ぎのところ、ご迷惑をおかけしています。


 

追記。

RunEvent will clear incorrect event parameter names

これじゃん~~~~~~~~~~~~~~~~~~~~~~~~~~は~~~~~~~~~~~~~~~~~~~~~

以上です。ありがとうございました。

無 解説

制作した「無」について解説します。

f:id:phi16_ind:20210529153428p:plain

きっと殆どの人は気づいていない。何より気づかなくて良い。

ただ其処に在るだけの概念です。

結論から言います。

あのワールドでは Unity についてる Realtime GI 機能は使われておらず、動的光源による光量の計算を全て自前で行っています。

たまに見かける「ライトマップの寄与をシェーダ側で制御することでオンオフする」やり方 (ref 1, 2) を拡張したものにあたると思います。この辺やってたのは伏線ですね。

加えて、アバターのライティングの為に視界ジャックによる光量の直接的な調整を行っており、また Directional Light の色も動的に制御しています。

理由は幾らかあって。

  • Realtime GI だと高速な点滅に対応できない
  • 処理負荷が高いと稀に「光りっぱなしになる」現象が起きる
  • アバターに影響する光量はシェーダによってまちまちなので統一させたい
  • 本物の「黒」を作りたい
  • 何よりも光るアバターを潰したい
  • 光るな

加えて、光量計算の際に光源の情報を取り入れることで、単純なライトマップだけでは出せない「反射感」を出していたりもします。実はこれの話は伏線です。

f:id:phi16_ind:20210529160344p:plain

概要

まずは単純な方から。

あの部屋にある16個のサイドランプは、見た目通り動きません。よってその寄与は「各々単体で焼いたLightmapに個別の明るさを乗算したものを全部足し合わせる」ことで計算が出来ると言えます。

つまりこうです。

f:id:phi16_ind:20210529161718p:plain

対して正面にあるアレは「動く」のでどうしようもありません。そこで逆に諦めて、一枚の平面と捉えることにしました。

まず可動域を囲える3.4m×3.1mの矩形を次のように8分割して、それぞれに対するLightmapを生成します。

そして光る部分を前面からCameraで256x256のRenderTextureにキャプチャして、Level 6 の LoD (4x4) で各々の中央の点を (bilinearで) サンプリングするとこれは各領域の平均色になります。

f:id:phi16_ind:20210529163103p:plain

というわけでこの色を係数として線形和を取れば、好きにアレを動かしたとしても概ねそれっぽく振る舞うようにできます。内部実装を知らなければ気づくことは無いと思います。

この分割は「天井に近い方は (壁に激しく映るので) 解像度が高い必要がある」「逆に壁から遠ければ曖昧で良い」という理由で選択しました。

また、加えてあの部屋には「VJ室」「キッチン」「通路」それぞれに光源があります (VJ室はローカルなので基本見えないです)。今回は「VJ室」と「通路」の光源に対するLightmapをこちらで処理していて、キッチンの光は「普通に焼いたもの」を利用する形になっています (一番ディティールが出るべき光源なので)。

ちなみに通路はドアが閉じると光が届かなくなるので、光が届かなくなります。自然すぎて気づかれないと思うんですけど。まず暗いんですけど。

f:id:phi16_ind:20210529164811p:plain

ビル側にも同じことをやる必要があるわけですが、bake工程を独立にしたかったので「ビル側の床だけをこっちに持ってきてbake、それを本物のビルの上からAdditiveで合成」ということをやっています。

つまりあっちにも一応赤い光はちゃんと乗ってるんですが……めっっっちゃよく見ないとわかりません。私の作った中では一番気づかれないディティールだと思います。

反射

これでベースとなる「光の広がり」ができたわけですが、加えて壁に反射が欲しい気持ちになります。あのワールドは通常のbakeに Bakery の SH モード を使っていて、それがあのとてつもないディティール感に結構寄与しています。つまり反射は偉いのです。

でもこの自作のLightmapアトラスにそこまでの情報を含めるのは面倒だし、何よりサンプリング回数が少ないほうが嬉しいのです。

「きれいな反射が出る」ことと「光源の位置がわかっている」ことはほぼ同義です。今回は正面のモニタ群の座標やサイドランプの座標が明らかなので、それぞれ「いい感じに」やる余地がありました。

面光源をいい感じに計算する手法として Real-Time Polygonal-Light Shading with Linearly Transformed Cosines というものを聞いたことがあったので、やってみることにしました。

f:id:phi16_ind:20210529170421p:plain

𝑹𝑻𝑿 𝑶𝑵 とか言ってはしゃいでました。ちなみにこれはroughnessの扱いがすごく適当で、BRDFの再現とかも一切やってないんですが、それっぽいのでOKということにしました。

キッチンにめちゃくちゃ綺麗にモニタ群が映るのはこれのせいです。本来はもうちょっと遮蔽されるはずですね。

さて、これを各サイドランプに適用してみたところとても重かったので良くないなとなりました。代わりに、サイドランプの光源はおおよそ「線分」であることを利用して、これを直線と解釈して計算できる擬似的な方向成分を使って指向性を出しています。いい感じです。

あ、ちなみに Realtime Reflection Probe (No Time Slicing) も置いてあります。デッキの反射はこっちです。

オーバーレイ

これまでの話は全てワールド側のマテリアルの話です。アバターには掛かりません。

アバターのライティングを行うために、「画面全体を一定の色で乗算するオーバーレイ」を作りました。基本的に光量は線形なので、乗算によって適切に色を乗せることができるはずです。

乗るべき色としては前面のモニタの平均色に加えて各サイドランプの明るさがあって、それらの重み付き平均でどうにかしています。重みは手で調節しました。

また、bake時にはこの場所はほぼ白色光源で囲まれた部屋になっているんですが、一応モニタ側が明るくなるように調節はしてあります。

あとアバターのみに掛かる Directional Light も同じ感じで計算しています。こちらはサイドランプ抑えめ、モニタ群強めになっていて、この2色でアバターのライティングを誤魔化しています。

f:id:phi16_ind:20210529173421p:plain

意外と綺麗に出ます。

さて、オーバーレイは見境なく「全て」に掛けるのでこのままでは壁もさらに暗くなってしまいます。というわけで、ワールド側のシェーダには「最終計算された明るさを、オーバーレイの色で除算する」処理が入っています (Directional Lightの色にも入ってます)。これで相殺されて狙った色で出るというわけ。普通のEmissionも綺麗に出ます。

アバターのEmissionは全て潰れます。

そして全てのライトを消せば、全てが消えます。「無」です。これがやりたかった。

ただこれだと写真撮影時に困るということで一応の回避策も用意してあります。ご要望あればやり方伝えますのでこっそり聞いてください。

実動作

というわけで、クラブ部屋のライティングの工程は次のように行われます。

  • サイドランプの光量がUdonによって決定、Texture2Dに記録
  • モニタ群を前面から撮影して RenderTextureに保存
  • これらを元にしてオーバーレイ色とDirectional Lightの色をシェーダで算出、カメラで撮影 (2x1)
  • ReadPixelsでUdon側で読み取ってDirectional Lightに反映 (ref)
  • SetPixelsでTexture2Dに記録
  • Realtime Reflection Probeが6面分をキャプチャ (ここにオーバーレイ自体は含まれていない、除算は行う)
  • ワールドとアバターを普通にレンダリング
  • オーバーレイによって色が乗算

f:id:phi16_ind:20210529153934p:plain

だいぶGPUに無理をさせているように見えますが、45fpsは出てるのでまぁいいんじゃないかな…。これまでの Realtime GI 分の負荷を考えればそう悪くはないはずです。

終わり

めちゃくちゃ大変です。bake工程も意味わからないくらい複雑になっちゃったし。まだ一部のbake結果おかしいし。データ受け渡しもしんどいし。

開発自体に時間がだいぶ掛かっているわけですが、実は途中成果として、ある時のコンクリでコレの一部分を使ったりもしました。気づいた方もいらっしゃいましたが、その完成形が今回の機構ということになります。

大変です。でもまぁ。その甲斐はあったのだとは思います。

あの良い空間の「良さ」を引き上げる成分になれていたら幸いです。

これからの 𝑮𝒉𝒐𝒔𝒕 𝑰𝒍𝒍𝒖𝒎𝒊𝒏𝒂𝒕𝒊𝒐𝒏 をよろしくお願いします。


 

ProjectCiAN 制作記に書かれていた FakeGI の「オリジナル」がコレということになるんでしょうけど、実物を見たとき、なんだかわたちゃんの純粋性を汚してしまった感じがあって複雑な気持ちでありました。というかそれでりふさんを恨んでいるという表現になっていたんですけど。

でもまぁ。そんなのは気にすることではなかったのかもしれない。我を持ったみなさん最高です。本当にありがとうございました。

頭の振動周期を計算する

VRChatでは頭の位置を気軽に取得できます。そして頭はよく揺れますなんとなくその揺れの周期を計算してみようと思いました。

まず入力データは次の値 headY を使います。

var player = Networking.LocalPlayer;
var head = player.GetTrackingData(VRCPlayerApi.TrackingDataType.Head);
float headY = head.position.y;
headY += (head.rotation * Vector3.forward).y * 0.1f;

つまり位置の上下変化に加えて視線方向の上下変化もちょっと入っています。なんとなく。

身長による差異を吸収する為にhighpassもどきに通します。

float dt = Time.deltaTime;
lowpass = Mathf.Lerp(headY, lowpass, Mathf.Exp(-dt*4));
headY -= lowpass;

これで基本的に headY は静止状態では 0 になるはずです。

で、周期を測るわけですが、ここでは「headY が正から負になった瞬間」の時間差を使うことにしました。

if(Mathf.Abs(headY) > 0.001f) waitBorder = true;
bool flash = prevHeadY > 0 && headY < 0 && waitBorder;
passed += dt;
if(flash) {
  // ...
  passed = 0;
  waitBorder = false;
}
prevHeadY = headY;

微振動による反応を防ぐ為に少しくらい揺れてからじゃないと認識しないことになっています。

で、経過時間 passed を使うわけですが、1Fの間のどこで境界を超えたかをできるだけ正確に把握する為に headYprevHeadY を使って補正をします。要は移動速度が一定であると仮定すれば正負の境界を超えた瞬間が計算できるのです。

float ratio = headY / (headY - prevHeadY); // headYが負であることに注意
passed -= ratio * dt;

今回は8回までの記録を残すようにしました。また、後々使うので DateTime.Now.Ticks を使った正確な時間も覚えておきます。

long lastDate = prevDate[0]; // also for later use
int n = 8;
for(int i=0;i<n-1;i++) {
  prevDate[i] = prevDate[i+1];
  prevTime[i] = prevTime[i+1];
}
prevDate[n-1] = DateTime.Now.Tics - (long) (dt * ratio * 10000000);
prevTime[n-1] = passed;
recordCount++;
if(recordCount > 8) recordCount = 8;

今気づいたけど recordCount 使ってないわ。

おおよその周期は平均を取れば計算できると言えますが、まずそれ以前に「周期的かどうか」を知るべきだという話があります。

今回は8個のデータの標準偏差が0.045以下のときに周期的であると判断することにしました。これは実際にやってみて決めた数値です。

float sumTime = 0;
for(int i=0;i<n;i++) {
  sumTime += prevTime[i];
}
float aveTime = sumTime / n;
float sigma = 0.0f;
for(int i=0;i<n;i++) {
  sigma += Mathf.Pow(prevTime[i] - aveTime, 2);
}
sigma = Mathf.Sqrt(sigma / n);

周期的であるときに限って「実際に計算を行って振動数を決定する」という処理をするため、preciseMode という変数で現在の状態を記録しておくことにします。

そして、周期的であるときには「周期性が始まったタイミングから現在までの時間」を「拍数」で割る方が誤差が小さくなっていくと考えられます。というわけで、このときは prevDate を使って時間を計算します。

if(preciseMode) {
  if(sigma > 0.045f) {
    // Reset
    preciseMode = false;
    recordCount = 0;
  } else {
    // Continue to measure
    long passed = prevDate[n-1] - preciseBegin;
    float dur = passed / 10000000.0f;
    preciseCount++;
    dur /= preciseCount;
    float bpm = 60 / dur;
    UseTempo(bpm); // Yes
  }
} else {
  if(sigma < 0.045f) {
    // Start
    preciseMode = true;
    preciseBegin = lastDate;
    preciseCount = n;
  }
}

というわけで無事、頭の振動周期が計算できました。だんだん周期が変化する場合にはあんまりうまく対応出来ていなかったりもするんですが、その場合には無理やり頭の振動をやめることで対応していたりします。

加えて重いとちゃんと精度が出ないという問題があり、70人くらいが視界に何故か入っていると preciseMode に一切入ってくれません。横を向くことで対応しています。

 

はい。

これは私が行った方法なので、さらに精度良く周期を測れる方法が存在しそうな気はします。特にこの辺の分野は何もわからんので。

まぁとりあえず。一例ということで。

ゆらゆらごーすとのつくりかた

f:id:phi16_ind:20210527123249p:plain

はじめに

まずロゴを頂きます。めちゃくちゃびっくりします。あまりにも良かったので叫びます。

折角なので動かしたいという話を聞きます。ついでにぶいちゃで動くと最強です。

というわけで Domain Warping をすることにします。

もっと言うと、特定の Vector Field に従って Domain Warp した結果がロゴになるようにすることに、します。

よういする

Kerasを取り出します。

まずベクトル場レイヤーをつくります。こんな感じ

2次元のグリッドの各格子点にベクトルを与えてbicubic補完した値を返すレイヤーです。

そしてこれを紆余曲折しながら4枚繋げます。

def create_model():

    input = Input((2,))
    flow1 = flowlayer.FlowLayer(8, 32, -ratio, ratio, -1, 1)
    flow2 = flowlayer.FlowLayer(8, 32, -ratio, ratio, -1, 1)
    flow3 = flowlayer.FlowLayer(16, 64, -ratio, ratio, -1, 1)
    
    def w4(x):
        return Lambda(lambda x: x * 0.25)(x)
    def w3(x):
        return Lambda(lambda x: x * 0.5)(x)
    def w2(x):
        return Lambda(lambda x: x * 0.5)(x)
    def w1(x):
        return Lambda(lambda x: x * 1.0)(x)

    d2 = Dense(2)

    def fu(a):
        x = a
        x = Add()([x, w4(flow3(x))])
        x = Add()([x, w3(flow2(x))])
        x = Add()([x, w2(flow3(x))])
        x = Add()([x, w1(flow2(x))])
        return x

    def ac(a):
        x = a
        x = flow1(x)
        x = d2(x)
        x = Activation("sigmoid")(x)
        return x

    x = input
    x = fu(x)
    x = ac(x)

    ...

あとは回すだけですが、適当なlossにするとぐちゃぐちゃになって終わるので、今回は binary cross-entropy に加えて「合成されたベクトル場の (Δ=0.001で差分を取った) ラプラシアンの総和」もlossにいれます。これで尖りが減った気がします。

実行すると全然きれいになる気配がないので対策を考えます。

そだてる

最初から複雑な形状にするのはしんどそうなので手で成長方向を指定してあげることにします。

f:id:phi16_ind:20210527125035p:plain

そしてこれらの間を適当に補間 (sdfにしてlerp) させつつ順番に学習器にあたえます。

f:id:phi16_ind:20210527125221p:plain

育ちました (GT, Result, 合成ベクトル場, flow1, flow2, flow3 です)。

ベクトル場の解像度が高いとロゴそのものになっちゃうので適度に能力を潰さないといけないのがむずかしいですね。能力を下げすぎると壊れるし。

現在もまぁ完璧ではないです。

うつしかえる

完成した重みを画像としてexportします。

f:id:phi16_ind:20210527125525p:plain

後はデコーダを書きます。ちなみに4-sample bicubic interpolationはこの過程で出来たやつです。

ついでにまだかわいい目と口?が無いのでシェーダおえかきでぐっと書きます。

f:id:phi16_ind:20210527130030p:plain

できました。

かわいがる

幾つかの演算の結果としてこの状態になっているわけで、その過程を弄るとそれ相応に歪みます。

f:id:phi16_ind:20210527181009p:plain

また、最終的な単色の値だけではなく、中間のベクトル場をそのままdistortionに使えばガラスっぽい感じにもなります。

f:id:phi16_ind:20210527181445p:plain

さて、色々試すにはシェーダコードを弄る必要があるわけですが、折角なのでぶいちゃ上で操作したいところです。

というわけでノードエディタをつくります。

f:id:phi16_ind:20210527181319p:plain

できました。そう。これです。これ

これのつくりかた

  • まずノードを「前から順にソート」します。(バブルソートの1工程だけを1Fでやって時間方向に負荷分散)
  • 前から走査していくとグラフの接続状況が確定します。
  • そこから処理内容のアセンブリコードみたいなものが生えます。
  • その情報をVectorArrayに詰め込んでシェーダに渡して、VMっぽい処理を走らせます。
  • できました。

が、つかいません。使いませんでした。

もっかいうつしかえる

とりあえず放置していたところWebページのティザーとして使えないかという話がきます。

というわけでWebGLに移植します。しました。

あとはいい感じにシェーダを書くといい感じになります。よかったね。

要望としては「ロゴをまだ明らかにしない段階で、ロゴの『輪郭』を出す」というものがあったので (ティザーだからね)、今回のこれはぴったりなのでした。元データ入ってないからね。

ちなみに背景は全部これまでとこれからのGC (とテクノビネガー) の写真です。その domain warp です。あれは。

f:id:phi16_ind:20210527183026p:plain

なつかしいですね。

ティザーとしての役割は全うできたのではないかなと思います。

おわりに

というわけでそんなことをやっていました。

今のところ使いみちがあるわけではないんですが、そのうちどこかで使えたら良いですね。

あともっとおもしろい方法はあると思う。当然この過程にはめちゃくちゃ試行錯誤があってこうなったわけですが、造り手が違えば結果も違うもので。

今回は私がこうしたかったからこうなったので、あの子のもっとかわいい動きがあればとても見てみたい気持ちです。

 

全てにおいて、あの素晴らしいロゴを制作なさったなべさんに感謝をもうしあげます。

おまけ

試行錯誤過程の写真です。

f:id:phi16_ind:20210527185842p:plain

これはこれでいいですね。

優しく在るという願い

ふと見えたので。返事です。

私は「何」なのか、というのは度々考えていますが、時系列的な解釈を述べてみようと思います。

 

多分、私は「言い負かされるのが嫌」だったのです。

誰かと話していて、自分にとって望ましくない結果になってしまうのが悲しかったんだと思います。

そうやってずっと誰かの「主張」…のように見える、唯の「思いつき」と「その強制力」に振り回されたくなかったのです。

だから私は考えることを選びました。相手の主張の正しくなさを示して拒絶したかった。

そうして論理性を獲得することになりました。加えて、「相手の発言の矛盾を先回りする能力」を手に入れることにもなりました。

つまり相手が気づいていない領域まで予め推論を行って、直近では気づけないような問題を先に発見する能力です。

これは所謂漫画世界によく居る人間が持っている能力で、単純にそれに憧れた結果だと思います。

また、これの為には「自分を話に巻き込まない」必要があって (推論においては私という情報は不要なので)、それが巡り巡って「個を強く出さないという願い」につながっているのかもしれません。

 

で、そうなってから、私は多分「余裕が出来た」んだと思います。

つまり相手が急に意味わからないこと言い出したら「意味わからない」と返す能力を。相手の発言が不明瞭だと思ったらそれを指摘する能力を。相手の発言から感情成分だけを抽出する能力を得て、私は「人の攻撃性」というものを当事者になること無く観測する能力を得ました。自身の安全性が保証されたのです。

そうして、特に私は「個人の主張」が薄いので、結果的に「全てを許容しようとする」ことができるようになります。要は優しくなりました。

正確に言うと「全てを許容しようと願い」「優しく在ろうと願う」ようになったということです。

ここから多分いろいろな話が説明できて:

  • 素直にものを言うけれど、相手が過剰な感情を得ないように調節しようとする
  • 相手との「正しい」共感を狙って話し、「正しくない」共感に対しては拒絶的に振る舞う
    • 私は「理解の伴わない『はい』という返事」がとても嫌いです。
  • 誰かの能力不足に対しては、それを事実として提示しつつ、その無問題性を示そうとする
  • 他者の会話内で言語的齟齬が発生しているように見える際には間に入って緩和しようとする
  • 問題が発生したとき、その外側の枠組みまで広げることで問題を無かったことに出来ないか図ろうとする
  • 総じて、「厳密性はあるが、所謂厳しい人ではないように振る舞おうとする」傾向がある

勿論これは「願い」に過ぎず、それが完璧に叶っているわけではないんですが、その願いそのものが私をこの方向に進めているのだとは思います。

 

ただ、これは別に「私を優しい人だと認知されたい」という願いではないんですよね。そして実際その逆を行っているというのはそうなんだと思う。

恐らく怖さには2つあると思っています。「正しさを提示される怖さ」。そして、「正しく無さを肯定される怖さ」です。

私は前者は「善い」と思っているので積極的にやってるんですが (当然その正しさの枠組みを拡張することを含みながら)、後者については未だに悩んでしまっているところがあります。

私は別に甘えさせようとしているわけではないんですよ。本当に正しくなくて問題ないからそれを肯定するんです。

でも「肯定する」という行為は「責任を取る」と同義です。つまりこれは「正しくないと宣言する権利を没収する」ことになるのです。

これが多分「私を参考にしないでほしい」と思う理由なんだと思います。結局はいつか自分で責任を負うことになるのです。

私のその肯定性は、その存在を否定されるべきなのです。内容じゃなくて。

私はまだ制御出来てないです。たぶん。

実際、私は「作品を肯定してしまう」即ち「適当な解釈を与えることでその作品を理解してしまう」即ち「作品を理解していない」ということがあって、どうにかならんかな、と考えたりはしています。

厄介なのは他人の評価を推論に利用できないという点なんですよね。


 

まぁ何はともあれ、結論としては「何かに頼ることなく事実を唯観測する能力」は便利だと思う、というところになりそうです。

 

こういう話は外に出すものではないんですよね、本質的に。その必要が無いからです。

だからまぁ。きっと同じことを考えてる人は居るのだろうし…っていう言い方、毎回してるのもよくないのかな。

私はそういう人が居る可能性を否定しませんが居ない可能性も否定しないです。そういうところから、なのかもしれない。

はい。よくわからない結びになりましたが。おわり。


 

追記。

具体的な言及をすこししてしまったので、その分ちょっと書いておこうかと思いました。

「悪を肯定できない」というのは「自分で解決するつもりがない」即ち「自分で責任を負う気がない」即ち「受け身である」ことを意味すると解釈しました。

ここは基本的に積極性が要求される世界です。だからこそのハードルであり、だからこそのクオリティなのでしょう。

だからその態度自体がきっと「違う」のです。

そしてもう一つ気になったこと。

私は「環境差」に過剰な反応をする人が総じて好きではありません。当然違いはするのでしょうけど、それは願いの前には些細なことでしょう。

結局壁を作っているのは「認識者側」で、当事者はそんなこと認知すらしていないのです。

その差がこの世界を支えているのかもしれませんけどね。

 

これに限らない話ではありますが、何かに「理解できない」と言う人は、自身の理解能力の低さを反省するべきなのです。だってそういう発言でしょうに。