目的
基本
- UVマッピングを使って画像から矩形を切りだす
- 切り出した矩形画像を等倍でスクリーンに表示する
- 1枚のテクスチャを使い回す
応用
- 切り出した画像の高さ・幅の拡大と左右反転して表示する
ようするにスプライトシート的な2D画像を用意して、切り出して使いたいって人の役に立つ記事です。
ソース
キャンバス
export default function Page() {
return (
<main>
<Suspense fallback={<span>loading...</span>}>
<Canvas
camera={{
position: [0, 0, 100],
zoom: 1,
near: 0.1,
far: 640,
}}
orthographic
gl={{ antialias: true }}
style={{ width: "100vw", height: "100vh" }}
onContextMenu={(e) => e.preventDefault()}
>
<ImagePlane
src="/assets/playing_cards.png"
position={[0, 0, 0]}
clipArea={{x: 2/13, y:3/4, width: 1/13, height: 1/4 }}
/>
</Canvas>
</Suspense>
</main>
);
}
注目ポイントは、orthographic
です。
カメラが平行投影になり、パースがつきませんが奥行きの計算が不要になり便利です。
ImagePlane
import { TextureLoader } from "three";
import { MeshProps, useLoader } from "@react-three/fiber";
import { useMemo } from "react";
import * as THREE from "three";
import { createRectUV } from "@/utils/threes/uv";
type Props = MeshProps & {
src: string;
clipArea?: [x: number, y: number, width: number, height: number];
};
export default function ImagePlane({
src,
clipArea = [0, 0, 1, 1],
...meshProps
}: Props) {
// useLoader の texture は共有される
const texture = useLoader(TextureLoader, src);
const [x, y, width, height] = clipArea;
const planeWidth = texture.image.width * width;
const planeHeight = texture.image.height * height;
const geometry = useMemo(() => {
const geometry = new THREE.PlaneGeometry(planeWidth, planeHeight, 1, 1);
const attribute = createRectUV(x, width, y, height );
// geometry に UV を設定
geometry.setAttribute("uv", new THREE.BufferAttribute(attribute, 2));
return geometry;
}, [planeWidth, planeHeight, x, width, y, height]);
return (
<mesh {...meshProps} geometry={geometry}>
<meshBasicMaterial attach="material" map={texture} transparent />
</mesh>
);
}
new THREE.PlaneGeometry(planeWidth, planeHeight, 1, 1)
の planeWidth
, planeHeight
は切り出した画像の表示サイズをピクセルで指定すればOKです。
Canvas
で orthographic
を指定しているため、カメラからの距離(奥行き)にかかわらず同じサイズで表示されます。
createRectUV
では、呼び出した texture から指定の矩形の範囲を指定したものを geometry の uv
に設定します。
createRectUV
export function createRectUV(
x: number,
width: number,
y: number,
height: number,
): Float32Array {
const left = width : x;
const right = x + width;
const top = y + height;
const bottom = height : y;
// UV マッピングをカスタマイズ
return new Float32Array([
left, top, // 左上
right, top, // 右上
left, bottom, // 左下
right, bottom, // 右下
]);
}
uv
は貼り付け元のテクスチャ(ここでは画像)の切り出し範囲の矩形を示す座標で、横方向(左から右)を u
、縦方向(下から上)を v
とし、元の画像の幅と高さのそれぞれを 1 としたときの 0〜1の範囲となります。
そして、uv
で表現した、左上、右上、左下、右下の4点を Float32Array
で指定します。
拡大・反転
import { TextureLoader } from "three";
import { MeshProps, useLoader } from "@react-three/fiber";
import { useMemo } from "react";
import * as THREE from "three";
import { createRectUV } from "@/utils/threes/uv";
type Props = MeshProps & {
src: string;
clipArea?: [x: number, y: number, width: number, height: number];
clipScale?: [x: number, y: number];
};
export default function ImagePlane({
src,
clipArea = [0, 0, 1, 1],
clipScale = [1, 1],
...meshProps
}: Props) {
// useLoader のtexture は共有される
const texture = useLoader(TextureLoader, src);
const [clipScaleX, clipScaleY] = clipScale;
const [x, y, width, height] = clipArea;
const planeWidth = texture.image.width * width * Math.abs(clipScaleX);
const planeHeight = texture.image.height * height * Math.abs(clipScaleY);
const reverseX = clipScaleX < 0;
const reverseY = clipScaleY < 0;
const geometry = useMemo(() => {
const geometry = new THREE.PlaneGeometry(planeWidth, planeHeight, 1, 1);
const attribute = createRectUvs(x, width, reverseX, y, height, reverseY);
// geometry に UV を設定
geometry.setAttribute("uv", new THREE.BufferAttribute(attribute, 2));
return geometry;
}, [planeWidth, planeHeight, reverseX, x, width, reverseY, y, height]);
return (
<mesh {...meshProps} geometry={geometry}>
<meshBasicMaterial attach="material" map={texture} transparent />
</mesh>
);
}
拡大
props
に clipScale
を追加しました。
THREE.PlaneGeometry(planeWidth, planeHeight, 1, 1);
となっているので、 planeWidth
, planeHeight
を clipScale
で倍すればOKです。
反転
反転は、UVの切り出し方を反転させる必要があります。
export function createRectUV(
x: number,
width: number,
reverseX: boolean,
y: number,
height: number,
reverseY: boolean
): Float32Array {
const left = reverseX ? x + width : x;
const right = reverseX ? x : x + width;
const top = reverseY ? y : y + height;
const bottom = reverseY ? y + height : y;
// UV マッピングをカスタマイズ
return new Float32Array([
left, top, // 左上
right, top, // 右上
left, bottom, // 左下
right, bottom, // 右下
]);
}
余談、別解(Bad Practice)
texture に offset と repeat を設定する
矩形の切り出しは UV
を使わず、offset
とrepeat
を使ってもできます。
const texture = useLoader(TextureLoader, src);
texture.offset.set(x, y);
texture.repeat.set(planeWidth, planeHeight);
useLoader(TextureLoader, src)
で呼び出した texture はプロジェクト内で共有されます
ただし、texture は src と結びついており、 複数回呼び出しても、同一テクスチャが使用されます。offset と repeat の設定はテクスチャの変更に含まれており、ImagePlane を複数呼び出した場合、全部のテクスチャの切り出し位置が同じになってしまいます。
const texture = useLoader(TextureLoader, src).clone();
clone()
を用いて、各ImagePlane で別のテクスチャを用意すれば、問題は解決しますが、メッシュごとにテクスチャを複数用意するのはメモリの無駄遣いです。