Implement filmic color grading. (#13121)

This commit expands Bevy's existing tonemapping feature to a complete
set of filmic color grading tools, matching those of engines like Unity,
Unreal, and Godot. The following features are supported:

* White point adjustment. This is inspired by Unity's implementation of
the feature, but simplified and optimized. *Temperature* and *tint*
control the adjustments to the *x* and *y* chromaticity values of [CIE
1931]. Following Unity, the adjustments are made relative to the [D65
standard illuminant] in the [LMS color space].

* Hue rotation. This simply converts the RGB value to [HSV], alters the
hue, and converts back.

* Color correction. This allows the *gamma*, *gain*, and *lift* values
to be adjusted according to the standard [ASC CDL combined function].

* Separate color correction for shadows, midtones, and highlights.
Blender's source code was used as a reference for the implementation of
this. The midtone ranges can be adjusted by the user. To avoid abrupt
color changes, a small crossfade is used between the different sections
of the image, again following Blender's formulas.

A new example, `color_grading`, has been added, offering a GUI to change
all the color grading settings. It uses the same test scene as the
existing `tonemapping` example, which has been factored out into a
shared glTF scene.

[CIE 1931]: https://en.wikipedia.org/wiki/CIE_1931_color_space

[D65 standard illuminant]:
https://en.wikipedia.org/wiki/Standard_illuminant#Illuminant_series_D

[LMS color space]: https://en.wikipedia.org/wiki/LMS_color_space

[HSV]: https://en.wikipedia.org/wiki/HSL_and_HSV

[ASC CDL combined function]:
https://en.wikipedia.org/wiki/ASC_CDL#Combined_Function

## Changelog

### Added

* Many new filmic color grading options have been added to the
`ColorGrading` component.

## Migration Guide

* `ColorGrading::gamma` and `ColorGrading::pre_saturation` are now set
separately for the `shadows`, `midtones`, and `highlights` sections. You
can migrate code with the `ColorGrading::all_sections` and
`ColorGrading::all_sections_mut` functions, which access and/or update
all sections at once.
* `ColorGrading::post_saturation` and `ColorGrading::exposure` are now
fields of `ColorGrading::global`.

## Screenshots

![Screenshot 2024-04-27
143144](https://github.com/bevyengine/bevy/assets/157897/c1de5894-917d-4101-b5c9-e644d141a941)

![Screenshot 2024-04-27
143216](https://github.com/bevyengine/bevy/assets/157897/da393c8a-d747-42f5-b47c-6465044c788d)
This commit is contained in:
Patrick Walton 2024-05-02 07:18:59 -05:00 committed by GitHub
parent abddbf2d95
commit 961b24deaf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 1969 additions and 184 deletions

View file

@ -2910,6 +2910,17 @@ description = "Demonstrates FPS overlay"
category = "Dev tools"
wasm = true
[[example]]
name = "color_grading"
path = "examples/3d/color_grading.rs"
doc-scrape-examples = true
[package.metadata.example.color_grading]
name = "Color grading"
description = "Demonstrates color grading"
category = "3D Rendering"
wasm = true
[profile.wasm-release]
inherits = "release"
opt-level = "z"

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

View file

@ -0,0 +1,679 @@
{
"asset":{
"generator":"Khronos glTF Blender I/O v4.0.44",
"version":"2.0"
},
"scene":0,
"scenes":[
{
"name":"Scene",
"nodes":[
0,
1,
2
]
}
],
"nodes":[
{
"mesh":0,
"name":"Plane",
"scale":[
50,
1,
50
]
},
{
"mesh":1,
"name":"Cube",
"translation":[
-1,
0.125,
0
]
},
{
"mesh":2,
"name":"Sphere",
"translation":[
0,
0.125,
0
]
}
],
"materials":[
{
"doubleSided":true,
"name":"Material.001",
"pbrMetallicRoughness":{
"baseColorFactor":[
0.10000000149011612,
0.20000000298023224,
0.10000000149011612,
1
],
"metallicFactor":0,
"roughnessFactor":0.5
}
},
{
"doubleSided":true,
"name":"Material.002",
"pbrMetallicRoughness":{
"baseColorTexture":{
"index":0
},
"metallicFactor":0,
"roughnessFactor":0.5
}
},
{
"doubleSided":true,
"name":"Sphere0",
"pbrMetallicRoughness":{
"baseColorFactor":[
0,
0,
1,
1
],
"metallicFactor":0,
"roughnessFactor":0.08900000154972076
}
},
{
"doubleSided":true,
"name":"Sphere1",
"pbrMetallicRoughness":{
"baseColorFactor":[
0,
1,
0,
1
],
"metallicFactor":0,
"roughnessFactor":0.08900000154972076
}
},
{
"doubleSided":true,
"name":"Sphere2",
"pbrMetallicRoughness":{
"baseColorFactor":[
1,
0,
0,
1
],
"metallicFactor":0,
"roughnessFactor":0.08900000154972076
}
},
{
"doubleSided":true,
"name":"Sphere3",
"pbrMetallicRoughness":{
"baseColorFactor":[
0.20000000298023224,
0.20000000298023224,
1,
1
],
"metallicFactor":0,
"roughnessFactor":0.08900000154972076
}
},
{
"doubleSided":true,
"name":"Sphere4",
"pbrMetallicRoughness":{
"baseColorFactor":[
0.20000000298023224,
1,
0.20000000298023224,
1
],
"metallicFactor":0,
"roughnessFactor":0.08900000154972076
}
},
{
"doubleSided":true,
"name":"Sphere5",
"pbrMetallicRoughness":{
"baseColorFactor":[
1,
0.20000000298023224,
0.20000000298023224,
1
],
"metallicFactor":0,
"roughnessFactor":0.08900000154972076
}
}
],
"meshes":[
{
"name":"Plane",
"primitives":[
{
"attributes":{
"POSITION":0,
"NORMAL":1,
"TEXCOORD_0":2
},
"indices":3,
"material":0
}
]
},
{
"name":"Cube.001",
"primitives":[
{
"attributes":{
"POSITION":4,
"NORMAL":5,
"TEXCOORD_0":6
},
"indices":7,
"material":1
}
]
},
{
"name":"Sphere",
"primitives":[
{
"attributes":{
"POSITION":8,
"NORMAL":9,
"TEXCOORD_0":10
},
"indices":11,
"material":2
},
{
"attributes":{
"POSITION":12,
"NORMAL":13,
"TEXCOORD_0":14
},
"indices":11,
"material":3
},
{
"attributes":{
"POSITION":15,
"NORMAL":16,
"TEXCOORD_0":17
},
"indices":11,
"material":4
},
{
"attributes":{
"POSITION":18,
"NORMAL":19,
"TEXCOORD_0":20
},
"indices":11,
"material":5
},
{
"attributes":{
"POSITION":21,
"NORMAL":22,
"TEXCOORD_0":23
},
"indices":11,
"material":6
},
{
"attributes":{
"POSITION":24,
"NORMAL":25,
"TEXCOORD_0":26
},
"indices":11,
"material":7
}
]
}
],
"textures":[
{
"sampler":0,
"source":0
}
],
"images":[
{
"mimeType":"image/png",
"name":"TestPattern",
"uri":"TestPattern.png"
}
],
"accessors":[
{
"bufferView":0,
"componentType":5126,
"count":4,
"max":[
1,
0,
1
],
"min":[
-1,
0,
-1
],
"type":"VEC3"
},
{
"bufferView":1,
"componentType":5126,
"count":4,
"type":"VEC3"
},
{
"bufferView":2,
"componentType":5126,
"count":4,
"type":"VEC2"
},
{
"bufferView":3,
"componentType":5123,
"count":6,
"type":"SCALAR"
},
{
"bufferView":4,
"componentType":5126,
"count":120,
"max":[
1.125,
0.125,
0.125
],
"min":[
-0.125,
-0.125,
-2.125
],
"type":"VEC3"
},
{
"bufferView":5,
"componentType":5126,
"count":120,
"type":"VEC3"
},
{
"bufferView":6,
"componentType":5126,
"count":120,
"type":"VEC2"
},
{
"bufferView":7,
"componentType":5123,
"count":180,
"type":"SCALAR"
},
{
"bufferView":8,
"componentType":5126,
"count":625,
"max":[
-0.42500004172325134,
0.125,
0.37499991059303284
],
"min":[
-0.6749998927116394,
-0.125,
0.125
],
"type":"VEC3"
},
{
"bufferView":9,
"componentType":5126,
"count":625,
"type":"VEC3"
},
{
"bufferView":10,
"componentType":5126,
"count":625,
"type":"VEC2"
},
{
"bufferView":11,
"componentType":5123,
"count":3264,
"type":"SCALAR"
},
{
"bufferView":12,
"componentType":5126,
"count":625,
"max":[
-0.17500004172325134,
0.125,
0.12499991804361343
],
"min":[
-0.4249998927116394,
-0.125,
-0.125
],
"type":"VEC3"
},
{
"bufferView":13,
"componentType":5126,
"count":625,
"type":"VEC3"
},
{
"bufferView":14,
"componentType":5126,
"count":625,
"type":"VEC2"
},
{
"bufferView":15,
"componentType":5126,
"count":625,
"max":[
0.07499995082616806,
0.125,
-0.12500005960464478
],
"min":[
-0.1749998927116394,
-0.125,
-0.3749999701976776
],
"type":"VEC3"
},
{
"bufferView":16,
"componentType":5126,
"count":625,
"type":"VEC3"
},
{
"bufferView":17,
"componentType":5126,
"count":625,
"type":"VEC2"
},
{
"bufferView":18,
"componentType":5126,
"count":625,
"max":[
-0.1250000298023224,
0.125,
0.6749999523162842
],
"min":[
-0.37499988079071045,
-0.125,
0.42500001192092896
],
"type":"VEC3"
},
{
"bufferView":19,
"componentType":5126,
"count":625,
"type":"VEC3"
},
{
"bufferView":20,
"componentType":5126,
"count":625,
"type":"VEC2"
},
{
"bufferView":21,
"componentType":5126,
"count":625,
"max":[
0.12499996274709702,
0.125,
0.4249999225139618
],
"min":[
-0.12499988079071045,
-0.125,
0.17500001192092896
],
"type":"VEC3"
},
{
"bufferView":22,
"componentType":5126,
"count":625,
"type":"VEC3"
},
{
"bufferView":23,
"componentType":5126,
"count":625,
"type":"VEC2"
},
{
"bufferView":24,
"componentType":5126,
"count":625,
"max":[
0.3749999403953552,
0.125,
0.1749999225139618
],
"min":[
0.12500008940696716,
-0.125,
-0.07499998807907104
],
"type":"VEC3"
},
{
"bufferView":25,
"componentType":5126,
"count":625,
"type":"VEC3"
},
{
"bufferView":26,
"componentType":5126,
"count":625,
"type":"VEC2"
}
],
"bufferViews":[
{
"buffer":0,
"byteLength":48,
"byteOffset":0,
"target":34962
},
{
"buffer":0,
"byteLength":48,
"byteOffset":48,
"target":34962
},
{
"buffer":0,
"byteLength":32,
"byteOffset":96,
"target":34962
},
{
"buffer":0,
"byteLength":12,
"byteOffset":128,
"target":34963
},
{
"buffer":0,
"byteLength":1440,
"byteOffset":140,
"target":34962
},
{
"buffer":0,
"byteLength":1440,
"byteOffset":1580,
"target":34962
},
{
"buffer":0,
"byteLength":960,
"byteOffset":3020,
"target":34962
},
{
"buffer":0,
"byteLength":360,
"byteOffset":3980,
"target":34963
},
{
"buffer":0,
"byteLength":7500,
"byteOffset":4340,
"target":34962
},
{
"buffer":0,
"byteLength":7500,
"byteOffset":11840,
"target":34962
},
{
"buffer":0,
"byteLength":5000,
"byteOffset":19340,
"target":34962
},
{
"buffer":0,
"byteLength":6528,
"byteOffset":24340,
"target":34963
},
{
"buffer":0,
"byteLength":7500,
"byteOffset":30868,
"target":34962
},
{
"buffer":0,
"byteLength":7500,
"byteOffset":38368,
"target":34962
},
{
"buffer":0,
"byteLength":5000,
"byteOffset":45868,
"target":34962
},
{
"buffer":0,
"byteLength":7500,
"byteOffset":50868,
"target":34962
},
{
"buffer":0,
"byteLength":7500,
"byteOffset":58368,
"target":34962
},
{
"buffer":0,
"byteLength":5000,
"byteOffset":65868,
"target":34962
},
{
"buffer":0,
"byteLength":7500,
"byteOffset":70868,
"target":34962
},
{
"buffer":0,
"byteLength":7500,
"byteOffset":78368,
"target":34962
},
{
"buffer":0,
"byteLength":5000,
"byteOffset":85868,
"target":34962
},
{
"buffer":0,
"byteLength":7500,
"byteOffset":90868,
"target":34962
},
{
"buffer":0,
"byteLength":7500,
"byteOffset":98368,
"target":34962
},
{
"buffer":0,
"byteLength":5000,
"byteOffset":105868,
"target":34962
},
{
"buffer":0,
"byteLength":7500,
"byteOffset":110868,
"target":34962
},
{
"buffer":0,
"byteLength":7500,
"byteOffset":118368,
"target":34962
},
{
"buffer":0,
"byteLength":5000,
"byteOffset":125868,
"target":34962
}
],
"samplers":[
{
"magFilter":9728,
"minFilter":9984
}
],
"buffers":[
{
"byteLength":130868,
"uri":"TonemappingTest.bin"
}
]
}

View file

@ -11,11 +11,12 @@ use bevy_render::render_resource::binding_types::{
};
use bevy_render::renderer::RenderDevice;
use bevy_render::texture::{CompressedImageFormats, GpuImage, Image, ImageSampler, ImageType};
use bevy_render::view::{ViewTarget, ViewUniform};
use bevy_render::view::{ExtractedView, ViewTarget, ViewUniform};
use bevy_render::{camera::Camera, texture::FallbackImage};
use bevy_render::{render_resource::*, Render, RenderApp, RenderSet};
#[cfg(not(feature = "tonemapping_luts"))]
use bevy_utils::tracing::error;
use bitflags::bitflags;
mod node;
@ -179,10 +180,27 @@ impl Tonemapping {
}
}
bitflags! {
/// Various flags describing what tonemapping needs to do.
///
/// This allows the shader to skip unneeded steps.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct TonemappingPipelineKeyFlags: u8 {
/// The hue needs to be changed.
const HUE_ROTATE = 0x01;
/// The white balance needs to be adjusted.
const WHITE_BALANCE = 0x02;
/// Saturation/contrast/gamma/gain/lift for one or more sections
/// (shadows, midtones, highlights) need to be adjusted.
const SECTIONAL_COLOR_GRADING = 0x04;
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct TonemappingPipelineKey {
deband_dither: DebandDither,
tonemapping: Tonemapping,
flags: TonemappingPipelineKeyFlags,
}
impl SpecializedRenderPipeline for TonemappingPipeline {
@ -194,6 +212,23 @@ impl SpecializedRenderPipeline for TonemappingPipeline {
shader_defs.push("DEBAND_DITHER".into());
}
// Define shader flags depending on the color grading options in use.
if key.flags.contains(TonemappingPipelineKeyFlags::HUE_ROTATE) {
shader_defs.push("HUE_ROTATE".into());
}
if key
.flags
.contains(TonemappingPipelineKeyFlags::WHITE_BALANCE)
{
shader_defs.push("WHITE_BALANCE".into());
}
if key
.flags
.contains(TonemappingPipelineKeyFlags::SECTIONAL_COLOR_GRADING)
{
shader_defs.push("SECTIONAL_COLOR_GRADING".into());
}
match key.tonemapping {
Tonemapping::None => shader_defs.push("TONEMAP_METHOD_NONE".into()),
Tonemapping::Reinhard => shader_defs.push("TONEMAP_METHOD_REINHARD".into()),
@ -292,12 +327,38 @@ pub fn prepare_view_tonemapping_pipelines(
pipeline_cache: Res<PipelineCache>,
mut pipelines: ResMut<SpecializedRenderPipelines<TonemappingPipeline>>,
upscaling_pipeline: Res<TonemappingPipeline>,
view_targets: Query<(Entity, Option<&Tonemapping>, Option<&DebandDither>), With<ViewTarget>>,
view_targets: Query<
(
Entity,
&ExtractedView,
Option<&Tonemapping>,
Option<&DebandDither>,
),
With<ViewTarget>,
>,
) {
for (entity, tonemapping, dither) in view_targets.iter() {
for (entity, view, tonemapping, dither) in view_targets.iter() {
// As an optimization, we omit parts of the shader that are unneeded.
let mut flags = TonemappingPipelineKeyFlags::empty();
flags.set(
TonemappingPipelineKeyFlags::HUE_ROTATE,
view.color_grading.global.hue != 0.0,
);
flags.set(
TonemappingPipelineKeyFlags::WHITE_BALANCE,
view.color_grading.global.temperature != 0.0 || view.color_grading.global.tint != 0.0,
);
flags.set(
TonemappingPipelineKeyFlags::SECTIONAL_COLOR_GRADING,
view.color_grading
.all_sections()
.any(|section| *section != default()),
);
let key = TonemappingPipelineKey {
deband_dither: *dither.unwrap_or(&DebandDither::Disabled),
tonemapping: *tonemapping.unwrap_or(&Tonemapping::None),
flags,
};
let pipeline = pipelines.specialize(&pipeline_cache, &upscaling_pipeline, key);

View file

@ -1,6 +1,7 @@
#define_import_path bevy_core_pipeline::tonemapping
#import bevy_render::view::ColorGrading
#import bevy_pbr::utils::{PI_2, hsv_to_rgb, rgb_to_hsv};
// hack !! not sure what to do with this
#ifdef TONEMAPPING_PASS
@ -11,6 +12,15 @@
@group(0) @binding(19) var dt_lut_sampler: sampler;
#endif
// Half the size of the crossfade region between shadows and midtones and
// between midtones and highlights. This value, 0.1, corresponds to 10% of the
// gamut on either side of the cutoff point.
const LEVEL_MARGIN: f32 = 0.1;
// The inverse reciprocal of twice the above, used when scaling the midtone
// region.
const LEVEL_MARGIN_DIV: f32 = 0.5 / LEVEL_MARGIN;
fn sample_current_lut(p: vec3<f32>) -> vec3<f32> {
// Don't include code that will try to sample from LUTs if tonemap method doesn't require it
// Allows this file to be imported without necessarily needing the lut texture bindings
@ -273,22 +283,92 @@ fn screen_space_dither(frag_coord: vec2<f32>) -> vec3<f32> {
return (dither - 0.5) / 255.0;
}
fn tone_mapping(in: vec4<f32>, color_grading: ColorGrading) -> vec4<f32> {
// Performs the "sectional" color grading: i.e. the color grading that applies
// individually to shadows, midtones, and highlights.
fn sectional_color_grading(
in: vec3<f32>,
color_grading: ptr<function, ColorGrading>,
) -> vec3<f32> {
var color = in;
// Determine whether the color is a shadow, midtone, or highlight. Colors
// close to the edges are considered a mix of both, to avoid sharp
// discontinuities. The formulas are taken from Blender's compositor.
let level = (color.r + color.g + color.b) / 3.0;
// Determine whether this color is a shadow, midtone, or highlight. If close
// to the cutoff points, blend between the two to avoid sharp color
// discontinuities.
var levels = vec3(0.0);
let midtone_range = (*color_grading).midtone_range;
if (level < midtone_range.x - LEVEL_MARGIN) {
levels.x = 1.0;
} else if (level < midtone_range.x + LEVEL_MARGIN) {
levels.y = ((level - midtone_range.x) * LEVEL_MARGIN_DIV) + 0.5;
levels.z = 1.0 - levels.y;
} else if (level < midtone_range.y - LEVEL_MARGIN) {
levels.y = 1.0;
} else if (level < midtone_range.y + LEVEL_MARGIN) {
levels.z = ((level - midtone_range.y) * LEVEL_MARGIN_DIV) + 0.5;
levels.y = 1.0 - levels.z;
} else {
levels.z = 1.0;
}
// Calculate contrast/saturation/gamma/gain/lift.
let contrast = dot(levels, (*color_grading).contrast);
let saturation = dot(levels, (*color_grading).saturation);
let gamma = dot(levels, (*color_grading).gamma);
let gain = dot(levels, (*color_grading).gain);
let lift = dot(levels, (*color_grading).lift);
// Adjust saturation and contrast.
let luma = tonemapping_luminance(color);
color = luma + saturation * (color - luma);
color = 0.5 + (color - 0.5) * contrast;
// The [ASC CDL] formula for color correction. Given *i*, an input color, we
// have:
//
// out = (i × s + o)
//
// Following the normal photographic naming convention, *gain* is the *s*
// factor, *lift* is the *o* term, and the inverse of *gamma* is the *n*
// exponent.
//
// [ASC CDL]: https://en.wikipedia.org/wiki/ASC_CDL#Combined_Function
color = powsafe(color * gain + lift, 1.0 / gamma);
// Account for exposure.
color = color * powsafe(vec3(2.0), (*color_grading).exposure);
return max(color, vec3(0.0));
}
fn tone_mapping(in: vec4<f32>, in_color_grading: ColorGrading) -> vec4<f32> {
var color = max(in.rgb, vec3(0.0));
var color_grading = in_color_grading; // So we can take pointers to it.
// Possible future grading:
// Rotate hue if needed, by converting to and from HSV. Remember that hue is
// an angle, so it needs to be modulo 2π.
#ifdef HUE_ROTATE
var hsv = rgb_to_hsv(color);
hsv.r = (hsv.r + color_grading.hue) % PI_2;
color = hsv_to_rgb(hsv);
#endif
// highlight gain gamma: 0..
// let luma = powsafe(vec3(tonemapping_luminance(color)), 1.0);
// Perform white balance correction. Conveniently, this is a linear
// transform. The matrix was pre-calculated from the temperature and tint
// values on the CPU.
#ifdef WHITE_BALANCE
color = max(color_grading.balance * color, vec3(0.0));
#endif
// highlight gain: 0..
// color += color * luma.xxx * 1.0;
// Linear pre tonemapping grading
color = saturation(color, color_grading.pre_saturation);
color = powsafe(color, color_grading.gamma);
color = color * powsafe(vec3(2.0), color_grading.exposure);
color = max(color, vec3(0.0));
// Perform the "sectional" color grading: i.e. the color grading that
// applies individually to shadows, midtones, and highlights.
#ifdef SECTIONAL_COLOR_GRADING
color = sectional_color_grading(color, &color_grading);
#endif
// tone_mapping
#ifdef TONEMAP_METHOD_NONE

View file

@ -2,7 +2,7 @@
#import bevy_pbr::{
mesh_view_bindings as bindings,
utils::{hsv2rgb, rand_f},
utils::{PI_2, hsv_to_rgb, rand_f},
}
// NOTE: Keep in sync with bevy_pbr/src/light.rs
@ -78,7 +78,11 @@ fn cluster_debug_visualization(
if (z_slice & 1u) == 1u {
z_slice = z_slice + bindings::lights.cluster_dimensions.z / 2u;
}
let slice_color = hsv2rgb(f32(z_slice) / f32(bindings::lights.cluster_dimensions.z + 1u), 1.0, 0.5);
let slice_color = hsv_to_rgb(
f32(z_slice) / f32(bindings::lights.cluster_dimensions.z + 1u) * PI_2,
1.0,
0.5
);
output_color = vec4<f32>(
(1.0 - cluster_overlay_alpha) * output_color.rgb + cluster_overlay_alpha * slice_color,
output_color.a
@ -96,7 +100,7 @@ fn cluster_debug_visualization(
// NOTE: Visualizes the cluster to which the fragment belongs
let cluster_overlay_alpha = 0.1;
var rng = cluster_index;
let cluster_color = hsv2rgb(rand_f(&rng), 1.0, 0.5);
let cluster_color = hsv_to_rgb(rand_f(&rng) * PI_2, 1.0, 0.5);
output_color = vec4<f32>(
(1.0 - cluster_overlay_alpha) * output_color.rgb + cluster_overlay_alpha * cluster_color,
output_color.a

View file

@ -3,7 +3,7 @@
#import bevy_pbr::{
mesh_view_types::POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE,
mesh_view_bindings as view_bindings,
utils::hsv2rgb,
utils::{hsv_to_rgb, PI_2},
shadow_sampling::{SPOT_SHADOW_TEXEL_SIZE, sample_shadow_cubemap, sample_shadow_map}
}
@ -170,7 +170,11 @@ fn cascade_debug_visualization(
) -> vec3<f32> {
let overlay_alpha = 0.95;
let cascade_index = get_cascade_index(light_id, view_z);
let cascade_color = hsv2rgb(f32(cascade_index) / f32(#{MAX_CASCADES_PER_LIGHT}u + 1u), 1.0, 0.5);
let cascade_color = hsv_to_rgb(
f32(cascade_index) / f32(#{MAX_CASCADES_PER_LIGHT}u + 1u) * PI_2,
1.0,
0.5
);
return vec3<f32>(
(1.0 - overlay_alpha) * output_color.rgb + overlay_alpha * cascade_color
);

View file

@ -2,20 +2,53 @@
#import bevy_pbr::rgb9e5
const PI: f32 = 3.141592653589793;
const HALF_PI: f32 = 1.57079632679;
const E: f32 = 2.718281828459045;
const PI: f32 = 3.141592653589793; // π
const PI_2: f32 = 6.283185307179586; // 2π
const HALF_PI: f32 = 1.57079632679; // π/2
const FRAC_PI_3: f32 = 1.0471975512; // π/3
const E: f32 = 2.718281828459045; // exp(1)
fn hsv2rgb(hue: f32, saturation: f32, value: f32) -> vec3<f32> {
let rgb = clamp(
abs(
((hue * 6.0 + vec3<f32>(0.0, 4.0, 2.0)) % 6.0) - 3.0
) - 1.0,
vec3<f32>(0.0),
vec3<f32>(1.0)
);
// Converts HSV to RGB.
//
// Input: H [0, 2π), S [0, 1], V [0, 1].
// Output: R [0, 1], G [0, 1], B [0, 1].
//
// <https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB_alternative>
fn hsv_to_rgb(hsv: vec3<f32>) -> vec3<f32> {
let n = vec3(5.0, 3.0, 1.0);
let k = (n + hsv.x / FRAC_PI_3) % 6.0;
return hsv.z - hsv.z * hsv.y * max(vec3(0.0), min(k, min(4.0 - k, vec3(1.0))));
}
return value * mix(vec3<f32>(1.0), rgb, vec3<f32>(saturation));
// Converts RGB to HSV.
//
// Input: R [0, 1], G [0, 1], B [0, 1].
// Output: H [0, 2π), S [0, 1], V [0, 1].
//
// <https://en.wikipedia.org/wiki/HSL_and_HSV#From_RGB>
fn rgb_to_hsv(rgb: vec3<f32>) -> vec3<f32> {
let x_max = max(rgb.r, max(rgb.g, rgb.b)); // i.e. V
let x_min = min(rgb.r, min(rgb.g, rgb.b));
let c = x_max - x_min; // chroma
var swizzle = vec3<f32>(0.0);
if (x_max == rgb.r) {
swizzle = vec3(rgb.gb, 0.0);
} else if (x_max == rgb.g) {
swizzle = vec3(rgb.br, 2.0);
} else {
swizzle = vec3(rgb.rg, 4.0);
}
let h = FRAC_PI_3 * (((swizzle.x - swizzle.y) / c + swizzle.z) % 6.0);
// Avoid division by zero.
var s = 0.0;
if (x_max > 0.0) {
s = c / x_max;
}
return vec3(h, s, x_max);
}
// Generates a random u32 in range [0, u32::MAX].

View file

@ -853,7 +853,7 @@ pub fn extract_cameras(
gpu_culling,
) in query.iter()
{
let color_grading = *color_grading.unwrap_or(&ColorGrading::default());
let color_grading = color_grading.unwrap_or(&ColorGrading::default()).clone();
if !camera.is_active {
continue;

View file

@ -24,13 +24,16 @@ use crate::{
};
use bevy_app::{App, Plugin};
use bevy_ecs::prelude::*;
use bevy_math::{Mat4, UVec4, Vec3, Vec4, Vec4Swizzles};
use bevy_math::{mat3, vec2, vec3, Mat3, Mat4, UVec4, Vec2, Vec3, Vec4, Vec4Swizzles};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_transform::components::GlobalTransform;
use bevy_utils::HashMap;
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,
use std::{
ops::Range,
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
};
use wgpu::{
Extent3d, RenderPassColorAttachment, RenderPassDepthStencilAttachment, StoreOp,
@ -39,6 +42,55 @@ use wgpu::{
pub const VIEW_TYPE_HANDLE: Handle<Shader> = Handle::weak_from_u128(15421373904451797197);
/// The matrix that converts from the RGB to the LMS color space.
///
/// To derive this, first we convert from RGB to [CIE 1931 XYZ]:
///
/// ```text
/// ⎡ X ⎤ ⎡ 0.490 0.310 0.200 ⎤ ⎡ R ⎤
/// ⎢ Y ⎥ = ⎢ 0.177 0.812 0.011 ⎥ ⎢ G ⎥
/// ⎣ Z ⎦ ⎣ 0.000 0.010 0.990 ⎦ ⎣ B ⎦
/// ```
///
/// Then we convert to LMS according to the [CAM16 standard matrix]:
///
/// ```text
/// ⎡ L ⎤ ⎡ 0.401 0.650 -0.051 ⎤ ⎡ X ⎤
/// ⎢ M ⎥ = ⎢ -0.250 1.204 0.046 ⎥ ⎢ Y ⎥
/// ⎣ S ⎦ ⎣ -0.002 0.049 0.953 ⎦ ⎣ Z ⎦
/// ```
///
/// The resulting matrix is just the concatenation of these two matrices, to do
/// the conversion in one step.
///
/// [CIE 1931 XYZ]: https://en.wikipedia.org/wiki/CIE_1931_color_space
/// [CAM16 standard matrix]: https://en.wikipedia.org/wiki/LMS_color_space
static RGB_TO_LMS: Mat3 = mat3(
vec3(0.311692, 0.0905138, 0.00764433),
vec3(0.652085, 0.901341, 0.0486554),
vec3(0.0362225, 0.00814478, 0.943700),
);
/// The inverse of the [`RGB_TO_LMS`] matrix, converting from the LMS color
/// space back to RGB.
static LMS_TO_RGB: Mat3 = mat3(
vec3(4.06305, -0.40791, -0.0118812),
vec3(-2.93241, 1.40437, -0.0486532),
vec3(-0.130646, 0.00353630, 1.0605344),
);
/// The [CIE 1931] *xy* chromaticity coordinates of the [D65 white point].
///
/// [CIE 1931]: https://en.wikipedia.org/wiki/CIE_1931_color_space
/// [D65 white point]: https://en.wikipedia.org/wiki/Standard_illuminant#D65_values
static D65_XY: Vec2 = vec2(0.31272, 0.32903);
/// The [D65 white point] in [LMS color space].
///
/// [LMS color space]: https://en.wikipedia.org/wiki/LMS_color_space
/// [D65 white point]: https://en.wikipedia.org/wiki/Standard_illuminant#D65_values
static D65_LMS: Vec3 = vec3(0.975538, 1.01648, 1.08475);
pub struct ViewPlugin;
impl Plugin for ViewPlugin {
@ -129,40 +181,217 @@ impl ExtractedView {
}
}
/// Configures basic color grading parameters to adjust the image appearance. Grading is applied just before/after tonemapping for a given [`Camera`](crate::camera::Camera) entity.
#[derive(Component, Reflect, Debug, Copy, Clone, ShaderType)]
/// Configures filmic color grading parameters to adjust the image appearance.
///
/// Color grading is applied just before tonemapping for a given
/// [`Camera`](crate::camera::Camera) entity, with the sole exception of the
/// `post_saturation` value in [`ColorGradingGlobal`], which is applied after
/// tonemapping.
#[derive(Component, Reflect, Debug, Default, Clone)]
#[reflect(Component, Default)]
pub struct ColorGrading {
/// Filmic color grading values applied to the image as a whole (as opposed
/// to individual sections, like shadows and highlights).
pub global: ColorGradingGlobal,
/// Color grading values that are applied to the darker parts of the image.
///
/// The cutoff points can be customized with the
/// [`ColorGradingGlobal::midtones_range`] field.
pub shadows: ColorGradingSection,
/// Color grading values that are applied to the parts of the image with
/// intermediate brightness.
///
/// The cutoff points can be customized with the
/// [`ColorGradingGlobal::midtones_range`] field.
pub midtones: ColorGradingSection,
/// Color grading values that are applied to the lighter parts of the image.
///
/// The cutoff points can be customized with the
/// [`ColorGradingGlobal::midtones_range`] field.
pub highlights: ColorGradingSection,
}
/// Filmic color grading values applied to the image as a whole (as opposed to
/// individual sections, like shadows and highlights).
#[derive(Clone, Debug, Reflect)]
#[reflect(Default)]
pub struct ColorGradingGlobal {
/// Exposure value (EV) offset, measured in stops.
pub exposure: f32,
/// Non-linear luminance adjustment applied before tonemapping. y = pow(x, gamma)
pub gamma: f32,
/// An adjustment made to the [CIE 1931] chromaticity *x* value.
///
/// Positive values make the colors redder. Negative values make the colors
/// bluer. This has no effect on luminance (brightness).
///
/// [CIE 1931]: https://en.wikipedia.org/wiki/CIE_1931_color_space#CIE_xy_chromaticity_diagram_and_the_CIE_xyY_color_space
pub temperature: f32,
/// Saturation adjustment applied before tonemapping.
/// Values below 1.0 desaturate, with a value of 0.0 resulting in a grayscale image
/// with luminance defined by ITU-R BT.709.
/// Values above 1.0 increase saturation.
pub pre_saturation: f32,
/// An adjustment made to the [CIE 1931] chromaticity *y* value.
///
/// Positive values make the colors more magenta. Negative values make the
/// colors greener. This has no effect on luminance (brightness).
///
/// [CIE 1931]: https://en.wikipedia.org/wiki/CIE_1931_color_space#CIE_xy_chromaticity_diagram_and_the_CIE_xyY_color_space
pub tint: f32,
/// An adjustment to the [hue], in radians.
///
/// Adjusting this value changes the perceived colors in the image: red to
/// yellow to green to blue, etc. It has no effect on the saturation or
/// brightness of the colors.
///
/// [hue]: https://en.wikipedia.org/wiki/HSL_and_HSV#Formal_derivation
pub hue: f32,
/// Saturation adjustment applied after tonemapping.
/// Values below 1.0 desaturate, with a value of 0.0 resulting in a grayscale image
/// with luminance defined by ITU-R BT.709
/// Values above 1.0 increase saturation.
pub post_saturation: f32,
/// The luminance (brightness) ranges that are considered part of the
/// "midtones" of the image.
///
/// This affects which [`ColorGradingSection`]s apply to which colors. Note
/// that the sections smoothly blend into one another, to avoid abrupt
/// transitions.
///
/// The default value is 0.2 to 0.7.
pub midtones_range: Range<f32>,
}
impl Default for ColorGrading {
/// The [`ColorGrading`] structure, packed into the most efficient form for the
/// GPU.
#[derive(Clone, Copy, Debug, ShaderType)]
struct ColorGradingUniform {
balance: Mat3,
saturation: Vec3,
contrast: Vec3,
gamma: Vec3,
gain: Vec3,
lift: Vec3,
midtone_range: Vec2,
exposure: f32,
hue: f32,
post_saturation: f32,
}
/// A section of color grading values that can be selectively applied to
/// shadows, midtones, and highlights.
#[derive(Reflect, Debug, Copy, Clone, PartialEq)]
pub struct ColorGradingSection {
/// Values below 1.0 desaturate, with a value of 0.0 resulting in a grayscale image
/// with luminance defined by ITU-R BT.709.
/// Values above 1.0 increase saturation.
pub saturation: f32,
/// Adjusts the range of colors.
///
/// A value of 1.0 applies no changes. Values below 1.0 move the colors more
/// toward a neutral gray. Values above 1.0 spread the colors out away from
/// the neutral gray.
pub contrast: f32,
/// A nonlinear luminance adjustment, mainly affecting the high end of the
/// range.
///
/// This is the *n* exponent in the standard [ASC CDL] formula for color
/// correction:
///
/// ```text
/// out = (i × s + o)ⁿ
/// ```
///
/// [ASC CDL]: https://en.wikipedia.org/wiki/ASC_CDL#Combined_Function
pub gamma: f32,
/// A linear luminance adjustment, mainly affecting the middle part of the
/// range.
///
/// This is the *s* factor in the standard [ASC CDL] formula for color
/// correction:
///
/// ```text
/// out = (i × s + o)ⁿ
/// ```
///
/// [ASC CDL]: https://en.wikipedia.org/wiki/ASC_CDL#Combined_Function
pub gain: f32,
/// A fixed luminance adjustment, mainly affecting the lower part of the
/// range.
///
/// This is the *o* term in the standard [ASC CDL] formula for color
/// correction:
///
/// ```text
/// out = (i × s + o)ⁿ
/// ```
///
/// [ASC CDL]: https://en.wikipedia.org/wiki/ASC_CDL#Combined_Function
pub lift: f32,
}
impl Default for ColorGradingGlobal {
fn default() -> Self {
Self {
exposure: 0.0,
gamma: 1.0,
pre_saturation: 1.0,
temperature: 0.0,
tint: 0.0,
hue: 0.0,
post_saturation: 1.0,
midtones_range: 0.2..0.7,
}
}
}
impl Default for ColorGradingSection {
fn default() -> Self {
Self {
saturation: 1.0,
contrast: 1.0,
gamma: 1.0,
gain: 1.0,
lift: 0.0,
}
}
}
impl ColorGrading {
/// Creates a new [`ColorGrading`] instance in which shadows, midtones, and
/// highlights all have the same set of color grading values.
pub fn with_identical_sections(
global: ColorGradingGlobal,
section: ColorGradingSection,
) -> ColorGrading {
ColorGrading {
global,
highlights: section,
midtones: section,
shadows: section,
}
}
/// Returns an iterator that visits the shadows, midtones, and highlights
/// sections, in that order.
pub fn all_sections(&self) -> impl Iterator<Item = &ColorGradingSection> {
[&self.shadows, &self.midtones, &self.highlights].into_iter()
}
/// Applies the given mutating function to the shadows, midtones, and
/// highlights sections, in that order.
///
/// Returns an array composed of the results of such evaluation, in that
/// order.
pub fn all_sections_mut(&mut self) -> impl Iterator<Item = &mut ColorGradingSection> {
[&mut self.shadows, &mut self.midtones, &mut self.highlights].into_iter()
}
}
#[derive(Clone, ShaderType)]
pub struct ViewUniform {
view_proj: Mat4,
@ -177,7 +406,7 @@ pub struct ViewUniform {
// viewport(x_origin, y_origin, width, height)
viewport: Vec4,
frustum: [Vec4; 6],
color_grading: ColorGrading,
color_grading: ColorGradingUniform,
mip_bias: f32,
render_layers: u32,
}
@ -208,6 +437,85 @@ pub struct PostProcessWrite<'a> {
pub destination: &'a TextureView,
}
impl From<ColorGrading> for ColorGradingUniform {
fn from(component: ColorGrading) -> Self {
// Compute the balance matrix that will be used to apply the white
// balance adjustment to an RGB color. Our general approach will be to
// convert both the color and the developer-supplied white point to the
// LMS color space, apply the conversion, and then convert back.
//
// First, we start with the CIE 1931 *xy* values of the standard D65
// illuminant:
// <https://en.wikipedia.org/wiki/Standard_illuminant#D65_values>
//
// We then adjust them based on the developer's requested white balance.
let white_point_xy = D65_XY + vec2(-component.global.temperature, component.global.tint);
// Convert the white point from CIE 1931 *xy* to LMS. First, we convert to XYZ:
//
// Y Y
// Y = 1 X = ─ x Z = ─ (1 - x - y)
// y y
//
// Then we convert from XYZ to LMS color space, using the CAM16 matrix
// from <https://en.wikipedia.org/wiki/LMS_color_space#Later_CIECAMs>:
//
// ⎡ L ⎤ ⎡ 0.401 0.650 -0.051 ⎤ ⎡ X ⎤
// ⎢ M ⎥ = ⎢ -0.250 1.204 0.046 ⎥ ⎢ Y ⎥
// ⎣ S ⎦ ⎣ -0.002 0.049 0.953 ⎦ ⎣ Z ⎦
//
// The following formula is just a simplification of the above.
let white_point_lms = vec3(0.701634, 1.15856, -0.904175)
+ (vec3(-0.051461, 0.045854, 0.953127)
+ vec3(0.452749, -0.296122, -0.955206) * white_point_xy.x)
/ white_point_xy.y;
// Now that we're in LMS space, perform the white point scaling.
let white_point_adjustment = Mat3::from_diagonal(D65_LMS / white_point_lms);
// Finally, combine the RGB → LMS → corrected LMS → corrected RGB
// pipeline into a single 3×3 matrix.
let balance = LMS_TO_RGB * white_point_adjustment * RGB_TO_LMS;
Self {
balance,
saturation: vec3(
component.shadows.saturation,
component.midtones.saturation,
component.highlights.saturation,
),
contrast: vec3(
component.shadows.contrast,
component.midtones.contrast,
component.highlights.contrast,
),
gamma: vec3(
component.shadows.gamma,
component.midtones.gamma,
component.highlights.gamma,
),
gain: vec3(
component.shadows.gain,
component.midtones.gain,
component.highlights.gain,
),
lift: vec3(
component.shadows.lift,
component.midtones.lift,
component.highlights.lift,
),
midtone_range: vec2(
component.global.midtones_range.start,
component.global.midtones_range.end,
),
exposure: component.global.exposure,
hue: component.global.hue,
post_saturation: component.global.post_saturation,
}
}
}
#[derive(Component)]
pub struct GpuCulling;
@ -445,7 +753,7 @@ pub fn prepare_view_uniforms(
.unwrap_or_else(|| Exposure::default().exposure()),
viewport,
frustum,
color_grading: extracted_view.color_grading,
color_grading: extracted_view.color_grading.clone().into(),
mip_bias: mip_bias.unwrap_or(&MipBias(0.0)).0,
render_layers: maybe_layers.copied().unwrap_or_default().bits(),
}),

View file

@ -1,9 +1,15 @@
#define_import_path bevy_render::view
struct ColorGrading {
balance: mat3x3<f32>,
saturation: vec3<f32>,
contrast: vec3<f32>,
gamma: vec3<f32>,
gain: vec3<f32>,
lift: vec3<f32>,
midtone_range: vec2<f32>,
exposure: f32,
gamma: f32,
pre_saturation: f32,
hue: f32,
post_saturation: f32,
}

View file

@ -0,0 +1,680 @@
//! Demonstrates color grading with an interactive adjustment UI.
use std::{
f32::consts::PI,
fmt::{self, Formatter},
};
use bevy::{
ecs::system::EntityCommands,
pbr::CascadeShadowConfigBuilder,
prelude::*,
render::view::{ColorGrading, ColorGradingGlobal, ColorGradingSection},
};
use std::fmt::Display;
static FONT_PATH: &str = "fonts/FiraMono-Medium.ttf";
/// How quickly the value changes per frame.
const OPTION_ADJUSTMENT_SPEED: f32 = 0.003;
/// The color grading section that the user has selected: highlights, midtones,
/// or shadows.
#[derive(Clone, Copy, PartialEq)]
enum SelectedColorGradingSection {
Highlights,
Midtones,
Shadows,
}
/// The global option that the user has selected.
///
/// See the documentation of [`ColorGradingGlobal`] for more information about
/// each field here.
#[derive(Clone, Copy, PartialEq, Default)]
enum SelectedGlobalColorGradingOption {
#[default]
Exposure,
Temperature,
Tint,
Hue,
}
/// The section-specific option that the user has selected.
///
/// See the documentation of [`ColorGradingSection`] for more information about
/// each field here.
#[derive(Clone, Copy, PartialEq)]
enum SelectedSectionColorGradingOption {
Saturation,
Contrast,
Gamma,
Gain,
Lift,
}
/// The color grading option that the user has selected.
#[derive(Clone, Copy, PartialEq, Resource)]
enum SelectedColorGradingOption {
/// The user has selected a global color grading option: one that applies to
/// the whole image as opposed to specifically to highlights, midtones, or
/// shadows.
Global(SelectedGlobalColorGradingOption),
/// The user has selected a color grading option that applies only to
/// highlights, midtones, or shadows.
Section(
SelectedColorGradingSection,
SelectedSectionColorGradingOption,
),
}
impl Default for SelectedColorGradingOption {
fn default() -> Self {
Self::Global(default())
}
}
/// Buttons consist of three parts: the button itself, a label child, and a
/// value child. This specifies one of the three entities.
#[derive(Clone, Copy, PartialEq, Component)]
enum ColorGradingOptionWidgetType {
/// The parent button.
Button,
/// The label of the button.
Label,
/// The numerical value that the button displays.
Value,
}
#[derive(Clone, Copy, Component)]
struct ColorGradingOptionWidget {
widget_type: ColorGradingOptionWidgetType,
option: SelectedColorGradingOption,
}
/// A marker component for the help text at the top left of the screen.
#[derive(Clone, Copy, Component)]
struct HelpText;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.init_resource::<SelectedColorGradingOption>()
.add_systems(Startup, setup)
.add_systems(
Update,
(
handle_button_presses,
adjust_color_grading_option,
update_ui_state,
)
.chain(),
)
.run();
}
fn setup(
mut commands: Commands,
currently_selected_option: Res<SelectedColorGradingOption>,
asset_server: Res<AssetServer>,
) {
// Create the scene.
add_basic_scene(&mut commands, &asset_server);
// Create the root UI element.
let font = asset_server.load(FONT_PATH);
let color_grading = ColorGrading::default();
add_buttons(
&mut commands,
&currently_selected_option,
&font,
&color_grading,
);
// Spawn help text.
add_help_text(&mut commands, &font, &currently_selected_option);
// Spawn the camera.
add_camera(&mut commands, &asset_server, color_grading);
}
/// Adds all the buttons on the bottom of the scene.
fn add_buttons(
commands: &mut Commands,
currently_selected_option: &SelectedColorGradingOption,
font: &Handle<Font>,
color_grading: &ColorGrading,
) {
// Spawn the parent node that contains all the buttons.
commands
.spawn(NodeBundle {
style: Style {
flex_direction: FlexDirection::Column,
position_type: PositionType::Absolute,
row_gap: Val::Px(6.0),
left: Val::Px(10.0),
bottom: Val::Px(10.0),
..default()
},
..default()
})
.with_children(|parent| {
// Create the first row, which contains the global controls.
add_buttons_for_global_controls(
parent,
*currently_selected_option,
color_grading,
font,
);
// Create the rows for individual controls.
for section in [
SelectedColorGradingSection::Highlights,
SelectedColorGradingSection::Midtones,
SelectedColorGradingSection::Shadows,
] {
add_buttons_for_section(
parent,
section,
*currently_selected_option,
color_grading,
font,
);
}
});
}
/// Adds the buttons for the global controls (those that control the scene as a
/// whole as opposed to shadows, midtones, or highlights).
fn add_buttons_for_global_controls(
parent: &mut ChildBuilder,
currently_selected_option: SelectedColorGradingOption,
color_grading: &ColorGrading,
font: &Handle<Font>,
) {
// Add the parent node for the row.
parent
.spawn(NodeBundle {
style: Style::default(),
..default()
})
.with_children(|parent| {
// Add some placeholder text to fill this column.
parent.spawn(NodeBundle {
style: Style {
width: Val::Px(125.0),
..default()
},
..default()
});
// Add each global color grading option button.
for option in [
SelectedGlobalColorGradingOption::Exposure,
SelectedGlobalColorGradingOption::Temperature,
SelectedGlobalColorGradingOption::Tint,
SelectedGlobalColorGradingOption::Hue,
] {
add_button_for_value(
parent,
SelectedColorGradingOption::Global(option),
currently_selected_option,
color_grading,
font,
);
}
});
}
/// Adds the buttons that control color grading for individual sections
/// (highlights, midtones, shadows).
fn add_buttons_for_section(
parent: &mut ChildBuilder,
section: SelectedColorGradingSection,
currently_selected_option: SelectedColorGradingOption,
color_grading: &ColorGrading,
font: &Handle<Font>,
) {
// Spawn the row container.
parent
.spawn(NodeBundle {
style: Style {
align_items: AlignItems::Center,
..default()
},
..default()
})
.with_children(|parent| {
// Spawn the label ("Highlights", etc.)
add_text(parent, &section.to_string(), font, Color::WHITE).insert(Style {
width: Val::Px(125.0),
..default()
});
// Spawn the buttons.
for option in [
SelectedSectionColorGradingOption::Saturation,
SelectedSectionColorGradingOption::Contrast,
SelectedSectionColorGradingOption::Gamma,
SelectedSectionColorGradingOption::Gain,
SelectedSectionColorGradingOption::Lift,
] {
add_button_for_value(
parent,
SelectedColorGradingOption::Section(section, option),
currently_selected_option,
color_grading,
font,
);
}
});
}
/// Adds a button that controls one of the color grading values.
fn add_button_for_value(
parent: &mut ChildBuilder,
option: SelectedColorGradingOption,
currently_selected_option: SelectedColorGradingOption,
color_grading: &ColorGrading,
font: &Handle<Font>,
) {
let is_selected = currently_selected_option == option;
let (bg_color, fg_color) = if is_selected {
(Color::WHITE, Color::BLACK)
} else {
(Color::BLACK, Color::WHITE)
};
// Add the button node.
parent
.spawn(ButtonBundle {
style: Style {
border: UiRect::all(Val::Px(1.0)),
width: Val::Px(200.0),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
padding: UiRect::axes(Val::Px(12.0), Val::Px(6.0)),
margin: UiRect::right(Val::Px(12.0)),
..default()
},
border_color: BorderColor(Color::WHITE),
border_radius: BorderRadius::MAX,
image: UiImage::default().with_color(bg_color),
..default()
})
.insert(ColorGradingOptionWidget {
widget_type: ColorGradingOptionWidgetType::Button,
option,
})
.with_children(|parent| {
// Add the button label.
let label = match option {
SelectedColorGradingOption::Global(option) => option.to_string(),
SelectedColorGradingOption::Section(_, option) => option.to_string(),
};
add_text(parent, &label, font, fg_color).insert(ColorGradingOptionWidget {
widget_type: ColorGradingOptionWidgetType::Label,
option,
});
// Add a spacer.
parent.spawn(NodeBundle {
style: Style {
flex_grow: 1.0,
..default()
},
..default()
});
// Add the value text.
add_text(
parent,
&format!("{:.3}", option.get(color_grading)),
font,
fg_color,
)
.insert(ColorGradingOptionWidget {
widget_type: ColorGradingOptionWidgetType::Value,
option,
});
});
}
/// Creates the help text at the top of the screen.
fn add_help_text(
commands: &mut Commands,
font: &Handle<Font>,
currently_selected_option: &SelectedColorGradingOption,
) {
commands
.spawn(TextBundle {
style: Style {
position_type: PositionType::Absolute,
left: Val::Px(10.0),
top: Val::Px(10.0),
..default()
},
..TextBundle::from_section(
create_help_text(currently_selected_option),
TextStyle {
font: font.clone(),
font_size: 24.0,
color: Color::WHITE,
},
)
})
.insert(HelpText);
}
/// Adds some text to the scene.
fn add_text<'a>(
parent: &'a mut ChildBuilder,
label: &str,
font: &Handle<Font>,
color: Color,
) -> EntityCommands<'a> {
parent.spawn(TextBundle::from_section(
label,
TextStyle {
font: font.clone(),
font_size: 18.0,
color,
},
))
}
fn add_camera(commands: &mut Commands, asset_server: &AssetServer, color_grading: ColorGrading) {
commands.spawn((
Camera3dBundle {
camera: Camera {
hdr: true,
..default()
},
transform: Transform::from_xyz(0.7, 0.7, 1.0)
.looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y),
color_grading,
..default()
},
FogSettings {
color: Color::srgb_u8(43, 44, 47),
falloff: FogFalloff::Linear {
start: 1.0,
end: 8.0,
},
..default()
},
EnvironmentMapLight {
diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"),
specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"),
intensity: 2000.0,
},
));
}
fn add_basic_scene(commands: &mut Commands, asset_server: &AssetServer) {
// Spawn the main scene.
commands.spawn(SceneBundle {
scene: asset_server.load("models/TonemappingTest/TonemappingTest.gltf#Scene0"),
..default()
});
// Spawn the flight helmet.
commands.spawn(SceneBundle {
scene: asset_server.load("models/FlightHelmet/FlightHelmet.gltf#Scene0"),
transform: Transform::from_xyz(0.5, 0.0, -0.5)
.with_rotation(Quat::from_rotation_y(-0.15 * PI)),
..default()
});
// Spawn the light.
commands.spawn(DirectionalLightBundle {
directional_light: DirectionalLight {
illuminance: 15000.0,
shadows_enabled: true,
..default()
},
transform: Transform::from_rotation(Quat::from_euler(
EulerRot::ZYX,
0.0,
PI * -0.15,
PI * -0.15,
)),
cascade_shadow_config: CascadeShadowConfigBuilder {
maximum_distance: 3.0,
first_cascade_far_bound: 0.9,
..default()
}
.into(),
..default()
});
}
impl Display for SelectedGlobalColorGradingOption {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let name = match *self {
SelectedGlobalColorGradingOption::Exposure => "Exposure",
SelectedGlobalColorGradingOption::Temperature => "Temperature",
SelectedGlobalColorGradingOption::Tint => "Tint",
SelectedGlobalColorGradingOption::Hue => "Hue",
};
f.write_str(name)
}
}
impl Display for SelectedColorGradingSection {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let name = match *self {
SelectedColorGradingSection::Highlights => "Highlights",
SelectedColorGradingSection::Midtones => "Midtones",
SelectedColorGradingSection::Shadows => "Shadows",
};
f.write_str(name)
}
}
impl Display for SelectedSectionColorGradingOption {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let name = match *self {
SelectedSectionColorGradingOption::Saturation => "Saturation",
SelectedSectionColorGradingOption::Contrast => "Contrast",
SelectedSectionColorGradingOption::Gamma => "Gamma",
SelectedSectionColorGradingOption::Gain => "Gain",
SelectedSectionColorGradingOption::Lift => "Lift",
};
f.write_str(name)
}
}
impl Display for SelectedColorGradingOption {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
SelectedColorGradingOption::Global(option) => write!(f, "\"{}\"", option),
SelectedColorGradingOption::Section(section, option) => {
write!(f, "\"{}\" for \"{}\"", option, section)
}
}
}
}
impl SelectedSectionColorGradingOption {
/// Returns the appropriate value in the given color grading section.
fn get(&self, section: &ColorGradingSection) -> f32 {
match *self {
SelectedSectionColorGradingOption::Saturation => section.saturation,
SelectedSectionColorGradingOption::Contrast => section.contrast,
SelectedSectionColorGradingOption::Gamma => section.gamma,
SelectedSectionColorGradingOption::Gain => section.gain,
SelectedSectionColorGradingOption::Lift => section.lift,
}
}
fn set(&self, section: &mut ColorGradingSection, value: f32) {
match *self {
SelectedSectionColorGradingOption::Saturation => section.saturation = value,
SelectedSectionColorGradingOption::Contrast => section.contrast = value,
SelectedSectionColorGradingOption::Gamma => section.gamma = value,
SelectedSectionColorGradingOption::Gain => section.gain = value,
SelectedSectionColorGradingOption::Lift => section.lift = value,
}
}
}
impl SelectedGlobalColorGradingOption {
/// Returns the appropriate value in the given set of global color grading
/// values.
fn get(&self, global: &ColorGradingGlobal) -> f32 {
match *self {
SelectedGlobalColorGradingOption::Exposure => global.exposure,
SelectedGlobalColorGradingOption::Temperature => global.temperature,
SelectedGlobalColorGradingOption::Tint => global.tint,
SelectedGlobalColorGradingOption::Hue => global.hue,
}
}
/// Sets the appropriate value in the given set of global color grading
/// values.
fn set(&self, global: &mut ColorGradingGlobal, value: f32) {
match *self {
SelectedGlobalColorGradingOption::Exposure => global.exposure = value,
SelectedGlobalColorGradingOption::Temperature => global.temperature = value,
SelectedGlobalColorGradingOption::Tint => global.tint = value,
SelectedGlobalColorGradingOption::Hue => global.hue = value,
}
}
}
impl SelectedColorGradingOption {
/// Returns the appropriate value in the given set of color grading values.
fn get(&self, color_grading: &ColorGrading) -> f32 {
match self {
SelectedColorGradingOption::Global(option) => option.get(&color_grading.global),
SelectedColorGradingOption::Section(
SelectedColorGradingSection::Highlights,
option,
) => option.get(&color_grading.highlights),
SelectedColorGradingOption::Section(SelectedColorGradingSection::Midtones, option) => {
option.get(&color_grading.midtones)
}
SelectedColorGradingOption::Section(SelectedColorGradingSection::Shadows, option) => {
option.get(&color_grading.shadows)
}
}
}
/// Sets the appropriate value in the given set of color grading values.
fn set(&self, color_grading: &mut ColorGrading, value: f32) {
match self {
SelectedColorGradingOption::Global(option) => {
option.set(&mut color_grading.global, value);
}
SelectedColorGradingOption::Section(
SelectedColorGradingSection::Highlights,
option,
) => option.set(&mut color_grading.highlights, value),
SelectedColorGradingOption::Section(SelectedColorGradingSection::Midtones, option) => {
option.set(&mut color_grading.midtones, value);
}
SelectedColorGradingOption::Section(SelectedColorGradingSection::Shadows, option) => {
option.set(&mut color_grading.shadows, value);
}
}
}
}
/// Handles mouse clicks on the buttons when the user clicks on a new one.
fn handle_button_presses(
mut interactions: Query<(&Interaction, &ColorGradingOptionWidget), Changed<Interaction>>,
mut currently_selected_option: ResMut<SelectedColorGradingOption>,
) {
for (interaction, widget) in interactions.iter_mut() {
if widget.widget_type == ColorGradingOptionWidgetType::Button
&& *interaction == Interaction::Pressed
{
*currently_selected_option = widget.option;
}
}
}
/// Updates the state of the UI based on the current state.
fn update_ui_state(
mut buttons: Query<(&mut UiImage, &ColorGradingOptionWidget)>,
mut button_text: Query<(&mut Text, &ColorGradingOptionWidget), Without<HelpText>>,
mut help_text: Query<&mut Text, With<HelpText>>,
cameras: Query<&ColorGrading>,
currently_selected_option: Res<SelectedColorGradingOption>,
) {
// The currently-selected option is drawn with inverted colors.
for (mut image, widget) in buttons.iter_mut() {
image.color = if *currently_selected_option == widget.option {
Color::WHITE
} else {
Color::BLACK
};
}
let value_label = cameras
.iter()
.next()
.map(|color_grading| format!("{:.3}", currently_selected_option.get(color_grading)));
// Update the buttons.
for (mut text, widget) in button_text.iter_mut() {
// Set the text color.
let color = if *currently_selected_option == widget.option {
Color::BLACK
} else {
Color::WHITE
};
for section in &mut text.sections {
section.style.color = color;
}
// Update the displayed value, if this is the currently-selected option.
if widget.widget_type == ColorGradingOptionWidgetType::Value
&& *currently_selected_option == widget.option
{
if let Some(ref value_label) = value_label {
for section in &mut text.sections {
section.value = value_label.clone();
}
}
}
}
// Update the help text.
for mut help_text in help_text.iter_mut() {
for section in &mut help_text.sections {
section.value = create_help_text(&currently_selected_option);
}
}
}
/// Creates the help text at the top left of the window.
fn create_help_text(currently_selected_option: &SelectedColorGradingOption) -> String {
format!("Press Left/Right to adjust {}", currently_selected_option)
}
/// Processes keyboard input to change the value of the currently-selected color
/// grading option.
fn adjust_color_grading_option(
mut cameras: Query<&mut ColorGrading>,
input: Res<ButtonInput<KeyCode>>,
currently_selected_option: Res<SelectedColorGradingOption>,
) {
let mut delta = 0.0;
if input.pressed(KeyCode::ArrowLeft) {
delta -= OPTION_ADJUSTMENT_SPEED;
}
if input.pressed(KeyCode::ArrowRight) {
delta += OPTION_ADJUSTMENT_SPEED;
}
for mut color_grading in cameras.iter_mut() {
let new_value = currently_selected_option.get(&color_grading) + delta;
currently_selected_option.set(&mut color_grading, new_value);
}
}

View file

@ -6,10 +6,8 @@ use bevy::{
prelude::*,
reflect::TypePath,
render::{
render_asset::RenderAssetUsages,
render_resource::{AsBindGroup, Extent3d, ShaderRef, TextureDimension, TextureFormat},
texture::{ImageSampler, ImageSamplerDescriptor},
view::ColorGrading,
render_resource::{AsBindGroup, ShaderRef},
view::{ColorGrading, ColorGradingGlobal, ColorGradingSection},
},
utils::HashMap,
};
@ -98,83 +96,14 @@ fn setup(
);
}
fn setup_basic_scene(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut images: ResMut<Assets<Image>>,
asset_server: Res<AssetServer>,
) {
// plane
commands.spawn((
PbrBundle {
mesh: meshes.add(Plane3d::default().mesh().size(50.0, 50.0)),
material: materials.add(Color::srgb(0.1, 0.2, 0.1)),
fn setup_basic_scene(mut commands: Commands, asset_server: Res<AssetServer>) {
// Main scene
commands
.spawn(SceneBundle {
scene: asset_server.load("models/TonemappingTest/TonemappingTest.gltf#Scene0"),
..default()
},
SceneNumber(1),
));
// cubes
let cube_material = materials.add(StandardMaterial {
base_color_texture: Some(images.add(uv_debug_texture())),
..default()
});
let cube_mesh = meshes.add(Cuboid::new(0.25, 0.25, 0.25));
for i in 0..5 {
commands.spawn((
PbrBundle {
mesh: cube_mesh.clone(),
material: cube_material.clone(),
transform: Transform::from_xyz(i as f32 * 0.25 - 1.0, 0.125, -i as f32 * 0.5),
..default()
},
SceneNumber(1),
));
}
// spheres
let sphere_mesh = meshes.add(Sphere::new(0.125).mesh().uv(32, 18));
for i in 0..6 {
let j = i % 3;
let s_val = if i < 3 { 0.0 } else { 0.2 };
let material = if j == 0 {
materials.add(StandardMaterial {
base_color: Color::srgb(s_val, s_val, 1.0),
perceptual_roughness: 0.089,
metallic: 0.0,
..default()
})
} else if j == 1 {
materials.add(StandardMaterial {
base_color: Color::srgb(s_val, 1.0, s_val),
perceptual_roughness: 0.089,
metallic: 0.0,
..default()
})
} else {
materials.add(StandardMaterial {
base_color: Color::srgb(1.0, s_val, s_val),
perceptual_roughness: 0.089,
metallic: 0.0,
..default()
})
};
commands.spawn((
PbrBundle {
mesh: sphere_mesh.clone(),
material,
transform: Transform::from_xyz(
j as f32 * 0.25 + if i < 3 { -0.15 } else { 0.15 } - 0.4,
0.125,
-j as f32 * 0.25 + if i < 3 { -0.15 } else { 0.15 } + 0.4,
),
..default()
},
SceneNumber(1),
));
}
})
.insert(SceneNumber(1));
// Flight Helmet
commands.spawn((
@ -403,10 +332,12 @@ fn toggle_tonemapping_method(
*method = Tonemapping::BlenderFilmic;
}
*color_grading = *per_method_settings
*color_grading = (*per_method_settings
.settings
.get::<Tonemapping>(&method)
.unwrap();
.as_ref()
.unwrap())
.clone();
}
#[derive(Resource)]
@ -448,16 +379,20 @@ fn update_color_grading_settings(
if keys.pressed(KeyCode::ArrowLeft) || keys.pressed(KeyCode::ArrowRight) {
match selected_parameter.value {
0 => {
color_grading.exposure += dt;
color_grading.global.exposure += dt;
}
1 => {
color_grading.gamma += dt;
color_grading
.all_sections_mut()
.for_each(|section| section.gamma += dt);
}
2 => {
color_grading.pre_saturation += dt;
color_grading
.all_sections_mut()
.for_each(|section| section.saturation += dt);
}
3 => {
color_grading.post_saturation += dt;
color_grading.global.post_saturation += dt;
}
_ => {}
}
@ -577,24 +512,24 @@ fn update_ui(
if selected_parameter.value == 0 {
text.push_str("> ");
}
text.push_str(&format!("Exposure: {}\n", color_grading.exposure));
text.push_str(&format!("Exposure: {}\n", color_grading.global.exposure));
if selected_parameter.value == 1 {
text.push_str("> ");
}
text.push_str(&format!("Gamma: {}\n", color_grading.gamma));
text.push_str(&format!("Gamma: {}\n", color_grading.shadows.gamma));
if selected_parameter.value == 2 {
text.push_str("> ");
}
text.push_str(&format!(
"PreSaturation: {}\n",
color_grading.pre_saturation
color_grading.shadows.saturation
));
if selected_parameter.value == 3 {
text.push_str("> ");
}
text.push_str(&format!(
"PostSaturation: {}\n",
color_grading.post_saturation
color_grading.global.post_saturation
));
text.push_str("(Space) Reset all to default\n");
@ -614,19 +549,30 @@ impl PerMethodSettings {
fn basic_scene_recommendation(method: Tonemapping) -> ColorGrading {
match method {
Tonemapping::Reinhard | Tonemapping::ReinhardLuminance => ColorGrading {
exposure: 0.5,
global: ColorGradingGlobal {
exposure: 0.5,
..default()
},
..default()
},
Tonemapping::AcesFitted => ColorGrading {
exposure: 0.35,
global: ColorGradingGlobal {
exposure: 0.35,
..default()
},
..default()
},
Tonemapping::AgX => ColorGrading {
exposure: -0.2,
gamma: 1.0,
pre_saturation: 1.1,
post_saturation: 1.1,
},
Tonemapping::AgX => ColorGrading::with_identical_sections(
ColorGradingGlobal {
exposure: -0.2,
post_saturation: 1.1,
..default()
},
ColorGradingSection {
saturation: 1.1,
..default()
},
),
_ => ColorGrading::default(),
}
}
@ -656,37 +602,6 @@ impl Default for PerMethodSettings {
}
}
/// Creates a colorful test pattern
fn uv_debug_texture() -> Image {
const TEXTURE_SIZE: usize = 8;
let mut palette: [u8; 32] = [
255, 102, 159, 255, 255, 159, 102, 255, 236, 255, 102, 255, 121, 255, 102, 255, 102, 255,
198, 255, 102, 198, 255, 255, 121, 102, 255, 255, 236, 102, 255, 255,
];
let mut texture_data = [0; TEXTURE_SIZE * TEXTURE_SIZE * 4];
for y in 0..TEXTURE_SIZE {
let offset = TEXTURE_SIZE * y * 4;
texture_data[offset..(offset + TEXTURE_SIZE * 4)].copy_from_slice(&palette);
palette.rotate_right(4);
}
let mut img = Image::new_fill(
Extent3d {
width: TEXTURE_SIZE as u32,
height: TEXTURE_SIZE as u32,
depth_or_array_layers: 1,
},
TextureDimension::D2,
&texture_data,
TextureFormat::Rgba8UnormSrgb,
RenderAssetUsages::RENDER_WORLD,
);
img.sampler = ImageSampler::Descriptor(ImageSamplerDescriptor::default());
img
}
impl Material for ColorGradientMaterial {
fn fragment_shader() -> ShaderRef {
"shaders/tonemapping_test_patterns.wgsl".into()

View file

@ -30,7 +30,7 @@ use bevy::{
prelude::*,
render::{
camera::{Exposure, TemporalJitter},
view::ColorGrading,
view::{ColorGrading, ColorGradingGlobal},
},
};
@ -345,7 +345,10 @@ fn setup(
},
transform: Transform::from_xyz(1.0, 1.8, 7.0).looking_at(Vec3::ZERO, Vec3::Y),
color_grading: ColorGrading {
post_saturation: 1.2,
global: ColorGradingGlobal {
post_saturation: 1.2,
..default()
},
..default()
},
tonemapping: Tonemapping::TonyMcMapface,

View file

@ -129,6 +129,7 @@ Example | Description
[Anti-aliasing](../examples/3d/anti_aliasing.rs) | Compares different anti-aliasing methods
[Atmospheric Fog](../examples/3d/atmospheric_fog.rs) | A scene showcasing the atmospheric fog effect
[Blend Modes](../examples/3d/blend_modes.rs) | Showcases different blend modes
[Color grading](../examples/3d/color_grading.rs) | Demonstrates color grading
[Deferred Rendering](../examples/3d/deferred_rendering.rs) | Renders meshes with both forward and deferred pipelines
[Fog](../examples/3d/fog.rs) | A scene showcasing the distance fog effect
[Generate Custom Mesh](../examples/3d/generate_custom_mesh.rs) | Simple showcase of how to generate a custom mesh with a custom texture