特殊な図形
大量のスプライトを一括描画するAtlas、三角形メッシュで描くVertices、Coonsパッチを描くPatchという、パフォーマンス寄りの3つの図形コンポーネントを解説します。
左からAtlas、Vertices、Patch。どれも大量の処理や補間をGPUへまとめて渡すための形です。
ここまでの図形はどれも「1つの形をどう描くか」がテーマでした。この章で扱う<Atlas> <Vertices> <Patch>は少し毛色が違い、大量の描画や特殊な塗りをGPU上で効率よく処理するための低レベルなプリミティブです。パフォーマンスに直結する分、実機での挙動確認をおすすめします。
Atlas: 大量のスプライトを一括描画する
<Atlas>は、同じテクスチャや画像を大量にインスタンス化して描くためのコンポーネントです。タイルマップやスプライトアニメーションのように、似た見た目のオブジェクトを多数、それぞれ異なる変形で描きたい場面に向いています。
| Name | Type | Description |
|---|---|---|
| image | SkImage | null |
スプライトを含むアトラス画像 |
| sprites | SkRect[] |
アトラス画像内での各スプライトの位置 |
| transforms | RSXform[] |
各スプライトに適用する回転・スケール変形 |
| colors? | SkColor[] |
スプライトに合成する色 |
| colorBlendMode? | BlendMode |
colorsをスプライトに合成するブレンドモード。デフォルトはdstOver |
| blendMode? | BlendMode |
Atlas全体をCanvasに合成する際のブレンドモード |
| sampling? | Sampling |
画像のサンプリング方法 |
RSXform: 位置・回転・スケールを1つにまとめた変形
transformsに渡すRSXformは、回転・スケール・平行移動を1つの軽量な構造にまとめたものです。フル変形行列よりデータ量が小さく、大量のスプライトを毎フレーム動かす用途に適しています。
import { Skia } from "@shopify/react-native-skia";
// 恒等変形(何もしない)
let form = Skia.RSXform(1, 0, 0, 0);
// 2倍スケールして(50, 100)へ平行移動
form = Skia.RSXform(2, 0, 50, 100);
// PI/4回転して(50, 100)へ平行移動(原点は(0, 0)固定)
const r = Math.PI / 4;
form = Skia.RSXform(Math.cos(r), Math.sin(r), 50, 100);
// 2倍スケール・PI/4回転、ピボットは(25, 25)
form = Skia.RSXformFromRadians(2, r, 0, 0, 25, 25);
Skia.RSXformはピボットが原点固定、Skia.RSXformFromRadiansはピボット座標を指定できる版です。
Hello World
以下は、1枚の矩形画像を150個、それぞれ中心を向くように回転させて並べる例です。
import { Atlas, Canvas, Group, Rect, Skia, drawAsImage, rect } from "@shopify/react-native-skia";
const size = { width: 25, height: 11.25 };
const strokeWidth = 2;
const imageSize = { width: size.width + strokeWidth, height: size.height + strokeWidth };
const image = await drawAsImage(
<Group>
<Rect rect={rect(strokeWidth / 2, strokeWidth / 2, size.width, size.height)} color="cyan" />
<Rect
rect={rect(strokeWidth / 2, strokeWidth / 2, size.width, size.height)}
color="blue"
style="stroke"
strokeWidth={strokeWidth}
/>
</Group>,
imageSize
);
const AtlasDemo = () => {
const numberOfBoxes = 150;
const pos = { x: 128, y: 128 };
const width = 256;
const sprites = new Array(numberOfBoxes).fill(0).map(() => rect(0, 0, imageSize.width, imageSize.height));
const transforms = new Array(numberOfBoxes).fill(0).map((_, i) => {
const tx = 5 + (i * size.width) % width;
const ty = 25 + Math.floor(i / (width / size.width)) * size.width;
const angle = Math.atan2(pos.y - ty, pos.x - tx);
return Skia.RSXform(Math.cos(angle), Math.sin(angle), tx, ty);
});
return (
<Canvas style={{ flex: 1 }}>
<Atlas image={image} sprites={sprites} transforms={transforms} />
</Canvas>
);
};
drawAsImageは、渡したコンポーネントツリーをその場でラスタライズして1枚のSkImageにするヘルパーです。ここで作った矩形+枠線の画像を、150個ぶんのsprites(アトラス内の切り出し位置)とtransforms(個々の配置)で並べています。
以下のデモは上のコードとほぼ同じもので、targetXを動かすと150個のスプライトが向きを変えて追従します。各スプライトのRSXformが毎レンダリングで再計算される様子を確認できます。
Reanimatedとの組み合わせ
<Atlas>は基本的にReanimatedと組み合わせて使います。useTextureフックでUIスレッド上に直接テクスチャを作り、useRSXformBufferのようなバッファ系フックで変形をworklet内から更新すれば、JSスレッドをまたがずに大量のスプライトを毎フレーム動かせます。
const transforms = useRSXformBuffer(numberOfBoxes, (val, i) => {
"worklet";
const tx = 5 + (i * size.width) % width;
const ty = 25 + Math.floor(i / (width / size.width)) * size.width;
const angle = Math.atan2(pos.value.y - ty, pos.value.x - tx);
val.set(Math.cos(angle), Math.sin(angle), tx, ty);
});
ジェスチャーでpos(共有値)を更新するだけで、150個のスプライトの向きがすべてUIスレッド上で再計算されます。worklet・共有値の詳細は第5部で扱います。
Vertices: 三角形メッシュで描く
<Vertices>は、頂点の集合を三角形メッシュとして描画するコンポーネントです。頂点ごとに色やテクスチャ座標を指定できるため、グラデーションメッシュや画像の歪み(ワーピング)表現に使えます。
| Name | Type | Description |
|---|---|---|
| vertices | Point[] |
描画する頂点 |
| mode? | VertexMode |
triangles・triangleStrip・triangleFan。デフォルトはtriangles |
| indices? | number[] |
三角形を構成する頂点のインデックス。省略時はverticesの並び順がそのまま使われる |
| textures | Point[] |
テクスチャマッピング用の座標。塗りとして渡されたシェーダーがテクスチャになる |
| colors? | string[] |
各頂点に対応する色 |
| blendMode? | BlendMode |
colors指定時にPaintの色と合成するブレンドモード。colors指定時はdstOver、未指定時はsrcOverがデフォルト |
テクスチャマッピング
import { Canvas, Group, ImageShader, Vertices, useImage, vec } from "@shopify/react-native-skia";
const VerticesDemo = () => {
const image = useImage(require("./assets/squares.png"));
const vertices = [vec(64, 0), vec(128, 256), vec(0, 256)];
const colors = ["#61dafb", "#fb61da", "#dafb61"];
const textures = [vec(0, 0), vec(0, 128), vec(64, 256)];
if (!image) return null;
return (
<Canvas style={{ flex: 1 }}>
<Group>
<ImageShader image={image} tx="repeat" ty="repeat" />
{/* colorsを指定: デフォルトのblendModeはdstOver */}
<Vertices vertices={vertices} colors={colors} />
<Group transform={[{ translateX: 128 }]}>
{/* textures指定: デフォルトのblendModeはsrcOver */}
<Vertices vertices={vertices} textures={textures} />
</Group>
</Group>
</Canvas>
);
};
<ImageShader>で作ったシェーダーがGroup配下に継承され、それが<Vertices>のテクスチャとして使われます。左側の三角形はcolorsで頂点ごとに着色、右側はtexturesで画像をマッピングしています。
indicesで頂点を使い回す
4頂点の矩形から2つの三角形を作る例です。indicesを使うと、頂点を重複させずに複数の三角形を定義できます。
import { Canvas, Vertices, vec } from "@shopify/react-native-skia";
const vertices = [vec(0, 0), vec(256, 0), vec(256, 256), vec(0, 256)];
const colors = ["#61dafb", "#fb61da", "#dafb61", "#61fbcf"];
const triangle1 = [0, 1, 2]; // 左上・右上・右下
const triangle2 = [0, 2, 3]; // 左上・右下・左下
const IndicesDemo = () => {
return (
<Canvas style={{ flex: 1 }}>
<Vertices vertices={vertices} colors={colors} indices={[...triangle1, ...triangle2]} />
</Canvas>
);
};
以下のデモでmodeを切り替えると、前節のcolors/texturesによるテクスチャマッピングと、このindicesによる頂点の使い回しの両方を確認できます。
Patch: Coonsパッチを描く
<Patch>はCoonsパッチを描くコンポーネントです。4辺すべてを3次ベジェ曲線で定義した、曲面を持つ矩形のようなものだと考えてください。頂点ごとに色を指定すると、曲がった境界を持つ滑らかなグラデーションが作れます。
| Name | Type | Description |
|---|---|---|
| cubics | CubicBezier[4] |
左上から時計回りに4つの3次ベジェ曲線を指定。各曲線は4つ目ごとの点を共有し、最後の曲線は最初の点に戻る |
| textures | Point[] |
テクスチャマッピング用の座標 |
| colors? | string[] |
各角に対応する色 |
| blendMode? | BlendMode |
colors指定時のブレンドモード。デフォルトはdstOver(未指定時はsrcOver) |
import { Canvas, Patch, vec } from "@shopify/react-native-skia";
const PatchDemo = () => {
const colors = ["#61dafb", "#fb61da", "#61fbcf", "#dafb61"];
const C = 64;
const width = 256;
const topLeft = { pos: vec(0, 0), c1: vec(0, C), c2: vec(C, 0) };
const topRight = { pos: vec(width, 0), c1: vec(width, C), c2: vec(width + C, 0) };
const bottomRight = { pos: vec(width, width), c1: vec(width, width - 2 * C), c2: vec(width - 2 * C, width) };
const bottomLeft = { pos: vec(0, width), c1: vec(0, width - 2 * C), c2: vec(-2 * C, width) };
return (
<Canvas style={{ flex: 1 }}>
<Patch colors={colors} patch={[topLeft, topRight, bottomRight, bottomLeft]} />
</Canvas>
);
};
各角(pos)とその両側の制御点(c1 c2)を時計回りに4つ定義すると、角ごとのcolorsがなめらかに補間された曲面が描かれます。
<Atlas> <Vertices> <Patch>はいずれもGPUのプリミティブに近い低レベルAPIで、通常の<Circle>や<Path>とは設計思想が異なります。大量描画や特殊なシェーディングが必要になったときの選択肢として覚えておいてください。