---
name: mml
description: >
  Expert reference for Metaverse Markup Language (MML) — the HTML-based markup for building
  multi-user 3D metaverse experiences. Use this skill whenever the user is writing, debugging,
  or asking about MML documents, MML elements (m-cube, m-model, m-character, m-frame, etc.),
  MML events, MML animations, or deploying MML content to virtual worlds. Also use when the
  user mentions Networked DOM, mml.io, MML WebSocket servers, the MML playground, or embedding
  interactive 3D objects in Otherside / The Construct. Trigger even if the user only mentions
  a specific element or event by its m- prefix name.
---

# MML (Metaverse Markup Language)

MML is an HTML-based markup language for building interactive 3D multi-user experiences.
Documents run **on a Node.js server** (via Networked DOM / JSDOM) and multiple clients connect
simultaneously over **WebSocket**. All JavaScript executes server-side; DOM mutations are
serialized and broadcast to every connected client in real time.

**Key mental model:** MML is NOT a browser page. It is a server process. Every `setAttribute`
call is multiplayer — all connected users see the change immediately. There is no `localStorage`,
no `window.location`, no browser-only APIs.

## Architecture

- Server runs the MML document using `@mml-io/networked-dom-server`
- Clients connect via `wss://` WebSocket URL
- User interactions (clicks, collisions) are sent to the server, processed, then re-broadcast
- Multiple rendering engines can connect to the same document simultaneously (THREE.js, PlayCanvas, Unreal Engine)
- Documents can be composed with `<m-frame>` embedding documents from other servers

**Starter project:** `git clone https://github.com/mml-io/mml-starter-project && npm install && npm start`
**Online viewer:** https://viewer.mml.io

---

## Elements

### Primitives
| Element | Purpose | Key unique attributes |
|---|---|---|
| `m-cube` | 3D box | `width`, `height`, `depth` (all default 1) |
| `m-sphere` | 3D sphere | `radius` (default 0.5) |
| `m-cylinder` | 3D cylinder | `radius`, `height` (default 1) |
| `m-plane` | Flat plane | `width`, `height` (default 1) |

All primitives support: `color`, `opacity`, `cast-shadows`, `collide`, `collision-interval`

### Media & Models
| Element | Purpose | Key unique attributes |
|---|---|---|
| `m-model` | GLB/glTF 3D model | `src` (required), `anim`, `anim-loop`, `anim-enabled`, `anim-start-time`, `anim-pause-time` |
| `m-character` | Skeletal avatar character | `src` (required), `anim` (URL to animation GLB) |
| `m-image` | 2D image in 3D space | `src` (required), `width`, `height` |
| `m-video` | Video in 3D space | `src`, `loop`, `enabled`, `volume`, `start-time`, `pause-time` |
| `m-audio` | Spatial audio | `src`, `loop`, `enabled`, `volume`, `start-time`, `pause-time` |
| `m-label` | 3D text | `content`, `font-size`, `width`, `height`, `alignment`, `color`, `font-color` |
| `m-light` | Light source | `type` (`point`/`spot`/`directional`), `intensity`, `distance`, `angle` |

### Interactive
| Element | Purpose | Key unique attributes |
|---|---|---|
| `m-interaction` | Proximity-activated interaction | `range` (default 5m), `prompt` (default "Interact"), `in-focus`, `priority` |
| `m-position-probe` | Detect players in range | `range` (default 10m), `interval` (default 100ms), `debug` |
| `m-chat-probe` | Receive nearby chat messages | `range` |
| `m-prompt` | Text input dialog | `message` (prompt text), `placeholder` |
| `m-link` | Clickable hyperlink area | `href` |

### Structure & Composition
| Element | Purpose | Key unique attributes |
|---|---|---|
| `m-group` | Transform container | none (transform + socket only) |
| `m-frame` | Embed external MML document | `src` (wss:// URL), `load-range`, `unload-range` |

### Animation
| Element | Purpose | Key unique attributes |
|---|---|---|
| `m-attr-anim` | Animates a parent attribute | `attr` (required), `start`, `end` (required), `duration` (ms, default 1000), `loop` (default true), `ping-pong`, `ping-pong-delay`, `easing`, `start-time`, `pause-time` |
| `m-attr-lerp` | Smoothly interpolates attribute changes | `attr` (required), `duration`, `easing` |
| `m-animation` | Multi-animation blending on m-model/m-character | `clip`, `weight`, `speed` |

---

## Common Attribute Groups

**Transform** (most 3D elements):
- `x`, `y`, `z` — position in meters (Y-up, right-hand coordinate system)
- `rx`, `ry`, `rz` — rotation in degrees (pitch, yaw, roll); applied X→Y→Z
- `sx`, `sy`, `sz` — scale multiplier (default 1)

**Visual** (primitives + label):
- `color` — CSS color: named (`red`), hex (`#ff5500`), `rgb(255,128,0)`, `hsl(180,100%,50%)`
- `opacity` — 0 to 1
- `cast-shadows` — boolean (default true)
- `visible` — boolean (default true)
- `debug` — boolean; shows axes/range sphere

**Collision** (primitives, model, character, image, video, label):
- `collide` — boolean (default true)
- `collision-interval` — ms between `collisionmove` events; **must be > 0 for collisionmove to fire**

**Socket** (bone attachment — child of m-model or m-character):
- `socket` — bone name string; child element follows the bone's transform during animation

---

## Events

All MML event data lives in `e.detail`, not directly on `e`.

| Event | Fires on | Key `e.detail` fields |
|---|---|---|
| `click` | primitives, model, character, image, video, label | `position` {x,y,z}, `connectionId` |
| `collisionstart` | collide-enabled elements | `position`, `connectionId` |
| `collisionmove` | collide-enabled elements (**requires `collision-interval > 0`**) | `position`, `connectionId` |
| `collisionend` | collide-enabled elements | `connectionId` |
| `positionenter` | `m-position-probe` | `position`, `connectionId` |
| `positionmove` | `m-position-probe` | `position`, `connectionId` |
| `positionleave` | `m-position-probe` | `connectionId` |
| `interact` | `m-interaction` | `connectionId` |
| `prompt` | `m-prompt` | `value` (string entered), `connectionId` |
| `chat` | `m-chat-probe` | `message`, `connectionId` |

**Connection events** fire on `window`, not `document`:
```js
window.addEventListener('connected', (e) => { /* e.detail.connectionId */ });
window.addEventListener('disconnected', (e) => { /* e.detail.connectionId */ });
```

---

## Scripting Patterns

### Basic setup
```html
<m-cube id="box" color="red"></m-cube>
<script>
  const box = document.getElementById('box');
  box.addEventListener('click', (e) => {
    box.setAttribute('color', 'blue');  // broadcasts to ALL connected users
  });
</script>
```

### Per-user state (critical for multiplayer)
```js
// Global variables are SHARED across all users — use Maps keyed by connectionId
const scores = new Map();
element.addEventListener('click', (e) => {
  const id = e.detail.connectionId;
  scores.set(id, (scores.get(id) || 0) + 1);
});
// Always clean up on disconnect
window.addEventListener('disconnected', (e) => scores.delete(e.detail.connectionId));
```

### Dynamic element creation
```js
const cube = document.createElement('m-cube');
cube.setAttribute('x', 5);
cube.setAttribute('color', 'green');
document.body.appendChild(cube);
// Remove later:
cube.remove();
```

### Triggering a non-looping animation on demand
```js
// You must set start-time to document.timeline.currentTime to start the animation NOW
const anim = document.getElementById('my-anim');
anim.setAttribute('start', currentValue);
anim.setAttribute('end', targetValue);
anim.setAttribute('start-time', document.timeline.currentTime);
```

### Attaching objects to character bones (socket)

Add a `socket` attribute to any child of `m-character` or `m-model` to pin it to a skeleton bone. The child tracks the bone's position and rotation as the character animates.

```html
<m-character src="https://aiml.sideload.gg/models/avt-xxx.glb"
             anim="https://aiml.sideload.gg/models/avt-xxx-idle.glb"
             sx="5" sy="5" sz="5">
  <!-- Sword in the right hand -->
  <m-model socket="hand_r"
           src="https://example.com/sword.glb"
           rx="180" ry="10" rz="-90"
           x="-0.15" y="-0.047" z="0.04"
           sx="0.1" sy="0.1" sz="0.1">
  </m-model>
  <!-- Use m-group to attach multiple items to one bone -->
  <m-group socket="head">
    <m-model src="https://example.com/hat.glb" y="0.1"></m-model>
  </m-group>
</m-character>
```

Bone names depend on the rig. Common UE5 Epic Skeleton names: `hand_r`, `hand_l`, `head`, `spine_01`, `foot_r`, `foot_l`. Use `debug="true"` on the parent to visualise the skeleton and identify exact bone names.

### Getting text input from a user (m-prompt)

`m-prompt` is the primary way to collect text from a specific user. Nest it inside `m-interaction` so the interaction button appears first, then the text dialog opens when the user activates it. The `prompt` event fires with the submitted text and the `connectionId` of who typed it.

```html
<m-interaction prompt="Enter your name" range="3">
  <m-prompt id="namePrompt" message="What's your name?" placeholder="Type here..."></m-prompt>
</m-interaction>
<script>
  document.getElementById('namePrompt').addEventListener('prompt', (e) => {
    const name = e.detail.value;          // text the user typed
    const id   = e.detail.connectionId;   // which user submitted it
    console.log(`${id} entered: ${name}`);
  });
</script>
```

### Fetch external data
```js
async function update() {
  const res = await fetch('https://api.example.com/data');
  const json = await res.json();
  document.getElementById('label').setAttribute('content', json.value);
}
update();
setInterval(update, 60_000);
```

---

## Animation Reference

`m-attr-anim` is the preferred way to animate — it runs client-side and generates no ongoing network traffic (unlike `setInterval` + `setAttribute`).

**Loop continuously:**
```html
<m-cube>
  <m-attr-anim attr="ry" start="0" end="360" duration="3000" loop="true"></m-attr-anim>
</m-cube>
```

**Float up and down (ping-pong):**
```html
<m-cube color="gold">
  <m-attr-anim attr="y" start="0" end="1" duration="2000" loop="true"
    ping-pong="true" easing="easeInOutQuad"></m-attr-anim>
</m-cube>
```
> **Note:** If the element or its parent has a non-zero `y`, set `start` to match that value (e.g. `start="5" end="6"` for a group at `y="5"`). `start`/`end` are absolute — not relative offsets.

**Rainbow color cycle:**
```html
<m-cube color="hsl(0,100%,50%)">
  <m-attr-anim attr="color" start="hsl(0,100%,50%)" end="hsl(360,100%,50%)"
    duration="5000" loop="true"></m-attr-anim>
</m-cube>
```

### Easing function cheatsheet
- **General purpose:** `easeInOutCubic`
- **Continuous rotation:** `linear`
- **Landing/dropping:** `easeOutBounce`
- **Playful buttons/UI:** `easeOutBack`
- **Entrance:** `easeInCubic` / `easeOutCubic`
- **Spring/elastic:** `easeOutElastic`

Full list (31 total): `linear` | `easeIn/Out/InOut` + `Sine`, `Quad`, `Cubic`, `Quart`, `Quint`, `Expo`, `Circ`, `Back`, `Elastic`, `Bounce`

---

## Coordinate System

- **Y-up, right-handed** — Y is up, X is right, Z is toward the viewer
- Units are **meters**; rotations are **degrees**
- Children inherit parent transforms (position, rotation, scale)
- `y="0"` = ground level; `y="1.5"` ≈ eye level; `y="2.5"` = above head
- Use `debug="true"` on any element to visualize axes (Red=X, Green=Y, Blue=Z)

---

## Common Mistakes

1. **`collisionmove` never fires** — you must set `collision-interval="100"` (or any value > 0) on the element.

2. **Connection events on document instead of window** — always use `window.addEventListener('connected', ...)`.

3. **Non-looping animation doesn't start** — you must set `start-time` to `document.timeline.currentTime` to trigger it. Setting `start-time="0"` sets a time in the past; the animation is treated as already finished and never plays:
   ```js
   // WRONG: time 0 is in the past — animation has already ended
   anim.setAttribute('start-time', 0);

   // RIGHT: starts the animation right now
   anim.setAttribute('start-time', document.timeline.currentTime);
   ```

4. **Accessing event data directly on `e`** — MML event data is in `e.detail.position`, `e.detail.connectionId`, etc.

5. **Using browser-only APIs** — MML runs on Node.js. No `localStorage`, `sessionStorage`, `window.location`, or DOM rendering APIs.

6. **Global variable treated as per-user state** — all connected users share the same script scope. Use `connectionId`-keyed Maps for per-user data.

7. **Memory leak from untracked user state** — always clean up Maps/Sets in a `disconnected` handler.

8. **`m-attr-anim` start/end don't account for parent offset** — `start` and `end` are absolute attribute values, not relative offsets. If a group is at `y="5"` and you animate `start="0" end="1"`, the element teleports to ground level first, then bobs between 0 and 1. Always include the element's current position in both values:
   ```html
   <!-- WRONG: group is at y=5, this teleports it to 0 first -->
   <m-group y="5">
     <m-attr-anim attr="y" start="0" end="1" loop="true" ping-pong="true"></m-attr-anim>
   </m-group>

   <!-- RIGHT: start and end include the y=5 offset -->
   <m-group y="5">
     <m-attr-anim attr="y" start="5" end="6" loop="true" ping-pong="true"></m-attr-anim>
   </m-group>
   ```
   The same applies to `rx`, `ry`, `rz`, `x`, `z`, `opacity`, or any attribute — `start` must match the element's current value, not zero.

9. **`connectionId` is not shared across `m-frame` boundaries** — a user's `connectionId` inside an embedded `m-frame` is scoped to that inner document's server and will not match their `connectionId` in the outer world. Don't pass `connectionId` between outer and inner documents expecting them to be equal. Use an external API or shared data store for cross-frame coordination.

---

## Performance Tips

- Prefer `m-attr-anim` over `setInterval`+`setAttribute` for animations — animations run client-side with no ongoing network traffic
- Set `collision-interval` as high as tolerable (100–500ms); avoid 10ms or less
- Use `m-position-probe` for proximity checks instead of collision when possible
- Disable `collide` on decorative elements that don't need it
- Remove elements with `.remove()` when no longer needed; use `visible="false"` when they'll return
- Avoid creating hundreds of elements dynamically — batch or reuse elements
- Batch related `setAttribute` calls; avoid rapid calls in tight loops (each is a network event)
- Compress GLB assets with Draco; keep textures appropriately sized

---

## m-frame Composition

Embed external MML documents to distribute compute and compose worlds:
```html
<m-frame src="wss://other-server.com/document.html" x="10" y="0" z="0"></m-frame>
```
- Each embedded document runs on its own server with its own script context
- Embedded content cannot access the parent DOM (sandboxed)
- Use `load-range`/`unload-range` for large worlds with many frames
- Use `wss://` (secure) in production

> ⚠️ **`connectionId` inside an `m-frame` is scoped to the inner document's server — it is not the same as the outer world's `connectionId` for the same user.** Do not try to coordinate between outer and inner documents using `connectionId` — the values won't match. Use shared external state (a database, API endpoint, or pub/sub service) to communicate across frame boundaries instead.

---

## Creating Avatars & Objects

For getting 3D assets into MML quickly, see the **MML Tools skill** (`mml-tools`) which covers
the full tooling ecosystem. Quick options:

| Goal | Tool | How |
|---|---|---|
| Generate a character from a photo or text | **Sideload** — https://sideload.gg | Upload image → get `<m-character src="...">` |
| Generate a 3D object from a photo or text | **Sideload Objects** — https://sideload.gg/objects/generate | Upload image → get `<m-model src="...">` |
| Convert a VRM file to an MML character | **Sideload Convert** — https://sideload.gg/convert | Upload VRM → get MML-compatible GLB |
| Test / inspect a GLB or VRM | **Sideload Debug** — https://sideload.gg/debug | Drag & drop to preview |
| Write and preview MML in a browser | **MML Editor** — https://mmleditor.com | Visual composer + code editor, no install |
| Agent-driven character generation | **Sideload Agent API** — https://sideload.gg/agents | `POST /api/agent/generate` via x402 (2 USDC) |
| Convert any mesh to a rigged MML character | **Blender workflow** — https://directivecreator.com/mml/avatar-tutorial | Manual rig with Blender + 3 add-ons; full control |
| Batch-generate MML files for a whole collection | **MML Avatar Starterkit** — https://mml-avatar-starterkit.onrender.com/ | Paste GLB URLs → download ZIP of `.mml` files |

### Using generated assets in MML

Once you have a GLB URL from Sideload (or any host), drop it into your document:

```html
<!-- Character (rigged, animatable) -->
<m-character src="https://aiml.sideload.gg/models/avt-xxx.glb"
             anim="https://aiml.sideload.gg/models/avt-xxx-idle.glb">
</m-character>

<!-- Static or animated object -->
<m-model src="https://aiml.sideload.gg/models/obj-xxx.glb"></m-model>
```

**Full reference:**
- `m-character`: https://mml.io/docs/reference/elements/m-character
- `m-model`: https://mml.io/docs/reference/elements/m-model
- MML docs: https://mml.io/docs

---

## Key npm Packages

| Package | Role |
|---|---|
| `@mml-io/networked-dom-server` | Run MML on Node.js |
| `@mml-io/networked-dom-web` | WebSocket client |
| `@mml-io/mml-web-threejs` | THREE.js renderer |
| `@mml-io/3d-web-experience-server` | Full world server |
| `@mml-io/3d-web-experience-client` | Full world client |

**Source:** https://github.com/mml-io/mml | https://mml.io/docs
