3D Web Experience

Open source multiplayer framework for shared 3D environments in the browser. Avatars, real-time networking, and MML.

<m-model src="/assets/museum.glb" x="-20" z="2" sx="20" sy="20" sz="20"></m-model>
<m-light type="point" y="10" intensity="100"></m-light>
<m-cube id="cube1" color="red" x="-3" y="1.5" z="-5">
  <m-attr-anim attr="ry" start="0" end="360" duration="5000"></m-attr-anim>
</m-cube>
<m-cube id="cube2" color="green" x="3" y="1.5" z="-5">
  <m-attr-anim attr="ry" start="0" end="360" duration="5000"></m-attr-anim>
</m-cube>
<m-group id="stairs" ry="180" y="0.1"></m-group>
<script>
  const colors = ["red", "green", "blue", "orange", "purple", "cyan", "yellow"];
  document.querySelectorAll("#cube1, #cube2").forEach(cube => {
    cube.addEventListener("click", () => {
      cube.setAttribute("color", colors[Math.floor(Math.random() * colors.length)]);
    });
  });

  const stairs = document.getElementById("stairs");

  function getHexForCurrentTime(lightness) {
    const hue = ((Date.now() % 2000)/2000) * 360;
    const saturation = 1.0;
    const alpha = saturation * Math.min(lightness, 1 - lightness);
    const getF = number => {
      const k = (number + hue / 30) % 12;
      return lightness - alpha * Math.max(-1, Math.min(k - 3, Math.min(9 - k, 1)))
    };
    const red = Math.round(255 * getF(0));
    const green = Math.round(255 * getF(8));
    const blue = Math.round(255 * getF(4));
    const hex = "#"+(red.toString(16).padStart(2, "0"))+(green.toString(16).padStart(2, "0"))+(blue.toString(16).padStart(2, "0"));
    return hex;
  }

  for (let i = 0; i < 15; i++) {
    const stair = document.createElement("m-cube");
    stair.setAttribute("z", i * 0.5);
    stair.setAttribute("y", i * 0.2);
    stair.setAttribute("width", 2);
    stair.setAttribute("height", 0.2);
    stair.setAttribute("depth", 0.5);
    stair.setAttribute("color", "blue");
    stair.setAttribute("collision-interval","1000");
    stair.addEventListener("collisionstart", () => {
      stair.setAttribute("color", "white");
    });
    stair.addEventListener("collisionend", () => {
      stair.setAttribute("color", getHexForCurrentTime(0.5));
    });
    stairs.append(stair);
  }
</script>

WASD to move. Click cubes to change color. Edit the MML on the left. This demo runs entirely in your browser — the same library that powers networked multiplayer servers also works standalone, with no backend required.

Architecture

Two independent networking systems run in parallel: DeltaNet for real-time multiplayer presence, and the Networked DOM for synchronized MML content.

1

Full Stack

The server orchestrates two distinct channels. DeltaNet handles high-frequency position and state updates for every connected user. The Networked DOM synchronizes MML document state — interactive 3D content that all users see and can manipulate together.

3D Web Experience Server
DeltaNet
Position & State
Networked DOM
MML Content
MML Documents
Client A
Client B
Client C
Position
MML State
2

DeltaNet Protocol

Rather than sending absolute positions each tick, DeltaNet encodes the change in change — second-order deltas that are almost always zero for moving users. These tiny values pack into single bytes and compress well, achieving a 20:1 reduction over the wire.

5 components per user
x·y·z··rystate
each int64 (8 B)
raw ±11
Δ ±2.5
ΔΔ ±0.5
5 × 8 B × 2,000
80,000 B
delta² encode + varint: near-zero values → 1 byte each
10,000 B5 × 1 B × 2,000
deflate: all users compressed in one pass
≈ 4,000 Bdeflate
80,000 → 4,000 B per tick·20:1 compression
4,000 B × 20 ticks/s × 8 bits = 640 kbps per client

Getting Started

# Scaffold a project
$ npx @mml-io/3d-web-experience init my-world
# Start the server
$ cd my-world
$ npx @mml-io/3d-web-experience serve world.json
# Open http://localhost:8080 - use multiple tabs for multi-user

Drop MML documents in mml-documents/ and add them to the world.json file to add interactive 3D content. Examples on GitHub