-
-
Notifications
You must be signed in to change notification settings - Fork 36.1k
Sky & Water: Improve physical accuracy and HDR support #32325
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Conversation
c29265e to
45386f8
Compare
45386f8 to
d116c6d
Compare
| renderer.setAnimationLoop( animate ); | ||
| renderer.toneMapping = THREE.ACESFilmicToneMapping; | ||
| renderer.toneMappingExposure = 0.5; | ||
| renderer.toneMappingExposure = 0.0025; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We normally not use such low tone mapping exposure values in our demos. How is this setting compatible when combining the sky with a PBR/IBL scene that requires a higher exposure?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Turns out the sun is very bright 🙃
If we want to be physically accurate seems like it's the only option.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just fear this change might break existing usage and apps won't get the exposure right for their scenes.
What do you think of a new flag like physicallyCorrectLighting that enables the new code path?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually I don't think I've ever seen anyone using Sky in the wild... 🤔 Have you?
Only Shota but he did his own.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There were some questions related to THREE.Sky at the forum and stackoverflow in the past so the effect is used for sure e.g.:
https://discourse.threejs.org/t/sky-shader-example/13653
https://discourse.threejs.org/t/sky-js-shader-example-why-doesnt-it-use-a-cube-map-camera/16602
TBH, I have no good feeling that the effect requires now such a special tone mapping exposure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll ask Claude to research how all the other engines handle this problem 👀
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I'll test asking https://jules.google/ now that it can be repoless.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's what Jules said:
Unreal, Unity, Filament, Godot
I have researched how other major rendering engines (Unreal Engine, Unity HDRP, Filament, Godot 4) handle the high intensity of the Sun and Sky.
Here is the summary of how they handle it:
1. They use Physical Units (High Values)
All modern PBR engines explicitly use physical units for lighting. They do not clamp or arbitrarily reduce the sun's intensity.
-
Unreal Engine: Uses Lux for Directional Lights. The default Sun intensity is ~100,000 - 120,000 Lux.
-
Unity HDRP: Uses Lux. The sun is consistently set to ~100,000 Lux.
-
Filament: Uses Lux. Sun is ~100,000 Lux.
-
Godot 4: Has moved to physical units, also using ~100,000 Lux for the sun.
2. They rely on Physical Camera Exposure (EV)
To handle these massive values, these engines rely on a physical camera model.
-
The scene is rendered with a camera Exposure Value (EV100) appropriate for daylight (typically EV 14-15).
-
Auto-Exposure (Eye Adaptation): Engines like Unreal and Unity heavily rely on auto-exposure to dynamically adjust the EV based on the scene's average luminance, allowing the camera to transition from bright outdoors (EV 15) to dark indoors (EV 5-8).
3. The "Dark Scene" Issue (Compatibility)
The concern raised in the PR ("mixing... with a PBR/IBL scene that requires a higher exposure") arises from mixing Physical units with Legacy/Arbitrary units.
-
In a fully physical workflow (Unreal/Unity), you cannot use "Intensity 1" lights alongside a "Intensity 100,000" Sun.
-
If you expose for the Sun (EV 15), an "Intensity 1" light becomes invisible.
-
The Solution in other engines: Users must set all scene lights to physical values. A standard 60W lightbulb is not Intensity 1, but ~800 Lumens (approx ~100 candela).
-
If the user wants to use the physical Sun, they must upgrade their other lights to physical intensities, or the scene will indeed look black.
4. Technical Note: "Pre-exposure" (Important for HalfFloat)
Google's Filament engine documentation notes a critical implementation detail relevant to HalfFloatType render targets (which the PR mentions):
-
FP16 (HalfFloat) maxes out at 65,504.
-
A Sun intensity of 100,000+ will clip if written directly to a HalfFloat buffer.
-
Solution: Filament "pre-exposes" lights. Before rendering, they multiply the Light Intensity by the Camera Exposure value: EffectiveLightIntensity = LightIntensity * Exposure (e.g., 100,000 * 0.0025 = 250).
-
This allows the high-dynamic-range lighting to fit comfortably within the precision and range limits of HalfFloat buffers while maintaining physical accuracy.
Conclusion for Three.js PR
-
The PR's approach of using 0.0025 exposure is physically correct for a Sun intensity of ~100k.
-
Compatibility: To support legacy scenes (where lights are Intensity ~1), you generally have two options:
-
Physical Mode: Require the user to use physical intensities for all lights (Sun ~100k, Bulbs ~800lm, Exposure ~0.0025).
-
Legacy Mode (Scaling): Provide a intensityScale on the Sky/Sun object to scale the 100k down to ~1 (e.g., 0.00001 scale) so it matches legacy lighting environments without changing global exposure.
-
The "Pre-exposure" optimization from Filament might be a good addition to the PR to ensure the 100k sun doesn't clip in the HalfFloat render target before tone mapping.
Blender, Maya, 3DS Max
I have researched how Blender and other major 3D software (Maya/Arnold, 3ds Max/V-Ray) handle sun intensity and exposure.
Here is the summary:
1. They Use Physical Units (High Intensity)
Just like the game engines, the major 3D software packages generally use physical units for the sun, resulting in high intensity values.
-
Blender:
-
The Sun Lamp strength is measured in Watts per square meter (
$W/m^2$ ). A physically accurate value for sunlight is ~1000$W/m^2$ (roughly 120,000 Lux). -
The Nishita Sky texture (Blender's physical sky) outputs these physical values by default.
-
Note: The default Sun Lamp object in Blender starts at a strength of 1.0 (non-physical, very dim). Users are expected to increase this to ~1000 for physical accuracy, or use the Sky Texture which is physical by default.
-
-
Maya (Arnold): Uses physical sky models where intensity is often left at default (physical) or adjusted slightly (e.g., to 4-5), but relies on camera exposure.
-
3ds Max (V-Ray): The VRaySun is extremely bright by default. Users are explicitly taught to set the Physical Camera Exposure to roughly EV 13-15 (Sunny 16 rule) to prevent the image from being blown out.
2. They Rely on Exposure Controls
To handle the high dynamic range of a physical sun, all these tools rely on "Exposure" settings in the render or camera properties.
-
Blender: Uses Filmic (and recently AgX) color management. Users are advised to lower the Exposure value in the Color Management settings (often by ~6 stops) when using a physical sun/sky to get a properly exposed image.
-
V-Ray/Corona/Arnold: Strongly link the sun intensity to the Physical Camera settings (ISO, Shutter Speed, f-stop). If you don't use a physical camera with correct exposure, the render is white.
3. Compatibility with Standard Lights
The "dark scene" issue is standard across the industry when mixing physical sunlight with arbitrary point lights.
-
If you expose for the Sun (EV 15), a standard "Intensity 1" point light becomes invisible.
-
The Solution: Users must also set their artificial lights to physical values (e.g., a light bulb is not 1 Watt in Blender, but often needs to be set to its radiant flux in Watts, which might be much higher effectively depending on efficacy, or users crank values until it looks right relative to the sun).
-
Blender Add-ons: Popular add-ons like "Photographer" automatically handle this by letting users set exposure and light units physically (Lumens, Candelas) to match the physical sun.
Conclusion
The industry standard approach (Blender, V-Ray, Arnold) matches what game engines (Unreal, Unity) do:
-
Sun Intensity: High, physical values (~100k Lux / 1000
$W/m^2$ ). -
Exposure: Low exposure values (or High EV) to compensate.
-
Tone Mapping: Filmic/AgX/ACES to handle the range.
The "issue" of the sky being too bright is actually the correct physical behavior, and the solution is to expose for the sky and upgrade other lights to match.
Sounds like everyone relies on exposure.
I like the idea of doing something like:
renderer.tonemappingExposure = THREE.AutoExposure;There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's good to know that other engines do it this way but still we must be prepared on questions from the user side.
How would you suggest users should migrate from the first to the second fiddle?
r182dev: https://jsfiddle.net/8f4bmc9u/
r182dev + this PR: https://jsfiddle.net/qd71h04t/
As you can see, the sky gets almost complete white with an exposure of 0.5. If I change the value to 0.0025, the mesh gets black:
https://jsfiddle.net/4w6q7khn/
I have tried to generate an environment map from the sky via CubeCamera but you don't get a proper result as well: https://jsfiddle.net/8zef3b1u/
You must noticeably increase the exposure to see an effect which means the result is not consistent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
renderer.tonemappingExposure = THREE.AutoExposure;
Shouldn't auto-exposure be performed in post-processing?
I think we will need parameters for eye adaptation and limits too.
r182dev + this PR: https://jsfiddle.net/qd71h04t/
Just a feeling about this: shouldn't there be less exposure at midday? Apparently, the same exposure is being used at sunset and at midday when the sunlight is most intense to show the same luminance intensity?
I think so. The problem is that the shader doesn't have decent waves. |

Related: #3856 #4729
Description
This PR improves the physical accuracy of the
SkyandWaterobjects, ensuring they work correctly in a linear HDR workflow.Screen.Recording.2025-11-21.at.4.28.18.PM.mov
Screen.Recording.2025-11-21.at.4.30.25.PM.mov
Sky/SkyMesh: Removed baked-in tone mapping and magic constants to ensure linear HDR output. FixedvSunfadecalculation to be scale-independent.Water/WaterMesh: Updatedrf0to0.02(physically accurate for water), removed magic mixing constants, and fixed specular application to be additive.Water: Updated internal render target toHalfFloatTypeto correctly capture HDR reflections.WaterMesh: Switched toMeshBasicNodeMaterialto prevent incorrect IBL application, ensuring consistent contrast withWater.js.Examples: AdjustedtoneMappingExposureto0.0025in sky/water examples to accommodate the new linear HDR brightness.(Made using https://antigravity.google/ with Gemini 3.0)