Skip to content
React Native Skia ガイドブック
Esc
navigateopen⌘Jpreview
On this page

テクスチャアニメーション

React要素をオフスクリーンで1枚のSkImageに焼き込むテクスチャの仕組みと、useTexture・useImageAsTexture・usePictureAsTextureの使い分けを解説します。

ここまでのアニメーションは、<Circle><Path>のpropsを毎フレーム再計算するものでした。図形がシンプルなうちは問題になりませんが、複雑な描画(グラデーション・複数のシェイプの組み合わせ・テキストなど)を毎フレーム丸ごと再計算するのはコストが高くなります。

そこで使うのがテクスチャ(texture)です。React要素を一度だけオフスクリーンでラスタライズして1枚のSkImageにしておき、それをGPU上のテクスチャとして使い回します。以降は「テクスチャそのものを動かす」だけで済むので、中身の再計算が要りません。

useTexture: React要素をテクスチャにする

useTextureは、React要素と出力サイズを渡すと、その描画結果を共有値(Shared Value)として返すフックです。

import { useTexture, Image, Canvas, Fill } from "@shopify/react-native-skia";

const Demo = () => {
  const texture = useTexture(<Fill color="cyan" />, { width: 256, height: 256 });
  return (
    <Canvas style={{ flex: 1 }}>
      <Image image={texture} x={0} y={0} width={256} height={256} />
    </Canvas>
  );
};

戻り値のtextureは共有値なので、前々章で見た通り<Image>imagepropにそのまま渡せます。

次のデモでは、星形のパスを一度だけテクスチャに焼き込み、回転・拡大縮小させています。星のパス自体はアニメーション中に一切再計算されていません。

Skiaを読み込み中…

本来なら<Group transform={transform}><Image .../></Group>useDerivedValueだけで、前章<Circle cx={cx} cy={cy}>と同じように書けるはずです。ただしこのプロジェクトのWeb版レンダラーには、<Group>transformで包んだ<Image>の再描画が反映されないという既知の制約があります(詳しくは前章の「注意点」を参照)。そのためこのデモでは、requestAnimationFrameで毎フレームSkPictureを作り直す方式を使っています。

const StarTexture = () => <Path path={starPath} color="#f5c451" />;
const recorder = Skia.PictureRecorder();

const TextureSpinDemo = () => {
  const texture = useTexture(<StarTexture />, { width: 96, height: 96 });
  const clock = useClock();
  const [picture, setPicture] = useState<SkPicture | null>(null);

  useEffect(() => {
    let frame: number;
    const tick = () => {
      if (texture.value) {
        const t = clock.value / 1000;
        const scale = 0.8 + Math.sin(t * 2) * 0.15;
        const canvas = recorder.beginRecording(Skia.XYWHRect(0, 0, 240, 240));
        canvas.save();
        canvas.translate(120, 120);
        canvas.rotate((t * 180) / Math.PI, 0, 0);
        canvas.scale(scale, scale);
        canvas.drawImage(texture.value, -48, -48);
        canvas.restore();
        setPicture(recorder.finishRecordingAsPicture());
      }
      frame = requestAnimationFrame(tick);
    };
    frame = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(frame);
  }, [clock, texture]);

  return (
    <Canvas style={{ flex: 1 }}>
      {picture && <Picture picture={picture} />}
    </Canvas>
  );
};

実機・ネイティブでは、この回避策は不要です。<Group transform={transform}><Image image={texture} .../></Group>useDerivedValueをそのまま使ってください。

useImageAsTexture・usePictureAsTexture

useTexture以外にも、テクスチャを作る手段が2つ用意されています。

フック 入力 用途
useTexture React要素 JSXで組んだ描画内容をそのままテクスチャに
useImageAsTexture 画像ソース(requireなど) 画像ファイルをGPUにアップロードしてテクスチャに
usePictureAsTexture SkPicture 命令的なAPIやReactツリーの外で作った描画命令をテクスチャに
// 画像ファイルをそのままテクスチャにする
const texture = useImageAsTexture(require("./assets/image.png"));
// SkPictureからテクスチャを作る
const rec = Skia.PictureRecorder();
const canvas = rec.beginRecording();
canvas.drawColor(Skia.Color("cyan"));
const picture = rec.finishRecordingAsPicture();

const texture = usePictureAsTexture(picture, { width: 200, height: 200 });

いずれも戻り値は共有値で、<Image>にそのまま渡せる点は共通です。下のデモは、useTextureuseImageAsTextureusePictureAsTextureの3つを並べ、それぞれ異なる入力(React要素・画像ファイル・SkPicture)から同じように<Image>で表示できることを示しています。

Skiaを読み込み中…

内部の仕組み: runOnUI

これらのフックは内部で、ReanimatedのrunOnUIを使ってUIスレッド上にオフスクリーンサーフェスを作り、そこに描画してからテクスチャとして取り出しています。

const createTexture = (image) => {
  "worklet";
  const surface = Skia.Surface.MakeOffscreen(200, 200)!;
  const canvas = surface.getCanvas();
  canvas.drawColor(Skia.Color("cyan"));
  surface.flush();
  image.value = surface.makeImageSnapshot();
};

const image = useSharedValue(null);
useEffect(() => {
  runOnUI(createTexture)(image);
}, []);

useTextureなどの高レベルフックを使っていれば、このあたりを自分で書く必要はほとんどありません。ただし「テクスチャがどこで作られているか」を理解しておくと、<Atlas>と組み合わせて大量のスプライトを動かす(第2部の該当章を参照)ような、より高度な使い方にも応用が効きます。

注意点

テクスチャは「一度作ったら描画内容が変わらないもの」に向いています。中身が毎フレーム変わるような描画には不向きで、その場合は素直に通常のコンポーネントとして描いたほうがシンプルです。パーティクルやスプライトのように、同じ絵を大量に、位置や角度だけ変えて繰り返し描く場面でこそ効果を発揮します。

公式ドキュメントのTexturesも参照してください。