WebGLを使ったマンガビューワを作っている
自炊用マイ・マンガビューワーの話
京都なんか #4
御池ビルの前の木々
こんにちは
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.glslfloat 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.glsluniform sampler2D u_image;uniform int u_page; // ページ番号uniform float u_ink; // 黒相当の色の値 アプリ側でページ送りbuttonが押される度に新たな
u_page が渡されて再計算される テクスチャに画像をupload
app.js// create textureconst texture = gl.createTexture()gl.bindTexture(gl.TEXTURE_2D, texture)// upload image to texturegl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image) 予め取得したlocationに値を渡す
app.js 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// 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 // グレーレイヤーの有効化/無効化 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<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書き換え) が軽快
通信環境に応じて描画クオリティを変えられる
おわり
ソースコード
もう少し詰めたら公開できそう
デモで登場したマンガ
購入して試してうまくいった漫画
個別にお見せできますので、よければお声掛けください