--- name: threejs-geometry description: Three.js geometry creation and manipulation adapted for React Three Fiber and Suaveplan R3F packages. Covers declarative mesh/geometry JSX, BufferGeometry with React refs, InstancedMesh patterns for high-performance rendering, custom geometry construction, and @react-three/drei geometry helpers. --- This skill covers geometry creation and manipulation in Three.js within the React Three Fiber (R3F) context used by the Suaveplan monorepo. All code targets **React 19 + TypeScript + R3F** and follows Suaveplan CLAUDE.md conventions. The user needs to create, modify, or optimize 3D geometry in an R3F scene. This includes built-in primitives, custom BufferGeometry, instanced rendering, and parametric shapes. ## Core Concepts ### Three.js Geometry System Three.js geometry is based on `BufferGeometry`, which stores vertex data in typed arrays: - **position**: `Float32Array` of `[x, y, z]` per vertex - **normal**: `Float32Array` of `[nx, ny, nz]` per vertex - **uv**: `Float32Array` of `[u, v]` per vertex - **index**: `Uint16Array` or `Uint32Array` of triangle vertex indices - **color**: `Float32Array` of `[r, g, b]` per vertex (optional) ### R3F Geometry Approach In R3F, geometries are declared as JSX children of ``: | Three.js (Imperative) | R3F (Declarative) | |---|---| | `new BoxGeometry(1, 1, 1)` | `` | | `mesh.geometry = geometry` | `` | | `geometry.setAttribute(...)` | Via ref or attach prop | | `new InstancedMesh(g, m, n)` | `` | ### Suaveplan R3F Packages | Package | Purpose | Import | |---|---|---| | `@suaveplan/r3f` | Core utilities, geometry helpers | `import { useR3F } from '@suaveplan/r3f'` | | `@suaveplan/r3f-performance` | Geometry LOD, draw call monitoring | `import { usePerformanceMonitor } from '@suaveplan/r3f-performance'` | ## Built-in Geometry Primitives All Three.js geometry classes are available as lowercase JSX elements. Constructor arguments pass via `args`. ### Box ```tsx ``` ### Sphere ```tsx ``` ### Common Primitives Reference | Geometry | JSX | Key Args | |---|---|---| | Box | `` | width, height, depth | | Sphere | `` | radius, segments | | Cylinder | `` | radii, height, segments | | Cone | `` | radius, height | | Torus | `` | radius, tube radius | | Plane | `` | width, height | | Ring | `` | inner/outer radius | | Circle | `` | radius, segments | | TorusKnot | `` | radius, tube, knot params | | Dodecahedron | `` | radius, detail level | | Icosahedron | `` | radius, detail level | | Octahedron | `` | radius, detail level | | Tetrahedron | `` | radius, detail level | ## Custom BufferGeometry ### Declarative with Refs ```tsx import { useRef, useEffect, useMemo } from 'react'; import { BufferAttribute } from 'three'; import type { BufferGeometry } from 'three'; interface CustomTriangleProps { size?: number; color?: string; } export function CustomTriangle({ size = 1, color = 'limegreen' }: CustomTriangleProps) { const geometryRef = useRef(null); const { positions, normals, uvs } = useMemo(() => { const s = size; return { positions: new Float32Array([ -s, -s, 0, // bottom-left s, -s, 0, // bottom-right 0, s, 0, // top-center ]), normals: new Float32Array([ 0, 0, 1, 0, 0, 1, 0, 0, 1, ]), uvs: new Float32Array([ 0, 0, 1, 0, 0.5, 1, ]), }; }, [size]); return ( ); } ``` ### Indexed Geometry For geometry with shared vertices (reduces memory and improves performance): ```tsx import { useMemo } from 'react'; export function IndexedQuad({ size = 1 }: { size?: number }) { const { positions, indices, uvs } = useMemo(() => { const s = size / 2; return { positions: new Float32Array([ -s, -s, 0, // 0: bottom-left s, -s, 0, // 1: bottom-right s, s, 0, // 2: top-right -s, s, 0, // 3: top-left ]), indices: new Uint16Array([ 0, 1, 2, // first triangle 0, 2, 3, // second triangle ]), uvs: new Float32Array([ 0, 0, 1, 0, 1, 1, 0, 1, ]), }; }, [size]); return ( ); } ``` ## Dynamic Geometry Updates ### Animating Vertices in useFrame ```tsx import { useRef, useMemo } from 'react'; import { useFrame } from '@react-three/fiber'; import type { BufferGeometry } from 'three'; export function WavingPlane({ width = 5, segments = 32 }: { width?: number; segments?: number }) { const geometryRef = useRef(null); useFrame(({ clock }) => { if (!geometryRef.current) return; const positions = geometryRef.current.attributes.position; const time = clock.elapsedTime; for (let i = 0; i < positions.count; i++) { const x = positions.getX(i); const z = positions.getZ(i); const y = Math.sin(x * 2 + time) * 0.3 + Math.cos(z * 2 + time * 0.7) * 0.2; positions.setY(i, y); } positions.needsUpdate = true; geometryRef.current.computeVertexNormals(); }); return ( ); } ``` ## InstancedMesh: High-Performance Rendering `InstancedMesh` renders many copies of the same geometry with a single draw call. Each instance can have its own transform and color. ### Basic Instancing ```tsx import { useRef, useEffect, useMemo } from 'react'; import { Object3D, Color, Matrix4 } from 'three'; import type { InstancedMesh } from 'three'; interface InstanceFieldProps { count: number; spread?: number; } export function InstanceField({ count, spread = 10 }: InstanceFieldProps) { const meshRef = useRef(null); const dummy = useMemo(() => new Object3D(), []); const color = useMemo(() => new Color(), []); useEffect(() => { if (!meshRef.current) return; for (let i = 0; i < count; i++) { dummy.position.set( (Math.random() - 0.5) * spread, Math.random() * 2, (Math.random() - 0.5) * spread ); dummy.rotation.set( Math.random() * Math.PI, Math.random() * Math.PI, 0 ); dummy.scale.setScalar(0.2 + Math.random() * 0.8); dummy.updateMatrix(); meshRef.current.setMatrixAt(i, dummy.matrix); color.setHSL(Math.random(), 0.7, 0.5); meshRef.current.setColorAt(i, color); } meshRef.current.instanceMatrix.needsUpdate = true; if (meshRef.current.instanceColor) { meshRef.current.instanceColor.needsUpdate = true; } }, [count, spread, dummy, color]); return ( ); } ``` ### Animated Instances ```tsx import { useRef, useMemo } from 'react'; import { useFrame } from '@react-three/fiber'; import { Object3D } from 'three'; import type { InstancedMesh } from 'three'; export function FloatingParticles({ count = 500 }: { count?: number }) { const meshRef = useRef(null); const dummy = useMemo(() => new Object3D(), []); const offsets = useMemo(() => { return Array.from({ length: count }, () => ({ x: (Math.random() - 0.5) * 20, y: Math.random() * 10, z: (Math.random() - 0.5) * 20, speed: 0.2 + Math.random() * 0.8, phase: Math.random() * Math.PI * 2, })); }, [count]); useFrame(({ clock }) => { if (!meshRef.current) return; const t = clock.elapsedTime; for (let i = 0; i < count; i++) { const { x, y, z, speed, phase } = offsets[i]; dummy.position.set( x + Math.sin(t * speed + phase) * 0.5, y + Math.sin(t * speed * 0.7 + phase) * 0.3, z + Math.cos(t * speed + phase) * 0.5 ); dummy.scale.setScalar(0.05 + Math.sin(t * speed + phase) * 0.02); dummy.updateMatrix(); meshRef.current.setMatrixAt(i, dummy.matrix); } meshRef.current.instanceMatrix.needsUpdate = true; }); return ( ); } ``` ## Drei Geometry Helpers `@react-three/drei` provides higher-level geometry components: ### RoundedBox ```tsx import { RoundedBox } from '@react-three/drei'; ``` ### Text3D ```tsx import { Text3D, Center } from '@react-three/drei';
Hello R3F
``` ### Parametric Shapes ```tsx import { Extrude, Lathe } from '@react-three/drei'; import { Shape, Vector2 } from 'three'; import { useMemo } from 'react'; export function CustomExtrusion() { const shape = useMemo(() => { const s = new Shape(); s.moveTo(0, 0); s.lineTo(1, 0); s.lineTo(1, 1); s.lineTo(0.5, 1.5); s.lineTo(0, 1); s.closePath(); return s; }, []); return ( ); } ``` ## Geometry Merging for Performance Merge static geometries to reduce draw calls: ```tsx import { useRef, useEffect } from 'react'; import { BoxGeometry, Matrix4 } from 'three'; import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js'; import type { BufferGeometry } from 'three'; export function MergedBoxes({ positions }: { positions: [number, number, number][] }) { const geometryRef = useRef(null); useEffect(() => { const geometries = positions.map(([x, y, z]) => { const box = new BoxGeometry(1, 1, 1); box.applyMatrix4(new Matrix4().makeTranslation(x, y, z)); return box; }); const merged = mergeGeometries(geometries, false); if (merged && geometryRef.current) { geometryRef.current.copy(merged); geometries.forEach((g) => g.dispose()); merged.dispose(); } }, [positions]); return ( ); } ``` ## Geometry Bounding Volumes Bounding volumes are essential for culling, collision detection, and raycasting: ```tsx import { useRef, useEffect } from 'react'; import type { Mesh } from 'three'; export function BoundedMesh() { const meshRef = useRef(null); useEffect(() => { if (!meshRef.current) return; const geometry = meshRef.current.geometry; // Compute bounding box geometry.computeBoundingBox(); const box = geometry.boundingBox; // box.min, box.max, box.getCenter(), box.getSize() // Compute bounding sphere geometry.computeBoundingSphere(); const sphere = geometry.boundingSphere; // sphere.center, sphere.radius }, []); return ( ); } ``` ## Level of Detail (LOD) Use `` from drei or Three.js LOD for distance-based geometry switching: ```tsx import { Detailed } from '@react-three/drei'; export function LODObject() { return ( {/* Closest: high detail */} {/* Medium distance */} {/* Far distance */} {/* Very far: billboard or point */} ); } ``` ## Testing Geometry ### Vitest Tests ```tsx import { describe, it, expect } from 'vitest'; import { BoxGeometry, SphereGeometry } from 'three'; describe('Geometry fundamentals', () => { it('creates box with correct vertex count', () => { const geom = new BoxGeometry(1, 1, 1); expect(geom.attributes.position.count).toBe(24); // 6 faces x 4 vertices }); it('creates indexed sphere', () => { const geom = new SphereGeometry(1, 16, 16); expect(geom.index).not.toBeNull(); expect(geom.attributes.position.count).toBeGreaterThan(0); }); it('computes bounding box', () => { const geom = new BoxGeometry(2, 3, 4); geom.computeBoundingBox(); expect(geom.boundingBox!.max.x).toBeCloseTo(1); expect(geom.boundingBox!.max.y).toBeCloseTo(1.5); expect(geom.boundingBox!.max.z).toBeCloseTo(2); }); }); ``` ### Storybook Stories ```tsx import type { Meta, StoryObj } from '@storybook/react'; import { Canvas } from '@react-three/fiber'; import { OrbitControls } from '@react-three/drei'; import { InstanceField } from './instance-field'; const meta: Meta = { title: 'R3F/Geometry/InstanceField', component: InstanceField, decorators: [ (Story) => ( ), ], }; export default meta; export const Default: StoryObj = { args: { count: 100, spread: 10 }, }; export const Dense: StoryObj = { args: { count: 1000, spread: 5 }, }; ``` ## Checklist When working with geometry in R3F: 1. Use built-in geometry JSX elements with `args` for standard shapes 2. Use `useMemo` for custom BufferGeometry data arrays 3. Set `needsUpdate = true` on attributes after modifying vertex data 4. Use InstancedMesh for rendering many identical objects (1 draw call) 5. Pre-allocate `Object3D` and `Color` objects outside `useFrame` for instancing 6. Merge static geometries with `mergeGeometries` to reduce draw calls 7. Compute bounding volumes after geometry modifications 8. Use LOD components for distance-based detail levels 9. Index geometry when vertices are shared between triangles 10. Keep geometry component files under 250 lines