7.7 Example codes (61~70)

Click the Open Sandbox button. Then, you can run all the examples on this page by changing the value of the variable example in App.jsx.

import React, { forwardRef, useEffect, useRef, useState } from "react";
import {
  loadTextures,
  Mesh,
  useFrame,
  useKeyDown,
  useKeyUp,
  useRefEffect,
  useSearch,
  useThree,
} from "threefy";
import { randomHSLColor, randomMatrix } from "./ThreeUtils";
import { Model } from "./ThreeModels";
import { hsvToHex, randomCoords, randomHexColor } from "./ThreeUtils";
import * as THREE from "three";
import { LightProbeGenerator } from "three/examples/jsm/lights/LightProbeGenerator";
import { mergeGeometries } from "three/examples/jsm/utils/BufferGeometryUtils";
import { RectAreaLightHelper } from "three/examples/jsm/helpers/RectAreaLightHelper";
import { RectAreaLightUniformsLib } from "three/examples/jsm/lights/RectAreaLightUniformsLib";
import { VertexNormalsHelper } from "three/examples/jsm/helpers/VertexNormalsHelper";
import { VertexTangentsHelper } from "three/examples/jsm/helpers/VertexTangentsHelper";

import crate_gif from "../public/images/crate.gif";

import raymarch_abstract1_jpg from "../public/images/raymarch/abstract1.jpg";
import raymarch_abstract2_jpg from "../public/images/raymarch/abstract2.jpg";

import moss_diffuse_jpg from "../public/images/moss/diffuse.jpg";

import cloud_nx_png from "../public/images/background/cloud/nx.png";
import cloud_ny_png from "../public/images/background/cloud/ny.png";
import cloud_nz_png from "../public/images/background/cloud/nz.png";
import cloud_px_png from "../public/images/background/cloud/px.png";
import cloud_py_png from "../public/images/background/cloud/py.png";
import cloud_pz_png from "../public/images/background/cloud/pz.png";

import sprites_character32px_png from "../public/images/sprites/character32px.png";

import jump_animation_glb from "../public/models/jump-animation.glb";
import LeePerrySmith_glb from "../public/models/LeePerrySmith.glb";
import treehouse_vox from "../public/models/treehouse.vox";

const DemoCubeRefraction = () => {
  const [step, setStep] = useState(0);

  const [data, setData] = useState({
    geometry: null,
    envTex: null,
  });

  const onLoad = (model) => {
    const { scene } = useThree();
    const mesh = model.children[0];
    model.visible = false;

    setTimeout(() => {
      scene.background.mapping = THREE.CubeRefractionMapping;

      setData({
        geometry: mesh.geometry,
        envTex: scene.background,
      });

      setStep(step + 1);
    }, 100);
  };

  if (step === 0) {
    return (
      <object3D>
        <background
          url={[
            cloud_px_png,
            cloud_nx_png,
            cloud_py_png,
            cloud_ny_png,
            cloud_pz_png,
            cloud_nz_png,
          ]}
        />
        <Model url={LeePerrySmith_glb} scale={5} onLoad={onLoad} />
      </object3D>
    );
  }

  if (step === 1) {
    return (
      <group scale={2.5}>
        <mesh>
          <primitive object={data.geometry} attach={"geometry"} />
          <material
            type={"phong"}
            color={0xffffff}
            envMap={data.envTex}
            refractionRatio={0.98}
          />
        </mesh>
        <mesh position-x={-10}>
          <primitive object={data.geometry} attach={"geometry"} />
          <material
            type={"phong"}
            color={0xccfffd}
            envMap={data.envTex}
            refractionRatio={0.985}
          />
        </mesh>
        <mesh position-x={10}>
          <primitive object={data.geometry} attach={"geometry"} />
          <material
            type={"phong"}
            color={0xccddff}
            envMap={data.envTex}
            refractionRatio={0.98}
            reflectivity={0.9}
          />
        </mesh>
      </group>
    );
  }
};

const DemoRectAreaLight = () => {
  const example = 2;

  if (example === 1) {
    const ref = useRef(null);

    const onLoad = (cubeTexture) => {
      // threePointLighting ==> turn off
      const searched = useSearch("Object3D", "threePointLighting");
      searched[0].visible = false;

      const group = ref.current;

      // lightProbe
      const lightProbe = group.children[0];
      lightProbe.copy(LightProbeGenerator.fromCubeTexture(cubeTexture));
      lightProbe.intensity = 1.0;

      // sphere
      const sphere = group.children[1];
      sphere.material.envMap = cubeTexture;
      sphere.material.envMapIntensity = 2;
    };

    return (
      <group ref={ref}>
        <lightProbe intensity />
        <background
          url={[
            cloud_px_png,
            cloud_nx_png,
            cloud_py_png,
            cloud_ny_png,
            cloud_pz_png,
            cloud_nz_png,
          ]}
          onLoad={onLoad}
        />
        <sphere
          scale={4}
          args={[5, 64, 32]}
          type={"standard"}
          color={0xffffff}
          metalness={1}
          roughness={0}
          envMap={null}
          envMapIntensity={1}
        />
      </group>
    );
  }

  if (example === 2) {
    // (cf) RectAreaLight
    // - there is no shadow support
    // - only MeshStandardMaterial and MeshPhysicalMaterial are supported
    // - you have to include RectAreaLightUniformsLib into your scene and call init()

    const ref = useRefEffect((group, scene) => {
      // threePointLighting ==> turn off
      const searched = useSearch("Object3D", "threePointLighting");
      searched[0].visible = false;

      RectAreaLightUniformsLib.init();
      for (let i = 0; i < 5; i++) {
        scene.add(new RectAreaLightHelper(group.children[i]));
      }

      useThree().camera.position.set(0, 6, -25);
    });

    useFrame((t) => {
      const group = ref.current;
      if (group) {
        const torusKnot = group.children[6];
        torusKnot.rotation.y = t * 1.5;
      }
    });

    return (
      <group ref={ref}>
        <rectAreaLight
          position={[-8.0, 5.0, 0.0]}
          args={[0xff0000, 5, 4, 10]}
          rotation-y={Math.PI / -3}
        />
        <rectAreaLight
          position={[-5.0, 5.0, 3.5]}
          args={[0xffff00, 5, 4, 10]}
          rotation-y={Math.PI / -6}
        />
        <rectAreaLight position={[0.0, 5.0, 5.0]} args={[0x00ff00, 5, 4, 10]} />
        <rectAreaLight
          position={[5.0, 5.0, 3.5]}
          args={[0x00ffff, 5, 4, 10]}
          rotation-y={Math.PI / 6}
        />
        <rectAreaLight
          position={[8.0, 5.0, 0.0]}
          args={[0x0000ff, 5, 4, 10]}
          rotation-y={Math.PI / 3}
        />
        <box
          name={"floor"}
          args={[1000, 0.1, 1000]}
          type={"standard"}
          color={0xbcbcbc}
          roughness={0.1}
          metalness={0}
        />
        <torusKnot
          name={""}
          position={[0, 5, 0]}
          args={[1.5, 0.5, 200, 16]}
          type={"standard"}
          color={0xffffff}
          roughness={0}
          metalness={0}
        />
      </group>
    );
  }
};

const DemoHelpers = () => {
  const example = 2;

  if (example === 1) {
    const dir = new THREE.Vector3(1, 2, 4).normalize();
    const backDir = dir.clone().multiplyScalar(100).negate();

    useFrame((t, threefy) => {
      const arrows = threefy.sceneHelpers.children;
      arrows.forEach((arrow) => {
        if (arrow.position.z > 30) arrow.position.add(backDir);
        arrow.position.add(dir);
      });
    });

    return (
      <group>
        {randomCoords([100, 100, 100], 1000, 3).map((p, i) => {
          const origin = new THREE.Vector3(...p);
          const length = Math.random() * 5 + 5;
          const color = randomHexColor();
          return <arrowHelper key={i} args={[dir, origin, length, color]} />;
        })}
      </group>
    );
  }

  if (example === 2) {
    const { scene } = useThree();

    const onLoad = (model) => {
      const { animator } = useThree();
      animator.playAction(model, model.animations[0]);

      // skeleton
      scene.add(new THREE.SkeletonHelper(model));
    };
    const onLoad2 = (model) => {
      const mesh = model.children[0];
      mesh.material.color.set(0x191919); //0xeec1ad

      // vertex normal
      scene.add(new VertexNormalsHelper(mesh, 0.15));

      // vertex tangent
      mesh.geometry.computeTangents();
      scene.add(new VertexTangentsHelper(mesh, 0.15));

      // edges geometry
      const edges = new THREE.EdgesGeometry(mesh.geometry);
      const line = new THREE.LineSegments(edges);
      line.material.opacity = 0.25;
      line.material.transparent = true;
      line.position.x = 20;
      line.scale.setScalar(1.5);
      scene.add(line);
    };

    return (
      <>
        <axesHelper args={[10]} />
        <gridHelper args={[100, 50, 0xff4444, 0x404040]} position-y={-10} />
        <polarGridHelper
          args={[50, 36, 20, 64, 0x444444, 0x888888]}
          position-y={10}
        />
        <Model
          url={jump_animation_glb}
          scale={10}
          position-y={-10}
          onLoad={onLoad}
        />
        <Model
          url={LeePerrySmith_glb}
          scale={1.5}
          position-x={-20}
          onLoad={onLoad2}
        />
      </>
    );
  }
};

const DemoSpirograph = () => {
  const example = 5;

  const Spirograph = forwardRef((props, ref) => {
    // Spirograph pattern
    // x = cx + r1 * cos(t) + r2 * cos(t * ratio)
    // y = cy + r1 * sin(t) + r2 * sin(t * ratio)
    const {
      children,
      type = "monochrome", // monochrome or polychrome
      cx = 0,
      cy = 0,
      r1 = 1,
      r2 = 1,
      ratio = 5,
      dt = 0.01,
      tmax = Math.PI * 2,
      ..._props
    } = props;

    // (cf) supported type:
    // monochrome ==> return <curve type={'catmullRom3'}/>
    // polychrome ==> return <curvePath/><curve/>...<curvePath>

    if (type === "monochrome") {
      let points = [];
      points.push([cx + r1 + r2, cy, 0]);

      let x, y;
      for (let t = 0; t <= tmax; t += dt) {
        x = cx + r1 * Math.cos(t) + r2 * Math.cos(t * ratio);
        y = cy + r1 * Math.sin(t) + r2 * Math.sin(t * ratio);
        points.push([x, y, 0]);
      }

      const divisions = points.length - 1;

      return (
        <curve
          ref={ref}
          type={"catmullRom3"}
          args={[points]}
          divisions={divisions}
          {..._props}
        />
      );
    } else if (type === "polychrome") {
      // const rc = ~~(r1 / 5) || 1;
      const rc = ~~(r1 / 20) || 1;

      let id = 0;
      let x, y;
      const elements = [];

      // start path
      let points = [];
      points.push([cx + r1 + r2, cy, 0]);

      for (let t = 0; t <= tmax; t += dt) {
        // build path
        x = cx + r1 * Math.cos(t) + r2 * Math.cos(t * ratio);
        y = cy + r1 * Math.sin(t) + r2 * Math.sin(t * ratio);
        points.push([x, y, 0]);

        const r = (x ** 2 + y ** 2) ** 0.5;
        if (~~r % rc === 0) {
          // path color
          const hexCol = hsvToHex(r * 0.1, 1, 0.7); // 0.01, 0.02, ..., 0.09, 0.1

          if (points.length >= 2) {
            // curve element
            const element = (
              <curve
                key={id++}
                type={"catmullRom3"}
                args={[points]}
                color={hexCol}
                divisions={points.length - 1}
                {..._props}
              />
            );
            elements.push(element);
          }

          // new path
          points = [];
          points.push([x, y, 0]);
        }
      }

      return <curvePath ref={ref}>{elements}</curvePath>;
    }
  });

  if (example === 1) {
    return (
      <>
        <Spirograph />
        <Spirograph r1={10} r2={5} ratio={10} color={randomHexColor()} />
        <Spirograph r1={20} r2={5} ratio={10} color={randomHexColor()} />
        <Spirograph r1={30} r2={5} ratio={10} color={randomHexColor()} />
      </>
    );
  }

  if (example === 2) {
    const r2 = [100, 75, 50, 25, 2, 1, 1];
    const ratio = [50, 20, 30, 20, 10, 10, 4];
    return (
      <>
        <Spirograph
          r1={10}
          r2={r2[0]}
          ratio={ratio[0]}
          dt={0.005}
          color={randomHexColor()}
        />
        <Spirograph
          r1={10}
          r2={r2[1]}
          ratio={ratio[1]}
          dt={0.005}
          color={randomHexColor()}
        />
        <Spirograph
          r1={10}
          r2={r2[2]}
          ratio={ratio[2]}
          dt={0.005}
          color={randomHexColor()}
        />
        <Spirograph
          r1={10}
          r2={r2[3]}
          ratio={ratio[3]}
          dt={0.005}
          color={randomHexColor()}
        />
        <Spirograph
          r1={10}
          r2={r2[4]}
          ratio={ratio[4]}
          dt={0.005}
          color={randomHexColor()}
        />
        <Spirograph
          r1={10}
          r2={r2[5]}
          ratio={ratio[5]}
          dt={0.005}
          color={randomHexColor()}
        />
        <Spirograph
          r1={5}
          r2={r2[5]}
          ratio={ratio[5]}
          dt={0.005}
          color={randomHexColor()}
        />
      </>
    );
  }

  if (example === 3) {
    let progress = 0;
    let numVertices = Infinity;

    const ref = useRefEffect((line) => {
      numVertices = line.geometry.attributes["position"].count;
    });

    useFrame((t) => {
      // rotating model
      if (ref.current) {
        const line = ref.current;
        line.scale.setScalar(1 + 0.5 * Math.sin(t));
        line.rotation.x = Math.cos(t * 1);
        line.rotation.y = Math.sin(t * 1);
      }

      // growing model
      if (ref.current) {
        const line = ref.current;
        progress += 10;
        line.geometry.setDrawRange(0, progress % numVertices);
      }
    });

    // setInterval(() =>
    // {
    //     // change color
    //     if( ref.current )
    //     {
    //         const line = ref.current;
    //         line.material.color.setHex( randomHexColor() );
    //     }
    // }, 1000 );

    return (
      <Spirograph
        ref={ref}
        r1={10}
        r2={10}
        ratio={1.618}
        tmax={Math.PI * 50}
        color={"tan"}
      />
    );
  }

  if (example === 4) {
    let ith = 0; // index of i-th line
    let iMax = 0; // no of lines
    let progress = 0; // no of vertices in progress on i-th line
    const numVertices = [];

    const ref = useRefEffect((group) => {
      iMax = group.children.length;
      group.children.forEach((line) => {
        numVertices.push(line.geometry.attributes["position"].count);
        line.geometry.setDrawRange(0, -1);
      });
    });

    useFrame((t) => {
      if (ref.current) {
        const group = ref.current;

        if (ith === 0) {
          // clear all lines at the start
          group.children.forEach((line) => line.geometry.setDrawRange(0, -1));
        }

        // for long line
        if (numVertices[ith] > 5) {
          progress += 5;
          const line = group.children[ith];
          if (progress > numVertices[ith]) {
            // finish drawing
            line.geometry.setDrawRange(0, Infinity);
            progress = 0;
            ith++;
          } else {
            // keep drawing
            line.geometry.setDrawRange(0, progress);
          }
        }
        // for short line
        else {
          while (progress <= 5 && ith < iMax) {
            // draw short lines at once
            const n = numVertices[ith];
            const line = group.children[ith];
            line.geometry.setDrawRange(0, n);
            progress += n;
            ith++;
          }
          progress = 0;
        }

        ith = ith % iMax;
      }
    });

    const choice = 5;
    switch (choice) {
      case 1:
        return (
          <Spirograph
            type={"polychrome"}
            ref={ref}
            r1={3}
            r2={3}
            ratio={10}
            dt={0.01}
            tmax={Math.PI * 2}
          />
        );
      case 2:
        return (
          <Spirograph
            type={"polychrome"}
            ref={ref}
            r1={6}
            r2={3}
            ratio={10}
            dt={0.01}
            tmax={Math.PI * 2}
          />
        );
      case 3:
        return (
          <Spirograph
            type={"polychrome"}
            ref={ref}
            r1={12}
            r2={3}
            ratio={10}
            dt={0.01}
            tmax={Math.PI * 2}
          />
        );
      case 4:
        return (
          <Spirograph
            type={"polychrome"}
            ref={ref}
            r1={5}
            r2={5}
            ratio={3.14}
            dt={0.01}
            tmax={Math.PI * 2 * 7}
          />
        );
      case 5:
        return (
          <Spirograph
            type={"polychrome"}
            ref={ref}
            r1={10}
            r2={10}
            ratio={1.618}
            dt={0.05}
            tmax={Math.PI * 2 * 90}
          />
        );
    }
  }

  if (example === 5) {
    const MoveAlongPath = (props) => {
      const { children, ..._props } = props;

      const up = new THREE.Vector3(0, 1, 0);
      const axis = new THREE.Vector3();
      const speed = 0.001;
      // let t = 0;
      let t = Math.random();

      const ref = useRefEffect((group) => {
        const model = group.children[0];
        const element = group.children[1];
        const path = element.userData["curve"] || element.userData["path"]; // <curve/> or <curvePath/>

        useFrame((_t) => {
          const position = path.getPoint(t);
          model.position.copy(position);

          const tangent = path.getTangent(t);
          axis.crossVectors(up, tangent).normalize();
          const theta = Math.acos(up.dot(tangent));
          model.quaternion.setFromAxisAngle(axis, theta);

          t += speed;
          if (t > 1) t = 0;
        });
      });

      return (
        <group ref={ref} {..._props}>
          {children}
        </group>
      );
    };

    const curvePaths = [];
    curvePaths.push(
      <Spirograph
        type={"monochrome"}
        r1={5}
        r2={2}
        ratio={20}
        color={randomHexColor()}
      />
    );
    curvePaths.push(
      <Spirograph
        type={"monochrome"}
        r1={10}
        r2={4}
        ratio={10}
        color={randomHexColor()}
      />
    );
    curvePaths.push(
      <Spirograph
        type={"monochrome"}
        r1={20}
        r2={8}
        ratio={10}
        color={randomHexColor()}
      />
    );
    curvePaths.push(
      <Spirograph
        type={"monochrome"}
        r1={30}
        r2={5}
        ratio={10}
        color={randomHexColor()}
      />
    );

    return (
      <>
        {Array(4)
          .fill()
          .map((x, i) => (
            <MoveAlongPath key={i}>
              <Mesh scale={0.3 + 0.1 * i}>
                <cylinderGeometry args={[0.4, 0.6, 3, 10]} />
                <coneGeometry args={[1, 2, 10]} translate={[0, 2.5, 0]} />
                <material type={"standard"} color={"white"} />
              </Mesh>
              {curvePaths[i]}
            </MoveAlongPath>
          ))}
      </>
    );
  }
};

const DemoSpiroPath = () => {
  const example = 0; // 0 ~ 3
  const _fillDraw = true;
  const _strokeDraw = false;

  const createSpiroPath = (options = {}) =>
    // const path = createSpiroPath()
    // ctx.fill( path ) or ctx.stroke( path )
    // new THREE.CanvasTexture( ctx.canvas )
    {
      const size = 1024;

      const clamp = THREE.MathUtils.clamp;
      const getNumLoops = (a, b, c, d) => {
        if (!c) c = a;
        if (!d) d = b;
        const dividend = Math.max(a, b);
        const divisor = Math.min(a, b);
        const remainder = dividend % divisor;
        const numLoops =
          remainder === 0
            ? (c * d) / divisor / d
            : getNumLoops(divisor, remainder, c, d);
        return numLoops;
      };

      let {
        scaleFactor = 5.5,
        ringCircumference = 96,
        wheelCircumference = 84,
        fraction = 0.6, // 'hole' on the wheel, between 0.78 - 0.15
      } = options;

      const path = new Path2D();

      ringCircumference *= scaleFactor;
      wheelCircumference *= scaleFactor;
      let cx = size * 0.5;
      let cy = size * 0.5;
      const radius = ringCircumference - wheelCircumference;

      let ratio = ringCircumference / wheelCircumference - 1;
      let dt = (1 / ratio) * 0.02; // speed of drawing & curve fidelity
      let t = 0;
      let x, y;
      const numLoops = getNumLoops(ringCircumference, wheelCircumference);
      const tmax = (Math.PI * 2 * numLoops) / (ratio + 1.0) + 0.2;

      while (t < tmax) {
        x =
          cx +
          radius * clamp(Math.cos(t), -1, 1) +
          fraction * wheelCircumference * Math.cos(t * ratio);
        y =
          cy +
          radius * clamp(Math.sin(t), -1, 1) -
          fraction * wheelCircumference * Math.sin(t * ratio);

        t += dt;
        path.lineTo(x, y);
      }
      path.closePath();
      return path;
    };

  const drawTexture = (options) => {
    const { hue, saturation, lightness } = options;

    const size = 1024;
    const ctx = document.createElement("canvas").getContext("2d");
    ctx.canvas.width = size;
    ctx.canvas.height = size;

    const path = createSpiroPath(options);

    if (_fillDraw) {
      ctx.fillStyle = `hsl(${hue}, ${saturation}%, ${lightness}%)`;
      ctx.fill(path);
    }

    if (_strokeDraw) {
      ctx.strokeStyle = `rgb(255, 255, 255)`;
      ctx.lineWidth = 5;
      ctx.stroke(path);
    }

    return new THREE.CanvasTexture(ctx.canvas);
  };

  const spiroPatterns = [
    function pattern0() {
      const graphs = [];
      let numSteps = 12;
      const rotations = [0, 10];
      const len = 2;
      const options = {};
      options.ringCircumference = 96;
      options.wheelCircumference = 84;
      options.hue = 240;
      options.saturation = 100;
      options.lightness = 25;
      options.scaleFactor = 6.5;
      let index = 0;
      for (let i = 0; i < len; i += 1) {
        options.fraction = 0.85;
        options.rotation = rotations[i];
        for (let j = 0; j < numSteps; j += 1) {
          options.fraction -= 0.03;
          options.rotation += 2.5;
          options.hue -= 3;
          options.lightness += 2.5;
          options.saturation -= 1;
          options.index = index;
          options.scaleFactor -= 0.125;
          graphs.push({ ...options });
          index += 1;
        }
      }
      return graphs;
    },
    function pattern1() {
      const graphs = [];
      const options = {};
      options.ringCircumference = 105;
      options.wheelCircumference = 96;
      options.fraction = 0.9;
      options.rotation = 12;
      options.scaleFactor = 5;
      options.hue = 350;
      options.saturation = 100;
      options.lightness = 50;
      const numSteps = 12;
      for (let i = 0; i < numSteps; i += 1) {
        options.fraction -= 0.1;
        options.hue += 4;
        options.rotation += 1;
        options.index = i;
        options.scaleFactor -= 0.5;
        options.scaleFactor = Math.max(1, options.scaleFactor);
        graphs.push({ ...options });
      }
      return graphs;
    },
    function pattern2() {
      const graphs = [];
      const options = {};
      options.ringCircumference = 105;
      options.wheelCircumference = 63;
      options.fraction = 0.85;
      options.hue = 190;
      options.rotation = 340;
      options.scaleFactor = 5;
      options.saturation = 100;
      options.lightness = 50;
      const numSteps = 12;
      for (let i = 0; i < numSteps; i += 1) {
        options.rotation += 2;
        options.hue = i <= 3 ? 0 : i >= 8 ? 140 : 195;
        options.fraction -= 0.02;
        options.index = i;
        graphs.push({ ...options });
      }
      return graphs;
    },
    function pattern3() {
      const graphs = [];
      const options = {};
      options.ringCircumference = 105;
      options.wheelCircumference = 96;
      options.fraction = 1;
      options.rotation = 12;
      options.scaleFactor = 5;
      options.hue = 260;
      options.saturation = 100;
      options.lightness = 50;
      const numSteps = 8;
      for (let i = 0; i < numSteps; i += 1) {
        options.hue += 4;
        options.index = i;
        options.scaleFactor -= 0.5;
        graphs.push({ ...options });
      }
      return graphs;
    },
  ];

  const pattern = spiroPatterns[example]();

  const ref = useRef(null);
  useFrame((t) => {
    const group = ref.current;
    if (group) {
      group.children.forEach((child, i) => {
        child.rotation.z = Math.cos(t + i * 0.1) * 0.5;
      });
    }
  });

  return (
    <group ref={ref}>
      {pattern.map((options, i) => (
        <mesh key={i} position-z={i * 0.05} scale={5}>
          <geometry type={"plane"} args={[10, 10]} />
          <material
            type={"basic"}
            map={drawTexture(options)}
            side={THREE.DoubleSide}
            transparent
            opacity={0.2}
          />
        </mesh>
      ))}
    </group>
  );
};

const DemoMoveAlongPath = () => {
  const example = 2;

  if (example === 1) {
    const RandomArrows = forwardRef((props, ref) => {
      const { count, ..._props } = props;

      // set count
      const geomCount = 2; // 2 = cone + cylinder
      const instCount = geomCount * count;
      const maxVertexCount = instCount * 1024;
      const maxIndexCount = maxVertexCount * 2;

      const initBatchedMesh = (batchMesh) => {
        const material = batchMesh.material;

        // transform matrices
        const matrices = [];
        for (let i = 0; i < count; i++) {
          matrices.push(randomMatrix(200, 200, 200, 3, 8).clone());
        }

        // add geometry & instance
        batchMesh.geometries.forEach((geometry) => {
          const geomId = batchMesh.addGeometry(geometry);
          for (let i = 0; i < count; i++) {
            const instId = batchMesh.addInstance(geomId);
            batchMesh.setMatrixAt(instId, matrices[i]);

            material.setValue(instId, "diffuse", ...randomHSLColor());
            material.setValue(instId, "roughness", 0);
            material.setValue(instId, "metalness", 0);
          }
        });

        // delete the attached
        delete batchMesh.geometries;
      };

      if (ref) {
        useEffect(() => initBatchedMesh(ref.current), []);
      } else {
        ref = useRefEffect((batchMesh) => initBatchedMesh(batchMesh));
      }

      return (
        <batchedMesh
          ref={ref}
          args={[instCount, maxVertexCount, maxIndexCount]}
          {..._props}
        >
          <cylinderGeometry args={[0.4, 0.6, 3, 10]} attach={`geometries-0`} />
          <coneGeometry
            args={[1, 2, 10]}
            translate={[0, 2.5, 0]}
            attach={`geometries-1`}
          />
          <batchedMaterial args={[instCount]} {..._props} />
        </batchedMesh>
      );
    });

    return <RandomArrows count={100} />;
  }

  if (example === 2) {
    const MoveAlongPath = (props) => {
      const { children, ..._props } = props;

      const up = new THREE.Vector3(0, 1, 0);
      const axis = new THREE.Vector3();
      const speed = 0.002;
      let t = 0;

      const ref = useRefEffect((group) => {
        const model = group.children[0];
        const element = group.children[1];
        const path = element.userData["curve"] || element.userData["path"]; // <curve/> or <curvePath/>

        useFrame((_t) => {
          const position = path.getPoint(t);
          model.position.copy(position);

          const tangent = path.getTangent(t);
          axis.crossVectors(up, tangent).normalize();
          const theta = Math.acos(up.dot(tangent));
          model.quaternion.setFromAxisAngle(axis, theta);

          t += speed;
          if (t > 1) t = 0;
        });
      });

      return (
        <group ref={ref} {..._props}>
          {children}
        </group>
      );
    };

    return (
      <MoveAlongPath>
        <Mesh scale={1}>
          <cylinderGeometry args={[0.4, 0.6, 3, 10]} />
          <coneGeometry args={[1, 2, 10]} translate={[0, 2.5, 0]} />
          <material type={"standard"} map={raymarch_abstract1_jpg} />
        </Mesh>
        <curvePath scale={1}>
          <curve
            type={"line"}
            color={"red"}
            dim={3}
            args={[
              [0, 0, 0],
              [-10, 0, 0],
            ]}
          />
          <curve
            type={"line"}
            color={"green"}
            dim={3}
            linetype={"dashed"}
            dashSize={0.5}
            gapSize={0.25}
            args={[
              [-10, 0, 0],
              [-10, 10, 0],
            ]}
          />
          <curve
            type={"line"}
            color={"blue"}
            dim={3}
            args={[
              [-10, 10, 0],
              [0, 10, 0],
            ]}
          />
          <curve
            type={"bezier"}
            order={"cubic"}
            color={"yellow"}
            dim={3}
            divisions={20}
            args={[
              [0, 10, 0],
              [20, 10, 0],
              [20, 0, 0],
              [0, 0, 0],
            ]}
          />
        </curvePath>
      </MoveAlongPath>
    );
  }
};

const DemoMesh2 = () => {
  const example = 9;
  // (cf) example 1 to 7: <mesh/> (these produce all the same result)
  // (cf) example 8: <instancedMesh/> = 1-geometry + 1-material ==> n-mesh-instance
  // (cf) example 9: <batchedMesh  /> = m-geometry + 1-material ==> n-mesh-instance

  const url = raymarch_abstract2_jpg;
  const geom1 = new THREE.TorusGeometry(9, 3, 16, 100);
  const geom2 = new THREE.BoxGeometry(15, 15, 15);
  const geom3 = new THREE.SphereGeometry(9, 32, 16);
  const geometry = mergeGeometries([geom1, geom2, geom3], false); //BufferGeometry
  const material = new THREE.MeshStandardMaterial({
    map: new THREE.TextureLoader().load(
      url,
      (tex) => (tex.colorSpace = THREE.SRGBColorSpace)
    ),
  });

  if (example === 1) {
    return (
      <mesh>
        <geometry type={"buffer"}>
          <bufferAttribute
            attach={"attributes-position"}
            args={[geometry.attributes.position.array, 3]}
          />
          <bufferAttribute
            attach={"attributes-normal"}
            args={[geometry.attributes.normal.array, 3]}
          />
          <bufferAttribute
            attach={"attributes-uv"}
            args={[geometry.attributes.uv.array, 2]}
          />
          <bufferAttribute attach={"index"} args={[geometry.index.array, 1]} />
        </geometry>
        <material type={"standard"} map={url} />
      </mesh>
    );
  }

  if (example === 2) {
    return (
      <mesh>
        <geometry type={"buffer"} copy={geometry} />
        <material type={"standard"} map={url} />
      </mesh>
    );
  }

  if (example === 3) {
    return (
      <mesh>
        <primitive object={geometry} attach={"geometry"} />
        <material type={"standard"} map={url} />
      </mesh>
    );
  }

  if (example === 4) {
    return (
      <mesh>
        <primitive object={geometry} attach={"geometry"} />
        <primitive object={material} attach={"material"} />
      </mesh>
    );
  }

  if (example === 5) {
    return <mesh geometry={geometry} material={material} />;
  }

  if (example === 6) {
    return (
      <mesh geometry={geometry}>
        <material type={"standard"} map={url} />
      </mesh>
    );
  }

  if (example === 7) {
    return (
      <mesh material={material}>
        <geometry type={"buffer"} copy={geometry} />
      </mesh>
    );
  }

  if (example === 8) {
    // using <instancedMesh/>
    const count = 500;

    const ref = useRefEffect((instMesh) => {
      for (let i = 0; i < count; i++) {
        const m = randomMatrix(500, 500, 500, 0.5, 2);
        instMesh.setMatrixAt(i, m);
      }
    });

    return (
      <instancedMesh ref={ref} count={count}>
        <geometry type={"buffer"} copy={geometry} />
        <material type={"standard"} map={url} />
      </instancedMesh>
    );
  }

  if (example === 9) {
    // using <batchedMesh/>
    const count = 500;

    const ref = useRefEffect((batchMesh) => {
      // transform matrices
      const matrices = [];
      for (let i = 0; i < count; i++) {
        matrices.push(randomMatrix(500, 500, 500, 0.5, 2).clone());
      }

      // geometries ==> instances
      batchMesh.geometries.forEach((geometry) => {
        const geomId = batchMesh.addGeometry(geometry);
        for (let i = 0; i < count; i++) {
          const instId = batchMesh.addInstance(geomId);
          batchMesh.setMatrixAt(instId, matrices[i]);
        }
      });
    });

    return (
      <batchedMesh ref={ref} args={[count * 3, 6553600, 6553600 * 2]}>
        <geometry type={"buffer"} copy={geom1} attach={"geometries-0"} />
        <geometry type={"buffer"} copy={geom2} attach={"geometries-1"} />
        <geometry type={"buffer"} copy={geom3} attach={"geometries-2"} />
        <material type={"standard"} map={url} />
      </batchedMesh>
    );
  }
};

const DemoMesh3 = () => {
  const example = 2;

  if (example === 1) {
    // 8 geometries ==> 1 geometry
    return (
      <Mesh>
        <geometry type={"buffer"} copy={new THREE.BoxGeometry(10, 1, 10)} />
        <geometry type={"box"} args={[5, 5, 5]} />
        <boxGeometry args={[1, 10, 1]} />

        <sphereGeometry args={[2, 32, 16]} translate={[0, 7, 0]} />

        <primitive
          object={new THREE.CylinderGeometry(1, 1, 5, 32)}
          translate={[5, 0, 5]}
        />
        <primitive
          object={new THREE.CylinderGeometry(1, 1, 5, 32)}
          translate={[5, 0, -5]}
        />
        <primitive
          object={new THREE.CylinderGeometry(1, 1, 5, 32)}
          translate={[-5, 0, 5]}
        />
        <primitive
          object={new THREE.CylinderGeometry(1, 1, 5, 32)}
          translate={[-5, 0, -5]}
        />

        <material type={"standard"} map={raymarch_abstract2_jpg} />
      </Mesh>
    );
  }

  if (example === 2) {
    const ref = useRefEffect((mesh) => {
      async function _loadAsync() {
        const urls = [crate_gif, raymarch_abstract2_jpg];
        const textures = await loadTextures(urls);

        mesh.userData.maps = { id: 0, textures: textures };

        setInterval(() => {
          const maps = mesh.userData.maps;
          maps.id = maps.id === 0 ? 1 : 0;
          mesh.material.map = maps.textures[maps.id];
          mesh.material.needsUpdate = true;
        }, 1000);
      }
      _loadAsync();
    });

    return (
      <Mesh ref={ref} scale={2}>
        <geometry type={"buffer"} copy={new THREE.BoxGeometry(10, 1, 10)} />
        <geometry type={"box"} args={[5, 5, 5]} />
        <boxGeometry args={[1, 10, 1]} />

        <sphereGeometry args={[2, 32, 16]} translate={[0, 7, 0]} />

        <primitive
          object={new THREE.CylinderGeometry(1, 1, 5, 32)}
          translate={[5, 0, 5]}
        />
        <primitive
          object={new THREE.CylinderGeometry(1, 1, 5, 32)}
          translate={[5, 0, -5]}
        />
        <primitive
          object={new THREE.CylinderGeometry(1, 1, 5, 32)}
          translate={[-5, 0, 5]}
        />
        <primitive
          object={new THREE.CylinderGeometry(1, 1, 5, 32)}
          translate={[-5, 0, -5]}
        />

        <material type={"standard"} color={"tan"} />
      </Mesh>
    );
  }
};

const DemoSpriteAnimation = () => {
  // sprite: init
  const ref = useRefEffect((sprite) => {
    const { threefy } = useThree();

    // set sprite texture
    const map = sprite.material.map; // 'images/sprites/character32px.png'
    const tilesX = 8; // # of characters along x
    const tilesY = 8; // # of characters along y
    map.magFilter = THREE.NearestFilter; // sharper pixels
    map.repeat.set(1 / tilesX, 1 / tilesY);

    // init camera
    threefy.controls.target = sprite.position;
    threefy.camera.position.x = sprite.position.x;
    threefy.camera.position.y = 1;
    threefy.camera.position.z = sprite.position.z + 3;

    //=========================================
    // init sprite.cc (cc: character controller)
    //=========================================
    sprite.cc = {};
    const cc = sprite.cc;
    cc.playDuration = 1.25;
    cc.moveSpeed = 2;
    cc.keysPressed = { w: false, a: false, s: false, d: false };
    cc.facingRight = true;
    cc.curPlay = "IDLE_RIGHT";
    cc.moveVec = new THREE.Vector3();
    cc.rotAngle = new THREE.Quaternion();
    cc.rotAxis = new THREE.Vector3(0, 1, 0);

    //======================
    // 1) set animations
    //======================
    cc.animations = {
      IDLE_RIGHT: {
        indices: [0, 1, 2, 3],
        maxTime: cc.playDuration / 4,
        index: 0,
        elapsedTime: 0,
      },
      RUN_RIGHT: {
        indices: [8, 9, 10, 11, 12, 13, 16, 17, 18, 19, 20, 21],
        maxTime: cc.playDuration / 12,
        index: 0,
        elapsedTime: 0,
      },
      IDLE_LEFT: {
        indices: [24, 25, 26, 27],
        maxTime: cc.playDuration / 4,
        index: 0,
        elapsedTime: 0,
      },
      RUN_LEFT: {
        indices: [32, 33, 34, 35, 36, 37, 40, 41, 42, 43, 44, 45],
        maxTime: cc.playDuration / 12,
        index: 0,
        elapsedTime: 0,
      },
    };

    //======================
    // 2) set keyboard
    //======================
    useKeyDown((e) => {
      e.stopPropagation();
      cc.keysPressed[e.key.toLowerCase()] = true;
    });
    useKeyUp((e) => {
      e.stopPropagation();
      cc.keysPressed[e.key.toLowerCase()] = false;
    });

    //======================
    // 3) set sprite-update
    //======================
    cc.update = function (dt) {
      //======================
      // 3.1) press keyborad
      //======================
      let nextPlay;
      const keysPressed = cc.keysPressed;

      if (keysPressed.a) cc.facingRight = false;
      if (keysPressed.d) cc.facingRight = true;

      if (keysPressed.w || keysPressed.a || keysPressed.s || keysPressed.d) {
        nextPlay = cc.facingRight ? "RUN_RIGHT" : "RUN_LEFT";
      } else {
        nextPlay = cc.facingRight ? "IDLE_RIGHT" : "IDLE_LEFT";
      }
      if (cc.curPlay !== nextPlay) cc.curPlay = nextPlay;

      //======================
      // 3.2) move sprite
      //======================
      if (nextPlay === "RUN_RIGHT" || nextPlay === "RUN_LEFT") {
        // movement direction
        threefy.camera.getWorldDirection(cc.moveVec); // walk direction where camera is pointing at
        cc.moveVec.y = 0;
        cc.moveVec.normalize();

        // rotation angle
        let rotAngle = 0;
        if (keysPressed.w) {
          if (keysPressed.a) rotAngle = Math.PI / 4;
          else if (keysPressed.d) rotAngle = -Math.PI / 4;
        } else if (keysPressed.s) {
          if (keysPressed.a) rotAngle = Math.PI / 4 + Math.PI / 2;
          else if (keysPressed.d) rotAngle = -Math.PI / 4 - Math.PI / 2;
          else rotAngle = Math.PI;
        } else if (keysPressed.a) rotAngle = Math.PI / 2;
        else if (keysPressed.d) rotAngle = -Math.PI / 2;

        cc.rotAngle.setFromAxisAngle(cc.rotAxis, rotAngle); // quaternion
        cc.moveVec.applyQuaternion(cc.rotAngle); // rotate movement direction
        cc.moveVec.multiplyScalar(cc.moveSpeed * dt); // movement length

        // sprite position
        sprite.position.add(cc.moveVec);

        // camera position
        threefy.camera.position.add(cc.moveVec);

        // controls target
        threefy.controls.target = sprite.position;
      }

      //======================
      // 3.3) change map texture
      //======================
      const play = cc.animations[cc.curPlay];
      play.elapsedTime += dt;

      if (play.maxTime > 0 && play.elapsedTime >= play.maxTime) {
        play.elapsedTime = 0;
        play.index = (play.index + 1) % play.indices.length;

        // tile index: left-top ==> right-bottom
        const curTile = play.indices[play.index];

        // offsetX: left ==> right
        const offsetX = (curTile % tilesX) / tilesX;

        // offsetY: bottom ==> up
        const offsetY = (tilesY - Math.floor(curTile / tilesX) - 1) / tilesY;

        const { map } = sprite.material;
        map.offset.x = offsetX;
        map.offset.y = offsetY;
      }
    };
  });

  // sprite: animate
  useFrame((t, threefy, dt) => {
    const sprite = ref.current;
    if (sprite?.cc) {
      sprite.cc.update(dt);
    }
  });

  // playing field
  const planeRef = useRefEffect((plane) => {
    const mapTex = plane.material.map;
    mapTex.wrapS = THREE.RepeatWrapping;
    mapTex.wrapT = THREE.RepeatWrapping;
    mapTex.repeat.set(8, 8);
  });

  return (
    <>
      <plane
        ref={planeRef}
        rotation-x={Math.PI / -2}
        args={[50, 50]}
        type={"lambert"}
        map={moss_diffuse_jpg}
      />
      <sprite ref={ref} position-y={0.5}>
        <material type={"sprite"} map={sprites_character32px_png} />
      </sprite>
    </>
  );
};

const DemoLoadThenSave = () => {
  const example = 1;

  if (example === 1) {
    const [model, setModel] = useState(null);

    const url = treehouse_vox;

    const ref = useRef(null);

    useEffect(() => {
      const timeoutId = setTimeout(() => {
        setModel(ref.current);
      }, 1000);

      return () => clearTimeout(timeoutId);
    }, []);

    const onLoad = (model) => (ref.current = model);

    return model ? (
      <Model url={"model.glb"} object={model} />
    ) : (
      <Model ref={ref} url={url} onLoad={onLoad} />
    );
  }
};

const DemoExamples = ({ example }) => {
  switch (example) {
    case 61:
      return <DemoCubeRefraction />;
    case 62:
      return <DemoRectAreaLight />;
    case 63:
      return <DemoHelpers />;
    case 64:
      return <DemoSpirograph />;
    case 65:
      return <DemoSpiroPath />;
    case 66:
      return <DemoMoveAlongPath />;
    case 67:
      return <DemoMesh2 />;
    case 68:
      return <DemoMesh3 />;
    case 69:
      return <DemoSpriteAnimation />;
    case 70:
      return <DemoLoadThenSave />;
  }
};

export { DemoExamples };

Last updated