Open13

memo:プロジェクト制作

ももちこももちこ
技術スタック

使用する技術スタック

  • Next.js(AppRauter): SSRとルーティングのため
  • React: UIコンポーネント構築
  • Three.js & React Three Fiber: 3Dコンテンツの実装
  • Tailwind CSS: スタイリング
  • shadcn/ui: 高品質なUIコンポーネント
  • biome: リンター及びフォーマッター
  • bun: パッケージマネージャー
ももちこももちこ
プロジェクトの作成

Next.js でプロジェクトを作成

bunx create-next-app@latest
✔ What is your project named?
// プロジェクト名は? => three-js-showcase

✔ Would you like to use TypeScript? … No / Yes
// TypeScriptを使用しますか? => Yes

✔ Would you like to use ESLint? … No / Yes
// ESLintを使用しますか? => No (bimoe使用のため)

✔ Would you like to use Tailwind CSS? … No / Yes
// Tailwind CSSを使用しますか? => Yes

✔ Would you like to use `src/` directory? … No / Yes
// src/ ディレクトリを使用しますか? => No

✔ Would you like to use App Router? (recommended) … No / Yes
// App Routerを使用しますか? => Yes

✔ Would you like to customize the default import alias (@/*)? … No / Yes
// カスタマイズされたデフォルトのインポートエイリアスを使用しますか? => No
ももちこももちこ
依存関係のインストール

依存関係のインストールを行う

まずはプロジェクトのディレクトリに移動

cd  three-js-showcase(ファイルのパスを指定)

Three.js 関連パッケージのインストール

bun add three @react-three/fiber @react-three/drei
  • three => Three.js (3Dグラフィックスを JavaScript で書きやすくしたライブラリ)
  • @react-three/fiber => React Three Fiber (Three.js を React で使えるようにしたライブラリ)
  • @react-three/drei => React Three Fiber の便利ツール郡

Three.js の型定義のインストール

bun add --save @types/three

TypeScriptを使用するので型定義をインストールしておく

shadcn/ui の初期化

bunx shadcn@latest init
✔ Which style would you like to use? › New York
// どのスタイルにしますか? => New York

✔ Which color would you like to use as the base color? › Zinc
// ベースカラーはどうしますか? => Zinc

✔ Would you like to use CSS variables for theming? … no / yes
// CSS  変数は使用しますか => Yes

biome のインストールと初期化

bun add --save --exact @biomejs/biome
nunx biome init
ももちこももちこ
プロジェクト構造の設定

プロジェクト構造の設定を行う

ディレクトリ構造の作成

/three-js-showcase
  /app
    /projects
      /[id]
    layout.tsx
    page.tsx
  /components
  /lib
  /public
    /models
    /textures
  /types
ももちこももちこ
app/layout.tsx の更新

メタデータの更新を行う

import type { Metadata } from 'next'
import './globals.css'

export const metadata: Metadata = {
  title: 'Thee.js Project Showcase',
  description: 'Explore amazing Three.js projects',
}

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode
}>) {
  return (
    <html lang="ja">
      <body>{children}</body>
    </html>
  )
}
ももちこももちこ
app/page.tsx の更新

page.tsx にスタイルを当てる

import Navigation from '@/components/Navigation'

export default function Home() {
  return (
    <div className=" flex h-screen bg-gray-100 text-gray-800">
      <nav className="w-64 bg-white shadow-md">
        <Navigation />
      </nav>

      <main className="flex-1 flex flex-col">
        <div>
          <div>{/* <Canvas/> */}</div>
        </div>
      </main>
    </div>
  )
}

Navigation コンポーネントと Canvas コンポーネントは未実装なのでコメントアウト

  • Navigation コンポーネント: サイドバーナビゲーション サンプルイメージを表示する
  • Canvas コンポーネント: 3Dシーンを描画する

完成イメージ

Image from Gyazo

ももちこももちこ
Navigation.tsx の作成
export default function Navigation() {
  return (
    <nav className="container mx-auto px-4">
      <h1 className="text-xl font-bold border-b mt-4 mb-8">
        Three.js Project Showcase
      </h1>
      <ul className="space-y-2">
        <li>
          <div className="flex flex-col border rounded-lg mb-4 bg-gray-200">
            <img
              className="aspect-video  object-cover object-center "
              src=""
              alt=""
            />
            <p className="p-2 bg-white">title 1</p>
          </div>
        </li>
        <li>
          <div className="flex flex-col border rounded-lg mb-4 bg-gray-200">
            <img
              className="aspect-video object-cover object-center"
              src=""
              alt=""
            />
            <p className="p-2 bg-white">title 1</p>
          </div>
        </li>
        <li>
          <div className="flex flex-col border rounded-lg mb-4 bg-gray-200">
            <img
              className="aspect-video  object-cover object-center "
              src=""
              alt=""
            />
            <p className="p-2 bg-white">title 1</p>
          </div>
        </li>
      </ul>
    </nav>
  )
}

完成イメージ

Image from Gyazo

ももちこももちこ
Canvas3D.tsx の作成

Canvas コンポーネントにするつもりだったが
React Three Fiber のCanvas と名前衝突するので Canvas3Dコンポーネントに変更

'use client'

import { OrbitControls } from '@react-three/drei'
import { Canvas } from '@react-three/fiber'
import { Suspense } from 'react'
import { CineonToneMapping, SRGBColorSpace } from 'three/webgpu'

function Box() {
  return (
    <mesh>
      <boxGeometry args={[2, 2, 2]} /> // ジオメトリの設定 配列内は[width(横幅), height(高さ), depth(奥行き)]
      <meshBasicMaterial color={'red'} /> // マテリアルの設定
    </mesh>
  )
}

export default function Canvas3D() {
  return (
    <div className="w-full h-screen">
      <Canvas
        gl={{
          antialias: true, // アンチエイリアスの有効化
          toneMapping: CineonToneMapping, // トーンマッピングの設定
          outputColorSpace: SRGBColorSpace, // カラースペースの設定
        }}
      >
        <Suspense fallback={null}>
          <Box />
          <OrbitControls /> 
        </Suspense>
      </Canvas>
    </div>
  )
}

React Three Fiber は未学習なので生成AIにお願いした
Three.js は学習したのである程度の意味はわかる

ももちこももちこ
app/layout.tsx の更新

常にサイドバーが表示されるように変更

import Navigation from '@/components/Navigation'
import type { Metadata } from 'next'
import './globals.css'

export const metadata: Metadata = {
  title: 'Thee.js Project Showcase',
  description: 'Explore amazing Three.js projects',
}

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode
}>) {
  return (
    <html lang="ja">
      <body>
        <div className="flex h-screen bg-gray-100 text-gray-800">
          <div className="w-64 bg-white shadow-md">
            <Navigation />
          </div>

          <main className="flex-1 overflow-y-auto">
            <div>
              <div>{children}</div>
            </div>
          </main>
        </div>
      </body>
    </html>
  )
}
ももちこももちこ
Nacigation.tsx の更新

デザインの追加と簡単なリファクタリング

画像を追加したりmap メソッドで配列を回して表示したり

import Image from 'next/image'
import Link from 'next/link'

export type Project = {
  id: string
  title: string
  imageSrc: string
}

const projects: Project[] = [
  { id: 'cube', title: 'cube', imageSrc: '/thumbnails/box.png' },
  { id: 'sphere', title: 'Sphere', imageSrc: '/thumbnails/sphere.png' },
  { id: 'torus', title: 'Torus', imageSrc: '/thumbnails/torus.png' },
]

interface NavigationProps {
  onProjectSlect: (project: Project) => void
}
export default function Navigation() {
  return (
    <nav className="container mx-auto px-4">
      <h1 className="text-xl font-bold border-b mt-4 mb-8">
        Three.js Project Showcase
      </h1>
      <ul className="space-y-2">
        {projects.map((project) => (
          <li key={project.id}>
            <Link href={`/projects/${project.id}`}>
              <div className="flex flex-col border rounded-lg relative mb-4 bg-gray-200 hover:bg-gray-300 transition-colors">
                <Image
                  className="aspect-video object-cover object-center"
                  src={project.imageSrc}
                  alt={`${project.title} thumbnail`}
                  width={300}
                  height={169}
                />
                <p className="p-2 bg-white">{project.title}</p>
              </div>
            </Link>
          </li>
        ))}
      </ul>
    </nav>
  )
}
ももちこももちこ
3Dオブジェクトの作成

テストオブジェクトの作成しコンポーネントとして保存

componets/3d-object/Cube.tsx
export default function Cube() {
  return (
    <mesh>
      <boxGeometry args={[1, 1, 1]} />
      <meshStandardMaterial color={'blue'} />
    </mesh>
  )
}

Image from Gyazo

componets/3d-object/Spherer.tsx
export default function Sphere() {
  return (
    <mesh>
      <sphereGeometry args={[1, 64, 64]} />
      <meshStandardMaterial color={'blue'} />
    </mesh>
  )
}

Image from Gyazo

componets/3d-object/Torus.tsx
export default function Torus() {
  return (
    <mesh>
      <torusGeometry args={[1, 0.5, 64, 64]} />
      <meshStandardMaterial color={'blue'} />
    </mesh>
  )
}

Image from Gyazo

ももちこももちこ
動的ルーティングで表示するオブジェクトの切り替え

/[id] を使用した動的なルーティング

[id]がproject/idとして適用される

project/[id]/page.tsx
'use client'
import Canvas3D from '@/components/Canvas3D'

export default function ProjectPage({ params }: { params: { id: string } }) {
  return (
    <div>
      <Canvas3D projectId={params.id} />
    </div>
  )
}