| name | lens-studio-vfx |
| description | Reference guide for Lens Studio's VFX Graph particle system — covering VFXComponent setup (VFX asset vs Component relationship), reading and writing .asset.properties to pass data into a VFX graph at runtime (position, color, float, direction vectors), particle system inspector settings (emitter rate, lifetime, shape, blending, world vs local simulation space), spawning particles from script (trigger emission via properties), sending scene object transforms and screen-space data to VFX, and common VFX bugs (type mismatches on properties, asset null checks). Use this skill whenever a lens needs particle effects, camera-reactive or gesture-driven VFX, attaching effects to face/body tracking anchors, or connecting real-time data (position, audio levels, expression weights) into a VFX graph. |
Lens Studio VFX — Reference Guide
Lens Studio's VFX Graph system lets you build GPU-accelerated particle effects visually. Scripts control VFX by writing to the graph's exposed properties, not by calling imperative particle APIs.
Core Concepts
| Term | Meaning |
|---|
| VFX Asset | The .vfx file — the graph definition with exposed properties |
| VFXComponent | The scene component that runs a VFX Asset on a scene object |
| Properties | Named, typed inputs on the asset that scripts can read/write |
const vfx = this.sceneObject.getComponent('Component.VFXComponent')
if (!vfx || !vfx.asset) {
print('[VFX] Asset not assigned!')
return
}
const props = vfx.asset.properties
Writing Properties to a VFX Graph
The VFX Graph exposes named properties that are declared inside the graph editor. Match the property name exactly (case-sensitive) and the type must match.
Supported property types
| Type | Example |
|---|
number | props['intensity'] = 0.8 |
boolean | props['emitting'] = true |
vec2 | props['uvOffset'] = new vec2(0.1, 0.2) |
vec3 | props['spawnPosition'] = new vec3(0, 1, 0) |
vec4 | props['color'] = new vec4(1, 0.5, 0, 1) |
quat | props['orientation'] = someQuat |
vfx.asset.properties['intensity'] = 0.5
const sourcePos: vec3 = sourceObject.getTransform().getWorldPosition()
vfx.asset.properties['spawnPosition'] = sourcePos
vfx.asset.properties['color'] = new vec4(1.0, 0.3, 0.0, 1.0)
vfx.asset.properties['emitting'] = true
Type mismatch warning: if the value type doesn't match what the graph expects, the property silently does nothing. Use typeof or a debug print to confirm the type before writing.
Sending Transform Data to VFX
World position
updateEvent.bind(() => {
const pos = anchor.getTransform().getWorldPosition()
vfx.asset.properties['targetPosition'] = pos
})
World rotation (as vec4 to avoid quat-to-prop issues)
const rot = anchor.getTransform().getWorldRotation()
vfx.asset.properties['rotation'] = new vec4(rot.w, rot.x, rot.y, rot.z)
Direction vector
const forward = anchor.getTransform().forward
vfx.asset.properties['emitDirection'] = forward
AABB mesh dimensions (for bounding-box-shaped emitters)
const meshVisual = anchor.getComponent('Component.RenderMeshVisual')
const aabbMax = meshVisual.mesh.aabbMax.mult(anchor.getTransform().getLocalScale())
const aabbMin = meshVisual.mesh.aabbMin.mult(anchor.getTransform().getLocalScale())
vfx.asset.properties['boundsMax'] = aabbMax
vfx.asset.properties['boundsMin'] = aabbMin
Screen-Space to World-Space Conversion
To spawn particles at a 2D screen position (e.g., where the user tapped):
function screenToWorld(screenTrans: ScreenTransform, camera: Camera, desiredDepth: number): vec3 {
const anchors = screenTrans.anchors
const center = anchors.getCenter()
const anchorHalfHeight = anchors.getSize().y / 2
const fov = camera.fov
const depth = (desiredDepth / anchorHalfHeight) * 0.5 / Math.tan(fov * 0.5)
const screenPos = screenTrans.localPointToScreenPoint(center)
return camera.screenSpaceToWorldSpace(screenPos, depth)
}
vfx.asset.properties['spawnPosition'] = screenToWorld(screenTrans, mainCamera, 250)
Particle System Inspector Settings
These are configured in the Lens Studio VFX Graph editor, not in script — but knowing them helps when authoring the graph.
| Setting | Purpose |
|---|
| Emitter Rate | Particles spawned per second (or per burst) |
| Lifetime | How long each particle lives (in seconds) |
| Simulation Space | World: particles stay where emitted; Local: particles move with the emitter |
| Blend Mode | Additive for fire/glow, Alpha for soft particles, Normal for opaque |
| Shape | Sphere, Cone, Box, Mesh surface — defines the spawn volume |
World vs Local simulation: use World space for fire or smoke that should trail behind a moving emitter. Use Local for effects that rigidly follow the emitter (e.g., a sparkle around a held object).
Attaching VFX to Face / Body Tracking Anchors
const faceTracking = headObject.getComponent('FaceTracking')
updateEvent.bind(() => {
const expressions = faceTracking.getFaceExpressionWeights()
const mouthOpen = expressions[FaceTracking.Expressions.MouthOpen]
vfx.asset.properties['emitRate'] = mouthOpen * 50
vfx.asset.properties['emitting'] = mouthOpen > 0.3
})
SpawnOnTap Pattern
@component
export class SpawnOnTap extends BaseScriptComponent {
@input vfxPrefab: ObjectPrefab
onAwake(): void {
const tapEvent = this.createEvent('TapEvent')
tapEvent.bind(() => {
const instance = this.vfxPrefab.instantiate(null)
instance.getTransform().setWorldPosition(spawnPos)
const cleanup = this.createEvent('DelayedCallbackEvent')
cleanup.bind(() => instance.destroy())
cleanup.reset(3)
})
}
}
Common Gotchas
- Property name must match exactly — property names in the VFX graph are case-sensitive. Always log
Object.keys(vfx.asset.properties) during development to see what's available.
- Asset null check: always guard with
if (!vfx || !vfx.asset) — if the asset isn't assigned in the inspector, the property access will throw.
- Type mismatch is silent — passing a
number to a vec3 property won't error; it just won't work. Match the type exposed in the VFX graph.
- Simulation space matters — a
Local-space emitter attached to moving content may produce unexpected particle trails; switch to World if that happens.
- Writing properties every frame is fine — VFX property writes happen on the CPU; the GPU reads them at emit time. Don't worry about performance for single-value writes.
VFXComponent is not ParticleSystem — Lens Studio's VFX Graph is separate from the legacy Particle System component. They have different APIs and can't be mixed.