ヘッドレス Chrome で 3D を撮る罠

WebGL の画面を自動で撮って確かめる仕組みが、何度も間違った結論を出しかけた。毎秒3フレーム、4秒かかるタップ、いつも左上を撃つイベントなど。

#テスト#Puppeteer#WebGL

このサイトは、AI の Claude Code と対話しながら作っています。3D の見た目を変えたら、その場で画面を撮って確かめる。この確認を、画面を持たないブラウザ(ヘッドレス Chrome)で自動化しています。

これがないと、AI は自分の書いたコードがどう見えているのかを知る手段がありません。一方で、この確認の仕組みそのものが、何度も間違った結論を出しかけました。この回は、その罠の記録です。同じように WebGL(ブラウザの 3D 描画)の画面を自動で確かめたい人の役に立てばうれしいです。

普通に撮ると、真っ白

まず、一番素朴な撮り方では、何も撮れません。curl でページを取ってきても、Chrome に「このページを撮って」と1行で頼んでも、写るのは白い画面です。3D の描画が始まる前に撮影が終わってしまうからです。

「仮想の時間を進めてから撮る」という Chrome の機能もあるのですが、これを使うと永遠に終わりません。3D の描画ループが次の描画を予約し続けるので、仮想の時間がいつまでも「今やることがある」状態になるのです。

結局、Puppeteer で Chrome を操作し、ページを開いてから7秒ほど待って撮る、という方法に落ち着きました。ヘッドレスには GPU が無いので、WebGL を CPU で描く SwiftShader というソフトウェアの描画を有効にして起動します。

毎秒3フレームの世界

SwiftShader は、GPU の仕事を CPU で真似しているので、とても遅いです。このサイトの惑星のシーンは、毎秒3フレームくらいしか出ません。3秒間に4フレーム、フレームの間隔は333ミリ秒から1.2秒まで揺れます。

このせいで、時間で進むものは、現実の何倍もの時間がかかります。

  • カメラが1.1秒で飛ぶ動きは、1フレームに進める量に上限があるので、最低でも11フレームかかります。毎秒3フレームなら、8秒待っても着地しきっていないことがありました。着地する前に撮ると、「距離を変えたはずなのに、ほとんど動いていない」という間違った結論になります
  • 0.14秒で消えるはずのメニューが、1秒以上残っていました。「閉じるボタンが効かない」と誤診しかけました

だから、1回の待ち時間を勘で決めず、数秒おきに何枚か撮って、値が落ち着いたかを見ることにしています。そして、「動きが滑らかか」は、ヘッドレスでは測れないと割り切りました。動きの質はコードの計算とテストで固めて、体感は実機で確かめます。

タップに4秒かかる

スマホ版を確かめるときにも、罠がありました。Puppeteer の「タップする」命令は、指を置いてから離すまでに、実測で4,083ミリ秒かかりました。Chrome と Puppeteer の通信の往復が遅いせいです。

このサイトでは、「350ミリ秒以内に離したらタップ」と判定しています。4秒かかるタップは、何度やってもタップになりません。これで一度、「タップが効かない」という間違った結論を出しかけました。

指を置く・離すのイベントを、ページの中で自分で続けて投げると、本物の指と同じくらいの間隔で届きます。

いつも左上を撃つ

もっと気づきにくい罠もありました。ページの中で自分で作った「指のイベント」は、3D のシーンの中の物(建物の上の目印など)に当たりません。いつも画面の左上を撃ってしまうのです。

React Three Fiber(React で Three.js を使うライブラリ)は、指の位置を「要素の中での座標」から読みます。ところが、イベントを自分で作るときに指定できるのは「画面全体での座標」だけで、要素の中での座標は0のままになります。だから、どこを狙っても、座標 (0, 0) を撃つことになる。

これで一度、「目印をタップすると、ジオラマの停止まで一緒に起きる」という、存在しない不具合を報告しかけました。実際には、目印ではなく、たまたま左上にあった何もない空をタップしていたのです。今は、作ったイベントに正しい座標を後から足してから投げています。

別のタブを開くと、3D が止まる

撮った画像を解析するために、同じブラウザで別のタブを開いたことがあります。すると、3D の画面が一切描かれなくなりました。撮れるのは、3D 以外の真っ白な部分だけです。

原因は、元のタブが裏に回ったことでした。裏に回ったタブでは、ブラウザが描画のループを止めます。解析用のタブは先に開いておき、3D のタブはいつも表に出しておくようにしました。

同じ種類の罠がもう1つあります。3D の画面を、ページの中から画像として読み出そうとしても、中身は空っぽで返ってきます。WebGL は、描き終わった画面を合成したあとで、描画用の領域を捨ててしまう設定になっているからです。画素を読むときは、必ず撮影した画像のほうから読むことにしています。

計測が壊れていても、結果は「0」で返ってくる

振り返ると、ここに書いた罠には共通点があります。どれも、計測が壊れているのに、エラーにならず、それらしい結果が返ってくるのです。

  • 白い画面は、「3D に何も描かれていない」ように見える
  • 着地前のカメラは、「設定が効いていない」ように見える
  • 4秒のタップは、「タップが効かない」ように見える
  • 左上を撃つイベントは、「存在しない不具合」を作る
  • 裏に回ったタブは、「3D が消えた」ように見える

どれも、アプリの不具合に見えます。でも、壊れていたのは測る側でした。

そこで、測った結果を信じる前に、何もしない「対照」を1枚撮ることにしています。たとえば、ジェスチャでカメラが動いたかを確かめるときは、先にジオラマを止めてから、何もしないで2枚撮ります。止まっていれば、2枚の差は0%になるはずです。そのあとでジェスチャをして、差が出れば、それはジェスチャのぶんだと言い切れます。

こうした罠は、見つけるたびに確認手順の文書に書き足しています。同じところで二度つまずかないためです。

このノートに出てくるプロダクト

ほかのノート