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