写真を絵みたいにしよう - Kuwaharaフィルタで作ったインタラクションの話

写真を絵みたいにしよう - Kuwaharaフィルタで作ったインタラクションの話
KuwaharaフィルタをかけたNext.jsブログの写真表示 - 絵画風に変換されたサンプル画像

ブログに写真を貼るとき、ただ並べるだけじゃなんだかなーと思っていました

旅の写真も、日常の写真も、ギャラリーとして見せるより「触れる体験」にしたい。そういう気持ちで始まったのが今回のシェーダー実装です。

このシェーダーは絵画風にする機能だけ付けようと思ってました
そのシェーダーの画像を見てて思ったのが、その絵画風にした後に写真に戻せば、カメラを撮っている時のピントを合わせるような雰囲気になるんじゃね?という発想でした

GLSLってそもそも何

最初に少しだけ前提の話をさせてください。

GLSLはGPU上で動くシェーダープログラムを書くための言語です。CPUで逐次処理するのとは違って、GPUは何千もの演算を並列でこなします。今回のような「全ピクセルに同じ処理を適用する」という用途は、GPUと相性がよかったりします。

普通のJavaScriptで画像をぼかそうとすると、ピクセル数が多いほど処理が重くなって詰まる。でもGLSLなら「このピクセルをどんな色にするか」という計算をGPU上で全ピクセル同時にやってくれます。

これがリアルタイムにぬるぬる動く理由です。

Kuwaharaフィルタとは何か

今回使ったのがKuwaharaフィルタというアルゴリズムです。

一言でいうと:

周囲の領域を複数に分割して、一番"均一(分散が小さい)"な領域の平均色を採用するフィルタ

1ピクセルの周囲をこんな感じで4分割します。

[左上] [右上]
[左下] [右下]

それぞれの領域で「色のばらつき(分散)」を計算して、一番ばらつきが少ない領域の平均色を採用します。

なぜこれが油絵っぽく見えるのか。

エッジ(色が急に変わる境界線)が必ずどこかの領域で"混ざる" ことになります。逆に言えば、どこかの領域は"純粋な面だけ"になる。そっちを選ぶので、輪郭が残りつつ、面の中はなめらかに潰れていく。絵の具で塗ったような質感が生まれる仕組みです。

8分割のほうがエッジの再現精度は上がりますが、計算量が増えます。今回は4分割ベースで実装しています。

もっと深くまで知りたい人はこちらの記事が参考になります。図解がとてもわかりやすいです。

自分も翻訳しながら読もうとしたんですが、行列や線形代数が出てきたところでギブアップしました笑。高精度な実装も考えたんですが、そもそも理解できないのと、自分のPCのGPUでは実装しても確認できるレベルの描画にならない、あと正直全く違いが分からない、という三段論法でやめました。

最初の設計:スクロールで絵画→写真

最初はスクロールで制御しようと思っていました。

ページを下にスクロールするにつれて、絵画からだんだんと写真に戻っていく。スクロール量を0.0〜1.0の数値として管理できるので実装としてはきれいで、数値レベルでフィルターを外していって、ある一定のスクロール量に達したら絵画から写真に切り替わっているのはかっこいいなと思っていました。

でも実際に試してみると、野暮ったく感じてきました。

写真は1枚じゃなくてブログ全体に何枚もある。スクロールのたびに毎回絵画→写真の変化を繰り返すのは、体験として重すぎる。「また同じ演出か」ってなってしまう。

スクロールという操作が、「コンテンツを読み進める動作」なのに、「シェーダーを操作する動作」と二重になってしまっていたんだと気づきました。

「保存すればよくない?」という疑問

実装していると、こういう疑問が浮かんできました。

「Kuwaharaフィルタかけた静止画を事前に書き出して、それを使えばよくない?」

確かにそうすれば処理は一切ないし、重くもない。でもそれは今回やりたいことのメリットを全部潰している気がしました。

自分がやりたかったのは**「触れることで変化する体験」** でした。静止画になった瞬間、それは体験ではなくただのコンテンツになるのはもったいないかなと思って今回作ってみました。

リアルタイム性・インタラクティブ性こそがGLSLを選ぶ理由になるんですかね

最終判断:ホバー形式

最終的に選んだのはホバー形式です。

ホバー前(アイドル状態): strength = 0.85(絵画風)
ホバー中: strength = 0.0(元の写真)
ホバーを外す: strength = 0.85に戻る(余韻あり)

実装ではこのstrengthという値を0.0〜1.0で管理していて、シェーダーはこの値に応じてKuwaharaフィルタの強度を変えています。

const HOVER_IN_DURATION = 800;   // 絵画 → 写真(速め)
const HOVER_OUT_DURATION = 2400; // 写真 → 絵画(余韻あり)

ホバーで写真に戻るのは800ms、離してから絵画に戻るのは2400ms。戻りをゆっくりにしているのは、写真を確認する時間を与えたかったから。あと単純に余韻のある動きのほうが好きです。

イージングにはeaseOutCubicを使っています。最初は速く、最後はゆっくり落ち着く感じ。これがKuwaharaのにじみ感と相性がよかったりします。

スクロールしているとき、マウスは止まっていても画面は動いている。つまり写真にカーソルが乗ったままスクロールすると、自動でホバー状態が継続されます。「スクロールしながら見ていたら自然と写真が見える」という体験が、結果的に成立していました。

パフォーマンスの話

正直に書くと、これは重いです。

自分のPCではカクつきがすごくてブラウザが重いのなんの。。。

性能が低めのPCでは、カクつきが出ることがあります。表示できないレベルではないですが、スムーズかといえばそうではない場面もある。iPhoneのほうがラグくなかったです

これをどう考えるかは、実装していてずっと気になっていたところです。

体験の質にこだわるなら、性能差によって体験が変わってしまうのはよくない。でもそれを理由に表現を下げるのも違う。最終的には「ある程度の性能を前提にした表現」という割り切りをしました。

モバイル対応はtouchStart / touchEndでホバーと同じイベントを発火させることで対応しています。タップすると絵画から写真に切り替わります。

この記事の写真も含め、ブログ全体の写真にKuwaharaを展開しました。

実は「20枚あったら20個のWebGLコンテキストが同時に動く」という問題があって、そのままでは記事に使えなかったんですが、Intersection Observerで解決しています。スクロールでビューポートに入った瞬間にWebGLを起動して、外れたらフリーズさせる。出ていくときはコンテキストを破棄せず保持しておくので、再度スクロールしてきたときに即座に動く。これで動いているコンテキスト数を「今見えている枚数」に抑えられました。

まとめ

結局、これは技術の話よりデザインの話だったなと思います。

何かをリアルタイムで動かすことに意味があるのかどうか。スクロールかホバーかという選択も、実装の都合じゃなくてユーザー体験から逆算して決めてみました。GLSLもKuwaharaも、手段として選んでいます。

「かっこいいから作る」という動機でもいいと思っているんですが、「なぜそれをやるのか」がブレると、実装の途中で迷子になります。今回はその迷子になった部分も含めてまとめてみました。

最後まで読んでくれてありがとうございました。

#TECH2026/05/01
···

CATEGORIES

TRIP(7)
LIFE(10)
TECH(17)
© Ayase 2026