剛体物理シミュレーションを書いた(衝突応答編)

https://roodni.github.io/rigidbody/ で遊べます

想像以上に安定して動作したので、実装方法について記しておきます。

作るもの

  • 2Dの剛体シミュレーション
  • 衝突・静止摩擦・動摩擦

剛体が持つ情報

  • 質量  m
  • 重心回りの慣性モーメント  I
  • 重心の位置  \vec{x}
  • 重心の速度  \vec{v}
  • 重心の加速度  \vec{a}
  • 角度  \theta
  • 角速度  \omega
  • 角加速度  \alpha

ステップ(時間を進める)

  1. 重力加速度の加算
  2. 数値積分
  3. 衝突判定
  4. 衝突応答
  5. 加速度・角加速度のリセット

1. 重力加速度の加算

剛体の加速度(毎回リセットされて \vec{0} になる)に重力加速度を加えてください。 重力以外の力もあれば同様に加速度や角加速度に加えるのが良いと思います。

速度に直接加算するのは、本記事の枠組みの中ではよろしくないです。

  • 速度に直接加算すると加えた力が剛体の位置に瞬時に反映されます。すると衝突応答(剛体の速度を変更する)では吸収できない振動が生じてしまいます。
  • 加速度は衝突応答の安定化のために利用できます。

2. 数値積分

オイラー法を使いました。一番単純なやつです。

進める時間を\Delta tとして:

 \begin{align}
\vec{x}_{i+1} &= \vec{x}_i + \vec{v}\Delta t \\
\vec{v}_{i+1} &= \vec{v}_i + \vec{a}\Delta t \\
\theta_{i+1} &= \theta_i + \omega\Delta t \\
\omega_{i+1} &= \omega_i + \alpha\Delta t
\end{align}

3. 衝突判定

私の実装では今のところ衝突判定で剛体の速度を考慮していません。位置が重なっているかどうかだけを見ます。 速すぎると貫通しますが……大抵は大丈夫です。ちゃんとした物理エンジンは貫通対策をしているらしいです、すごいですね。

本記事では衝突判定の詳細には触れません。 どうにかして衝突点・法線ベクトル・めり込みの深さを取得してください。 面衝突の場合は衝突点が2個あると安定します。

ちなみに凸多角形同士の衝突判定には次の2つの方法が有力な選択肢です:

  • 分離軸判定
  • GJK-EPA

4. 衝突応答

本記事での衝突応答とは「衝突を剛体の速度に関する拘束と見なして、拘束を満たすように撃力を加えて、剛体の速度を瞬間的に変更する」ことです。

具体例を出しましょう。剛体1と剛体2が反発係数  e で衝突して、衝突前の法線方向の相対速度が  v_{12} だったとします。 このとき、衝突後の法線方向の相対速度は -ev_{12} であってほしいです。それを満たすように撃力を加えます。

もし静止摩擦があれば、衝突後の接線方向の相対速度がゼロになるという拘束も加わります。ただし摩擦力の上限が垂直抗力の大きさに比例することに注意が必要です。他にもいろいろ考慮して拘束条件を組み立てます。

撃力を求めるには複数の衝突をまとめてLCP(線形相補性問題)に定式化して解くのが一般的らしいですが、 本記事では各衝突をそれぞれ単体で解いて撃力を与えるのを繰り返す方法を扱います。

剛体の衝突と撃力

剛体1と剛体2が衝突するときに、剛体1から剛体2に与えられる撃力  \vec{F} を求めます。他の衝突はここでは無視します。

  • 衝突位置  \vec{p}
  • 剛体1から剛体2への衝突法線 (単位ベクトル) \vec{n}
  • 接線(単位ベクトル)\vec{t} = R(\frac{\pi}{2}) \vec{n}Rは回転行列)
  • 反発係数  e
  • 摩擦係数  \mu(簡単のため静止摩擦係数 = 動摩擦係数とする)

その他文字一覧:

\begin{align}
\vec{r}_1 &= \vec{p} - \vec{x}_1 \\
\vec{r}_2 &= \vec{p} - \vec{x}_2 \\
M^{-1}_{n} &= \frac{1}{m_1} + \frac{1}{m_2} + \frac{(\vec{r}_1\times\vec{n})^2}{I_1} + \frac{(\vec{r}_2\times\vec{n})^2}{I_2} \\
M^{-1}_{t} &= \frac{1}{m_1} + \frac{1}{m_2} + \frac{(\vec{r}_1\times\vec{t})^2}{I_1} + \frac{(\vec{r}_2\times\vec{t})^2}{I_2} \\
M^{-1}_{nt} &= \frac{(\vec{r}_1\times\vec{n})(\vec{r_1}\times\vec{t})}{I_1} + \frac{(\vec{r}_2\times\vec{n})(\vec{r_2}\times\vec{t})}{I_2}
\end{align}

\times外積です。2次元での外積スカラーになることに留意してください)

導出は省きますが、位置 \vec{p}での相対速度\vec{\nu}_{12}と、撃力\vec{F}=\lambda_n\vec{n} + \lambda_t\vec{t}を相互に作用させたあとの相対速度\vec{\nu}'_{12}はそれぞれ次のように表せます。

\begin{align}
\vec{\nu}_{12} &= \vec{v}_2 + \omega_2 R\left(\frac{\pi}{2}\right) \vec{r}_2 - \vec{v}_1 - \omega_1 R\left(\frac{\pi}{2}\right) \vec{r}_1 \\
\vec{n}\cdot\vec{\nu}'_{12} &= \vec{n}\cdot\vec{\nu}_{12} + \lambda_n M^{-1}_n + \lambda_t M^{-1}_{nt} \\
\vec{t}\cdot\vec{\nu}'_{12} &= \vec{t}\cdot\vec{\nu}_{12} + \lambda_n M^{-1}_{nt} + \lambda_t M^{-1}_{t}
\end{align}

まずは静止摩擦が働く場合の撃力を求めてみましょう。 法線方向の目標相対速度を u(基本は u=-e\vec{n}\cdot\vec{\nu}_{12} ですが補正が入ります)として、 連立方程式

\left\{\begin{aligned}
\vec{n}\cdot\vec{\nu}'_{12} &= u \\
\vec{t}\cdot\vec{\nu}'_{12} &= 0
\end{aligned}\right.

\lambda_n\lambda_tについて解きます。解は省略しますがゼロ除算は発生しないので安心してください。

さて、解が |\lambda_t| \leq \mu\lambda_nを満たしていればそれで良いのですが、そうでなければ動摩擦に切り替えます。 先ほど求めた\lambda_tの符号を sとして、\lambda_t = s\mu\lambda_n\vec{n}\cdot\vec{\nu}'_{12} = uに代入して解けば、次の解が得られます:

\displaystyle
\lambda_n = \frac{u-\vec{n}\cdot\vec{\nu}_{12}}{M^{-1}_{n} + s\mu M^{-1}_{nt}}

先ほどの連立方程式の解について |\lambda_t| > \mu\lambda_nならば M^{-1}_{n} + s\mu M^{-1}_{nt} > 0が成り立つので安心してください。

以上で撃力\vec{F}=\lambda_n\vec{n} + \lambda_t\vec{t}が求まりました。 ただし法線方向の目標相対速度 uの設定についてもう少し考える必要があります。

法線方向の目標相対速度の設定

ベースは u=-e\vec{n}\cdot\vec{\nu}_{12} ですが、安定化のために補正を入れます。

重力加速度による補正

重力加速度 gで落下する質点が水平面に速さ vで衝突して、反発係数 eで跳ね返ったとします。 計算は省きますが、本記事で説明した数値積分の方法では、再衝突の速さは最大で ev + g\Delta tになります。

再衝突の速さが本来より g\Delta tだけ大きくなる可能性があるわけです。 このままでは、とくに反発係数が大きいときに、相対速度がゼロに収束せず跳ね続けるという事態が発生します。

そこで目標相対速度から g\Delta tに相当する量を引くとうまくいきました。 具体的には uの値を次のとおりに変更します:

\begin{align} u_{g} &= \min(0,\ \vec{n}\cdot \vec{g}\Delta t) \\ u &= \max(0,\ -e\vec{n}\cdot \vec{\nu}_{12} + u_g) \end{align}

 \vec{g}のかわりに剛体の相対加速度を使うのもアリだと思います。

ついでに、衝突前の相対速度が十分小さいとき、この補正によって目標相対速度がゼロになります。 これで床に置かれた剛体が重力による加速で振動することも防げます。

めり込み解消のための補正

めり込みの深さを \mathit{depth}、許容めり込み量を \mathit{slop}として \frac{\mathit{depth}-\mathit{slop}}{\Delta t}

ただし\mathit{depth}が大きいと反発係数以上に反発することになるので、適切に上限を設定した方が良いかもしれません。

反復

各衝突をそれぞれ単体で解くのを繰り返して撃力を伝播させます。

最初に各衝突について法線方向の目標相対速度を計算しておきます。 これをすべて達成することを目指して撃力を計算します。反復の途中で目標相対速度を更新する必要はたぶんないです。

以下を繰り返す:

  • 各衝突について以下の操作を行う:
    1. 前回の反復で与えた撃力を反転して速度に加算する(打ち消す)
    2. 法線方向の相対速度が目標相対速度より大きければ何もしない。撃力はゼロ
    3. そうでなければ、上で述べた計算式で撃力を計算する
    4. 撃力を剛体の速度に加算する

反復を開始する前に前回のタイムステップで求めた撃力を加えること (Warm start) で撃力計算が安定します。 時刻をまたいだ衝突点の同定がやや面倒ですが、本当に目に見えて安定するのでおすすめです。

おわり

ここまで実装すれば動きます。

本記事では私が実装で迷った部分にできるだけ言及したつもりです。 実のところ、数値積分の方法からして正しいかどうか自信がないのですが、それっぽく動いたので大丈夫ということにして記事を書きました。 物理エンジン自作に興味があるがよくわからない、という方の参考になれば幸いです。

扁桃腺摘出術を受けた

口蓋扁桃の摘出手術を受けてきました。

私がどの程度苦しんだかを記しておきます。これから手術を受けられる方の参考になれば幸いです。

検索でこんなブログにまで辿りついた方は既にお察しのことと思いますが、術後の経過にはずいぶん個人差があるようです。本記事の内容も過信しないのが賢明でしょう。

なぜ手術したのか

腎臓の病気です。腎臓の病気で喉の手術をするなんて不思議ですね。そういうこともあります。

私は扁桃腺を腫らしたことはありませんが、そんなことは関係なく取った方が良いとお医者さんが言ってました。実際、摘出した扁桃腺には炎症の所見があったそうです。自覚症状が全くなかったので意外でした。

手術は効いたか

微妙なところです。術後2ヶ月くらい経ちますが、何とも言えません。

術前は、血尿と蛋白尿が出まくっていましたが……

  • 潜血は術後にずいぶん減りました。まだありますが。
  • 蛋白はあまり変わりません。すこしずつ減っているような感じもします。
  • 腎機能は良くなったようです。eGFR*1が78から90に上がりました。

私はパルス療法の点滴3回が先で扁摘が後だったと付け加えておきます。

どれくらい苦しんだか

手術日(0日目)

全身麻酔なので手術自体は怖くないですね。問題はその後です。

  • 喉の痛み
    • 唾を飲まなくても十分痛いです。
    • 唾は飲めません。唾(と血)を出す用のバケツに出します。痛くて飲めないという次元ではなく、唾を飲むという動き自体がうまくできません。
    • 喉の奥で鼻水がからまっている感覚がして最悪でした。出そうとしても出ません。たぶん出す必要もないのだと思います。
  • 発熱
    • 37.5℃くらいまで熱が上がりました。まもなく下がりました。
  • 吐き気
    • 手術室からの帰り道と安静解除後の歩行で吐き気を感じました。
    • 吐き気のせいで夕食を摂れませんでした。
    • 寝ていれば耐えられましたが、トイレの帰りに隙を突かれて吐きました。
    • 原因は麻酔ではなく、術中に血を飲み込んだことだったみたいです (私は吸血鬼じゃないんだなと実感しました)。吐いた後は楽でした。
    • 吐き気に耐えて夕方に寝ていたので、夜間は寝られませんでした。

あと鼻呼吸が全くできなくなりました。この症状は「全くできない」→「仰向けのときできない」→「できる」という経過で数週間で改善しました。

術後1日目

日中はそれなりに余裕がありました。

  • 食事はお粥です。味はおいしいですが、痛くて食べるのに難儀します。昼食から完食できるようになりました。
  • 鎮痛剤の効果を感じました。薬が効いている間は唾さえ飲まなければ痛くないです。切れるとどうにもなりません。
  • 唾を飲むべきかどうか先生に聞いたら「どっちでもいい」とのことです。そこで私は唾を全く飲まずにバケツに出し続けることにしました。
  • 手術による口角炎が地味に痛いです。

消灯後。地獄を見ました。

寝つくのは簡単ではなく、覚悟を決める必要があります。それは唾を飲む覚悟と喉を乾燥させる覚悟です。

  • 寝るための姿勢は仰向けしかありませんでした。横向きでは涎が枕にたれて覚醒してしまうからです。
  • 仰向け姿勢では120秒に1回くらい唾を飲んでダメージを受けます。
  • 病室はエアコンで乾燥していて、しかも口呼吸なので、目が覚めたときには喉がとんでもなく痛くなっています。

そうやってダメージを食らいながら眠るわけですが、この日は1時間に1回目が覚めました。私は覚悟を8回決める必要がありました。

こんなのを毎日やったら気が狂ってしまいます。翌日以降は、涎を垂れ流しながら眠る方法を身に付けたので、これほど苦しみませんでしたが……

最も妥当な対処は睡眠薬を処方してもらうことだと思います。私はそうしませんでした。なんで貰わなかったのでしょう。

術後2日目

  • 術後はじめてお通じがありました。安心です。
  • 唾を飲むと耳が通るようになりました。痛いですが。
  • のどちんこに妙な塊が付着していて違和感がありました。その正体はかさぶたです、取ってはいけません。
  • やはり睡眠が不十分です。この睡眠不足は退院まで続いて私は弱っていきました。皆さんは睡眠薬を処方してもらいましょう。

術後3〜4日目

  • 喉の痛みはある程度改善しました。鎮痛剤が効いていれば唾を飲んでも痛くない、という程度です。
  • 舌の両側のつけ根にかさぶたができていて、食事のときは喉よりこっちの方が痛かったです。
  • 2時間に1回のペースで尿が出て、私は腎機能低下を心配していましたが、お粥ばっかり食べていたのが要因だと思います。

術後5日目(退院日)

  • 口角炎はわりと治ってきています。
  • 朝食にお粥ではなくご飯が出ました。舌の両側がたいへん痛かったですが無理矢理食べました。
  • 睡眠不足もあってかなり体力が弱っており、帰宅するだけで疲れて動けなくなりました。

術後6日目

  • 家では夜間エアコンを消せるので乾燥はずいぶんマシです。
    • 寝るときにやたら涎が出ます。
  • 朝はまだ鎮痛剤を使うくらいに痛いです。
  • ときどき咳を出したくなります。我慢できる範囲内です。
    • 喉風邪の終わりに咳がめっちゃ出るのを想像していましたが、私の場合は咳はほとんど出ませんでした。

術後7日目

  • お粥ではなくご飯を食べられる程度に舌の痛みが改善しました。
  • 体力も元に戻りました。

…………

術後10日目

  • このあたりから鎮痛剤を飲まなくなりました。
  • くしゃみはだいぶ痛いです。
  • あくびも少し痛いです。

術後17日目

通院しました。

  • かさぶたは残っているそうです。
  • 湯船につかる許可が出ました。
  • 食べ物だけ気をつければ良いみたいです。

術後20日

  • くしゃみをしても平気でした!
  • 喉の違和感はまだあります。

術後31日目

通院して、無事かさぶたが取れたということで診察終了になりました。

雑記

  • 手術の数日〜1週間後くらいに痛さが増す時期がある、という噂を聞いていましたが、私には該当しませんでした。とくに喉の痛みは日ごとに改善しました。
  • とはいえ鎮痛剤の効果もあり「前日の一番楽なときの痛さ」<「当日のつらいときの痛さ」なのでわかりづらいです。
  • 処方されたうがい薬を最初ガラガラうがいで使っていたら、後でブクブクうがいが正しかったと判明しました。要確認です。

おわり

*1:eGFRは腎臓の残りHPみたいな数値です。0から100くらいの範囲をとります。私は一番悪いときで60でした

AWS Lambda Function URLsをSPAから直に叩く

AWS LambdaにはFunction URLsという機能がある。提供されたのはわりと最近 (2022年4月) のことだ。

AWS Lambda Function URLs の提供開始: 単一機能のマイクロサービス向けの組み込み HTTPS エンドポイント | Amazon Web Services ブログ

Function URLsを使うとAPI Gatewayを噛ますことなくLambda関数にHTTPSエンドポイントを用意することができる。API Gatewayが有料なのに対してFunction URLsは無料だ!いやLambdaの使用分の課金があるが、リクエスト毎月100万件まで無期限無料なので問題なかろう。

そういうわけで、Github Pages上のSPAからFunction URLsを直に叩く構成なら無期限無料で簡単なWebアプリをデプロイできそうだと思い立った私は、実際なんか作ってデプロイしてみた……

どうだったのか

作ったのは brainf*ckを吐く自作言語のPlayground だ。自作言語のコンパイラ (OCamlで書いた) の実行ファイルを載せたコンテナイメージ利用のLambda Functionで 自作言語 → brainf*ck の変換をして結果を返す。

で、まあ問題なく動いたのだが、いくつか微妙な点があってデプロイ後にAPI Gatewayを使う構成に変えてしまった。「微妙な点」とは具体的に次の2つだ:

  • ローカルでの動作確認が難しい。
  • ロットリング機能がない。
    • AWS Lambdaはかなりスケールするやつなので、DoS攻撃とかで大量のリクエストが来たとき全部捌いてしまいその分の料金を支払うことになる。
    • リクエスト100万件につき0.2ドルなので大した金額にはならないし、そもそも攻撃するメリットが無いのであまり気にせんで良いのかもしれない。
    • 例えば私が誰かの恨みを買ったとして、そいつが私のURLに向けてリクエストを1億件 (←秒間1000件でも24時間かかるな) 送りつけて、そのとき仮に円安がヤバくて1ドル=360円とすると、私はAWS7200円くらい支払うことになる。……微妙だ。

さきほどAPI Gatewayが有料と述べたが、1ドル=360円のレートでも課金は2800件につき1円程度だ。月1000件もリクエストは来ないと思うし、100円くらいなら払っても構わないし、冷静に考えてみるとこの用途では料金の観点でもFunction URLsを使う必要はなさそうだった。

ついでに言うと、コンテナ利用のLambda Functionを利用するにはECRにイメージをpushする必要があり、プライベートリポジトリはデータ量で課金される。私の場合 (12ヶ月の無料期間のあとに) 月額1円くらい取られるかも。無期限無料ではなかったな。

以下、Function URLsのデプロイまでの流れとかハマった点とかを記しておく。

記録

AWS SAM CLIというのを使うとサーバーレスアプリケーションのプロジェクト作成・ビルド・ローカル動作確認・デプロイが全部できるらしい。

チュートリアル: Hello World アプリケーションのデプロイ - AWS Serverless Application Model ←このページを参考にプロジェクトを作成する。

API Gatewayを消してFunction URLsを使う

template.yamlapp.jsを書きかえればFunction URLsでAPIをデプロイできるわけだな。

Hello Worldテンプレートのtemplate.yamlには最初からAPI Gatewayを使用する旨の設定が書いてあるわけだが、これを消す。

      Events:
        HelloWorld:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            Path: /hello
            Method: get

↑消すやつ。これの他にOutputs:以下にもAPI Gatewayに関する項目があって、そいつも消す。

さてAPI GatewayのかわりにFunction URLsの設定を書く。さっき消したEvents:と同じ場所にFunctionUrlConfigを書けば良いらしい。今回必要なのはAuthTypeCorsだ。

      FunctionUrlConfig:
        AuthType: NONE
        Cors:
          AllowOrigins:
            - http://localhost:3000
            - https://roodni.github.io
          AllowHeaders:
            - Content-Type

ブラウザから呼べねえ

よし動作確認するぞ!ローカルにエンドポイントを生やすコマンドはsam local start-lambdasam local start-apiの二通りがあるんだが、start-lambdaは (たぶん) ブラウザから叩くのに適さない。ここで使うのはstart-apiの方だ。

$ sam local start-api -p 3001
Error: Template does not have any APIs connected to Lambda functions

あれ?もしかしてFunction URLsってSAM CLIで動作確認できない??いや設定書けるのにテストできないってのは変だろ。やり方を調べるぞ……

API Gatewayで動作確認する

もういい。Lambda 関数 URL の呼び出し - AWS Lambda ←このページによればリクエストイベントの形式はFunction URLsとAPI Gatewayでだいたい同じらしいので、ローカルで試すときはAPI Gatewayを使ってやる。

CORSの設定のためResources:以下に AWS::Serverless::HttpApi - AWS Serverless Application Model を書く。

  BfreCompileApi:
    Type: AWS::Serverless::HttpApi
    Properties:
      CorsConfiguration:
        AllowOrigins:
          - http://localhost:3000
        AllowHeaders:
          - Content-Type

BfreCompileApiというのは私が適当につけた名前なので気にしないでほしい。

さっき消したEvents:をいろいろ書き換えて復活させる。

      Events:
        BfreCompileEvent:
          Type: HttpApi
          Properties:
            ApiId: !Ref BfreCompileApi
            Path: /codegen
            Method: POST

TypeApiからHttpApiに変えたほかApiIdという項目を追加した。

これでプリフライトリクエストが通るようになる。さらにLambda Functionのハンドラをいじって返値のheadersプロパティにAccess-Control-Allow-Originを付与してやることで、ようやくsam local start-apiのエンドポイントをブラウザから叩けるようになる。

デプロイ用に修正が要る

sam deploy --guidedでアプリケーションをデプロイできるが、API Gatewayはあくまで動作確認用なのでデプロイ前に関連する設定をyamlからコメントアウトしておく。←アホ

デプロイしたらCORSエラーが出た。ヘッダが重複……?うわっハンドラで手動付与したCORSヘッダとFunction URLsの設定のCORSヘッダが二重になって返されてやがる!! もう知らん。デプロイ前にCORSヘッダの手動付与する部分をコメントアウトして対処する。←アホ

ちなみにAPI Gatewayでデプロイする場合、手動付与のCORSヘッダが存在してもAPI Gatewayの設定を優先して塗り潰してくれるので問題がないのだが、ならsam local start-apiでもそうしてくれたら最初から手動付与しなくても良くなって万事解決だよな。挙動が謎だ。

最後にリクエストボディがBase64エンコードされる場合があることに引っ掛かったが、これはイベントオブジェクトのisBase64Encodedを見れば対応できるので大した問題ではない。

動いた

わぁい

はてなインターン2022に参加しました

はてなリモートインターンシップ2022 の参加記です。

選考

学部生のころから、たまに目に入る参加記を見てはてなインターンには興味を持っていました。

そろそろインターンへの参加を本格的に考えなければと焦りはじめた修士1年の5月の頭のことです。はてなインターンの募集要項を調べてみたら、なんと既に第一次選考が始まっているではありませんか。私は慌てて準備を始めました。

エントリーには勇気が要りました。インターンでの「使用技術・あると望ましいスキル」が技術スタック - エンジニア採用 - 採用情報のページに挙げられているのですが、私はそれらのスキルの多くを持っていませんでした。

はてなブログの技術スタックを見てみましょう:

言語/フレームワークに注目すると、TypeScriptだけは……JavaScriptならよく書くから……何とかなりそうですが、PerlとGoは全く書いたことがありませんでした。Scalaは大学の授業でほんの少し触った程度です。GraphQLも使ったことがなくて、関連技術であるApolloはその存在すら知りません。

ミドルウェアクラウドサービス等はもう壊滅で、MySQLはともかくAWSに触れたことはないし、RedisやEnvoyやLet's Encryptとは何なのか見当もつきませんでした。

このようにスキルには心許なさを感じていましたが、ここでエントリーできなければ私は一生インターンに行けないだろうと気を強く持ち、今までの活動をかき集めてポートフォリオを作りエントリーしたのでした。

 

面談は私のGitHubリポジトリやこのブログの記事をもとに話が進みました。自分の作ったものや書いたものにフィードバックを貰える機会はあまりないものなので、面談ではたくさんのフィードバックを頂けて有難かったです。

 

講義

講義パートは座学と課題にわかれていて、2.5日間でWebサービスの開発・運用の話題を座学で叩き込まれたあとに1.5日間で課題に挑みます。

インフラやマイクロサービスなど一人では手を出しにくい分野の話を聞くことができて非常に勉強になりました。エントリー時に眺めては唸っていた技術スタックのページに書かれている話題がしばしば出てきて、面接前に付け焼き刃でGoogle検索したときより理解が深まった、などと思いながら聞いていました。

課題ではkubernetesでマイクロサービス化された簡易ブログサービスの雛形に機能を追加していきます。丁寧にテストを書いたりコードレビューに対応したりするのが私にとって新鮮でした。なお1.5日という期間は相当に短く、いきなり締切直前の状況に放り込まれたような感覚になります。刺激的です。

 

実践

講義パート後に id:smallkirby さんとブログチームに配属されました。ブログというのはもちろん私が今こうして書いているはてなブログのことです。すごいですね。

今回私たちが行った業務はスマートフォン版(≠アプリ版)の編集画面の強化です。プロデューサーやデザイナーの方との打ち合わせから始まり、次の2つの機能をリリースすることができました。

図付きで説明しましょう。実践パートが始まる前、2週間前のスマホ版の編集画面はこんな感じでした↓

それが今はこうなっています↓

要するにPC版と比較して非力だったスマホ版の編集画面がだいぶPC版に追いついたということです。今では太字とか使い放題で嬉しいので、外出先なんかで機会があればぜひ使ってみてください。

 

業務中はメンターの id:Furutsuki さんと id:nanto_vi さんが付きっきりで面倒を見てくださって大変助かりました。深く深く感謝しております。また、たくさんのコードレビューや品質保証のフィードバックを頂けて勉強になりました。

 

そういえば、エントリー前に技術スタックのページを見て震え上がっていた件について補足しておくと、私たちは今回の業務ではほぼずっとTypeScriptを書いていました。他には一部端末(Androidとか3DSとか)で見たままモードでの新規投稿ができなかった仕様を解消するためにPerlを少しだけ触ったくらいでしょうか。

はてなブログは大きなサービスなのでインターン生が全ての技術に同時に触れることはないわけです。同じチームでも時期によってタスクは違って、それによって使う技術も変わるから、たぶんPerlかGoかScalaをたくさん書いてフロントエンドにはあまり触れない年もあるのだろうと思います。

 

終了

3週間の日程が終わりました。たくさんのフィードバックをもらえる環境で大きなプロダクトに触れるという、貴重な経験をすることができました。

お世話になったメンターのお二人、ブログチームの方々、インターン関連のスタッフの方々に感謝を申し上げます。ありがとうございました。

 

展示

本番環境での動作確認の結果マリオのパチモンに制圧された id:rood_niはてなフォトライフの図↓

ニコニコ動画のコメントを正規表現でNGするユーザースクリプト

ニコ動は楽しいコメントが流れてくるのが良いところだが、 不快なコメントも流れてくるのが悪いところである。 楽しもうとして不快になるのは本末転倒だ。 これを回避するためにNG機能というものがあって、 登録したワードやアカウントに該当するコメントを非表示にすることができる。

しかしこのNG機能、クライアントサイドで処理するくせに、 何故か登録数の上限が40件しかないので使っているとすぐ上限に達してしまう。 それから、登録したワードが含まれているコメントは全て非表示になるので、 例えば「←○○○○○」みたいな形のコメント(コメントでバトルする人がよく使う)をNGしようとすると左端以外の場所に「←」がある無実のコメントも消えてしまう。

そこでコメントを正規表現でNGするスクリプトを書いた。 正規表現なので|で区切ればいくらでも追加できるし、 さっきの例はパターン^←.+を使えば冤罪がすこし減る。

雑に作ったので使い勝手に少々難があるのだが、 スクリプトの解説も書いておくので適当に改良して使ってもらえると嬉しい。

説明

ニコニコ動画のコメントを正規表現でNGするスクリプト · GitHub

↑コードをGistに載せた。Tampermonkeyとかに登録すれば使える。 Google Chromeでしか動作確認をしていないが、変な機能は使ってないのでFirefoxでも動くと思う。

動画再生ページでEscキーを押すとNGパターン設定のプロンプトが開く。 この設定はLocal Storageに保存されるので永続する。

NGパターン設定

なお、このスクリプトが非表示にするのは動画に流れるコメントだけで、 動画の右側に出るコメントリストは非表示にしない。

そういえば、スクリプトではなく素のNGに引っ掛かって「字幕が視聴者の総意」というコメントが消えてしまっているが、これは楽しいコメントであって冤罪だな。まあ正規表現であっても文脈を読むことはできない……

解説

ニコ動のコメントは次の手順で描画される。

  1. document.createElementCanvasを生成する。
  2. 生成されたCanvasにコメントをひとつ描画する。
  3. drawImageでコメント表示用のCanvas (.CommentRenderer > canvas) にコピーする。

これを踏まえて以下のようなスクリプトを実行することで正規表現によるNGを実現した。

まずdocument.createElementを書きかえて、 Canvasが生成された場合にfillTextを監視するようにする。 改造されたfillTextCanvasと文字列の対応をWeakMapに保存する。

// createElementで生成されたcanvasのfillTextを監視
const canvas_to_text = new WeakMap();
const createElement = document.createElement;
document.createElement = function(tagName, ...args) {
    const elem = createElement.call(this, tagName, ...args);
    if (tagName === 'canvas') {
        const ctx = elem.getContext('2d');
        const fillText = ctx.fillText;
        ctx.fillText = function(text, ...args) {
            canvas_to_text.set(elem, text);
            return fillText.call(this, text, ...args);
        };
    }
    return elem;
};

つぎにコメント表示用のCanvasdrawImageを書きかえる。 改造されたdrawImageは、第一引数がCanvasであるとき、 対応する文字列をWeakMapから取り出す。 これがNGのパターンとマッチする場合に書き込みをキャンセルすることで、 正規表現によるNG機能が実現する。

// コメント表示用canvasのdrawImageを監視
let image_to_is_safe = new WeakMap();
const commentCanvas = document.querySelector('.CommentRenderer > canvas');
const commentCtx = commentCanvas.getContext('2d');
const drawImage = commentCtx.drawImage;
commentCtx.drawImage = function(image, ...args) {
    let is_safe = image_to_is_safe.get(image);
    if (is_safe === undefined) {
        const text = canvas_to_text.get(image);
        if (text === undefined) {
            is_safe = true;
        } else {
            const m = text.match(regexp);
            if (m) {
                console.log('NG', m[0]);
                is_safe = false;
            } else {
                is_safe = true;
            }
        }
        image_to_is_safe.set(image, is_safe);
    }
    if (is_safe) {
        drawImage.call(this, image, ...args);
    }
};

確認済みのCanvasimage_to_is_safeというWeakMapに格納することで同じ文字列に何度も同じ正規表現のマッチが行われることを避けている (パフォーマンスにどれくらい影響するのかよくわからないが)。 NGパターンが変更された際にはimage_to_is_safeがリセットされるようになっていて、新しいNGパターンが即座に反映される。

ところで

実はニコ動のLocal Storageには公式NG機能の設定がJSONで保存されている。

Local Storageに保存された公式NG機能の設定

これを適当に編集してページをリロードすると……驚くべきことに上限を突破してNG設定を登録できてしまう。しかもちゃんと動く。

上限突破

わざわざCanvasをいじるコードを書くよりLocal Storageの書きかえを支援するコードを書いた方が良かったかもしれない。 正直なところ正規表現が使えてもそんなに嬉しくないからなぁ。

一応デメリットもある。

  • 設定の反映にリロードが要るので、NG設定の追加が即座に反映されない。
  • NG設定が上限を超えているとき、NGの追加ボタンや削除ボタンが機能しなくなる。
  • 上限を超えたNG設定はサーバーに反映されない。

チャーレムでバトルステージ170連勝した

私の好きなポケモンチャーレムです。

好きなポケモンで金ケイトを倒したいですよね。

倒しました。

f:id:rood_ni:20210304171010j:plainf:id:rood_ni:20210304171029j:plain
170連勝

チャーレムの魅力

チャーレムは特性ヨガパワーのおかげで常に攻撃のステータスが2倍です。

チャーレム種族値
 H60 A60 B75 C60 D75 S80 (合計410)
ですが、Lv.50でA個体値が最高かつA努力値に252振った場合の実質種族値
 H60 A172 B75 C60 D75 S80 (合計 522)
になります。他の第4世代までのポケモンと比べてみましょう。

圧倒的ですね!ロマンがあります。私の内なる小学生心がチャーレムを使えと叫ぶのです。

パーティ紹介

f:id:rood_ni:20210304170940j:plain

バトルステージは1種類のポケモンだけで戦う施設ですが、使用する個体は周回ごとに変えることができます。

ダメージ計算するスクリプトを書いていろいろ考察した結果、次の4個体で挑むと勝てそうだという結論に至りました。

努力値はAとSに全振りです。個体値もAとSだけ最高にして他は気にしていません。

サイコカッター型

Lv.50 いじっぱり(A↑C↓) サイコカッター/ねこだまし/けたぐり

空いた技スペースには相手タイプに応じて三色パンチを入れ替えて戦います。 大抵のタイプはこの型でなんとかなります。

いわなだれ

Lv.50 いじっぱり(A↑C↓) いわなだれ/ねこだまし

飛行と虫を担当します。いわなだれメガヤンマアーマルドを突破するために必要です。

なげつける型

Lv.37 ようき(S↑C↓) なげつける/ねこだまし/けたぐり/ほのおのパンチ

エスパーとゴーストにプレートを投げつけて倒します。 けたぐりはメタグロス対策です。ほのおのパンチはユキメノコ(ひかりのこな所持)を意識していますが、他の粉所持ポケモン対策の技にしても良いかもしれません。

みやぶる型

Lv.50 ようき(S↑C↓) みやぶる/ねこだまし/けたぐり/(ヤミカラスへの有効打を何か)

悪と戦います。ミカルゲヤミラミはみやぶる+けたぐりで成敗します。

攻略の順番  

挑戦タイプ数が少ないうちは敵のレベルが低いので、同レベルでは勝てないポケモンに強引に勝つことができます。

周回 タイプ 使用個体と技 もちもの 備考
1 エスパー なげつけ 格闘プレート 格闘プレートはメタグロス
2 みず サイコ(雷) タスキ サメハダー(タスキ)を抜く
3 じめん サイコ(冷) タスキ みず+じめんが怖い
4 ゴースト なげつけ 悪プレート RANK9,10のゲンガーに勝てないのでケイトで回避
5 くさ サイコ(冷) タスキ リーフィアを抜く
6 ノーマル サイコ(冷) タスキ
7 でんき サイコ(何でも) タスキ ロトムを高乱1にする
8 はがね サイコ(炎) いのちのたま メタグロスをギリギリ倒せる
9 ひこう 岩(冷, 雷) タスキ クロバットを確1にする
10 ほのお サイコ(炎) タスキ 威嚇ウインディは無理かも
11 むし 岩(冷, 炎) タスキ
12 こおり サイコ(炎) タスキ
13 どく サイコ(何でも) タスキ
14 かくとう サイコ(まもる) タスキ まもるはゴウカザル猫騙し対策
15 あく みやぶる タスキ サメハダー(タスキ)を抜く
16 いわ サイコ(雷) タスキ 雷パンチはジーランスプテラ対策
17 ドラゴン サイコ(冷) タスキ

三色パンチの入れ替えのためのハートのウロコ消費が痛いので、サイコカッター型は何匹か育てた方が良かったかもしれません。

チャーレムの魅力2 

プラチナでは進化前のアサナンを序盤(自転車入手直前)から仲間にできるので、長い間一緒に旅ができて愛着が湧きやすいです。

Lv.29ではっけいを覚えるまでまともな物理技を覚えないのが序盤にはつらそうに見えますが、 自転車入手後すぐにかわらわりわざマシンが手に入るのでさほど問題ありません。

中盤になると教え技でしねんのずつきを習得できるほか、チャーレムに進化すると三色パンチを思い出すことができて、技範囲がぐっと広がります。 シロナに挑むころには序盤の技不足時代からは考えられないほど頼もしいポケモンに育っていることでしょう。

戦果

3回目の挑戦で170連勝を達成しました。わりと運が良かったと思います。

1回目

30戦目 対トリトドン(のんきのおこう)

最初のターンにサイコカッターを選択した結果、だくりゅうで命中ダウンを引いて次ターンにサイコカッターを外して負けました。 猫騙し+サイコカッターでは威力不足だと思っていたのですが、レベル差や相手の個体値を考慮すると猫騙しの方が良かったかもしれません。

2回目

67戦目 対ライボルト(きあいのタスキ)

静電気による麻痺を嫌って最初のターンにサイコカッターを選択した結果、かみなりで麻痺して素早さが逆転して負けました。 猫騙しから入るべきでした。

3回目

ケイトの使用ポケモンはゲンガー(するどいキバ)で、シャドーボールをタスキで耐えてサイコカッターで倒しました。 キバで怯んだ場合負けていました(10%)。

チャーレムの魅力3

チャーレムは見た目がかわいいですね。髪飾りとか首筋とか膝とかすきです。

アサナンも良いと思います。オスとメスで耳の角度が違うんですが、 男の子は凛々しい感じに、女の子はおしとやかな感じになってどちらもかわいいです。

TOEIC465点を710点まで上げた

とある大学では卒業のためにTOEIC L&Rで600点以上取る必要があって絶望していたのですが、意外なことに達成できてしまったので調子に乗ってブログを書きます。

f:id:rood_ni:20201019135057p:plain

はじめに今までの私の英語試験の成績がどれくらいだったかを述べておきましょう。スタートラインによって対策の方法は変わってくるはずです。

やったこと

1. 大学入試対策(3年前・半年間くらい)

大学に進学するためにはセンター試験と二次試験の英語である程度点を取る必要があるので、高3の夏から英語の勉強をしていました。これで英語の基礎力が大幅に上昇したと思います。ちなみに重視したのは長文読解で、リスニング、発音、アクセント、語法の対策は自主的には一切行っていません。

 2. TOEIC L&R対策(半年前・1ヶ月間くらい)

大学入試のあとは一切英語の勉強をしていませんでしたが、春休みにさすがにヤバいだろうと思って適当なTOEIC対策本を購入して対策を始めました。しかしやる気がほとんどないので勉強時間は1日15分〜30分くらいです。対策本にはTOEIC1回分相当の問題が載っていましたが、1ヶ月かけてなんとか1周しました。

2回以上遭遇した単語は単語帳カードに書いて覚えるようにしていました。それ以外の対策はテキトーです。

さて、5月のTOEICを受ける予定だったのですが、感染症の大流行により中止になってしまったので対策の成果が発揮されることはありませんでした。これでモチベーションが完全に消失して、春休みが終わった後はTOEIC対策を一切行っていません。

3. TOEIC L&R対策(直前・3日間)

TOEICが再開したので、夏休み開けのタイミングに合わせて申し込んだところ抽選に通りました。私はその手の運には恵まれている方です。

本当は夏休みにTOEIC対策をする予定だったのですが、いろいろあって夏休み中には一切手を付けませんでした。対策を始めたのは試験の3日前です。

まず春に作成した単語カードを引っぱり出してきて単語を覚え直しました。ほぼ全て忘れていたのですが、一度覚えたものを再度覚えるのは案外簡単で、すぐに覚え直すことができました。

つぎに試験前日に問題を通しで時間を計りながら解きました。これで問題の形式とか時間配分とかを思い出すことができます。

役に立ったと思うこと

よく出る単語を覚える

TOEICはビジネス系のやりとりがメインなので、それに関連して露骨に良く出る単語があります。supervisorとかfurnitureとかpress conferenceとかです。この手の単語がわかるだけで解ける問題が一気に増えるのでコスパが非常によろしいです。そういうわけで、2回以上見た単語だけ覚えるようにするのは効率的な覚え方だったと思います。

リスニング問題の解き方を知る

TOEICのリスニングはセンター試験と違って1回しか読まないので、解き方が異なってきます。

セクション2の応答問題については、とにかく最初の疑問詞を聞き取るのが大切です。Whatなら物、Whereなら場所、Whenなら時間、Whoなら人、Howなら手段ですね。これさえ聞き取ればなんとかなります。それ以上は難しいので聞き取れません、諦めてください。

セクション3と4に関しては、本文の読み上げが始まるより前にテキストに書かれた問題文を読んでおく必要があります。これは絶対です。さもなくば解けません。この解き方を知らずに受験すると3と4は壊滅してしまいますが、知ってさえいればまともな点が取れることでしょう。

リスニングに共通する事項として、解答に迷ったらその問題は捨てて次に移った方が良いということが言えると思います。

リーディングで絶望しない

TOEICのリーディングは問題の量がやたら多いのですが、ここで私にとって本当に重要にだったのは速読力などではなくて、絶望しないことでした。問題の多さに絶望すると解ける問題も解けなくなってしまいます。多少は時間オーバーになるのを認めて、焦る気持ちを抑えて普通に読みましょう。問題集の問題を時間を計って解いて、自分の読む速度がどれくらいか把握しておくのも大切だと思います。

語彙・文法問題に関しては知りません。適当に解いてください。

やらなかったこと

単語帳・単語アプリ

これは個人的な好みの問題ですが、私は単語や語法だけが並んでいるタイプの参考書が非常に嫌いです。高得点を狙うのでもなければ、2回以上見た単語だけ覚えるので問題ないと思っています。

英語サイトを読む

GitHubとかStack Overflowとかから情報を得るには英語を読む必要がありますが、大抵面倒なのでブラウザの翻訳機能にぶちこんでいました。私はプログラミングの調べ物をしているときに英語学習なんて考えてる余裕はないと思います。