Initial raytraced lighting progress (bevy_solari) (#19058)
# Bevy Solari <img src="https://github.com/user-attachments/assets/94061fc8-01cf-4208-b72a-8eecad610d76" width="100" /> ## Preface - See release notes. - Please talk to me in #rendering-dev on discord or open a github discussion if you have questions about the long term plan, and keep discussion in this PR limited to the contents of the PR :) ## Connections - Works towards #639, #16408. - Spawned https://github.com/bevyengine/bevy/issues/18993. - Need to fix RT stuff in naga_oil first https://github.com/bevyengine/naga_oil/pull/116. ## This PR After nearly two years, I've revived the raytraced lighting effort I first started in https://github.com/bevyengine/bevy/pull/10000. Unlike that PR, which has realtime techniques, I've limited this PR to: * `RaytracingScenePlugin` - BLAS and TLAS building, geometry and texture binding, sampling functions. * `PathtracingPlugin` - A non-realtime path tracer intended to serve as a testbed and reference. ## What's implemented?  * BLAS building on mesh load * Emissive lights * Directional lights with soft shadows * Diffuse (lambert, not Bevy's diffuse BRDF) and emissive materials * A reference path tracer with: * Antialiasing * Direct light sampling (next event estimation) with 0/1 MIS weights * Importance-sampled BRDF bounces * Russian roulette ## What's _not_ implemented? * Anything realtime, including a real-time denoiser * Integration with Bevy's rasterized gbuffer * Specular materials * Non-opaque geometry * Any sort of CPU or GPU optimizations * BLAS compaction, proper bindless, and further RT APIs are things that we need wgpu to add * PointLights, SpotLights, or skyboxes / environment lighting * Support for materials other than StandardMaterial (and only a subset of properties are supported) * Skinned/morphed or otherwise animating/deformed meshes * Mipmaps * Adaptive self-intersection ray bias * A good way for developers to detect whether the user's GPU supports RT or not, and fallback to baked lighting. * Documentation and actual finalized APIs (literally everything is subject to change) ## End-user Usage * Have a GPU that supports RT with inline ray queries * Add `SolariPlugin` to your app * Ensure any `Mesh` asset you want to use for raytracing has `enable_raytracing: true` (defaults to true), and that it uses the standard uncompressed position/normal/uv_0/tangent vertex attribute set, triangle list topology, and 32-bit indices. * If you don't want to build a BLAS and use the mesh for RT, set enable_raytracing to false. * Add the `RaytracingMesh3d` component to your entity (separate from `Mesh3d` or `MeshletMesh3d`). ## Testing - Did you test these changes? If so, how? - Ran the solari example. - Are there any parts that need more testing? - Other test scenes probably. Normal mapping would be good to test. - How can other people (reviewers) test your changes? Is there anything specific they need to know? - See the solari.rs example for how to setup raytracing. - If relevant, what platforms did you test these changes on, and are there any important ones you can't test? - Windows 11, NVIDIA RTX 3080. --------- Co-authored-by: atlv <email@atlasdostal.com> Co-authored-by: IceSentry <IceSentry@users.noreply.github.com> Co-authored-by: Carter Anderson <mcanders1@gmail.com>
This commit is contained in:
parent
58c276ab44
commit
bab31e3777
21
Cargo.toml
21
Cargo.toml
@ -247,6 +247,15 @@ bevy_render = ["bevy_internal/bevy_render", "bevy_color"]
|
||||
# Provides scene functionality
|
||||
bevy_scene = ["bevy_internal/bevy_scene", "bevy_asset"]
|
||||
|
||||
# Provides raytraced lighting (experimental)
|
||||
bevy_solari = [
|
||||
"bevy_internal/bevy_solari",
|
||||
"bevy_asset",
|
||||
"bevy_core_pipeline",
|
||||
"bevy_pbr",
|
||||
"bevy_render",
|
||||
]
|
||||
|
||||
# Provides sprite functionality
|
||||
bevy_sprite = [
|
||||
"bevy_internal/bevy_sprite",
|
||||
@ -1261,6 +1270,18 @@ description = "Load a cubemap texture onto a cube like a skybox and cycle throug
|
||||
category = "3D Rendering"
|
||||
wasm = false
|
||||
|
||||
[[example]]
|
||||
name = "solari"
|
||||
path = "examples/3d/solari.rs"
|
||||
doc-scrape-examples = true
|
||||
required-features = ["bevy_solari"]
|
||||
|
||||
[package.metadata.example.solari]
|
||||
name = "Solari"
|
||||
description = "Demonstrates realtime dynamic global illumination rendering using Bevy Solari."
|
||||
category = "3D Rendering"
|
||||
wasm = false
|
||||
|
||||
[[example]]
|
||||
name = "spherical_area_lights"
|
||||
path = "examples/3d/spherical_area_lights.rs"
|
||||
|
113
assets/branding/bevy_solari.svg
Normal file
113
assets/branding/bevy_solari.svg
Normal file
@ -0,0 +1,113 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
id="svg2321"
|
||||
version="1.1"
|
||||
viewBox="0 0 63.304429 63.304432"
|
||||
height="63.304432mm"
|
||||
width="63.304428mm"
|
||||
sodipodi:docname="bevy_solari.svg"
|
||||
inkscape:version="1.4 (86a8ad7, 2024-10-11)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:zoom="0.27643554"
|
||||
inkscape:cx="54.262198"
|
||||
inkscape:cy="-311.10327"
|
||||
inkscape:window-width="1440"
|
||||
inkscape:window-height="788"
|
||||
inkscape:window-x="-6"
|
||||
inkscape:window-y="-6"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg2321" />
|
||||
<defs
|
||||
id="defs2315" />
|
||||
<metadata
|
||||
id="metadata2318">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="" />
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
id="g11"
|
||||
style="display:inline"
|
||||
transform="translate(-27.298342,-111.49082)">
|
||||
<path
|
||||
style="fill:#ff8904;fill-opacity:1;stroke:none;stroke-width:0.264583;stroke-opacity:1"
|
||||
id="path10-7"
|
||||
d="m 38.15031,109.44704 c 0.743942,3.07133 6.496307,7.00051 6.678099,10.15542 0.116231,2.01713 -5.225098,3.23914 -5.79502,5.17757 -0.891392,3.03182 2.125712,9.31077 0.705692,12.1339 -0.907907,1.80501 -6.144635,0.19263 -7.607418,1.5864 -2.287879,2.17994 -2.814466,9.12622 -5.455804,10.86111 -1.688772,1.10923 -5.417723,-2.9055 -7.381414,-2.42985 -3.071331,0.74394 -7.000511,6.49631 -10.1554234,6.6781 -2.0171309,0.11623 -3.2391354,-5.2251 -5.1775665,-5.79502 -3.03182154,-0.89139 -9.3107731,2.12571 -12.1339038,0.70569 -1.8050021,-0.9079 -0.1926248,-6.14463 -1.5863942,-7.60742 -2.1799381,-2.28787 -9.1262221,-2.81446 -10.8611151,-5.4558 -1.109224,-1.68877 2.9055,-5.41772 2.429851,-7.38141 -0.743942,-3.07133 -6.496306,-7.00051 -6.678099,-10.15543 -0.116231,-2.01713 5.225098,-3.23913 5.79502,-5.17756 0.891393,-3.03182 -2.125711,-9.31078 -0.705692,-12.13391 0.907907,-1.804999 6.144635,-0.19262 7.607418,-1.586391 2.2878791,-2.179939 2.8144662,-9.126222 5.4558046,-10.861115 1.6887711,-1.109225 5.4177222,2.905499 7.38141398,2.429851 3.07133072,-0.743942 7.00051032,-6.496307 10.15542342,-6.678099 2.01713,-0.116231 3.239135,5.225097 5.177566,5.79502 3.031822,0.891392 9.310773,-2.125712 12.133904,-0.705692 1.805002,0.907906 0.192625,6.144634 1.586394,7.607417 2.179938,2.28788 9.126222,2.814467 10.861115,5.455809 1.109224,1.68877 -2.905499,5.41772 -2.429851,7.38141 z"
|
||||
transform="matrix(0.90823691,0,0,0.90823691,49.886257,35.27956)" />
|
||||
<g
|
||||
id="g3-6"
|
||||
transform="translate(-517.96199,-278.01119)"
|
||||
style="display:inline;opacity:1;fill:#ffd230;fill-opacity:1;stroke-width:0.755906;stroke-miterlimit:4;stroke-dasharray:none">
|
||||
<g
|
||||
transform="matrix(-0.40426719,-0.17438247,-0.17438247,0.40426719,678.77611,389.84765)"
|
||||
style="fill:#ffd230;fill-opacity:1"
|
||||
id="g2-1">
|
||||
<path
|
||||
id="path1-4"
|
||||
style="fill:#ffd230;fill-opacity:1;stroke:none;stroke-width:0.559814px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 2246.0312,2340.1914 v 0 c -0.016,3e-4 -0.031,0 -0.047,0 -0.9804,3.0675 -1.7386,6.3997 -1.8828,10.1953 -0.2712,7.1263 0.453,11.4639 -0.3672,16.0801 -0.8202,4.6163 -3.2453,9.161 -9.4141,16.2871 -7.3424,8.482 -18.9789,15.0453 -32.4199,17.2637 -2.5015,1.5971 -5.1421,3.0609 -7.9199,4.3633 10.4618,3.9385 21.4025,4.1531 30.0761,1.3066 15.2793,-5.0141 14.0962,-8.6155 20.9434,-19.1074 2.1569,-3.3051 4.6474,-5.8282 7.1484,-7.9004 7.1248,3.1068 14.1431,5.1015 18.5157,4.6074 2.351,-5.4505 -0.057,-11.7712 -4.0586,-17.7461 3.2821,-10.196 -1.6986,-20.4059 -12.7305,-24.0156 -2.8775,-0.9415 -5.4633,-1.3844 -7.8438,-1.3379 z m 8.2754,14.9707 a 4.1668789,4.2454995 48.679502 0 1 3.1973,1.3965 4.1668789,4.2454995 48.679502 0 1 -0.4375,5.9336 4.1668789,4.2454995 48.679502 0 1 -5.9394,-0.3262 4.1668789,4.2454995 48.679502 0 1 0.4375,-5.9336 4.1668789,4.2454995 48.679502 0 1 2.7421,-1.0703 z m -68.375,45.3789 c 0.1273,0.075 0.2572,0.1408 0.3848,0.2149 0.131,-0.049 0.2642,-0.1009 0.3945,-0.1504 -0.2598,-0.023 -0.5188,-0.039 -0.7793,-0.064 z"
|
||||
transform="matrix(-0.55180403,-0.23802315,-0.23802315,0.55180403,1946.7322,-620.612)" />
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
id="g5-2"
|
||||
transform="matrix(-0.45399624,0.36689705,0.36689705,0.45399624,73.527335,10.816805)"
|
||||
style="display:inline;opacity:1;fill:#fef3c6;fill-opacity:1;stroke:none;stroke-width:0.755906;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1">
|
||||
<g
|
||||
id="g4-3"
|
||||
transform="matrix(-0.35254083,0.28490586,0.28490586,0.35254083,477.11004,-1021.7666)"
|
||||
style="opacity:1;fill:#fef3c6;fill-opacity:1">
|
||||
<path
|
||||
id="path2-2"
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#fef3c6;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.02362;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
|
||||
d="m 2191.1465,2276.7832 c -5.9729,-0.035 -12.0979,2.348 -17.3613,7.459 -6.9129,6.7127 -9.0602,12.7555 -7.8477,20.2949 l 0.332,2.0684 -2.0664,-0.336 c -15.1877,-2.4609 -33.9847,-1.2178 -55.3711,7.4336 6.2868,2.6948 17.8259,7.1926 30.6309,13.3418 l 4.0605,1.9512 -4.414,0.8945 c -16.9087,3.4274 -36.9729,13.3275 -55.2989,34.9336 8.1981,-0.6372 24.9531,-2.6089 42.4278,-2.582 9.7138,0.015 19.2869,0.687 27.0859,2.709 7.7991,2.022 14.8874,6.6498 15.8861,10.0406 0.9987,3.3908 0.432,5.1761 -0.5519,7.8285 -0.9839,2.6524 -4.0098,6.6817 -8.1953,9.3418 -4.1855,2.6601 -9.4961,4.9849 -15.0137,6.9609 -11.0352,3.9521 -22.7798,6.4773 -27.9648,6.959 -1.1021,0.1024 -1.5421,0.4983 -1.9668,1.2696 -0.4247,0.7712 -0.659,1.9824 -0.6934,3.25 -0.046,1.6926 0.217,2.576 0.6949,3.246 0.4779,0.67 1.2243,0.9381 1.9934,0.9902 32.5822,2.2052 56.9441,-5.9907 74.6379,-13.0116 20.3508,-9.3311 33.2134,-27.7577 36.0058,-44.3477 1.7499,-10.395 1.3746,-15.4894 -0.3124,-19.8281 -1.6873,-4.3387 -4.9223,-8.1914 -9.0254,-15.5488 -2.6368,-4.7281 -4.1077,-9.367 -5.0196,-13.6875 l -0.1933,-0.9102 0.7265,-0.582 c 7.5403,-6.0446 13.6809,-12.6444 15.9102,-17.4492 -4.5742,-4.8648 -12.4787,-5.893 -21.3223,-4.9473 l -0.7265,0.076 -0.5118,-0.5215 c -4.7125,-4.8006 -10.5615,-7.2614 -16.5351,-7.2969 z m 2.6484,11.2324 c 2.593,-0.041 4.8808,1.7566 5.502,4.3223 0.7307,3.0216 -1.0812,6.0525 -4.0469,6.7695 -2.9656,0.7176 -5.9625,-1.1502 -6.6934,-4.1719 -0.7307,-3.0216 1.0812,-6.0525 4.0469,-6.7695 0.3902,-0.094 0.7897,-0.1445 1.1914,-0.1504 z"
|
||||
transform="translate(5.0092774e-5,-757.87625)" />
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
style="display:inline;opacity:1;fill:#fee685;fill-opacity:1;stroke-width:0.755906;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="g10-2"
|
||||
transform="matrix(-0.50509374,0.06754889,0.06754889,0.50509374,156.75523,55.243465)">
|
||||
<g
|
||||
style="fill:#fee685;fill-opacity:1"
|
||||
id="g9-1"
|
||||
transform="translate(-20.244579,-6.1209206)">
|
||||
<g
|
||||
style="fill:#fee685;fill-opacity:1"
|
||||
id="g8-6"
|
||||
transform="translate(61.54776,-5.6726683)">
|
||||
<g
|
||||
id="g7-8"
|
||||
style="fill:#fee685;fill-opacity:1">
|
||||
<g
|
||||
id="g6-5"
|
||||
transform="matrix(-0.514626,0.06882369,0.06882369,0.514626,1184.3644,-811.9091)"
|
||||
style="opacity:1;fill:#fee685;fill-opacity:1">
|
||||
<path
|
||||
id="path4-7"
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#fee685;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.02362;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
|
||||
d="m 2230.8945,2301.1738 c -1.9108,-0.039 -3.9117,0.162 -5.9785,0.6328 -0.1394,0.032 -0.2613,0.071 -0.3984,0.1036 -2.274,2.2481 -4.8127,4.5047 -7.5293,6.7168 0.8746,3.8597 2.1735,7.8829 4.4707,12.0019 3.9872,7.1495 7.2742,10.9657 9.2031,15.9258 1.9289,4.9601 2.2639,10.7945 0.4746,21.4238 -2.2183,13.178 -10.2404,27.1324 -22.959,37.4336 9.8717,-2.8792 18.2866,-8.1915 23.8575,-14.6269 6.0132,-6.9464 8.0191,-10.8762 8.7226,-14.836 0.7036,-3.9598 0.044,-8.2997 0.3242,-15.664 0.1805,-4.7447 1.1911,-8.8958 2.4766,-12.545 l 0.3086,-0.875 0.9219,-0.1211 c 8.2284,-1.0673 15.6654,-3.167 19.5097,-5.6484 -1.2349,-5.5522 -6.4807,-9.8603 -13.4277,-13.1348 l -0.6621,-0.3125 -0.166,-0.7129 c -2.2034,-9.4614 -9.5905,-15.5632 -19.1485,-15.7617 z m 4.7832,11.6856 a 4.8229105,4.9139092 17.729059 0 1 1.4473,0.2246 4.8229105,4.9139092 17.729059 0 1 3.0977,6.1484 4.8229105,4.9139092 17.729059 0 1 -6.0899,3.2129 4.8229105,4.9139092 17.729059 0 1 -3.0976,-6.1484 4.8229105,4.9139092 17.729059 0 1 4.6425,-3.4375 z"
|
||||
transform="translate(1.2499985e-4,-757.87627)" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 11 KiB |
@ -410,6 +410,7 @@ bevy_picking = { path = "../bevy_picking", optional = true, version = "0.16.0-de
|
||||
bevy_remote = { path = "../bevy_remote", optional = true, version = "0.16.0-dev" }
|
||||
bevy_render = { path = "../bevy_render", optional = true, version = "0.16.0-dev" }
|
||||
bevy_scene = { path = "../bevy_scene", optional = true, version = "0.16.0-dev" }
|
||||
bevy_solari = { path = "../bevy_solari", optional = true, version = "0.16.0-dev" }
|
||||
bevy_sprite = { path = "../bevy_sprite", optional = true, version = "0.16.0-dev" }
|
||||
bevy_state = { path = "../bevy_state", optional = true, version = "0.16.0-dev", default-features = false, features = [
|
||||
"bevy_app",
|
||||
|
@ -62,6 +62,8 @@ pub use bevy_remote as remote;
|
||||
pub use bevy_render as render;
|
||||
#[cfg(feature = "bevy_scene")]
|
||||
pub use bevy_scene as scene;
|
||||
#[cfg(feature = "bevy_solari")]
|
||||
pub use bevy_solari as solari;
|
||||
#[cfg(feature = "bevy_sprite")]
|
||||
pub use bevy_sprite as sprite;
|
||||
#[cfg(feature = "bevy_state")]
|
||||
|
@ -119,6 +119,21 @@ pub struct Mesh {
|
||||
morph_targets: Option<Handle<Image>>,
|
||||
morph_target_names: Option<Vec<String>>,
|
||||
pub asset_usage: RenderAssetUsages,
|
||||
/// Whether or not to build a BLAS for use with `bevy_solari` raytracing.
|
||||
///
|
||||
/// Note that this is _not_ whether the mesh is _compatible_ with `bevy_solari` raytracing.
|
||||
/// This field just controls whether or not a BLAS gets built for this mesh, assuming that
|
||||
/// the mesh is compatible.
|
||||
///
|
||||
/// The use case for this field is using lower-resolution proxy meshes for raytracing (to save on BLAS memory usage),
|
||||
/// while using higher-resolution meshes for raster. You can set this field to true for the lower-resolution proxy mesh,
|
||||
/// and to false for the high-resolution raster mesh.
|
||||
///
|
||||
/// Alternatively, you can use the same mesh for both raster and raytracing, with this field set to true.
|
||||
///
|
||||
/// Does nothing if not used with `bevy_solari`, or if the mesh is not compatible
|
||||
/// with `bevy_solari` (see `bevy_solari`'s docs).
|
||||
pub enable_raytracing: bool,
|
||||
}
|
||||
|
||||
impl Mesh {
|
||||
@ -203,6 +218,7 @@ impl Mesh {
|
||||
morph_targets: None,
|
||||
morph_target_names: None,
|
||||
asset_usage,
|
||||
enable_raytracing: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1197,6 +1197,10 @@ fn get_capabilities(features: Features, downlevel: DownlevelFlags) -> Capabiliti
|
||||
Capabilities::MULTISAMPLED_SHADING,
|
||||
downlevel.contains(DownlevelFlags::MULTISAMPLED_SHADING),
|
||||
);
|
||||
capabilities.set(
|
||||
Capabilities::RAY_QUERY,
|
||||
features.contains(Features::EXPERIMENTAL_RAY_QUERY),
|
||||
);
|
||||
capabilities.set(
|
||||
Capabilities::DUAL_SOURCE_BLENDING,
|
||||
features.contains(Features::DUAL_SOURCE_BLENDING),
|
||||
|
@ -180,12 +180,6 @@ pub async fn initialize_renderer(
|
||||
features -= wgpu::Features::MAPPABLE_PRIMARY_BUFFERS;
|
||||
}
|
||||
|
||||
// RAY_QUERY and RAY_TRACING_ACCELERATION STRUCTURE will sometimes cause DeviceLost failures on platforms
|
||||
// that report them as supported:
|
||||
// <https://github.com/gfx-rs/wgpu/issues/5488>
|
||||
features -= wgpu::Features::EXPERIMENTAL_RAY_QUERY;
|
||||
features -= wgpu::Features::EXPERIMENTAL_RAY_TRACING_ACCELERATION_STRUCTURE;
|
||||
|
||||
limits = adapter.limits();
|
||||
}
|
||||
|
||||
|
38
crates/bevy_solari/Cargo.toml
Normal file
38
crates/bevy_solari/Cargo.toml
Normal file
@ -0,0 +1,38 @@
|
||||
[package]
|
||||
name = "bevy_solari"
|
||||
version = "0.16.0-dev"
|
||||
edition = "2024"
|
||||
description = "Provides raytraced lighting for Bevy Engine"
|
||||
homepage = "https://bevyengine.org"
|
||||
repository = "https://github.com/bevyengine/bevy"
|
||||
license = "MIT OR Apache-2.0"
|
||||
keywords = ["bevy"]
|
||||
|
||||
[dependencies]
|
||||
# bevy
|
||||
bevy_app = { path = "../bevy_app", version = "0.16.0-dev" }
|
||||
bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" }
|
||||
bevy_color = { path = "../bevy_color", version = "0.16.0-dev" }
|
||||
bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.16.0-dev" }
|
||||
bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" }
|
||||
bevy_math = { path = "../bevy_math", version = "0.16.0-dev" }
|
||||
bevy_mesh = { path = "../bevy_mesh", version = "0.16.0-dev" }
|
||||
bevy_pbr = { path = "../bevy_pbr", version = "0.16.0-dev" }
|
||||
bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [
|
||||
"std",
|
||||
] }
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" }
|
||||
bevy_render = { path = "../bevy_render", version = "0.16.0-dev" }
|
||||
bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" }
|
||||
|
||||
# other
|
||||
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
||||
derive_more = { version = "1", default-features = false, features = ["from"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"]
|
||||
all-features = true
|
176
crates/bevy_solari/LICENSE-APACHE
Normal file
176
crates/bevy_solari/LICENSE-APACHE
Normal file
@ -0,0 +1,176 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
19
crates/bevy_solari/LICENSE-MIT
Normal file
19
crates/bevy_solari/LICENSE-MIT
Normal file
@ -0,0 +1,19 @@
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
9
crates/bevy_solari/README.md
Normal file
9
crates/bevy_solari/README.md
Normal file
@ -0,0 +1,9 @@
|
||||
# Bevy Solari
|
||||
|
||||
[](https://github.com/bevyengine/bevy#license)
|
||||
[](https://crates.io/crates/bevy_solari)
|
||||
[](https://crates.io/crates/bevy_solari)
|
||||
[](https://docs.rs/bevy_solari/latest/bevy_solari/)
|
||||
[](https://discord.gg/bevy)
|
||||
|
||||

|
52
crates/bevy_solari/src/lib.rs
Normal file
52
crates/bevy_solari/src/lib.rs
Normal file
@ -0,0 +1,52 @@
|
||||
#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")]
|
||||
|
||||
//! Provides raytraced lighting.
|
||||
//!
|
||||
//! See [`SolariPlugin`] for more info.
|
||||
//!
|
||||
//! 
|
||||
pub mod pathtracer;
|
||||
pub mod scene;
|
||||
|
||||
/// The solari prelude.
|
||||
///
|
||||
/// This includes the most common types in this crate, re-exported for your convenience.
|
||||
pub mod prelude {
|
||||
pub use super::SolariPlugin;
|
||||
pub use crate::pathtracer::Pathtracer;
|
||||
pub use crate::scene::RaytracingMesh3d;
|
||||
}
|
||||
|
||||
use bevy_app::{App, Plugin};
|
||||
use bevy_render::settings::WgpuFeatures;
|
||||
use pathtracer::PathtracingPlugin;
|
||||
use scene::RaytracingScenePlugin;
|
||||
|
||||
/// An experimental plugin for raytraced lighting.
|
||||
///
|
||||
/// This plugin provides:
|
||||
/// * (Coming soon) - Raytraced direct and indirect lighting.
|
||||
/// * [`RaytracingScenePlugin`] - BLAS building, resource and lighting binding.
|
||||
/// * [`PathtracingPlugin`] - A non-realtime pathtracer for validation purposes.
|
||||
///
|
||||
/// To get started, add `RaytracingMesh3d` and `MeshMaterial3d::<StandardMaterial>` to your entities.
|
||||
pub struct SolariPlugin;
|
||||
|
||||
impl Plugin for SolariPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_plugins((RaytracingScenePlugin, PathtracingPlugin));
|
||||
}
|
||||
}
|
||||
|
||||
impl SolariPlugin {
|
||||
/// [`WgpuFeatures`] required for this plugin to function.
|
||||
pub fn required_wgpu_features() -> WgpuFeatures {
|
||||
WgpuFeatures::EXPERIMENTAL_RAY_TRACING_ACCELERATION_STRUCTURE
|
||||
| WgpuFeatures::EXPERIMENTAL_RAY_QUERY
|
||||
| WgpuFeatures::BUFFER_BINDING_ARRAY
|
||||
| WgpuFeatures::TEXTURE_BINDING_ARRAY
|
||||
| WgpuFeatures::UNIFORM_BUFFER_AND_STORAGE_TEXTURE_ARRAY_NON_UNIFORM_INDEXING
|
||||
| WgpuFeatures::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING
|
||||
| WgpuFeatures::PARTIALLY_BOUND_BINDING_ARRAY
|
||||
}
|
||||
}
|
33
crates/bevy_solari/src/pathtracer/extract.rs
Normal file
33
crates/bevy_solari/src/pathtracer/extract.rs
Normal file
@ -0,0 +1,33 @@
|
||||
use super::{prepare::PathtracerAccumulationTexture, Pathtracer};
|
||||
use bevy_ecs::{
|
||||
change_detection::DetectChanges,
|
||||
system::{Commands, Query},
|
||||
world::Ref,
|
||||
};
|
||||
use bevy_render::{camera::Camera, sync_world::RenderEntity, Extract};
|
||||
use bevy_transform::components::GlobalTransform;
|
||||
|
||||
pub fn extract_pathtracer(
|
||||
cameras_3d: Extract<
|
||||
Query<(
|
||||
RenderEntity,
|
||||
&Camera,
|
||||
Ref<GlobalTransform>,
|
||||
Option<&Pathtracer>,
|
||||
)>,
|
||||
>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
for (entity, camera, global_transform, pathtracer) in &cameras_3d {
|
||||
let mut entity_commands = commands
|
||||
.get_entity(entity)
|
||||
.expect("Camera entity wasn't synced.");
|
||||
if pathtracer.is_some() && camera.is_active {
|
||||
let mut pathtracer = pathtracer.unwrap().clone();
|
||||
pathtracer.reset |= global_transform.is_changed();
|
||||
entity_commands.insert(pathtracer);
|
||||
} else {
|
||||
entity_commands.remove::<(Pathtracer, PathtracerAccumulationTexture)>();
|
||||
}
|
||||
}
|
||||
}
|
67
crates/bevy_solari/src/pathtracer/mod.rs
Normal file
67
crates/bevy_solari/src/pathtracer/mod.rs
Normal file
@ -0,0 +1,67 @@
|
||||
mod extract;
|
||||
mod node;
|
||||
mod prepare;
|
||||
|
||||
use crate::SolariPlugin;
|
||||
use bevy_app::{App, Plugin};
|
||||
use bevy_asset::embedded_asset;
|
||||
use bevy_core_pipeline::core_3d::graph::{Core3d, Node3d};
|
||||
use bevy_ecs::{component::Component, reflect::ReflectComponent, schedule::IntoScheduleConfigs};
|
||||
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
|
||||
use bevy_render::{
|
||||
render_graph::{RenderGraphApp, ViewNodeRunner},
|
||||
renderer::RenderDevice,
|
||||
view::Hdr,
|
||||
ExtractSchedule, Render, RenderApp, RenderSystems,
|
||||
};
|
||||
use extract::extract_pathtracer;
|
||||
use node::PathtracerNode;
|
||||
use prepare::prepare_pathtracer_accumulation_texture;
|
||||
use tracing::warn;
|
||||
|
||||
/// Non-realtime pathtracing.
|
||||
///
|
||||
/// This plugin is meant to generate reference screenshots to compare against,
|
||||
/// and is not intended to be used by games.
|
||||
pub struct PathtracingPlugin;
|
||||
|
||||
impl Plugin for PathtracingPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
embedded_asset!(app, "pathtracer.wgsl");
|
||||
|
||||
app.register_type::<Pathtracer>();
|
||||
}
|
||||
|
||||
fn finish(&self, app: &mut App) {
|
||||
let render_app = app.sub_app_mut(RenderApp);
|
||||
|
||||
let render_device = render_app.world().resource::<RenderDevice>();
|
||||
let features = render_device.features();
|
||||
if !features.contains(SolariPlugin::required_wgpu_features()) {
|
||||
warn!(
|
||||
"PathtracingPlugin not loaded. GPU lacks support for required features: {:?}.",
|
||||
SolariPlugin::required_wgpu_features().difference(features)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
render_app
|
||||
.add_systems(ExtractSchedule, extract_pathtracer)
|
||||
.add_systems(
|
||||
Render,
|
||||
prepare_pathtracer_accumulation_texture.in_set(RenderSystems::PrepareResources),
|
||||
)
|
||||
.add_render_graph_node::<ViewNodeRunner<PathtracerNode>>(
|
||||
Core3d,
|
||||
node::graph::PathtracerNode,
|
||||
)
|
||||
.add_render_graph_edges(Core3d, (Node3d::EndMainPass, node::graph::PathtracerNode));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Reflect, Default, Clone)]
|
||||
#[reflect(Component, Default, Clone)]
|
||||
#[require(Hdr)]
|
||||
pub struct Pathtracer {
|
||||
pub reset: bool,
|
||||
}
|
134
crates/bevy_solari/src/pathtracer/node.rs
Normal file
134
crates/bevy_solari/src/pathtracer/node.rs
Normal file
@ -0,0 +1,134 @@
|
||||
use super::{prepare::PathtracerAccumulationTexture, Pathtracer};
|
||||
use crate::scene::RaytracingSceneBindings;
|
||||
use bevy_asset::load_embedded_asset;
|
||||
use bevy_ecs::{
|
||||
query::QueryItem,
|
||||
world::{FromWorld, World},
|
||||
};
|
||||
use bevy_render::{
|
||||
camera::ExtractedCamera,
|
||||
render_graph::{NodeRunError, RenderGraphContext, ViewNode},
|
||||
render_resource::{
|
||||
binding_types::{texture_storage_2d, uniform_buffer},
|
||||
BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, CachedComputePipelineId,
|
||||
ComputePassDescriptor, ComputePipelineDescriptor, ImageSubresourceRange, PipelineCache,
|
||||
ShaderStages, StorageTextureAccess, TextureFormat,
|
||||
},
|
||||
renderer::{RenderContext, RenderDevice},
|
||||
view::{ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms},
|
||||
};
|
||||
|
||||
pub mod graph {
|
||||
use bevy_render::render_graph::RenderLabel;
|
||||
|
||||
#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)]
|
||||
pub struct PathtracerNode;
|
||||
}
|
||||
|
||||
pub struct PathtracerNode {
|
||||
bind_group_layout: BindGroupLayout,
|
||||
pipeline: CachedComputePipelineId,
|
||||
}
|
||||
|
||||
impl ViewNode for PathtracerNode {
|
||||
type ViewQuery = (
|
||||
&'static Pathtracer,
|
||||
&'static PathtracerAccumulationTexture,
|
||||
&'static ExtractedCamera,
|
||||
&'static ViewTarget,
|
||||
&'static ViewUniformOffset,
|
||||
);
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_graph: &mut RenderGraphContext,
|
||||
render_context: &mut RenderContext,
|
||||
(pathtracer, accumulation_texture, camera, view_target, view_uniform_offset): QueryItem<
|
||||
Self::ViewQuery,
|
||||
>,
|
||||
world: &World,
|
||||
) -> Result<(), NodeRunError> {
|
||||
let pipeline_cache = world.resource::<PipelineCache>();
|
||||
let scene_bindings = world.resource::<RaytracingSceneBindings>();
|
||||
let view_uniforms = world.resource::<ViewUniforms>();
|
||||
let (Some(pipeline), Some(scene_bindings), Some(viewport), Some(view_uniforms)) = (
|
||||
pipeline_cache.get_compute_pipeline(self.pipeline),
|
||||
&scene_bindings.bind_group,
|
||||
camera.physical_viewport_size,
|
||||
view_uniforms.uniforms.binding(),
|
||||
) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let bind_group = render_context.render_device().create_bind_group(
|
||||
"pathtracer_bind_group",
|
||||
&self.bind_group_layout,
|
||||
&BindGroupEntries::sequential((
|
||||
&accumulation_texture.0.default_view,
|
||||
view_target.get_unsampled_color_attachment().view,
|
||||
view_uniforms,
|
||||
)),
|
||||
);
|
||||
|
||||
let command_encoder = render_context.command_encoder();
|
||||
|
||||
if pathtracer.reset {
|
||||
command_encoder.clear_texture(
|
||||
&accumulation_texture.0.texture,
|
||||
&ImageSubresourceRange::default(),
|
||||
);
|
||||
}
|
||||
|
||||
let mut pass = command_encoder.begin_compute_pass(&ComputePassDescriptor {
|
||||
label: Some("pathtracer"),
|
||||
timestamp_writes: None,
|
||||
});
|
||||
pass.set_pipeline(pipeline);
|
||||
pass.set_bind_group(0, scene_bindings, &[]);
|
||||
pass.set_bind_group(1, &bind_group, &[view_uniform_offset.offset]);
|
||||
pass.dispatch_workgroups(viewport.x.div_ceil(8), viewport.y.div_ceil(8), 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromWorld for PathtracerNode {
|
||||
fn from_world(world: &mut World) -> Self {
|
||||
let render_device = world.resource::<RenderDevice>();
|
||||
let pipeline_cache = world.resource::<PipelineCache>();
|
||||
let scene_bindings = world.resource::<RaytracingSceneBindings>();
|
||||
|
||||
let bind_group_layout = render_device.create_bind_group_layout(
|
||||
"pathtracer_bind_group_layout",
|
||||
&BindGroupLayoutEntries::sequential(
|
||||
ShaderStages::COMPUTE,
|
||||
(
|
||||
texture_storage_2d(TextureFormat::Rgba32Float, StorageTextureAccess::ReadWrite),
|
||||
texture_storage_2d(
|
||||
ViewTarget::TEXTURE_FORMAT_HDR,
|
||||
StorageTextureAccess::WriteOnly,
|
||||
),
|
||||
uniform_buffer::<ViewUniform>(true),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
let pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
|
||||
label: Some("pathtracer_pipeline".into()),
|
||||
layout: vec![
|
||||
scene_bindings.bind_group_layout.clone(),
|
||||
bind_group_layout.clone(),
|
||||
],
|
||||
push_constant_ranges: vec![],
|
||||
shader: load_embedded_asset!(world, "pathtracer.wgsl"),
|
||||
shader_defs: vec![],
|
||||
entry_point: "pathtrace".into(),
|
||||
zero_initialize_workgroup_memory: false,
|
||||
});
|
||||
|
||||
Self {
|
||||
bind_group_layout,
|
||||
pipeline,
|
||||
}
|
||||
}
|
||||
}
|
78
crates/bevy_solari/src/pathtracer/pathtracer.wgsl
Normal file
78
crates/bevy_solari/src/pathtracer/pathtracer.wgsl
Normal file
@ -0,0 +1,78 @@
|
||||
#import bevy_core_pipeline::tonemapping::tonemapping_luminance as luminance
|
||||
#import bevy_pbr::utils::{rand_f, rand_vec2f}
|
||||
#import bevy_render::maths::PI
|
||||
#import bevy_render::view::View
|
||||
#import bevy_solari::sampling::{sample_random_light, sample_cosine_hemisphere}
|
||||
#import bevy_solari::scene_bindings::{trace_ray, resolve_ray_hit_full, RAY_T_MIN, RAY_T_MAX}
|
||||
|
||||
@group(1) @binding(0) var accumulation_texture: texture_storage_2d<rgba32float, read_write>;
|
||||
@group(1) @binding(1) var view_output: texture_storage_2d<rgba16float, write>;
|
||||
@group(1) @binding(2) var<uniform> view: View;
|
||||
|
||||
@compute @workgroup_size(8, 8, 1)
|
||||
fn pathtrace(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
||||
if any(global_id.xy >= vec2u(view.viewport.zw)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let old_color = textureLoad(accumulation_texture, global_id.xy);
|
||||
|
||||
// Setup RNG
|
||||
let pixel_index = global_id.x + global_id.y * u32(view.viewport.z);
|
||||
let frame_index = u32(old_color.a) * 5782582u;
|
||||
var rng = pixel_index + frame_index;
|
||||
|
||||
// Shoot the first ray from the camera
|
||||
let pixel_center = vec2<f32>(global_id.xy) + 0.5;
|
||||
let jitter = rand_vec2f(&rng) - 0.5;
|
||||
let pixel_uv = (pixel_center + jitter) / view.viewport.zw;
|
||||
let pixel_ndc = (pixel_uv * 2.0) - 1.0;
|
||||
let primary_ray_target = view.world_from_clip * vec4(pixel_ndc.x, -pixel_ndc.y, 1.0, 1.0);
|
||||
var ray_origin = view.world_position;
|
||||
var ray_direction = normalize((primary_ray_target.xyz / primary_ray_target.w) - ray_origin);
|
||||
var ray_t_min = 0.0;
|
||||
|
||||
// Path trace
|
||||
var radiance = vec3(0.0);
|
||||
var throughput = vec3(1.0);
|
||||
loop {
|
||||
let ray_hit = trace_ray(ray_origin, ray_direction, ray_t_min, RAY_T_MAX, RAY_FLAG_NONE);
|
||||
if ray_hit.kind != RAY_QUERY_INTERSECTION_NONE {
|
||||
let ray_hit = resolve_ray_hit_full(ray_hit);
|
||||
|
||||
// Evaluate material BRDF
|
||||
let diffuse_brdf = ray_hit.material.base_color / PI;
|
||||
|
||||
// Use emissive only on the first ray (coming from the camera)
|
||||
if ray_t_min == 0.0 { radiance = ray_hit.material.emissive; }
|
||||
|
||||
// Sample direct lighting
|
||||
radiance += throughput * diffuse_brdf * sample_random_light(ray_hit.world_position, ray_hit.world_normal, &rng);
|
||||
|
||||
// Sample new ray direction from the material BRDF for next bounce
|
||||
ray_direction = sample_cosine_hemisphere(ray_hit.world_normal, &rng);
|
||||
|
||||
// Update other variables for next bounce
|
||||
ray_origin = ray_hit.world_position;
|
||||
ray_t_min = RAY_T_MIN;
|
||||
|
||||
// Update throughput for next bounce
|
||||
let cos_theta = dot(-ray_direction, ray_hit.world_normal);
|
||||
let cosine_hemisphere_pdf = cos_theta / PI; // Weight for the next bounce because we importance sampled the diffuse BRDF for the next ray direction
|
||||
throughput *= (diffuse_brdf * cos_theta) / cosine_hemisphere_pdf;
|
||||
|
||||
// Russian roulette for early termination
|
||||
let p = luminance(throughput);
|
||||
if rand_f(&rng) > p { break; }
|
||||
throughput /= p;
|
||||
} else { break; }
|
||||
}
|
||||
|
||||
// Camera exposure
|
||||
radiance *= view.exposure;
|
||||
|
||||
// Accumulation over time via running average
|
||||
let new_color = mix(old_color.rgb, radiance, 1.0 / (old_color.a + 1.0));
|
||||
textureStore(accumulation_texture, global_id.xy, vec4(new_color, old_color.a + 1.0));
|
||||
textureStore(view_output, global_id.xy, vec4(new_color, 1.0));
|
||||
}
|
52
crates/bevy_solari/src/pathtracer/prepare.rs
Normal file
52
crates/bevy_solari/src/pathtracer/prepare.rs
Normal file
@ -0,0 +1,52 @@
|
||||
use super::Pathtracer;
|
||||
use bevy_ecs::{
|
||||
component::Component,
|
||||
entity::Entity,
|
||||
query::With,
|
||||
system::{Commands, Query, Res, ResMut},
|
||||
};
|
||||
use bevy_render::{
|
||||
camera::ExtractedCamera,
|
||||
render_resource::{
|
||||
Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages,
|
||||
},
|
||||
renderer::RenderDevice,
|
||||
texture::{CachedTexture, TextureCache},
|
||||
};
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct PathtracerAccumulationTexture(pub CachedTexture);
|
||||
|
||||
pub fn prepare_pathtracer_accumulation_texture(
|
||||
query: Query<(Entity, &ExtractedCamera), With<Pathtracer>>,
|
||||
mut texture_cache: ResMut<TextureCache>,
|
||||
render_device: Res<RenderDevice>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
for (entity, camera) in &query {
|
||||
let Some(viewport) = camera.physical_viewport_size else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let descriptor = TextureDescriptor {
|
||||
label: Some("pathtracer_accumulation_texture"),
|
||||
size: Extent3d {
|
||||
width: viewport.x,
|
||||
height: viewport.y,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: TextureDimension::D2,
|
||||
format: TextureFormat::Rgba32Float,
|
||||
usage: TextureUsages::STORAGE_BINDING,
|
||||
view_formats: &[],
|
||||
};
|
||||
|
||||
commands
|
||||
.entity(entity)
|
||||
.insert(PathtracerAccumulationTexture(
|
||||
texture_cache.get(&render_device, descriptor),
|
||||
));
|
||||
}
|
||||
}
|
366
crates/bevy_solari/src/scene/binder.rs
Normal file
366
crates/bevy_solari/src/scene/binder.rs
Normal file
@ -0,0 +1,366 @@
|
||||
use super::{blas::BlasManager, extract::StandardMaterialAssets, RaytracingMesh3d};
|
||||
use bevy_asset::{AssetId, Handle};
|
||||
use bevy_color::{ColorToComponents, LinearRgba};
|
||||
use bevy_ecs::{
|
||||
resource::Resource,
|
||||
system::{Query, Res, ResMut},
|
||||
world::{FromWorld, World},
|
||||
};
|
||||
use bevy_math::{ops::cos, Mat4, Vec3};
|
||||
use bevy_pbr::{ExtractedDirectionalLight, MeshMaterial3d, StandardMaterial};
|
||||
use bevy_platform::{collections::HashMap, hash::FixedHasher};
|
||||
use bevy_render::{
|
||||
mesh::allocator::MeshAllocator,
|
||||
render_asset::RenderAssets,
|
||||
render_resource::{binding_types::*, *},
|
||||
renderer::{RenderDevice, RenderQueue},
|
||||
texture::{FallbackImage, GpuImage},
|
||||
};
|
||||
use bevy_transform::components::GlobalTransform;
|
||||
use core::{f32::consts::TAU, hash::Hash, num::NonZeroU32, ops::Deref};
|
||||
|
||||
const MAX_MESH_SLAB_COUNT: NonZeroU32 = NonZeroU32::new(500).unwrap();
|
||||
const MAX_TEXTURE_COUNT: NonZeroU32 = NonZeroU32::new(5_000).unwrap();
|
||||
|
||||
/// Average angular diameter of the sun as seen from earth.
|
||||
/// <https://en.wikipedia.org/wiki/Angular_diameter#Use_in_astronomy>
|
||||
const SUN_ANGULAR_DIAMETER_RADIANS: f32 = 0.00930842;
|
||||
|
||||
#[derive(Resource)]
|
||||
pub struct RaytracingSceneBindings {
|
||||
pub bind_group: Option<BindGroup>,
|
||||
pub bind_group_layout: BindGroupLayout,
|
||||
}
|
||||
|
||||
pub fn prepare_raytracing_scene_bindings(
|
||||
instances_query: Query<(
|
||||
&RaytracingMesh3d,
|
||||
&MeshMaterial3d<StandardMaterial>,
|
||||
&GlobalTransform,
|
||||
)>,
|
||||
directional_lights_query: Query<&ExtractedDirectionalLight>,
|
||||
mesh_allocator: Res<MeshAllocator>,
|
||||
blas_manager: Res<BlasManager>,
|
||||
material_assets: Res<StandardMaterialAssets>,
|
||||
texture_assets: Res<RenderAssets<GpuImage>>,
|
||||
fallback_texture: Res<FallbackImage>,
|
||||
render_device: Res<RenderDevice>,
|
||||
render_queue: Res<RenderQueue>,
|
||||
mut raytracing_scene_bindings: ResMut<RaytracingSceneBindings>,
|
||||
) {
|
||||
raytracing_scene_bindings.bind_group = None;
|
||||
|
||||
if instances_query.iter().len() == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut vertex_buffers = CachedBindingArray::new();
|
||||
let mut index_buffers = CachedBindingArray::new();
|
||||
let mut textures = CachedBindingArray::new();
|
||||
let mut samplers = Vec::new();
|
||||
let mut materials = StorageBufferList::<GpuMaterial>::default();
|
||||
let mut tlas = TlasPackage::new(render_device.wgpu_device().create_tlas(
|
||||
&CreateTlasDescriptor {
|
||||
label: Some("tlas"),
|
||||
flags: AccelerationStructureFlags::PREFER_FAST_TRACE,
|
||||
update_mode: AccelerationStructureUpdateMode::Build,
|
||||
max_instances: instances_query.iter().len() as u32,
|
||||
},
|
||||
));
|
||||
let mut transforms = StorageBufferList::<Mat4>::default();
|
||||
let mut geometry_ids = StorageBufferList::<GpuInstanceGeometryIds>::default();
|
||||
let mut material_ids = StorageBufferList::<u32>::default();
|
||||
let mut light_sources = StorageBufferList::<GpuLightSource>::default();
|
||||
let mut directional_lights = StorageBufferList::<GpuDirectionalLight>::default();
|
||||
|
||||
let mut material_id_map: HashMap<AssetId<StandardMaterial>, u32, FixedHasher> =
|
||||
HashMap::default();
|
||||
let mut material_id = 0;
|
||||
let mut process_texture = |texture_handle: &Option<Handle<_>>| -> Option<u32> {
|
||||
match texture_handle {
|
||||
Some(texture_handle) => match texture_assets.get(texture_handle.id()) {
|
||||
Some(texture) => {
|
||||
let (texture_id, is_new) =
|
||||
textures.push_if_absent(texture.texture_view.deref(), texture_handle.id());
|
||||
if is_new {
|
||||
samplers.push(texture.sampler.deref());
|
||||
}
|
||||
Some(texture_id)
|
||||
}
|
||||
None => None,
|
||||
},
|
||||
None => Some(u32::MAX),
|
||||
}
|
||||
};
|
||||
for (asset_id, material) in material_assets.iter() {
|
||||
let Some(base_color_texture_id) = process_texture(&material.base_color_texture) else {
|
||||
continue;
|
||||
};
|
||||
let Some(normal_map_texture_id) = process_texture(&material.normal_map_texture) else {
|
||||
continue;
|
||||
};
|
||||
let Some(emissive_texture_id) = process_texture(&material.emissive_texture) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
materials.get_mut().push(GpuMaterial {
|
||||
base_color: material.base_color.to_linear(),
|
||||
emissive: material.emissive,
|
||||
base_color_texture_id,
|
||||
normal_map_texture_id,
|
||||
emissive_texture_id,
|
||||
_padding: Default::default(),
|
||||
});
|
||||
|
||||
material_id_map.insert(*asset_id, material_id);
|
||||
material_id += 1;
|
||||
}
|
||||
|
||||
if material_id == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
if textures.is_empty() {
|
||||
textures.vec.push(fallback_texture.d2.texture_view.deref());
|
||||
samplers.push(fallback_texture.d2.sampler.deref());
|
||||
}
|
||||
|
||||
let mut instance_id = 0;
|
||||
for (mesh, material, transform) in &instances_query {
|
||||
let Some(blas) = blas_manager.get(&mesh.id()) else {
|
||||
continue;
|
||||
};
|
||||
let Some(vertex_slice) = mesh_allocator.mesh_vertex_slice(&mesh.id()) else {
|
||||
continue;
|
||||
};
|
||||
let Some(index_slice) = mesh_allocator.mesh_index_slice(&mesh.id()) else {
|
||||
continue;
|
||||
};
|
||||
let Some(material_id) = material_id_map.get(&material.id()).copied() else {
|
||||
continue;
|
||||
};
|
||||
let Some(material) = materials.get().get(material_id as usize) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let transform = transform.compute_matrix();
|
||||
*tlas.get_mut_single(instance_id).unwrap() = Some(TlasInstance::new(
|
||||
blas,
|
||||
tlas_transform(&transform),
|
||||
Default::default(),
|
||||
0xFF,
|
||||
));
|
||||
|
||||
transforms.get_mut().push(transform);
|
||||
|
||||
let (vertex_buffer_id, _) = vertex_buffers.push_if_absent(
|
||||
vertex_slice.buffer.as_entire_buffer_binding(),
|
||||
vertex_slice.buffer.id(),
|
||||
);
|
||||
let (index_buffer_id, _) = index_buffers.push_if_absent(
|
||||
index_slice.buffer.as_entire_buffer_binding(),
|
||||
index_slice.buffer.id(),
|
||||
);
|
||||
|
||||
geometry_ids.get_mut().push(GpuInstanceGeometryIds {
|
||||
vertex_buffer_id,
|
||||
vertex_buffer_offset: vertex_slice.range.start,
|
||||
index_buffer_id,
|
||||
index_buffer_offset: index_slice.range.start,
|
||||
});
|
||||
|
||||
material_ids.get_mut().push(material_id);
|
||||
|
||||
if material.emissive != LinearRgba::BLACK {
|
||||
light_sources
|
||||
.get_mut()
|
||||
.push(GpuLightSource::new_emissive_mesh_light(
|
||||
instance_id as u32,
|
||||
(index_slice.range.len() / 3) as u32,
|
||||
));
|
||||
}
|
||||
|
||||
instance_id += 1;
|
||||
}
|
||||
|
||||
if instance_id == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
for directional_light in &directional_lights_query {
|
||||
let directional_lights = directional_lights.get_mut();
|
||||
let directional_light_id = directional_lights.len() as u32;
|
||||
|
||||
directional_lights.push(GpuDirectionalLight::new(directional_light));
|
||||
|
||||
light_sources
|
||||
.get_mut()
|
||||
.push(GpuLightSource::new_directional_light(directional_light_id));
|
||||
}
|
||||
|
||||
materials.write_buffer(&render_device, &render_queue);
|
||||
transforms.write_buffer(&render_device, &render_queue);
|
||||
geometry_ids.write_buffer(&render_device, &render_queue);
|
||||
material_ids.write_buffer(&render_device, &render_queue);
|
||||
light_sources.write_buffer(&render_device, &render_queue);
|
||||
directional_lights.write_buffer(&render_device, &render_queue);
|
||||
|
||||
let mut command_encoder = render_device.create_command_encoder(&CommandEncoderDescriptor {
|
||||
label: Some("build_tlas_command_encoder"),
|
||||
});
|
||||
command_encoder.build_acceleration_structures(&[], [&tlas]);
|
||||
render_queue.submit([command_encoder.finish()]);
|
||||
|
||||
raytracing_scene_bindings.bind_group = Some(render_device.create_bind_group(
|
||||
"raytracing_scene_bind_group",
|
||||
&raytracing_scene_bindings.bind_group_layout,
|
||||
&BindGroupEntries::sequential((
|
||||
vertex_buffers.as_slice(),
|
||||
index_buffers.as_slice(),
|
||||
textures.as_slice(),
|
||||
samplers.as_slice(),
|
||||
materials.binding().unwrap(),
|
||||
tlas.as_binding(),
|
||||
transforms.binding().unwrap(),
|
||||
geometry_ids.binding().unwrap(),
|
||||
material_ids.binding().unwrap(),
|
||||
light_sources.binding().unwrap(),
|
||||
directional_lights.binding().unwrap(),
|
||||
)),
|
||||
));
|
||||
}
|
||||
|
||||
impl FromWorld for RaytracingSceneBindings {
|
||||
fn from_world(world: &mut World) -> Self {
|
||||
let render_device = world.resource::<RenderDevice>();
|
||||
|
||||
Self {
|
||||
bind_group: None,
|
||||
bind_group_layout: render_device.create_bind_group_layout(
|
||||
"raytracing_scene_bind_group_layout",
|
||||
&BindGroupLayoutEntries::sequential(
|
||||
ShaderStages::COMPUTE,
|
||||
(
|
||||
storage_buffer_read_only_sized(false, None).count(MAX_MESH_SLAB_COUNT),
|
||||
storage_buffer_read_only_sized(false, None).count(MAX_MESH_SLAB_COUNT),
|
||||
texture_2d(TextureSampleType::Float { filterable: true })
|
||||
.count(MAX_TEXTURE_COUNT),
|
||||
sampler(SamplerBindingType::Filtering).count(MAX_TEXTURE_COUNT),
|
||||
storage_buffer_read_only_sized(false, None),
|
||||
acceleration_structure(),
|
||||
storage_buffer_read_only_sized(false, None),
|
||||
storage_buffer_read_only_sized(false, None),
|
||||
storage_buffer_read_only_sized(false, None),
|
||||
storage_buffer_read_only_sized(false, None),
|
||||
storage_buffer_read_only_sized(false, None),
|
||||
),
|
||||
),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CachedBindingArray<T, I: Eq + Hash> {
|
||||
map: HashMap<I, u32>,
|
||||
vec: Vec<T>,
|
||||
}
|
||||
|
||||
impl<T, I: Eq + Hash> CachedBindingArray<T, I> {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
map: HashMap::default(),
|
||||
vec: Vec::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn push_if_absent(&mut self, item: T, item_id: I) -> (u32, bool) {
|
||||
let mut is_new = false;
|
||||
let i = *self.map.entry(item_id).or_insert_with(|| {
|
||||
is_new = true;
|
||||
let i = self.vec.len() as u32;
|
||||
self.vec.push(item);
|
||||
i
|
||||
});
|
||||
(i, is_new)
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.vec.is_empty()
|
||||
}
|
||||
|
||||
fn as_slice(&self) -> &[T] {
|
||||
self.vec.as_slice()
|
||||
}
|
||||
}
|
||||
|
||||
type StorageBufferList<T> = StorageBuffer<Vec<T>>;
|
||||
|
||||
#[derive(ShaderType)]
|
||||
struct GpuInstanceGeometryIds {
|
||||
vertex_buffer_id: u32,
|
||||
vertex_buffer_offset: u32,
|
||||
index_buffer_id: u32,
|
||||
index_buffer_offset: u32,
|
||||
}
|
||||
|
||||
#[derive(ShaderType)]
|
||||
struct GpuMaterial {
|
||||
base_color: LinearRgba,
|
||||
emissive: LinearRgba,
|
||||
base_color_texture_id: u32,
|
||||
normal_map_texture_id: u32,
|
||||
emissive_texture_id: u32,
|
||||
_padding: u32,
|
||||
}
|
||||
|
||||
#[derive(ShaderType)]
|
||||
struct GpuLightSource {
|
||||
kind: u32,
|
||||
id: u32,
|
||||
}
|
||||
|
||||
impl GpuLightSource {
|
||||
fn new_emissive_mesh_light(instance_id: u32, triangle_count: u32) -> GpuLightSource {
|
||||
Self {
|
||||
kind: triangle_count << 1,
|
||||
id: instance_id,
|
||||
}
|
||||
}
|
||||
|
||||
fn new_directional_light(directional_light_id: u32) -> GpuLightSource {
|
||||
Self {
|
||||
kind: 1,
|
||||
id: directional_light_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(ShaderType, Default)]
|
||||
struct GpuDirectionalLight {
|
||||
direction_to_light: Vec3,
|
||||
cos_theta_max: f32,
|
||||
luminance: Vec3,
|
||||
inverse_pdf: f32,
|
||||
}
|
||||
|
||||
impl GpuDirectionalLight {
|
||||
fn new(directional_light: &ExtractedDirectionalLight) -> Self {
|
||||
let cos_theta_max = cos(SUN_ANGULAR_DIAMETER_RADIANS / 2.0);
|
||||
let solid_angle = TAU * (1.0 - cos_theta_max);
|
||||
let luminance =
|
||||
(directional_light.color.to_vec3() * directional_light.illuminance) / solid_angle;
|
||||
|
||||
Self {
|
||||
direction_to_light: directional_light.transform.back().into(),
|
||||
cos_theta_max,
|
||||
luminance,
|
||||
inverse_pdf: solid_angle,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn tlas_transform(transform: &Mat4) -> [f32; 12] {
|
||||
transform.transpose().to_cols_array()[..12]
|
||||
.try_into()
|
||||
.unwrap()
|
||||
}
|
133
crates/bevy_solari/src/scene/blas.rs
Normal file
133
crates/bevy_solari/src/scene/blas.rs
Normal file
@ -0,0 +1,133 @@
|
||||
use bevy_asset::AssetId;
|
||||
use bevy_ecs::{
|
||||
resource::Resource,
|
||||
system::{Res, ResMut},
|
||||
};
|
||||
use bevy_mesh::{Indices, Mesh};
|
||||
use bevy_platform::collections::HashMap;
|
||||
use bevy_render::{
|
||||
mesh::{
|
||||
allocator::{MeshAllocator, MeshBufferSlice},
|
||||
RenderMesh,
|
||||
},
|
||||
render_asset::ExtractedAssets,
|
||||
render_resource::*,
|
||||
renderer::{RenderDevice, RenderQueue},
|
||||
};
|
||||
|
||||
#[derive(Resource, Default)]
|
||||
pub struct BlasManager(HashMap<AssetId<Mesh>, Blas>);
|
||||
|
||||
impl BlasManager {
|
||||
pub fn get(&self, mesh: &AssetId<Mesh>) -> Option<&Blas> {
|
||||
self.0.get(mesh)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prepare_raytracing_blas(
|
||||
mut blas_manager: ResMut<BlasManager>,
|
||||
extracted_meshes: Res<ExtractedAssets<RenderMesh>>,
|
||||
mesh_allocator: Res<MeshAllocator>,
|
||||
render_device: Res<RenderDevice>,
|
||||
render_queue: Res<RenderQueue>,
|
||||
) {
|
||||
let blas_manager = &mut blas_manager.0;
|
||||
|
||||
// Delete BLAS for deleted or modified meshes
|
||||
for asset_id in extracted_meshes
|
||||
.removed
|
||||
.iter()
|
||||
.chain(extracted_meshes.modified.iter())
|
||||
{
|
||||
blas_manager.remove(asset_id);
|
||||
}
|
||||
|
||||
if extracted_meshes.extracted.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new BLAS for added or changed meshes
|
||||
let blas_resources = extracted_meshes
|
||||
.extracted
|
||||
.iter()
|
||||
.filter(|(_, mesh)| is_mesh_raytracing_compatible(mesh))
|
||||
.map(|(asset_id, _)| {
|
||||
let vertex_slice = mesh_allocator.mesh_vertex_slice(asset_id).unwrap();
|
||||
let index_slice = mesh_allocator.mesh_index_slice(asset_id).unwrap();
|
||||
|
||||
let (blas, blas_size) =
|
||||
allocate_blas(&vertex_slice, &index_slice, asset_id, &render_device);
|
||||
|
||||
blas_manager.insert(*asset_id, blas);
|
||||
|
||||
(*asset_id, vertex_slice, index_slice, blas_size)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Build geometry into each BLAS
|
||||
let build_entries = blas_resources
|
||||
.iter()
|
||||
.map(|(asset_id, vertex_slice, index_slice, blas_size)| {
|
||||
let geometry = BlasTriangleGeometry {
|
||||
size: blas_size,
|
||||
vertex_buffer: vertex_slice.buffer,
|
||||
first_vertex: vertex_slice.range.start,
|
||||
vertex_stride: 48,
|
||||
index_buffer: Some(index_slice.buffer),
|
||||
first_index: Some(index_slice.range.start),
|
||||
transform_buffer: None,
|
||||
transform_buffer_offset: None,
|
||||
};
|
||||
BlasBuildEntry {
|
||||
blas: &blas_manager[asset_id],
|
||||
geometry: BlasGeometries::TriangleGeometries(vec![geometry]),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut command_encoder = render_device.create_command_encoder(&CommandEncoderDescriptor {
|
||||
label: Some("build_blas_command_encoder"),
|
||||
});
|
||||
command_encoder.build_acceleration_structures(&build_entries, &[]);
|
||||
render_queue.submit([command_encoder.finish()]);
|
||||
}
|
||||
|
||||
fn allocate_blas(
|
||||
vertex_slice: &MeshBufferSlice,
|
||||
index_slice: &MeshBufferSlice,
|
||||
asset_id: &AssetId<Mesh>,
|
||||
render_device: &RenderDevice,
|
||||
) -> (Blas, BlasTriangleGeometrySizeDescriptor) {
|
||||
let blas_size = BlasTriangleGeometrySizeDescriptor {
|
||||
vertex_format: Mesh::ATTRIBUTE_POSITION.format,
|
||||
vertex_count: vertex_slice.range.len() as u32,
|
||||
index_format: Some(IndexFormat::Uint32),
|
||||
index_count: Some(index_slice.range.len() as u32),
|
||||
flags: AccelerationStructureGeometryFlags::OPAQUE,
|
||||
};
|
||||
|
||||
let blas = render_device.wgpu_device().create_blas(
|
||||
&CreateBlasDescriptor {
|
||||
label: Some(&asset_id.to_string()),
|
||||
flags: AccelerationStructureFlags::PREFER_FAST_TRACE,
|
||||
update_mode: AccelerationStructureUpdateMode::Build,
|
||||
},
|
||||
BlasGeometrySizeDescriptors::Triangles {
|
||||
descriptors: vec![blas_size.clone()],
|
||||
},
|
||||
);
|
||||
|
||||
(blas, blas_size)
|
||||
}
|
||||
|
||||
fn is_mesh_raytracing_compatible(mesh: &Mesh) -> bool {
|
||||
let triangle_list = mesh.primitive_topology() == PrimitiveTopology::TriangleList;
|
||||
let vertex_attributes = mesh.attributes().map(|(attribute, _)| attribute.id).eq([
|
||||
Mesh::ATTRIBUTE_POSITION.id,
|
||||
Mesh::ATTRIBUTE_NORMAL.id,
|
||||
Mesh::ATTRIBUTE_UV_0.id,
|
||||
Mesh::ATTRIBUTE_TANGENT.id,
|
||||
]);
|
||||
let indexed_32 = matches!(mesh.indices(), Some(Indices::U32(..)));
|
||||
mesh.enable_raytracing && triangle_list && vertex_attributes && indexed_32
|
||||
}
|
45
crates/bevy_solari/src/scene/extract.rs
Normal file
45
crates/bevy_solari/src/scene/extract.rs
Normal file
@ -0,0 +1,45 @@
|
||||
use super::RaytracingMesh3d;
|
||||
use bevy_asset::{AssetId, Assets};
|
||||
use bevy_derive::Deref;
|
||||
use bevy_ecs::{
|
||||
resource::Resource,
|
||||
system::{Commands, Query},
|
||||
};
|
||||
use bevy_pbr::{MeshMaterial3d, StandardMaterial};
|
||||
use bevy_platform::collections::HashMap;
|
||||
use bevy_render::{extract_resource::ExtractResource, sync_world::RenderEntity, Extract};
|
||||
use bevy_transform::components::GlobalTransform;
|
||||
|
||||
pub fn extract_raytracing_scene(
|
||||
instances: Extract<
|
||||
Query<(
|
||||
RenderEntity,
|
||||
&RaytracingMesh3d,
|
||||
&MeshMaterial3d<StandardMaterial>,
|
||||
&GlobalTransform,
|
||||
)>,
|
||||
>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
for (render_entity, mesh, material, transform) in &instances {
|
||||
commands
|
||||
.entity(render_entity)
|
||||
.insert((mesh.clone(), material.clone(), *transform));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource, Deref, Default)]
|
||||
pub struct StandardMaterialAssets(HashMap<AssetId<StandardMaterial>, StandardMaterial>);
|
||||
|
||||
impl ExtractResource for StandardMaterialAssets {
|
||||
type Source = Assets<StandardMaterial>;
|
||||
|
||||
fn extract_resource(source: &Self::Source) -> Self {
|
||||
Self(
|
||||
source
|
||||
.iter()
|
||||
.map(|(asset_id, material)| (asset_id, material.clone()))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
77
crates/bevy_solari/src/scene/mod.rs
Normal file
77
crates/bevy_solari/src/scene/mod.rs
Normal file
@ -0,0 +1,77 @@
|
||||
mod binder;
|
||||
mod blas;
|
||||
mod extract;
|
||||
mod types;
|
||||
|
||||
pub use binder::RaytracingSceneBindings;
|
||||
pub use types::RaytracingMesh3d;
|
||||
|
||||
use crate::SolariPlugin;
|
||||
use bevy_app::{App, Plugin};
|
||||
use bevy_ecs::schedule::IntoScheduleConfigs;
|
||||
use bevy_render::{
|
||||
extract_resource::ExtractResourcePlugin,
|
||||
load_shader_library,
|
||||
mesh::{
|
||||
allocator::{allocate_and_free_meshes, MeshAllocator},
|
||||
RenderMesh,
|
||||
},
|
||||
render_asset::prepare_assets,
|
||||
render_resource::BufferUsages,
|
||||
renderer::RenderDevice,
|
||||
ExtractSchedule, Render, RenderApp, RenderSystems,
|
||||
};
|
||||
use binder::prepare_raytracing_scene_bindings;
|
||||
use blas::{prepare_raytracing_blas, BlasManager};
|
||||
use extract::{extract_raytracing_scene, StandardMaterialAssets};
|
||||
use tracing::warn;
|
||||
|
||||
/// Creates acceleration structures and binding arrays of resources for raytracing.
|
||||
pub struct RaytracingScenePlugin;
|
||||
|
||||
impl Plugin for RaytracingScenePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
load_shader_library!(app, "raytracing_scene_bindings.wgsl");
|
||||
load_shader_library!(app, "sampling.wgsl");
|
||||
|
||||
app.register_type::<RaytracingMesh3d>();
|
||||
}
|
||||
|
||||
fn finish(&self, app: &mut App) {
|
||||
let render_app = app.sub_app_mut(RenderApp);
|
||||
let render_device = render_app.world().resource::<RenderDevice>();
|
||||
let features = render_device.features();
|
||||
if !features.contains(SolariPlugin::required_wgpu_features()) {
|
||||
warn!(
|
||||
"RaytracingScenePlugin not loaded. GPU lacks support for required features: {:?}.",
|
||||
SolariPlugin::required_wgpu_features().difference(features)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
app.add_plugins(ExtractResourcePlugin::<StandardMaterialAssets>::default());
|
||||
|
||||
let render_app = app.sub_app_mut(RenderApp);
|
||||
|
||||
render_app
|
||||
.world_mut()
|
||||
.resource_mut::<MeshAllocator>()
|
||||
.extra_buffer_usages |= BufferUsages::BLAS_INPUT | BufferUsages::STORAGE;
|
||||
|
||||
render_app
|
||||
.init_resource::<BlasManager>()
|
||||
.init_resource::<StandardMaterialAssets>()
|
||||
.init_resource::<RaytracingSceneBindings>()
|
||||
.add_systems(ExtractSchedule, extract_raytracing_scene)
|
||||
.add_systems(
|
||||
Render,
|
||||
(
|
||||
prepare_raytracing_blas
|
||||
.in_set(RenderSystems::PrepareAssets)
|
||||
.before(prepare_assets::<RenderMesh>)
|
||||
.after(allocate_and_free_meshes),
|
||||
prepare_raytracing_scene_bindings.in_set(RenderSystems::PrepareBindGroups),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
164
crates/bevy_solari/src/scene/raytracing_scene_bindings.wgsl
Normal file
164
crates/bevy_solari/src/scene/raytracing_scene_bindings.wgsl
Normal file
@ -0,0 +1,164 @@
|
||||
#define_import_path bevy_solari::scene_bindings
|
||||
|
||||
struct InstanceGeometryIds {
|
||||
vertex_buffer_id: u32,
|
||||
vertex_buffer_offset: u32,
|
||||
index_buffer_id: u32,
|
||||
index_buffer_offset: u32,
|
||||
}
|
||||
|
||||
struct VertexBuffer { vertices: array<PackedVertex> }
|
||||
|
||||
struct IndexBuffer { indices: array<u32> }
|
||||
|
||||
struct PackedVertex {
|
||||
a: vec4<f32>,
|
||||
b: vec4<f32>,
|
||||
tangent: vec4<f32>,
|
||||
}
|
||||
|
||||
struct Vertex {
|
||||
position: vec3<f32>,
|
||||
normal: vec3<f32>,
|
||||
uv: vec2<f32>,
|
||||
tangent: vec4<f32>,
|
||||
}
|
||||
|
||||
fn unpack_vertex(packed: PackedVertex) -> Vertex {
|
||||
var vertex: Vertex;
|
||||
vertex.position = packed.a.xyz;
|
||||
vertex.normal = vec3(packed.a.w, packed.b.xy);
|
||||
vertex.uv = packed.b.zw;
|
||||
vertex.tangent = packed.tangent;
|
||||
return vertex;
|
||||
}
|
||||
|
||||
struct Material {
|
||||
base_color: vec4<f32>,
|
||||
emissive: vec4<f32>,
|
||||
base_color_texture_id: u32,
|
||||
normal_map_texture_id: u32,
|
||||
emissive_texture_id: u32,
|
||||
_padding: u32,
|
||||
}
|
||||
|
||||
const TEXTURE_MAP_NONE = 0xFFFFFFFFu;
|
||||
|
||||
struct LightSource {
|
||||
kind: u32, // 1 bit for kind, 31 bits for extra data
|
||||
id: u32,
|
||||
}
|
||||
|
||||
const LIGHT_SOURCE_KIND_EMISSIVE_MESH = 0u;
|
||||
const LIGHT_SOURCE_KIND_DIRECTIONAL = 1u;
|
||||
|
||||
struct DirectionalLight {
|
||||
direction_to_light: vec3<f32>,
|
||||
cos_theta_max: f32,
|
||||
luminance: vec3<f32>,
|
||||
inverse_pdf: f32,
|
||||
}
|
||||
|
||||
@group(0) @binding(0) var<storage> vertex_buffers: binding_array<VertexBuffer>;
|
||||
@group(0) @binding(1) var<storage> index_buffers: binding_array<IndexBuffer>;
|
||||
@group(0) @binding(2) var textures: binding_array<texture_2d<f32>>;
|
||||
@group(0) @binding(3) var samplers: binding_array<sampler>;
|
||||
@group(0) @binding(4) var<storage> materials: array<Material>;
|
||||
@group(0) @binding(5) var tlas: acceleration_structure;
|
||||
@group(0) @binding(6) var<storage> transforms: array<mat4x4<f32>>;
|
||||
@group(0) @binding(7) var<storage> geometry_ids: array<InstanceGeometryIds>;
|
||||
@group(0) @binding(8) var<storage> material_ids: array<u32>; // TODO: Store material_id in instance_custom_index instead?
|
||||
@group(0) @binding(9) var<storage> light_sources: array<LightSource>;
|
||||
@group(0) @binding(10) var<storage> directional_lights: array<DirectionalLight>;
|
||||
|
||||
const RAY_T_MIN = 0.0001;
|
||||
const RAY_T_MAX = 100000.0;
|
||||
|
||||
const RAY_NO_CULL = 0xFFu;
|
||||
|
||||
fn trace_ray(ray_origin: vec3<f32>, ray_direction: vec3<f32>, ray_t_min: f32, ray_t_max: f32, ray_flag: u32) -> RayIntersection {
|
||||
let ray = RayDesc(ray_flag, RAY_NO_CULL, ray_t_min, ray_t_max, ray_origin, ray_direction);
|
||||
var rq: ray_query;
|
||||
rayQueryInitialize(&rq, tlas, ray);
|
||||
rayQueryProceed(&rq);
|
||||
return rayQueryGetCommittedIntersection(&rq);
|
||||
}
|
||||
|
||||
fn sample_texture(id: u32, uv: vec2<f32>) -> vec3<f32> {
|
||||
return textureSampleLevel(textures[id], samplers[id], uv, 0.0).rgb; // TODO: Mipmap
|
||||
}
|
||||
|
||||
struct ResolvedMaterial {
|
||||
base_color: vec3<f32>,
|
||||
emissive: vec3<f32>,
|
||||
}
|
||||
|
||||
struct ResolvedRayHitFull {
|
||||
world_position: vec3<f32>,
|
||||
world_normal: vec3<f32>,
|
||||
geometric_world_normal: vec3<f32>,
|
||||
uv: vec2<f32>,
|
||||
triangle_area: f32,
|
||||
material: ResolvedMaterial,
|
||||
}
|
||||
|
||||
fn resolve_material(material: Material, uv: vec2<f32>) -> ResolvedMaterial {
|
||||
var m: ResolvedMaterial;
|
||||
|
||||
m.base_color = material.base_color.rgb;
|
||||
if material.base_color_texture_id != TEXTURE_MAP_NONE {
|
||||
m.base_color *= sample_texture(material.base_color_texture_id, uv);
|
||||
}
|
||||
|
||||
m.emissive = material.emissive.rgb;
|
||||
if material.emissive_texture_id != TEXTURE_MAP_NONE {
|
||||
m.emissive *= sample_texture(material.emissive_texture_id, uv);
|
||||
}
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
fn resolve_ray_hit_full(ray_hit: RayIntersection) -> ResolvedRayHitFull {
|
||||
let barycentrics = vec3(1.0 - ray_hit.barycentrics.x - ray_hit.barycentrics.y, ray_hit.barycentrics);
|
||||
return resolve_triangle_data_full(ray_hit.instance_id, ray_hit.primitive_index, barycentrics);
|
||||
}
|
||||
|
||||
fn resolve_triangle_data_full(instance_id: u32, triangle_id: u32, barycentrics: vec3<f32>) -> ResolvedRayHitFull {
|
||||
let instance_geometry_ids = geometry_ids[instance_id];
|
||||
let material_id = material_ids[instance_id];
|
||||
|
||||
let index_buffer = &index_buffers[instance_geometry_ids.index_buffer_id].indices;
|
||||
let vertex_buffer = &vertex_buffers[instance_geometry_ids.vertex_buffer_id].vertices;
|
||||
let material = materials[material_id];
|
||||
|
||||
let indices_i = (triangle_id * 3u) + vec3(0u, 1u, 2u) + instance_geometry_ids.index_buffer_offset;
|
||||
let indices = vec3((*index_buffer)[indices_i.x], (*index_buffer)[indices_i.y], (*index_buffer)[indices_i.z]) + instance_geometry_ids.vertex_buffer_offset;
|
||||
let vertices = array<Vertex, 3>(unpack_vertex((*vertex_buffer)[indices.x]), unpack_vertex((*vertex_buffer)[indices.y]), unpack_vertex((*vertex_buffer)[indices.z]));
|
||||
|
||||
let transform = transforms[instance_id];
|
||||
let local_position = mat3x3(vertices[0].position, vertices[1].position, vertices[2].position) * barycentrics;
|
||||
let world_position = (transform * vec4(local_position, 1.0)).xyz;
|
||||
|
||||
let uv = mat3x2(vertices[0].uv, vertices[1].uv, vertices[2].uv) * barycentrics;
|
||||
|
||||
let local_normal = mat3x3(vertices[0].normal, vertices[1].normal, vertices[2].normal) * barycentrics; // TODO: Use barycentric lerp, ray_hit.object_to_world, cross product geo normal
|
||||
var world_normal = normalize(mat3x3(transform[0].xyz, transform[1].xyz, transform[2].xyz) * local_normal);
|
||||
let geometric_world_normal = world_normal;
|
||||
if material.normal_map_texture_id != TEXTURE_MAP_NONE {
|
||||
let local_tangent = mat3x3(vertices[0].tangent.xyz, vertices[1].tangent.xyz, vertices[2].tangent.xyz) * barycentrics;
|
||||
let world_tangent = normalize(mat3x3(transform[0].xyz, transform[1].xyz, transform[2].xyz) * local_tangent);
|
||||
let N = world_normal;
|
||||
let T = world_tangent;
|
||||
let B = vertices[0].tangent.w * cross(N, T);
|
||||
let Nt = sample_texture(material.normal_map_texture_id, uv);
|
||||
world_normal = normalize(Nt.x * T + Nt.y * B + Nt.z * N);
|
||||
}
|
||||
|
||||
let triangle_edge0 = vertices[0].position - vertices[1].position;
|
||||
let triangle_edge1 = vertices[0].position - vertices[2].position;
|
||||
let triangle_area = length(cross(triangle_edge0, triangle_edge1)) / 2.0;
|
||||
|
||||
let resolved_material = resolve_material(material, uv);
|
||||
|
||||
return ResolvedRayHitFull(world_position, world_normal, geometric_world_normal, uv, triangle_area, resolved_material);
|
||||
}
|
169
crates/bevy_solari/src/scene/sampling.wgsl
Normal file
169
crates/bevy_solari/src/scene/sampling.wgsl
Normal file
@ -0,0 +1,169 @@
|
||||
#define_import_path bevy_solari::sampling
|
||||
|
||||
#import bevy_pbr::utils::{rand_f, rand_vec2f, rand_range_u}
|
||||
#import bevy_render::maths::PI_2
|
||||
#import bevy_solari::scene_bindings::{trace_ray, RAY_T_MIN, RAY_T_MAX, light_sources, directional_lights, LIGHT_SOURCE_KIND_DIRECTIONAL, resolve_triangle_data_full}
|
||||
|
||||
// https://www.realtimerendering.com/raytracinggems/unofficial_RayTracingGems_v1.9.pdf#0004286901.INDD%3ASec28%3A303
|
||||
fn sample_cosine_hemisphere(normal: vec3<f32>, rng: ptr<function, u32>) -> vec3<f32> {
|
||||
let cos_theta = 1.0 - 2.0 * rand_f(rng);
|
||||
let phi = PI_2 * rand_f(rng);
|
||||
let sin_theta = sqrt(max(1.0 - cos_theta * cos_theta, 0.0));
|
||||
let x = normal.x + sin_theta * cos(phi);
|
||||
let y = normal.y + sin_theta * sin(phi);
|
||||
let z = normal.z + cos_theta;
|
||||
return vec3(x, y, z);
|
||||
}
|
||||
|
||||
fn sample_random_light(ray_origin: vec3<f32>, origin_world_normal: vec3<f32>, rng: ptr<function, u32>) -> vec3<f32> {
|
||||
let light_sample = generate_random_light_sample(rng);
|
||||
let light_contribution = calculate_light_contribution(light_sample, ray_origin, origin_world_normal);
|
||||
let visibility = trace_light_visibility(light_sample, ray_origin);
|
||||
return light_contribution.radiance * visibility * light_contribution.inverse_pdf;
|
||||
}
|
||||
|
||||
struct LightSample {
|
||||
light_id: vec2<u32>,
|
||||
random: vec2<f32>,
|
||||
}
|
||||
|
||||
struct LightContribution {
|
||||
radiance: vec3<f32>,
|
||||
inverse_pdf: f32,
|
||||
}
|
||||
|
||||
fn generate_random_light_sample(rng: ptr<function, u32>) -> LightSample {
|
||||
let light_count = arrayLength(&light_sources);
|
||||
let light_id = rand_range_u(light_count, rng);
|
||||
let random = rand_vec2f(rng);
|
||||
|
||||
let light_source = light_sources[light_id];
|
||||
var triangle_id = 0u;
|
||||
|
||||
if light_source.kind != LIGHT_SOURCE_KIND_DIRECTIONAL {
|
||||
let triangle_count = light_source.kind >> 1u;
|
||||
triangle_id = rand_range_u(triangle_count, rng);
|
||||
}
|
||||
|
||||
return LightSample(vec2(light_id, triangle_id), random);
|
||||
}
|
||||
|
||||
fn calculate_light_contribution(light_sample: LightSample, ray_origin: vec3<f32>, origin_world_normal: vec3<f32>) -> LightContribution {
|
||||
let light_id = light_sample.light_id.x;
|
||||
let light_source = light_sources[light_id];
|
||||
|
||||
var light_contribution: LightContribution;
|
||||
if light_source.kind == LIGHT_SOURCE_KIND_DIRECTIONAL {
|
||||
light_contribution = calculate_directional_light_contribution(light_sample, light_source.id, origin_world_normal);
|
||||
} else {
|
||||
let triangle_count = light_source.kind >> 1u;
|
||||
light_contribution = calculate_emissive_mesh_contribution(light_sample, light_source.id, triangle_count, ray_origin, origin_world_normal);
|
||||
}
|
||||
|
||||
let light_count = arrayLength(&light_sources);
|
||||
light_contribution.inverse_pdf *= f32(light_count);
|
||||
|
||||
return light_contribution;
|
||||
}
|
||||
|
||||
fn calculate_directional_light_contribution(light_sample: LightSample, directional_light_id: u32, origin_world_normal: vec3<f32>) -> LightContribution {
|
||||
let directional_light = directional_lights[directional_light_id];
|
||||
|
||||
// Sample a random direction within a cone whose base is the sun approximated as a disk
|
||||
// https://www.realtimerendering.com/raytracinggems/unofficial_RayTracingGems_v1.9.pdf#0004286901.INDD%3ASec30%3A305
|
||||
let cos_theta = (1.0 - light_sample.random.x) + light_sample.random.x * directional_light.cos_theta_max;
|
||||
let sin_theta = sqrt(1.0 - cos_theta * cos_theta);
|
||||
let phi = light_sample.random.y * PI_2;
|
||||
let x = cos(phi) * sin_theta;
|
||||
let y = sin(phi) * sin_theta;
|
||||
var ray_direction = vec3(x, y, cos_theta);
|
||||
|
||||
// Rotate the ray so that the cone it was sampled from is aligned with the light direction
|
||||
ray_direction = build_orthonormal_basis(directional_light.direction_to_light) * ray_direction;
|
||||
|
||||
let cos_theta_origin = saturate(dot(ray_direction, origin_world_normal));
|
||||
let radiance = directional_light.luminance * cos_theta_origin;
|
||||
|
||||
return LightContribution(radiance, directional_light.inverse_pdf);
|
||||
}
|
||||
|
||||
fn calculate_emissive_mesh_contribution(light_sample: LightSample, instance_id: u32, triangle_count: u32, ray_origin: vec3<f32>, origin_world_normal: vec3<f32>) -> LightContribution {
|
||||
let barycentrics = triangle_barycentrics(light_sample.random);
|
||||
let triangle_id = light_sample.light_id.y;
|
||||
|
||||
let triangle_data = resolve_triangle_data_full(instance_id, triangle_id, barycentrics);
|
||||
|
||||
let light_distance = distance(ray_origin, triangle_data.world_position);
|
||||
let ray_direction = (triangle_data.world_position - ray_origin) / light_distance;
|
||||
let cos_theta_origin = saturate(dot(ray_direction, origin_world_normal));
|
||||
let cos_theta_light = saturate(dot(-ray_direction, triangle_data.world_normal));
|
||||
let light_distance_squared = light_distance * light_distance;
|
||||
|
||||
let radiance = triangle_data.material.emissive.rgb * cos_theta_origin * (cos_theta_light / light_distance_squared);
|
||||
let inverse_pdf = f32(triangle_count) * triangle_data.triangle_area;
|
||||
|
||||
return LightContribution(radiance, inverse_pdf);
|
||||
}
|
||||
|
||||
fn trace_light_visibility(light_sample: LightSample, ray_origin: vec3<f32>) -> f32 {
|
||||
let light_id = light_sample.light_id.x;
|
||||
let light_source = light_sources[light_id];
|
||||
|
||||
if light_source.kind == LIGHT_SOURCE_KIND_DIRECTIONAL {
|
||||
return trace_directional_light_visibility(light_sample, light_source.id, ray_origin);
|
||||
} else {
|
||||
return trace_emissive_mesh_visibility(light_sample, light_source.id, ray_origin);
|
||||
}
|
||||
}
|
||||
|
||||
fn trace_directional_light_visibility(light_sample: LightSample, directional_light_id: u32, ray_origin: vec3<f32>) -> f32 {
|
||||
let directional_light = directional_lights[directional_light_id];
|
||||
|
||||
// Sample a random direction within a cone whose base is the sun approximated as a disk
|
||||
// https://www.realtimerendering.com/raytracinggems/unofficial_RayTracingGems_v1.9.pdf#0004286901.INDD%3ASec30%3A305
|
||||
let cos_theta = (1.0 - light_sample.random.x) + light_sample.random.x * directional_light.cos_theta_max;
|
||||
let sin_theta = sqrt(1.0 - cos_theta * cos_theta);
|
||||
let phi = light_sample.random.y * PI_2;
|
||||
let x = cos(phi) * sin_theta;
|
||||
let y = sin(phi) * sin_theta;
|
||||
var ray_direction = vec3(x, y, cos_theta);
|
||||
|
||||
// Rotate the ray so that the cone it was sampled from is aligned with the light direction
|
||||
ray_direction = build_orthonormal_basis(directional_light.direction_to_light) * ray_direction;
|
||||
|
||||
let ray_hit = trace_ray(ray_origin, ray_direction, RAY_T_MIN, RAY_T_MAX, RAY_FLAG_TERMINATE_ON_FIRST_HIT);
|
||||
return f32(ray_hit.kind == RAY_QUERY_INTERSECTION_NONE);
|
||||
}
|
||||
|
||||
fn trace_emissive_mesh_visibility(light_sample: LightSample, instance_id: u32, ray_origin: vec3<f32>) -> f32 {
|
||||
let barycentrics = triangle_barycentrics(light_sample.random);
|
||||
let triangle_id = light_sample.light_id.y;
|
||||
|
||||
let triangle_data = resolve_triangle_data_full(instance_id, triangle_id, barycentrics);
|
||||
|
||||
let light_distance = distance(ray_origin, triangle_data.world_position);
|
||||
let ray_direction = (triangle_data.world_position - ray_origin) / light_distance;
|
||||
|
||||
let ray_t_max = light_distance - RAY_T_MIN - RAY_T_MIN;
|
||||
if ray_t_max < RAY_T_MIN { return 0.0; }
|
||||
|
||||
let ray_hit = trace_ray(ray_origin, ray_direction, RAY_T_MIN, ray_t_max, RAY_FLAG_TERMINATE_ON_FIRST_HIT);
|
||||
return f32(ray_hit.kind == RAY_QUERY_INTERSECTION_NONE);
|
||||
}
|
||||
|
||||
// https://www.realtimerendering.com/raytracinggems/unofficial_RayTracingGems_v1.9.pdf#0004286901.INDD%3ASec22%3A297
|
||||
fn triangle_barycentrics(random: vec2<f32>) -> vec3<f32> {
|
||||
var barycentrics = random;
|
||||
if barycentrics.x + barycentrics.y > 1.0 { barycentrics = 1.0 - barycentrics; }
|
||||
return vec3(1.0 - barycentrics.x - barycentrics.y, barycentrics);
|
||||
}
|
||||
|
||||
// https://jcgt.org/published/0006/01/01/paper.pdf
|
||||
fn build_orthonormal_basis(normal: vec3<f32>) -> mat3x3<f32> {
|
||||
let sign = select(-1.0, 1.0, normal.z >= 0.0);
|
||||
let a = -1.0 / (sign + normal.z);
|
||||
let b = normal.x * normal.y * a;
|
||||
let tangent = vec3(1.0 + sign * normal.x * normal.x * a, sign * b, -sign * normal.x);
|
||||
let bitangent = vec3(b, sign + normal.y * normal.y * a, -normal.y);
|
||||
return mat3x3(tangent, bitangent, normal);
|
||||
}
|
21
crates/bevy_solari/src/scene/types.rs
Normal file
21
crates/bevy_solari/src/scene/types.rs
Normal file
@ -0,0 +1,21 @@
|
||||
use bevy_asset::Handle;
|
||||
use bevy_derive::{Deref, DerefMut};
|
||||
use bevy_ecs::{component::Component, prelude::ReflectComponent};
|
||||
use bevy_mesh::Mesh;
|
||||
use bevy_pbr::{MeshMaterial3d, StandardMaterial};
|
||||
use bevy_reflect::{prelude::ReflectDefault, Reflect};
|
||||
use bevy_render::sync_world::SyncToRenderWorld;
|
||||
use bevy_transform::components::Transform;
|
||||
use derive_more::derive::From;
|
||||
|
||||
/// A mesh component used for raytracing.
|
||||
///
|
||||
/// The mesh used in this component must have [`bevy_render::mesh::Mesh::enable_raytracing`] set to true,
|
||||
/// use the following set of vertex attributes: `{POSITION, NORMAL, UV_0, TANGENT}`, use [`bevy_render::render_resource::PrimitiveTopology::TriangleList`],
|
||||
/// and use [`bevy_mesh::Indices::U32`].
|
||||
///
|
||||
/// The material used for this entity must be [`MeshMaterial3d<StandardMaterial>`].
|
||||
#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq, From)]
|
||||
#[reflect(Component, Default, Clone, PartialEq)]
|
||||
#[require(MeshMaterial3d<StandardMaterial>, Transform, SyncToRenderWorld)]
|
||||
pub struct RaytracingMesh3d(pub Handle<Mesh>);
|
@ -69,6 +69,7 @@ The default feature set enables most of the expected features of a game engine,
|
||||
|bevy_dev_tools|Provides a collection of developer tools|
|
||||
|bevy_image|Load and access image data. Usually added by an image format|
|
||||
|bevy_remote|Enable the Bevy Remote Protocol|
|
||||
|bevy_solari|Provides raytraced lighting (experimental)|
|
||||
|bevy_ui_debug|Provides a debug overlay for bevy UI|
|
||||
|bmp|BMP image format support|
|
||||
|critical-section|`critical-section` provides the building blocks for synchronization primitives on all platforms, including `no_std`.|
|
||||
|
85
examples/3d/solari.rs
Normal file
85
examples/3d/solari.rs
Normal file
@ -0,0 +1,85 @@
|
||||
//! Demonstrates realtime dynamic global illumination rendering using Bevy Solari.
|
||||
|
||||
#[path = "../helpers/camera_controller.rs"]
|
||||
mod camera_controller;
|
||||
|
||||
use bevy::{
|
||||
prelude::*,
|
||||
render::{camera::CameraMainTextureUsages, mesh::Indices, render_resource::TextureUsages},
|
||||
scene::SceneInstanceReady,
|
||||
solari::{
|
||||
pathtracer::Pathtracer,
|
||||
prelude::{RaytracingMesh3d, SolariPlugin},
|
||||
},
|
||||
};
|
||||
use camera_controller::{CameraController, CameraControllerPlugin};
|
||||
use std::f32::consts::PI;
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins((DefaultPlugins, SolariPlugin, CameraControllerPlugin))
|
||||
.add_systems(Startup, setup)
|
||||
.run();
|
||||
}
|
||||
|
||||
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||||
commands
|
||||
.spawn(SceneRoot(asset_server.load(
|
||||
GltfAssetLabel::Scene(0).from_asset("models/CornellBox/CornellBox.glb"),
|
||||
)))
|
||||
.observe(add_raytracing_meshes_on_scene_load);
|
||||
|
||||
commands.spawn((
|
||||
DirectionalLight {
|
||||
illuminance: light_consts::lux::FULL_DAYLIGHT,
|
||||
shadows_enabled: true,
|
||||
..default()
|
||||
},
|
||||
Transform::from_rotation(Quat::from_euler(EulerRot::XYZ, PI * -0.43, PI * -0.08, 0.0)),
|
||||
));
|
||||
|
||||
commands.spawn((
|
||||
Camera3d::default(),
|
||||
Camera {
|
||||
clear_color: ClearColorConfig::Custom(Color::BLACK),
|
||||
..default()
|
||||
},
|
||||
CameraController {
|
||||
walk_speed: 500.0,
|
||||
run_speed: 1500.0,
|
||||
..Default::default()
|
||||
},
|
||||
Pathtracer::default(),
|
||||
CameraMainTextureUsages::default().with(TextureUsages::STORAGE_BINDING),
|
||||
Transform::from_xyz(-278.0, 273.0, 800.0),
|
||||
));
|
||||
}
|
||||
|
||||
fn add_raytracing_meshes_on_scene_load(
|
||||
trigger: On<SceneInstanceReady>,
|
||||
children: Query<&Children>,
|
||||
mesh: Query<&Mesh3d>,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
// Ensure meshes are bery_solari compatible
|
||||
for (_, mesh) in meshes.iter_mut() {
|
||||
mesh.remove_attribute(Mesh::ATTRIBUTE_UV_1.id);
|
||||
mesh.generate_tangents().unwrap();
|
||||
|
||||
if let Some(indices) = mesh.indices_mut() {
|
||||
if let Indices::U16(u16_indices) = indices {
|
||||
*indices = Indices::U32(u16_indices.iter().map(|i| *i as u32).collect());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for descendant in children.iter_descendants(trigger.target().unwrap()) {
|
||||
if let Ok(mesh) = mesh.get(descendant) {
|
||||
commands
|
||||
.entity(descendant)
|
||||
.insert(RaytracingMesh3d(mesh.0.clone()))
|
||||
.remove::<Mesh3d>();
|
||||
}
|
||||
}
|
||||
}
|
@ -185,6 +185,7 @@ Example | Description
|
||||
[Shadow Biases](../examples/3d/shadow_biases.rs) | Demonstrates how shadow biases affect shadows in a 3d scene
|
||||
[Shadow Caster and Receiver](../examples/3d/shadow_caster_receiver.rs) | Demonstrates how to prevent meshes from casting/receiving shadows in a 3d scene
|
||||
[Skybox](../examples/3d/skybox.rs) | Load a cubemap texture onto a cube like a skybox and cycle through different compressed texture formats.
|
||||
[Solari](../examples/3d/solari.rs) | Demonstrates realtime dynamic global illumination rendering using Bevy Solari.
|
||||
[Specular Tint](../examples/3d/specular_tint.rs) | Demonstrates specular tints and maps
|
||||
[Spherical Area Lights](../examples/3d/spherical_area_lights.rs) | Demonstrates how point light radius values affect light behavior
|
||||
[Split Screen](../examples/3d/split_screen.rs) | Demonstrates how to render two cameras to the same window to accomplish "split screen"
|
||||
|
32
release-content/release-notes/bevy_solari.md
Normal file
32
release-content/release-notes/bevy_solari.md
Normal file
@ -0,0 +1,32 @@
|
||||
---
|
||||
title: Initial raytraced lighting progress (bevy_solari)
|
||||
authors: ["@JMS55"]
|
||||
pull_requests: [19058]
|
||||
---
|
||||
|
||||
(TODO: Embed solari example screenshot here)
|
||||
|
||||
In Bevy 0.17, we've made the first steps towards realtime raytraced lighting in the form of the new bevy_solari crate.
|
||||
|
||||
For some background, lighting in video games can be split into two parts: direct and indirect lighting.
|
||||
|
||||
Direct lighting is light that that is emitted from a light source, bounces off of one surface, and then reaches the camera. Indirect lighting by contrast is light that bounces off of different surfaces many times before reaching the camera, and is often called global illumination.
|
||||
|
||||
(TODO: Diagrams of direct vs indirect light)
|
||||
|
||||
In Bevy, direct lighting comes from analytical light components (`DirectionalLight`, `PointLight`, `SpotLight`) and shadow maps. Indirect lighting comes from a hardcoded `AmbientLight`, baked lighting components (`EnvironmentMapLight`, `IrradianceVolume`, `Lightmap`), and screen-space calculations (`ScreenSpaceAmbientOcclusion`, `ScreenSpaceReflections`, `specular_transmission`, `diffuse_transmission`).
|
||||
|
||||
The problem with these methods is that they all have large downsides:
|
||||
|
||||
* Emissive meshes do not cast light onto other objects, either direct or indirect.
|
||||
* Shadow maps are very expensive to render and consume a lot of memory, so you're limited to using only a few shadow casting lights. Good quality can be difficult to obtain in large scenes.
|
||||
* Baked lighting does not update in realtime as objects and lights move around, is low resolution/quality, and requires time to bake, slowing down game production.
|
||||
* Screen-space methods have low quality and do not capture off-screen geometry and light.
|
||||
|
||||
Bevy Solari is intended as a completely alternate, high-end lighting solution for Bevy that uses GPU-accelerated raytracing to fix all of the above problems. Emissive meshes will properly cast light and shadows, you will be able to have hundreds of shadow casting lights, quality will be much better, it will require no baking time, and it will support _fully_ dynamic scenes!
|
||||
|
||||
While Bevy 0.17 adds the bevy_solari crate, it's intended as a long-term project. Currently there is only a non-realtime path tracer intended as a reference and testbed for developing Bevy Solari. There is nothing usable yet for game developers. However, feel free to run the solari example to see the path tracer in action, and look forwards to more work on Bevy Solari in future releases! (TODO: Is this burying the lede?)
|
||||
|
||||
(TODO: Embed bevy_solari logo here, or somewhere else that looks good)
|
||||
|
||||
Special thanks to @Vecvec for adding raytracing support to wgpu.
|
Loading…
Reference in New Issue
Block a user