株式会社グローバルゲート公式ブログ
(こんな格好で仕事をしているわけではありません)
こんにちは、株式会社グローバルゲートのモーリーです。
先日大阪万博に行ってきました!
あまり下調べもせずイベントやパビリオンの予約もせずに行きましたが、それでも個性的なパビリオンや賑やかな会場を歩いているだけでその非日常感にワクワクさせられました。
15km前後は歩くことになると思いますので、これから行かれる方は靴や水分など体調にはくれぐれもご注意ください。
(会場全体はUSJの3倍近くあるそうです)
さて、万博のことは一旦置いておいて、今記事では2DグラフィックスライブラリであるPixiJSをご紹介したいと思います。
PixiJSとはWebブラウザ上で高速かつ高品質な2Dグラフィックスを描画するためのJavaScriptライブラリです。主にHTML5の<canvas>要素やWebGLを活用して、ゲームやアニメーション、インタラクティブな演出をスムーズに表現することができます。
ブラウザでアニメーションや図形描画を行うにはcanvasやSVGをJavaScriptで操作する必要があります。しかし、これらは複雑なアニメーションや大量のオブジェクトを行おうとすると、コードが煩雑になったりパフォーマンスに限界が生じたりします。CSSによるアニメーションはできることに限界があります。
PixiJSはその煩雑さをシンプルなコードで実装し、更にWebGLによる表示によって高いパフォーマンスを発揮します。
PixiJSを使うことで、低レベル(ブラウザの根本に近い機能)のWebGL APIを抽象化し、より簡単かつ効率的に2D描画を実現することができます。たとえば、スプライト(画像を表示するオブジェクト)、テキスト、シェイプ(図形)、フィルター効果、マスク処理など、ゲームや視覚演出でよく使われる機能がすべて備わっています。
PixiJSは純粋な2Dエンジンであるため3D処理は行いませんが、その分2D表現に特化しており、非常に軽量かつ高速です。そのため、カジュアルゲームから広告バナー、アニメーション付きUI、ビジュアライゼーションまで、さまざまなプロジェクトで活用されています。
実際にPixiJSは、企業のキャンペーンサイトやスマホゲームのブラウザ版、デジタルサイネージなどでも使われており、プロの現場でも多く使われているライブラリのひとつです。
それでは、実際にPixiJSを使ってみましょう。
PixiJSによる描画は
1.ステージとなるcanvasの配置
2.ステージ上にオブジェクトを配置
3.オブジェクトにアニメーションやフィルター処理を追加
という手順で行います。
まずはライブラリのロードを行います。
今回はモダンブラウザで使えるimport構文を使ってみました。
<script type="module">
import * as PIXI from 'https://cdn.jsdelivr.net/npm/pixi.js@8.10.0/dist/pixi.min.mjs';
</script>
はじめにPIXIの初期化と土台となるcanvasの設置を行います。
async function main() {
//PIXIの初期化
const app = new PIXI.Application();
await app.init({
resizeTo: window,
backgroundColor: 0xffffff,
antialias: true,
});
//appendChildでcanvasを設置
document.body.appendChild(app.view);
}
main();
DEMO
画面上には何も表示されていないように見えますが、canvasがHTML上に設置されています。
canvas上にオブジェクトを設置します。
今回は簡単に円を配置してみます。
//円を配置
const circle = new PIXI.Graphics();
circle.beginFill(0xff0000); // 塗りつぶし色(16進数)
circle.drawCircle(0, 0, 50); // (x, y, 半径)
circle.endFill();
// 画面中央に配置
circle.x = app.screen.width / 2;
circle.y = app.screen.height / 2;
//stageへのappendChildでオブジェクトを配置
app.stage.addChild(circle);
DEMO
画面中央に赤い円が表示されました。
次にこの円を動かしてみます。
// 移動速度
let speed = 3;
// 毎フレーム実行
app.ticker.add(() => {
circle.x += speed;
// 画面端で反転
if (circle.x + radius >= app.screen.width || circle.x - radius <= 0) {
speed *= -1;
}
});
// ウィンドウリサイズ時にも対応
window.addEventListener('resize', () => {
if (circle.x + radius > app.screen.width) {
circle.x = app.screen.width - radius;
}
if (circle.x - radius < 0) {
circle.x = radius;
}
});
DEMO
これで丸が動くアニメーションが作成できました。
これだけならCSSとHTMLだけでもできそうですが、更に複雑なアニメーションに挑戦したいと思います。
この丸を10000個配置してみましょう。
HTML/CSS/Javascriptならコマ落ちやブラウザのフリーズもありそうですが、果たしてどうでしょうか。
//PixiJS v8.10.0をCDNからインポート
import * as PIXI from 'https://cdn.jsdelivr.net/npm/pixi.js@8.10.0/dist/pixi.min.mjs';
async function main() {
//PIXIの初期化
const app = new PIXI.Application();
await app.init({
resizeTo: window,
backgroundColor: 0x111111,
antialias: true,
});
//appendChildでcanvasを設置
document.body.appendChild(app.view);
//1万個チャレンジ
const NUM_CIRCLES = 10000;
const RADIUS = 4;
// 円の配列と速度ベクトルの配列
const circles = [];
const velocities = [];
for (let i = 0; i < NUM_CIRCLES; i++) {
// ランダム色
const color = Math.floor(Math.random() * 0xffffff);
const g = new PIXI.Graphics();
g.beginFill(color);
g.drawCircle(0, 0, RADIUS);
g.endFill();
// ランダム初期位置
g.x = Math.random() * app.screen.width;
g.y = Math.random() * app.screen.height;
app.stage.addChild(g);
circles.push(g);
// ランダム速度ベクトル (-1〜1)
const angle = Math.random() * Math.PI * 2;
const speed = 1 + Math.random() * 1.5; // 少し速度にバラつき
velocities.push({
dx: Math.cos(angle) * speed,
dy: Math.sin(angle) * speed,
});
}
// アニメーション処理
app.ticker.add(() => {
for (let i = 0; i < NUM_CIRCLES; i++) {
const g = circles[i];
const v = velocities[i];
g.x += v.dx;
g.y += v.dy;
// 画面端で反射
if (g.x - RADIUS <= 0 || g.x + RADIUS >= app.screen.width) {
v.dx *= -1;
}
if (g.y - RADIUS <= 0 || g.y + RADIUS >= app.screen.height) {
v.dy *= -1;
}
}
});
}
main();
DEMO
1万個の円が表示されました!
私の環境ではスムーズに動いているように見えますが、皆様の環境ではどうでしょうか?
丸が接触するときにメタボールのように融合と分離をする処理を足してみましょう。
これはCSSだけでは表現が難しい動きです。
メタボールのフィルタ処理部分がかなり難しかったのでChatGPTに頼りました。
import * as PIXI from 'https://cdn.jsdelivr.net/npm/pixi.js@8.10.0/dist/pixi.min.mjs';
async function main() {
const app = new PIXI.Application();
await app.init({
resizeTo: window,
backgroundColor: 0xeeeeee,
antialias: true,
});
document.body.appendChild(app.view);
const BALL_COUNT = 200;
const makeBalls = () => {
const balls = [];
for (let i = 0; i < BALL_COUNT; i++) {
balls.push({
x: Math.random() * app.screen.width,
y: Math.random() * app.screen.height,
vx: (Math.random() - 0.5) * 4,
vy: (Math.random() - 0.5) * 4,
radius: 1 + Math.random() * 20,
});
}
return balls;
};
const ballsRed = makeBalls();
const ballsBlue = makeBalls();
const fragShader = (colorVec) => `precision mediump float;
uniform vec2 u_resolution;
uniform float u_threshold;
uniform vec3 u_balls[${BALL_COUNT}];
void main() {
vec2 uv = gl_FragCoord.xy;
float field = 0.0;
for (int i = 0; i < ${BALL_COUNT}; i++) {
vec3 b = u_balls[i];
float dx = uv.x - b.x;
float dy = uv.y - b.y;
float d2 = dx*dx + dy*dy + 0.0001;
field += (b.z * b.z) / d2;
}
if (field > u_threshold) {
gl_FragColor = vec4${colorVec};
} else {
discard;
}
}
`;
const vertex = `in vec2 aPosition;
out vec2 vTextureCoord;
uniform vec4 uInputSize;
uniform vec4 uOutputFrame;
uniform vec4 uOutputTexture;
vec4 filterVertexPosition(void) {
vec2 position = aPosition * uOutputFrame.zw + uOutputFrame.xy;
position.x = position.x * (2.0 / uOutputTexture.x) - 1.0;
position.y = position.y * (2.0 * uOutputTexture.z / uOutputTexture.y) - uOutputTexture.z;
return vec4(position, 0.0, 1.0);
}
vec2 filterTextureCoord(void) {
return aPosition * (uOutputFrame.zw * uInputSize.zw);
}
void main(void) {
gl_Position = filterVertexPosition();
vTextureCoord = filterTextureCoord();
}
`;
// フィルター作成関数(色をパラメータに)
const createMetaFilter = (colorVec, name) => {
return PIXI.Filter.from({
gl: {
vertex: vertex,
fragment: fragShader(colorVec),
},
resources: {
[name]: {
u_resolution: {value: [app.screen.width, app.screen.height], type: 'vec2<f32>'},
u_threshold: {value: 1.0, type: 'f32'},
u_balls: {value: new Float32Array(BALL_COUNT * 3), type: 'vec3<f32>', size: BALL_COUNT},
}
}
});
};
const filterBlue = createMetaFilter('(0.0, 0.3, 0.8, 1.0)', 'blueUniforms');
const filterRed = createMetaFilter('(1.0, 0.0, 0.0, 1.0)', 'redUniforms');
const quadBlue = new PIXI.Sprite(PIXI.Texture.WHITE);
quadBlue.width = app.screen.width;
quadBlue.height = app.screen.height;
quadBlue.filters = [filterBlue];
app.stage.addChild(quadBlue); // 背景レイヤー
const quadRed = new PIXI.Sprite(PIXI.Texture.WHITE);
quadRed.width = app.screen.width;
quadRed.height = app.screen.height;
quadRed.filters = [filterRed];
app.stage.addChild(quadRed); // 前景レイヤー
app.ticker.add((ticker) => {
// 位置更新(赤)
for (const b of ballsRed) {
b.x += b.vx * ticker.deltaTime;
b.y += b.vy * ticker.deltaTime;
if (b.x < b.radius || b.x > app.screen.width - b.radius) b.vx *= -1;
if (b.y < b.radius || b.y > app.screen.height - b.radius) b.vy *= -1;
}
// 位置更新(青)
for (const b of ballsBlue) {
b.x += b.vx * ticker.deltaTime;
b.y += b.vy * ticker.deltaTime;
if (b.x < b.radius || b.x > app.screen.width - b.radius) b.vx *= -1;
if (b.y < b.radius || b.y > app.screen.height - b.radius) b.vy *= -1;
}
// uniform更新(赤)
const uRed = filterRed.resources.redUniforms.uniforms;
for (let i = 0; i < BALL_COUNT; i++) {
uRed.u_balls[i * 3 + 0] = ballsRed[i].x;
uRed.u_balls[i * 3 + 1] = ballsRed[i].y;
uRed.u_balls[i * 3 + 2] = ballsRed[i].radius;
}
// uniform更新(青)
const uBlue = filterBlue.resources.blueUniforms.uniforms;
for (let i = 0; i < BALL_COUNT; i++) {
uBlue.u_balls[i * 3 + 0] = ballsBlue[i].x;
uBlue.u_balls[i * 3 + 1] = ballsBlue[i].y;
uBlue.u_balls[i * 3 + 2] = ballsBlue[i].radius;
}
});
// リサイズ対応
window.addEventListener('resize', () => {
quadBlue.width = app.screen.width;
quadBlue.height = app.screen.height;
filterBlue.resources.blueUniforms.uniforms.u_resolution = [app.screen.width, app.screen.height];
quadRed.width = app.screen.width;
quadRed.height = app.screen.height;
filterRed.resources.redUniforms.uniforms.u_resolution = [app.screen.width, app.screen.height];
});
}
main();
DEMO
無事冒頭の伏線(万博行ってきましたレポ)を回収できました!
AIをプログラムで活用しようとすると、従来のプログラミング能力というよりやりたいことを言語化する能力が必要だと感じました(たとえば球体が変形しながら合成される形状を「メタボール」や「ブロブ」と呼ぶことを知っていないとAIに指示を出せない、など)
ということで、今回はPixiJSの紹介と簡単な描画やアニメーションの作成を試してみました。
特にWebGLによる滑らかなアニメーションが実装できる点は非常に心強く、CSSとJavascriptでは負荷が高すぎて諦めていた表現にも挑戦できそうです。
当社によるWeb制作でも新たな表現方法として活用していきたいと思います。
【関連記事】
カテゴリー
月別アーカイブ
ブログ内検索
執筆メンバーについて
モーリー
Webデザイナー。
当サイトのデザインと管理も担当しています。
ナミー
Webディレクター。
本社制作部の紅一点。お客様に寄り添った提案を心かげています。
タカ
サーバーエンジニア。
Webサイトにとってサーバーは命、ネットワークは血液です。Webサイトの安定稼働のために日夜注力しています。
たっくん
ITアドバイザー
Webサイトの活用方法からオフィスのネットワーク整備まで、多角的にITの活用方法をご案内させていただきます。
ノーさん
制作部ディレクター。
業種を問わず多くのお客様を担当させていただきました。Webサイトのお悩み、活用方法などぜひご相談ください。
カン
制作部デザイナー。
制作部最年少の若手ですが、だからこそ生まれるアイデア・発想にご期待ください。