FOS Mirror
Ce contenu n’est pas encore disponible dans votre langue.
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.
- Create an empty GameObject and add FOS Essentials > Mirror > Mirror Manager.
- 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. - Decide the Mode: exclusive, or every mirror independent.
- 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.
Components
Section titled “Components”| 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.
What is actually switched
Section titled “What is actually switched”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.
Colliderdoes not derive fromBehaviour— it derives straight fromComponentwith its ownenabledproperty. 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.
What an off mirror looks like
Section titled “What an off mirror looks like”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.
Rendererderives fromComponent, not fromBehaviour, and carries its ownenabledproperty — 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.
The two halves of a mirror’s state
Section titled “The two halves of a mirror’s state”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.
Distance
Section titled “Distance”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.
Toggle scope, per mirror
Section titled “Toggle scope, per mirror”| 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
SendCustomNetworkEventis 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.
Quality
Section titled “Quality”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 cost estimate
Section titled “The cost estimate”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:
-
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.
-
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.
-
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.
Scripting
Section titled “Scripting”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.
Notes and limits
Section titled “Notes and limits”- 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
targetsand callRefreshTargets(). - 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.
