0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

R3F React Three Fiber UVマッピングを理解して画像を矩形で切り出し表示する

Posted at

目的

基本

  • UVマッピングを使って画像から矩形を切りだす
  • 切り出した矩形画像を等倍でスクリーンに表示する
  • 1枚のテクスチャを使い回す

応用

  • 切り出した画像の高さ・幅の拡大と左右反転して表示する

ようするにスプライトシート的な2D画像を用意して、切り出して使いたいって人の役に立つ記事です。

ソース

キャンバス

Canvas.tsx
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

ImagePlane.tsx
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です。
Canvasorthographic を指定しているため、カメラからの距離(奥行き)にかかわらず同じサイズで表示されます。

createRectUV では、呼び出した texture から指定の矩形の範囲を指定したものを geometry の uv に設定します。

createRectUV

three/uv.ts
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 で指定します。

拡大・反転

ImagePlane.tsx
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>
  );
}

拡大

propsclipScale を追加しました。
THREE.PlaneGeometry(planeWidth, planeHeight, 1, 1); となっているので、 planeWidth, planeHeightclipScale で倍すればOKです。

反転

反転は、UVの切り出し方を反転させる必要があります。

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 を使わず、offsetrepeat を使ってもできます。

ImagePlane.tsx
  const texture = useLoader(TextureLoader, src);
  texture.offset.set(x, y);
  texture.repeat.set(planeWidth, planeHeight);

useLoader(TextureLoader, src) で呼び出した texture はプロジェクト内で共有されます

ただし、texture は src と結びついており、 複数回呼び出しても、同一テクスチャが使用されます。offset と repeat の設定はテクスチャの変更に含まれており、ImagePlane を複数呼び出した場合、全部のテクスチャの切り出し位置が同じになってしまいます。

ImagePlane.tsx
  const texture = useLoader(TextureLoader, src).clone();

clone() を用いて、各ImagePlane で別のテクスチャを用意すれば、問題は解決しますが、メッシュごとにテクスチャを複数用意するのはメモリの無駄遣いです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?