みかづきブログ・カスタム

基本的にはちょちょいのほいです。

Three.jsでオブジェクトを発光させる ✨

先日、 Blenderでオブジェクトを発光させました が、今回はThree.jsでオブジェクトを発光させる手順のメモです。
Three.jsとNext.jsを使っていますが、本質的な部分としては Three.jsのEffectComposer と postprocessing unreal bloom を使って実装します。

threejs.org

ソースコード

app/src/page.tsx

'use client';

import { useEffect, useRef } from 'react';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass';
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass';
import { useOrbitControls } from '@/hooks/useOrbitControls';
import { useEffectComposer } from '@/hooks/useEffectComposer';
import { useResize } from '@/hooks/useResize';
import { useThree } from '@/hooks/useThree';

export default function UnrealBloomPassPage() {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const { windowWidth, windowHeight } = useResize();
  const { THREE, renderer, camera, scene } = useThree(canvasRef.current);
  const effectComposer = useEffectComposer(renderer);
  const orbitControls = useOrbitControls(camera, canvasRef.current);

  useEffect(() => {
    if (!renderer || !camera || !effectComposer || !orbitControls) {
      return;
    }

    camera.position.set(4, 4, 8);
    camera.lookAt(new THREE.Vector3(0, 0, 0));

    const cube = new THREE.Mesh(
      new THREE.BoxGeometry(1, 1, 1),
      new THREE.MeshNormalMaterial()
    );

    scene.add(cube);

    renderer.setClearColor(0x000000, 1);

    const light = new THREE.DirectionalLight(0xFFFFFF);

    light.position.set(1, 1, 1);
    scene.add(light)

    effectComposer.addPass(
      new RenderPass(scene, camera)
    );
    effectComposer.addPass(new UnrealBloomPass(
      new THREE.Vector2(windowWidth, windowHeight),
      1, // Strength
      0, // Radius
      0, // Threshold
    ));

    renderer.setAnimationLoop(() => {
      orbitControls.update();
      effectComposer.render();
    });
  }, [renderer, camera, scene, effectComposer, orbitControls]);

  useEffect(() => {
    if (!renderer || !camera || !effectComposer) {
      return;
    }

    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(windowWidth, windowHeight);
    effectComposer.setSize(windowWidth, windowHeight);

    camera.aspect = windowWidth / windowHeight;
    camera.updateProjectionMatrix();
  }, [renderer, camera, effectComposer, windowWidth, windowHeight]);

  return (
    <canvas ref={ canvasRef } />
  );
}

src/hooks/useOrbitControls.tsx

import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';

export function useOrbitControls(camera: THREE.PerspectiveCamera | undefined, canvas: HTMLCanvasElement | null) {
  if (!camera || !canvas) {
    return;
  }

  const orbitControls = new OrbitControls(camera, canvas);

  return orbitControls;
}

src/hooks/useEffectComposer.tsx

import * as THREE from 'three';
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer';

export function useEffectComposer(
  renderer: THREE.WebGLRenderer | undefined
) {
  if (!renderer) {
    return;
  }

  const effectComposer = new EffectComposer(renderer);

  return effectComposer;
}

src/hooks/useResize.tsx

import { useEffect, useState } from 'react';

export function useResize() {
  const [ windowWidth, setWindowWidth ] = useState(0);
  const [ windowHeight, setWindowHeight ] = useState(0);

  useEffect(() => {
    window.removeEventListener('resize', handleResize);
    window.addEventListener('resize', handleResize, {
      passive: true
    });

    handleResize();

    return () => {
      window.removeEventListener('resize', handleResize);
    }
  }, []);

  function handleResize() {
    setWindowWidth(window.innerWidth);
    setWindowHeight(window.innerHeight);
  }

  return { windowWidth, windowHeight };
}

src/hooks/useThree.tsx

import { useEffect, useState } from 'react';
import * as THREE from 'three'

export function useThree(canvas: HTMLCanvasElement | null) {
  const [ renderer, setRenderer ] = useState<THREE.WebGLRenderer>();
  const [ camera, setCamera ] = useState<THREE.PerspectiveCamera>();
  const [ scene ] = useState<THREE.Scene>(new THREE.Scene);

  useEffect(() => {
    if (!canvas) {
      return;
    }

    setRenderer(new THREE.WebGLRenderer({
      canvas,
      antialias: true,
      alpha: true
    }));
  }, [canvas]);

  useEffect(() => {
    if (!renderer) {
      return;
    }

    setCamera(new THREE.PerspectiveCamera());
  }, [renderer]);

  return { THREE, renderer, camera, scene };
}