Lente
Lente is a photo mode for Godot 4.x. Add one node to a 3D scene and the current gameplay camera becomes a tactile, bounded photographic camera with collision, real lens controls, color grading, supersampled capture, presets, and an in-game gallery. I wanted to build a mode like this for my game so here we are.
Five-minute setup
- Copy
addons/lenteinto your Godot project. - Enable Lente under Project → Project Settings → Plugins.
- In a 3D scene, click the Lente camera button in the 3D toolbar (or add a
LentePhotoModenode). - Run the scene and press P, or call:
$LentePhotoMode.enter_photo_mode()
That is the complete minimum setup. Lente inherits the active Camera3D, pauses the world, creates a modest entry bubble when no bounds are authored, and restores the previous camera, pause state, mouse mode, focus owner, and keep-alive node modes on exit.
The included showcase scene is the project’s main scene. Run the project and press P to try it.
How P is linked, and how to replace it
P is not hardcoded in the camera controller. When the editor plugin is enabled, Lente creates a Godot Input Map action named lente_toggle and gives that action two default events: physical key P and gamepad Start. Existing actions are never overwritten.
The LentePhotoMode.activation_action Inspector property defaults to lente_toggle. At runtime, the node listens for whichever action name is assigned there.
To change the binding while keeping Lente’s action name:
- Open Project → Project Settings → Input Map.
- Find
lente_toggle. - Remove
P, add your preferred keyboard/controller events, and save.
To use an action already owned by your game, such as open_photo_mode, create that action in the Input Map and set the node’s Activation Action to open_photo_mode. Set it to an empty StringName to disable Lente’s automatic enter/toggle shortcut completely.
You can also bypass the Input Map and call the public functions from any menu, input router, accessibility layer, or platform integration:
@onready var photo_mode: LentePhotoMode = $LentePhotoMode
func _on_photo_mode_command() -> void:
photo_mode.toggle_photo_mode() # Enter when inactive; exit when active.
func _on_enter_photo_mode_command() -> void:
photo_mode.enter_photo_mode() # Uses the viewport's current Camera3D.
func _on_exit_photo_mode_command() -> void:
photo_mode.exit_photo_mode()
lente_exit is the separate default exit action (Escape / gamepad B). Pressing lente_toggle again also exits.
Steam Input
Honestly I still need to understand deeply SteamInput for Godot, for now this part is a #TODO
What ships
- Weighted six-axis flight with mouse and full gamepad look, acceleration, settle, boost, slow movement, and roll.
- Sliding world collision using a small spherical
CharacterBody3Drig. - Soft movement boundaries authored as a union of oriented boxes, spheres, and Curve3D corridors. No volumes means a zero-configuration entry bubble.
- Field of view, click-to-focus, practical depth of field, aperture, exposure compensation, roll, color moods, temperature, saturation, contrast, and vignette.
- Explicit support for game-owned full-screen
ColorRectfilters, with a developer-fixed include/remove policy or an optional player choice. - A localized, gamepad-navigable English/Italian/Spanish interface with viewfinder, settings, feedback, presets, and gallery.
- A small signal-and-command contract for completely replacing the default UI.
- Supersampled off-screen capture with configurable MSAA and a safety cap, stored as PNG files with JSON sidecars.
- Exact pause-state preservation, opt-in living effects through the
lente_unpausedgroup, single-session arbitration, and graceful source-camera destruction handling. - Native editor icons, 3D gizmos, Inspector quick-add controls, configuration warnings, and non-destructive default input setup.
Default controls and Input Map names
| Input Map action | Purpose | Keyboard / mouse | Gamepad |
|---|---|---|---|
lente_toggle |
Enter/toggle photo mode | P | Start |
lente_move_* |
Move | WASD, Q/E down/up | Left stick, LT/RT down/up |
lente_look_* |
Look | Mouse motion | Right stick |
lente_boost / lente_slow |
Fast / precise movement | Shift / Ctrl | LB / RB |
lente_focus |
Focus subject | Left mouse | Right-stick click |
lente_capture |
Capture | Space | A / Cross |
lente_roll_left/right |
Roll | Z / X | D-pad left/right |
lente_ui |
Settings cursor | Tab | Y / Triangle |
lente_gallery |
Gallery | G | X / Square |
lente_reset |
Reset lens | R | D-pad down |
lente_zoom_in/out |
Zoom | Mouse wheel | D-pad up |
lente_exit |
Exit | Escape | B / Circle |
Every action is added only when its name does not already exist. Customize bindings in Godot’s Input Map as usual.
World collision and sliding
World collision and authored movement bounds are separate systems:
- World collision stops the camera sphere from crossing physics bodies such as walls and large props.
- Movement bounds limit how far the photographer may travel, even in empty space.
When world collision is enabled, Lente creates a floating CharacterBody3D at the gameplay camera with a small SphereShape3D of radius collision_radius. The body has no collision layer of its own and scans only collision_mask. Each physics tick, Lente calculates the smoothed flight velocity and calls move_and_slide(). Godot removes the velocity component pointing into a contacted surface while preserving the tangent component—so pushing diagonally into a wall should move the camera along the wall instead of stopping it completely.
Gameplay cameras often begin inside the player character’s capsule. Lente now detects that initial overlap and temporarily leaves world collision disarmed. As soon as the camera clears the overlapping body, collision arms automatically. This prevents the physics engine from shoving the photo camera out of the player on the first frame.
If collision feels awkward
The most common causes are a mask that includes detailed prop colliders, a sphere that is too large for the level, or the soft boundary being mistaken for collision. Recommended tuning order:
- Temporarily set Collision Enabled to
false. If the heavy feeling remains, it comes fromsoft_boundary_distanceor acceleration—not world collision. - Give photo collision a dedicated physics layer containing walls and major level shells, then assign only that layer to
collision_mask. Small props, foliage, the player body, and invisible gameplay blockers usually should not be included. - Reduce
collision_radiusfrom0.22toward0.10–0.16for tight interiors. - Keep
collision_safe_marginsmall (0.001–0.005). Increase it only to solve visible contact jitter. - Lower
collision_max_slidesif complex corners produce circling or repeated deflections.
To remove solid collision for a project or scene, uncheck World collision → Collision Enabled before entering photo mode:
$LentePhotoMode.collision_enabled = false
It can also be changed safely during a session:
$LentePhotoMode.set_world_collision_enabled(false)
Authored/fallback bounds remain active when world collision is disabled. Lente keeps sliding enabled by default because preventing obvious wall clipping protects the photographic illusion, but it is intentionally optional; games with dense collision meshes will often feel better with a dedicated simplified mask or with solid collision disabled.
Authoring boundaries
Select LentePhotoMode and use the Inspector buttons to add:
LenteBoxBoundfor rooms, platforms, and oriented regions.LenteSphereBoundfor pockets around landmarks.LentePathBoundfor a tubular corridor following aCurve3D.
All descendants of the photo-mode node are discovered recursively and combine as a union. The boundary starts damping outward velocity inside soft_boundary_distance, then projects the camera gently back to the permitted shell if needed. If authored volumes miss the gameplay camera at entry, Lente refuses to teleport and safely uses its fallback bubble for that session.
Built-in color filters
The default Color mood menu contains twelve shader-based looks: Neutral, Cinema, Noir, Warm, Cool, Vintage, Vivid, Bleach Bypass, Teal & Orange, Faded Film, Dream, and Night. Selecting one now applies a complete recommended preset—filter strength, vignette, saturation, contrast, and temperature—so its effect is immediately visible and the affected sliders update. Neutral restores neutral grading. You can refine any value afterward; filters affect both the live view and the supersampled capture.
The same behavior is available in code:
$LentePhotoMode.apply_filter_preset(8) # Teal & Orange
Game-owned ColorRect filters
Games often (or at least mine) already draw a full-screen ColorRect with a ShaderMaterial for tints, stylized color grading, night vision, CRT effects and so on. The gameplay viewport displays that rectangle, but Lente's high-resolution capture uses a separate SubViewport; without explicit integration, the game filter would be visible in the live preview but absent from the saved PNG.
To control such a filter, select LentePhotoMode and expand Game screen filters in the Inspector:
- Expand Screen Filter Paths, add an element, and use the node picker to select the filter
ColorRect. Add more elements for multiple full-screen passes. Paths are stored relative toLentePhotoMode. - Set Include Screen Filters to the developer's default:
- enabled: keep the assigned filters in photo mode and saved photographs;
- disabled: hide them during photo mode and omit them from saved photographs.
- Enable Allow Player Screen Filter Toggle only if players should see a localized Keep game filter switch in Lente's settings. Leave it disabled to make the developer's choice fixed.
Only explicitly assigned nodes are managed, so HUD panels and unrelated ColorRect controls are never selected accidentally. A filter that was already hidden when photo mode opened stays excluded. On exit, Lente restores every assigned rectangle's exact original visible value, including after an immediate exit or source-camera removal.
At capture time, Lente creates a full-screen rectangle in the capture viewport and reuses the assigned node's color, modulation, texture settings, and material. Standard full-screen ShaderMaterial effects—including shaders that read hint_screen_texture—therefore apply at the photograph's output resolution. The assigned ColorRect itself is reproduced; child controls, scripts, and unrelated CanvasLayer contents are not copied.
The same session choice is available in code, including for a custom UI:
$LentePhotoMode.set_screen_filters_enabled(false)
# Equivalent state parameter:
$LentePhotoMode.set_parameter(&"screen_filters_enabled", false)
When the player toggle is disabled, saved presets cannot override the developer-fixed setting. The active value and assigned-filter count are still included in controller state and capture metadata for diagnostics and reproducibility.
Interface language
Lente ships complete English (en), Italian (it), and Spanish (es) UI catalogs. It reads TranslationServer.get_locale() when the interface is created, accepts regional variants such as it_IT and es_MX, and uses English for every unsupported locale. This does not replace or change your project’s global locale.
TranslationServer.set_locale("es")
$LentePhotoMode.enter_photo_mode()
Captures and gallery
By default, photos are saved to the real Documents location reported by the operating system:
<Documents>/<application/config/name>/screenshots/
At the default 2× scale, a 1920×1080 game produces a 3840×2160 image. maximum_capture_dimension prevents accidental excessive allocations. Every PNG gets a same-name JSON file containing:
- project, plugin, engine, and timestamp information;
- output resolution;
- camera position, rotation, near/far planes, and cull mask;
- every lens and grading setting;
- the boundary source used for the session.
Captures require an active rendering backend. Godot’s --headless mode deliberately disables rendering; Lente detects the missing frame and reports a capture failure instead of hanging.
Pause behavior
When pause_world is enabled, Lente records whether the tree was already paused before touching it. Nodes in any continue_processing_groups entry are temporarily changed to PROCESS_MODE_ALWAYS and restored to their exact previous mode on exit. Add particles, weather, cloth controllers, or ambience to the default lente_unpaused group to let them keep breathing.
Custom UI
Assign any PackedScene to ui_scene. Its root may be a Control or CanvasLayer. The compact contract is:
signal command_requested(command: StringName, payload: Variant)
func bind_lente(controller: LentePhotoMode) -> void:
controller.state_changed.connect(_on_state_changed)
controller.photo_captured.connect(_on_photo_captured)
_on_state_changed(controller.get_state())
Emit commands such as set_parameter, capture, focus, exit, reset, set_ui_interactive, save_preset, and load_preset. See API.md for exact payloads and the public controller API.
For implementation details, lifecycle ordering, collision/boundary math, capture internals, extension points, and performance notes, see TECHNICAL.md.
Compatibility
Developed on Godot 4.4.1 with Forward+; the runtime also captures correctly with the Compatibility renderer. Depth of field is an engine feature available in Forward+ and Mobile, not Compatibility. The add-on uses only GDScript and standard Godot resources—no native library or autoload is required.
Development disclosure
Lente was developed with assistance from OpenAI Codex for implementation, and for translating and revising documentation (Italians, we're bad in english sorry). The project was directed, reviewed, and validated in Godot by me, the human. The add-on does not use generative AI at runtime (of course is a photo mode) and contains no AI-generated art or audio (except from your game that are for sure AI-generated).
License
Lente is available under the MIT License.
Donations
If you want to donate me an enormous amount of money, text me, I'll provide a link to my bank account.
Changelog for version v.09999
No changelog provided for this version.