# Custom geometry and behaviors

> Go beyond the built-in pieces. Write reusable per-frame behaviors with defineBehavior, draw your own three.js geometry with Mesh, bake multi-part objects into one draw call, add existing three.js objects, register material presets and studios, and build your own scene components.

Most apps never need this page. Reach for it when no shape, model or behavior does the job.

## Your own behavior: `defineBehavior`

A behavior is a component that wraps children and moves them every frame, like `<Spin>`. `defineBehavior` turns a frame function into one. Every prop accepts a plain value or a shared value.

```tsx title="Bob.tsx"
import { defineBehavior } from "@apricot/react";

/** Floats its children up and down. */
export const Bob = defineBehavior({
  name: "Bob",
  defaults: { height: 0.2, speed: 1.5 },
  frame: (object, { height, speed }, { time }) => {
    object.position.y = Math.sin(time * speed * Math.PI * 2) * height;
    return true;
  },
});
```

```tsx title="Floating.tsx"
import { Scene, Spin, Torus } from "@apricot/react";
import { Bob } from "./Bob";

export const Floating = () => (
  <Scene style={{ flex: 1 }} look="studio">
    <Bob height={0.3}>
      <Spin speed={0.8}>
        <Torus material="gold" />
      </Spin>
    </Bob>
  </Scene>
);
```

| Field | Meaning |
| --- | --- |
| `name` | The component's display name, and the wrapping group's name |
| `defaults` | Every prop with its default value. The keys become the component's props |
| `frame(object, props, state)` | Runs before each frame. `object` is a group wrapping the children; `props` are the current values (shared values already read); `state` is `{ time, delta, frame }` |

Return `true` from `frame` while something still moves, so the scene keeps rendering under `frameloop="demand"`; return `false` when it is at rest. Change the wrapping group (its `position`, `rotation`, `scale`), not the children, so behaviors stack.

`Spin` is built the same way:

```ts
export const Spin = defineBehavior({
  name: "Spin",
  defaults: { speed: 1, axis: [0, 1, 0] as Vec3 },
  frame: (object, { speed, axis }, { delta }) => stepSpin(object, speed, axis, delta),
});
```

For one-off logic in a single component, `useFrame` is simpler: see [Animation](https://matthiasbrandolin.dev/apricot/concepts/animation#3-useframe).

## Your own geometry: `<Mesh>`

`<Mesh geometry>` draws any three.js `BufferGeometry` with a preset material, and takes the same props as the shapes (`material`, `color`, `onPress`, transforms, shadows).

```tsx title="Star.tsx"
import { Mesh } from "@apricot/react";
import { useEffect, useMemo } from "react";
import * as THREE from "three/webgpu";

function starShape(points = 5, outer = 1, inner = 0.45) {
  const shape = new THREE.Shape();
  for (let i = 0; i < points * 2; i++) {
    const r = i % 2 === 0 ? outer : inner;
    const a = (i / (points * 2)) * Math.PI * 2 + Math.PI / 2;
    if (i === 0) shape.moveTo(Math.cos(a) * r, Math.sin(a) * r);
    else shape.lineTo(Math.cos(a) * r, Math.sin(a) * r);
  }
  return shape;
}

export function Star({ color = "#e9bd63" }: { color?: string }) {
  const geometry = useMemo(() => {
    const g = new THREE.ExtrudeGeometry(starShape(), { depth: 0.2, bevelEnabled: true, bevelSize: 0.04, bevelThickness: 0.04 });
    g.center();
    return g;
  }, []);
  // The geometry is yours: the mesh does not dispose it.
  useEffect(() => () => geometry.dispose(), [geometry]);
  return <Mesh geometry={geometry} material="gold" color={color} />;
}
```

- Import three.js from `three/webgpu` (Metro resolves plain `three` there too, with `withApricot`).
- Build the geometry once (`useMemo`) and dispose it on unmount. Rebuilding it every render rebuilds the mesh's buffers every render.
- Geometries you make are yours: Apricot never disposes them. See [Objects and lifetime](https://matthiasbrandolin.dev/apricot/concepts/objects).

## One draw call for a multi-part object: `mergeColoredParts`

Each mesh costs a draw call. `mergeColoredParts` bakes several geometries, each with its own colour, into **one** geometry with vertex colours. Draw it with `vertexColors: true` and a white material. The apple, peach and toy car in `@apricot/assets` are built this way.

```tsx title="Mushroom.tsx"
import { mergeColoredParts } from "@apricot/core";
import { Mesh } from "@apricot/react";
import { useEffect, useMemo } from "react";
import * as THREE from "three/webgpu";

export function Mushroom() {
  const geometry = useMemo(
    () =>
      mergeColoredParts([
        { geometry: new THREE.CylinderGeometry(0.18, 0.24, 0.6, 24), color: "#f3ead8" },
        {
          geometry: new THREE.SphereGeometry(0.5, 32, 16, 0, Math.PI * 2, 0, Math.PI / 2),
          color: "#d9342b",
          matrix: new THREE.Matrix4().makeTranslation(0, 0.25, 0),
        },
      ]),
    [],
  );
  useEffect(() => () => geometry.dispose(), [geometry]);
  return <Mesh geometry={geometry} material={{ preset: "clay", color: "#ffffff", vertexColors: true }} />;
}
```

A part's `color` is a colour, a function of each vertex's position (for gradients), or `"vertex"` to keep the geometry's own `color` attribute. `matrix` places the part. UVs are dropped, so merged objects cannot take image textures.

## Existing three.js objects: `<Primitive>`

`<Primitive object>` adds any three.js `Object3D` as it is, with transform props and children. Use it for objects from the `@apricot/assets` factories (`createCar()`, `createTrees()`) or from other three.js code.

```tsx title="Lamp.tsx"
import { Primitive } from "@apricot/react";
import { disposeObject } from "@apricot/core";
import { useEffect, useMemo } from "react";
import * as THREE from "three/webgpu";

export function Lamp() {
  const lamp = useMemo(() => {
    const group = new THREE.Group();
    const pole = new THREE.Mesh(new THREE.CylinderGeometry(0.03, 0.03, 2), new THREE.MeshStandardNodeMaterial({ color: "#222222" }));
    pole.position.y = 1;
    group.add(pole);
    return group;
  }, []);
  useEffect(() => () => disposeObject(lamp), [lamp]);
  return <Primitive object={lamp} position={[1, 0, 0]} />;
}
```

`disposeObject(object)` frees every geometry, material and texture under an object. Call it on unmount for objects you built.

## Your own material presets: `definePreset`

Register a preset once, at startup (for example next to your theme), and use its name anywhere a preset is accepted:

```ts title="apricot-presets.ts"
import { definePreset } from "@apricot/react";

definePreset("brand-orange", { color: "#ff7a1a", roughness: 0.35, metalness: 0, clearcoat: 0.6, clearcoatRoughness: 0.1 });
definePreset("frosted", { color: "#ffffff", roughness: 0.35, metalness: 0, transmission: 1, ior: 1.4, thickness: 0.2 });
```

A `PresetSpec` needs `color`, `roughness` and `metalness`, and can add `clearcoat`, `clearcoatRoughness`, `brushed`, `flakes`, `transmission`, `ior`, `thickness`, `attenuationColor`, `attenuationDistance`, `emissive` (a colour or `"color"`) and `emissiveIntensity`. Registering an existing name replaces it. See [Materials reference](https://matthiasbrandolin.dev/apricot/reference/materials#presetspec).

## Your own studio: `defineEnvironment`

An environment is a procedural studio: a three-colour sky and up to six soft boxes, evaluated in the shader.

```ts title="apricot-environments.ts"
import { defineEnvironment } from "@apricot/react";

defineEnvironment("warm-studio", {
  sky: ["#5a4a3c", "#2a221c", "#0d0b09"],
  boxes: [
    { direction: [0.4, 0.6, 0.7], size: [0.3, 0.15], intensity: 6, color: "#ffe2c0" },
    { direction: [-0.9, 0.1, 0.4], size: [0.06, 0.6], intensity: 4, color: "#fff4e6" },
  ],
  intensity: 1,
});
```

Then `<Scene environment="warm-studio">` or `<Environment name="warm-studio" />`. A box's `direction` points from the object towards the box (+z is towards the default camera); `size` is its half width and half height in radians. See [Lighting](https://matthiasbrandolin.dev/apricot/guides/lighting).

## Your own scene components

Components built on the scene use the same hooks Apricot's own do:

| Hook | Use |
| --- | --- |
| `useScene()` | The scene: `invalidate()`, `camera`, `scene`, `size`, `onResize`, `ready` |
| `useFrame(cb)` | Per-frame code; return `true` while moving |
| `useAnimatable(value)` | Read a plain-or-shared value inside `useFrame` |
| `useParent()` / `useAttach(object)` | Attach a three.js object to the nearest Apricot parent while mounted |
| `ParentContext` | Make your object the parent of your component's children |
| `useTransform(object, props)` | Apply `position`, `rotation`, `scale` props (plain or shared) |

```tsx title="Pulse.tsx"
import { type Animatable, ParentContext, useAnimatable, useAttach, useFrame } from "@apricot/react";
import { type ReactNode, useState } from "react";
import * as THREE from "three/webgpu";

/** Scales its children with a heartbeat. */
export function Pulse({ rate = 1, children }: { rate?: Animatable<number>; children?: ReactNode }) {
  const [group] = useState(() => new THREE.Group());
  useAttach(group);
  const readRate = useAnimatable(rate);
  useFrame(({ time }) => {
    const beat = 1 + Math.max(0, Math.sin(time * Math.PI * 2 * readRate())) * 0.08;
    group.scale.setScalar(beat);
    return true;
  });
  return <ParentContext.Provider value={group}>{children}</ParentContext.Provider>;
}
```

All of these must run inside `<Scene>`. See [Hooks reference](https://matthiasbrandolin.dev/apricot/reference/hooks).

## Related

- [Objects and lifetime](https://matthiasbrandolin.dev/apricot/concepts/objects).
- [Core reference](https://matthiasbrandolin.dev/apricot/reference/core): `mergeColoredParts`, `disposeObject`, `seededRandom`.
- [Performance](https://matthiasbrandolin.dev/apricot/concepts/performance).
