1
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?

【React Three Fiber】Reactを使ってポートフォリオを刷新した話

Last updated at Posted at 2023-08-11

🔮 はじめに

React Three Fiberを使用して開発したポートフォリオの解説を掲載していきたいと思います.誰かの参考になれば幸いです.

🔮 自己紹介

初めまして.web開発を勉強している273*(ツナサンド) / Kei.と申します.関西の大学生です.
Reactを勉強し始めて二ヶ月くらい経過しました.仕組みが少しずつ理解できるようになったので,私のポートフォリオを3D表現を用いて刷新してみることにしました.

初めての投稿となりますので,文章や表現に誤りがある可能性がございますがご了承ください.

🔮 完成品

以下のリンクからポートフォリオを閲覧することができます.PC版の方はブラウザのハードウェアアクセラレーションをONにしてくださいね.

テキストをクリックすることでデモを行なったりすることが可能です.

サイトと記事の画像が一部違いますが,技術的には同じです.

🔮 制作について

非常に綺麗なシェーダーでテキストを表示していますが,これは公式のExamplesを参考にしました.

以下から実装までの手順を掲載していきます.

1. ライブラリのインストール

実装するにあたって必要なライブラリをインストールしていきます.

npm install @pmndrs/branding
npm install @react-three/drei
npm install @react-three/fiber
npm install @react-three/postprocessing
npm install @types/three
npm install leva

参考元と同様です.

2. 動作確認

とりあえず参考元のコードをコピペして自身の環境で動作することを確認します.

コード
import { RGBELoader } from 'three-stdlib'
import { Canvas, useLoader } from '@react-three/fiber'
import {
  Center,
  Text3D,
  Instance,
  Instances,
  Environment,
  Lightformer,
  OrbitControls,
  RandomizedLight,
  AccumulativeShadows,
  MeshTransmissionMaterial
} from '@react-three/drei'
import { useControls, button } from 'leva'
import { EffectComposer, HueSaturation, BrightnessContrast } from '@react-three/postprocessing'

export function App() {
  const { autoRotate, text, shadow, ...config } = useControls({
    text: 'Inter',
    backside: true,
    backsideThickness: { value: 0.3, min: 0, max: 2 },
    samples: { value: 16, min: 1, max: 32, step: 1 },
    resolution: { value: 1024, min: 64, max: 2048, step: 64 },
    transmission: { value: 1, min: 0, max: 1 },
    clearcoat: { value: 0, min: 0.1, max: 1 },
    clearcoatRoughness: { value: 0.0, min: 0, max: 1 },
    thickness: { value: 0.3, min: 0, max: 5 },
    chromaticAberration: { value: 5, min: 0, max: 5 },
    anisotropy: { value: 0.3, min: 0, max: 1, step: 0.01 },
    roughness: { value: 0, min: 0, max: 1, step: 0.01 },
    distortion: { value: 0.5, min: 0, max: 4, step: 0.01 },
    distortionScale: { value: 0.1, min: 0.01, max: 1, step: 0.01 },
    temporalDistortion: { value: 0, min: 0, max: 1, step: 0.01 },
    ior: { value: 1.5, min: 0, max: 2, step: 0.01 },
    color: '#ff9cf5',
    gColor: '#ff7eb3',
    shadow: '#750d57',
    autoRotate: false,
    screenshot: button(() => {
      // Save the canvas as a *.png
      const link = document.createElement('a')
      link.setAttribute('download', 'canvas.png')
      link.setAttribute('href', document.querySelector('canvas').toDataURL('image/png').replace('image/png', 'image/octet-stream'))
      link.click()
    })
  })
  return (
    <Canvas shadows orthographic camera={{ position: [10, 20, 20], zoom: 80 }} gl={{ preserveDrawingBuffer: true }}>
      <color attach="background" args={['#f2f2f5']} />
      {/** The text and the grid */}
      <Text config={config} rotation={[-Math.PI / 2, 0, 0]} position={[0, -1, 2.25]}>
        {text}
      </Text>
      {/** Controls */}
      <OrbitControls
        autoRotate={autoRotate}
        autoRotateSpeed={-0.1}
        zoomSpeed={0.25}
        minZoom={40}
        maxZoom={140}
        enablePan={false}
        dampingFactor={0.05}
        minPolarAngle={Math.PI / 3}
        maxPolarAngle={Math.PI / 3}
      />
      {/** The environment is just a bunch of shapes emitting light. This is needed for the clear-coat */}
      <Environment resolution={32}>
        <group rotation={[-Math.PI / 4, -0.3, 0]}>
          <Lightformer intensity={20} rotation-x={Math.PI / 2} position={[0, 5, -9]} scale={[10, 10, 1]} />
          <Lightformer intensity={2} rotation-y={Math.PI / 2} position={[-5, 1, -1]} scale={[10, 2, 1]} />
          <Lightformer intensity={2} rotation-y={Math.PI / 2} position={[-5, -1, -1]} scale={[10, 2, 1]} />
          <Lightformer intensity={2} rotation-y={-Math.PI / 2} position={[10, 1, 0]} scale={[20, 2, 1]} />
          <Lightformer type="ring" intensity={2} rotation-y={Math.PI / 2} position={[-0.1, -1, -5]} scale={10} />
        </group>
      </Environment>
      {/** Soft shadows */}
      <AccumulativeShadows frames={100} color={shadow} colorBlend={5} toneMapped={true} alphaTest={0.9} opacity={1} scale={30} position={[0, -1.01, 0]}>
        <RandomizedLight amount={4} radius={10} ambient={0.5} intensity={1} position={[0, 10, -10]} size={15} mapSize={1024} bias={0.0001} />
      </AccumulativeShadows>
    </Canvas>
  )
}

const Grid = ({ number = 23, lineWidth = 0.026, height = 0.5 }) => (
  // Renders a grid and crosses as instances
  <Instances position={[0, -1.02, 0]}>
    <planeGeometry args={[lineWidth, height]} />
    <meshBasicMaterial color="#999" />
    {Array.from({ length: number }, (_, y) =>
      Array.from({ length: number }, (_, x) => (
        <group key={x + ':' + y} position={[x * 2 - Math.floor(number / 2) * 2, -0.01, y * 2 - Math.floor(number / 2) * 2]}>
          <Instance rotation={[-Math.PI / 2, 0, 0]} />
          <Instance rotation={[-Math.PI / 2, 0, Math.PI / 2]} />
        </group>
      ))
    )}
    <gridHelper args={[100, 100, '#bbb', '#bbb']} position={[0, -0.01, 0]} />
  </Instances>
)

function Text({ children, config, font = '/Inter_Medium_Regular.json', ...props }) {
  const texture = useLoader(RGBELoader, 'https://dl.polyhaven.org/file/ph-assets/HDRIs/hdr/1k/aerodynamics_workshop_1k.hdr')
  return (
    <>
      <group>
        <Center scale={[0.8, 1, 1]} front top {...props}>
          <Text3D
            castShadow
            bevelEnabled
            font={font}
            scale={5}
            letterSpacing={-0.03}
            height={0.25}
            bevelSize={0.01}
            bevelSegments={10}
            curveSegments={128}
            bevelThickness={0.01}>
            {children}
            <MeshTransmissionMaterial {...config} background={texture} />
          </Text3D>
        </Center>
        <Grid />
      </group>
    </>
  )
}

3. 書き換え

背景色や文字をツールごとに変更させる

動作が確認できたらコードを書き換えていきます.

完成ページを見ていただけると,ツールアイコンを選択した時やテーマを切り替えるたびに文字や色,背景色が変わっていると思います.参考元のコードでは下記のようにプロパティが設定されており,こちらを変更することによってマテリアルの編集が可能です.

コード
export function App() {
  const { autoRotate, text, shadow, ...config } = useControls({
    text: 'Inter',
    backside: true,
    backsideThickness: { value: 0.3, min: 0, max: 2 },
    samples: { value: 16, min: 1, max: 32, step: 1 },
    resolution: { value: 1024, min: 64, max: 2048, step: 64 },
    transmission: { value: 1, min: 0, max: 1 },
    clearcoat: { value: 0, min: 0.1, max: 1 },
    clearcoatRoughness: { value: 0.0, min: 0, max: 1 },
    thickness: { value: 0.3, min: 0, max: 5 },
    chromaticAberration: { value: 5, min: 0, max: 5 },
    anisotropy: { value: 0.3, min: 0, max: 1, step: 0.01 },
    roughness: { value: 0, min: 0, max: 1, step: 0.01 },
    distortion: { value: 0.5, min: 0, max: 4, step: 0.01 },
    distortionScale: { value: 0.1, min: 0.01, max: 1, step: 0.01 },
    temporalDistortion: { value: 0, min: 0, max: 1, step: 0.01 },
    ior: { value: 1.5, min: 0, max: 2, step: 0.01 },
    color: '#ff9cf5',
    gColor: '#ff7eb3',
    shadow: '#750d57',
    autoRotate: false,
    screenshot: button(() => {
      // Save the canvas as a *.png
      const link = document.createElement('a')
      link.setAttribute('download', 'canvas.png')
      link.setAttribute('href', document.querySelector('canvas').toDataURL('image/png').replace('image/png', 'image/octet-stream'))
      link.click()
    })
  })

直接プロパティを変数で変更するようにしたら画面上でマテリアル変更されるだろうと考え,変数を書き換えるボタンを設置し試したところ全く変化ありませんでした.当初困惑してましたが,以下のようにプロパティの値をuseEffectを用いて更新してあげる必要がありました.併せて必要な変数も関数に追加していきます.

export function Three({ mainText, mainColor, mainFunc, imgURL, BGtheme }) {
  
  const { autoRotate, text, shadow, ...config } = useControls({
    //省略
  });

  useEffect(() => {
      config.text = mainText;
      config.color = mainColor[0];
      config.gColor = mainColor[1];
      config.shadow = mainColor[2];
      BGColor = BGtheme;
   }, [mainText, mainColor, BGColor]);

  return(
    //省略
  );
}

背景色や文字に関しては以下のように直接入れてあげても動作します.

//変更前
//背景:50行目
 <color attach="background" args={['#f2f2f5']} />
//変更後
//背景18行目下に追加
var BGColor = BGtheme ? "#222" : "#f4ede4";
//背景:50行目
<color attach="background" args={[BGColor]} />
//変更前
//文字:20行目
text: 'Inter',
//文字70行目
{text}
//変更後
//文字:20行目
text: mainText,
//文字70行目
{mainText}

文字にクリック判定を設定する

こちらはreturnの中の <Text3D>onClick を追記することでクリック判定を設定できます.

 <Text3D
    castShadow
    bevelEnabled
    font={font}
    scale={5}
    letterSpacing={-0.03}
    lineHeight={0.6}
    height={0.25}
    bevelSize={0.01}
    bevelSegments={10}
    curveSegments={128}
    bevelThickness={0.01}
    onClick={() => demo(func, img)} //<-コレ
   >

私はデモ画面を表示するようにしています.これで文字の部分をクリックすることで任意の関数を実行することが可能になります.

4. コンポーネントの設定

上記で変更を加えたコンポーネントを使うための記述をしていきます.私は Three というコンポーネントとして記述しました.変数は mainText, mainColor, mainFunc, imgURL, BGtheme です.役割は以下の通りです.

変数名 役割
mainText 立体の文字を設定する文字列
mainColor 立体文字用の3つのカラーコードが格納されている配列
mainFunc ツールを識別してデモ画面を表示するためのid
imgURL ツールロゴのパス
BGtheme ライトテーマとダークテーマの状態が格納されている

ツールページコンポーネント(Tool)でThreeコンポーネントを

 <Three
   cmainText={text}
   mainColor={color}
   mainFunc={func}
   imgURL={img}
   BGtheme={theme}
 />

といったように呼び出しており,App.jsxの方で他の変数と一緒に以下のように呼び出しています.


 <Tool
   text={"Credit_\nChecker"}
   desc={
   "ユニパからダウンロードした 成績表 PDFファイルを読み込むと,自動で不足単位数や修得進捗,GPA表示してくれます.現在は情報と経済学部に対応しています."}
   PTime={"2週間"}
   color={["#79a4d3", "#4e6292", "#0a2d53"]}
   func={0}
   img={Cc}
 />
          
 <Tool
   text={"My_\nPortfolio"}
   desc={"私のポートフォリオを作成しました."}
   PTime={"2週間"}
   color={["#d5b6f8", "#b8cce8", "#3d1b65"]}
   func={5}
   img={Mp}
 />
             

text,color,func,img が該当する部分です.この辺りは配列データにしてmap関数などで呼び出したりするとより綺麗に記述できるかと思います.今回はやりませんでしたが.

5. 動作確認

動いた!

これでToolページの完成です.新たなツールができても先述のようにApp.jsxに追記するだけでOK.楽チンだね.
スクリーンショット 2023-08-11 23.05.33.png
スクリーンショット 2023-08-06 23.53.08.png
スクリーンショット 2023-08-06 23.54.19.png

🔮 コード全体

ポートフォリオ自体のソースはGitHubにて公開しておりますが,念の為今回解説したコードを下記に掲載しておきます.

コード
import { RGBELoader } from "three-stdlib";
import { Canvas, useLoader } from "@react-three/fiber";
import {
  Center,
  Text3D,
  Instance,
  Instances,
  Environment,
  Lightformer,
  OrbitControls,
  RandomizedLight,
  AccumulativeShadows,
  MeshTransmissionMaterial,
} from "@react-three/drei";
import { useControls, button } from "leva";
import { useEffect, useState } from "react";
import { demo } from "./components/Preview";

export function Three({ mainText, mainColor, mainFunc, imgURL, BGtheme }) {
  var BGColor = BGtheme ? "#222" : "#f4ede4";

  const { autoRotate, text, shadow, ...config } = useControls({
    text: mainText,
    backside: true,
    backsideThickness: { value: 0.3, min: 0, max: 2 },
    samples: { value: 16, min: 1, max: 32, step: 1 },
    resolution: { value: 512, min: 64, max: 2048, step: 64 },
    transmission: { value: 1, min: 0, max: 1 },
    clearcoat: { value: 0, min: 0.1, max: 1 },
    clearcoatRoughness: { value: 0.0, min: 0, max: 1 },
    thickness: { value: 0.3, min: 0, max: 5 },
    chromaticAberration: { value: 5, min: 0, max: 5 },
    anisotropy: { value: 0.3, min: 0, max: 1, step: 0.01 },
    roughness: { value: 0, min: 0, max: 1, step: 0.01 },
    distortion: { value: 0.5, min: 0, max: 4, step: 0.01 },
    distortionScale: { value: 0.1, min: 0.01, max: 1, step: 0.01 },
    temporalDistortion: { value: 0.08, min: 0, max: 1, step: 0.01 },
    ior: { value: 1.5, min: 0, max: 2, step: 0.01 },
    color: mainColor[0],
    gColor: mainColor[1],
    shadow: mainColor[2],
    autoRotate: true,
  });

  // mainTextやmainColorが変更されたときにconfigの値を更新する
  useEffect(() => {
    config.text = mainText;
    config.color = mainColor[0];
    config.gColor = mainColor[1];
    config.shadow = mainColor[2];
    BGColor = BGtheme;
  }, [mainText, mainColor, BGColor]);

  return (
    <Canvas
      shadows
      orthographic
      camera={{ position: [10, 20, 20], zoom: 80 }}
      gl={{ preserveDrawingBuffer: true }}
    >
      <color attach="background" args={[BGColor]} />
      {/** The text and the grid */}
      <Text
        func={mainFunc}
        img={imgURL}
        config={config}
        rotation={[-Math.PI / 2, 0, 0]}
        position={[0, -1, 5]}
      >
        {mainText}
      </Text>
      {/** Controls */}
      <OrbitControls
        autoRotate={autoRotate}
        autoRotateSpeed={-0.3}
        zoomSpeed={0.25}
        minZoom={40}
        maxZoom={140}
        enablePan={false}
        dampingFactor={0.05}
        minPolarAngle={Math.PI / 3}
        maxPolarAngle={Math.PI / 3}
      />
      {/** The environment is just a bunch of shapes emitting light. This is needed for the clear-coat */}
      <Environment resolution={32}>
        <group rotation={[-Math.PI / 4, -0.3, 0]}>
          <Lightformer
            intensity={20}
            rotation-x={Math.PI / 2}
            position={[0, 5, -9]}
            scale={[10, 10, 1]}
          />
          <Lightformer
            intensity={2}
            rotation-y={Math.PI / 2}
            position={[-5, 1, -1]}
            scale={[10, 2, 1]}
          />
          <Lightformer
            intensity={2}
            rotation-y={Math.PI / 2}
            position={[-5, -1, -1]}
            scale={[10, 2, 1]}
          />
          <Lightformer
            intensity={2}
            rotation-y={-Math.PI / 2}
            position={[10, 1, 0]}
            scale={[20, 2, 1]}
          />
          <Lightformer
            type="ring"
            intensity={2}
            rotation-y={Math.PI / 2}
            position={[-0.1, -1, -5]}
            scale={10}
          />
        </group>
      </Environment>
      {/** Soft shadows */}
      <AccumulativeShadows
        frames={100}
        color={mainColor[2]}
        colorBlend={5}
        toneMapped={true}
        alphaTest={0.9}
        opacity={1}
        scale={30}
        position={[0, -1.01, 0]}
      >
        <RandomizedLight
          amount={4}
          radius={10}
          ambient={0.5}
          intensity={1}
          position={[0, 10, -10]}
          size={15}
          mapSize={1024}
          bias={0.0001}
        />
      </AccumulativeShadows>
    </Canvas>
  );
}

const Grid = ({ number = 23, lineWidth = 0.026, height = 0.5 }) => (
  // Renders a grid and crosses as instances
  <Instances position={[0, -1.02, 0]}>
    <planeGeometry args={[lineWidth, height]} />
    <meshBasicMaterial color="#666" />
    {Array.from({ length: number }, (_, y) =>
      Array.from({ length: number }, (_, x) => (
        <group
          key={x + ":" + y}
          position={[
            x * 2 - Math.floor(number / 2) * 2,
            -0.01,
            y * 2 - Math.floor(number / 2) * 2,
          ]}
        >
          <Instance rotation={[-Math.PI / 2, 0, 0]} />
          <Instance rotation={[-Math.PI / 2, 0, Math.PI / 2]} />
        </group>
      ))
    )}
    {/* <gridHelper args={[100, 100, "#bbb", "#bbb"]} position={[0, -0.01, 0]} /> */}
  </Instances>
);

function Text({
  func,
  img,
  children,
  config,
  font = "/Inter_Medium_Regular.json",
  ...props
}) {
  const texture = useLoader(
    RGBELoader,
    "https://dl.polyhaven.org/file/ph-assets/HDRIs/hdr/1k/aerodynamics_workshop_1k.hdr"
  );

  return (
    <>
      <group>
        <Center scale={[0.8, 1, 1]} front top {...props}>
          <Text3D
            castShadow
            bevelEnabled
            font={font}
            scale={5}
            letterSpacing={-0.03}
            lineHeight={0.6}
            height={0.25}
            bevelSize={0.01}
            bevelSegments={10}
            curveSegments={128}
            bevelThickness={0.01}
            onClick={() => demo(func, img)}
          >
            {children}
            <MeshTransmissionMaterial {...config} background={texture} />
          </Text3D>
        </Center>
        <Grid />
      </group>
    </>
  );
}

🔮 使用言語,ライブラリ,フレームワーク

ポートフォリオ開発で使用したものです.

OS:macOS Ventura
デザイン:Figma,Blender
開発ツール:Visual Studio Code,Git,GitHub,Docker,FileZila AWS
サイト系:HTML,CSS,JavaScript,React,react-router-dom
3D:three.js,pmndrs/branding,react-three(drei,fiber,postprocessing),leva
ポップアップ:Sweetalert2
アイコン:DEVICON,ionicons

🔮 最後に

いかがでしたでしょうか.他のページはよくあるReactを用いたwebページと同じです.自力で React Three Fiber を用いて制作を行うのはかなりの知識と労力がかかると思います.マスターしたいところですが私には少し早かったかもしれません(笑).公式のExamplesには更に面白い例が沢山掲載されているので覗くだけでも楽しいですよ.長文となりましたがここまで読んでいただきありがとうございました.

1
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
1
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?