# Spectacles 522 Portable Design

> Design rules for building Spectacles lenses in Lens Studio 5.22 (CLAD/MCP workflow) so they downgrade to LS 5.15.4 for device testing in under an hour. Load when STARTING or architecting a new 5.22 Spectacles project that will later be tested on Spectacles (2024), when the user says "we will downgrade to 5.15" / "5.15-portable" / "we will test on device later", or before adding any shader/material/package to a 5.22 project that must survive the downgrade. Distilled from two full real-project migrations.

- Skill: `ptpavlotkachenko/spectacles-522-portable-design` (Agent Skill)
- Install (CLI): `npx skillmds@latest add ptpavlotkachenko/spectacles-522-portable-design`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ptpavlotkachenko/spectacles-522-portable-design/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: PtPavloTkachenko (https://skillmd.com/u/ptpavlotkachenko)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/ptpavlotkachenko/spectacles-522-portable-design

---


# Building 5.15-portable lenses in Lens Studio 5.22

**Why this exists:** CLAD/agentic development lives in LS 5.22+, but Spectacles (2024)
device work ships from LS 5.15.4. Two real projects proved
you can have both — IF the project is architected for portability from day one.
The second migrated in ~1 hour with ZERO code changes (19 scripts compiled first try).
Companion skill for the actual move: `spectacles-522-to-515-migration`.

## The Golden Rule: runtime-first architecture

**The scene must be trivially reconstructible.** Target: ONE SceneObject with the root
controller script + a handful of @input references (modules, base materials, font).
EVERYTHING else — meshes, materials-on-clones, UI, FX, colliders, Interactables — is
created in `onStart()` from code.

- Migration cost of a code-built scene = create 1 object + set N inputs (minutes via
  the 5.15 granular MCP ChatTools: CreateLensStudioSceneObject →
  CreateLensStudioComponent(ScriptComponent) → SetLensStudioProperty scriptAsset +
  inputs by name).
- Migration cost of an editor-built scene = re-authoring everything by hand. Don't.
- Add a `DebugHarness` object (debugState seeder) as the ONLY other scene object —
  it makes every state reachable in preview without play-through AND survives
  migration the same cheap way.

## Materials & shaders (where migrations die)

1. **Base materials = ImageMaterialPreset ONLY**, passed as @input. It has the
   `baseTex`/`baseColor` ports runtime code writes; Unlit does NOT (silent no-op,
   invisible additive-black meshes).
2. **Set EVERYTHING on clones in code**: blendMode, depthWrite, depthTest, twoSided,
   baseColor, baseTex. 5.15 `clone()` resets graph values to defaults — Inspector
   values do not survive cloning. Code that always re-sets is version-proof.
3. **NO custom graph shaders unless they have a rebuildable GLSL source.**
   - 5.22-converted `.ss_graph` → runtime-DEAD (params wrapped in a subgraph don't
     bind; vertex stage renders NOTHING even with uniforms bound — proven).
   - 5.22-authored `.graphShader` → not portable back to 5.15 at all.
   - The ONLY portable custom-shader path: keep `.glsl` sources in the repo and
     rebuild with `ss_graph_tool.py build` per version (verify RENDERING, not import).
   - Best: skip custom shaders. CPU MeshBuilder geometry + ImageMaterial clones
     covered ribbons, tornado funnels, particles, trails, floor overlays in both
     projects — and made both migrations shader-free.
4. `pass.colorMask` runtime writes don't work in 5.15 — occluders need it baked in
   the .mat file.

## FX: CPU over GPU

MeshBuilder strips/quads with `setVertexInterleaved` per frame + pooled billboard
quads cover 95% of "magic" FX and are 100% portable. Design rules that double as
device-perf rules (Spectacles 2024):
- ONE batched MeshBuilder mesh for anything that accumulates (trails, floor swaths)
  — N separate quads = N draw calls; batched = 1.
- Shared sparkle POOL for all markers/indicators (fields registered per shape),
  not per-feature emitters.
- Zero per-frame allocations in hot loops: reuse scratch arrays for
  `setVertexInterleaved`, mutate `vec3` components in place.
- World-space-vertex meshes live at SCENE ROOT (identity transform) — parenting
  them under a moved root double-transforms every vertex.

## APIs: stick to the 5.15 surface

- Before using ANY API, sanity-check it exists in a 5.15 `StudioLib.d.ts` (keep a
  donor project handy). Known-good: InternetModule
  (`createWebSocket`, fetch), WorldQueryModule `hitTest` (NOT `hitTestWithFilter`),
  GestureModule targeting ray, `Text.backgroundSettings`, MeshBuilder full surface,
  `ProceduralTextureProvider`.
- **SIK: use only the stable core** — `NativeLogger`, `LogLevel`,
  `SIKLogLevelProvider`, `WorldCameraFinderProvider`, `HandInputData`
  (wrist/indexTip), runtime-created `Interactable`. Import paths are identical in
  0.16.4 → 0.18; code compiled unmodified across all three.
- Avoid: 5.22-only packages (LEAF, AiPreviewAgent*, VirtualScene-era anything),
  UIKit-heavy UI (build UI in code), SnapDecorators, deep SIK internals.
- Wrap optional hand/gesture reads in guards (`isTracked()`, try/catch) and give
  every feature an `isEditor()` fallback — this is ALSO what makes the lens
  testable in preview.

## Assets

- Version-agnostic, copy verbatim: PNG/JPG/TTF/WAV/GLB/OBJ. Reference by path via
  `requireAsset("../Textures/x.png")` — path-based refs survive fresh meta
  generation.
- When migrating: copy WITHOUT .meta files into the fresh 5.15 project (LS
  regenerates). The one exception where .meta must ride along: moving
  graphShader+mat pairs between same-version projects (UUID inheritance) — but if
  you followed the shader rule above you have none.
- InternetModule asset file copies fine.

## Config & networking

- WS endpoint pattern (firewall gotcha — macOS blocks inbound on the LAN IP even
  from localhost):
  ```typescript
  public static readonly WS_URL = global.deviceInfoSystem.isEditor()
    ? "ws://127.0.0.1:8779"        // preview: lens and bridge on the same Mac
    : "ws://192.168.1.42:8779";   // device: the Mac's LAN IP
  ```
- Experimental APIs checkbox per project (ws://); Extended Permissions on device
  (camera+internet) → lens unpublishable, fine for POC.

## The migration itself (summary; full playbook = spectacles-522-to-515-migration)

1. Fresh LS 5.15.4 project from the Spectacles template (it already ships
   5.15-native SIK+UIKit, camera with Device Tracking World, lighting — do NOT
   copy 5.22 .lspkg packages, ever).
2. Copy `Assets/<Project>/` scripts+textures+fonts WITHOUT metas → compile should
   be green immediately if the rules above were followed.
3. Via 5.15 MCP: create base materials from ImageMaterialPreset, create the root
   object + DebugHarness, wire inputs, set camera Far=100000.
4. Enable Experimental APIs, preview device Spectacles (2024), Cmd+S by hand
   (MCP never saves), smoke-test against the mock bridge.
5. `.mcp.json` for Claude Code needs key `mcpServers` — Lens Studio's "Copy MCP
   Config" button emits `servers` and is silently ignored. Rewrite the key.
6. MCP connections are established ONLY at session start — after editing
   `.mcp.json`, restart/resume the session.

## Checklist before calling a 5.22 feature "done"

- [ ] Works with zero editor-authored scene objects beyond root+harness?
- [ ] All material state set in code on clones?
- [ ] No graph shaders (or GLSL source committed)?
- [ ] APIs exist in 5.15 d.ts?
- [ ] Editor fallback exists (no hands / no device)?
- [ ] Accumulating geometry batched into one mesh?

