シェーダー入門とSkSL
SkSL(Skia Shading Language)の基本構文と、Skia.RuntimeEffect.Makeによるシェーダーのコンパイル、Shaderコンポーネントとuniformの使い方をコード例中心に解説します。
シェーダーは時間・色・座標・画像などを受け取り、各ピクセルの色を並列に決めます。
ここまでの章で登場したBlur・Shadow・グラデーションといったエフェクトは、すべて内部的にはシェーダー(shader)という仕組みで実装されています。この章では、自分で書ける汎用シェーダー言語であるSkSL(Skia Shading Language)の基本構文を、コード例を中心に見ていきます。
SkSLの構文はGLSLとよく似ています。ブラウザで触りながら試せるshaders.skia.orgや、GLSLとの差分をまとめた公式のsksl READMEも参考になります。
シェーダーをコンパイルする
SkSLのコードは文字列として書き、Skia.RuntimeEffect.Makeでコンパイルします。コンパイルに失敗するとnullが返るので、エラーハンドリングが必要です。
import { Skia } from "@shopify/react-native-skia";
const source = Skia.RuntimeEffect.Make(`
vec4 main(vec2 pos) {
// このキャンバスは256x256
vec2 canvas = vec2(256);
// 0〜1に正規化したx,y座標
vec2 normalized = pos / canvas;
return vec4(normalized.x, normalized.y, 0.5, 1);
}`);
if (!source) {
throw new Error("シェーダーのコンパイルに失敗しました");
}
すべてのSkSLシェーダーはmain関数を持ち、引数として現在描画中のピクセル座標(vec2 pos)を受け取り、そのピクセルの色(vec4、RGBA各0〜1)を返します。この最小限の例では、x座標を赤(R)、y座標を緑(G)にマッピングしているので、左上が黒に近く、右下が黄色に近いグラデーションになります。
Shader: コンパイル結果を描画に使う
コンパイルしたsourceはShaderコンポーネントに渡して使います。Shaderはシェーダーの一種なので、FillやCircleなどの塗りとして機能します。
| Name | Type | Description |
|---|---|---|
| source | RuntimeEffect |
コンパイル済みシェーダー |
| uniforms | { [name: string]: number | Vector | Vector[] | number[] | number[][] } |
uniform変数の値 |
| children | Shader |
uniform shaderとして渡す別のシェーダー |
import { Skia, Canvas, Shader, Fill } from "@shopify/react-native-skia";
const source = Skia.RuntimeEffect.Make(`
vec4 main(vec2 pos) {
vec2 normalized = pos / vec2(256);
return vec4(normalized.x, normalized.y, 0.5, 1);
}`)!;
const SimpleShader = () => {
return (
<Canvas style={{ width: 256, height: 256 }}>
<Fill>
<Shader source={source} />
</Fill>
</Canvas>
);
};
posをキャンバスサイズで正規化してそのままR・Gチャンネルに使うと、左上が黒に近く右下が黄色に近いグラデーションになります。
uniform: 外から値を渡す
uniformは、JavaScript側からシェーダーにパラメーターを渡すための変数です。Reactのpropsのように、毎フレーム異なる値を渡してシェーダーの見た目を動的に変えられます。対応する型はfloat・float2・float3・float4・float2x2・float3x3・float4x4・int・int2・int3・int4とそれらの配列(uniform float3 colors[12]のように書く)です。
import { Canvas, Skia, Shader, Fill, vec } from "@shopify/react-native-skia";
const source = Skia.RuntimeEffect.Make(`
uniform vec2 c;
uniform float r;
uniform float blue;
vec4 main(vec2 pos) {
vec2 normalized = pos / vec2(2 * r);
return distance(pos, c) > r ? vec4(1) : vec4(normalized, blue, 1);
}`)!;
const UniformShader = () => {
const r = 128;
const c = vec(2 * r, r);
const blue = 1.0;
return (
<Canvas style={{ width: 256, height: 256 }}>
<Fill>
<Shader source={source} uniforms={{ c, r, blue }} />
</Fill>
</Canvas>
);
};
distance(pos, c) > rは、現在のピクセルが円の外側にあるかどうかの判定です。外側なら白(vec4(1))、内側ならx・y座標由来の色とblueuniformを組み合わせた色を返します。以下のデモではblueをスライダーで操作でき、JavaScript側の状態変化がそのままシェーダーの計算結果に反映される様子を確認できます。
uniformspropに渡すオブジェクトのキー名は、SkSLコード内のuniform変数名と一致させる必要があります。型が合っていない場合(たとえばvec2のuniformに単一の数値を渡すなど)はランタイムエラーになるので注意してください。
ネストしたシェーダー: 画像を入力にする
uniform shaderという特別な型を使うと、別のシェーダー(画像やグラデーションなど)を入力として受け取れます。Shaderコンポーネントの子要素に別のシェーダー系コンポーネントを渡すことで、このuniform shaderにバインドされます。
import { Canvas, Skia, ImageShader, Shader, Fill, useImage } from "@shopify/react-native-skia";
const source = Skia.RuntimeEffect.Make(`
uniform shader image;
half4 main(float2 xy) {
xy.x += sin(xy.y / 3) * 4;
return image.eval(xy).rbga;
}`)!;
const NestedShader = () => {
const image = useImage(require("./assets/photo.jpg"));
if (!image) {
return null;
}
return (
<Canvas style={{ width: 256, height: 256 }}>
<Fill>
<Shader source={source}>
<ImageShader
image={image}
fit="cover"
rect={{ x: 0, y: 0, width: 256, height: 256 }}
/>
</Shader>
</Fill>
</Canvas>
);
};
image.eval(xy)で任意の座標の色を取得できるので、xy自体を書き換えてからevalすれば、波打つような歪みも簡単に作れます。.rbgaのようにチャンネルの並びを入れ替えるスウィズル(swizzle)記法もGLSL・SkSL共通の書き方です。以下のデモでは、amplitude(歪みの強さ)とfrequency(波の細かさ)をuniformとしてスライダーから操作できます。
主な組み込み関数
SkSLではGLSL同様、次のような組み込み関数がよく使われます。
mix(a, b, t):aとbをt(0〜1)で線形補間するclamp(x, min, max):xをmin〜maxの範囲に収めるsmoothstep(edge0, edge1, x):edge0〜edge1の間を滑らかに0→1へ補間するdistance(a, b): 2点間の距離sin/cos/fract/mod: 波や繰り返しパターンの生成によく使う
これらを組み合わせるだけでも、グラデーションや簡単な模様は十分に表現できます。より実践的な例(グラデーション・パーリンノイズ・画像処理)は次章でまとめて扱います。
注意点
SkSLはGLSLとほぼ同じですが完全に同一ではありません。texture()ではなく.eval()を使う、gl_FragCoordではなくmainの引数として座標を受け取るなど、細かな違いがあります。GLSLのシェーダーを移植する場合は公式の差分一覧を確認してください。
公式ドキュメントのShading Languageも参照してください。