mirror of
https://github.com/bevyengine/bevy
synced 2024-11-21 20:23:28 +00:00
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:
parent
abddbf2d95
commit
961b24deaf
16 changed files with 1969 additions and 184 deletions
11
Cargo.toml
11
Cargo.toml
|
@ -2910,6 +2910,17 @@ description = "Demonstrates FPS overlay"
|
||||||
category = "Dev tools"
|
category = "Dev tools"
|
||||||
wasm = true
|
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]
|
[profile.wasm-release]
|
||||||
inherits = "release"
|
inherits = "release"
|
||||||
opt-level = "z"
|
opt-level = "z"
|
||||||
|
|
BIN
assets/models/TonemappingTest/TestPattern.png
Normal file
BIN
assets/models/TonemappingTest/TestPattern.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 361 B |
BIN
assets/models/TonemappingTest/TonemappingTest.bin
Normal file
BIN
assets/models/TonemappingTest/TonemappingTest.bin
Normal file
Binary file not shown.
679
assets/models/TonemappingTest/TonemappingTest.gltf
Normal file
679
assets/models/TonemappingTest/TonemappingTest.gltf
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -11,11 +11,12 @@ use bevy_render::render_resource::binding_types::{
|
||||||
};
|
};
|
||||||
use bevy_render::renderer::RenderDevice;
|
use bevy_render::renderer::RenderDevice;
|
||||||
use bevy_render::texture::{CompressedImageFormats, GpuImage, Image, ImageSampler, ImageType};
|
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::{camera::Camera, texture::FallbackImage};
|
||||||
use bevy_render::{render_resource::*, Render, RenderApp, RenderSet};
|
use bevy_render::{render_resource::*, Render, RenderApp, RenderSet};
|
||||||
#[cfg(not(feature = "tonemapping_luts"))]
|
#[cfg(not(feature = "tonemapping_luts"))]
|
||||||
use bevy_utils::tracing::error;
|
use bevy_utils::tracing::error;
|
||||||
|
use bitflags::bitflags;
|
||||||
|
|
||||||
mod node;
|
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)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
pub struct TonemappingPipelineKey {
|
pub struct TonemappingPipelineKey {
|
||||||
deband_dither: DebandDither,
|
deband_dither: DebandDither,
|
||||||
tonemapping: Tonemapping,
|
tonemapping: Tonemapping,
|
||||||
|
flags: TonemappingPipelineKeyFlags,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SpecializedRenderPipeline for TonemappingPipeline {
|
impl SpecializedRenderPipeline for TonemappingPipeline {
|
||||||
|
@ -194,6 +212,23 @@ impl SpecializedRenderPipeline for TonemappingPipeline {
|
||||||
shader_defs.push("DEBAND_DITHER".into());
|
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 {
|
match key.tonemapping {
|
||||||
Tonemapping::None => shader_defs.push("TONEMAP_METHOD_NONE".into()),
|
Tonemapping::None => shader_defs.push("TONEMAP_METHOD_NONE".into()),
|
||||||
Tonemapping::Reinhard => shader_defs.push("TONEMAP_METHOD_REINHARD".into()),
|
Tonemapping::Reinhard => shader_defs.push("TONEMAP_METHOD_REINHARD".into()),
|
||||||
|
@ -292,12 +327,38 @@ pub fn prepare_view_tonemapping_pipelines(
|
||||||
pipeline_cache: Res<PipelineCache>,
|
pipeline_cache: Res<PipelineCache>,
|
||||||
mut pipelines: ResMut<SpecializedRenderPipelines<TonemappingPipeline>>,
|
mut pipelines: ResMut<SpecializedRenderPipelines<TonemappingPipeline>>,
|
||||||
upscaling_pipeline: Res<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 {
|
let key = TonemappingPipelineKey {
|
||||||
deband_dither: *dither.unwrap_or(&DebandDither::Disabled),
|
deband_dither: *dither.unwrap_or(&DebandDither::Disabled),
|
||||||
tonemapping: *tonemapping.unwrap_or(&Tonemapping::None),
|
tonemapping: *tonemapping.unwrap_or(&Tonemapping::None),
|
||||||
|
flags,
|
||||||
};
|
};
|
||||||
let pipeline = pipelines.specialize(&pipeline_cache, &upscaling_pipeline, key);
|
let pipeline = pipelines.specialize(&pipeline_cache, &upscaling_pipeline, key);
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
#define_import_path bevy_core_pipeline::tonemapping
|
#define_import_path bevy_core_pipeline::tonemapping
|
||||||
|
|
||||||
#import bevy_render::view::ColorGrading
|
#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
|
// hack !! not sure what to do with this
|
||||||
#ifdef TONEMAPPING_PASS
|
#ifdef TONEMAPPING_PASS
|
||||||
|
@ -11,6 +12,15 @@
|
||||||
@group(0) @binding(19) var dt_lut_sampler: sampler;
|
@group(0) @binding(19) var dt_lut_sampler: sampler;
|
||||||
#endif
|
#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> {
|
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
|
// 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
|
// 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;
|
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 = 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..
|
// Perform white balance correction. Conveniently, this is a linear
|
||||||
// let luma = powsafe(vec3(tonemapping_luminance(color)), 1.0);
|
// 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..
|
// Perform the "sectional" color grading: i.e. the color grading that
|
||||||
// color += color * luma.xxx * 1.0;
|
// applies individually to shadows, midtones, and highlights.
|
||||||
|
#ifdef SECTIONAL_COLOR_GRADING
|
||||||
// Linear pre tonemapping grading
|
color = sectional_color_grading(color, &color_grading);
|
||||||
color = saturation(color, color_grading.pre_saturation);
|
#endif
|
||||||
color = powsafe(color, color_grading.gamma);
|
|
||||||
color = color * powsafe(vec3(2.0), color_grading.exposure);
|
|
||||||
color = max(color, vec3(0.0));
|
|
||||||
|
|
||||||
// tone_mapping
|
// tone_mapping
|
||||||
#ifdef TONEMAP_METHOD_NONE
|
#ifdef TONEMAP_METHOD_NONE
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
#import bevy_pbr::{
|
#import bevy_pbr::{
|
||||||
mesh_view_bindings as bindings,
|
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
|
// NOTE: Keep in sync with bevy_pbr/src/light.rs
|
||||||
|
@ -78,7 +78,11 @@ fn cluster_debug_visualization(
|
||||||
if (z_slice & 1u) == 1u {
|
if (z_slice & 1u) == 1u {
|
||||||
z_slice = z_slice + bindings::lights.cluster_dimensions.z / 2u;
|
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>(
|
output_color = vec4<f32>(
|
||||||
(1.0 - cluster_overlay_alpha) * output_color.rgb + cluster_overlay_alpha * slice_color,
|
(1.0 - cluster_overlay_alpha) * output_color.rgb + cluster_overlay_alpha * slice_color,
|
||||||
output_color.a
|
output_color.a
|
||||||
|
@ -96,7 +100,7 @@ fn cluster_debug_visualization(
|
||||||
// NOTE: Visualizes the cluster to which the fragment belongs
|
// NOTE: Visualizes the cluster to which the fragment belongs
|
||||||
let cluster_overlay_alpha = 0.1;
|
let cluster_overlay_alpha = 0.1;
|
||||||
var rng = cluster_index;
|
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>(
|
output_color = vec4<f32>(
|
||||||
(1.0 - cluster_overlay_alpha) * output_color.rgb + cluster_overlay_alpha * cluster_color,
|
(1.0 - cluster_overlay_alpha) * output_color.rgb + cluster_overlay_alpha * cluster_color,
|
||||||
output_color.a
|
output_color.a
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
#import bevy_pbr::{
|
#import bevy_pbr::{
|
||||||
mesh_view_types::POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE,
|
mesh_view_types::POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE,
|
||||||
mesh_view_bindings as view_bindings,
|
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}
|
shadow_sampling::{SPOT_SHADOW_TEXEL_SIZE, sample_shadow_cubemap, sample_shadow_map}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -170,7 +170,11 @@ fn cascade_debug_visualization(
|
||||||
) -> vec3<f32> {
|
) -> vec3<f32> {
|
||||||
let overlay_alpha = 0.95;
|
let overlay_alpha = 0.95;
|
||||||
let cascade_index = get_cascade_index(light_id, view_z);
|
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>(
|
return vec3<f32>(
|
||||||
(1.0 - overlay_alpha) * output_color.rgb + overlay_alpha * cascade_color
|
(1.0 - overlay_alpha) * output_color.rgb + overlay_alpha * cascade_color
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,20 +2,53 @@
|
||||||
|
|
||||||
#import bevy_pbr::rgb9e5
|
#import bevy_pbr::rgb9e5
|
||||||
|
|
||||||
const PI: f32 = 3.141592653589793;
|
const PI: f32 = 3.141592653589793; // π
|
||||||
const HALF_PI: f32 = 1.57079632679;
|
const PI_2: f32 = 6.283185307179586; // 2π
|
||||||
const E: f32 = 2.718281828459045;
|
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> {
|
// Converts HSV to RGB.
|
||||||
let rgb = clamp(
|
//
|
||||||
abs(
|
// Input: H ∈ [0, 2π), S ∈ [0, 1], V ∈ [0, 1].
|
||||||
((hue * 6.0 + vec3<f32>(0.0, 4.0, 2.0)) % 6.0) - 3.0
|
// Output: R ∈ [0, 1], G ∈ [0, 1], B ∈ [0, 1].
|
||||||
) - 1.0,
|
//
|
||||||
vec3<f32>(0.0),
|
// <https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB_alternative>
|
||||||
vec3<f32>(1.0)
|
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].
|
// Generates a random u32 in range [0, u32::MAX].
|
||||||
|
|
|
@ -853,7 +853,7 @@ pub fn extract_cameras(
|
||||||
gpu_culling,
|
gpu_culling,
|
||||||
) in query.iter()
|
) 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 {
|
if !camera.is_active {
|
||||||
continue;
|
continue;
|
||||||
|
|
|
@ -24,13 +24,16 @@ use crate::{
|
||||||
};
|
};
|
||||||
use bevy_app::{App, Plugin};
|
use bevy_app::{App, Plugin};
|
||||||
use bevy_ecs::prelude::*;
|
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_reflect::{std_traits::ReflectDefault, Reflect};
|
||||||
use bevy_transform::components::GlobalTransform;
|
use bevy_transform::components::GlobalTransform;
|
||||||
use bevy_utils::HashMap;
|
use bevy_utils::HashMap;
|
||||||
use std::sync::{
|
use std::{
|
||||||
atomic::{AtomicUsize, Ordering},
|
ops::Range,
|
||||||
Arc,
|
sync::{
|
||||||
|
atomic::{AtomicUsize, Ordering},
|
||||||
|
Arc,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use wgpu::{
|
use wgpu::{
|
||||||
Extent3d, RenderPassColorAttachment, RenderPassDepthStencilAttachment, StoreOp,
|
Extent3d, RenderPassColorAttachment, RenderPassDepthStencilAttachment, StoreOp,
|
||||||
|
@ -39,6 +42,55 @@ use wgpu::{
|
||||||
|
|
||||||
pub const VIEW_TYPE_HANDLE: Handle<Shader> = Handle::weak_from_u128(15421373904451797197);
|
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;
|
pub struct ViewPlugin;
|
||||||
|
|
||||||
impl Plugin for 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.
|
/// Configures filmic color grading parameters to adjust the image appearance.
|
||||||
#[derive(Component, Reflect, Debug, Copy, Clone, ShaderType)]
|
///
|
||||||
|
/// 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)]
|
#[reflect(Component, Default)]
|
||||||
pub struct ColorGrading {
|
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.
|
/// Exposure value (EV) offset, measured in stops.
|
||||||
pub exposure: f32,
|
pub exposure: f32,
|
||||||
|
|
||||||
/// Non-linear luminance adjustment applied before tonemapping. y = pow(x, gamma)
|
/// An adjustment made to the [CIE 1931] chromaticity *x* value.
|
||||||
pub gamma: f32,
|
///
|
||||||
|
/// 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.
|
/// An adjustment made to the [CIE 1931] chromaticity *y* value.
|
||||||
/// Values below 1.0 desaturate, with a value of 0.0 resulting in a grayscale image
|
///
|
||||||
/// with luminance defined by ITU-R BT.709.
|
/// Positive values make the colors more magenta. Negative values make the
|
||||||
/// Values above 1.0 increase saturation.
|
/// colors greener. This has no effect on luminance (brightness).
|
||||||
pub pre_saturation: f32,
|
///
|
||||||
|
/// [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.
|
/// Saturation adjustment applied after tonemapping.
|
||||||
/// Values below 1.0 desaturate, with a value of 0.0 resulting in a grayscale image
|
/// Values below 1.0 desaturate, with a value of 0.0 resulting in a grayscale image
|
||||||
/// with luminance defined by ITU-R BT.709
|
/// with luminance defined by ITU-R BT.709
|
||||||
/// Values above 1.0 increase saturation.
|
/// Values above 1.0 increase saturation.
|
||||||
pub post_saturation: f32,
|
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 {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
exposure: 0.0,
|
exposure: 0.0,
|
||||||
gamma: 1.0,
|
temperature: 0.0,
|
||||||
pre_saturation: 1.0,
|
tint: 0.0,
|
||||||
|
hue: 0.0,
|
||||||
post_saturation: 1.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)]
|
#[derive(Clone, ShaderType)]
|
||||||
pub struct ViewUniform {
|
pub struct ViewUniform {
|
||||||
view_proj: Mat4,
|
view_proj: Mat4,
|
||||||
|
@ -177,7 +406,7 @@ pub struct ViewUniform {
|
||||||
// viewport(x_origin, y_origin, width, height)
|
// viewport(x_origin, y_origin, width, height)
|
||||||
viewport: Vec4,
|
viewport: Vec4,
|
||||||
frustum: [Vec4; 6],
|
frustum: [Vec4; 6],
|
||||||
color_grading: ColorGrading,
|
color_grading: ColorGradingUniform,
|
||||||
mip_bias: f32,
|
mip_bias: f32,
|
||||||
render_layers: u32,
|
render_layers: u32,
|
||||||
}
|
}
|
||||||
|
@ -208,6 +437,85 @@ pub struct PostProcessWrite<'a> {
|
||||||
pub destination: &'a TextureView,
|
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)]
|
#[derive(Component)]
|
||||||
pub struct GpuCulling;
|
pub struct GpuCulling;
|
||||||
|
|
||||||
|
@ -445,7 +753,7 @@ pub fn prepare_view_uniforms(
|
||||||
.unwrap_or_else(|| Exposure::default().exposure()),
|
.unwrap_or_else(|| Exposure::default().exposure()),
|
||||||
viewport,
|
viewport,
|
||||||
frustum,
|
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,
|
mip_bias: mip_bias.unwrap_or(&MipBias(0.0)).0,
|
||||||
render_layers: maybe_layers.copied().unwrap_or_default().bits(),
|
render_layers: maybe_layers.copied().unwrap_or_default().bits(),
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
#define_import_path bevy_render::view
|
#define_import_path bevy_render::view
|
||||||
|
|
||||||
struct ColorGrading {
|
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,
|
exposure: f32,
|
||||||
gamma: f32,
|
hue: f32,
|
||||||
pre_saturation: f32,
|
|
||||||
post_saturation: f32,
|
post_saturation: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
680
examples/3d/color_grading.rs
Normal file
680
examples/3d/color_grading.rs
Normal 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,
|
||||||
|
¤tly_selected_option,
|
||||||
|
&font,
|
||||||
|
&color_grading,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Spawn help text.
|
||||||
|
add_help_text(&mut commands, &font, ¤tly_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, §ion.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(¤tly_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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,10 +6,8 @@ use bevy::{
|
||||||
prelude::*,
|
prelude::*,
|
||||||
reflect::TypePath,
|
reflect::TypePath,
|
||||||
render::{
|
render::{
|
||||||
render_asset::RenderAssetUsages,
|
render_resource::{AsBindGroup, ShaderRef},
|
||||||
render_resource::{AsBindGroup, Extent3d, ShaderRef, TextureDimension, TextureFormat},
|
view::{ColorGrading, ColorGradingGlobal, ColorGradingSection},
|
||||||
texture::{ImageSampler, ImageSamplerDescriptor},
|
|
||||||
view::ColorGrading,
|
|
||||||
},
|
},
|
||||||
utils::HashMap,
|
utils::HashMap,
|
||||||
};
|
};
|
||||||
|
@ -98,83 +96,14 @@ fn setup(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_basic_scene(
|
fn setup_basic_scene(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||||||
mut commands: Commands,
|
// Main scene
|
||||||
mut meshes: ResMut<Assets<Mesh>>,
|
commands
|
||||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
.spawn(SceneBundle {
|
||||||
mut images: ResMut<Assets<Image>>,
|
scene: asset_server.load("models/TonemappingTest/TonemappingTest.gltf#Scene0"),
|
||||||
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)),
|
|
||||||
..default()
|
..default()
|
||||||
},
|
})
|
||||||
SceneNumber(1),
|
.insert(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),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flight Helmet
|
// Flight Helmet
|
||||||
commands.spawn((
|
commands.spawn((
|
||||||
|
@ -403,10 +332,12 @@ fn toggle_tonemapping_method(
|
||||||
*method = Tonemapping::BlenderFilmic;
|
*method = Tonemapping::BlenderFilmic;
|
||||||
}
|
}
|
||||||
|
|
||||||
*color_grading = *per_method_settings
|
*color_grading = (*per_method_settings
|
||||||
.settings
|
.settings
|
||||||
.get::<Tonemapping>(&method)
|
.get::<Tonemapping>(&method)
|
||||||
.unwrap();
|
.as_ref()
|
||||||
|
.unwrap())
|
||||||
|
.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
|
@ -448,16 +379,20 @@ fn update_color_grading_settings(
|
||||||
if keys.pressed(KeyCode::ArrowLeft) || keys.pressed(KeyCode::ArrowRight) {
|
if keys.pressed(KeyCode::ArrowLeft) || keys.pressed(KeyCode::ArrowRight) {
|
||||||
match selected_parameter.value {
|
match selected_parameter.value {
|
||||||
0 => {
|
0 => {
|
||||||
color_grading.exposure += dt;
|
color_grading.global.exposure += dt;
|
||||||
}
|
}
|
||||||
1 => {
|
1 => {
|
||||||
color_grading.gamma += dt;
|
color_grading
|
||||||
|
.all_sections_mut()
|
||||||
|
.for_each(|section| section.gamma += dt);
|
||||||
}
|
}
|
||||||
2 => {
|
2 => {
|
||||||
color_grading.pre_saturation += dt;
|
color_grading
|
||||||
|
.all_sections_mut()
|
||||||
|
.for_each(|section| section.saturation += dt);
|
||||||
}
|
}
|
||||||
3 => {
|
3 => {
|
||||||
color_grading.post_saturation += dt;
|
color_grading.global.post_saturation += dt;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
@ -577,24 +512,24 @@ fn update_ui(
|
||||||
if selected_parameter.value == 0 {
|
if selected_parameter.value == 0 {
|
||||||
text.push_str("> ");
|
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 {
|
if selected_parameter.value == 1 {
|
||||||
text.push_str("> ");
|
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 {
|
if selected_parameter.value == 2 {
|
||||||
text.push_str("> ");
|
text.push_str("> ");
|
||||||
}
|
}
|
||||||
text.push_str(&format!(
|
text.push_str(&format!(
|
||||||
"PreSaturation: {}\n",
|
"PreSaturation: {}\n",
|
||||||
color_grading.pre_saturation
|
color_grading.shadows.saturation
|
||||||
));
|
));
|
||||||
if selected_parameter.value == 3 {
|
if selected_parameter.value == 3 {
|
||||||
text.push_str("> ");
|
text.push_str("> ");
|
||||||
}
|
}
|
||||||
text.push_str(&format!(
|
text.push_str(&format!(
|
||||||
"PostSaturation: {}\n",
|
"PostSaturation: {}\n",
|
||||||
color_grading.post_saturation
|
color_grading.global.post_saturation
|
||||||
));
|
));
|
||||||
text.push_str("(Space) Reset all to default\n");
|
text.push_str("(Space) Reset all to default\n");
|
||||||
|
|
||||||
|
@ -614,19 +549,30 @@ impl PerMethodSettings {
|
||||||
fn basic_scene_recommendation(method: Tonemapping) -> ColorGrading {
|
fn basic_scene_recommendation(method: Tonemapping) -> ColorGrading {
|
||||||
match method {
|
match method {
|
||||||
Tonemapping::Reinhard | Tonemapping::ReinhardLuminance => ColorGrading {
|
Tonemapping::Reinhard | Tonemapping::ReinhardLuminance => ColorGrading {
|
||||||
exposure: 0.5,
|
global: ColorGradingGlobal {
|
||||||
|
exposure: 0.5,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
Tonemapping::AcesFitted => ColorGrading {
|
Tonemapping::AcesFitted => ColorGrading {
|
||||||
exposure: 0.35,
|
global: ColorGradingGlobal {
|
||||||
|
exposure: 0.35,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
Tonemapping::AgX => ColorGrading {
|
Tonemapping::AgX => ColorGrading::with_identical_sections(
|
||||||
exposure: -0.2,
|
ColorGradingGlobal {
|
||||||
gamma: 1.0,
|
exposure: -0.2,
|
||||||
pre_saturation: 1.1,
|
post_saturation: 1.1,
|
||||||
post_saturation: 1.1,
|
..default()
|
||||||
},
|
},
|
||||||
|
ColorGradingSection {
|
||||||
|
saturation: 1.1,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
),
|
||||||
_ => ColorGrading::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 {
|
impl Material for ColorGradientMaterial {
|
||||||
fn fragment_shader() -> ShaderRef {
|
fn fragment_shader() -> ShaderRef {
|
||||||
"shaders/tonemapping_test_patterns.wgsl".into()
|
"shaders/tonemapping_test_patterns.wgsl".into()
|
||||||
|
|
|
@ -30,7 +30,7 @@ use bevy::{
|
||||||
prelude::*,
|
prelude::*,
|
||||||
render::{
|
render::{
|
||||||
camera::{Exposure, TemporalJitter},
|
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),
|
transform: Transform::from_xyz(1.0, 1.8, 7.0).looking_at(Vec3::ZERO, Vec3::Y),
|
||||||
color_grading: ColorGrading {
|
color_grading: ColorGrading {
|
||||||
post_saturation: 1.2,
|
global: ColorGradingGlobal {
|
||||||
|
post_saturation: 1.2,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
tonemapping: Tonemapping::TonyMcMapface,
|
tonemapping: Tonemapping::TonyMcMapface,
|
||||||
|
|
|
@ -129,6 +129,7 @@ Example | Description
|
||||||
[Anti-aliasing](../examples/3d/anti_aliasing.rs) | Compares different anti-aliasing methods
|
[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
|
[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
|
[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
|
[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
|
[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
|
[Generate Custom Mesh](../examples/3d/generate_custom_mesh.rs) | Simple showcase of how to generate a custom mesh with a custom texture
|
||||||
|
|
Loading…
Reference in a new issue