Skip to content

Conversation

@mrdoob
Copy link
Owner

@mrdoob mrdoob commented Nov 21, 2025

Related: #3856 #4729

Description

This PR improves the physical accuracy of the Sky and Water objects, ensuring they work correctly in a linear HDR workflow.

Before After
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. Fixed vSunfade calculation to be scale-independent.
  • Water / WaterMesh: Updated rf0 to 0.02 (physically accurate for water), removed magic mixing constants, and fixed specular application to be additive.
  • Water: Updated internal render target to HalfFloatType to correctly capture HDR reflections.
  • WaterMesh: Switched to MeshBasicNodeMaterial to prevent incorrect IBL application, ensuring consistent contrast with Water.js.
  • Examples: Adjusted toneMappingExposure to 0.0025 in sky/water examples to accommodate the new linear HDR brightness.

(Made using https://antigravity.google/ with Gemini 3.0)

@mrdoob mrdoob added this to the r182 milestone Nov 21, 2025
@Mugen87
Copy link
Collaborator

Mugen87 commented Nov 21, 2025

Is the ocean supposed to look that black when looking straight from top?

image

renderer.setAnimationLoop( animate );
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 0.5;
renderer.toneMappingExposure = 0.0025;
Copy link
Collaborator

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?

Copy link
Owner Author

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.

Copy link
Collaborator

@Mugen87 Mugen87 Nov 21, 2025

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?

Copy link
Owner Author

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.

Copy link
Collaborator

@Mugen87 Mugen87 Nov 21, 2025

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.

Copy link
Owner Author

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 👀

Copy link
Owner Author

@mrdoob mrdoob Nov 21, 2025

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.

Copy link
Owner Author

@mrdoob mrdoob Nov 21, 2025

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:

    1. Physical Mode: Require the user to use physical intensities for all lights (Sun ~100k, Bulbs ~800lm, Exposure ~0.0025).

    2. 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:

  1. Sun Intensity: High, physical values (~100k Lux / 1000 $W/m^2$).

  2. Exposure: Low exposure values (or High EV) to compensate.

  3. 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;

Copy link
Collaborator

@Mugen87 Mugen87 Nov 22, 2025

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.

Copy link
Collaborator

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?

@mrdoob
Copy link
Owner Author

mrdoob commented Nov 21, 2025

Is the ocean supposed to look that black when looking straight from top?

I think so. The problem is that the shader doesn't have decent waves.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants