WebGLを使ったマンガビューワを作っている

WebGLを使ったマンガビューワを作っている

自炊用マイ・マンガビューワーの話

御池ビルの前の木々

こんにちは
daiizです
Nota Inc.でスクボScrapbox というwikiを作っています
趣味開発では、画像に関するネタで何か作るのが好き

今日は、3つくらい工夫を施して簡単なマンガビューワーを作っている話をします
漫画に限らず、短編小説とかにも使えるはず

複数の画像を1枚にまとめる
1年くらいScrapboxで眠っていたアイデアをようやく使えた
当時は応用先として思いつかなかったが、最近マンガと相性良さに気づいた
白黒表示で問題なくて、1話ぶんならページ数もそれほど多くないはず

2値化された複数の白黒画像を1枚のpng画像にする
入出力画像
入力:
2値化済み白黒画像 複数枚
出力:
RGBカラー画像 1枚 (hangaと呼んでいる)

まとめる前後で座標は対応関係にある
元の白黒画像の点はそのままに対応する

での様子
黒:0, 白:1 として、白黒画像の最初の8ページの色を並べて、8bitの2進数として扱う
これが出力画像の点のRGBのRになる
9ページ目以降を使って、G, Bも同様に求まる


hangaには最大24枚まで格納できる
R, G, B はそれぞれ8bitの2進数で表現できるため、合計24bit
こういう画像ができあがる
カラフル画像
この画像だけを管理すればよい
自炊に便利
Gyazoにuploadしやすい。一番嬉しいこと。

まとめた画像を1枚ずつ見たい
hanga画像ピクセルぶんに対して逆操作をすればいい
ブラウザで動くビューワを作りたい
結構大きめの行列を相手にすることになり大変そう


WebGLで画像ビューワを作る
WebGLはじめて触った
glslでshaderを書き、programをGPUにuploadして演算し、結果をHTMLのcanvas要素に描画する
主に fragment shader を書いた
今回は各ピクセルでの色を決定するだけでよい
WebGLRenderingContext
const gl = canvas.getContext('webgl', {alpha: true})
普段 '2d' としていたところを 'webgl' にするだけで良い手軽さ

各点ごとに計算できる
hanga上の各点に対して以下を行う
ページ番号に応じて、R, G, Bのどれかを選び、値を取得する
glslでは配列の添字に変数を使えない
frag.glsl
Copied!
float getColor (vec4 rgba, int pageNum) {
if (pageNum <= 7) return rgba[0];
if (pageNum <= 15) return rgba[1];
return rgba[2];
}
0 ~ 1 の範囲で返ってくるので255倍して使う

AND演算すると、任意のページの、このピクセルでの色 (黒 or 透明) が確定する
この点での、1ページ目の色を求める例
RGBのR値 & 01000000 を計算するだけで良い
0 黒相当で着色
0以外の値 透明にする
白ではなく、透明にしておくのが後のポイント
WebGLでは & 演算子がないらしいので、自前でand関数を書く必要がある

fragment shader
application js と shader で共有する値
frag.glsl
Copied!
uniform sampler2D u_image;
uniform int u_page; // ページ番号
uniform float u_ink; // 黒相当の色の値

アプリ側でページ送りbuttonが押される度に新たな u_page が渡されて再計算される

テクスチャに画像をupload
app.js
Copied!
// create texture
const texture = gl.createTexture()
gl.bindTexture(gl.TEXTURE_2D, texture)
// upload image to texture
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image)

予め取得したlocationに値を渡す
app.js
Copied!
const locPage = gl.getUniformLocation(program, 'u_page')
const locInk = gl.getUniformLocation(program, 'u_ink')
gl.uniform1f(locInk, ink)
gl.uniform1i(locPage, state.page)

fragment shader が返す値
そのピクセルを何色で着色するか (gl_FragColor)
frag.glsl
Copied!
// 1ページ目の色を取り出す例
void main () {
float page = 64.; // 01000000
vec4 pixel_color = texture2D(u_image, v_texCoord).rgba;
float color = getColor(pixel_color, page); // 0 ~ 1
float alpha = and(int(color * 255.), int(page)) == 0 ? 1. : 0.; // 黒なら、alphaを1
gl_FragColor = alpha == 1. ? vec4(vec3(u_ink), 1.) : vec4(0.);
}
gl_FragColor のとりうる値
黒相当: vec4(vec3(u_ink), 1.)
透明: vec4(0.)
この色がcanvas要素の各点に着色される

canvasの内容
1個のcanvasに全ページぶんの画像情報が入っていて、適宜切り替えて表示している状態
この画像をダウンロードしておき、ファイル選択から読み込むと、全ページを一気にロードできる
少々粗いが一瞬で24ページぶん読める

ページ送りのたびに通信されない
最初に一回だけ1MB程度の画像をロードするだけ

ページジャンプのような非連続な表示も素早い
栞機能とか作れる

これだと白か黒しか表現できない?
二値化の閾値
単純なモノクロ画像を作るときは、256段階の中央として、128を採用した
オリジナル画像の各ピクセル色に対して、ならば黒、それ以外は白とする
 


閾値を変えてhangaを作ることで、グレーとして扱う画像を作成できる
閾値を大きくすると、黒相当で着色する領域が広がる

漫画なら3段階くらいグレーでもそこそこ綺麗
app.js
Copied!
// グレーレイヤーの有効化/無効化
setupGrayLayerSwitch(1, {ink: 0.500, gray: 192}) // Gray1
setupGrayLayerSwitch(2, {ink: 0.750, gray: 224}) // Gray2
setupGrayLayerSwitch(3, {ink: 0.875, gray: 240}) // Gray3
grayは閾値
inkは0に近いほど黒く、1に近いほど白い


グレー層を複数のcanvasに描画する
3段階のグレー扱いの画像をそれぞれの <canvas> に描画する
それぞれのcanvasは黒 (またはグレー) 以外のピクセルは透過にしてあるので重ねて見せられる
html
Copied!
<div>
<canvas class='c128'></canvas>
<!-- 以下、グレー層 -->
<canvas class='c192'></canvas>
<canvas class='c224'></canvas>
<canvas class='c240'></canvas>
</div>
すべてのcanvasは同時にページ送りされる

粗い画像を数枚重ねればそれなりにいい感じになる
多段階のグレー層を使うことで表現力が上がる
次々スクリーントーンを貼っていく感じ
薄い文字が読みやすくなる
/wakaba-manga/第3話 クリエイター必見!1分でポートフォリオサイトを作れるScrapboxより

通信速度が安定しているときだけ、グレーを配信すればいい
最初のシンプルな白黒バージョンだけでもとりあえず読める
グレー画像層は独立して配信できる

意外と嬉しいこと多かった
軽量データなのでダウンロードが速い
予想以上にページ送り (canvas書き換え) が軽快
通信環境に応じて描画クオリティを変えられる


おわり
ソースコード
もう少し詰めたら公開できそう

デモで登場したマンガ

購入して試してうまくいった漫画
個別にお見せできますので、よければお声掛けください

Powered by Helpfeel