Ir al contenido

FOS Mirror

Esta página aún no está disponible en tu idioma.

Version 1.0.0 · requires FOS Essentials Core 1.0.0 or later

Switches mirrors on and off — by distance, by button, by interaction — from a single central manager, with quality presets and an indicative cost estimate. Requires FOS Essentials Core 1.0.0.

Mirrors are the first thing that kills the frame rate of a VRChat world. This pack does not make them cheap: nothing can. It makes them controlled — off by default, on only where and when someone actually asked for one.


  1. Create an empty GameObject and add FOS Essentials > Mirror > Mirror Manager.
  2. Click Scan the scene. Every VRChat mirror gets a FOSMirrorTarget, its mirror component and its manager are wired up, and the manager’s list is filled.
  3. Decide the Mode: exclusive, or every mirror independent.
  4. Add a control where a player should be able to switch a mirror: Mirror Control (UI Button) or Mirror Control (Interact).

Re-run the scan after adding new mirrors. Existing targets are never overwritten, so any manual tuning survives.


Component Role
FOSMirrorManager One per scene. Owns the loop, the distance test and exclusive mode.
FOSMirrorTarget One per mirror. Holds settings and the on/off state — no per-frame work.
FOSMirrorButton Switches a mirror from a UI Button. Wires the OnClick for you.
FOSMirrorInteract Switches a mirror when a player uses the object it sits on.

The pack deliberately declares no custom editor for VRC_MirrorReflection. Unity accepts only one per type, and VRWorldToolkit already ships one. Everything here goes through the FOS component, so both tools keep working side by side.


The VRC_MirrorReflection component, through .enabled — never the GameObject.

SetActive(false) would re-run every OnEnable in the hierarchy, Udon included, which can reset the state of unrelated scripts. It would also hide the mirror object itself, and a mirror is usually a decorated frame that should stay visible when it goes dark.

Extra components and Extra colliders are switched along with it.

Collider does not derive from Behaviour — it derives straight from Component with its own enabled property. That is why colliders have their own list.

Never put the collider a player uses to switch a mirror back on into that list: it would be disabled together with the mirror and the mirror could never be turned on again. The target inspector reports it as an error when it detects the case.


Disabling the mirror component stops the reflection, but the surface keeps rendering — and a mirror shader with nothing to reflect draws pure black. A switched-off mirror is therefore a black rectangle unless you say otherwise.

Two fields on the target decide the look:

Mirror surface Off material Result
empty The black rectangle. Nothing is touched.
set empty The surface is hidden. Its draw call goes away too.
set set The surface swaps to that material: dark glass, frosted pane, painted panel.

The scan fills Mirror surface with the renderer sitting on the mirror, so hiding is the default behaviour. Re-run the scan on an older scene to pick it up, or use the Use the mirror’s renderer button in the target inspector.

The pack ships one ready-made off material, Materials/FOSMirrorOff.mat — a dark, glossy pane. It uses Unity’s Standard shader, chosen so the material never depends on anything outside Unity itself. On Quest, either swap its shader for a mobile one such as VRChat/Mobile/Standard Lite, or simply leave Off material empty: hiding the surface costs nothing and needs no material at all.

The swap writes renderer.material, never sharedMaterial — writing sharedMaterial at runtime edits the material asset on disk, which in ClientSim would leave the mirror material permanently overwritten in your project.

Renderer derives from Component, not from Behaviour, and carries its own enabled property — the same reason colliders get their own list.

The “on” material is captured from the renderer when the world starts. Do not leave the off material assigned to the renderer in the scene: it would be captured as the mirror look and the reflection would never come back. The inspector reports that case as an error.

This applies to distance culling too, which is usually what you want: a mirror out of range loses both its reflection and its surface.


A mirror renders only when both are true:

Half Owner Set by
Manual state — does anyone want it on? The target Button, interaction, startsOn
Range — is the player close enough? The manager Distance test, every frame budget

A mirror the player switched off stays off, however close they walk. Range never turns a mirror on by itself — it only lets one that is already on come back.

Same logic as FOS UI Culling: squared distances, two thresholds, round-robin checks, and a write only when the state actually changes.

Option Effect
Default distance Metres below which a mirror may render, unless the target overrides it.
Show distance (target) Per-mirror override. -1 means “inherit the manager’s default”.
Distance reference (target) Point the distance is measured from. Empty means the mirror itself.
Hysteresis Extra metres before switching off again.
Checks per frame How many mirrors are tested each frame. 2 by default.

The target inspector spells out the result — “Effective distance: 12 m (inherited from the manager)” — so you never have to remember what -1 means.

Distances are read once at Start. Changing one at runtime requires calling RefreshTargets() on the manager.

A mirror the player just switched on is measured immediately instead of waiting for its turn in the round-robin: standing right in front of a mirror that takes four frames to appear feels broken.


Mode Behaviour
Free Every mirror is independent. Nothing stops a player from turning them all on.
Exclusive Turning one on turns every other one off.

Exclusive mode is the single most effective setting in this pack, because the second cost factor of a world is how many mirrors render at the same time — and that number is otherwise decided by whoever is feeling curious.

It is enforced at startup too: if several mirrors are set to start on, only the first one does, and the manager inspector says so.


Scope Networking Who pays
Local None at all Only the player who switched it
Synchronized [UdonSynced], manual sync Everyone in the instance

Local is the default, and it is the right one for a public mirror: a player who wants to check their avatar gets one, and nobody else pays for it.

Synchronized is for a mirror that is part of the world — a dance stage, a shared dressing room. Ownership is taken before the variable is written, and the state is rebuilt for players who join later from OnDeserialization.

A SendCustomNetworkEvent is not replayed for late joiners. That is why the state lives in a synced variable and not in an event.

Because the state is stored on the target, the two scopes mix freely in one scene. In exclusive mode, be aware of what mixing means: switching a local mirror off only switches it off for you.

Manual sync is rate-limited to roughly one serialisation per second per behaviour. Turning on a synchronized mirror in exclusive mode writes to the other synchronized mirrors as well, so avoid making a dozen mirrors synchronized when only one of them needs to be.


Two settings of the VRChat mirror are exposed on the target: reflected layers and disable pixel lights.

Preset Reflected layers
Untouched (default) Nothing is applied; the mirror keeps its own settings.
Players only Player + MirrorReflection — avatars over the skybox.
Players and world Default + Player + Environment + MirrorReflection.
World only Default + Environment. No avatar is ever drawn twice.
Nothing An empty mask. The mirror renders a blank reflection.
Custom Whatever you set by hand in Reflected layers.

The preset resolves into the Reflected layers field as soon as you pick it, so the mask you see is the mask that will be applied. The manager writes it into the mirror at Start; the target inspector also has Apply to the mirror now to write it in the editor, which is handy when you want VRWorldToolkit’s own checks to see it.

Untouched exists because a tool must never silently overwrite settings someone tuned by hand. Nothing is written until you choose a preset.

The inspector warns about the classic mistakes: the untouched default mask, UiMenu being reflected, PlayerLocal being reflected, and players reflected without the MirrorReflection layer, which means players see each other but not themselves.


The target and manager inspectors show an indicative score out of 100. It is a weighted estimate, not a measurement, and it is built in this order:

  1. Reflected layers, dominant. A mirror draws the scene a second time — twice in VR. What it costs is therefore what it draws again. Reflecting avatars doubles the heaviest item of a populated instance, and that item grows with the number of players, not with the world. A world-only mirror costs a fraction of the same mirror reflecting players, at identical size and position.

  2. How many mirrors can render at the same time. Two mirrors on means two extra render passes; nothing is shared between them. Exclusive mode removes this factor outright, so the scene score uses the worst mirror instead of the sum.

  3. Screen coverage, last and lightly weighted. VRChat renders a mirror into a texture sized by the headset resolution, not by the mirror: a big mirror does not mechanically cost more than a small one. Size counts indirectly — a big mirror is visible from further away, fills up with avatars more often and culls by distance less well.

Use the score to compare two settings of the same world. It is not an absolute figure and it does not replace profiling.


Selecting a mirror draws its plane, an arrow along the reflecting face and a red cross over the back. A mirror is a flat object: nothing in the scene view tells a correctly placed one from a mirror that was rotated by 180°, and you normally find out in game, standing in front of a black rectangle.

The normal is read from the mesh, not guessed from the transform, so it stays right whatever mesh the mirror uses.

The gizmo lives in an editor script using [DrawGizmo]. It is deliberately not an OnDrawGizmos in the Udon behaviour: Udon never calls that method, and UdonSharp would still have to compile it — along with Gizmos and Handles, which are not exposed.


Every public entry point takes no parameter, so any script can reach it with SendCustomEvent:

Call On Effect
FOSRequestToggle() FOSMirrorTarget Flips the mirror
FOSRequestOn() / FOSRequestOff() FOSMirrorTarget Forces one state
TriggerMirror() FOSMirrorButton / FOSMirrorInteract Runs the control’s action
FOSTurnAllOff() FOSMirrorManager Panic button: switches every mirror off
RefreshTargets() FOSMirrorManager Rebuilds the caches after a scene change

The OnClick wiring a UI button needs is SendCustomEvent("TriggerMirror") on the control’s UdonBehaviour. Getting that right by hand means aiming at the UdonBehaviour rather than the visible component and typing the event name without a typo — a typo raises no error, the button simply does nothing. The inspector wires it in one click and tells you whether it is already wired.


  • One manager per scene. Several managers would fight over the same mirrors; the inspector reports it as an error.
  • Legacy mirrors are skipped. Only the SDK3 mirror component is exposed to Udon. The scan counts them and tells you.
  • Mirrors created at runtime are not picked up automatically. Fill targets and call RefreshTargets().
  • A mirror with no manager cannot be switched. The scan assigns it; the target inspector says so when it is missing.
  • Quest and PC behave identically here — there is no platform-specific code, and there could not be: Udon is compiled once for both.