Neon Lamp with WebGL
https://gyazo.com/4946843765f1650c8c96d71b9ab217a9
おほほ
https://scrapbox.io/files/5ff608b3462b01003ad5c175.mp4
𝑺𝑰𝑪𝑲
https://scrapbox.io/files/5ff608f9c4b369001cd0b01f.mp4
💭動機
Z年代!みたいなグラフィック作りたい!
今年は異常行動だけで周りの気を引くの辞めたい
Adobe 使えね〜(泣)
3DCG 作成ソフトは大学の授業思い出すのでやりたくね〜(怨)
⚙️中身
普通の Next.js + TypeScript .特に変わったことはしていない.
WebGL のレンダリングは例によって Three.js に頼っている.いつもありがとうな...
1. テキストをテクスチャにする
code:index.tsx
const createTexture = (params: CreateTextureParams) => {
const { textureWidth, textureHeight, dpr } = params;
const canvas = document.createElement("canvas");
const canvasWidth = textureWidth * dpr;
const canvasHeight = textureHeight * dpr;
canvas.width = canvasWidth;
canvas.height = canvasHeight;
const context = canvas.getContext("2d");
if (context !== null) {
context.textAlign = "center";
// context.textBaseline = "middle";
context.font = bold italic ${160 * dpr}px Astloch;
context.fillStyle = "#69e5ff"; // 青
context.fillText("Happy New Year", canvasWidth * 0.5, canvasHeight * 0.3);
context.font = ${540 * dpr}px Text Me One;
context.fillStyle = "#e36bee"; // ピンク
context.fillText("2021", canvasWidth * 0.5, canvasHeight * 0.75);
}
const texture = new CanvasTexture(canvas);
texture.needsUpdate = true;
return texture;
};
Canvas API の getContext("2d") を使うと 手続き的に書かないといけないので複雑になってくるとマジで最悪 2Dのテクスチャを作れる.テキストも乗せれて便利.
別に2のべき乗の大きさの画像をソフトで作って THREE.TextureLoader 使うとかでここらへんは代用してもいい気がする(Window の DPR までは考えてないから適当にリサイズ書いてくれ)
2. bloom の重み,オフセットを計算してテクスチャごと GLSL 側に渡す
code:index.tsx
const onCanvasLoaded = (canvas: HTMLCanvasElement) => {
if (!canvas) return;
const width = window.innerWidth;
const height = window.innerHeight;
const dpr = window.devicePixelRatio;
// calc offset, weight
const bloomConfig = {
sampleCount: 15,
};
const offset = {
};
const weight = {
totalHorizontal: 0,
totalVertical: 0,
};
for (let i = 0; i < bloomConfig.sampleCount; i++) {
const p = (i - (bloomConfig.sampleCount - 1) * 0.5) * 0.00065;
offset.tmpHorizontali = p; weight.horizontali = Math.exp((-p * p) / 2) / Math.sqrt(Math.PI * 2); weight.totalHorizontal += weight.horizontali; weight.verticali = Math.exp((-p * p) / 2) / Math.sqrt(Math.PI * 2); weight.totalVertical += weight.verticali; }
weight.horizontal.map((value, index) => {
weight.horizontalindex = value / weight.totalHorizontal; });
weight.vertical.map((value, index) => {
weight.verticalindex = value / weight.totalVertical; });
offset.tmpHorizontal.map((value, index) => {
offset.vector2Horizontalindex = new Vector2(value, 0); });
offset.tmpVertical.map((value, index) => {
offset.vector2Verticalindex = new Vector2(0, value); });
// ~~
// THREE.Scene とかでシーン作る部分(略)
// ~~
// uniform
const uniforms = {
time: {
type: "f",
value: 0.0,
},
texture: {
type: "t",
// 1. のテキストをテクスチャにする関数の使い所さん
value: createTexture({ textureHeight: 1024, textureWidth: 2048, dpr }),
},
offsetHorizontal: {
type: "v2v",
value: offset.vector2Horizontal,
},
offsetVertical: {
type: "v2v",
value: offset.vector2Vertical,
},
weightHorizontal: {
type: "fv1",
value: weight.horizontal,
},
weightVertical: {
type: "fv1",
value: weight.vertical,
},
isVertical: {
type: "b",
value: false,
},
};
// ~~
// シーンのレンダリング,リサイズ関数の呼び出し(略)
// ~~
};
参考(2)の方法で createTexture() したテクスチャに Gauss フィルタをかける準備として,重みとオフセットを計算している.結構冗長な気がするが配列にして渡したいのでこれ以外自分でも思いつかなかった.
3. GLSL 書いて色合いの調整
code:frag.glsl
// snoise の記述
float minBright = 0.1;
uniform bool isVertical;
void main() {
vec2 uv = vUv;
vec4 color = vec4(0.085);
// 背景
color.r = snoise(vec3(uv.x, uv.y, time * 0.5)) * 0.4;
color.b = snoise(vec3(uv.x, uv.y, time * 0.5)) * 0.4;
// ネオン管(bloom)
if (isVertical) {
for (int i = 0; i < SAMPLE_COUNT; i++) {
color += texture2D(texture, uv + offsetVerticali) * weightVerticali; }
} else {
for (int i = 0; i < SAMPLE_COUNT; i++) {
color += texture2D(texture, uv + offsetHorizontali) * weightHorizontali; }
}
vec4 texel = vec4(0.0);
// 高輝度部の抽出
if (mod(time * snoise(vec3(time * 0.2)), 5.0) > 3.0) {
minBright = 0.5;
} else {
minBright = 0.05;
}
texel = vec4(max(vec3(0.0), (texture2D(texture, uv) - minBright).rgb), 1.0);
// 乗算
texel += color;
gl_FragColor = texel;
}
JS 側からうまく int 型の数値渡せなかったのでとりあえずマクロにした.
ここではJS 側で計算したオフセットに基づいて重みを足し込んでいっている.
高輝度部の抽出はネオンランプが点滅する表現で使った.mod()以外でやるともっと面白い感じになるのかもしれない.
背景のピンクがかった靄は 3Dノイズ で作っている.License : Copyright (C) 2011 Ashima Arts. All rights reserved. のアレ.
他に,Google Fonts が読み込めるまで Canvas を表示しないように WebFontLoader 使って表示非表示切り替えたりしている.evilな実装ですね
code:index.tsx
WebFontLoader.load({
google: {
},
active: () => setActive(true),
});
👁参考
Three.js の postprocessing に一応 bloom があった
が,使いたくない(なんか THREE の postprocessing 全般めっちゃ重くなる,書き方が悪いんか...?)
なのでどうにか参考に出来るものを他に探した
これは普通の画像をテクスチャにして bloom かけている
じゃあ THREE.CanvasTexture でテクスチャ作れば出来そう → 出来た
https://gyazo.com/ded2f68380dba56e5f3f878e6c678a0c
ので、後は GLSL 側に Canvas の文字テクスチャ渡す部分ごにょごにょ
WebGL だと JS 側から bloom の offset を Float32Array でシェーダー側に渡しているのを THREE.Vector2 に書き換えるような感じ.人がやる作業じゃない
今回のネオンライトには水平方向の Gauss フィルタしか使わなかった
垂直方向入れるとなんかぼやけすぎた,目コンパイル
テキストからパスなんかそんなもん取れなくてマジで焦った
参考記事の方法でオフセット作って解決
👋その他
隠さなくてもちゃんと Cyberpunk 2077 の影響受けてる
幼子にパチンコとかの光見せながら育てるとこうなる
https://gyazo.com/35185d48a083fa5eb08d918f65bc638e
WebGL で 2DCG やってると CSS で出来るじゃんとか言われたら負けだな~って思った
PIXI.js ......
h_doxas さんに触れてもらえたのがかなり嬉しかった
2021年も頑張りましょう