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)
- 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).
- 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.
- 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.
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
The migration itself (summary; full playbook = spectacles-522-to-515-migration)
- 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).
- Copy
Assets/<Project>/ scripts+textures+fonts WITHOUT metas → compile should
be green immediately if the rules above were followed.
- Via 5.15 MCP: create base materials from ImageMaterialPreset, create the root
object + DebugHarness, wire inputs, set camera Far=100000.
- Enable Experimental APIs, preview device Spectacles (2024), Cmd+S by hand
(MCP never saves), smoke-test against the mock bridge.
.mcp.json for Claude Code needs key mcpServers — Lens Studio's "Copy MCP
Config" button emits servers and is silently ignored. Rewrite the key.
- MCP connections are established ONLY at session start — after editing
.mcp.json, restart/resume the session.
Checklist before calling a 5.22 feature "done"
1---2name: spectacles-522-portable-design3description: 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.4---56# Building 5.15-portable lenses in Lens Studio 5.2278**Why this exists:** CLAD/agentic development lives in LS 5.22+, but Spectacles (2024)9device work ships from LS 5.15.4. Two real projects proved10you can have both — IF the project is architected for portability from day one.11The second migrated in ~1 hour with ZERO code changes (19 scripts compiled first try).12Companion skill for the actual move: `spectacles-522-to-515-migration`.1314## The Golden Rule: runtime-first architecture1516**The scene must be trivially reconstructible.** Target: ONE SceneObject with the root17controller script + a handful of @input references (modules, base materials, font).18EVERYTHING else — meshes, materials-on-clones, UI, FX, colliders, Interactables — is19created in `onStart()` from code.2021- Migration cost of a code-built scene = create 1 object + set N inputs (minutes via22 the 5.15 granular MCP ChatTools: CreateLensStudioSceneObject →23 CreateLensStudioComponent(ScriptComponent) → SetLensStudioProperty scriptAsset +24 inputs by name).25- Migration cost of an editor-built scene = re-authoring everything by hand. Don't.26- Add a `DebugHarness` object (debugState seeder) as the ONLY other scene object —27 it makes every state reachable in preview without play-through AND survives28 migration the same cheap way.2930## Materials & shaders (where migrations die)31321. **Base materials = ImageMaterialPreset ONLY**, passed as @input. It has the33 `baseTex`/`baseColor` ports runtime code writes; Unlit does NOT (silent no-op,34 invisible additive-black meshes).352. **Set EVERYTHING on clones in code**: blendMode, depthWrite, depthTest, twoSided,36 baseColor, baseTex. 5.15 `clone()` resets graph values to defaults — Inspector37 values do not survive cloning. Code that always re-sets is version-proof.383. **NO custom graph shaders unless they have a rebuildable GLSL source.**39 - 5.22-converted `.ss_graph` → runtime-DEAD (params wrapped in a subgraph don't40 bind; vertex stage renders NOTHING even with uniforms bound — proven).41 - 5.22-authored `.graphShader` → not portable back to 5.15 at all.42 - The ONLY portable custom-shader path: keep `.glsl` sources in the repo and43 rebuild with `ss_graph_tool.py build` per version (verify RENDERING, not import).44 - Best: skip custom shaders. CPU MeshBuilder geometry + ImageMaterial clones45 covered ribbons, tornado funnels, particles, trails, floor overlays in both46 projects — and made both migrations shader-free.474. `pass.colorMask` runtime writes don't work in 5.15 — occluders need it baked in48 the .mat file.4950## FX: CPU over GPU5152MeshBuilder strips/quads with `setVertexInterleaved` per frame + pooled billboard53quads cover 95% of "magic" FX and are 100% portable. Design rules that double as54device-perf rules (Spectacles 2024):55- ONE batched MeshBuilder mesh for anything that accumulates (trails, floor swaths)56 — N separate quads = N draw calls; batched = 1.57- Shared sparkle POOL for all markers/indicators (fields registered per shape),58 not per-feature emitters.59- Zero per-frame allocations in hot loops: reuse scratch arrays for60 `setVertexInterleaved`, mutate `vec3` components in place.61- World-space-vertex meshes live at SCENE ROOT (identity transform) — parenting62 them under a moved root double-transforms every vertex.6364## APIs: stick to the 5.15 surface6566- Before using ANY API, sanity-check it exists in a 5.15 `StudioLib.d.ts` (keep a67 donor project handy). Known-good: InternetModule68 (`createWebSocket`, fetch), WorldQueryModule `hitTest` (NOT `hitTestWithFilter`),69 GestureModule targeting ray, `Text.backgroundSettings`, MeshBuilder full surface,70 `ProceduralTextureProvider`.71- **SIK: use only the stable core** — `NativeLogger`, `LogLevel`,72 `SIKLogLevelProvider`, `WorldCameraFinderProvider`, `HandInputData`73 (wrist/indexTip), runtime-created `Interactable`. Import paths are identical in74 0.16.4 → 0.18; code compiled unmodified across all three.75- Avoid: 5.22-only packages (LEAF, AiPreviewAgent*, VirtualScene-era anything),76 UIKit-heavy UI (build UI in code), SnapDecorators, deep SIK internals.77- Wrap optional hand/gesture reads in guards (`isTracked()`, try/catch) and give78 every feature an `isEditor()` fallback — this is ALSO what makes the lens79 testable in preview.8081## Assets8283- Version-agnostic, copy verbatim: PNG/JPG/TTF/WAV/GLB/OBJ. Reference by path via84 `requireAsset("../Textures/x.png")` — path-based refs survive fresh meta85 generation.86- When migrating: copy WITHOUT .meta files into the fresh 5.15 project (LS87 regenerates). The one exception where .meta must ride along: moving88 graphShader+mat pairs between same-version projects (UUID inheritance) — but if89 you followed the shader rule above you have none.90- InternetModule asset file copies fine.9192## Config & networking9394- WS endpoint pattern (firewall gotcha — macOS blocks inbound on the LAN IP even95 from localhost):96 ```typescript97 public static readonly WS_URL = global.deviceInfoSystem.isEditor()98 ? "ws://127.0.0.1:8779" // preview: lens and bridge on the same Mac99 : "ws://192.168.1.42:8779"; // device: the Mac's LAN IP100 ```101- Experimental APIs checkbox per project (ws://); Extended Permissions on device102 (camera+internet) → lens unpublishable, fine for POC.103104## The migration itself (summary; full playbook = spectacles-522-to-515-migration)1051061. Fresh LS 5.15.4 project from the Spectacles template (it already ships107 5.15-native SIK+UIKit, camera with Device Tracking World, lighting — do NOT108 copy 5.22 .lspkg packages, ever).1092. Copy `Assets/<Project>/` scripts+textures+fonts WITHOUT metas → compile should110 be green immediately if the rules above were followed.1113. Via 5.15 MCP: create base materials from ImageMaterialPreset, create the root112 object + DebugHarness, wire inputs, set camera Far=100000.1134. Enable Experimental APIs, preview device Spectacles (2024), Cmd+S by hand114 (MCP never saves), smoke-test against the mock bridge.1155. `.mcp.json` for Claude Code needs key `mcpServers` — Lens Studio's "Copy MCP116 Config" button emits `servers` and is silently ignored. Rewrite the key.1176. MCP connections are established ONLY at session start — after editing118 `.mcp.json`, restart/resume the session.119120## Checklist before calling a 5.22 feature "done"121122- [ ] Works with zero editor-authored scene objects beyond root+harness?123- [ ] All material state set in code on clones?124- [ ] No graph shaders (or GLSL source committed)?125- [ ] APIs exist in 5.15 d.ts?126- [ ] Editor fallback exists (no hands / no device)?127- [ ] Accumulating geometry batched into one mesh?