
R3f Geometry
- 61 installs
- 8 repo stars
- Updated August 4, 2026
- bbeierle12/skill-mcp-claude
r3f-geometry is a Claude skill for creating and optimizing 3D geometry in React Three Fiber, including BufferGeometry, custom buffer attributes, and instanced meshes.
About
This skill teaches how to define 3D shapes in React Three Fiber using BufferGeometry, built-in primitives, and custom vertex data in buffer attributes. A developer uses it when creating custom meshes, working directly with position/normal/uv arrays, or optimizing with instanced meshes to render many objects at once. It also covers indexed geometry and animating geometry per frame.
- Covers BufferGeometry, built-in geometries, and custom geometry from raw buffer attributes in React Three Fiber
- Explains instanced meshes for rendering thousands of objects in one draw call
- Includes dynamic geometry updates and indexed geometry patterns with code examples
R3f Geometry by the numbers
- 61 all-time installs (skills.sh)
- Ranked #1,203 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
r3f-geometry capabilities & compatibility
- Capabilities
- r3f materials · r3f performance
- Use cases
- frontend · ui design
What r3f-geometry says it does
BufferGeometry creation, built-in geometries, custom geometry with buffer attributes, instanced meshes for rendering thousands of objects
Geometry defines the shape of 3D objects via vertices, faces, normals, and UVs stored in buffer attributes.
Render thousands of identical meshes with different transforms in a single draw call:
npx skills add https://github.com/bbeierle12/skill-mcp-claude --skill r3f-geometryAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 61 |
|---|---|
| repo stars | ★ 8 |
| Last updated | August 4, 2026 |
| Repository | bbeierle12/skill-mcp-claude ↗ |
What it does
Build custom 3D geometry and instanced meshes in a React Three Fiber scene.
Who is it for?
Building custom or instanced 3D geometry in React Three Fiber apps.
By the numbers
- Instanced mesh example renders 10,000 identical meshes with different transforms in a single draw call
Files
R3F Geometry
Geometry defines the shape of 3D objects via vertices, faces, normals, and UVs stored in buffer attributes.
Quick Start
// Built-in geometry
<mesh>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial />
</mesh>
// Custom geometry
<mesh>
<bufferGeometry>
<bufferAttribute
attach="attributes-position"
count={3}
array={new Float32Array([0, 0, 0, 1, 0, 0, 0.5, 1, 0])}
itemSize={3}
/>
</bufferGeometry>
<meshBasicMaterial side={THREE.DoubleSide} />
</mesh>Built-in Geometries
All geometries accept args array matching constructor parameters:
// Box: [width, height, depth, widthSegments?, heightSegments?, depthSegments?]
<boxGeometry args={[1, 2, 1, 1, 2, 1]} />
// Sphere: [radius, widthSegments, heightSegments, phiStart?, phiLength?, thetaStart?, thetaLength?]
<sphereGeometry args={[1, 32, 32]} />
// Plane: [width, height, widthSegments?, heightSegments?]
<planeGeometry args={[10, 10, 10, 10]} />
// Cylinder: [radiusTop, radiusBottom, height, radialSegments?, heightSegments?, openEnded?]
<cylinderGeometry args={[0.5, 0.5, 2, 32]} />
// Cone: [radius, height, radialSegments?, heightSegments?, openEnded?]
<coneGeometry args={[1, 2, 32]} />
// Torus: [radius, tube, radialSegments, tubularSegments, arc?]
<torusGeometry args={[1, 0.3, 16, 100]} />
// TorusKnot: [radius, tube, tubularSegments, radialSegments, p?, q?]
<torusKnotGeometry args={[1, 0.3, 100, 16]} />
// Ring: [innerRadius, outerRadius, thetaSegments?, phiSegments?]
<ringGeometry args={[0.5, 1, 32]} />
// Circle: [radius, segments?, thetaStart?, thetaLength?]
<circleGeometry args={[1, 32]} />
// Dodecahedron/Icosahedron/Octahedron/Tetrahedron: [radius, detail?]
<icosahedronGeometry args={[1, 0]} />Buffer Attributes
Geometry data lives in typed arrays attached as attributes:
| Attribute | ItemSize | Purpose |
|---|---|---|
position | 3 | Vertex positions (x, y, z) |
normal | 3 | Surface normals for lighting |
uv | 2 | Texture coordinates (u, v) |
color | 3 | Per-vertex colors (r, g, b) |
index | 1 | Triangle indices (optional) |
Custom Geometry from Scratch
import { useMemo } from 'react';
import * as THREE from 'three';
function Triangle() {
const geometry = useMemo(() => {
const geo = new THREE.BufferGeometry();
// 3 vertices × 3 components (x, y, z)
const positions = new Float32Array([
-1, -1, 0, // vertex 0
1, -1, 0, // vertex 1
0, 1, 0 // vertex 2
]);
// 3 vertices × 3 components (nx, ny, nz)
const normals = new Float32Array([
0, 0, 1,
0, 0, 1,
0, 0, 1
]);
// 3 vertices × 2 components (u, v)
const uvs = new Float32Array([
0, 0,
1, 0,
0.5, 1
]);
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geo.setAttribute('normal', new THREE.BufferAttribute(normals, 3));
geo.setAttribute('uv', new THREE.BufferAttribute(uvs, 2));
return geo;
}, []);
return (
<mesh geometry={geometry}>
<meshStandardMaterial side={THREE.DoubleSide} />
</mesh>
);
}Declarative Buffer Attributes
function Triangle() {
const positions = useMemo(() =>
new Float32Array([-1, -1, 0, 1, -1, 0, 0, 1, 0]),
[]);
return (
<mesh>
<bufferGeometry>
<bufferAttribute
attach="attributes-position"
count={3}
array={positions}
itemSize={3}
/>
</bufferGeometry>
<meshBasicMaterial side={THREE.DoubleSide} />
</mesh>
);
}Indexed Geometry
Use indices to share vertices between triangles:
function Quad() {
const geometry = useMemo(() => {
const geo = new THREE.BufferGeometry();
// 4 unique vertices
const positions = new Float32Array([
-1, -1, 0, // 0: bottom-left
1, -1, 0, // 1: bottom-right
1, 1, 0, // 2: top-right
-1, 1, 0 // 3: top-left
]);
// 2 triangles, 6 indices
const indices = new Uint16Array([
0, 1, 2, // first triangle
0, 2, 3 // second triangle
]);
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geo.setIndex(new THREE.BufferAttribute(indices, 1));
geo.computeVertexNormals();
return geo;
}, []);
return (
<mesh geometry={geometry}>
<meshStandardMaterial side={THREE.DoubleSide} />
</mesh>
);
}Dynamic Geometry Updates
import { useRef } from 'react';
import { useFrame } from '@react-three/fiber';
import * as THREE from 'three';
function WavingPlane() {
const geometryRef = useRef<THREE.BufferGeometry>(null!);
useFrame(({ clock }) => {
const positions = geometryRef.current.attributes.position;
const time = clock.elapsedTime;
for (let i = 0; i < positions.count; i++) {
const x = positions.getX(i);
const y = positions.getY(i);
const z = Math.sin(x * 2 + time) * Math.cos(y * 2 + time) * 0.5;
positions.setZ(i, z);
}
positions.needsUpdate = true; // Critical!
geometryRef.current.computeVertexNormals();
});
return (
<mesh rotation={[-Math.PI / 2, 0, 0]}>
<planeGeometry ref={geometryRef} args={[10, 10, 50, 50]} />
<meshStandardMaterial color="royalblue" side={THREE.DoubleSide} />
</mesh>
);
}Instanced Mesh
Render thousands of identical meshes with different transforms in a single draw call:
import { useRef, useMemo } from 'react';
import { useFrame } from '@react-three/fiber';
import * as THREE from 'three';
function Particles({ count = 1000 }) {
const meshRef = useRef<THREE.InstancedMesh>(null!);
// Pre-allocate transformation objects
const dummy = useMemo(() => new THREE.Object3D(), []);
// Initialize instance matrices
useEffect(() => {
for (let i = 0; i < count; i++) {
dummy.position.set(
(Math.random() - 0.5) * 10,
(Math.random() - 0.5) * 10,
(Math.random() - 0.5) * 10
);
dummy.rotation.set(
Math.random() * Math.PI,
Math.random() * Math.PI,
0
);
dummy.scale.setScalar(0.1 + Math.random() * 0.2);
dummy.updateMatrix();
meshRef.current.setMatrixAt(i, dummy.matrix);
}
meshRef.current.instanceMatrix.needsUpdate = true;
}, [count, dummy]);
// Animate instances
useFrame(({ clock }) => {
for (let i = 0; i < count; i++) {
meshRef.current.getMatrixAt(i, dummy.matrix);
dummy.matrix.decompose(dummy.position, dummy.quaternion, dummy.scale);
dummy.rotation.x += 0.01;
dummy.rotation.y += 0.01;
dummy.updateMatrix();
meshRef.current.setMatrixAt(i, dummy.matrix);
}
meshRef.current.instanceMatrix.needsUpdate = true;
});
return (
<instancedMesh ref={meshRef} args={[undefined, undefined, count]}>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="hotpink" />
</instancedMesh>
);
}Instance Colors
function ColoredInstances({ count = 1000 }) {
const meshRef = useRef<THREE.InstancedMesh>(null!);
useEffect(() => {
const color = new THREE.Color();
for (let i = 0; i < count; i++) {
color.setHSL(i / count, 1, 0.5);
meshRef.current.setColorAt(i, color);
}
meshRef.current.instanceColor!.needsUpdate = true;
}, [count]);
return (
<instancedMesh ref={meshRef} args={[undefined, undefined, count]}>
<sphereGeometry args={[0.1, 16, 16]} />
<meshStandardMaterial />
</instancedMesh>
);
}Instance Attributes (Custom Data)
function CustomInstanceData({ count = 1000 }) {
const meshRef = useRef<THREE.InstancedMesh>(null!);
// Custom per-instance data
const speeds = useMemo(() => {
const arr = new Float32Array(count);
for (let i = 0; i < count; i++) {
arr[i] = 0.5 + Math.random();
}
return arr;
}, [count]);
useEffect(() => {
// Attach as instanced buffer attribute
meshRef.current.geometry.setAttribute(
'aSpeed',
new THREE.InstancedBufferAttribute(speeds, 1)
);
}, [speeds]);
return (
<instancedMesh ref={meshRef} args={[undefined, undefined, count]}>
<boxGeometry />
<shaderMaterial
vertexShader={`
attribute float aSpeed;
varying float vSpeed;
void main() {
vSpeed = aSpeed;
gl_Position = projectionMatrix * modelViewMatrix * instanceMatrix * vec4(position, 1.0);
}
`}
fragmentShader={`
varying float vSpeed;
void main() {
gl_FragColor = vec4(vSpeed, 0.5, 1.0 - vSpeed, 1.0);
}
`}
/>
</instancedMesh>
);
}Geometry Utilities
Compute Normals
const geometry = useMemo(() => {
const geo = new THREE.BufferGeometry();
// ... set positions
geo.computeVertexNormals(); // Auto-calculate smooth normals
return geo;
}, []);Compute Bounding Box/Sphere
useEffect(() => {
geometry.computeBoundingBox();
geometry.computeBoundingSphere();
console.log(geometry.boundingBox); // THREE.Box3
console.log(geometry.boundingSphere); // THREE.Sphere
}, [geometry]);Center Geometry
const geometry = useMemo(() => {
const geo = new THREE.BoxGeometry(2, 3, 1);
geo.center(); // Move to origin
return geo;
}, []);Merge Geometries
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils';
const merged = useMemo(() => {
const box = new THREE.BoxGeometry(1, 1, 1);
const sphere = new THREE.SphereGeometry(0.5, 16, 16);
sphere.translate(0, 1, 0);
return mergeGeometries([box, sphere]);
}, []);Performance Tips
| Technique | When to Use | Impact |
|---|---|---|
| Instancing | 100+ identical meshes | Massive |
| Indexed geometry | Shared vertices | Moderate |
| Lower segments | Non-hero geometry | Moderate |
| Merge geometries | Static scene | Moderate |
| Dispose unused | Dynamic loading | Memory |
Disposal
useEffect(() => {
return () => {
geometry.dispose(); // Clean up GPU memory
};
}, [geometry]);File Structure
r3f-geometry/
├── SKILL.md
├── references/
│ ├── buffer-attributes.md # Deep-dive on attribute types
│ ├── instancing-patterns.md # Advanced instancing
│ └── procedural-shapes.md # Algorithmic geometry
└── scripts/
├── procedural/
│ ├── grid.ts # Grid mesh generator
│ ├── terrain.ts # Heightmap terrain
│ └── tube.ts # Custom tube geometry
└── utils/
├── geometry-utils.ts # Merge, center, clone
└── instancing.ts # Instance helpersReference
references/buffer-attributes.md— All attribute types and usagereferences/instancing-patterns.md— Advanced instancing techniquesreferences/procedural-shapes.md— Generating geometry algorithmically
{
"name": "r3f-geometry",
"description": "BufferGeometry creation, built-in geometries, custom geometry with buffer attributes, instanced meshes for rendering thousands of objects, and geometry manipulation. Use when creating custom shapes, optimizing with instancing, or working with vertex data directly.",
"tags": [
"3d",
"r3f",
"react",
"code-generation"
],
"sub_skills": [],
"source": "claude-user",
"type": "template",
"depends_on": [
"r3f-fundamentals"
],
"enhances": [
"r3f-performance"
],
"last_reviewed_at": null,
"review_score": null,
"relevance_tier": null
}
Instancing Patterns
Advanced patterns for rendering thousands of objects efficiently with InstancedMesh.
Core Concept
InstancedMesh renders N copies of the same geometry+material in a single draw call. Each instance can have unique:
- Position, rotation, scale (via matrix)
- Color (via instanceColor)
- Custom attributes (via InstancedBufferAttribute)
Basic Setup
import { useRef, useEffect, useMemo } from 'react';
import { useFrame } from '@react-three/fiber';
import * as THREE from 'three';
interface ParticlesProps {
count: number;
}
function Particles({ count }: ParticlesProps) {
const meshRef = useRef<THREE.InstancedMesh>(null!);
const dummy = useMemo(() => new THREE.Object3D(), []);
// Initialize positions
useEffect(() => {
for (let i = 0; i < count; i++) {
dummy.position.set(
(Math.random() - 0.5) * 10,
(Math.random() - 0.5) * 10,
(Math.random() - 0.5) * 10
);
dummy.updateMatrix();
meshRef.current.setMatrixAt(i, dummy.matrix);
}
meshRef.current.instanceMatrix.needsUpdate = true;
}, [count, dummy]);
return (
<instancedMesh ref={meshRef} args={[undefined, undefined, count]}>
<boxGeometry args={[0.1, 0.1, 0.1]} />
<meshStandardMaterial />
</instancedMesh>
);
}Per-Instance Colors
function ColoredInstances({ count }: { count: number }) {
const meshRef = useRef<THREE.InstancedMesh>(null!);
useEffect(() => {
const color = new THREE.Color();
for (let i = 0; i < count; i++) {
// Rainbow colors
color.setHSL(i / count, 1, 0.5);
meshRef.current.setColorAt(i, color);
}
// Critical: mark colors for upload
meshRef.current.instanceColor!.needsUpdate = true;
}, [count]);
return (
<instancedMesh ref={meshRef} args={[undefined, undefined, count]}>
<sphereGeometry args={[0.1, 16, 16]} />
<meshStandardMaterial />
</instancedMesh>
);
}Animated Instances
function AnimatedInstances({ count }: { count: number }) {
const meshRef = useRef<THREE.InstancedMesh>(null!);
const dummy = useMemo(() => new THREE.Object3D(), []);
// Store per-instance data
const particles = useMemo(() => {
return Array.from({ length: count }, () => ({
position: new THREE.Vector3(
(Math.random() - 0.5) * 10,
(Math.random() - 0.5) * 10,
(Math.random() - 0.5) * 10
),
velocity: new THREE.Vector3(
(Math.random() - 0.5) * 0.02,
(Math.random() - 0.5) * 0.02,
(Math.random() - 0.5) * 0.02
),
scale: 0.05 + Math.random() * 0.1
}));
}, [count]);
useFrame(() => {
particles.forEach((particle, i) => {
// Update position
particle.position.add(particle.velocity);
// Wrap around bounds
if (Math.abs(particle.position.x) > 5) particle.velocity.x *= -1;
if (Math.abs(particle.position.y) > 5) particle.velocity.y *= -1;
if (Math.abs(particle.position.z) > 5) particle.velocity.z *= -1;
// Apply to instance
dummy.position.copy(particle.position);
dummy.scale.setScalar(particle.scale);
dummy.updateMatrix();
meshRef.current.setMatrixAt(i, dummy.matrix);
});
meshRef.current.instanceMatrix.needsUpdate = true;
});
return (
<instancedMesh ref={meshRef} args={[undefined, undefined, count]}>
<sphereGeometry args={[1, 8, 8]} />
<meshStandardMaterial />
</instancedMesh>
);
}Custom Instance Attributes
Pass arbitrary per-instance data to shaders:
function CustomAttributeInstances({ count }: { count: number }) {
const meshRef = useRef<THREE.InstancedMesh>(null!);
// Custom per-instance data
const speeds = useMemo(() => {
const arr = new Float32Array(count);
for (let i = 0; i < count; i++) {
arr[i] = 0.5 + Math.random() * 2;
}
return arr;
}, [count]);
const phases = useMemo(() => {
const arr = new Float32Array(count);
for (let i = 0; i < count; i++) {
arr[i] = Math.random() * Math.PI * 2;
}
return arr;
}, [count]);
useEffect(() => {
const geometry = meshRef.current.geometry;
geometry.setAttribute(
'aSpeed',
new THREE.InstancedBufferAttribute(speeds, 1)
);
geometry.setAttribute(
'aPhase',
new THREE.InstancedBufferAttribute(phases, 1)
);
}, [speeds, phases]);
return (
<instancedMesh ref={meshRef} args={[undefined, undefined, count]}>
<boxGeometry args={[0.1, 0.1, 0.1]} />
<shaderMaterial
uniforms={{ uTime: { value: 0 } }}
vertexShader={`
attribute float aSpeed;
attribute float aPhase;
uniform float uTime;
void main() {
vec3 pos = position;
// Oscillate based on speed and phase
float offset = sin(uTime * aSpeed + aPhase) * 0.5;
vec4 mvPosition = modelViewMatrix * instanceMatrix * vec4(pos, 1.0);
mvPosition.y += offset;
gl_Position = projectionMatrix * mvPosition;
}
`}
fragmentShader={`
void main() {
gl_FragColor = vec4(1.0, 0.5, 0.2, 1.0);
}
`}
/>
</instancedMesh>
);
}Drei Instances Helper
Simpler API for common cases:
import { Instances, Instance } from '@react-three/drei';
function DreiInstances() {
const positions = useMemo(() =>
Array.from({ length: 100 }, () => [
(Math.random() - 0.5) * 10,
(Math.random() - 0.5) * 10,
(Math.random() - 0.5) * 10
] as [number, number, number])
, []);
return (
<Instances limit={100}>
<boxGeometry args={[0.2, 0.2, 0.2]} />
<meshStandardMaterial />
{positions.map((pos, i) => (
<Instance
key={i}
position={pos}
rotation={[Math.random(), Math.random(), 0]}
color={`hsl(${i * 3.6}, 100%, 50%)`}
/>
))}
</Instances>
);
}Performance Tips
Pre-allocate Objects
// Bad: Creates new objects every frame
useFrame(() => {
const pos = new THREE.Vector3(); // Garbage!
const quat = new THREE.Quaternion(); // Garbage!
});
// Good: Reuse objects
const pos = useMemo(() => new THREE.Vector3(), []);
const quat = useMemo(() => new THREE.Quaternion(), []);
useFrame(() => {
pos.set(1, 2, 3);
quat.setFromEuler(/* ... */);
});Batch Updates
// Update all instances, then mark once
useFrame(() => {
for (let i = 0; i < count; i++) {
// ... update matrices
meshRef.current.setMatrixAt(i, dummy.matrix);
}
// Single GPU upload
meshRef.current.instanceMatrix.needsUpdate = true;
});Frustum Culling
For large instance counts, consider manual culling:
function CulledInstances({ count }: { count: number }) {
const meshRef = useRef<THREE.InstancedMesh>(null!);
useFrame(({ camera }) => {
const frustum = new THREE.Frustum();
const matrix = new THREE.Matrix4();
matrix.multiplyMatrices(
camera.projectionMatrix,
camera.matrixWorldInverse
);
frustum.setFromProjectionMatrix(matrix);
// Only update visible instances
let visibleCount = 0;
particles.forEach((p, i) => {
if (frustum.containsPoint(p.position)) {
// Update this instance
visibleCount++;
}
});
});
}Instance Limits
| Instance Count | Performance | Notes |
|---|---|---|
| < 1,000 | Excellent | No optimization needed |
| 1,000 - 10,000 | Good | Use instancing |
| 10,000 - 100,000 | Fair | Consider GPU particles |
| > 100,000 | Variable | Need custom shaders, compute |