WorldRepresentation / render_scripts /UNITY_PIPELINE.md
Nirav Madhani
Import World Representation Space
5af572e
|
Raw
History Blame Contribute Delete
13.6 kB

MuJoCo β†’ Unity β†’ WebGL Pipeline

Export your MuJoCo scene to Unity, visualize the Ξ¦+G representation, and deploy as WebGL for the browser.


Overview

MuJoCo MJCF (.xml)
    ↓  Unity MuJoCo Plugin (import)
Unity Scene (3D objects + physics)
    ↓  Custom scripts (add Ξ¦+G visualization)
Unity Scene with field/graph overlays
    ↓  Unity WebGL Build
Browser-ready .html + .wasm + .data
    ↓  Host on HuggingFace Spaces (static)
Interactive 3D playbook

Step 1: Install MuJoCo Unity Plugin

Prerequisites

  • Unity 2021.3+ (LTS recommended)
  • Git
  • Windows/Mac/Linux

Setup

# Clone the MuJoCo repo (or just the unity folder)
git clone https://github.com/google-deepmind/mujoco.git
cd mujoco
git checkout 3.2.4  # match your MuJoCo version

In Unity:

  1. Open Window β†’ Package Manager
  2. Click + β†’ Add package from disk
  3. Navigate to mujoco/unity/package.json and select it
  4. Unity will import the C# scripts

Then copy the MuJoCo native library:

  • Windows: Copy mujoco.dll from your MuJoCo install to Assets/Plugins/
  • Mac: Copy libmujoco.dylib
  • Linux: Copy libmujoco.so

Verify

After import, you should see Assets β†’ Import MuJoCo Scene in the menu bar.


Step 2: Import Your MJCF Scene

The scene XML files are in the render_scripts/ folder. Here's the main tabletop scene:

<!-- Save as tabletop_scene.xml -->
<mujoco model="tabletop_scene">
  <option timestep="0.002" gravity="0 0 -9.81"/>
  <worldbody>
    <light pos="0 0 2" dir="0 0 -1"/>
    <geom name="floor" type="plane" size="2 2 0.1" rgba="0.7 0.7 0.7 1"/>
    <body name="table" pos="0 0 0.4">
      <geom type="box" size="0.4 0.3 0.02" rgba="0.45 0.35 0.25 1"/>
    </body>
    <body name="wood_block" pos="-0.15 0.06 0.455">
      <joint type="free"/>
      <geom type="box" size="0.035 0.035 0.035" mass="0.6" 
            rgba="0.65 0.45 0.25 1" friction="0.4 0.005 0.0001"/>
    </body>
    <body name="rubber_ball" pos="0.05 -0.06 0.455">
      <joint type="free"/>
      <geom type="sphere" size="0.03" mass="1.1"
            rgba="0.18 0.18 0.20 1" friction="0.8 0.005 0.0001"/>
    </body>
    <body name="metal_cylinder" pos="0.18 0.07 0.455">
      <joint type="free"/>
      <geom type="cylinder" size="0.025 0.03" mass="3.0"
            rgba="0.7 0.72 0.75 1" friction="0.2 0.005 0.0001"/>
    </body>
    <body name="plastic_cup" pos="-0.08 -0.1 0.46">
      <joint type="free"/>
      <geom type="cylinder" size="0.022 0.035" mass="0.12"
            rgba="0.2 0.55 0.85 1" friction="0.35 0.005 0.0001"/>
    </body>
    <body name="ceramic_plate" pos="0.1 0.0 0.44">
      <joint type="free"/>
      <geom type="cylinder" size="0.05 0.008" mass="0.35"
            rgba="0.92 0.90 0.85 1" friction="0.25 0.005 0.0001"/>
    </body>
  </worldbody>
</mujoco>

In Unity:

  1. Assets β†’ Import MuJoCo Scene β†’ select tabletop_scene.xml
  2. The plugin creates GameObjects for each body/geom with MuJoCo components
  3. Each geom gets a MeshRenderer with procedural mesh
  4. The scene runs MuJoCo physics in Play mode

Step 3: Add Ξ¦+G Visualization Overlays

Create these C# scripts to visualize the field and graph:

FieldVisualizer.cs β€” Ownership heatmap + SDF wireframes

using UnityEngine;
using System.Collections.Generic;

public class FieldVisualizer : MonoBehaviour
{
    [Header("Objects")]
    public Transform[] trackedObjects;  // Drag MuJoCo bodies here
    public Color[] objectColors;        // One color per object
    
    [Header("Ownership Grid")]
    public bool showOwnership = false;
    public float gridResolution = 0.01f;
    public float gridExtent = 0.3f;
    public float gridHeight = 0.455f;
    
    [Header("SDF Wireframe")]
    public bool showSDF = true;
    
    private GameObject[,] ownershipTiles;
    private List<LineRenderer> edgeRenderers = new List<LineRenderer>();
    
    void Start()
    {
        if (objectColors.Length == 0)
            objectColors = new Color[] {
                new Color(1f, 0.45f, 0.09f),  // orange
                new Color(0.23f, 0.51f, 0.96f), // blue
                new Color(0.13f, 0.77f, 0.37f), // green
                new Color(0.55f, 0.36f, 0.96f), // purple
                new Color(0.92f, 0.70f, 0.03f), // yellow
            };
        
        CreateOwnershipGrid();
    }
    
    void CreateOwnershipGrid()
    {
        int size = Mathf.CeilToInt(gridExtent * 2 / gridResolution);
        ownershipTiles = new GameObject[size, size];
        
        for (int y = 0; y < size; y++)
        {
            for (int x = 0; x < size; x++)
            {
                float wx = -gridExtent + x * gridResolution;
                float wy = -gridExtent + y * gridResolution;
                
                var tile = GameObject.CreatePrimitive(PrimitiveType.Quad);
                tile.transform.SetParent(transform);
                tile.transform.position = new Vector3(wx, wy, gridHeight + 0.001f);
                tile.transform.localScale = Vector3.one * gridResolution;
                tile.transform.rotation = Quaternion.identity;
                
                var mat = new Material(Shader.Find("Unlit/Color"));
                mat.color = Color.clear;
                tile.GetComponent<Renderer>().material = mat;
                tile.SetActive(false);
                
                ownershipTiles[y, x] = tile;
            }
        }
    }
    
    void Update()
    {
        if (showOwnership)
            UpdateOwnership();
        
        // Toggle visibility
        foreach (var tile in ownershipTiles)
            if (tile != null) tile.SetActive(showOwnership);
    }
    
    void UpdateOwnership()
    {
        int size = ownershipTiles.GetLength(0);
        for (int y = 0; y < size; y++)
        {
            for (int x = 0; x < size; x++)
            {
                var tile = ownershipTiles[y, x];
                if (tile == null) continue;
                
                Vector3 pos = tile.transform.position;
                
                // Find nearest object (simplified gating)
                float minDist = float.MaxValue;
                int owner = -1;
                
                for (int i = 0; i < trackedObjects.Length; i++)
                {
                    float d = Vector3.Distance(pos, trackedObjects[i].position);
                    if (d < minDist) { minDist = d; owner = i; }
                }
                
                var mat = tile.GetComponent<Renderer>().material;
                if (minDist < 0.05f && owner >= 0)
                {
                    Color c = objectColors[owner % objectColors.Length];
                    c.a = Mathf.Clamp01(1f - minDist / 0.05f) * 0.4f;
                    mat.color = c;
                }
                else
                {
                    mat.color = Color.clear;
                }
            }
        }
    }
}

GraphVisualizer.cs β€” Contact edges + probabilities

using UnityEngine;
using System.Collections.Generic;

public class GraphVisualizer : MonoBehaviour
{
    public Transform[] objects;
    public float contactThreshold = 0.08f; // meters
    public Color edgeColor = new Color(1f, 0.45f, 0.09f, 0.8f);
    public bool showEdges = true;
    
    private List<LineRenderer> edges = new List<LineRenderer>();
    
    void Update()
    {
        // Clear old edges
        foreach (var lr in edges) if (lr != null) Destroy(lr.gameObject);
        edges.Clear();
        
        if (!showEdges) return;
        
        // Create edges between close objects
        for (int i = 0; i < objects.Length; i++)
        {
            for (int j = i + 1; j < objects.Length; j++)
            {
                float dist = Vector3.Distance(objects[i].position, objects[j].position);
                float contactProb = 1f / (1f + Mathf.Exp((dist - contactThreshold) / 0.02f));
                
                if (contactProb > 0.01f)
                {
                    var go = new GameObject($"Edge_{i}_{j}");
                    go.transform.SetParent(transform);
                    var lr = go.AddComponent<LineRenderer>();
                    lr.positionCount = 2;
                    lr.SetPositions(new Vector3[] { objects[i].position, objects[j].position });
                    lr.startWidth = 0.002f + contactProb * 0.005f;
                    lr.endWidth = lr.startWidth;
                    lr.material = new Material(Shader.Find("Unlit/Color"));
                    
                    Color c = edgeColor;
                    c.a = contactProb;
                    lr.startColor = c;
                    lr.endColor = c;
                    
                    edges.Add(lr);
                }
            }
        }
    }
}

FieldQueryUI.cs β€” Click-to-query the field

using UnityEngine;
using UnityEngine.UI;

public class FieldQueryUI : MonoBehaviour
{
    public Text positionText;
    public Text sdfText;
    public Text ownerText;
    public Text frictionText;
    public Transform[] objects;
    public string[] objectNames;
    public float[] objectFriction;
    
    // Material data per object (set in inspector)
    [System.Serializable]
    public struct MaterialData {
        public float friction;
        public float density;
        public float stiffness;
    }
    public MaterialData[] materials;
    
    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            Plane plane = new Plane(Vector3.up, new Vector3(0, 0.455f, 0));
            
            if (plane.Raycast(ray, out float enter))
            {
                Vector3 hitPoint = ray.GetPoint(enter);
                QueryField(hitPoint);
            }
        }
    }
    
    void QueryField(Vector3 point)
    {
        positionText.text = $"({point.x:F3}, {point.y:F3}, {point.z:F3})";
        
        float minDist = float.MaxValue;
        int nearest = -1;
        for (int i = 0; i < objects.Length; i++)
        {
            float d = Vector3.Distance(point, objects[i].position);
            if (d < minDist) { minDist = d; nearest = i; }
        }
        
        sdfText.text = $"{minDist:F4}m";
        ownerText.text = minDist < 0.03f ? objectNames[nearest] : "Background";
        frictionText.text = minDist < 0.03f ? $"{materials[nearest].friction:F2}" : "β€”";
    }
}

Step 4: Build for WebGL

  1. File β†’ Build Settings β†’ Select WebGL
  2. Click Switch Platform
  3. Player Settings:
    • Set Compression Format to Brotli (smallest)
    • Disable Auto Graphics API and add WebGL 2.0
    • Set Memory Size to 256MB
    • Under Publishing Settings, enable Decompression Fallback
  4. Click Build

Unity produces:

Build/
  index.html
  Build/
    Build.data.br       (scene data)
    Build.framework.js  (runtime)
    Build.loader.js     (loader)
    Build.wasm.br       (compiled code)

IMPORTANT: MuJoCo WASM Limitation

The MuJoCo Unity plugin uses native C libraries, which do not compile to WebAssembly. For WebGL builds, you have two options:

Option A: Static scene (no live physics)

  • Import the scene in Unity, position everything, bake the visual state
  • Remove all MuJoCo components before building
  • The WebGL build shows the scene visually with your Ξ¦+G overlays but without live MuJoCo physics
  • This is sufficient for the playbook β€” you want to show the representation, not run live sim

Option B: Pre-recorded simulation

  • Run the MuJoCo sim in the editor, record object trajectories as AnimationClips
  • Replace MuJoCo physics with Unity's Animation system for WebGL playback
  • Gives you "live" looking simulation without native MuJoCo at runtime

I recommend Option A for the playbook. The Three.js version (phi_g_interactive_scene.html, already built) gives you the interactive field query without Unity overhead.


Step 5: Host on HuggingFace Spaces

For the Unity WebGL build:

# Create HF Space
huggingface-cli repo create your-username/hybrid-world-model --type space --space_sdk static

# Copy build files
cp -r Build/* hybrid-world-model/
cd hybrid-world-model
git add .
git commit -m "Add Unity WebGL scene"
git push

For the Three.js version (simpler, recommended):

# Just upload the HTML file as index.html
huggingface-cli repo create your-username/phi-g-scene --type space --space_sdk static
cp phi_g_interactive_scene.html index.html
git add index.html
git commit -m "Interactive Phi+G scene"
git push

The Three.js version is a single HTML file with no dependencies β€” it just works.


Step 6: Recommended Approach

For the playbook, use both:

  1. Three.js scene (phi_g_interactive_scene.html) β€” embed directly in the playbook HTML. Lightweight, no build step, works everywhere. Shows the Ξ¦+G representation with clickable objects, field queries, graph edges, and ownership maps.

  2. Unity rendered videos (from render scripts) β€” pre-rendered MP4s showing MuJoCo physics in action. Embed as <video> tags or link to them.

  3. Unity WebGL (optional) β€” only if you want high-quality rendering with shadows, PBR materials, post-processing. Use Option A (static scene, no live MuJoCo). Worth it for a polished final version but not needed for v1.

The Three.js version already gives you the core demo: "click a point in 3D space, the field returns SDF + material properties + ownership." That's the pitch.