Passthrough brightness control by creating lighting spheres tutorial.
Resources.
How to make the brightness layer more natural.
What does 0.05f convert to in lux light intensity?
User experience of lighting modification using lighting spheres in AR.
When the right controller trigger is pressed, it will add a lighting sphere in that position.
When button A is pressed, it will delete the created sphere.
When button X is pressed, it will delete everything in the scene and restart.
Create a New project
Download Meta Core SDK, Meta - All in one package, Open XR, and Package Manager
Go to Edit > Project Setting > Meta XR > Apply all
Project setting > package management > select Open XR
Add Building blocks from tools
You can watch the video below for reference.
However Building Blocks update quickly and some parts are alreday outdated, so please view the rest of the tutorial for changes!
In the Hierarchy, add these building blocks:
Camera Rig Building Block
Passthrough Building Block
Room Guardian Building Block
This enables scanning your real room and placing objects within it.
Right-click in the Hierarchy → 3D Object → Create Sphere.
Scale it down (0.05, 0.05, 0.05) for a reasonable size.
Add a Point Light as a child (for glow effect).
Change intensity to 10
Reduce Range to 0.3
Change Color to yellow
Created materials folder and prefab folder in assets.
In the materials folder create material - save as 'yellow'
Save it as a Prefab (Assets/Prefabs/SpherePrefab).
This script will spawn the sphere when the right controller trigger is pressed.
Create a C# Script and name it SphereSpawner.cs.
Then copy the code below. (Code was written with the help of generative AI)
using UnityEngine;
using System.Collections.Generic;
public class SphereSpawner : MonoBehaviour
{
public GameObject BrightYellowLightPrefab;
public Transform controller;
private OVRPassthroughLayer passthroughLayer;
private List<GameObject> spawnedSpheres = new List<GameObject>();
private float currentBrightness = 0.05f; // Start brightness
private float maxBrightness = 2.0f;
private float minBrightness = 0.05f;
private float brightnessIncrement = 0.05f;
private float defaultBrightness = 0.05f;
void Start()
{
passthroughLayer = FindFirstObjectByType<OVRPassthroughLayer>();
if (passthroughLayer == null)
{
Debug.LogError("⚠️ OVRPassthroughLayer not found! Ensure Passthrough is enabled.");
return;
}
passthroughLayer.SetBrightnessContrastSaturation(defaultBrightness, 0.2f, 0.2f);
Debug.Log($"🔄 Initial Passthrough Brightness Set: {defaultBrightness}");
}
void Update()
{
if (OVRInput.GetDown(OVRInput.Button.SecondaryIndexTrigger)) // Right Trigger → Spawn Sphere & Increase Brightness
{
SpawnSphere();
IncreasePassthroughBrightness();
}
if (OVRInput.GetDown(OVRInput.Button.One)) // 'A' Button → Delete Last Sphere & Decrease Brightness
{
DeleteLastSpawnedSphere();
DecreasePassthroughBrightness();
}
if (OVRInput.GetDown(OVRInput.Button.Three)) // 'B' Button → Delete All Spheres & Reset Brightness
{
DeleteAllSpheres();
ResetPassthroughBrightness();
}
}
void SpawnSphere()
{
if (BrightYellowLightPrefab == null || controller == null)
{
Debug.LogError("⚠️ Prefab or Controller is not assigned!");
return;
}
Vector3 spawnPosition = controller.position;
GameObject newSphere = Instantiate(BrightYellowLightPrefab, spawnPosition, Quaternion.identity);
spawnedSpheres.Add(newSphere);
}
void DeleteLastSpawnedSphere()
{
if (spawnedSpheres.Count > 0)
{
GameObject lastSphere = spawnedSpheres[spawnedSpheres.Count - 1];
spawnedSpheres.RemoveAt(spawnedSpheres.Count - 1);
Destroy(lastSphere);
}
}
void DeleteAllSpheres()
{
foreach (GameObject sphere in spawnedSpheres)
{
Destroy(sphere);
}
spawnedSpheres.Clear();
Debug.Log("🗑️ All Spheres Deleted.");
}
void IncreasePassthroughBrightness()
{
if (passthroughLayer == null) return;
currentBrightness += brightnessIncrement;
currentBrightness = Mathf.Clamp(currentBrightness, minBrightness, maxBrightness);
passthroughLayer.SetBrightnessContrastSaturation(currentBrightness, 0.2f, 0.2f);
}
void DecreasePassthroughBrightness()
{
if (passthroughLayer == null) return;
currentBrightness -= brightnessIncrement;
currentBrightness = Mathf.Clamp(currentBrightness, minBrightness, maxBrightness);
passthroughLayer.SetBrightnessContrastSaturation(currentBrightness, 0.2f, 0.2f);
}
void ResetPassthroughBrightness()
{
if (passthroughLayer == null) return;
currentBrightness = defaultBrightness;
passthroughLayer.SetBrightnessContrastSaturation(currentBrightness, 0.2f, 0.2f);
Debug.Log("🔄 Passthrough Brightness Reset.");
}
}
Create an Empty GameObject and name it Sphere Spawner
Attach the SphereSpawner.cs script to the Sphere Spawner
Assign References in the Inspector:
Drag SpherePrefab into spherePrefab
Drag Right Controller into controller (right controller anchor)
How to use light and shadow in passthrough mode (Building blocks update quite quickly, additional googling could help keep track of updates!)
This function in OVRPassthroughLayer adjusts how the passthrough environment is rendered in your Meta Quest app by modifying brightness, contrast, and saturation.
Breakdown of Parameters
The variable brightnessIncrement is set to 0.05f.
When the Right Trigger is pressed, it calls the function IncreasePassthroughBrightness().
Inside IncreasePassthroughBrightness(), the brightness value (currentBrightness) is increased by brightnessIncrement (0.05f).
The new brightness value is clamped between minBrightness (0.05f) and maxBrightness (2.0f), so it never goes beyond these limits.
The updated brightness is applied using passthroughLayer.SetBrightnessContrastSaturation(currentBrightness, 0.2f, 0.2f)
passthroughLayer.SetBrightnessContrastSaturation(currentBrightness, 0.2f, 0.2f);
brightness → Adjusts the overall light intensity of the passthrough image.
Starts at 0.05f (default).
Increases by 0.05f per Right Trigger press (up to a max of 2.0f).
Decreases by 0.05f per 'A' button press (down to a min of 0.05f).
Resets to 0.05f when the 'B' button is pressed.
contrast (0.2f) → Controls the difference between light and dark areas.
saturation (0.2f) → Adjusts color intensity (how "vivid" the real-world colors appear).
Try out different numbers and optimize to get the effect you're looking for!
Passthrough brightness in SetBrightnessContrastSaturation(float brightness, float contrast, float saturation) is not directly measured in lux (lumens per square meter), but we can estimate based on real-world comparisons.
Approximate Brightness-to-Lux Estimation :
How much is 0.5f in lux?
Understanding the Conversion :
OVRPassthroughLayer brightness values (0.05 - 2.0) are relative adjustments that affect the overall passthrough image in Meta Quest's AR environment.
Lux (lx) measures real-world light intensity in terms of lumens per square meter.
Estimating Lux from Brightness Factor:
There is no direct built-in conversion from OVRPassthrough brightness (0.05f - 2.0f) to lux because:
The passthrough brightness is a digital multiplier applied to the passthrough image, not an absolute lighting measurement.
The actual lux level in a scene depends on the physical lighting conditions.
However, we can estimate relative brightness effects:
A brightness factor of 1.0f (default) may correspond to ambient indoor lighting (~300-500 lx).
A brightness factor of 0.05f is much lower than default and may represent a dim environment (~15-50 lx, similar to candlelight or twilight).
A brightness factor of 2.0f may simulate very bright indoor lighting (~1000+ lx, similar to a well-lit office or indirect sunlight).
Usability
Engagement
Added by : Eunjin Hong 2025/03/12