"An ICameraModifier writes to every camera that draws, including the editor viewport"
▸ SYMPTOM
You position a camera during play-in-editor, then read the rendered frame. The position moves. The rotation does not.
You shoot the same spot at four bearings, 0, 90, 180, and 270 degrees. All four render the same frame to the pixel, every one aimed down the player's spawn bearing. The behaviour survives play stop and start, and it survives a full editor restart.
▸ CAUSE
An ICameraModifier runs against every camera that draws, not only the camera its owning component conceptually belongs to. The rendered view is composed by running every ICameraModifier in the scene against whichever camera is currently drawing, in ascending CameraOrder.
Play-in-editor draws more than one camera. It draws the game's own camera and the editor viewport camera, which is a real CameraComponent on a real GameObject named editor_camera. An unfiltered modifier writes its pose into the editor viewport too, every frame.
A modifier that writes only some fields makes the viewport obey some instructions and ignore others. A first-person player component's ModifyCamera set view.Rotation from the player's aim and never set view.Position. So a camera tool had its position honoured and its rotation discarded on every shot. The player respawns at the same authored yaw every time, which is why play stop and start and editor restarts never clear it.
A transform readback cannot see this defect. Reading the object back reports exactly the WorldRotation you just set, because the set really does land on the object. The override happens later, during view composition, downstream of the object. A check that reads back the value it just wrote can never go red here.
▸ FIX
- Filter every modifier callback to the game view. Open every
ModifyCameraandPostCameraSetupbody by refusing any camera that is not the game view:if ( !camera.IsValid() || !camera.IsMainCamera ) return;. - Verify against a rendered frame, not a transform readback. Shoot the same position at two bearings 180 degrees apart and compare the images. The readback is vacuous, so only the frame is a witness.
- Compare frames with a mean-luminance signature, not a byte compare. Temporal AA, streaming, and exposure drift make two shots of the same view differ every time, so a byte compare is useless. Downsample to a small signature, for example 32 by 18, and apply a difference floor. On the live case, frozen pairs scored 0.00 to 6.3 and genuinely different bearings from the same spot scored 23.0 to 39.9.
▸ WHY IT WORKS
Code that looks up a camera almost always filters on IsMainCamera already, because picking one camera out of many makes that the obvious step. Code that writes inside a modifier callback is handed the camera and never selects one, so the filter gets forgotten there. The IsMainCamera guard puts the missing selection back where the write happens.
The fingerprint is cheap to read. In edit mode, with no play session running, the editor viewport rotates correctly, because no game component is running to write to it. Rotation that works in edit mode and freezes once play starts points at a project component, not an engine defect. Try that before anything else.
- Published