WebOS Goodies

WebOS の未来を模索する、ゲームプログラマあがりの Web 開発者のブログ。

WebOS Goodies へようこそ! WebOS はインターネットの未来形。あらゆる Web サイトが繋がり、共有し、協力して創り上げる、ひとつの巨大な情報システムです。そこでは、あらゆる情報がネットワーク上に蓄積され、我々はいつでも、どこからでも、多彩なデバイスを使ってそれらにアクセスできます。 WebOS Goodies は、さまざまな情報提供やツール開発を通して、そんな世界の実現に少しでも貢献するべく活動していきます。
Subscribe       

WebGL のシェーダーでグリグリできる CSS Shaders の使い方

こちらの記事を見て「おお、ついに CSS Shaders 実装されたんか!」と喜び勇んで試した後、すべてが終わってから Dev 版 Chrome でも使えることに気づくという情弱ぶりを発揮した今日この頃、みなさんいかがお過ごしでしょうか。いったいいつから使えてたんだろう・・・(´・ω・`)

そんなわけで、最新の話題でもないみたいですが、せっかく試したので記事にしておきます。 CSS Shaders は「プログラマブルシェーダー」という特殊な言語でページ上の要素をグリグリ変形したりできる技術。従来の CSS 3D transform は単に要素全体を回転させるだけでしたが、 CSS Shaders は形状を変えたり、ピクセル単位でライティング計算したりと、かなり自由に加工できます。

まだ一般の Web サイトで使える段階ではありませんが、見た目のインパクトはかなり大きいので、ネタとして知っていれば自慢できるかもしれませんよ(笑)

追記 : Chromium なら 3 月から使えていたみたい・・・ orz (thanks @yoshikawa_t)

CSS Shaders を有効にする

最初に CSS Shaders を有効にする方法から。 CSS Shaders は Google Chrome のバージョン 23 以降(現段階だと Dev 版と Canary 版かな?)に実装されているようです。したがって、この記事のサンプルを動かすには、こちらのページからダウンロード・インストールする必要があります。 Chrome の正式版を使っている場合は、それとは別個にインストールできる Canary 版がおすすめです。

さらに設定変更も必要です。アドレスバーに chrome://flags と入力し設定画面を表示し、「Enabls CSS Shaders」という項目を有効にして、ブラウザを再起動してください。これで CSS Shaders の機能が有効になるはずです。

使ってみる

さっそく CSS Shaders を動かしてみましょう。 CSS の指定は以下の様な感じになります。

-webkit-filter: custom(
  url(sample.vs)
  mix(url(sample.fs) multiply source-atop),
  64 64,
  transform perspective(1000) rotateX(45deg), angle 0);

2行目はプログラマブルシェーダーのひとつである頂点シェーダーが記述されたファイルの URL 、 3 行目は同じくフラグメントシェーダーの URL です。 mix(〜) というのはフラグメントシェーダーで計算した色とページコンテンツとのブレンド方法を指定するものです。 4 行目はメッシュ分割数の指定、最後の行はシェーダーに渡すパラメータの指定です。まあ詳しくは後述。

プログラマブルシェーダーのソースコードは別ファイルで用意します。まずは頂点シェーダー。メッシュ分割された各頂点の座標を計算し、コンテンツを変形させるものです。

precision mediump float;

attribute vec4 a_position;
attribute vec2 a_texCoord;

uniform vec2 u_textureSize;
uniform mat4 u_projectionMatrix;
uniform mat4 transform;
uniform float angle;

varying vec3 v_normal;

void main() {
    vec2 k = vec2(0.025, 0.0125);
    vec2 pos2d = a_texCoord.xy * u_textureSize * k + vec2(angle, 0);
    float z = sin(pos2d.x + pos2d.y) * 30.0;

    float d = -(cos(pos2d.x)*cos(pos2d.y) - sin(pos2d.x)*sin(pos2d.y));
    v_normal = normalize(vec3(d, k.y / k.x * d, 1.0));

    vec4 pos = vec4(a_position.xy, z, 1.0);
    gl_Position = u_projectionMatrix * transform * pos;
}

次はフラグメントシェーダーです。各ピクセルのフィルタリングを行うものですが、ここではごく簡単なライティング計算をしてます。

precision mediump float;

varying vec3 v_normal;

void main() {
  vec3 l = normalize(vec3(1, 1, 1));
  float i = max(0.0, dot(normalize(v_normal), l)) * 0.7 + 0.3;
  css_MixColor = vec4(i, i, i, 1.0);
}

実際にこれらを適用すると、以下のような表示になります(下はスクリーンショットです。対応ブラウザをお使いの方は、画像下のリンクで実際に CSS Shaders を適用したものが見れます)。

[実際のデモはこちら]

こんな感じで、ページコンテンツを 3 次元的に変形させることができます。上ではとりあえず簡単にできる波型への変形をやっていますが、もちろんそれだけではありません。ページコンテンツを任意の数のメッシュに分割し、その各頂点を自由に制御できるので、あらゆる形に変形できます。色についてもピクセル単位で加工が可能で、頂点シェーダーで計算した値も取得できるので、上の例のように形に合わせたライティングも実現できます。

使い方

CSS Shaders 仕様のドラフトは以下の場所にあります。

http://dvcs.w3.org/hg/FXTF/raw-file/tip/filters/in...

とはいえ、 CSS Shaders だけではなく CSS フィルタと SVG フィルタをすべて統合する仕様になっているので、前提知識がないとわけがわかりません(あってもかなり辛い)。そこで、ここでは CSS で使う CSS Shaders の話に絞って概要を説明することにします。ただし、現在の仕様はドラフトなので、今後大きく変更される可能性があります。あらかじめご了承ください。

スタイル指定

まずはスタイル指定の詳細ですが、 CSS シェーダーのスタイル指定の文法は概ね以下のようになっています(<〜> は省略不可、 [〜] は省略可能)。

-webkit-filter: custom(
  url(<頂点シェーダー>)
  mix(url(<フラグメントシェーダー>) <ブレンドモード> <合成モード>),
  [<横メッシュ分割数> [縦メッシュ分割数]] [ボックス] [attached または detached],
  [<変数名1> <値1>[, <変数名2> <値2>...]]);

<頂点シェーダー> と <フラグメントシェーダー> は(前述のとおり)シェーダープログラムが書かれたテキストファイルの URL です。 mix(〜) の指定についてはフラグメントシェーダーのところで説明します。

<横メッシュ分割数> と <縦メッシュ分割数> はコンテンツをメッシュに分割する際の分割数です。 CSS Shaders では図のようにコンテンツを格子状に分割して、頂点シェーダーでそれぞれの頂点(赤や灰色の丸)を移動させることで変形を行います。この格子の細かさがこのパラメータで調整できるというわけです。もちろん細かいほど滑らかな変形が可能ですが、そのぶん処理が重くなります。 GPU アクセラレーションがかかっていればほぼ問題になりませんが、環境によってはソフトウェアでエミュレーションしているかもしれないので。


Copyright © 2012 W3C® (MIT, ERCIM, Keio), All Rights Reserved. (仕様書より引用)

<ボックス> はこのメッシュの外枠をどのボックスに合わせるかで、 border-box, padding-box, content-box, filter-box のいずれかです。最初の 3 つは CSS でお馴染みのボックスモデルでお馴染みなので省略。 filter-box というのはフィルタの効果をすべて含むボックスです。例えばぼかしをかけたとき(後述しますが、 CSS Shaders はぼかしフィルタ等と併用できます)、ぼかしの部分は border-box の外に広がりますよね。そのぼかしの部分も含むボックスということです。

<attached または detached> は、メッシュの各ポリゴンを独立させるかどうかです。デフォルトの attached では、各ポリゴンは連結しているので、メッシュに隙間ができることはありません。 detached では各メッシュの頂点が別になるので、それぞれのポリゴンをバラバラにできます。メッシュが爆発するように飛び散るとか、そういう効果を出したいときは detached を指定します。

最後の <変数名1> などは、プログラマブルシェーダーに渡すパラメータです。詳しくは後述しますが、シェーダーには「uniform 変数」というパラメータを受け取るための変数があり、変数名で指定した名前の変数に、対応する値が代入されるという仕組みになっています。値としては以下のものが指定できます。値としては以下のものが指定できます。

データ型指定方法
真偽値true または false(ベクトルの場合は空白区切りで並べる)
数値任意の浮動小数点値(ベクトルの場合は空白区切りで並べる)
配列数値を空白区切りで並べる
2x2行列mat2(値1, 値2, 値3, 値4)
3x3行列mat3(値1, 値2, ... 値9)
4x4行列mat3(値1, 値2, ... 値16) もしくは CSS 3D Transform と同じ指定方法
テクスチャtexture(URL または SVG フィルタの出力名)

このページのサンプルの場合、 4x4 行列の transform 変数と数値の angle 変数にそれぞれ値を指定していたわけですね。

ちなみに、 CSS Shaders は CSS フィルタの効果のひとつと位置づけられているので、他の CSS Filter と重ねがけできます。例えば以下のようにすると、セピア調に色変換したコンテンツに対してシェーダーによる変形を適用できます。

-webkit-filter: blur(4px) custom(...);

CSS フィルタや CSS 3D Transform については、手前味噌ですが @IT さんのこちらの記事で解説していますので、ご参照くださいませ。

頂点シェーダー

次は頂点シェーダーですが、シェーダー言語の文法をブログ記事ひとつで解説するのは明らかに無謀なので、そこには立ち入りません。興味のある方は OpenGL 関連の書籍などをあたると良いかと思います(CSS Shaders のシェーダー言語は WebGL と同じく OpenGL ES 2.0 (ほぼ)互換です)。私は右の「Open GL ES 2.0 プログラミングガイド」をリファレンス代わりによく使っています。

ではなにを説明するかというと、外部からのパラメータ(前述のスタイル指定によるものや、メッシュの頂点座標など)を受け取る方法にフォーカスします。ここは CSS Shaders 特有の部分です。

まず単純なところで、スタイルで指定されたパラメータの受け取りですが、これには「uniform 変数」というものを使います。サンプルの頂点シェーダーでも、以下の 4 つの uniform 変数が定義されていましたね。

uniform vec2 u_textureSize;
uniform mat4 u_projectionMatrix;
uniform mat4 transform;
uniform float angle;

このうち、 transform と angle がスタイルで指定したパラメータを受け取る変数です。このように、スタイルと同じ名前の uniform 変数を定義しておけば、自動的に値が設定される仕組みになっています。他の "u_" で始まる変数はスタイル指定にはありませんが、これはシステム定義の変数です。この種の uniform 変数には以下のものがあります。

uniform mat4 u_projectionMatrix
透視変換行列です。座標変換の最後でこの行列を掛けてやれば OK です。
uniform vec2 u_textureSize
filter-box の縦横のピクセル数が渡されます。
uniform vec4 u_meshBox
メッシュの左上座標と縦横のサイズが filter-box の座標系で渡されます。スタイルでの <ボックス> の指定が filter-box なら、 vec4(-0.5, -0.5, 1.0, 1.0) になります。
uniform vec2 u_tileSize
頂点座標と同じ座標系で、メッシュのひとマスの縦横サイズが渡されます。
uniform vec2 u_meshSize
メッシュの分割数が渡されます。

一見なぜこんなものを渡してくるのかわからないかもしれませんが、実はないと困るものばかりです。実際、サンプルの頂点シェーダーでも、テクスチャ座標に u_textureSize を掛けることで頂点のピクセル単位の座標(DOM 要素内でのローカル座標)を算出しています。

さて、次はメッシュの頂点の情報の受け取り方です。こちらは「attribute 変数」を使います。サンプルでも以下の 2 つの attribute 変数を定義しています。

attribute vec4 a_position;
attribute vec2 a_texCoord;

attribute 変数の名前はあらかじめ決まっていて、以下のなかから必要なものを定義すれば OK です。

attribute vec4 a_position
頂点の座標が渡されます。座標系は filter-region (CSS Filters の範囲なら filter-box と同じかな?まだきちんと把握できていません)を -0.5〜0.5 にスケーリングしたもの。
attribute vec2 a_texCoord
テクスチャ座標が渡されます。 0〜1 の範囲。
attribute vec2 a_meshCoord
スタイル指定の <ボックス> で指定した座標系での頂点座標。 0〜1 の範囲。
attribute vec3 a_triangleCoord
x と y は、現在の頂点が所属するタイル(メッシュ内のマス)の位置。 (0, 0) が右上(仕様にこう書いてあるけど、左上の間違いかも)で、 (<横分割数>, <縦分割数>) まで(ここも仕様にそう書いてある。分割数 - 1 までな気もするけど、端にもうひとつタイルがあると考えるのかなぁ)。 Z 座標はタイル内のどの頂点かを示す整数。以下の画像参照。


Copyright © 2012 W3C® (MIT, ERCIM, Keio), All Rights Reserved. (仕様書より引用)

最後に出力値について。計算した頂点座標は、 gl_Position という変数に代入する決まりになっています。サンプルでもそうしていますね。また、頂点座標以外にフラグメントシェーダーに渡したい値は、「varying 変数」に設定しておきます。フラグメントシェーダーにも同じ名前の varying 変数があれば、そこにその値が設定されます(ただし、そのままの値ではなく、ポリゴンの 3 頂点の値を補間したものになる)。

フラグメントシェーダー

最後はフラグメントシェーダーです。これがちょっと曲者。 OpenGL や WebGL の感覚だと、 DOM 要素の画像をテクスチャとして読み込んでゴニョゴニョできると考えてしまいますが、残念ながらそれはできません。 CSS Shaders のフラグメントシェーダーでは、ページコンテンツの画像を直接取得することは完全に禁止されています。その代わり、ページコンテンツとブレンドする色(css_MixColor)と、ページコンテンツの色を変換する行列(css_ColorMatrix)を出力します。それらをもとに、下図の方法で最終的な色を決定します。


Copyright © 2012 W3C® (MIT, ERCIM, Keio), All Rights Reserved. (仕様書より引用)

簡単に説明すると、まず背景色(ページコンテンツの色)は css_ColorMatrix と乗算され、上の図でいう「multiColor」が算出されます。これにより、簡単な色変換っぽいことができます(ただ、あまり自由度はない。将来的に 5x4 行列にするかもと仕様にも書いてある)。次に、 multiColor の RGB 要素と css_MixColor が、スタイル指定の <ブレンドモード> で指定された方法でブレンドされて、 blendedColor が算出されます(アルファ値は css_MixColor のものが引き継がれる)。最後に、 multiColor と blendedColor がスタイル指定の <合成モード> で指定された方法で合成され、最終的な描画色が決定されます。

<ブレンドモード> と <合成モード> の詳細については仕様書に記載がないのですが、たぶんそれぞれ SVG フィルタの feBlend タグ、Canvas の globalCompositeOperation と同じものっぽい気がします。またもや手前味噌ですが、 SVG フィルタについてはこちらの記事、 Canvas API についてはこちらの記事で解説していますので、ご参考までに。

ちなみに、スタイル指定の時に mix(〜) を記述せず、 url(〜) のみでフラグメントシェーダーを指定した場合は、上記の合成処理が行われず、フラグメントシェーダーで直接 gl_FragColor に値を設定できます。ただし、この場合でもページコンテンツの画像が取れるわけではないので、後段のフィルタで使う画像を動的生成するくらいにしか使えないでしょう。頑張ればフラグメントシェーダだけで画像を生成できますが、そこまでするなら素直に WebGL 使うほうが・・・ ^^;

出力の話を先に済ませてしまいましたが、フラグメントシェーダーの入力についても少し触れておきます。フラグメントシェーダーに入力される値は、大きく分けて uniform 変数、 varying 変数、テクスチャの 3 つです。 uniform 変数については頂点シェーダーとまったく同じ物が使えます。 varying 変数も頂点シェーダーのところで説明したとおり、三角形の 3 頂点で算出された値を補間したものが渡されます。

テクスチャに関しては実際に試していないのですが、スタイル指定の時に texture(〜) の形で指定した画像にアクセスできるようです。画像は WebGL と同じく、ページと同じドメインからの画像、 CORS でアクセス許可された画像、 tainted でない Canvas が指定できる、はず。

CSS Transition や Animation の適用

いくら柔軟に形状を変形できるといっても、静止画ではインパクトに欠けますよね。もちろん CSS Filters も CSS Transition や CSS Animation で動かすことができます。具体的には、スタイル指定での uniform 変数を変化させることでアニメーションを実現します。冒頭のサンプルには波を動かすための angle パラメータがすでにあるので、以下のように CSS Animation を適用することで、はためいているようなアニメーションになります。

@-webkit-keyframes anim {
  0% {
    -webkit-filter: custom(
      url(sample.vs) mix(url(sample.fs) multiply source-atop),
      64 64, transform perspective(1000) rotateX(45deg), angle 6.283184);
  }
  100% {
    -webkit-filter: custom(
      url(sample.vs) mix(url(sample.fs) multiply source-atop), 64 64,
      transform perspective(1000) rotateX(45deg), angle 0);
  }
}

[実際のデモはこちら]

与えられたパラメータをもとに自由に形を計算できるので、とくにトランジション系はかなり凝ったものができるでしょう。形状が変化しない従来のトランジションは、機械のように無機質な印象になりがちですが、 CSS Shaders を使えば、ゴムのように変形して引っ張られながら出てくるといった、柔らかい動きが表現できます。これが普及した時にどんなサイトが出てくるのか、考えるだけでもワクワクしますね。

気付いた点など

最後に、実際に CSS Shaders を使ってみて、気付いた点をいくつか挙げておきます。

けっこう重い?
まだ最適化されていないのかもしれませんが、 CPU 負荷がけっこう重いようです。 とくにアニメーションしだすと、 WebGL に比べても数倍くらいの CPU パワーを消費します。 Mac mini のファンが唸りを上げるぜ。
頂点シェーダーはかなり使える
頂点シェーダーによる変形はかなりいいです。 CSS での指定が uniform 変数に直結していて、しかも行列は CSS 3D Transform 風に指定できるので使いやすい。現在の頂点がどのタイルに所属しているか、タイルのどの位置の頂点かもわかるので、たいていのことは実装できそうな気がします。
フラグメントシェーダーはSVG フィルタとの組み合わせがキモ?
前述のとおり、フラグメントシェーダーでページコンテンツの画像を直接扱えなくなったのは大きな後退です。フラグメントシェーダーを活用した凝ったフィルタがガンガン使えるかと期待していたのですが、その野望は脆くも崩れ去りました (´・ω・`) ただ、 SVGフィルタにはもともと多くの機能があるので、その補完として使うのはありかもしれません。フィルタの入力画像や合成用のマスクを動的生成して、ピクセル単位でフィルタの効果を制御するとかですね。時間のあるときに考えてみたいです。
変換対象の要素のクリックなどには注意が必要
CSS Shaders では、変形するのは要素の見た目だけで、クリックなどに反応する場所は元のままです。 CSS Transform が座標変換後の要素に対してクリックや文字選択ができたのとは対照的です。ユーザーが混乱するのを避けるためにも、変形したコンテンツはクリックなどに反応しないようにしたほうがいいかもしれません。
デバッグ大変
これは本当に辛かった。実装されたばかりだから仕方ありませんが、エラーがあってもコンソールにメッセージすら表示されず、すべての効果が静かに無視されます。なので、動かなくなると原因がまったくわかりません。括弧の対応が取れてないとか、スタイルとシェーダーで変数名が一文字違うとか、些細なことですごく時間を取られます。せめてシェーダーのコンパイルエラーくらいは早急に表示するようにしてほしいです。

というわけで、本日は CSS Shaders の機能をご紹介しました。いろいろ問題はありますが、面白い機能であることは間違いありません。シェーダーのプログラムの変更が見た目にダイレクトに反映されるので、(バグが出なければ)使っていてすごく楽しい。皆さんもぜひ、グリグリしてみてください。

関連記事

この記事にコメントする

Recommendations
Books
「Closure Library」の入門書です。
詳しくはこちらの記事をどうぞ!
Categories
Recent Articles