ホロ・箔押し・ラメをブラウザで作る:color-dodge の虹とカード全体に乗せる1枚の層
に公開
トレーディングカードのホロ、箔押し、ラメをブラウザで再現した記録。color-dodge の虹、傾きで流れる金のグラデーション、mix-blend-mode が立体の重なりを潰す罠、紙の加工10種の作り分けまで書きました。
目次
カードを傾けると虹色に光る「ホロ」、金色が流れる「箔押し」、粒がきらめく「ラメ」を、画像素材を使わずブラウザの中だけで作りました。ホロは color-dodge の虹、箔は傾きに合わせて動く金のグラデーション、ラメは WebGL のシェーダです。途中で、mix-blend-mode を立体カードの直下に置くと重なりが潰れる罠と、ラメを人物にだけ付けると不自然になる失敗に当たりました。その経緯とコードを書きます。

作りたかったもの
カードゲームのレアカードは、光り方で格が分かります。紙の上の加工で言えば、絵が虹色に光るホロ、名前や枠に金属の箔を押す箔押し、細かい粒が散るラメなどです。これをマウスや指で傾ける操作に合わせて動かしたい、というのが出発点でした。
土台のカードは、絵1枚を深度マップで立体に動かす作りになっています(深度マップの記事)。その上に加工を重ねるので、立体の動きを壊さないことが条件でした。
ホロ:color-dodge の虹
最初のホロは CSS だけで作りました。カードの上に虹色のグラデーションの層を重ね、mix-blend-mode: color-dodge で下の絵に混ぜます。color-dodge は下の色を明るく焼く合成方法で、暗い所は変わらず、明るい所が虹色に染まります。写真全体が色つきのセロハンで覆われたようにならず、光る場所だけに色がのるのが利点です。層の濃さは傾きの大きさに合わせます。
.rar.h .holo { opacity: calc(.06 + var(--tilt) * .8); }
--tilt は、傾きの大きさ(0〜1)です。止まっているときはほとんど見えず(0.06)、大きく傾けると濃くなる(最大約0.86)ので、「止まっている間は静か」という方針を守れます。
人物にかかるホロは、後述の理由でシェーダで足しました。虹の色は、角度に応じた位相を作り、コサインで3色をずらす定番の式です。
float ph = v.x*1.9 + v.y*1.2 + uHl.x*1.5 + uHl.y*.8;
vec3 rb = .5 + .5*cos(6.283*(ph + vec3(0., .33, .67)));
float band = pow(.5 + .5*sin((v.x - v.y*.55)*10. + uHl.x*5. + uHl.y*3.), 3.);
float lum = dot(col, vec3(.3, .59, .11));
col += rb * band * lum * uHl.z;
uHl は傾きの値です。虹の rb に斜めの帯 band を掛け、さらに元の色の明るさ lum を掛けて足しています。明るい所ほど強く光り、暗い所には出ません。color-dodge の「明るい所だけ染まる」性質を、シェーダの中で式として再現した形です。
罠:mix-blend-mode を直下に置くと立体が潰れる
CSS で作ったホロを、立体に動くカードに載せたとき、奥行きの重なりが平らになる問題が出ました。カードは transform-style: preserve-3d で奥行きを持たせていますが、mix-blend-mode を normal 以外にした要素は、CSS の仕様で「合成のためにひとまとまりに描く」扱いになり、その親の立体は平らに畳まれます。記録では、「mix-blend-mode をカード直下に置くと立体の重なりが潰れる」と書いて、人物にかかるホロはシェーダに移しました。シェーダなら、色を足す処理が WebGL のキャンバスの中で終わるので、CSS の合成を使わずに済みます。
一方で、平らなカードで立体を使わない場合は、color-dodge をそのまま使っても問題はありません。ラメやレア表現の12種類では、派手さを出すために .lame.dg { mix-blend-mode: color-dodge; } を使っています。この使い分けは、「その加工が立体の上に乗るかどうか」で決めました。
箔押し:傾きで流れる金のグラデーション
箔押しは、名前や枠の金色を、傾きに合わせて流れるグラデーションにしました。金色のグラデーションを名前の文字や枠の線にだけ見せ、傾きの値で動かすと、金属の反射が移動したように見えます。専用の層を増やさず、CSS だけで済むのが利点です。箔は .fo というクラスで切り替え、名前の文字の色は別のクラスで金・銀・虹と変えられるようにしています。
ラメ:人物だけに付けたら不自然だった
ラメは、細かい星や粒が、傾けたときだけ瞬くものです。シェーダでは、画面を格子に区切り、格子ごとに乱数で大きさと位置と光る時機を決めます。
vec2 gv = v * vec2(34., 47.6);
vec2 g = floor(gv);
float h = fract(sin(dot(g, vec2(127.1, 311.7))) * 43758.5453);
float h2 = fract(h*57.31), h3 = fract(h*113.7);
vec2 dd = fract(gv) - .5 - vec2(h2-.5, h3-.5)*.5;
float sz = .35 + .65*h3;
float star = exp(-abs(dd.x*dd.y)*520./sz) * smoothstep(.42*sz, 0., length(dd))
+ smoothstep(.07*sz, 0., length(dd));
float tw = pow(max(0., sin(h*60. + uHl.x*9. + uHl.y*7.)), 6.) * step(.45, h2) * star;
float a = tw * (.22 + min(1., length(uHl.xy)) * .78);
exp(-abs(dd.x*dd.y)*...) は、十字に伸びる光条を作る式で、星形のきらめきになります。step(.45, h2) で半分以上の格子は光らせず、sin に傾きを入れて、傾きに合わせて光る格子が入れ替わります。
ここでの失敗は、最初のラメを人物にだけ付けたことです。人物の上だけが光り、枠や文字の箱は光らないので、「人物だけに付くと違和感がある」という指摘が出ました。そこで、カード全体(枠や文字の箱も含む)に乗る1枚の層(canvas.lame)として作り直しました。粒の色は、白から金を中心にしました。層は一番上に置き、立体のカードでは .rar.d .lame { transform: translateZ(62px); } で手前に出します。
紙の加工10種を1つのシェーダで作る
その後、絵は平らなままで、加工だけをカード全体に一様にかける10段を作りました。ふつう、ラメ、ツヤ分け、箔押し、ミラー、ホロ、柄ホロ(肉球)、割れホロ、縁ホロ、全面箔です。
実装は、一番上に重ねる1枚の層を FS.coat というシェーダで描き、uMode の値で分岐させる形です。傾きから作った光の筋の位置 sw を共通にして、分岐ごとに形だけを変えます。
float sw = v.x*.8 + v.y*.6 - (.7 + uHl.x*.75 + uHl.y*.5);
// 柄ホロ: 格子の中に肉球の形を描き、sw の帯に合わせて光らせる
// 割れホロ: 格子の最寄りの点(ボロノイ)ごとに向きを決め、傾きに合う面だけ光る
// 縁ホロ: 端からの距離 ed が小さいところだけ
// 全面箔: 金色を一面に敷き、帯の位置で明るさを変える
1ページに5枚ずつ並べ、1枚をなぞると5枚が同じ傾きで動くようにして、比べながら調整しました。最初は柄・割れ・縁をベタ塗りで作ったため派手すぎ、薄くしました。
測った結果と、残った弱点
検証は、実ブラウザでエラー0、右に傾けた画面の目視、配色のコントラスト検査の合格でした。弱点は記録に残っています。ツヤ分けは、差がごく小さい。ミラーは、左右上の数字の丸にも薄くかかり、色が浅く見える。そして、実際に見た時の評価で、柄ホロ・割れホロ・全面箔は「地味すぎる」とされました。そこで次の段階では、実際のカードゲームのレア表現を調べて作り直しています(トレーディングカードのレア表現12種)。
まとめ
ホロは color-dodge の虹、箔は傾きで流れる金のグラデーション、ラメは格子ごとの乱数で作れます。立体のカードの上では、CSS の mix-blend-mode をカード直下に置くと重なりが潰れるので、色を足す処理をシェーダの中に入れました。ラメは人物だけでなくカード全体に乗せる1枚の層にすると、自然に見えました。加工の10種類は、傾きの値と形だけを変えた1つのシェーダで作れます。