diff --git a/CHANGELOG.md b/CHANGELOG.md index 774a51e9c3..ad5a79c74e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added an option for experience number accumulation. - Added an option for damage number rounding (when greater than or equal to 1.0). - Added sliders for incoming/non-incoming damage accumulation duration. +- New ambience sounds +- Slider for ambience volume +- Weather generated on server is sent to clients, and seen on clients as rain/clouds. ### Changed @@ -69,6 +72,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed an issue where the hurt animation would "jump" whenever you lost/gained health. - Fixed a bug where multiple damage sources in the same tick would show up as a singular attack. - Fixed an issue where, if the same amount of healing and damage was received in the same tick, nothing would be shown. +- UI sfx now play from UI instead of from camera (allowing stereo sfx) +- Most sfx now correctly play when camera is underwater +- All sounds now stop upon quitting to main menu + ## [0.12.0] - 2022-02-19 ### Added diff --git a/Cargo.lock b/Cargo.lock index e3b3ec9978..72959cd1f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -654,16 +654,16 @@ dependencies = [ [[package]] name = "clap" -version = "3.1.8" +version = "3.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71c47df61d9e16dc010b55dba1952a57d8c215dbb533fd13cdd13369aac73b1c" +checksum = "3124f3f75ce09e22d1410043e1e24f2ecc44fad3afe4f08408f1f7663d68da2b" dependencies = [ "atty", "bitflags", "clap_derive", + "clap_lex", "indexmap", "lazy_static", - "os_str_bytes", "strsim 0.10.0", "termcolor", "textwrap 0.15.0", @@ -682,6 +682,15 @@ dependencies = [ "syn 1.0.90", ] +[[package]] +name = "clap_lex" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "189ddd3b5d32a70b35e7686054371742a937b0d99128e76dde6340210e966669" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "clipboard-win" version = "3.1.1" @@ -3980,9 +3989,6 @@ name = "os_str_bytes" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" -dependencies = [ - "memchr", -] [[package]] name = "owned_ttf_parser" @@ -6362,7 +6368,7 @@ dependencies = [ "async-channel", "authc", "byteorder", - "clap 3.1.8", + "clap 3.1.10", "hashbrown 0.11.2", "image", "num 0.4.0", @@ -6551,7 +6557,7 @@ dependencies = [ "bincode", "bitflags", "bytes", - "clap 3.1.8", + "clap 3.1.10", "criterion", "crossbeam-channel", "futures-core", @@ -6635,6 +6641,7 @@ dependencies = [ "humantime", "itertools", "lazy_static", + "noise", "num_cpus", "portpicker", "prometheus", @@ -6673,7 +6680,7 @@ name = "veloren-server-cli" version = "0.12.0" dependencies = [ "ansi-parser", - "clap 3.1.8", + "clap 3.1.10", "crossterm 0.23.2", "lazy_static", "mimalloc", @@ -6824,7 +6831,7 @@ dependencies = [ name = "veloren-voxygen-i18n" version = "0.10.0" dependencies = [ - "clap 3.1.8", + "clap 3.1.10", "deunicode", "git2", "hashbrown 0.11.2", @@ -6841,7 +6848,7 @@ dependencies = [ "arr_macro", "bincode", "bitvec", - "clap 3.1.8", + "clap 3.1.10", "criterion", "csv", "deflate", diff --git a/assets/voxygen/audio/ambience/leaves.ogg b/assets/voxygen/audio/ambience/leaves.ogg new file mode 100644 index 0000000000..22276e12f7 --- /dev/null +++ b/assets/voxygen/audio/ambience/leaves.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:30a6d9bc0e63f7216eaaddef5d9ada465f241b2f1206b522dd388a552dba5708 +size 461250 diff --git a/assets/voxygen/audio/ambience/rain.ogg b/assets/voxygen/audio/ambience/rain.ogg new file mode 100644 index 0000000000..f8d3570d79 --- /dev/null +++ b/assets/voxygen/audio/ambience/rain.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8a6062ad150d11cd40439f3e87881448069092b9f26a1ef3a60cb710a5b5320 +size 328599 diff --git a/assets/voxygen/audio/ambience/thunder.ogg b/assets/voxygen/audio/ambience/thunder.ogg new file mode 100644 index 0000000000..3a214838cb --- /dev/null +++ b/assets/voxygen/audio/ambience/thunder.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c1d9844b1eefb7ae0d3fdeea6407ed1c2da54fc7e4b89f2d57db38e16f17d3d +size 269156 diff --git a/assets/voxygen/audio/ambient/wind.ogg b/assets/voxygen/audio/ambience/wind.ogg similarity index 100% rename from assets/voxygen/audio/ambient/wind.ogg rename to assets/voxygen/audio/ambience/wind.ogg diff --git a/assets/voxygen/audio/ambient.ron b/assets/voxygen/audio/ambient.ron index ddc2af2fbb..a1fb6a9d48 100644 --- a/assets/voxygen/audio/ambient.ron +++ b/assets/voxygen/audio/ambient.ron @@ -1,9 +1,24 @@ ( tracks: [ ( - path: "voxygen.audio.ambient.wind", - length: 14.2, + path: "voxygen.audio.ambience.wind", + length: 14.203, tag: Wind, - ), + ), + ( + path: "voxygen.audio.ambience.rain", + length: 17.0, + tag: Rain, + ), + ( + path:"voxygen.audio.ambience.thunder", + length: 32.0, + tag: Thunder, + ), + ( + path:"voxygen.audio.ambience.leaves", + length: 26.0, + tag: Leaves, + ), ] ) diff --git a/assets/voxygen/audio/sfx.ron b/assets/voxygen/audio/sfx.ron index 4e89f79b85..ecff170504 100644 --- a/assets/voxygen/audio/sfx.ron +++ b/assets/voxygen/audio/sfx.ron @@ -155,7 +155,7 @@ "voxygen.audio.sfx.footsteps.stepgrass_5", "voxygen.audio.sfx.footsteps.stepgrass_6", ], - threshold: 1.6, + threshold: 1.8, ), QuadRun(Grass): ( files: [ @@ -166,7 +166,7 @@ "voxygen.audio.sfx.footsteps.stepgrass_5", "voxygen.audio.sfx.footsteps.stepgrass_6", ], - threshold: 0.8, + threshold: 0.9, ), // For when sand 1) exists and 2) has unique sounds // Run(Sand): ( @@ -195,7 +195,7 @@ "voxygen.audio.sfx.footsteps.snow_step_2", "voxygen.audio.sfx.footsteps.snow_step_3", ], - threshold: 1.6, + threshold: 1.8, ), QuadRun(Snow): ( files: [ @@ -203,7 +203,7 @@ "voxygen.audio.sfx.footsteps.snow_step_2", "voxygen.audio.sfx.footsteps.snow_step_3", ], - threshold: 0.8, + threshold: 0.9, ), Run(Rock): ( files: [ @@ -220,7 +220,7 @@ "voxygen.audio.sfx.footsteps.stone_step_11", "voxygen.audio.sfx.footsteps.stone_step_12", ], - threshold: 1.6, + threshold: 1.8, ), QuadRun(Rock): ( files: [ @@ -237,39 +237,18 @@ "voxygen.audio.sfx.footsteps.stone_step_11", "voxygen.audio.sfx.footsteps.stone_step_12", ], - threshold: 0.8, + threshold: 0.9, ), - //ExperienceGained: ( - // files: [ - // "voxygen.audio.sfx.character.experience_gained_1", - // "voxygen.audio.sfx.character.experience_gained_2", - // "voxygen.audio.sfx.character.experience_gained_3", - // ], - // threshold: 0.5, - //), - // unused for now - // Jump: ( - // files: [ - // "voxygen.audio.sfx.utterance.humanmale_hurt1" - // ], - // threshold: 0.25, - // ), - //Fall: ( - // files: [ - // // Event not implemented? - // ], - // threshold: 0.25, - //), Roll: ( files: [ "voxygen.audio.sfx.character.dive_roll_1", "voxygen.audio.sfx.character.dive_roll_2", ], - threshold: 0.25, + threshold: 0.3, ), Climb: ( files: [ - // TODO: sync with animation + // TODO: sync with animation, make actual sfx "voxygen.audio.sfx.footsteps.stepdirt_1", "voxygen.audio.sfx.footsteps.stepdirt_2", "voxygen.audio.sfx.footsteps.stepdirt_3", @@ -641,6 +620,12 @@ ], threshold: 0.3, ), + Inventory(Craft): ( + files: [ + "voxygen.audio.sfx.crafting.hammer", + ], + threshold: 0.05, + ), // // Consumables diff --git a/assets/voxygen/audio/sfx/ambient/bees_1.ogg b/assets/voxygen/audio/sfx/ambient/bees_1.ogg index 57738d6ca8..00511a8725 100644 --- a/assets/voxygen/audio/sfx/ambient/bees_1.ogg +++ b/assets/voxygen/audio/sfx/ambient/bees_1.ogg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1eee9c850c3fba96e607284abac60c52ec7703a31fc42e0f97c367df1a12d168 -size 174456 +oid sha256:51e3b36a0ddded060a009ae2c0e0f282d1bcc3826019573de5d68a10847f2335 +size 137186 diff --git a/assets/voxygen/audio/sfx/ambient/birdcall_1.ogg b/assets/voxygen/audio/sfx/ambient/birdcall_1.ogg index 2d8abedaf2..06ceea2840 100644 --- a/assets/voxygen/audio/sfx/ambient/birdcall_1.ogg +++ b/assets/voxygen/audio/sfx/ambient/birdcall_1.ogg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:673504d4e6e1ccf272782b8159baedabf7fde73d7c006cccaf0beaedc5d314fa -size 84946 +oid sha256:b8e7061dca633f53c30cc91d18b266d46c53504926ad3168ace1c673de444cef +size 66488 diff --git a/assets/voxygen/audio/sfx/ambient/birdcall_2.ogg b/assets/voxygen/audio/sfx/ambient/birdcall_2.ogg index a9a5a7f47b..e1e6cbfb50 100644 --- a/assets/voxygen/audio/sfx/ambient/birdcall_2.ogg +++ b/assets/voxygen/audio/sfx/ambient/birdcall_2.ogg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cedcdff91a632c439467420293b2f22f18399268f7ec08b76f81c34fdddaab29 -size 137561 +oid sha256:9a305641f3eaa45b8fadd4160e6a677bfd9bf0a5950c79e62bbc0d4bb72b4fa6 +size 107298 diff --git a/assets/voxygen/audio/sfx/ambient/frog_croak_1.ogg b/assets/voxygen/audio/sfx/ambient/frog_croak_1.ogg index e6fb9f3650..cdb7fee5d8 100644 --- a/assets/voxygen/audio/sfx/ambient/frog_croak_1.ogg +++ b/assets/voxygen/audio/sfx/ambient/frog_croak_1.ogg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cfc457273f6a969f064ee8a149ea2b1ca5f2cd80c4747bb4a65a2b1b6cb7c439 -size 9212 +oid sha256:1f77f03cfdde09602f6cfe676c7e9a12ae42feff41fc548d4ac7ac880312ca75 +size 7409 diff --git a/assets/voxygen/audio/sfx/character/dive_roll_1.ogg b/assets/voxygen/audio/sfx/character/dive_roll_1.ogg index ccf1cec114..6295dcdb16 100644 --- a/assets/voxygen/audio/sfx/character/dive_roll_1.ogg +++ b/assets/voxygen/audio/sfx/character/dive_roll_1.ogg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a3f7f26a385ede32dc79744078967caf3449b8623785729c1f186f78d9399d3 -size 16489 +oid sha256:3b0a80cfe9688a00b5690ffc79746fe6b47de1cadf053b8af509ed6e28ba82ee +size 8989 diff --git a/assets/voxygen/audio/sfx/character/dive_roll_2.ogg b/assets/voxygen/audio/sfx/character/dive_roll_2.ogg index 3b05cabb0e..356487fb28 100644 --- a/assets/voxygen/audio/sfx/character/dive_roll_2.ogg +++ b/assets/voxygen/audio/sfx/character/dive_roll_2.ogg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7638f3a2053e3cadc7551bbe105eb1fb9afb934e4d71d551064e5d740c411df2 -size 20611 +oid sha256:b3107b18d513c69bb794bad320b36e02f8dcf80b2f4eec1704de553861a38cce +size 9105 diff --git a/assets/voxygen/audio/sfx/crafting/hammer.ogg b/assets/voxygen/audio/sfx/crafting/hammer.ogg index 231793d411..0000a6454b 100644 --- a/assets/voxygen/audio/sfx/crafting/hammer.ogg +++ b/assets/voxygen/audio/sfx/crafting/hammer.ogg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ad18acbbf98dcb1f6b0dcbb37c5bb97331d1eec6f0ea3e1b68c6aacf3a629afb -size 11280 +oid sha256:4f3936067b4e070911420ff038f6784177c1682c0f0d4da29ddf6b734a3b7a14 +size 8707 diff --git a/assets/voxygen/audio/sfx/footsteps/snow_step_1.ogg b/assets/voxygen/audio/sfx/footsteps/snow_step_1.ogg index 3eb0d491ac..625097be82 100644 --- a/assets/voxygen/audio/sfx/footsteps/snow_step_1.ogg +++ b/assets/voxygen/audio/sfx/footsteps/snow_step_1.ogg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c6aeabd16949b87a43d194d08f6aedd173103ce8535cd930029f280d6b00c841 -size 12849 +oid sha256:7586e83188dbb014be78922eec4c3418d59924516bde0c67a8b3031a6740b320 +size 13332 diff --git a/assets/voxygen/audio/sfx/footsteps/snow_step_2.ogg b/assets/voxygen/audio/sfx/footsteps/snow_step_2.ogg index ab72e4cb88..85474aaaf6 100644 --- a/assets/voxygen/audio/sfx/footsteps/snow_step_2.ogg +++ b/assets/voxygen/audio/sfx/footsteps/snow_step_2.ogg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9810dca5425173b196a794f46ed2f8a0e707c636290ef6cc84ca7686761cd924 -size 12297 +oid sha256:33ef3179c67c9c2f27f9b509fcfe21ad8586d9f0eb4b9a29e3046daf0940448c +size 12950 diff --git a/assets/voxygen/audio/sfx/footsteps/snow_step_3.ogg b/assets/voxygen/audio/sfx/footsteps/snow_step_3.ogg index 9e63e16562..cd7182040e 100644 --- a/assets/voxygen/audio/sfx/footsteps/snow_step_3.ogg +++ b/assets/voxygen/audio/sfx/footsteps/snow_step_3.ogg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:07d11684646b9942e6fceb342a6b26e66b704a9072c2ca127dd2f896ff196fd3 -size 10593 +oid sha256:26e6212bb82f1a7f5795944a55ba71e317c8bd0124bdec99ed11d466612b4da4 +size 11012 diff --git a/assets/voxygen/audio/soundtrack.ron b/assets/voxygen/audio/soundtrack.ron index 7e81ee0614..26efe80a21 100644 --- a/assets/voxygen/audio/soundtrack.ron +++ b/assets/voxygen/audio/soundtrack.ron @@ -1,4 +1,3 @@ -// TODO: Add an ambient-soundtrack that runs independently from the musical soundtrack // Times: Some(Day), Some(Night), None [both] // Biomes: Grassland, Forest, Desert, Snowland, Lake, Mountain, Ocean, Jungle, Savannah, Taiga // planned biomes: Swamp @@ -122,6 +121,7 @@ path: "voxygen.audio.soundtrack.town.rest_assured", length: 189.0, timing: Some(Day), + weather: None, biomes: [], site: Some(Settlement), music_state: Activity(Explore), @@ -132,6 +132,7 @@ path: "voxygen.audio.soundtrack.town.im_home", length: 125.0, timing: Some(Night), + weather: None, biomes: [], site: Some(Settlement), music_state: Activity(Explore), @@ -142,6 +143,7 @@ path: "voxygen.audio.soundtrack.dungeon.dank_dungeon", length: 130.0, timing: None, + weather: None, biomes: [], site: Some(Dungeon), music_state: Activity(Explore), @@ -152,6 +154,7 @@ path: "voxygen.audio.soundtrack.overworld.calming_hills", length: 101.0, timing: Some(Day), + weather: None, biomes: [ (Mountain, 1), ], @@ -164,6 +167,7 @@ path: "voxygen.audio.soundtrack.town.fiesta_del_pueblo", length: 183.0, timing: Some(Day), + weather: None, biomes: [ (Desert, 1) ], @@ -176,6 +180,7 @@ path: "voxygen.audio.soundtrack.dungeon.ruination", length: 135.0, timing: None, + weather: None, biomes: [], site: Some(Dungeon), music_state: Activity(Explore), @@ -186,6 +191,7 @@ path: "voxygen.audio.soundtrack.cave.saturated_hallows", length: 227.0, timing: None, + weather: None, biomes: [], site: Some(Cave), music_state: Activity(Explore), @@ -196,6 +202,7 @@ path: "voxygen.audio.soundtrack.dungeon.vast_onslaught", length: 237.0, timing: None, + weather: None, biomes: [], site: Some(Dungeon), music_state: Activity(Explore), @@ -206,6 +213,7 @@ path: "voxygen.audio.soundtrack.dungeon.sacred_temple", length: 75.0, timing: None, + weather: None, biomes: [], site: Some(Dungeon), music_state: Activity(Explore), @@ -216,6 +224,7 @@ path: "voxygen.audio.soundtrack.overworld.true_nature", length: 169.0, timing: Some(Day), + weather: None, biomes: [ (Forest, 2), ], @@ -228,6 +237,7 @@ path: "voxygen.audio.soundtrack.overworld.jungle_ambient", length: 218.0, timing: Some(Day), + weather: None, biomes: [ (Jungle, 1), ], @@ -240,6 +250,7 @@ path: "voxygen.audio.soundtrack.overworld.ethereal_bonds", length: 59.0, timing: Some(Night), + weather: None, biomes: [ (Mountain, 1), ], @@ -252,6 +263,7 @@ path: "voxygen.audio.soundtrack.overworld.leap_of_faith", length: 269.0, timing: Some(Night), + weather: None, biomes: [ (Ocean, 1), (Lake, 1), @@ -265,6 +277,7 @@ path: "voxygen.audio.soundtrack.overworld.highland_of_the_hawk", length: 283.0, timing: Some(Day), + weather: None, biomes: [ (Desert, 1), (Savannah, 1), @@ -278,6 +291,7 @@ path: "voxygen.audio.soundtrack.overworld.verdant_glades", length: 97.0, timing: Some(Day), + weather: None, biomes: [ (Grassland, 1), ], @@ -290,6 +304,7 @@ path: "voxygen.audio.soundtrack.overworld.calling_wild", length: 160.0, timing: Some(Night), + weather: None, biomes: [ (Grassland, 1), (Savannah, 1), @@ -303,6 +318,7 @@ path: "voxygen.audio.soundtrack.overworld.drifting_along", length: 164.0, timing: None, + weather: None, biomes: [ (Lake, 1), (Ocean, 1), @@ -316,6 +332,7 @@ path: "voxygen.audio.soundtrack.overworld.winter_falls", length: 215.0, timing: Some(Day), + weather: None, biomes: [ (Snowland, 1), (Taiga, 1), @@ -329,6 +346,7 @@ path: "voxygen.audio.soundtrack.overworld.short_meandering", length: 147.0, timing: Some(Night), + weather: None, biomes: [ (Desert, 1), (Mountain, 1), @@ -342,6 +360,7 @@ path: "voxygen.audio.soundtrack.overworld.oceania", length: 135.0, timing: None, + weather: None, biomes: [ (Lake, 1), (Ocean, 1), @@ -355,6 +374,7 @@ path: "voxygen.audio.soundtrack.overworld.a_solemn_quest", length: 206.0, timing: Some(Night), + weather: None, biomes: [ (Forest, 2), ], @@ -367,6 +387,7 @@ path: "voxygen.audio.soundtrack.overworld.into_the_dark_forest", length: 184.0, timing: Some(Night), + weather: None, biomes: [ (Forest, 2), (Jungle, 1), @@ -380,6 +401,7 @@ path: "voxygen.audio.soundtrack.overworld.field_grazing", length: 154.0, timing: Some(Day), + weather: None, biomes: [ (Grassland, 1), (Forest, 2), @@ -393,6 +415,7 @@ path: "voxygen.audio.soundtrack.overworld.wandering_voices", length: 137.0, timing: Some(Night), + weather: None, biomes: [ (Grassland, 1), ], @@ -405,6 +428,7 @@ path: "voxygen.audio.soundtrack.overworld.snowtop_volume", length: 89.0, timing: Some(Day), + weather: None, biomes: [ (Snowland, 1), (Taiga, 1), @@ -418,6 +442,7 @@ path: "voxygen.audio.soundtrack.cave.mineral_deposits", length: 148.0, timing: None, + weather: None, biomes: [], site: Some(Cave), music_state: Activity(Explore), @@ -428,6 +453,7 @@ path: "voxygen.audio.soundtrack.overworld.moonbeams", length: 158.0, timing: Some(Night), + weather: None, biomes: [ (Snowland, 1), (Taiga, 1), @@ -441,6 +467,7 @@ path: "voxygen.audio.soundtrack.overworld.serene_meadows", length: 173.0, timing: Some(Night), + weather: None, biomes: [ (Grassland, 1), ], @@ -453,6 +480,7 @@ path: "voxygen.audio.soundtrack.overworld.just_the_beginning", length: 188.0, timing: Some(Day), + weather: None, biomes: [ (Grassland, 1), ], @@ -465,6 +493,7 @@ path: "voxygen.audio.soundtrack.overworld.campfire_stories", length: 100.0, timing: Some(Night), + weather: None, biomes: [ (Forest, 2), ], @@ -477,6 +506,7 @@ path: "voxygen.audio.soundtrack.overworld.limits", length: 203.0, timing: Some(Day), + weather: None, biomes: [ (Mountain, 1), ], @@ -489,6 +519,7 @@ path: "voxygen.audio.soundtrack.dungeon.down_the_rabbit_hole", length: 244.0, timing: None, + weather: None, biomes: [], site: Some(Cave), music_state: Activity(Explore), @@ -499,13 +530,36 @@ path: "voxygen.audio.soundtrack.overworld.between_the_fairies", length: 175.0, timing: Some(Day), + weather: None, biomes: [ (Forest, 2), ], site: Some(Void), music_state: Activity(Explore), artist: "badbbad", - )), + )), + Individual(( + title: "The Heavens Weep", + path: "voxygen.audio.soundtrack.overworld.the_heavens_weep", + length: 209.0, + timing: None, + weather: Some(Rain), + biomes: [], + site: None, + music_state: Activity(Explore), + artist: "Oolnokk", + )), + Individual(( + title: "A Heroes Sorrow", + path: "voxygen.audio.soundtrack.overworld.a_heroes_sorrow", + length: 251.0, + timing: None, + weather: Some(Rain), + biomes: [], + site: None, + music_state: Activity(Explore), + artist: "Oolnokk", + )), // Combat Music @@ -513,6 +567,7 @@ title: "Barred Paths", author: "DaforLynx", timing: None, + weather: None, biomes: [], site: Some(Dungeon), segments: [ diff --git a/assets/voxygen/audio/soundtrack/overworld/a_heros_sorrow.ogg b/assets/voxygen/audio/soundtrack/overworld/a_heros_sorrow.ogg new file mode 100644 index 0000000000..bb1a8bae6e --- /dev/null +++ b/assets/voxygen/audio/soundtrack/overworld/a_heros_sorrow.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b288c7e767b064b7df6b25165141c0d49f4c5db1e312f8537791f28bea034a42 +size 6734031 diff --git a/assets/voxygen/audio/soundtrack/overworld/the_heavens_weep.ogg b/assets/voxygen/audio/soundtrack/overworld/the_heavens_weep.ogg new file mode 100644 index 0000000000..fe2d956f92 --- /dev/null +++ b/assets/voxygen/audio/soundtrack/overworld/the_heavens_weep.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8cb54b3992d09b489701c9d55b25eae1ad0dc5b7648e849aed0768da01fecac0 +size 2993026 diff --git a/assets/voxygen/i18n/en/hud/settings.ron b/assets/voxygen/i18n/en/hud/settings.ron index 631bff9d65..93d1656faa 100644 --- a/assets/voxygen/i18n/en/hud/settings.ron +++ b/assets/voxygen/i18n/en/hud/settings.ron @@ -106,6 +106,7 @@ "hud.settings.shadow_rendering_mode.cheap": "Cheap", "hud.settings.shadow_rendering_mode.map": "Map", "hud.settings.shadow_rendering_mode.map.resolution": "Resolution", + "hud.settings.rain_occlusion.resolution": "Rain Occlusion Resolution", "hud.settings.lod_detail": "LoD Detail", "hud.settings.save_window_size": "Save window size", "hud.settings.reset_graphics": "Reset to Defaults", @@ -116,6 +117,7 @@ "hud.settings.inactive_master_volume_perc": "Inactive Window Volume", "hud.settings.music_volume": "Music Volume", "hud.settings.sound_effect_volume": "Sound Effects Volume", + "hud.settings.ambience_volume": "Ambience Volume", "hud.settings.audio_device": "Audio Device", "hud.settings.reset_sound": "Reset to Defaults", diff --git a/assets/voxygen/shaders/clouds-frag.glsl b/assets/voxygen/shaders/clouds-frag.glsl index 057dabe157..01faedb424 100644 --- a/assets/voxygen/shaders/clouds-frag.glsl +++ b/assets/voxygen/shaders/clouds-frag.glsl @@ -28,31 +28,29 @@ // This *MUST* come after `cloud.glsl`: it contains a function that depends on `cloud.glsl` when clouds are enabled #include -layout(set = 1, binding = 0) +layout(set = 2, binding = 0) uniform texture2D t_src_color; -layout(set = 1, binding = 1) +layout(set = 2, binding = 1) uniform sampler s_src_color; -layout(set = 1, binding = 2) +layout(set = 2, binding = 2) uniform texture2D t_src_depth; -layout(set = 1, binding = 3) +layout(set = 2, binding = 3) uniform sampler s_src_depth; layout(location = 0) in vec2 uv; -layout (std140, set = 1, binding = 4) +layout (std140, set = 2, binding = 4) uniform u_locals { - mat4 proj_mat_inv; - mat4 view_mat_inv; + mat4 all_mat_inv; }; layout(location = 0) out vec4 tgt_color; vec3 wpos_at(vec2 uv) { float buf_depth = texture(sampler2D(t_src_depth, s_src_depth), uv).x; - mat4 inv = view_mat_inv * proj_mat_inv;//inverse(all_mat); vec4 clip_space = vec4((uv * 2.0 - 1.0) * vec2(1, -1), buf_depth, 1.0); - vec4 view_space = inv * clip_space; + vec4 view_space = all_mat_inv * clip_space; view_space /= view_space.w; if (buf_depth == 0.0) { vec3 direction = normalize(view_space.xyz); @@ -84,6 +82,76 @@ void main() { #if (CLOUD_MODE == CLOUD_MODE_NONE) color.rgb = apply_point_glow(cam_pos.xyz + focus_off.xyz, dir, dist, color.rgb); + #else + vec3 old_color = color.rgb; + + // normalized direction from the camera position to the fragment in world, transformed by the relative rain direction + vec3 adjusted_dir = (vec4(dir, 0) * rain_dir_mat).xyz; + + // stretch z values as they move away from 0 + float z = (-1 / (abs(adjusted_dir.z) - 1) - 1) * sign(adjusted_dir.z); + // normalize xy to get a 2d direction + vec2 dir_2d = normalize(adjusted_dir.xy); + // sort of map cylinder around the camera to 2d grid + vec2 view_pos = vec2(atan2(dir_2d.x, dir_2d.y), z); + + // compute camera position in the world + vec3 cam_wpos = cam_pos.xyz + focus_off.xyz; + + // Rain density is now only based on the cameras current position. + // This could be affected by a setting where rain_density_at is instead + // called each iteration of the loop. With the current implementation + // of rain_dir this has issues with being in a place where it doesn't rain + // and seeing rain. + float rain_density = rain_density * 1.0; + if (medium.x == MEDIUM_AIR && rain_density > 0.0) { + float rain_dist = 50.0; + #if (CLOUD_MODE <= CLOUD_MODE_LOW) + const int iterations = 2; + #else + const int iterations = 4; + #endif + + for (int i = 0; i < iterations; i ++) { + float old_rain_dist = rain_dist; + rain_dist *= 0.3 / 4.0 * iterations; + + vec2 drop_density = vec2(30, 1); + + vec2 rain_pos = (view_pos * rain_dist); + rain_pos.y += integrated_rain_vel; + + vec2 cell = floor(rain_pos * drop_density) / drop_density; + + float drop_depth = mix( + old_rain_dist, + rain_dist, + fract(hash(fract(vec4(cell, rain_dist, 0) * 0.1))) + ); + + float dist_to_rain = drop_depth / length(dir.xy); + vec3 rpos = dir * dist_to_rain; + if (dist < dist_to_rain || cam_wpos.z + rpos.z > CLOUD_AVG_ALT) { + continue; + } + + if (dot(rpos * vec3(1, 1, 0.5), rpos) < 1.0) { + break; + } + float rain_density = 10.0 * rain_density * floor(rain_occlusion_at(cam_pos.xyz + rpos.xyz)); + + if (rain_density < 0.001 || fract(hash(fract(vec4(cell, rain_dist, 0) * 0.01))) > rain_density) { + continue; + } + vec2 near_drop = cell + (vec2(0.5) + (vec2(hash(vec4(cell, 0, 0)), 0.5) - 0.5) * vec2(2, 0)) / drop_density; + + vec2 drop_size = vec2(0.0008, 0.03); + float avg_alpha = (drop_size.x * drop_size.y) * 10 / 1; + float alpha = sign(max(1 - length((rain_pos - near_drop) / drop_size * 0.1), 0)); + float light = sqrt(dot(old_color, vec3(1))) + (get_sun_brightness() + get_moon_brightness()) * 0.01; + color.rgb = mix(color.rgb, vec3(0.3, 0.4, 0.5) * light, mix(avg_alpha, alpha, min(1000 / dist_to_rain, 1)) * 0.25); + } + } #endif tgt_color = vec4(color.rgb, 1); diff --git a/assets/voxygen/shaders/fluid-frag/shiny.glsl b/assets/voxygen/shaders/fluid-frag/shiny.glsl index e07f8cc92d..413e9809ea 100644 --- a/assets/voxygen/shaders/fluid-frag/shiny.glsl +++ b/assets/voxygen/shaders/fluid-frag/shiny.glsl @@ -103,7 +103,8 @@ void main() { uint norm_dir = ((f_pos_norm >> 29) & 0x1u) * 3u; // Use an array to avoid conditional branching // Temporarily assume all water faces up (this is incorrect but looks better) - vec3 f_norm = vec3(0, 0, 1);//normals[norm_axis + norm_dir]; + vec3 surf_norm = normals[norm_axis + norm_dir]; + vec3 f_norm = vec3(0, 0, 1);//surf_norm; vec3 cam_to_frag = normalize(f_pos - cam_pos.xyz); // vec4 light_pos[2]; @@ -131,10 +132,11 @@ void main() { } vec3 c_norm = cross(f_norm, b_norm); - vec3 wave_pos = f_pos + focus_off.xyz; + vec3 wave_pos = mod(f_pos + focus_off.xyz, vec3(100.0)); + float wave_sample_dist = 0.025; float wave00 = wave_height(wave_pos); - float wave10 = wave_height(wave_pos + vec3(0.1, 0, 0)); - float wave01 = wave_height(wave_pos + vec3(0, 0.1, 0)); + float wave10 = wave_height(wave_pos + vec3(wave_sample_dist, 0, 0)); + float wave01 = wave_height(wave_pos + vec3(0, wave_sample_dist, 0)); // Possibility of div by zero when slope = 0, // however this only results in no water surface appearing @@ -142,11 +144,35 @@ void main() { float slope = abs((wave00 - wave10) * (wave00 - wave01)) + 0.001; vec3 nmap = vec3( - -(wave10 - wave00) / 0.1, - -(wave01 - wave00) / 0.1, - 0.1 / slope + -(wave10 - wave00) / wave_sample_dist, + -(wave01 - wave00) / wave_sample_dist, + wave_sample_dist / slope ); + #if (CLOUD_MODE != CLOUD_MODE_NONE) + if (rain_density > 0 && surf_norm.z > 0.5) { + vec3 drop_density = vec3(2, 2, 2); + vec3 drop_pos = wave_pos + vec3(0, 0, -time_of_day.x * 0.025); + vec2 cell2d = floor(drop_pos.xy * drop_density.xy); + drop_pos.z += noise_2d(cell2d * 13.1) * 10; + drop_pos.z *= 0.5 + hash_fast(uvec3(cell2d, 0)); + vec3 cell = vec3(cell2d, floor(drop_pos.z * drop_density.z)); + + if (fract(hash(fract(vec4(cell, 0) * 0.01))) < rain_density * rain_occlusion_at(f_pos.xyz) * 50.0) { + vec3 off = vec3(hash_fast(uvec3(cell * 13)), hash_fast(uvec3(cell * 5)), 0); + vec3 near_cell = (cell + 0.5 + (off - 0.5) * 0.5) / drop_density; + + float dist = length((drop_pos - near_cell) / vec3(1, 1, 2)); + float drop_rad = 0.125; + nmap.xy += (drop_pos - near_cell).xy + * max(1.0 - abs(dist - drop_rad) * 50, 0) + * 2500 + * sign(dist - drop_rad) + * max(drop_pos.z - near_cell.z, 0); + } + } + #endif + nmap = mix(f_norm, normalize(nmap), min(1.0 / pow(frag_dist, 0.75), 1)); //float suppress_waves = max(dot(), 0); diff --git a/assets/voxygen/shaders/include/cloud/regular.glsl b/assets/voxygen/shaders/include/cloud/regular.glsl index cddf83003e..ddb4a2f552 100644 --- a/assets/voxygen/shaders/include/cloud/regular.glsl +++ b/assets/voxygen/shaders/include/cloud/regular.glsl @@ -61,13 +61,16 @@ vec4 cloud_at(vec3 pos, float dist, out vec3 emission, out float not_underground ; } + float cloud_alt = alt + 1800; + //vec2 cloud_attr = get_cloud_heights(wind_pos.xy); float sun_access = 0.0; float moon_access = 0.0; - float cloud_sun_access = 0.0; + float cloud_sun_access = clamp((pos.z - cloud_alt) / 1500 + 0.5, 0, 1); float cloud_moon_access = 0.0; float cloud_broad_a = 0.0; float cloud_broad_b = 0.0; + // This is a silly optimisation but it actually nets us a fair few fps by skipping quite a few expensive calcs if ((pos.z < CLOUD_AVG_ALT + 15000.0 && cloud_tendency > 0.0)) { // Turbulence (small variations in clouds/mist) @@ -78,11 +81,10 @@ vec4 cloud_at(vec3 pos, float dist, out vec3 emission, out float not_underground const float CLOUD_DENSITY = 10000.0; const float CLOUD_ALT_VARI_WIDTH = 100000.0; const float CLOUD_ALT_VARI_SCALE = 5000.0; - float cloud_alt = CLOUD_AVG_ALT + alt * 0.5; cloud_broad_a = cloud_broad(wind_pos + sun_dir.xyz * 250); cloud_broad_b = cloud_broad(wind_pos - sun_dir.xyz * 250); - cloud = cloud_tendency + (0.0 + cloud = cloud_tendency + cloud_tendency * (0.0 + 24 * (cloud_broad_a + cloud_broad_b) * 0.5 #if (CLOUD_MODE >= CLOUD_MODE_MINIMAL) + 4 * (noise_3d((wind_pos + turb_offset) / 2000.0 / cloud_scale) - 0.5) @@ -93,24 +95,30 @@ vec4 cloud_at(vec3 pos, float dist, out vec3 emission, out float not_underground #if (CLOUD_MODE >= CLOUD_MODE_HIGH) + 0.75 * (noise_3d(wind_pos / 500.0 / cloud_scale) - 0.5) #endif - ) * 0.01; + ) * 0.1; cloud = pow(max(cloud, 0), 3) * sign(cloud); - cloud *= CLOUD_DENSITY * sqrt(cloud_tendency) * falloff(abs(pos.z - cloud_alt) / CLOUD_DEPTH); + cloud *= CLOUD_DENSITY * sqrt(cloud_tendency + 0.001) * falloff(abs(pos.z - cloud_alt) / CLOUD_DEPTH); // What proportion of sunlight is *not* being blocked by nearby cloud? (approximation) // Basically, just throw together a few values that roughly approximate this term and come up with an average - cloud_sun_access = exp(( + cloud_sun_access = mix(cloud_sun_access, exp(( // Cloud density gradient 0.25 * (cloud_broad_a - cloud_broad_b + (0.25 * (noise_3d(wind_pos / 4000 / cloud_scale) - 0.5) + 0.1 * (noise_3d(wind_pos / 1000 / cloud_scale) - 0.5))) #if (CLOUD_MODE >= CLOUD_MODE_HIGH) // More noise + 0.01 * (noise_3d(wind_pos / 500) / cloud_scale - 0.5) #endif - ) * 15.0 - 1.5) * 1.5; + ) * 15.0 - 1.5) * 1.5, min(cloud_tendency * 10, 1)); // Since we're assuming the sun/moon is always above (not always correct) it's the same for the moon cloud_moon_access = 1.0 - cloud_sun_access; } + #if (CLOUD_MODE >= CLOUD_MODE_LOW) + cloud += max(noise_3d((wind_pos) / 25000.0 / cloud_scale) - 0.75 + noise_3d((wind_pos) / 2500.0 / cloud_scale) * 0.1, 0) + * 0.1 + / (abs(pos.z - cloud_alt) / 500.0 + 0.2); + #endif + // Keeping this because it's something I'm likely to reenable later /* #if (CLOUD_MODE >= CLOUD_MODE_HIGH) @@ -173,11 +181,6 @@ vec4 cloud_at(vec3 pos, float dist, out vec3 emission, out float not_underground return vec4(sun_access, moon_access, vapor_density, air); } -float atan2(in float y, in float x) { - bool s = (abs(x) > abs(y)); - return mix(PI/2.0 - atan(x,y), atan(y,x), s); -} - #if (CLOUD_MODE == CLOUD_MODE_ULTRA) const uint QUALITY = 200u; #elif (CLOUD_MODE == CLOUD_MODE_HIGH) @@ -237,12 +240,24 @@ vec3 get_cloud_color(vec3 surf_color, vec3 dir, vec3 origin, const float time_of // i is an emergency brake float min_dist = clamp(max_dist / 4, 0.25, 24); int i; + + #if (CLOUD_MODE >= CLOUD_MODE_MEDIUM) + #ifdef EXPERIMENTAL_RAINBOWS + // TODO: Make it a double rainbow + float rainbow_t = (0.7 - dot(sun_dir.xyz, dir)) * 8 / 0.05; + int rainbow_c = int(floor(rainbow_t)); + rainbow_t = fract(rainbow_t); + rainbow_t = rainbow_t * rainbow_t; + #endif + #endif + for (i = 0; cdist > min_dist && i < 250; i ++) { ldist = cdist; cdist = step_to_dist(trunc(dist_to_step(cdist - 0.25, quality)), quality); vec3 emission; float not_underground; // Used to prevent sunlight leaking underground + vec3 pos = origin + dir * ldist * splay; // `sample` is a reserved keyword vec4 sample_ = cloud_at(origin + dir * ldist * splay, ldist, emission, not_underground); @@ -256,15 +271,47 @@ vec3 get_cloud_color(vec3 surf_color, vec3 dir, vec3 origin, const float time_of float step = (ldist - cdist) * 0.01; float cloud_darken = pow(1.0 / (1.0 + cloud_scatter_factor), step); float global_darken = pow(1.0 / (1.0 + global_scatter_factor), step); + // Proportion of light diffusely scattered instead of absorbed + float cloud_diffuse = 0.25; surf_color = // Attenuate light passing through the clouds surf_color * cloud_darken * global_darken + // Add the directed light light scattered into the camera by the clouds and the atmosphere (global illumination) - sun_color * sun_scatter * get_sun_brightness() * (sun_access * (1.0 - cloud_darken) /*+ sky_color * global_scatter_factor*/) + - moon_color * moon_scatter * get_moon_brightness() * (moon_access * (1.0 - cloud_darken) /*+ sky_color * global_scatter_factor*/) + + sun_color * sun_scatter * get_sun_brightness() * (sun_access * (1.0 - cloud_darken) * cloud_diffuse /*+ sky_color * global_scatter_factor*/) + + moon_color * moon_scatter * get_moon_brightness() * (moon_access * (1.0 - cloud_darken) * cloud_diffuse /*+ sky_color * global_scatter_factor*/) + sky_light * (1.0 - global_darken) * not_underground + emission * density_integrals.y * step; + + // Rainbow + #if (CLOUD_MODE >= CLOUD_MODE_MEDIUM) + #ifdef EXPERIMENTAL_RAINBOWS + if (rainbow_c >= 0 && rainbow_c < 8) { + vec3 colors[9] = { + surf_color, + vec3(0.9, 0.5, 0.9), + vec3(0.25, 0.0, 0.5), + vec3(0.0, 0.0, 1.0), + vec3(0.0, 0.5, 0.0), + vec3(1.0, 1.0, 0.0), + vec3(1.0, 0.6, 0.0), + vec3(1.0, 0.0, 0.0), + surf_color, + }; + float h = max(0.0, min(pos.z, 900.0 - pos.z) / 450.0); + float rain = rain_density_at(pos.xy) * pow(h, 0.1); + + float sun = sun_access * get_sun_brightness(); + float energy = pow(rain * sun * min(cdist / 500.0, 1.0), 2.0) * 0.4; + + surf_color = mix( + surf_color, + mix(colors[rainbow_c], colors[rainbow_c + 1], rainbow_t), + energy + ); + } + #endif + #endif } #ifdef IS_POSTPROCESS } diff --git a/assets/voxygen/shaders/include/lod.glsl b/assets/voxygen/shaders/include/lod.glsl index fa417dd021..d2efde0737 100644 --- a/assets/voxygen/shaders/include/lod.glsl +++ b/assets/voxygen/shaders/include/lod.glsl @@ -5,19 +5,11 @@ #include #include -layout(set = 0, binding = 5) uniform texture2D t_alt; -layout(set = 0, binding = 6) uniform sampler s_alt; layout(set = 0, binding = 7) uniform texture2D t_horizon; layout(set = 0, binding = 8) uniform sampler s_horizon; -const float MIN_SHADOW = 0.33; -vec2 pos_to_uv(texture2D tex, sampler s, vec2 pos) { - // Want: (pixel + 0.5) / W - vec2 texSize = textureSize(sampler2D(tex, s), 0); - vec2 uv_pos = (focus_off.xy + pos + 16) / (32.0 * texSize); - return vec2(uv_pos.x, /*1.0 - */uv_pos.y); -} +const float MIN_SHADOW = 0.33; vec2 pos_to_tex(vec2 pos) { // Want: (pixel + 0.5) @@ -36,6 +28,12 @@ vec4 cubic(float v) { return vec4(x, y, z, w) * (1.0/6.0); } +// Computes atan(y, x), except with more stability when x is near 0. +float atan2(in float y, in float x) { + bool s = (abs(x) > abs(y)); + return mix(PI/2.0 - atan(x,y), atan(y,x), s); +} + // NOTE: We assume the sampled coordinates are already in "texture pixels". vec4 textureBicubic(texture2D tex, sampler sampl, vec2 texCoords) { // TODO: remove all textureSize calls and replace with constants @@ -126,8 +124,9 @@ vec2 textureBicubic16(texture2D tex, sampler sampl, vec2 texCoords) { , sy); } +// Gets the altitude at a position relative to focus_off. float alt_at(vec2 pos) { - vec4 alt_sample = textureLod/*textureBicubic16*/(sampler2D(t_alt, s_alt), pos_to_uv(t_alt, s_alt, pos), 0); + vec4 alt_sample = textureLod/*textureBicubic16*/(sampler2D(t_alt, s_alt), wpos_to_uv(focus_off.xy + pos), 0); return (/*round*/((alt_sample.r / 256.0 + alt_sample.g) * (/*1300.0*//*1278.7266845703125*/view_distance.w)) + /*140.0*/view_distance.z - focus_off.z); //+ (texture(t_noise, pos * 0.002).x - 0.5) * 64.0; diff --git a/assets/voxygen/shaders/include/rain_occlusion.glsl b/assets/voxygen/shaders/include/rain_occlusion.glsl new file mode 100644 index 0000000000..5f525c282f --- /dev/null +++ b/assets/voxygen/shaders/include/rain_occlusion.glsl @@ -0,0 +1,31 @@ + +#ifndef RAIN_OCCLUSION_GLSL +#define RAIN_OCCLUSION_GLSL + +// Use with sampler2DShadow +layout(set = 1, binding = 4) +uniform texture2D t_directed_occlusion_maps; +layout(set = 1, binding = 5) +uniform samplerShadow s_directed_occlusion_maps; + +layout (std140, set = 0, binding = 14) +uniform u_rain_occlusion { + mat4 rain_occlusion_matrices; + mat4 rain_occlusion_texture_mat; + mat4 rain_dir_mat; + float integrated_rain_vel; + float rain_density; + vec2 occlusion_dummy; // Fix alignment. +}; + +float rain_occlusion_at(in vec3 fragPos) +{ + float bias = -0.2; + + vec4 rain_pos = rain_occlusion_texture_mat * vec4(fragPos, 1.0) - vec4(0, 0, bias, 0); + + float visibility = textureProj(sampler2DShadow(t_directed_occlusion_maps, s_directed_occlusion_maps), rain_pos); + + return visibility; +} +#endif diff --git a/assets/voxygen/shaders/include/sky.glsl b/assets/voxygen/shaders/include/sky.glsl index 9c2b9bd0ce..167ae6b407 100644 --- a/assets/voxygen/shaders/include/sky.glsl +++ b/assets/voxygen/shaders/include/sky.glsl @@ -5,6 +5,7 @@ #include #include #include +#include // Information about an approximately directional light, like the sun or moon. struct DirectionalLight { @@ -97,10 +98,31 @@ vec2 wind_offset = vec2(time_of_day.x * wind_speed); float cloud_scale = view_distance.z / 150.0; -float cloud_tendency_at(vec2 pos) { - float nz = textureLod(sampler2D(t_noise, s_noise), (pos + wind_offset) / 60000.0 / cloud_scale, 0).x - 0.3; - nz = pow(clamp(nz, 0, 1), 3); - return nz; +layout(set = 0, binding = 5) uniform texture2D t_alt; +layout(set = 0, binding = 6) uniform sampler s_alt; + +// Transforms coordinate in the range 0..WORLD_SIZE to 0..1 +vec2 wpos_to_uv(vec2 wpos) { + // Want: (pixel + 0.5) / W + vec2 texSize = textureSize(sampler2D(t_alt, s_alt), 0); + vec2 uv_pos = (wpos + 16) / (32.0 * texSize); + return vec2(uv_pos.x, /*1.0 - */uv_pos.y); +} + +// Weather texture +layout(set = 0, binding = 12) uniform texture2D t_weather; +layout(set = 0, binding = 13) uniform sampler s_weather; + +vec4 sample_weather(vec2 wpos) { + return textureLod(sampler2D(t_weather, s_weather), wpos_to_uv(wpos), 0); +} + +float cloud_tendency_at(vec2 wpos) { + return sample_weather(wpos).r; +} + +float rain_density_at(vec2 wpos) { + return sample_weather(wpos).g; } float cloud_shadow(vec3 pos, vec3 light_dir) { diff --git a/assets/voxygen/shaders/particle-vert.glsl b/assets/voxygen/shaders/particle-vert.glsl index 0e595f09d9..b230abb7d7 100644 --- a/assets/voxygen/shaders/particle-vert.glsl +++ b/assets/voxygen/shaders/particle-vert.glsl @@ -215,7 +215,7 @@ void main() { vec3(rand2 * 0.1, rand3 * 0.1, 2.0 + rand4 * 1.0) ), vec3(1.0), - vec4(2, 1.5 + rand5 * 0.5, 0, start_end(1.0, 0.0)), + vec4(6, 3 + rand5 * 0.3 - 0.8 * percent(), 0.4, 1), spin_in_axis(vec3(rand6, rand7, rand8), rand9 * 3) ); break; @@ -431,16 +431,16 @@ void main() { attr = Attr( (inst_dir * slow_end(1.5)) + vec3(rand0, rand1, rand2) * (percent() + 2) * 0.1, vec3((2.5 * (1 - slow_start(0.2)))), - vec4(3, 1.6 + rand5 * 0.3 - 0.4 * percent(), 0.2, 1), + vec4(6, 3 + rand5 * 0.6 - 0.8 * percent(), 0.4, 1), spin_in_axis(vec3(rand6, rand7, rand8), percent() * 10 + 3 * rand9) ); break; case EXPLOSION: f_reflect = 0.0; // Fire doesn't reflect light, it emits it attr = Attr( - inst_dir * ((rand0+1.0)/2 + 0.4) * slow_end(2.0) + 0.3 * grav_vel(earth_gravity), + inst_dir * ((rand0+1.0)/2 + 0.4) * slow_end(0.25) + 0.3 * grav_vel(earth_gravity), vec3((3 * (1 - slow_start(0.1)))), - vec4(3, 1.6 + rand5 * 0.3 - 0.4 * percent(), 0.2, 1), + vec4(6, 3 + rand5 * 0.3 - 0.8 * percent(), 0.4, 1), spin_in_axis(vec3(rand6, rand7, rand8), percent() * 10 + 3 * rand9) ); break; @@ -459,7 +459,7 @@ void main() { attr = Attr( vec3(rand0, rand1, lifetime * 10 + rand2), vec3((5 * (1 - slow_start(0.5)))), - vec4(3, 1.6 + rand5 * 0.3 - 0.4 * percent(), 0.2, 1), + vec4(6, 3 + rand5 * 0.6 - 0.8 * percent(), 0.4, 1), spin_in_axis(vec3(rand3, rand4, rand5), rand6) ); break; diff --git a/assets/voxygen/shaders/rain-occlusion-directed-vert.glsl b/assets/voxygen/shaders/rain-occlusion-directed-vert.glsl new file mode 100644 index 0000000000..c8bfd187e4 --- /dev/null +++ b/assets/voxygen/shaders/rain-occlusion-directed-vert.glsl @@ -0,0 +1,65 @@ +#version 420 core +// #extension ARB_texture_storage : enable + +#include + +#define LIGHTING_TYPE LIGHTING_TYPE_REFLECTION + +#define LIGHTING_REFLECTION_KIND LIGHTING_REFLECTION_KIND_GLOSSY + +#if (FLUID_MODE == FLUID_MODE_CHEAP) +#define LIGHTING_TRANSPORT_MODE LIGHTING_TRANSPORT_MODE_IMPORTANCE +#elif (FLUID_MODE == FLUID_MODE_SHINY) +#define LIGHTING_TRANSPORT_MODE LIGHTING_TRANSPORT_MODE_RADIANCE +#endif + +#define LIGHTING_DISTRIBUTION_SCHEME LIGHTING_DISTRIBUTION_SCHEME_MICROFACET + +#define LIGHTING_DISTRIBUTION LIGHTING_DISTRIBUTION_BECKMANN + +#define HAS_SHADOW_MAPS + +// Currently, we only need globals for focus_off. +#include +// For shadow locals. +// #include + +layout (std140, set = 0, binding = 14) +uniform u_rain_occlusion { + mat4 rain_occlusion_matrices; + mat4 rain_occlusion_texture_mat; + mat4 rain_dir_mat; + float integrated_rain_vel; + float rain_density; + vec2 occlusion_dummy; // Fix alignment. +}; + +/* Accurate packed shadow maps for many lights at once! + * + * Ideally, we would just write to a bitmask... + * + * */ + +layout(location = 0) in uint v_pos_norm; +// in uint v_col_light; +// in vec4 v_pos; +// layout(location = 1) in uint v_atlas_pos; + +// Light projection matrices. +layout (std140, set = 1, binding = 0) +uniform u_locals { + vec3 model_offs; + float load_time; + ivec4 atlas_offs; +}; + +// out vec4 shadowMapCoord; + +const float EXTRA_NEG_Z = 32768.0; + +void main() { + vec3 f_chunk_pos = vec3(v_pos_norm & 0x3Fu, (v_pos_norm >> 6) & 0x3Fu, float((v_pos_norm >> 12) & 0xFFFFu) - EXTRA_NEG_Z); + vec3 f_pos = f_chunk_pos + (model_offs - focus_off.xyz); + + gl_Position = rain_occlusion_matrices * vec4(f_pos, 1.0); +} diff --git a/assets/voxygen/shaders/rain-occlusion-figure-vert.glsl b/assets/voxygen/shaders/rain-occlusion-figure-vert.glsl new file mode 100644 index 0000000000..b7f940ea2b --- /dev/null +++ b/assets/voxygen/shaders/rain-occlusion-figure-vert.glsl @@ -0,0 +1,86 @@ +#version 420 core +// #extension ARB_texture_storage : enable + +#define FIGURE_SHADER + +#include + +#define LIGHTING_TYPE LIGHTING_TYPE_REFLECTION + +#define LIGHTING_REFLECTION_KIND LIGHTING_REFLECTION_KIND_GLOSSY + +#if (FLUID_MODE == FLUID_MODE_CHEAP) +#define LIGHTING_TRANSPORT_MODE LIGHTING_TRANSPORT_MODE_IMPORTANCE +#elif (FLUID_MODE == FLUID_MODE_SHINY) +#define LIGHTING_TRANSPORT_MODE LIGHTING_TRANSPORT_MODE_RADIANCE +#endif + +#define LIGHTING_DISTRIBUTION_SCHEME LIGHTING_DISTRIBUTION_SCHEME_MICROFACET + +#define LIGHTING_DISTRIBUTION LIGHTING_DISTRIBUTION_BECKMANN + +#define HAS_SHADOW_MAPS + +// Currently, we only need globals for focus_off. +#include +// For shadow locals. +// #include + +layout (std140, set = 0, binding = 14) +uniform u_rain_occlusion { + mat4 rainOcclusionMatrices; + mat4 texture_mat; + mat4 rain_dir_mat; + float integrated_rain_vel; + float rain_density; + vec2 occlusion_dummy; // Fix alignment. +}; + +/* Accurate packed shadow maps for many lights at once! + * + * Ideally, we would just write to a bitmask... + * + * */ + +layout(location = 0) in uint v_pos_norm; +layout(location = 1) in uint v_atlas_pos; +// in uint v_col_light; +// in vec4 v_pos; + +layout (std140, set = 1, binding = 0) +uniform u_locals { + mat4 model_mat; + vec4 highlight_col; + vec4 model_light; + vec4 model_glow; + ivec4 atlas_offs; + vec3 model_pos; + // bit 0 - is player + // bit 1-31 - unused + int flags; +}; + +struct BoneData { + mat4 bone_mat; + mat4 normals_mat; +}; + +layout (std140, set = 1, binding = 1) +uniform u_bones { + // Warning: might not actually be 16 elements long. Don't index out of bounds! + BoneData bones[16]; +}; + +// out vec4 shadowMapCoord; + +void main() { + uint bone_idx = (v_pos_norm >> 27) & 0xFu; + vec3 pos = (vec3((uvec3(v_pos_norm) >> uvec3(0, 9, 18)) & uvec3(0x1FFu)) - 256.0) / 2.0; + + vec3 f_pos = ( + bones[bone_idx].bone_mat * + vec4(pos, 1.0) + ).xyz + (model_pos - focus_off.xyz/* + vec3(0.0, 0.0, 0.0001)*/); + + gl_Position = rainOcclusionMatrices * vec4(f_pos, 1.0); +} diff --git a/assets/voxygen/shaders/sprite-vert.glsl b/assets/voxygen/shaders/sprite-vert.glsl index 44ee20bfe8..f5d25606f2 100644 --- a/assets/voxygen/shaders/sprite-vert.glsl +++ b/assets/voxygen/shaders/sprite-vert.glsl @@ -30,7 +30,7 @@ layout(location = 7) in float inst_glow; layout(location = 8) in float model_wind_sway; // NOTE: this only varies per model layout(location = 9) in float model_z_scale; // NOTE: this only varies per model -layout(set = 0, binding = 12) restrict readonly buffer sprite_verts { +layout(set = 0, binding = 15) restrict readonly buffer sprite_verts { uvec2 verts[]; }; @@ -92,6 +92,7 @@ void main() { #endif #ifndef EXPERIMENTAL_BAREMINIMUM + // TODO: take wind_vel into account // Wind sway effect f_pos += model_wind_sway * vec3( sin(tick.x * 1.5 + f_pos.y * 0.1) * sin(tick.x * 0.35), diff --git a/assets/voxygen/shaders/terrain-frag.glsl b/assets/voxygen/shaders/terrain-frag.glsl index 034c98edcb..2c821f9cf4 100644 --- a/assets/voxygen/shaders/terrain-frag.glsl +++ b/assets/voxygen/shaders/terrain-frag.glsl @@ -231,6 +231,39 @@ void main() { vec3 k_d = vec3(1.0); vec3 k_s = vec3(R_s); + // Toggle to see rain_occlusion + // tgt_color = vec4(rain_occlusion_at(f_pos.xyz), 0.0, 0.0, 1.0); + // return; + #if (CLOUD_MODE != CLOUD_MODE_NONE) + if (rain_density > 0 && !faces_fluid && f_norm.z > 0.5) { + vec3 pos = f_pos + focus_off.xyz; + vec3 drop_density = vec3(2, 2, 2); + vec3 drop_pos = pos + vec3(pos.zz, 0) + vec3(0, 0, -tick.x * 1.0); + drop_pos.z += noise_2d(floor(drop_pos.xy * drop_density.xy) * 13.1) * 10; + vec2 cell2d = floor(drop_pos.xy * drop_density.xy); + drop_pos.z *= 0.5 + hash_fast(uvec3(cell2d, 0)); + vec3 cell = vec3(cell2d, floor(drop_pos.z * drop_density.z)); + + if (fract(hash(fract(vec4(cell, 0) * 0.01))) < rain_density * rain_occlusion_at(f_pos.xyz) * 50.0) { + vec3 off = vec3(hash_fast(uvec3(cell * 13)), hash_fast(uvec3(cell * 5)), 0); + vec3 near_cell = (cell + 0.5 + (off - 0.5) * 0.5) / drop_density; + + float dist = length((drop_pos - near_cell) / vec3(1, 1, 2)); + float drop_rad = 0.1; + float distort = max(1.0 - abs(dist - drop_rad) * 100, 0) * 1.5 * max(drop_pos.z - near_cell.z, 0); + k_a += distort; + k_d += distort; + k_s += distort; + f_norm.xy += (drop_pos - near_cell).xy + * max(1.0 - abs(dist - drop_rad) * 30, 0) + * 500.0 + * max(drop_pos.z - near_cell.z, 0) + * sign(dist - drop_rad) + * max(drop_pos.z - near_cell.z, 0); + } + } + #endif + // float sun_light = get_sun_brightness(sun_dir); // float moon_light = get_moon_brightness(moon_dir); /* float sun_shade_frac = horizon_at(f_pos, sun_dir); diff --git a/client/src/lib.rs b/client/src/lib.rs index eb6da6b25a..6398a07cf0 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -48,6 +48,7 @@ use common::{ trade::{PendingTrade, SitePrices, TradeAction, TradeId, TradeResult}, uid::{Uid, UidAllocator}, vol::RectVolSize, + weather::{Weather, WeatherGrid}, }; #[cfg(feature = "tracy")] use common_base::plot; use common_base::{prof_span, span}; @@ -151,12 +152,60 @@ pub struct SiteInfoRich { pub economy: Option, } +struct WeatherLerp { + old: (WeatherGrid, Instant), + new: (WeatherGrid, Instant), +} + +impl WeatherLerp { + fn weather_update(&mut self, weather: WeatherGrid) { + self.old = mem::replace(&mut self.new, (weather, Instant::now())); + } + + // TODO: Make impprovements to this interpolation, it's main issue is assuming + // that updates come at regular intervals. + fn update(&mut self, to_update: &mut WeatherGrid) { + prof_span!("WeatherLerp::update"); + let old = &self.old.0; + let new = &self.new.0; + if new.size() == Vec2::zero() { + return; + } + if to_update.size() != new.size() { + *to_update = new.clone(); + } + if old.size() == new.size() { + // Assumes updates are regular + let t = (self.new.1.elapsed().as_secs_f32() + / self.new.1.duration_since(self.old.1).as_secs_f32()) + .clamp(0.0, 1.0); + + to_update + .iter_mut() + .zip(old.iter().zip(new.iter())) + .for_each(|((_, current), ((_, old), (_, new)))| { + *current = Weather::lerp_unclamped(old, new, t); + }); + } + } +} + +impl Default for WeatherLerp { + fn default() -> Self { + Self { + old: (WeatherGrid::new(Vec2::zero()), Instant::now()), + new: (WeatherGrid::new(Vec2::zero()), Instant::now()), + } + } +} + pub struct Client { registered: bool, presence: Option, runtime: Arc, server_info: ServerInfo, world_data: WorldData, + weather: WeatherLerp, player_list: HashMap, character_list: CharacterList, sites: HashMap, @@ -608,6 +657,7 @@ impl Client { lod_horizon, map: world_map, }, + weather: WeatherLerp::default(), player_list: HashMap::new(), character_list: CharacterList::default(), sites: sites @@ -1413,6 +1463,13 @@ impl Client { .map(|v| v.0) } + /// Returns Weather::default if no player position exists. + pub fn weather_at_player(&self) -> Weather { + self.position() + .map(|wpos| self.state.weather_at(wpos.xy())) + .unwrap_or_default() + } + pub fn current_chunk(&self) -> Option> { let chunk_pos = Vec2::from(self.position()?) .map2(TerrainChunkSize::RECT_SIZE, |e: f32, sz| { @@ -1631,6 +1688,9 @@ impl Client { self.invite = None; } + // Lerp the clientside weather. + self.weather.update(&mut self.state.weather_grid_mut()); + // Lerp towards the target time of day - this ensures a smooth transition for // large jumps in TimeOfDay such as when using /time if let Some(target_tod) = self.target_time_of_day { @@ -2193,6 +2253,9 @@ impl Client { ServerGeneral::MapMarker(event) => { frontend_events.push(Event::MapMarker(event)); }, + ServerGeneral::WeatherUpdate(weather) => { + self.weather.weather_update(weather); + }, _ => unreachable!("Not a in_game message"), } Ok(()) diff --git a/common/net/src/msg/server.rs b/common/net/src/msg/server.rs index 4b233de6bc..be95b5ddbe 100644 --- a/common/net/src/msg/server.rs +++ b/common/net/src/msg/server.rs @@ -15,6 +15,7 @@ use common::{ trade::{PendingTrade, SitePrices, TradeId, TradeResult}, uid::Uid, uuid::Uuid, + weather::WeatherGrid, }; use hashbrown::HashMap; use serde::{Deserialize, Serialize}; @@ -197,6 +198,7 @@ pub enum ServerGeneral { /// Economic information about sites SiteEconomy(EconomyInfo), MapMarker(comp::MapMarkerUpdate), + WeatherUpdate(WeatherGrid), } impl ServerGeneral { @@ -309,7 +311,8 @@ impl ServerMsg { | ServerGeneral::UpdatePendingTrade(_, _, _) | ServerGeneral::FinishedTrade(_) | ServerGeneral::SiteEconomy(_) - | ServerGeneral::MapMarker(_) => { + | ServerGeneral::MapMarker(_) + | ServerGeneral::WeatherUpdate(_) => { c_type == ClientType::Game && presence.is_some() }, // Always possible diff --git a/common/src/cmd.rs b/common/src/cmd.rs index 479eae7585..3d13aad316 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -125,6 +125,13 @@ lazy_static! { .map(|s| s.to_string()) .collect(); + static ref WEATHERS: Vec = vec![ + "clear", "cloudy", "rain", "wind", "storm" + ] + .iter() + .map(|s| s.to_string()) + .collect(); + pub static ref BUFF_PARSER: HashMap = { let string_from_buff = |kind| match kind { BuffKind::Burning => "burning", @@ -297,6 +304,7 @@ pub enum ServerChatCommand { Location, CreateLocation, DeleteLocation, + WeatherZone, } impl ServerChatCommand { @@ -686,6 +694,15 @@ impl ServerChatCommand { "Delete a location", Some(Moderator), ), + ServerChatCommand::WeatherZone => cmd( + vec![ + Enum("weather kind", WEATHERS.clone(), Required), + Float("radius", 500.0, Optional), + Float("time", 300.0, Optional), + ], + "Create a weather zone", + Some(Admin), + ), } } @@ -763,6 +780,7 @@ impl ServerChatCommand { ServerChatCommand::Location => "location", ServerChatCommand::CreateLocation => "create_location", ServerChatCommand::DeleteLocation => "delete_location", + ServerChatCommand::WeatherZone => "weather_zone", } } diff --git a/common/src/lib.rs b/common/src/lib.rs index e1c0a036e8..88ae0bdefb 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -84,6 +84,8 @@ pub mod uid; #[cfg(not(target_arch = "wasm32"))] pub mod vol; #[cfg(not(target_arch = "wasm32"))] pub mod volumes; +#[cfg(not(target_arch = "wasm32"))] +pub mod weather; #[cfg(not(target_arch = "wasm32"))] pub use cached_spatial_grid::CachedSpatialGrid; diff --git a/common/src/outcome.rs b/common/src/outcome.rs index 3604c49a52..69ef8fb3da 100644 --- a/common/src/outcome.rs +++ b/common/src/outcome.rs @@ -52,8 +52,6 @@ pub enum Outcome { uid: Uid, skill_tree: comp::skillset::SkillGroupKind, total_points: u16, - // TODO: Access ECS to get position from Uid to conserve bandwidth - pos: Vec3, }, ComboChange { uid: Uid, @@ -104,7 +102,6 @@ impl Outcome { | Outcome::ProjectileShot { pos, .. } | Outcome::ProjectileHit { pos, .. } | Outcome::Beam { pos, .. } - | Outcome::SkillPointGain { pos, .. } | Outcome::SummonedCreature { pos, .. } | Outcome::HealthChange { pos, .. } | Outcome::Death { pos, .. } @@ -114,7 +111,9 @@ impl Outcome { | Outcome::Utterance { pos, .. } | Outcome::Glider { pos, .. } => Some(*pos), Outcome::BreakBlock { pos, .. } => Some(pos.map(|e| e as f32 + 0.5)), - Outcome::ExpChange { .. } | Outcome::ComboChange { .. } => None, + Outcome::ExpChange { .. } + | Outcome::ComboChange { .. } + | Outcome::SkillPointGain { .. } => None, } } } diff --git a/common/src/weather.rs b/common/src/weather.rs new file mode 100644 index 0000000000..b2e163be3a --- /dev/null +++ b/common/src/weather.rs @@ -0,0 +1,161 @@ +use std::fmt; + +use serde::{Deserialize, Serialize}; +use vek::{Lerp, Vec2, Vec3}; + +use crate::{grid::Grid, terrain::TerrainChunkSize, vol::RectVolSize}; + +/// Weather::default is Clear, 0 degrees C and no wind +#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)] +pub struct Weather { + /// Clouds currently in the area between 0 and 1 + pub cloud: f32, + /// Rain per time, between 0 and 1 + pub rain: f32, + /// Wind velocity in block / second + pub wind: Vec2, +} + +impl Weather { + pub fn new(cloud: f32, rain: f32, wind: Vec2) -> Self { Self { cloud, rain, wind } } + + pub fn get_kind(&self) -> WeatherKind { + // Over 24.5 m/s wind is a storm + if self.wind.magnitude_squared() >= 24.5f32.powi(2) { + WeatherKind::Storm + } else if (0.1..=1.0).contains(&self.rain) { + WeatherKind::Rain + } else if (0.2..=1.0).contains(&self.cloud) { + WeatherKind::Cloudy + } else { + WeatherKind::Clear + } + } + + pub fn lerp_unclamped(from: &Self, to: &Self, t: f32) -> Self { + Self { + cloud: f32::lerp_unclamped(from.cloud, to.cloud, t), + rain: f32::lerp_unclamped(from.rain, to.rain, t), + wind: Vec2::::lerp_unclamped(from.wind, to.wind, t), + } + } + + // Get the rain velocity for this weather + pub fn rain_vel(&self) -> Vec3 { + const FALL_RATE: f32 = 50.0; + Vec3::new(self.wind.x, self.wind.y, -FALL_RATE) + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +pub enum WeatherKind { + Clear, + Cloudy, + Rain, + Storm, +} + +impl fmt::Display for WeatherKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + WeatherKind::Clear => write!(f, "Clear"), + WeatherKind::Cloudy => write!(f, "Cloudy"), + WeatherKind::Rain => write!(f, "Rain"), + WeatherKind::Storm => write!(f, "Storm"), + } + } +} + +// How many chunks wide a weather cell is. +// So one weather cell has (CHUNKS_PER_CELL * CHUNKS_PER_CELL) chunks. +pub const CHUNKS_PER_CELL: u32 = 16; + +pub const CELL_SIZE: u32 = CHUNKS_PER_CELL * TerrainChunkSize::RECT_SIZE.x; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WeatherGrid { + weather: Grid, +} + +/// Transforms a world position to cell coordinates. Where (0.0, 0.0) in cell +/// coordinates is the center of the weather cell located at (0, 0) in the grid. +fn to_cell_pos(wpos: Vec2) -> Vec2 { wpos / CELL_SIZE as f32 - 0.5 } + +// TODO: Move consts from world to common to avoid duplication +const LOCALITY: [Vec2; 9] = [ + Vec2::new(0, 0), + Vec2::new(0, 1), + Vec2::new(1, 0), + Vec2::new(0, -1), + Vec2::new(-1, 0), + Vec2::new(1, 1), + Vec2::new(1, -1), + Vec2::new(-1, 1), + Vec2::new(-1, -1), +]; + +impl WeatherGrid { + pub fn new(size: Vec2) -> Self { + size.map(|e| debug_assert!(i32::try_from(e).is_ok())); + Self { + weather: Grid::new(size.as_(), Weather::default()), + } + } + + pub fn iter(&self) -> impl Iterator, &Weather)> { self.weather.iter() } + + pub fn iter_mut(&mut self) -> impl Iterator, &mut Weather)> { + self.weather.iter_mut() + } + + pub fn size(&self) -> Vec2 { self.weather.size().as_() } + + /// Get the weather at a given world position by doing bilinear + /// interpolation between four cells. + pub fn get_interpolated(&self, wpos: Vec2) -> Weather { + let cell_pos = to_cell_pos(wpos); + let rpos = cell_pos.map(|e| e.fract() + (1.0 - e.signum()) / 2.0); + let cell_pos = cell_pos.map(|e| e.floor()); + + let cpos = cell_pos.as_::(); + Weather::lerp_unclamped( + &Weather::lerp_unclamped( + self.weather.get(cpos).unwrap_or(&Weather::default()), + self.weather + .get(cpos + Vec2::unit_x()) + .unwrap_or(&Weather::default()), + rpos.x, + ), + &Weather::lerp_unclamped( + self.weather + .get(cpos + Vec2::unit_y()) + .unwrap_or(&Weather::default()), + self.weather + .get(cpos + Vec2::one()) + .unwrap_or(&Weather::default()), + rpos.x, + ), + rpos.y, + ) + } + + /// Get the max weather near a position + pub fn get_max_near(&self, wpos: Vec2) -> Weather { + let cell_pos: Vec2 = to_cell_pos(wpos).as_(); + LOCALITY + .iter() + .map(|l| { + self.weather + .get(cell_pos + l) + .cloned() + .unwrap_or_default() + }) + .reduce(|a, b| Weather { + cloud: a.cloud.max(b.cloud), + rain: a.rain.max(b.rain), + wind: a.wind.map2(b.wind, |a, b| a.max(b)), + }) + // There will always be 9 elements in locality + .unwrap() + } +} diff --git a/common/state/src/state.rs b/common/state/src/state.rs index 6697f30394..d088bc84d3 100644 --- a/common/state/src/state.rs +++ b/common/state/src/state.rs @@ -21,6 +21,7 @@ use common::{ time::DayPeriod, trade::Trades, vol::{ReadVol, WriteVol}, + weather::{Weather, WeatherGrid}, }; use common_base::span; use common_ecs::{PhysicsMetrics, SysMetrics}; @@ -206,6 +207,7 @@ impl State { // Register synced resources used by the ECS. ecs.insert(TimeOfDay(0.0)); ecs.insert(Calendar::default()); + ecs.insert(WeatherGrid::new(Vec2::zero())); // Register unsynced resources used by the ECS. ecs.insert(Time(0.0)); @@ -346,13 +348,28 @@ impl State { /// last game tick. pub fn terrain_changes(&self) -> Fetch { self.ecs.read_resource() } + /// Get a reference the current in-game weather grid. + pub fn weather_grid(&self) -> Fetch { self.ecs.read_resource() } + + /// Get a mutable reference the current in-game weather grid. + pub fn weather_grid_mut(&mut self) -> FetchMut { self.ecs.write_resource() } + + /// Get the current weather at a position in worldspace. + pub fn weather_at(&self, pos: Vec2) -> Weather { + self.weather_grid().get_interpolated(pos) + } + + /// Get the max weather near a position in worldspace. + pub fn max_weather_near(&self, pos: Vec2) -> Weather { + self.weather_grid().get_max_near(pos) + } + /// Get the current in-game time of day. /// /// Note that this should not be used for physics, animations or other such /// localised timings. pub fn get_time_of_day(&self) -> f64 { self.ecs.read_resource::().0 } - /// Get the current in-game day period (period of the day/night cycle) /// Get the current in-game day period (period of the day/night cycle) pub fn get_day_period(&self) -> DayPeriod { self.get_time_of_day().into() } diff --git a/server/Cargo.toml b/server/Cargo.toml index 0ccd17c0aa..b24acdf414 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -58,6 +58,7 @@ authc = { git = "https://gitlab.com/veloren/auth.git", rev = "fb3dcbc4962b367253 slab = "0.4" rand_distr = "0.4.0" enumset = "1.0.8" +noise = { version = "0.7", default-features = false } rusqlite = { version = "0.24.2", features = ["array", "vtab", "bundled", "trace"] } refinery = { git = "https://gitlab.com/veloren/refinery.git", rev = "8ecf4b4772d791e6c8c0a3f9b66a7530fad1af3e", features = ["rusqlite"] } diff --git a/server/src/client.rs b/server/src/client.rs index 202790ad08..012c77efb0 100644 --- a/server/src/client.rs +++ b/server/src/client.rs @@ -113,7 +113,8 @@ impl Client { | ServerGeneral::Outcomes(_) | ServerGeneral::Knockback(_) | ServerGeneral::UpdatePendingTrade(_, _, _) - | ServerGeneral::FinishedTrade(_) => { + | ServerGeneral::FinishedTrade(_) + | ServerGeneral::WeatherUpdate(_) => { self.in_game_stream.lock().unwrap().send(g) }, //Ingame related, terrain @@ -187,7 +188,8 @@ impl Client { | ServerGeneral::SiteEconomy(_) | ServerGeneral::UpdatePendingTrade(_, _, _) | ServerGeneral::FinishedTrade(_) - | ServerGeneral::MapMarker(_) => { + | ServerGeneral::MapMarker(_) + | ServerGeneral::WeatherUpdate(_) => { PreparedMsg::new(2, &g, &self.in_game_stream_params) }, //Ingame related, terrain diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 677aec2b49..ab65b6d843 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -10,6 +10,7 @@ use crate::{ Ban, BanAction, BanInfo, EditableSetting, SettingError, WhitelistInfo, WhitelistRecord, }, sys::terrain::NpcData, + weather::WeatherSim, wiring, wiring::OutputFormula, Server, Settings, SpawnPoint, StateExt, @@ -44,7 +45,7 @@ use common::{ terrain::{Block, BlockKind, SpriteKind, TerrainChunkSize}, uid::{Uid, UidAllocator}, vol::{ReadVol, RectVolSize}, - Damage, DamageKind, DamageSource, Explosion, LoadoutBuilder, RadiusEffect, + weather, Damage, DamageKind, DamageSource, Explosion, LoadoutBuilder, RadiusEffect, }; use common_net::{ msg::{DisconnectReason, Notification, PlayerListUpdate, ServerGeneral}, @@ -190,6 +191,7 @@ fn do_command( ServerChatCommand::Location => handle_location, ServerChatCommand::CreateLocation => handle_create_location, ServerChatCommand::DeleteLocation => handle_delete_location, + ServerChatCommand::WeatherZone => handle_weather_zone, }; handler(server, client, target, args, cmd) @@ -3595,3 +3597,72 @@ fn handle_delete_location( Err(action.help_string()) } } + +fn handle_weather_zone( + server: &mut Server, + client: EcsEntity, + _target: EcsEntity, + args: Vec, + action: &ServerChatCommand, +) -> CmdResult<()> { + if let (Some(name), radius, time) = parse_cmd_args!(args, String, f32, f32) { + let radius = radius.map(|r| r / weather::CELL_SIZE as f32).unwrap_or(1.0); + let time = time.unwrap_or(100.0); + + let mut add_zone = |weather: weather::Weather| { + if let Ok(pos) = position(server, client, "player") { + let pos = pos.0.xy() / weather::CELL_SIZE as f32; + server + .state + .ecs_mut() + .write_resource::() + .add_zone(weather, pos, radius, time); + } + }; + match name.as_str() { + "clear" => { + add_zone(weather::Weather { + cloud: 0.0, + rain: 0.0, + wind: Vec2::zero(), + }); + Ok(()) + }, + "cloudy" => { + add_zone(weather::Weather { + cloud: 0.4, + rain: 0.0, + wind: Vec2::zero(), + }); + Ok(()) + }, + "rain" => { + add_zone(weather::Weather { + cloud: 0.1, + rain: 0.15, + wind: Vec2::new(1.0, -1.0), + }); + Ok(()) + }, + "wind" => { + add_zone(weather::Weather { + cloud: 0.0, + rain: 0.0, + wind: Vec2::new(10.0, 10.0), + }); + Ok(()) + }, + "storm" => { + add_zone(weather::Weather { + cloud: 0.3, + rain: 0.3, + wind: Vec2::new(15.0, 20.0), + }); + Ok(()) + }, + _ => Err("Valid values are 'clear', 'rain', 'wind', 'storm'".to_string()), + } + } else { + Err(action.help_string()) + } +} diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index d79963f320..cdb9ec07e9 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -91,7 +91,7 @@ pub fn handle_health_change(server: &Server, entity: EcsEntity, change: HealthCh // This if statement filters out anything under 5 damage, for DOT ticks // TODO: Find a better way to separate direct damage from DOT here let damage = -change.amount; - if damage > -5.0 { + if damage > 5.0 { if let Some(agent) = ecs.write_storage::().get_mut(entity) { agent.inbox.push_front(AgentEvent::Hurt); } @@ -379,23 +379,16 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt exp_awards.iter().for_each(|(attacker, exp_reward, _)| { // Process the calculated EXP rewards - if let ( - Some(mut attacker_skill_set), - Some(attacker_uid), - Some(attacker_inventory), - Some(pos), - ) = ( + if let (Some(mut attacker_skill_set), Some(attacker_uid), Some(attacker_inventory)) = ( skill_sets.get_mut(*attacker), uids.get(*attacker), inventories.get(*attacker), - positions.get(*attacker), ) { handle_exp_gain( *exp_reward, attacker_inventory, &mut attacker_skill_set, attacker_uid, - pos, &mut outcomes, ); } @@ -1178,7 +1171,6 @@ fn handle_exp_gain( inventory: &Inventory, skill_set: &mut SkillSet, uid: &Uid, - pos: &Pos, outcomes: &mut EventBus, ) { use comp::inventory::{item::ItemKind, slot::EquipSlot}; @@ -1219,7 +1211,6 @@ fn handle_exp_gain( uid: *uid, skill_tree: *pool, total_points: level_outcome, - pos: pos.0, }); } } diff --git a/server/src/events/interaction.rs b/server/src/events/interaction.rs index e374f8f8f5..e60e359d45 100644 --- a/server/src/events/interaction.rs +++ b/server/src/events/interaction.rs @@ -186,16 +186,13 @@ pub fn handle_mine_block( ) { let skill_group = SkillGroupKind::Weapon(tool); let outcome_bus = state.ecs().read_resource::>(); - let positions = state.ecs().read_component::(); - if let (Some(level_outcome), Some(pos)) = ( - skillset.add_experience(skill_group, exp_reward), - positions.get(entity), - ) { + if let Some(level_outcome) = + skillset.add_experience(skill_group, exp_reward) + { outcome_bus.emit_now(Outcome::SkillPointGain { uid, skill_tree: skill_group, total_points: level_outcome, - pos: pos.0, }); } outcome_bus.emit_now(Outcome::ExpChange { diff --git a/server/src/lib.rs b/server/src/lib.rs index 39adc8fa4b..921e92dd73 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -39,6 +39,7 @@ pub mod sys; #[cfg(feature = "persistent_world")] pub mod terrain_persistence; #[cfg(not(feature = "worldgen"))] mod test_world; +mod weather; pub mod wiring; // Reexports @@ -569,6 +570,8 @@ impl Server { #[cfg(not(feature = "worldgen"))] rtsim::init(&mut state); + weather::init(&mut state, &world); + let this = Self { state, world, @@ -707,6 +710,7 @@ impl Server { sys::add_server_systems(dispatcher_builder); #[cfg(feature = "worldgen")] rtsim::add_server_systems(dispatcher_builder); + weather::add_server_systems(dispatcher_builder); }, false, ); diff --git a/server/src/weather/mod.rs b/server/src/weather/mod.rs new file mode 100644 index 0000000000..46ad28540c --- /dev/null +++ b/server/src/weather/mod.rs @@ -0,0 +1,40 @@ +use common::weather::CHUNKS_PER_CELL; +use common_ecs::{dispatch, System}; +use common_state::State; +use specs::DispatcherBuilder; +use std::time::Duration; + +use crate::sys::SysScheduler; + +mod sim; +mod sync; +mod tick; + +pub use sim::WeatherSim; + +/// How often the weather is updated, in seconds +const WEATHER_DT: f32 = 5.0; + +pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) { + dispatch::(dispatch_builder, &[]); + dispatch::(dispatch_builder, &[&tick::Sys::sys_name()]); +} + +pub fn init(state: &mut State, world: &world::World) { + let weather_size = world.sim().get_size() / CHUNKS_PER_CELL; + let sim = WeatherSim::new(weather_size, world); + state.ecs_mut().insert(sim); + + // NOTE: If weather computations get too heavy, this should not block the main + // thread. + state + .ecs_mut() + .insert(SysScheduler::::every(Duration::from_secs_f32( + WEATHER_DT, + ))); + state + .ecs_mut() + .insert(SysScheduler::::every(Duration::from_secs_f32( + WEATHER_DT, + ))); +} diff --git a/server/src/weather/sim.rs b/server/src/weather/sim.rs new file mode 100644 index 0000000000..50612bcbc8 --- /dev/null +++ b/server/src/weather/sim.rs @@ -0,0 +1,131 @@ +use common::{ + grid::Grid, + resources::TimeOfDay, + weather::{Weather, WeatherGrid, CELL_SIZE, CHUNKS_PER_CELL}, +}; +use noise::{NoiseFn, SuperSimplex, Turbulence}; +use vek::*; +use world::World; + +use crate::weather::WEATHER_DT; + +fn cell_to_wpos(p: Vec2) -> Vec2 { p * CELL_SIZE as i32 } + +#[derive(Clone)] +struct WeatherZone { + weather: Weather, + /// Time, in seconds this zone lives. + time_to_live: f32, +} + +struct CellConsts { + rain_factor: f32, +} + +pub struct WeatherSim { + size: Vec2, + consts: Grid, + zones: Grid>, +} + +impl WeatherSim { + pub fn new(size: Vec2, world: &World) -> Self { + Self { + size, + consts: Grid::from_raw( + size.as_(), + (0..size.x * size.y) + .map(|i| Vec2::new(i % size.x, i / size.x)) + .map(|p| { + let mut humid_sum = 0.0; + + for y in 0..CHUNKS_PER_CELL { + for x in 0..CHUNKS_PER_CELL { + let chunk_pos = p * CHUNKS_PER_CELL + Vec2::new(x, y); + if let Some(chunk) = world.sim().get(chunk_pos.as_()) { + let env = chunk.get_environment(); + humid_sum += env.humid; + } + } + } + let average_humid = humid_sum / (CHUNKS_PER_CELL * CHUNKS_PER_CELL) as f32; + let rain_factor = (2.0 * average_humid.powf(0.2)).min(1.0); + CellConsts { rain_factor } + }) + .collect::>(), + ), + zones: Grid::new(size.as_(), None), + } + } + + /// Adds a weather zone as a circle at a position, with a given radius. Both + /// of which should be in weather cell units + pub fn add_zone(&mut self, weather: Weather, pos: Vec2, radius: f32, time: f32) { + let min: Vec2 = (pos - radius).as_::().map(|e| e.max(0)); + let max: Vec2 = (pos + radius) + .ceil() + .as_::() + .map2(self.size.as_::(), |a, b| a.min(b)); + for y in min.y..max.y { + for x in min.x..max.x { + let ipos = Vec2::new(x, y); + let p = ipos.as_::(); + + if p.distance_squared(pos) < radius.powi(2) { + self.zones[ipos] = Some(WeatherZone { + weather, + time_to_live: time, + }); + } + } + } + } + + // Time step is cell size / maximum wind speed + pub fn tick(&mut self, time_of_day: &TimeOfDay, out: &mut WeatherGrid) { + let time = time_of_day.0; + + let base_nz = Turbulence::new( + Turbulence::new(SuperSimplex::new()) + .set_frequency(0.2) + .set_power(1.5), + ) + .set_frequency(2.0) + .set_power(0.2); + + let rain_nz = SuperSimplex::new(); + + for (point, cell) in out.iter_mut() { + if let Some(zone) = &mut self.zones[point] { + *cell = zone.weather; + zone.time_to_live -= WEATHER_DT; + if zone.time_to_live <= 0.0 { + self.zones[point] = None; + } + } else { + let wpos = cell_to_wpos(point); + + let pos = wpos.as_::() + time as f64 * 0.1; + + let space_scale = 7_500.0; + let time_scale = 100_000.0; + let spos = (pos / space_scale).with_z(time as f64 / time_scale); + + let pressure = + (base_nz.get(spos.into_array()) * 0.5 + 1.0).clamped(0.0, 1.0) as f32; + + const RAIN_CLOUD_THRESHOLD: f32 = 0.26; + cell.cloud = (1.0 - pressure) * 0.5; + cell.rain = (1.0 - pressure - RAIN_CLOUD_THRESHOLD).max(0.0) + * self.consts[point].rain_factor; + cell.wind = Vec2::new( + rain_nz.get(spos.into_array()).powi(3) as f32, + rain_nz.get((spos + 1.0).into_array()).powi(3) as f32, + ) * 200.0 + * (1.0 - pressure); + } + } + } + + pub fn size(&self) -> Vec2 { self.size } +} diff --git a/server/src/weather/sync.rs b/server/src/weather/sync.rs new file mode 100644 index 0000000000..b5d58c22ed --- /dev/null +++ b/server/src/weather/sync.rs @@ -0,0 +1,37 @@ +use common::weather::WeatherGrid; +use common_ecs::{Origin, Phase, System}; +use common_net::msg::ServerGeneral; +use specs::{Join, ReadExpect, ReadStorage, Write}; + +use crate::{client::Client, sys::SysScheduler}; + +#[derive(Default)] +pub struct Sys; + +impl<'a> System<'a> for Sys { + type SystemData = ( + ReadExpect<'a, WeatherGrid>, + Write<'a, SysScheduler>, + ReadStorage<'a, Client>, + ); + + const NAME: &'static str = "weather::sync"; + const ORIGIN: Origin = Origin::Server; + const PHASE: Phase = Phase::Create; + + fn run( + _job: &mut common_ecs::Job, + (weather_grid, mut scheduler, clients): Self::SystemData, + ) { + if scheduler.should_run() { + let mut lazy_msg = None; + for client in clients.join() { + if lazy_msg.is_none() { + lazy_msg = + Some(client.prepare(ServerGeneral::WeatherUpdate(weather_grid.clone()))); + } + lazy_msg.as_ref().map(|msg| client.send_prepared(msg)); + } + } + } +} diff --git a/server/src/weather/tick.rs b/server/src/weather/tick.rs new file mode 100644 index 0000000000..db5154b73c --- /dev/null +++ b/server/src/weather/tick.rs @@ -0,0 +1,35 @@ +use common::{resources::TimeOfDay, weather::WeatherGrid}; +use common_ecs::{Origin, Phase, System}; +use specs::{Read, Write, WriteExpect}; + +use crate::sys::SysScheduler; + +use super::sim::WeatherSim; + +#[derive(Default)] +pub struct Sys; + +impl<'a> System<'a> for Sys { + type SystemData = ( + Read<'a, TimeOfDay>, + WriteExpect<'a, WeatherSim>, + WriteExpect<'a, WeatherGrid>, + Write<'a, SysScheduler>, + ); + + const NAME: &'static str = "weather::tick"; + const ORIGIN: Origin = Origin::Server; + const PHASE: Phase = Phase::Create; + + fn run( + _job: &mut common_ecs::Job, + (game_time, mut sim, mut grid, mut scheduler): Self::SystemData, + ) { + if scheduler.should_run() { + if grid.size() != sim.size() { + *grid = WeatherGrid::new(sim.size()); + } + sim.tick(&game_time, &mut grid); + } + } +} diff --git a/voxygen/Cargo.toml b/voxygen/Cargo.toml index b86d69f315..2d4ff19849 100644 --- a/voxygen/Cargo.toml +++ b/voxygen/Cargo.toml @@ -35,7 +35,9 @@ shaderc-from-source = ["shaderc/build-from-source"] # We don't ship egui with published release builds so a separate feature is required that excludes it. default-publish = ["singleplayer", "native-dialog", "plugins", "simd"] -default = ["default-publish", "egui-ui", "hot-reloading", "shaderc-from-source"] +# Temp for bug on current wgpu version that has access violation in vulkan when constructing egui pipeline +default-no-egui = ["default-publish", "hot-reloading", "shaderc-from-source"] +default = ["default-no-egui", "egui-ui"] [dependencies] client = {package = "veloren-client", path = "../client"} diff --git a/voxygen/src/audio/ambient.rs b/voxygen/src/audio/ambient.rs index 1c3941216b..8382a43be7 100644 --- a/voxygen/src/audio/ambient.rs +++ b/voxygen/src/audio/ambient.rs @@ -11,15 +11,15 @@ use common::{ use common_state::State; use serde::Deserialize; use std::time::Instant; +use strum::IntoEnumIterator; use tracing::warn; use vek::*; #[derive(Debug, Default, Deserialize)] -struct AmbientCollection { +pub struct AmbientCollection { tracks: Vec, } -/// Configuration for a single music track in the soundtrack #[derive(Debug, Deserialize)] pub struct AmbientItem { path: String, @@ -30,28 +30,10 @@ pub struct AmbientItem { } pub struct AmbientMgr { - soundtrack: AssetHandle, - began_playing: Instant, - next_track_change: f32, - volume: f32, - tree_multiplier: f32, -} - -impl Default for AmbientMgr { - fn default() -> Self { - Self { - soundtrack: Self::load_soundtrack_items(), - began_playing: Instant::now(), - next_track_change: 0.0, - volume: 0.0, - tree_multiplier: 0.0, - } - } + pub ambience: AssetHandle, } impl AmbientMgr { - /// Checks whether the previous track has completed. If so, sends a - /// request to play the next (random) track pub fn maintain( &mut self, audio: &mut AudioFrontend, @@ -59,82 +41,203 @@ impl AmbientMgr { client: &Client, camera: &Camera, ) { - if audio.sfx_enabled() && !self.soundtrack.read().tracks.is_empty() { - let focus_off = camera.get_focus_pos().map(f32::trunc); - let cam_pos = camera.dependents().cam_pos + focus_off; + // Checks if the ambience volume is set to zero or audio is disabled + // This prevents us from running all the following code unnecessarily + if !audio.ambience_enabled() { + return; + } + let ambience_volume = audio.get_ambience_volume(); + let ambience = self.ambience.read(); + // Iterate through each tag + for tag in AmbientChannelTag::iter() { + // If the conditions warrant creating a channel of that tag + if AmbientChannelTag::get_tag_volume(tag, client, camera) + > match tag { + AmbientChannelTag::Wind => 0.0, + AmbientChannelTag::Rain => 0.1, + AmbientChannelTag::Thunder => 0.0, + AmbientChannelTag::Leaves => 0.1, + } + && audio.get_ambient_channel(tag).is_none() + { + audio.new_ambient_channel(tag); + } + // If a channel exists run volume code + if let Some(channel_index) = audio.get_ambient_channel_index(tag) { + let channel = &mut audio.ambient_channels[channel_index]; - let (terrain_alt, tree_density) = if let Some(chunk) = client.current_chunk() { - (chunk.meta().alt(), chunk.meta().tree_density()) - } else { - (0.0, 0.0) - }; + // Maintain: get the correct multiplier of whatever the tag of the current + // channel is + let target_volume = get_target_volume(tag, state, client, camera); + // Get multiplier of the current channel + let initial_volume = channel.multiplier; - // The following code is specifically for wind, as it is the only - // non-positional ambient sound in the game. Others can be added - // as seen fit. + // Lerp multiplier of current channel + // TODO: Make this not framerate dependent + channel.multiplier = Lerp::lerp(initial_volume, target_volume, 0.02); + + // Update with sfx volume + channel.set_volume(ambience_volume); + + // Set the duration of the loop to whatever the current value is (0.0 by + // default) + + // If the sound should loop at this point: + if channel.began_playing.elapsed().as_secs_f32() > channel.next_track_change { + let track = ambience.tracks.iter().find(|track| track.tag == tag); + // Set the channel's start point at this instant + channel.began_playing = Instant::now(); + if let Some(track) = track { + // Set loop duration to the one specified in the ron + channel.next_track_change = track.length; + // Play the file of the current tag at the current multiplier; + let current_multiplier = channel.multiplier; + audio.play_ambient(tag, &track.path, current_multiplier); + } + }; + + // Remove channel if not playing + if audio.ambient_channels[channel_index].multiplier <= 0.001 { + audio.ambient_channels.remove(channel_index); + }; + } + } + } +} + +impl AmbientChannelTag { + // Gets appropriate volume for each tag + pub fn get_tag_volume(tag: AmbientChannelTag, client: &Client, camera: &Camera) -> f32 { + match tag { + AmbientChannelTag::Wind => { + let focus_off = camera.get_focus_pos().map(f32::trunc); + let cam_pos = camera.dependents().cam_pos + focus_off; + + let (terrain_alt, tree_density) = if let Some(chunk) = client.current_chunk() { + (chunk.meta().alt(), chunk.meta().tree_density()) + } else { + (0.0, 0.0) + }; - let target_volume = { // Wind volume increases with altitude let alt_multiplier = (cam_pos.z / 1200.0).abs(); // Tree density factors into wind volume. The more trees, // the lower wind volume. The trees make more of an impact // the closer the camera is to the ground. - self.tree_multiplier = ((1.0 - tree_density) + let tree_multiplier = ((1.0 - tree_density) + ((cam_pos.z - terrain_alt).abs() / 150.0).powi(2)) .min(1.0); - let mut volume_multiplier = alt_multiplier * self.tree_multiplier; + // Lastly, we of course have to take into account actual wind speed from + // weathersim + // Client wind speed is a float approx. -30.0 to 30.0 (polarity depending on + // direction) + let wind_speed_multiplier = (client.weather_at_player().wind.magnitude_squared() + / 30.0_f32.powi(2)) + .min(1.0); - // Checks if the camera is underwater to stop ambient sounds - if state - .terrain() - .get((cam_pos).map(|e| e.floor() as i32)) - .map(|b| b.is_liquid()) - .unwrap_or(false) - { - volume_multiplier *= 0.1; + alt_multiplier + * tree_multiplier + * (wind_speed_multiplier + ((cam_pos.z - terrain_alt).abs() / 150.0).powi(2)) + .min(1.0) + }, + AmbientChannelTag::Rain => { + let focus_off = camera.get_focus_pos().map(f32::trunc); + let cam_pos = camera.dependents().cam_pos + focus_off; + + let terrain_alt = if let Some(chunk) = client.current_chunk() { + chunk.meta().alt() + } else { + 0.0 + }; + // Make rain diminish with camera distance above terrain + let camera_multiplier = + 1.0 - ((cam_pos.z - terrain_alt).abs() / 75.0).powi(2).min(1.0); + + let rain_intensity = (client.weather_at_player().rain * 500.0) * camera_multiplier; + + rain_intensity.min(0.9) + }, + AmbientChannelTag::Thunder => { + let rain_intensity = client.weather_at_player().rain * 500.0; + + if rain_intensity < 0.7 { + 0.0 + } else { + rain_intensity } - if cam_pos.z < terrain_alt - 10.0 { - volume_multiplier = 0.0; + }, + AmbientChannelTag::Leaves => { + let focus_off = camera.get_focus_pos().map(f32::trunc); + let cam_pos = camera.dependents().cam_pos + focus_off; + + let (terrain_alt, tree_density) = if let Some(chunk) = client.current_chunk() { + (chunk.meta().alt(), chunk.meta().tree_density()) + } else { + (0.0, 0.0) + }; + + // Tree density factors into leaves volume. The more trees, + // the higher volume. The trees make more of an impact + // the closer the camera is to the ground + let tree_multiplier = 1.0 + - (((1.0 - tree_density) + + ((cam_pos.z - terrain_alt + 20.0).abs() / 150.0).powi(2)) + .min(1.0)); + + if tree_multiplier > 0.1 { + tree_multiplier + } else { + 0.0 } - - volume_multiplier.clamped(0.0, 1.0) - }; - - // Transitions the ambient sounds (more) smoothly - self.volume = audio.get_ambient_volume(); - audio.set_ambient_volume(Lerp::lerp(self.volume, target_volume, 0.01)); - - if self.began_playing.elapsed().as_secs_f32() > self.next_track_change { - // Right now there is only wind non-positional sfx so it is always - // selected. Modify this variable assignment when adding other non- - // positional sfx - let soundtrack = self.soundtrack.read(); - let track = &soundtrack - .tracks - .iter() - .find(|track| track.tag == AmbientChannelTag::Wind); - - if let Some(track) = track { - self.began_playing = Instant::now(); - self.next_track_change = track.length; - - audio.play_ambient(AmbientChannelTag::Wind, &track.path, target_volume); - } - } + }, } } +} - fn load_soundtrack_items() -> AssetHandle { - AmbientCollection::load_or_insert_with("voxygen.audio.ambient", |error| { - warn!( - "Error reading ambience config file, ambience will not be available: {:#?}", - error - ); - AmbientCollection::default() - }) +/// Checks various factors to determine the target volume to lerp to +fn get_target_volume( + tag: AmbientChannelTag, + state: &State, + client: &Client, + camera: &Camera, +) -> f32 { + let focus_off = camera.get_focus_pos().map(f32::trunc); + let cam_pos = camera.dependents().cam_pos + focus_off; + + let mut volume_multiplier: f32 = AmbientChannelTag::get_tag_volume(tag, client, camera); + + let terrain_alt = if let Some(chunk) = client.current_chunk() { + chunk.meta().alt() + } else { + 0.0 + }; + // Checks if the camera is underwater to diminish ambient sounds + if state + .terrain() + .get((cam_pos).map(|e| e.floor() as i32)) + .map(|b| b.is_liquid()) + .unwrap_or(false) + { + volume_multiplier *= 0.1; } + // Is the camera roughly under the terrain? + if cam_pos.z < terrain_alt - 20.0 { + volume_multiplier = 0.0; + } + + volume_multiplier.clamped(0.0, 1.0) +} + +pub fn load_ambience_items() -> AssetHandle { + AmbientCollection::load_or_insert_with("voxygen.audio.ambient", |error| { + warn!( + "Error reading ambience config file, ambience will not be available: {:#?}", + error + ); + AmbientCollection::default() + }) } impl assets::Asset for AmbientCollection { diff --git a/voxygen/src/audio/channel.rs b/voxygen/src/audio/channel.rs index 91c53d6b63..3d3a5642f1 100644 --- a/voxygen/src/audio/channel.rs +++ b/voxygen/src/audio/channel.rs @@ -11,7 +11,7 @@ //! [`AudioSettings`](../../settings/struct.AudioSettings.html) //! //! When the AudioFrontend's -//! [`play_sfx`](../struct.AudioFrontend.html#method.play_sfx) +//! [`emit_sfx`](../struct.AudioFrontend.html#method.emit_sfx) //! methods is called, it attempts to retrieve an SfxChannel for playback. If //! the channel capacity has been reached and all channels are occupied, a //! warning is logged, and no sound is played. @@ -22,6 +22,8 @@ use crate::audio::{ }; use rodio::{OutputStreamHandle, Sample, Sink, Source, SpatialSink}; use serde::Deserialize; +use std::time::Instant; +use strum::EnumIter; use tracing::warn; use vek::*; @@ -157,16 +159,22 @@ impl MusicChannel { /// AmbientChannelTags are used for non-positional sfx. Currently the only use /// is for wind. -#[derive(Debug, PartialEq, Clone, Copy, Deserialize)] +#[derive(Debug, PartialEq, Clone, Copy, Deserialize, EnumIter)] pub enum AmbientChannelTag { Wind, + Rain, + Thunder, + Leaves, } + /// A AmbientChannel uses a non-positional audio sink designed to play sounds /// which are always heard at the camera's position. pub struct AmbientChannel { tag: AmbientChannelTag, - multiplier: f32, + pub multiplier: f32, sink: Sink, + pub began_playing: Instant, + pub next_track_change: f32, } impl AmbientChannel { @@ -177,13 +185,17 @@ impl AmbientChannel { tag, multiplier, sink, + began_playing: Instant::now(), + next_track_change: 0.0, }, Err(_) => { - warn!("Failed to create rodio sink. May not play wind sounds."); + warn!("Failed to create rodio sink. May not play ambient sounds."); Self { tag, multiplier, sink: Sink::new_idle().0, + began_playing: Instant::now(), + next_track_change: 0.0, } }, } @@ -203,11 +215,11 @@ impl AmbientChannel { pub fn set_volume(&mut self, volume: f32) { self.sink.set_volume(volume * self.multiplier); } - pub fn set_multiplier(&mut self, multiplier: f32) { self.multiplier = multiplier; } - - pub fn get_volume(&mut self) -> f32 { self.sink.volume() } + // pub fn get_volume(&mut self) -> f32 { self.sink.volume() } pub fn get_tag(&self) -> AmbientChannelTag { self.tag } + + // pub fn set_tag(&mut self, tag: AmbientChannelTag) { self.tag = tag } } /// An SfxChannel uses a positional audio sink, and is designed for short-lived @@ -252,6 +264,8 @@ impl SfxChannel { pub fn set_volume(&mut self, volume: f32) { self.sink.set_volume(volume); } + pub fn stop(&mut self) { self.sink.stop(); } + pub fn is_done(&self) -> bool { self.sink.empty() } pub fn set_pos(&mut self, pos: Vec3) { self.pos = pos; } @@ -267,3 +281,31 @@ impl SfxChannel { .set_right_ear_position(listener.ear_right_rpos.into_array()); } } + +pub struct UiChannel { + sink: Sink, +} + +impl UiChannel { + pub fn new(stream: &OutputStreamHandle) -> Self { + Self { + sink: Sink::try_new(stream).unwrap(), + } + } + + pub fn play(&mut self, source: S) + where + S: Source + Send + 'static, + S::Item: Sample, + S::Item: Send, + ::Item: std::fmt::Debug, + { + self.sink.append(source); + } + + pub fn set_volume(&mut self, volume: f32) { self.sink.set_volume(volume); } + + pub fn stop(&mut self) { self.sink.stop(); } + + pub fn is_done(&self) -> bool { self.sink.empty() } +} diff --git a/voxygen/src/audio/mod.rs b/voxygen/src/audio/mod.rs index 005cb91683..482b8d0d20 100644 --- a/voxygen/src/audio/mod.rs +++ b/voxygen/src/audio/mod.rs @@ -7,7 +7,9 @@ pub mod music; pub mod sfx; pub mod soundcache; -use channel::{AmbientChannel, AmbientChannelTag, MusicChannel, MusicChannelTag, SfxChannel}; +use channel::{ + AmbientChannel, AmbientChannelTag, MusicChannel, MusicChannelTag, SfxChannel, UiChannel, +}; use fader::Fader; use music::MusicTransitionManifest; use sfx::{SfxEvent, SfxTriggerItem}; @@ -43,7 +45,9 @@ pub struct AudioFrontend { music_channels: Vec, ambient_channels: Vec, sfx_channels: Vec, + ui_channels: Vec, sfx_volume: f32, + ambience_volume: f32, music_volume: f32, master_volume: f32, listener: Listener, @@ -53,7 +57,7 @@ pub struct AudioFrontend { impl AudioFrontend { /// Construct with given device - pub fn new(/* dev: String, */ num_sfx_channels: usize) -> Self { + pub fn new(/* dev: String, */ num_sfx_channels: usize, num_ui_channels: usize) -> Self { // Commented out until audio device switcher works //let audio_device = get_device_raw(&dev); @@ -77,7 +81,9 @@ impl AudioFrontend { }; let mut sfx_channels = Vec::with_capacity(num_sfx_channels); + let mut ui_channels = Vec::with_capacity(num_ui_channels); if let Some(audio_stream) = &audio_stream { + ui_channels.resize_with(num_ui_channels, || UiChannel::new(audio_stream)); sfx_channels.resize_with(num_sfx_channels, || SfxChannel::new(audio_stream)); }; @@ -90,8 +96,10 @@ impl AudioFrontend { audio_stream, music_channels: Vec::new(), sfx_channels, + ui_channels, ambient_channels: Vec::new(), sfx_volume: 1.0, + ambience_volume: 1.0, music_volume: 1.0, master_volume: 1.0, listener: Listener::default(), @@ -119,8 +127,10 @@ impl AudioFrontend { audio_stream: None, music_channels: Vec::new(), sfx_channels: Vec::new(), + ui_channels: Vec::new(), ambient_channels: Vec::new(), sfx_volume: 1.0, + ambience_volume: 1.0, music_volume: 1.0, master_volume: 1.0, listener: Listener::default(), @@ -151,6 +161,19 @@ impl AudioFrontend { None } + fn get_ui_channel(&mut self) -> Option<&mut UiChannel> { + if self.audio_stream.is_some() { + let sfx_volume = self.get_sfx_volume(); + if let Some(channel) = self.ui_channels.iter_mut().find(|c| c.is_done()) { + channel.set_volume(sfx_volume); + + return Some(channel); + } + } + + None + } + /// Retrieve a music channel from the channel list. This inspects the /// MusicChannelTag to determine whether we are transitioning between /// music types and acts accordingly. For example transitioning between @@ -193,36 +216,6 @@ impl AudioFrontend { self.music_channels.last_mut() } - /// Function to play sfx from external places. Useful for UI and - /// inventory events - pub fn emit_sfx_item(&mut self, trigger_item: Option<(&SfxEvent, &SfxTriggerItem)>) { - if let Some((event, item)) = trigger_item { - let sfx_file = match item.files.len() { - 0 => { - debug!("Sfx event {:?} is missing audio file.", event); - "voxygen.audio.sfx.placeholder" - }, - 1 => item - .files - .last() - .expect("Failed to determine sound file for this trigger item."), - _ => { - // If more than one file is listed, choose one at random - let rand_step = rand::random::() % item.files.len(); - &item.files[rand_step] - }, - }; - - // TODO: Should this take `underwater` into consideration? - match self.play_sfx(sfx_file, self.listener.pos, None, false) { - Ok(_) => {}, - Err(e) => warn!("Failed to play sfx '{:?}'. {}", sfx_file, e), - } - } else { - debug!("Missing sfx trigger config for external sfx event.",); - } - } - /// Play an sfx file given the position, SfxEvent, and whether it is /// underwater or not pub fn emit_sfx( @@ -233,6 +226,9 @@ impl AudioFrontend { underwater: bool, ) { if let Some((event, item)) = trigger_item { + // Find sound based on given trigger_item + // Randomizes if multiple sounds are found + // Errors if no sounds are found let sfx_file = match item.files.len() { 0 => { debug!("Sfx event {:?} is missing audio file.", event); @@ -248,10 +244,20 @@ impl AudioFrontend { &item.files[rand_step] }, }; + // Play sound in empty channel at given position + if self.audio_stream.is_some() { + let sound = load_ogg(sfx_file).amplify(volume.unwrap_or(1.0)); - match self.play_sfx(sfx_file, position, volume, underwater) { - Ok(_) => {}, - Err(e) => warn!("Failed to play sfx '{:?}'. {}", sfx_file, e), + let listener = self.listener.clone(); + if let Some(channel) = self.get_sfx_channel() { + channel.set_pos(position); + channel.update(&listener); + if underwater { + channel.play_with_low_pass_filter(sound.convert_samples()); + } else { + channel.play(sound); + } + } } } else { debug!( @@ -261,32 +267,47 @@ impl AudioFrontend { } } - /// Play (once) an sfx file by file path at the given position and volume. - /// If `underwater` is true, the sound is played with a low pass filter - pub fn play_sfx( + /// Plays a sfx using a non-spatial sink at the given volume; doesn't need a + /// position + /// Passing no volume will default to 1.0 + pub fn emit_ui_sfx( &mut self, - sound: &str, - pos: Vec3, - vol: Option, - underwater: bool, - ) -> Result<(), rodio::decoder::DecoderError> { - if self.audio_stream.is_some() { - let sound = load_ogg(sound).amplify(vol.unwrap_or(1.0)); + trigger_item: Option<(&SfxEvent, &SfxTriggerItem)>, + volume: Option, + ) { + // Find sound based on given trigger_item + // Randomizes if multiple sounds are found + // Errors if no sounds are found + if let Some((event, item)) = trigger_item { + let sfx_file = match item.files.len() { + 0 => { + debug!("Sfx event {:?} is missing audio file.", event); + "voxygen.audio.sfx.placeholder" + }, + 1 => item + .files + .last() + .expect("Failed to determine sound file for this trigger item."), + _ => { + // If more than one file is listed, choose one at random + let rand_step = rand::random::() % item.files.len(); + &item.files[rand_step] + }, + }; + // Play sound in empty channel + if self.audio_stream.is_some() { + let sound = load_ogg(sfx_file).amplify(volume.unwrap_or(1.0)); - let listener = self.listener.clone(); - if let Some(channel) = self.get_sfx_channel() { - channel.set_pos(pos); - channel.update(&listener); - if underwater { - channel.play_with_low_pass_filter(sound.convert_samples()); - } else { + if let Some(channel) = self.get_ui_channel() { channel.play(sound); } } + } else { + debug!("Missing sfx trigger config for external sfx event.",); } - Ok(()) } + /// Plays a file at a given volume in the channel with a given tag fn play_ambient( &mut self, channel_tag: AmbientChannelTag, @@ -294,60 +315,75 @@ impl AudioFrontend { volume_multiplier: f32, ) { if self.audio_stream.is_some() { - if let Some(channel) = self.get_ambient_channel(channel_tag, volume_multiplier) { + if let Some(channel) = self.get_ambient_channel(channel_tag) { + channel.set_volume(volume_multiplier); channel.play(load_ogg(sound)); } } } + /// Adds a new ambient channel of the given tag at zero volume + fn new_ambient_channel(&mut self, channel_tag: AmbientChannelTag) { + if let Some(audio_stream) = &self.audio_stream { + let ambient_channel = AmbientChannel::new(audio_stream, channel_tag, 0.0); + self.ambient_channels.push(ambient_channel); + } + } + + /// Retrieves the channel currently having the given tag + /// If no channel with the given tag is found, returns None fn get_ambient_channel( &mut self, channel_tag: AmbientChannelTag, - volume_multiplier: f32, ) -> Option<&mut AmbientChannel> { - if let Some(audio_stream) = &self.audio_stream { - if self.ambient_channels.is_empty() { - let mut ambient_channel = - AmbientChannel::new(audio_stream, channel_tag, volume_multiplier); - ambient_channel.set_volume(self.get_sfx_volume()); - self.ambient_channels.push(ambient_channel); - } else { - let sfx_volume = self.get_sfx_volume(); - for channel in self.ambient_channels.iter_mut() { - if channel.get_tag() == channel_tag { - channel.set_multiplier(volume_multiplier); - channel.set_volume(sfx_volume); - return Some(channel); - } - } - } - } - - None - } - - fn set_ambient_volume(&mut self, volume_multiplier: f32) { if self.audio_stream.is_some() { - let sfx_volume = self.get_sfx_volume(); - if let Some(channel) = self.ambient_channels.iter_mut().last() { - channel.set_multiplier(volume_multiplier); - channel.set_volume(sfx_volume); - } - } - } - - fn get_ambient_volume(&mut self) -> f32 { - if self.audio_stream.is_some() { - if let Some(channel) = self.ambient_channels.iter_mut().last() { - channel.get_volume() / self.get_sfx_volume() - } else { - 0.0 - } + self.ambient_channels + .iter_mut() + .find(|channel| channel.get_tag() == channel_tag) } else { - 0.0 + None } } + /// Retrieves the index of the channel having the given tag in the array of + /// ambient channels This is used for times when borrowing becomes + /// difficult If no channel with the given tag is found, returns None + fn get_ambient_channel_index(&self, channel_tag: AmbientChannelTag) -> Option { + if self.audio_stream.is_some() { + self.ambient_channels + .iter() + .position(|channel| channel.get_tag() == channel_tag) + } else { + None + } + } + + // Unused code that may be useful in the future: + // Sets the volume of the channel with the given tag to the given volume + // fn set_ambient_volume(&mut self, channel_tag: AmbientChannelTag, + // volume_multiplier: f32) { if self.audio_stream.is_some() { + // let sfx_volume = self.get_sfx_volume(); + // if let Some(channel) = self.get_ambient_channel(channel_tag) { + // channel.set_multiplier(volume_multiplier); + // channel.set_volume(sfx_volume); + // } + // } + // } + + // Retrieves volume (pre-sfx-setting) of the channel with a given tag + // fn get_ambient_volume(&mut self, channel_tag: AmbientChannelTag) -> f32 { + // if self.audio_stream.is_some() { + // if let Some(channel) = self.get_ambient_channel(channel_tag) { + // let channel_multiplier = channel.get_multiplier(); + // channel_multiplier + // } else { + // 0.0 + // } + // } else { + // 0.0 + // } + // } + fn play_music(&mut self, sound: &str, channel_tag: MusicChannelTag) { if self.music_enabled() { if let Some(channel) = self.get_music_channel(channel_tag) { @@ -406,18 +442,40 @@ impl AudioFrontend { } } + /// Retrieves the current setting for sfx volume pub fn get_sfx_volume(&self) -> f32 { self.sfx_volume * self.master_volume } + /// Retrieves the current setting for ambience volume + pub fn get_ambience_volume(&self) -> f32 { self.ambience_volume * self.master_volume } + + /// Retrieves the current setting for music volume pub fn get_music_volume(&self) -> f32 { self.music_volume * self.master_volume } pub fn sfx_enabled(&self) -> bool { self.get_sfx_volume() > 0.0 } + pub fn ambience_enabled(&self) -> bool { self.get_ambience_volume() > 0.0 } + pub fn music_enabled(&self) -> bool { self.get_music_volume() > 0.0 } pub fn set_sfx_volume(&mut self, sfx_volume: f32) { self.sfx_volume = sfx_volume; - self.update_sfx_volumes(); + let sfx_volume = self.get_sfx_volume(); + for channel in self.sfx_channels.iter_mut() { + channel.set_volume(sfx_volume); + } + for channel in self.ui_channels.iter_mut() { + channel.set_volume(sfx_volume); + } + } + + pub fn set_ambience_volume(&mut self, ambience_volume: f32) { + self.ambience_volume = ambience_volume; + + let ambience_volume = self.get_ambience_volume(); + for channel in self.ambient_channels.iter_mut() { + channel.set_volume(ambience_volume) + } } pub fn set_music_volume(&mut self, music_volume: f32) { @@ -429,6 +487,7 @@ impl AudioFrontend { } } + /// Updates master volume in all channels pub fn set_master_volume(&mut self, master_volume: f32) { self.master_volume = master_volume; @@ -436,19 +495,17 @@ impl AudioFrontend { for channel in self.music_channels.iter_mut() { channel.set_volume(music_volume); } - - self.update_sfx_volumes(); - } - - fn update_sfx_volumes(&mut self) { let sfx_volume = self.get_sfx_volume(); for channel in self.sfx_channels.iter_mut() { channel.set_volume(sfx_volume); } - - for channel in self.ambient_channels.iter_mut() { + for channel in self.ui_channels.iter_mut() { channel.set_volume(sfx_volume); } + let ambience_volume = self.get_ambience_volume(); + for channel in self.ambient_channels.iter_mut() { + channel.set_volume(ambience_volume) + } } pub fn stop_ambient_sounds(&mut self) { @@ -457,6 +514,15 @@ impl AudioFrontend { } } + pub fn stop_all_sfx(&mut self) { + for channel in self.sfx_channels.iter_mut() { + channel.stop() + } + for channel in self.ui_channels.iter_mut() { + channel.stop() + } + } + // The following is for the disabled device switcher //// TODO: figure out how badly this will break things when it is called //pub fn set_device(&mut self, name: String) { diff --git a/voxygen/src/audio/music.rs b/voxygen/src/audio/music.rs index d4c974b1bb..262e9aa4b8 100644 --- a/voxygen/src/audio/music.rs +++ b/voxygen/src/audio/music.rs @@ -48,6 +48,7 @@ use client::Client; use common::{ assets::{self, AssetExt, AssetHandle}, terrain::{BiomeKind, SitesKind}, + weather::WeatherKind, }; use common_state::State; use hashbrown::HashMap; @@ -78,6 +79,8 @@ pub struct SoundtrackItem { length: f32, /// Whether this track should play during day or night timing: Option, + /// Whether this track should play during a certain weather + weather: Option, /// What biomes this track should play in with chance of play biomes: Vec<(BiomeKind, u8)>, /// Whether this track should play in a specific site @@ -98,6 +101,7 @@ enum RawSoundtrackItem { Segmented { title: String, timing: Option, + weather: Option, biomes: Vec<(BiomeKind, u8)>, site: Option, segments: Vec<(String, f32, MusicState, Option)>, @@ -224,6 +228,11 @@ impl MusicMgr { use common::comp::{group::ENEMY, Group, Health, Pos}; use specs::{Join, WorldExt}; + // Checks if the music volume is set to zero or audio is disabled + // This prevents us from running all the following code unnecessarily + if !audio.music_enabled() { + return; + } let mut activity_state = MusicActivity::Explore; @@ -328,6 +337,7 @@ impl MusicMgr { let is_dark = (state.get_day_period().is_dark()) as bool; let current_period_of_day = Self::get_current_day_period(is_dark); + let current_weather = client.weather_at_player(); let current_biome = client.current_biome(); let current_site = client.current_site(); @@ -348,6 +358,9 @@ impl MusicMgr { }) && match &track.site { Some(site) => site == ¤t_site, None => true, + } && match &track.weather { + Some(weather) => weather == ¤t_weather.get_kind(), + None => true, } }) .filter(|track| { @@ -457,6 +470,7 @@ impl assets::Compound for SoundtrackCollection { RawSoundtrackItem::Segmented { title, timing, + weather, biomes, site, segments, @@ -467,6 +481,7 @@ impl assets::Compound for SoundtrackCollection { path, length, timing: timing.clone(), + weather, biomes: biomes.clone(), site, music_state, diff --git a/voxygen/src/audio/sfx/mod.rs b/voxygen/src/audio/sfx/mod.rs index 6749138dd0..1a45c39927 100644 --- a/voxygen/src/audio/sfx/mod.rs +++ b/voxygen/src/audio/sfx/mod.rs @@ -82,6 +82,8 @@ mod event_mapper; +use specs::WorldExt; + use crate::{ audio::AudioFrontend, scene::{Camera, Terrain}, @@ -100,6 +102,7 @@ use common::{ }, outcome::Outcome, terrain::{BlockKind, TerrainChunk}, + uid::Uid, }; use common_state::State; use event_mapper::SfxEventMapper; @@ -293,6 +296,7 @@ pub enum SfxInventoryEvent { Dropped, Given, Swapped, + Craft, } // TODO Move to a separate event mapper? @@ -325,6 +329,7 @@ impl From<&InventoryUpdateEvent> for SfxEvent { InventoryUpdateEvent::Dropped => SfxEvent::Inventory(SfxInventoryEvent::Dropped), InventoryUpdateEvent::Given => SfxEvent::Inventory(SfxInventoryEvent::Given), InventoryUpdateEvent::Swapped => SfxEvent::Inventory(SfxInventoryEvent::Swapped), + InventoryUpdateEvent::Craft => SfxEvent::Inventory(SfxInventoryEvent::Craft), _ => SfxEvent::Inventory(SfxInventoryEvent::Swapped), } } @@ -407,11 +412,13 @@ impl SfxMgr { outcome: &Outcome, audio: &mut AudioFrontend, client: &Client, + underwater: bool, ) { if !audio.sfx_enabled() { return; } let triggers = self.triggers.read(); + let uids = client.state().ecs().read_storage::(); // TODO handle underwater match outcome { @@ -421,12 +428,12 @@ impl SfxMgr { sfx_trigger_item, *pos, Some((power.abs() / 2.5).min(1.5)), - false, + underwater, ); }, Outcome::GroundSlam { pos, .. } => { let sfx_trigger_item = triggers.get_key_value(&SfxEvent::GroundSlam); - audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), false); + audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), underwater); }, Outcome::ProjectileShot { pos, body, .. } => { match body { @@ -437,7 +444,7 @@ impl SfxMgr { | object::Body::ArrowTurret, ) => { let sfx_trigger_item = triggers.get_key_value(&SfxEvent::ArrowShot); - audio.emit_sfx(sfx_trigger_item, *pos, None, false); + audio.emit_sfx(sfx_trigger_item, *pos, None, underwater); }, Body::Object( object::Body::BoltFire @@ -445,7 +452,7 @@ impl SfxMgr { | object::Body::BoltNature, ) => { let sfx_trigger_item = triggers.get_key_value(&SfxEvent::FireShot); - audio.emit_sfx(sfx_trigger_item, *pos, None, false); + audio.emit_sfx(sfx_trigger_item, *pos, None, underwater); }, _ => { // not mapped to sfx file @@ -467,37 +474,41 @@ impl SfxMgr { ) => { if target.is_none() { let sfx_trigger_item = triggers.get_key_value(&SfxEvent::ArrowMiss); - audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), false); + audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), underwater); } else if *source == client.uid() { let sfx_trigger_item = triggers.get_key_value(&SfxEvent::ArrowHit); audio.emit_sfx( sfx_trigger_item, client.position().unwrap_or(*pos), Some(2.0), - false, + underwater, ); } else { let sfx_trigger_item = triggers.get_key_value(&SfxEvent::ArrowHit); - audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), false); + audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), underwater); } }, _ => {}, }, - Outcome::SkillPointGain { pos, .. } => { - let sfx_trigger_item = triggers.get_key_value(&SfxEvent::SkillPointGain); - audio.emit_sfx(sfx_trigger_item, *pos, None, false); + Outcome::SkillPointGain { uid, .. } => { + if let Some(client_uid) = uids.get(client.entity()) { + if uid == client_uid { + let sfx_trigger_item = triggers.get_key_value(&SfxEvent::SkillPointGain); + audio.emit_ui_sfx(sfx_trigger_item, Some(0.4)); + } + } }, Outcome::Beam { pos, specifier } => match specifier { beam::FrontendSpecifier::LifestealBeam => { if thread_rng().gen_bool(0.5) { let sfx_trigger_item = triggers.get_key_value(&SfxEvent::SceptreBeam); - audio.emit_sfx(sfx_trigger_item, *pos, None, false); + audio.emit_sfx(sfx_trigger_item, *pos, None, underwater); }; }, beam::FrontendSpecifier::Flamethrower | beam::FrontendSpecifier::Cultist => { if thread_rng().gen_bool(0.5) { let sfx_trigger_item = triggers.get_key_value(&SfxEvent::FlameThrower); - audio.emit_sfx(sfx_trigger_item, *pos, None, false); + audio.emit_sfx(sfx_trigger_item, *pos, None, underwater); } }, beam::FrontendSpecifier::ClayGolem @@ -511,27 +522,27 @@ impl SfxMgr { sfx_trigger_item, pos.map(|e| e as f32 + 0.5), Some(3.0), - false, + underwater, ); }, Outcome::HealthChange { pos, info, .. } => { // Don't emit sound effects from positive damage (healing) if info.amount < Health::HEALTH_EPSILON { let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Damage); - audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), false); + audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), underwater); } }, Outcome::Death { pos, .. } => { let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Death); - audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), false); + audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), underwater); }, Outcome::Block { pos, parry, .. } => { if *parry { let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Parry); - audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), false); + audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), underwater); } else { let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Block); - audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), false); + audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), underwater); } }, Outcome::PoiseChange { pos, state, .. } => match state { @@ -539,22 +550,22 @@ impl SfxMgr { PoiseState::Interrupted => { let sfx_trigger_item = triggers.get_key_value(&SfxEvent::PoiseChange(PoiseState::Interrupted)); - audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), false); + audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), underwater); }, PoiseState::Stunned => { let sfx_trigger_item = triggers.get_key_value(&SfxEvent::PoiseChange(PoiseState::Stunned)); - audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), false); + audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), underwater); }, PoiseState::Dazed => { let sfx_trigger_item = triggers.get_key_value(&SfxEvent::PoiseChange(PoiseState::Dazed)); - audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), false); + audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), underwater); }, PoiseState::KnockedDown => { let sfx_trigger_item = triggers.get_key_value(&SfxEvent::PoiseChange(PoiseState::KnockedDown)); - audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), false); + audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), underwater); }, }, Outcome::Utterance { pos, kind, body } => { @@ -562,7 +573,7 @@ impl SfxMgr { let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Utterance(*kind, voice)); if let Some(sfx_trigger_item) = sfx_trigger_item { - audio.emit_sfx(Some(sfx_trigger_item), *pos, Some(1.5), false); + audio.emit_sfx(Some(sfx_trigger_item), *pos, Some(1.5), underwater); } else { debug!( "No utterance sound effect exists for ({:?}, {:?})", @@ -574,10 +585,10 @@ impl SfxMgr { Outcome::Glider { pos, wielded } => { if *wielded { let sfx_trigger_item = triggers.get_key_value(&SfxEvent::GliderOpen); - audio.emit_sfx(sfx_trigger_item, *pos, Some(1.0), false); + audio.emit_sfx(sfx_trigger_item, *pos, Some(1.0), underwater); } else { let sfx_trigger_item = triggers.get_key_value(&SfxEvent::GliderClose); - audio.emit_sfx(sfx_trigger_item, *pos, Some(1.0), false); + audio.emit_sfx(sfx_trigger_item, *pos, Some(1.0), underwater); } }, Outcome::ExpChange { .. } diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 3021f68b71..4379a47a01 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -264,6 +264,7 @@ widget_ids! { current_site, graphics_backend, gpu_timings[], + weather, // Game Version version, @@ -2385,12 +2386,28 @@ impl Hud { .font_id(self.fonts.cyri.conrod_id) .font_size(self.fonts.cyri.scale(14)) .set(self.ids.time, ui_widgets); + // Weather + let weather = client.weather_at_player(); + Text::new(&format!( + "Weather({kind}): {{cloud: {cloud:.2}, rain: {rain:.2}, wind: <{wind_x:.0}, \ + {wind_y:.0}>}}", + kind = weather.get_kind(), + cloud = weather.cloud, + rain = weather.rain, + wind_x = weather.wind.x, + wind_y = weather.wind.y + )) + .color(TEXT_COLOR) + .down_from(self.ids.time, V_PAD) + .font_id(self.fonts.cyri.conrod_id) + .font_size(self.fonts.cyri.scale(14)) + .set(self.ids.weather, ui_widgets); // Number of entities let entity_count = client.state().ecs().entities().join().count(); Text::new(&format!("Entity count: {}", entity_count)) .color(TEXT_COLOR) - .down_from(self.ids.time, V_PAD) + .down_from(self.ids.weather, V_PAD) .font_id(self.fonts.cyri.conrod_id) .font_size(self.fonts.cyri.scale(14)) .set(self.ids.entity_count, ui_widgets); @@ -2503,7 +2520,8 @@ impl Hud { // Set debug box dimensions, only timings height is dynamic // TODO: Make the background box size fully dynamic - let debug_bg_size = [320.0, 370.0 + timings_height]; + + let debug_bg_size = [320.0, 385.0 + timings_height]; Rectangle::fill(debug_bg_size) .rgba(0.0, 0.0, 0.0, global_state.settings.chat.chat_opacity) diff --git a/voxygen/src/hud/settings_window/sound.rs b/voxygen/src/hud/settings_window/sound.rs index 96fda99e6f..7d3e1a9503 100644 --- a/voxygen/src/hud/settings_window/sound.rs +++ b/voxygen/src/hud/settings_window/sound.rs @@ -31,6 +31,9 @@ widget_ids! { sfx_volume_text, sfx_volume_slider, sfx_volume_number, + ambience_volume_text, + ambience_volume_slider, + ambience_volume_number, //audio_device_list, //audio_device_text, reset_sound_button, @@ -246,6 +249,40 @@ impl<'a> Widget for Sound<'a> { .font_id(self.fonts.cyri.conrod_id) .color(TEXT_COLOR) .set(state.ids.sfx_volume_number, ui); + // Ambience Volume + Text::new(self.localized_strings.get("hud.settings.ambience_volume")) + .down_from(state.ids.sfx_volume_slider, 10.0) + .font_size(self.fonts.cyri.scale(14)) + .font_id(self.fonts.cyri.conrod_id) + .color(TEXT_COLOR) + .set(state.ids.ambience_volume_text, ui); + // Ambience Volume Slider + if let Some(new_val) = ImageSlider::continuous( + self.global_state.settings.audio.ambience_volume, + 0.0, + 1.0, + self.imgs.slider_indicator, + self.imgs.slider, + ) + .w_h(104.0, 22.0) + .down_from(state.ids.ambience_volume_text, 10.0) + .track_breadth(12.0) + .slider_length(10.0) + .pad_track((5.0, 5.0)) + .set(state.ids.ambience_volume_slider, ui) + { + events.push(AdjustAmbienceVolume(new_val)); + } + // Ambience Volume Number + Text::new(&format!( + "{:2.0}%", + self.global_state.settings.audio.ambience_volume * 100.0 + )) + .right_from(state.ids.ambience_volume_slider, 8.0) + .font_size(self.fonts.cyri.scale(14)) + .font_id(self.fonts.cyri.conrod_id) + .color(TEXT_COLOR) + .set(state.ids.ambience_volume_number, ui); // Audio Device Selector // -------------------------------------------- @@ -279,7 +316,7 @@ impl<'a> Widget for Sound<'a> { .w_h(RESET_BUTTONS_WIDTH, RESET_BUTTONS_HEIGHT) .hover_image(self.imgs.button_hover) .press_image(self.imgs.button_press) - .down_from(state.ids.sfx_volume_slider, 12.0) + .down_from(state.ids.ambience_volume_slider, 12.0) .label(self.localized_strings.get("hud.settings.reset_sound")) .label_font_size(self.fonts.cyri.scale(14)) .label_color(TEXT_COLOR) diff --git a/voxygen/src/hud/settings_window/video.rs b/voxygen/src/hud/settings_window/video.rs index 3bf10af9ea..6667abaa2e 100644 --- a/voxygen/src/hud/settings_window/video.rs +++ b/voxygen/src/hud/settings_window/video.rs @@ -116,6 +116,9 @@ widget_ids! { shadow_mode_map_resolution_text, shadow_mode_map_resolution_slider, shadow_mode_map_resolution_value, + rain_map_resolution_text, + rain_map_resolution_slider, + rain_map_resolution_value, save_window_size_button, } @@ -1116,11 +1119,51 @@ impl<'a> Widget for Video<'a> { .set(state.ids.shadow_mode_map_resolution_value, ui); } + // Rain occlusion texture size + // Display the shadow map mode if selected. + Text::new( + self.localized_strings + .get("hud.settings.rain_occlusion.resolution"), + ) + .down_from(state.ids.shadow_mode_list, 10.0) + .font_size(self.fonts.cyri.scale(14)) + .font_id(self.fonts.cyri.conrod_id) + .color(TEXT_COLOR) + .set(state.ids.rain_map_resolution_text, ui); + + if let Some(new_val) = ImageSlider::discrete( + (render_mode.rain_occlusion.resolution.log2() * 4.0).round() as i8, + -8, + 8, + self.imgs.slider_indicator, + self.imgs.slider, + ) + .w_h(104.0, 22.0) + .right_from(state.ids.rain_map_resolution_text, 8.0) + .track_breadth(12.0) + .slider_length(10.0) + .pad_track((5.0, 5.0)) + .set(state.ids.rain_map_resolution_slider, ui) + { + events.push(GraphicsChange::ChangeRenderMode(Box::new(RenderMode { + rain_occlusion: ShadowMapMode { + resolution: 2.0f32.powf(f32::from(new_val) / 4.0), + }, + ..render_mode.clone() + }))); + } + Text::new(&format!("{}", render_mode.rain_occlusion.resolution)) + .right_from(state.ids.rain_map_resolution_slider, 8.0) + .font_size(self.fonts.cyri.scale(14)) + .font_id(self.fonts.cyri.conrod_id) + .color(TEXT_COLOR) + .set(state.ids.rain_map_resolution_value, ui); + // GPU Profiler Text::new(self.localized_strings.get("hud.settings.gpu_profiler")) .font_size(self.fonts.cyri.scale(14)) .font_id(self.fonts.cyri.conrod_id) - .down_from(state.ids.shadow_mode_list, 8.0) + .down_from(state.ids.rain_map_resolution_text, 8.0) .color(TEXT_COLOR) .set(state.ids.gpu_profiler_label, ui); diff --git a/voxygen/src/lib.rs b/voxygen/src/lib.rs index 1411a128b8..e74d85ff69 100644 --- a/voxygen/src/lib.rs +++ b/voxygen/src/lib.rs @@ -11,7 +11,8 @@ trait_alias, option_get_or_insert_default, map_try_insert, - slice_as_chunks + slice_as_chunks, + unzip_option )] #![recursion_limit = "2048"] diff --git a/voxygen/src/main.rs b/voxygen/src/main.rs index 9b48cffaab..86c43540aa 100644 --- a/voxygen/src/main.rs +++ b/voxygen/src/main.rs @@ -223,13 +223,17 @@ fn main() { // Setup audio let mut audio = match settings.audio.output { AudioOutput::Off => AudioFrontend::no_audio(), - AudioOutput::Automatic => AudioFrontend::new(settings.audio.num_sfx_channels), + AudioOutput::Automatic => AudioFrontend::new( + settings.audio.num_sfx_channels, + settings.audio.num_ui_channels, + ), // AudioOutput::Device(ref dev) => Some(dev.clone()), }; audio.set_master_volume(settings.audio.master_volume); audio.set_music_volume(settings.audio.music_volume); audio.set_sfx_volume(settings.audio.sfx_volume); + audio.set_ambience_volume(settings.audio.ambience_volume); // Load the profile. let profile = Profile::load(&config_dir); diff --git a/voxygen/src/menu/main/scene.rs b/voxygen/src/menu/main/scene.rs index 77f5c709d0..68d25b8f47 100644 --- a/voxygen/src/menu/main/scene.rs +++ b/voxygen/src/menu/main/scene.rs @@ -1,6 +1,6 @@ use crate::render::{ - GlobalModel, Globals, GlobalsBindGroup, Light, LodData, PointLightMatrix, Renderer, Shadow, - ShadowLocals, + GlobalModel, Globals, GlobalsBindGroup, Light, LodData, PointLightMatrix, RainOcclusionLocals, + Renderer, Shadow, ShadowLocals, }; pub struct Scene { @@ -14,6 +14,8 @@ impl Scene { lights: renderer.create_consts(&[Light::default(); 32]), shadows: renderer.create_consts(&[Shadow::default(); 32]), shadow_mats: renderer.create_shadow_bound_locals(&[ShadowLocals::default()]), + rain_occlusion_mats: renderer + .create_rain_occlusion_bound_locals(&[RainOcclusionLocals::default()]), point_light_matrices: Box::new([PointLightMatrix::default(); 126]), }; diff --git a/voxygen/src/render/mod.rs b/voxygen/src/render/mod.rs index 9b50e4d4e8..a88e2f686f 100644 --- a/voxygen/src/render/mod.rs +++ b/voxygen/src/render/mod.rs @@ -30,6 +30,7 @@ pub use self::{ lod_terrain::{LodData, Vertex as LodTerrainVertex}, particle::{Instance as ParticleInstance, Vertex as ParticleVertex}, postprocess::Locals as PostProcessLocals, + rain_occlusion::Locals as RainOcclusionLocals, shadow::{Locals as ShadowLocals, PointLightMatrix}, skybox::{create_mesh as create_skybox_mesh, Vertex as SkyboxVertex}, sprite::{ @@ -122,6 +123,10 @@ pub enum CloudMode { High, } +impl CloudMode { + pub fn is_enabled(&self) -> bool { *self != CloudMode::None } +} + impl Default for CloudMode { fn default() -> Self { CloudMode::High } } @@ -342,6 +347,7 @@ pub struct RenderMode { pub fluid: FluidMode, pub lighting: LightingMode, pub shadow: ShadowMode, + pub rain_occlusion: ShadowMapMode, pub bloom: BloomMode, /// 0.0..1.0 pub point_glow: f32, @@ -361,6 +367,7 @@ impl Default for RenderMode { fluid: FluidMode::default(), lighting: LightingMode::default(), shadow: ShadowMode::default(), + rain_occlusion: ShadowMapMode::default(), bloom: BloomMode::default(), point_glow: 0.35, experimental_shaders: HashSet::default(), @@ -380,6 +387,7 @@ impl RenderMode { fluid: self.fluid, lighting: self.lighting, shadow: self.shadow, + rain_occlusion: self.rain_occlusion, bloom: self.bloom, point_glow: self.point_glow, experimental_shaders: self.experimental_shaders, @@ -398,10 +406,11 @@ impl RenderMode { #[derive(PartialEq, Clone, Debug)] pub struct PipelineModes { aa: AaMode, - cloud: CloudMode, + pub cloud: CloudMode, fluid: FluidMode, lighting: LightingMode, pub shadow: ShadowMode, + pub rain_occlusion: ShadowMapMode, bloom: BloomMode, point_glow: f32, experimental_shaders: HashSet, @@ -467,4 +476,6 @@ pub enum ExperimentalShader { /// Display grid lines to visualize the distribution of shadow map texels /// for the directional light from the sun. DirectionalShadowMapTexelGrid, + /// Enable rainbows + Rainbows, } diff --git a/voxygen/src/render/pipelines/clouds.rs b/voxygen/src/render/pipelines/clouds.rs index 95e03f65dc..52773d3c0c 100644 --- a/voxygen/src/render/pipelines/clouds.rs +++ b/voxygen/src/render/pipelines/clouds.rs @@ -8,8 +8,7 @@ use vek::*; #[repr(C)] #[derive(Copy, Clone, Debug, Zeroable, Pod)] pub struct Locals { - proj_mat_inv: [[f32; 4]; 4], - view_mat_inv: [[f32; 4]; 4], + all_mat_inv: [[f32; 4]; 4], } impl Default for Locals { @@ -19,8 +18,7 @@ impl Default for Locals { impl Locals { pub fn new(proj_mat_inv: Mat4, view_mat_inv: Mat4) -> Self { Self { - proj_mat_inv: proj_mat_inv.into_col_arrays(), - view_mat_inv: view_mat_inv.into_col_arrays(), + all_mat_inv: (view_mat_inv * proj_mat_inv).into_col_arrays(), } } } @@ -153,7 +151,11 @@ impl CloudsPipeline { device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("Clouds pipeline layout"), push_constant_ranges: &[], - bind_group_layouts: &[&global_layout.globals, &layout.layout], + bind_group_layouts: &[ + &global_layout.globals, + &global_layout.shadow_textures, + &layout.layout, + ], }); let samples = match aa_mode { diff --git a/voxygen/src/render/pipelines/lod_terrain.rs b/voxygen/src/render/pipelines/lod_terrain.rs index 4257428dbd..7a4a757eaf 100644 --- a/voxygen/src/render/pipelines/lod_terrain.rs +++ b/voxygen/src/render/pipelines/lod_terrain.rs @@ -36,6 +36,7 @@ pub struct LodData { pub alt: Texture, pub horizon: Texture, pub tgt_detail: u32, + pub weather: Texture, } impl LodData { @@ -52,6 +53,7 @@ impl LodData { &map_image, &alt_image, &horizon_image, + Vec2::new(1, 1), 1, //map_border.into(), ) @@ -63,6 +65,7 @@ impl LodData { lod_base: &[u32], lod_alt: &[u32], lod_horizon: &[u32], + weather_size: Vec2, tgt_detail: u32, //border_color: gfx::texture::PackedColor, ) -> Self { @@ -132,12 +135,57 @@ impl LodData { ); // SamplerInfo { // border: [1.0, 0.0, 1.0, 0.0].into(), + let weather = { + let texture_info = wgpu::TextureDescriptor { + label: None, + size: wgpu::Extent3d { + width: weather_size.x, + height: weather_size.y, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8Unorm, + usage: wgpu::TextureUsage::SAMPLED | wgpu::TextureUsage::COPY_DST, + }; + let sampler_info = wgpu::SamplerDescriptor { + label: None, + address_mode_u: wgpu::AddressMode::ClampToBorder, + address_mode_v: wgpu::AddressMode::ClampToBorder, + address_mode_w: wgpu::AddressMode::ClampToBorder, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::FilterMode::Nearest, + border_color: Some(wgpu::SamplerBorderColor::TransparentBlack), + ..Default::default() + }; + + let view_info = wgpu::TextureViewDescriptor { + label: None, + format: Some(wgpu::TextureFormat::Rgba8Unorm), + dimension: Some(wgpu::TextureViewDimension::D2), + aspect: wgpu::TextureAspect::All, + base_mip_level: 0, + mip_level_count: None, + base_array_layer: 0, + array_layer_count: None, + }; + + renderer.create_texture_with_data_raw( + &texture_info, + &view_info, + &sampler_info, + vec![0; weather_size.x as usize * weather_size.y as usize * 4].as_slice(), + ) + }; Self { map, alt, horizon, tgt_detail, + weather, } } } diff --git a/voxygen/src/render/pipelines/mod.rs b/voxygen/src/render/pipelines/mod.rs index 0fad8f5598..3ddd7252e6 100644 --- a/voxygen/src/render/pipelines/mod.rs +++ b/voxygen/src/render/pipelines/mod.rs @@ -8,6 +8,7 @@ pub mod lod_object; pub mod lod_terrain; pub mod particle; pub mod postprocess; +pub mod rain_occlusion; pub mod shadow; pub mod skybox; pub mod sprite; @@ -66,9 +67,11 @@ pub struct Globals { ambiance: f32, cam_mode: u32, sprite_render_distance: f32, - /// To keep 16-byte-aligned. - globals_dummy: f32, + // To keep 16-byte-aligned. + globals_dummy: [f32; 1], } +/// Make sure Globals is 16-byte-aligned. +const _: () = assert!(core::mem::size_of::() % 16 == 0); #[repr(C)] #[derive(Copy, Clone, Debug, Zeroable, Pod)] @@ -156,7 +159,7 @@ impl Globals { ambiance: ambiance.clamped(0.0, 1.0), cam_mode: cam_mode as u32, sprite_render_distance, - globals_dummy: 0.0, + globals_dummy: [0.0; 1], } } @@ -250,6 +253,7 @@ pub struct GlobalModel { pub lights: Consts, pub shadows: Consts, pub shadow_mats: shadow::BoundLocals, + pub rain_occlusion_mats: rain_occlusion::BoundLocals, pub point_light_matrices: Box<[shadow::PointLightMatrix; 126]>, } @@ -401,6 +405,37 @@ impl GlobalsLayouts { }, count: None, }, + // clouds t_weather + wgpu::BindGroupLayoutEntry { + binding: 12, + visibility: wgpu::ShaderStage::VERTEX | wgpu::ShaderStage::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 13, + visibility: wgpu::ShaderStage::VERTEX | wgpu::ShaderStage::FRAGMENT, + ty: wgpu::BindingType::Sampler { + filtering: true, + comparison: false, + }, + count: None, + }, + // rain occlusion + wgpu::BindGroupLayoutEntry { + binding: 14, + visibility: wgpu::ShaderStage::VERTEX | wgpu::ShaderStage::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, ] } @@ -479,6 +514,26 @@ impl GlobalsLayouts { }, count: None, }, + // Rain occlusion maps + wgpu::BindGroupLayoutEntry { + binding: 4, + visibility: wgpu::ShaderStage::VERTEX | wgpu::ShaderStage::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Depth, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 5, + visibility: wgpu::ShaderStage::VERTEX | wgpu::ShaderStage::FRAGMENT, + ty: wgpu::BindingType::Sampler { + filtering: true, + comparison: true, + }, + count: None, + }, ], }); @@ -552,6 +607,19 @@ impl GlobalsLayouts { binding: 11, resource: wgpu::BindingResource::Sampler(&lod_data.map.sampler), }, + wgpu::BindGroupEntry { + binding: 12, + resource: wgpu::BindingResource::TextureView(&lod_data.weather.view), + }, + wgpu::BindGroupEntry { + binding: 13, + resource: wgpu::BindingResource::Sampler(&lod_data.weather.sampler), + }, + // rain occlusion + wgpu::BindGroupEntry { + binding: 14, + resource: global_model.rain_occlusion_mats.buf().as_entire_binding(), + }, ] } @@ -576,6 +644,7 @@ impl GlobalsLayouts { device: &wgpu::Device, point_shadow_map: &Texture, directed_shadow_map: &Texture, + rain_occlusion_map: &Texture, ) -> ShadowTexturesBindGroup { let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label: None, @@ -597,6 +666,14 @@ impl GlobalsLayouts { binding: 3, resource: wgpu::BindingResource::Sampler(&directed_shadow_map.sampler), }, + wgpu::BindGroupEntry { + binding: 4, + resource: wgpu::BindingResource::TextureView(&rain_occlusion_map.view), + }, + wgpu::BindGroupEntry { + binding: 5, + resource: wgpu::BindingResource::Sampler(&rain_occlusion_map.sampler), + }, ], }); diff --git a/voxygen/src/render/pipelines/rain_occlusion.rs b/voxygen/src/render/pipelines/rain_occlusion.rs new file mode 100644 index 0000000000..9963b4ccae --- /dev/null +++ b/voxygen/src/render/pipelines/rain_occlusion.rs @@ -0,0 +1,231 @@ +use super::super::{ + AaMode, Bound, Consts, FigureLayout, GlobalsLayouts, TerrainLayout, TerrainVertex, +}; +use bytemuck::{Pod, Zeroable}; +use vek::*; + +#[repr(C)] +#[derive(Copy, Clone, Debug, Zeroable, Pod, Default)] +pub struct Locals { + rain_occlusion_matrices: [[f32; 4]; 4], + rain_occlusion_texture_mat: [[f32; 4]; 4], + /// A rotation of the direction of the rain, relative to the players + /// velocity. + rain_dir_mat: [[f32; 4]; 4], + /// A value to offset the rain, to make it move over time. + integrated_rain_vel: f32, + rain_density: f32, + // To keep 16-byte-aligned. + occlusion_dummy: [f32; 2], +} +/// Make sure Locals is 16-byte-aligned. +const _: () = assert!(core::mem::size_of::() % 16 == 0); + +impl Locals { + pub fn new( + rain_occlusion_matrices: Mat4, + rain_occlusion_texture_mat: Mat4, + rain_dir_mat: Mat4, + rain_density: f32, + integrated_rain_vel: f32, + ) -> Self { + Self { + rain_occlusion_matrices: rain_occlusion_matrices.into_col_arrays(), + rain_occlusion_texture_mat: rain_occlusion_texture_mat.into_col_arrays(), + rain_dir_mat: rain_dir_mat.into_col_arrays(), + integrated_rain_vel, + rain_density, + occlusion_dummy: [0.0; 2], + } + } +} + +pub type BoundLocals = Bound>; + +pub struct RainOcclusionLayout { + pub locals: wgpu::BindGroupLayout, +} + +impl RainOcclusionLayout { + pub fn new(device: &wgpu::Device) -> Self { + Self { + locals: device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: None, + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStage::VERTEX | wgpu::ShaderStage::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }), + } + } + + pub fn bind_locals(&self, device: &wgpu::Device, locals: Consts) -> BoundLocals { + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: None, + layout: &self.locals, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: locals.buf().as_entire_binding(), + }], + }); + + BoundLocals { + bind_group, + with: locals, + } + } +} + +pub struct RainOcclusionFigurePipeline { + pub pipeline: wgpu::RenderPipeline, +} + +impl RainOcclusionFigurePipeline { + pub fn new( + device: &wgpu::Device, + vs_module: &wgpu::ShaderModule, + global_layout: &GlobalsLayouts, + figure_layout: &FigureLayout, + aa_mode: AaMode, + ) -> Self { + common_base::span!(_guard, "new"); + + let render_pipeline_layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Rain occlusion figure pipeline layout"), + push_constant_ranges: &[], + bind_group_layouts: &[&global_layout.globals, &figure_layout.locals], + }); + + let samples = match aa_mode { + AaMode::None | AaMode::Fxaa => 1, + AaMode::MsaaX4 => 4, + AaMode::MsaaX8 => 8, + AaMode::MsaaX16 => 16, + }; + + let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Rain occlusion figure pipeline"), + layout: Some(&render_pipeline_layout), + vertex: wgpu::VertexState { + module: vs_module, + entry_point: "main", + buffers: &[TerrainVertex::desc()], + }, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: Some(wgpu::Face::Back), + clamp_depth: true, + polygon_mode: wgpu::PolygonMode::Fill, + conservative: false, + }, + depth_stencil: Some(wgpu::DepthStencilState { + format: wgpu::TextureFormat::Depth24Plus, + depth_write_enabled: true, + depth_compare: wgpu::CompareFunction::Less, + stencil: wgpu::StencilState { + front: wgpu::StencilFaceState::IGNORE, + back: wgpu::StencilFaceState::IGNORE, + read_mask: !0, + write_mask: !0, + }, + bias: wgpu::DepthBiasState { + constant: 0, + slope_scale: 0.0, + clamp: 0.0, + }, + }), + multisample: wgpu::MultisampleState { + count: samples, + mask: !0, + alpha_to_coverage_enabled: false, + }, + fragment: None, + }); + + Self { + pipeline: render_pipeline, + } + } +} + +pub struct RainOcclusionPipeline { + pub pipeline: wgpu::RenderPipeline, +} + +impl RainOcclusionPipeline { + pub fn new( + device: &wgpu::Device, + vs_module: &wgpu::ShaderModule, + global_layout: &GlobalsLayouts, + terrain_layout: &TerrainLayout, + aa_mode: AaMode, + ) -> Self { + let render_pipeline_layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Rain occlusion pipeline layout"), + push_constant_ranges: &[], + bind_group_layouts: &[&global_layout.globals, &terrain_layout.locals], + }); + + let samples = match aa_mode { + AaMode::None | AaMode::Fxaa => 1, + AaMode::MsaaX4 => 4, + AaMode::MsaaX8 => 8, + AaMode::MsaaX16 => 16, + }; + + let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Rain occlusion pipeline"), + layout: Some(&render_pipeline_layout), + vertex: wgpu::VertexState { + module: vs_module, + entry_point: "main", + buffers: &[TerrainVertex::desc()], + }, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: Some(wgpu::Face::Front), + clamp_depth: true, + polygon_mode: wgpu::PolygonMode::Fill, + conservative: false, + }, + depth_stencil: Some(wgpu::DepthStencilState { + format: wgpu::TextureFormat::Depth24Plus, + depth_write_enabled: true, + depth_compare: wgpu::CompareFunction::Less, + stencil: wgpu::StencilState { + front: wgpu::StencilFaceState::IGNORE, + back: wgpu::StencilFaceState::IGNORE, + read_mask: !0, + write_mask: !0, + }, + bias: wgpu::DepthBiasState { + constant: 0, + slope_scale: 0.0, + clamp: 0.0, + }, + }), + multisample: wgpu::MultisampleState { + count: samples, + mask: !0, + alpha_to_coverage_enabled: false, + }, + fragment: None, + }); + + Self { + pipeline: render_pipeline, + } + } +} diff --git a/voxygen/src/render/pipelines/sprite.rs b/voxygen/src/render/pipelines/sprite.rs index fd0514bfcf..e7b81f0d88 100644 --- a/voxygen/src/render/pipelines/sprite.rs +++ b/voxygen/src/render/pipelines/sprite.rs @@ -176,11 +176,11 @@ pub struct SpriteLayout { impl SpriteLayout { pub fn new(device: &wgpu::Device) -> Self { let mut entries = GlobalsLayouts::base_globals_layout(); - debug_assert_eq!(12, entries.len()); // To remember to adjust the bindings below + debug_assert_eq!(15, entries.len()); // To remember to adjust the bindings below entries.extend_from_slice(&[ // sprite_verts wgpu::BindGroupLayoutEntry { - binding: 12, + binding: 15, visibility: wgpu::ShaderStage::VERTEX, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Storage { read_only: true }, @@ -214,7 +214,7 @@ impl SpriteLayout { entries.extend_from_slice(&[ // sprite_verts wgpu::BindGroupEntry { - binding: 12, + binding: 15, resource: sprite_verts.0.buf.as_entire_binding(), }, ]); diff --git a/voxygen/src/render/renderer.rs b/voxygen/src/render/renderer.rs index 4f8a17baf2..156cafb83d 100644 --- a/voxygen/src/render/renderer.rs +++ b/voxygen/src/render/renderer.rs @@ -3,6 +3,7 @@ pub(super) mod drawer; // Consts and bind groups for post-process and clouds mod locals; mod pipeline_creation; +mod rain_occlusion_map; mod screenshot; mod shaders; mod shadow_map; @@ -14,6 +15,8 @@ use pipeline_creation::{ use shaders::Shaders; use shadow_map::{ShadowMap, ShadowMapRenderer}; +use self::{pipeline_creation::RainOcclusionPipelines, rain_occlusion_map::RainOcclusionMap}; + use super::{ buffer::Buffer, consts::Consts, @@ -21,8 +24,8 @@ use super::{ mesh::Mesh, model::{DynamicModel, Model}, pipelines::{ - blit, bloom, clouds, debug, figure, postprocess, shadow, sprite, terrain, ui, - GlobalsBindGroup, GlobalsLayouts, ShadowTexturesBindGroup, + blit, bloom, clouds, debug, figure, postprocess, rain_occlusion, shadow, sprite, terrain, + ui, GlobalsBindGroup, GlobalsLayouts, ShadowTexturesBindGroup, }, texture::Texture, AaMode, AddressMode, FilterMode, OtherModes, PipelineModes, RenderError, RenderMode, @@ -54,6 +57,7 @@ struct ImmutableLayouts { debug: debug::DebugLayout, figure: figure::FigureLayout, shadow: shadow::ShadowLayout, + rain_occlusion: rain_occlusion::RainOcclusionLayout, sprite: sprite::SpriteLayout, terrain: terrain::TerrainLayout, clouds: clouds::CloudsLayout, @@ -90,6 +94,7 @@ struct Views { /// Shadow rendering textures, layouts, pipelines, and bind groups struct Shadow { + rain_map: RainOcclusionMap, map: ShadowMap, bind: ShadowTexturesBindGroup, } @@ -104,6 +109,7 @@ enum State { Interface { pipelines: InterfacePipelines, shadow_views: Option<(Texture, Texture)>, + rain_occlusion_view: Option, // In progress creation of the remaining pipelines in the background creating: PipelineCreation, }, @@ -117,6 +123,7 @@ enum State { ( Pipelines, ShadowPipelines, + RainOcclusionPipelines, Arc, ), RenderError, @@ -359,6 +366,13 @@ impl Renderer { }) .ok(); + let rain_occlusion_view = + RainOcclusionMap::create_view(&device, &pipeline_modes.rain_occlusion) + .map_err(|err| { + warn!("Could not create rain occlusion map views: {:?}", err); + }) + .ok(); + let shaders = Shaders::load_expect(""); let shaders_watcher = shaders.reload_watcher(); @@ -368,6 +382,7 @@ impl Renderer { let debug = debug::DebugLayout::new(&device); let figure = figure::FigureLayout::new(&device); let shadow = shadow::ShadowLayout::new(&device); + let rain_occlusion = rain_occlusion::RainOcclusionLayout::new(&device); let sprite = sprite::SpriteLayout::new(&device); let terrain = terrain::TerrainLayout::new(&device); let clouds = clouds::CloudsLayout::new(&device); @@ -385,6 +400,7 @@ impl Renderer { debug, figure, shadow, + rain_occlusion, sprite, terrain, clouds, @@ -417,6 +433,7 @@ impl Renderer { let state = State::Interface { pipelines: interface_pipelines, shadow_views, + rain_occlusion_view, creating, }; @@ -680,21 +697,33 @@ impl Renderer { // Get mutable reference to shadow views out of the current state let shadow_views = match &mut self.state { - State::Interface { shadow_views, .. } => { - shadow_views.as_mut().map(|s| (&mut s.0, &mut s.1)) - }, + State::Interface { + shadow_views, + rain_occlusion_view, + .. + } => shadow_views + .as_mut() + .map(|s| (&mut s.0, &mut s.1)) + .zip(rain_occlusion_view.as_mut()), State::Complete { shadow: Shadow { map: ShadowMap::Enabled(shadow_map), + rain_map: RainOcclusionMap::Enabled(rain_occlusion_map), .. }, .. - } => Some((&mut shadow_map.point_depth, &mut shadow_map.directed_depth)), + } => Some(( + (&mut shadow_map.point_depth, &mut shadow_map.directed_depth), + &mut rain_occlusion_map.depth, + )), State::Complete { .. } => None, State::Nothing => None, // Should never hit this }; + let mut update_shadow_bind = false; + let (shadow_views, rain_views) = shadow_views.unzip(); + if let (Some((point_depth, directed_depth)), ShadowMode::Map(mode)) = (shadow_views, self.pipeline_modes.shadow) { @@ -702,29 +731,50 @@ impl Renderer { Ok((new_point_depth, new_directed_depth)) => { *point_depth = new_point_depth; *directed_depth = new_directed_depth; - // Recreate the shadow bind group if needed - if let State::Complete { - shadow: - Shadow { - bind, - map: ShadowMap::Enabled(shadow_map), - .. - }, - .. - } = &mut self.state - { - *bind = self.layouts.global.bind_shadow_textures( - &self.device, - &shadow_map.point_depth, - &shadow_map.directed_depth, - ); - } + + update_shadow_bind = true; }, Err(err) => { warn!("Could not create shadow map views: {:?}", err); }, } } + if let Some(rain_depth) = rain_views { + match RainOcclusionMap::create_view( + &self.device, + &self.pipeline_modes.rain_occlusion, + ) { + Ok(new_rain_depth) => { + *rain_depth = new_rain_depth; + + update_shadow_bind = true; + }, + Err(err) => { + warn!("Could not create rain occlusion map view: {:?}", err); + }, + } + } + if update_shadow_bind { + // Recreate the shadow bind group if needed + if let State::Complete { + shadow: + Shadow { + bind, + map: ShadowMap::Enabled(shadow_map), + rain_map: RainOcclusionMap::Enabled(rain_occlusion_map), + .. + }, + .. + } = &mut self.state + { + *bind = self.layouts.global.bind_shadow_textures( + &self.device, + &shadow_map.point_depth, + &shadow_map.directed_depth, + &rain_occlusion_map.depth, + ); + } + } } else { self.is_minimized = true; } @@ -951,12 +1001,17 @@ impl Renderer { self.state = if let State::Interface { pipelines: interface, shadow_views, + rain_occlusion_view, creating, } = state { match creating.try_complete() { Ok(pipelines) => { - let IngameAndShadowPipelines { ingame, shadow } = pipelines; + let IngameAndShadowPipelines { + ingame, + shadow, + rain_occlusion, + } = pipelines; let pipelines = Pipelines::consolidate(interface, ingame); @@ -969,14 +1024,26 @@ impl Renderer { shadow_views, ); + let rain_occlusion_map = RainOcclusionMap::new( + &self.device, + &self.queue, + rain_occlusion.terrain, + rain_occlusion.figure, + rain_occlusion_view, + ); + let shadow_bind = { let (point, directed) = shadow_map.textures(); - self.layouts - .global - .bind_shadow_textures(&self.device, point, directed) + self.layouts.global.bind_shadow_textures( + &self.device, + point, + directed, + rain_occlusion_map.texture(), + ) }; let shadow = Shadow { + rain_map: rain_occlusion_map, map: shadow_map, bind: shadow_bind, }; @@ -991,6 +1058,7 @@ impl Renderer { Err(creating) => State::Interface { pipelines: interface, shadow_views, + rain_occlusion_view, creating, }, } @@ -1002,7 +1070,12 @@ impl Renderer { } = state { match pipeline_creation.try_complete() { - Ok(Ok((pipelines, shadow_pipelines, postprocess_layout))) => { + Ok(Ok(( + pipelines, + shadow_pipelines, + rain_occlusion_pipelines, + postprocess_layout, + ))) => { if let ( Some(point_pipeline), Some(terrain_directed_pipeline), @@ -1019,6 +1092,19 @@ impl Renderer { shadow_map.figure_directed_pipeline = figure_directed_pipeline; } + if let ( + Some(terrain_directed_pipeline), + Some(figure_directed_pipeline), + RainOcclusionMap::Enabled(rain_occlusion_map), + ) = ( + rain_occlusion_pipelines.terrain, + rain_occlusion_pipelines.figure, + &mut shadow.rain_map, + ) { + rain_occlusion_map.terrain_pipeline = terrain_directed_pipeline; + rain_occlusion_map.figure_pipeline = figure_directed_pipeline; + } + self.pipeline_modes = new_pipeline_modes; self.layouts.postprocess = postprocess_layout; // TODO: we have the potential to skip recreating bindings / render targets on diff --git a/voxygen/src/render/renderer/binding.rs b/voxygen/src/render/renderer/binding.rs index f42808c0a8..6deffb0f28 100644 --- a/voxygen/src/render/renderer/binding.rs +++ b/voxygen/src/render/renderer/binding.rs @@ -1,3 +1,5 @@ +use crate::render::pipelines::rain_occlusion; + use super::{ super::{ pipelines::{ @@ -74,6 +76,16 @@ impl Renderer { self.layouts.shadow.bind_locals(&self.device, locals) } + pub fn create_rain_occlusion_bound_locals( + &mut self, + locals: &[rain_occlusion::Locals], + ) -> rain_occlusion::BoundLocals { + let locals = self.create_consts(locals); + self.layouts + .rain_occlusion + .bind_locals(&self.device, locals) + } + pub fn figure_bind_col_light(&self, col_light: Texture) -> ColLights { self.layouts.global.bind_col_light(&self.device, col_light) } diff --git a/voxygen/src/render/renderer/drawer.rs b/voxygen/src/render/renderer/drawer.rs index 4d0912c25c..b1fb5e1e88 100644 --- a/voxygen/src/render/renderer/drawer.rs +++ b/voxygen/src/render/renderer/drawer.rs @@ -9,6 +9,7 @@ use super::{ ShadowTexturesBindGroup, }, }, + rain_occlusion_map::{RainOcclusionMap, RainOcclusionMapRenderer}, Renderer, ShadowMap, ShadowMapRenderer, }; use core::{num::NonZeroU32, ops::Range}; @@ -135,6 +136,46 @@ impl<'frame> Drawer<'frame> { /// Get the pipeline modes. pub fn pipeline_modes(&self) -> &super::super::PipelineModes { self.borrow.pipeline_modes } + /// Returns None if the rain occlusion renderer is not enabled at some + /// level, the pipelines are not available yet or clouds are disabled. + pub fn rain_occlusion_pass(&mut self) -> Option { + if !self.borrow.pipeline_modes.cloud.is_enabled() { + return None; + } + + if let RainOcclusionMap::Enabled(ref rain_occlusion_renderer) = self.borrow.shadow?.rain_map + { + let encoder = self.encoder.as_mut().unwrap(); + let device = self.borrow.device; + let mut render_pass = encoder.scoped_render_pass( + "rain_occlusion_pass", + device, + &wgpu::RenderPassDescriptor { + label: Some("rain occlusion pass"), + color_attachments: &[], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &rain_occlusion_renderer.depth.view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Clear(1.0), + store: true, + }), + stencil_ops: None, + }), + }, + ); + + render_pass.set_bind_group(0, &self.globals.bind_group, &[]); + + Some(RainOcclusionPassDrawer { + render_pass, + borrow: &self.borrow, + rain_occlusion_renderer, + }) + } else { + None + } + } + /// Returns None if the shadow renderer is not enabled at some level or the /// pipelines are not available yet pub fn shadow_pass(&mut self) -> Option { @@ -216,6 +257,8 @@ impl<'frame> Drawer<'frame> { /// Returns None if the clouds pipeline is not available pub fn second_pass(&mut self) -> Option { let pipelines = &self.borrow.pipelines.all()?; + let shadow = self.borrow.shadow?; + let encoder = self.encoder.as_mut().unwrap(); let device = self.borrow.device; let mut render_pass = @@ -237,6 +280,7 @@ impl<'frame> Drawer<'frame> { }); render_pass.set_bind_group(0, &self.globals.bind_group, &[]); + render_pass.set_bind_group(1, &shadow.bind.bind_group, &[]); Some(SecondPassDrawer { render_pass, @@ -619,7 +663,7 @@ impl<'pass> ShadowPassDrawer<'pass> { pub fn draw_figure_shadows(&mut self) -> FigureShadowDrawer<'_, 'pass> { let mut render_pass = self .render_pass - .scope("direcred_figure_shadows", self.borrow.device); + .scope("directed_figure_shadows", self.borrow.device); render_pass.set_pipeline(&self.shadow_renderer.figure_directed_pipeline.pipeline); set_quad_index_buffer::(&mut render_pass, self.borrow); @@ -630,7 +674,7 @@ impl<'pass> ShadowPassDrawer<'pass> { pub fn draw_terrain_shadows(&mut self) -> TerrainShadowDrawer<'_, 'pass> { let mut render_pass = self .render_pass - .scope("direcred_terrain_shadows", self.borrow.device); + .scope("directed_terrain_shadows", self.borrow.device); render_pass.set_pipeline(&self.shadow_renderer.terrain_directed_pipeline.pipeline); set_quad_index_buffer::(&mut render_pass, self.borrow); @@ -639,6 +683,37 @@ impl<'pass> ShadowPassDrawer<'pass> { } } +#[must_use] +pub struct RainOcclusionPassDrawer<'pass> { + render_pass: OwningScope<'pass, wgpu::RenderPass<'pass>>, + borrow: &'pass RendererBorrow<'pass>, + rain_occlusion_renderer: &'pass RainOcclusionMapRenderer, +} + +impl<'pass> RainOcclusionPassDrawer<'pass> { + pub fn draw_figure_shadows(&mut self) -> FigureShadowDrawer<'_, 'pass> { + let mut render_pass = self + .render_pass + .scope("directed_figure_rain_occlusion", self.borrow.device); + + render_pass.set_pipeline(&self.rain_occlusion_renderer.figure_pipeline.pipeline); + set_quad_index_buffer::(&mut render_pass, self.borrow); + + FigureShadowDrawer { render_pass } + } + + pub fn draw_terrain_shadows(&mut self) -> TerrainShadowDrawer<'_, 'pass> { + let mut render_pass = self + .render_pass + .scope("directed_terrain_rain_occlusion", self.borrow.device); + + render_pass.set_pipeline(&self.rain_occlusion_renderer.terrain_pipeline.pipeline); + set_quad_index_buffer::(&mut render_pass, self.borrow); + + TerrainShadowDrawer { render_pass } + } +} + #[must_use] pub struct FigureShadowDrawer<'pass_ref, 'pass: 'pass_ref> { render_pass: Scope<'pass_ref, wgpu::RenderPass<'pass>>, @@ -970,7 +1045,7 @@ impl<'pass> SecondPassDrawer<'pass> { self.render_pass .set_pipeline(&self.clouds_pipeline.pipeline); self.render_pass - .set_bind_group(1, &self.borrow.locals.clouds_bind.bind_group, &[]); + .set_bind_group(2, &self.borrow.locals.clouds_bind.bind_group, &[]); self.render_pass.draw(0..3, 0..1); } diff --git a/voxygen/src/render/renderer/pipeline_creation.rs b/voxygen/src/render/renderer/pipeline_creation.rs index 01664803fa..ac29f6e087 100644 --- a/voxygen/src/render/renderer/pipeline_creation.rs +++ b/voxygen/src/render/renderer/pipeline_creation.rs @@ -1,3 +1,5 @@ +use crate::render::pipelines::rain_occlusion; + use super::{ super::{ pipelines::{ @@ -60,9 +62,16 @@ pub struct ShadowPipelines { pub figure: Option, } +pub struct RainOcclusionPipelines { + pub terrain: Option, + pub figure: Option, +} + +// TODO: Find a better name for this? pub struct IngameAndShadowPipelines { pub ingame: IngamePipelines, pub shadow: ShadowPipelines, + pub rain_occlusion: RainOcclusionPipelines, } /// Pipelines neccesary to display the UI and take screenshots @@ -131,6 +140,8 @@ struct ShaderModules { point_light_shadows_vert: wgpu::ShaderModule, light_shadows_directed_vert: wgpu::ShaderModule, light_shadows_figure_vert: wgpu::ShaderModule, + rain_occlusion_directed_vert: wgpu::ShaderModule, + rain_occlusion_figure_vert: wgpu::ShaderModule, } impl ShaderModules { @@ -151,6 +162,7 @@ impl ShaderModules { let random = shaders.get("include.random").unwrap(); let lod = shaders.get("include.lod").unwrap(); let shadows = shaders.get("include.shadows").unwrap(); + let rain_occlusion = shaders.get("include.rain_occlusion").unwrap(); let point_glow = shaders.get("include.point_glow").unwrap(); // We dynamically add extra configuration settings to the constants file. @@ -252,6 +264,7 @@ impl ShaderModules { "constants.glsl" => constants.clone(), "globals.glsl" => globals.0.to_owned(), "shadows.glsl" => shadows.0.to_owned(), + "rain_occlusion.glsl" => rain_occlusion.0.to_owned(), "sky.glsl" => sky.0.to_owned(), "light.glsl" => light.0.to_owned(), "srgb.glsl" => srgb.0.to_owned(), @@ -332,6 +345,14 @@ impl ShaderModules { "light-shadows-figure-vert", ShaderKind::Vertex, )?, + rain_occlusion_directed_vert: create_shader( + "rain-occlusion-directed-vert", + ShaderKind::Vertex, + )?, + rain_occlusion_figure_vert: create_shader( + "rain-occlusion-figure-vert", + ShaderKind::Vertex, + )?, }) } } @@ -422,7 +443,7 @@ fn create_ingame_and_shadow_pipelines( needs: PipelineNeeds, pool: &rayon::ThreadPool, // TODO: Reduce the boilerplate in this file - tasks: [Task; 16], + tasks: [Task; 18], ) -> IngameAndShadowPipelines { prof_span!(_guard, "create_ingame_and_shadow_pipelines"); @@ -454,6 +475,8 @@ fn create_ingame_and_shadow_pipelines( point_shadow_task, terrain_directed_shadow_task, figure_directed_shadow_task, + terrain_directed_rain_occlusion_task, + figure_directed_rain_occlusion_task, ] = tasks; // TODO: pass in format of target color buffer @@ -739,6 +762,36 @@ fn create_ingame_and_shadow_pipelines( "figure directed shadow pipeline creation", ) }; + // Pipeline for rendering directional light terrain rain occlusion maps. + let create_terrain_directed_rain_occlusion = || { + terrain_directed_rain_occlusion_task.run( + || { + rain_occlusion::RainOcclusionPipeline::new( + device, + &shaders.rain_occlusion_directed_vert, + &layouts.global, + &layouts.terrain, + pipeline_modes.aa, + ) + }, + "terrain directed rain occlusion pipeline creation", + ) + }; + // Pipeline for rendering directional light figure rain occlusion maps. + let create_figure_directed_rain_occlusion = || { + figure_directed_rain_occlusion_task.run( + || { + rain_occlusion::RainOcclusionFigurePipeline::new( + device, + &shaders.rain_occlusion_figure_vert, + &layouts.global, + &layouts.figure, + pipeline_modes.aa, + ) + }, + "figure directed rain occlusion pipeline creation", + ) + }; let j1 = || pool.join(create_debug, || pool.join(create_skybox, create_figure)); let j2 = || pool.join(create_terrain, || pool.join(create_fluid, create_bloom)); @@ -755,7 +808,14 @@ fn create_ingame_and_shadow_pipelines( create_figure_directed_shadow, ) }; - let j7 = create_lod_object; + let j7 = || { + pool.join(create_lod_object, || { + pool.join( + create_terrain_directed_rain_occlusion, + create_figure_directed_rain_occlusion, + ) + }) + }; // Ignore this let ( @@ -765,7 +825,7 @@ fn create_ingame_and_shadow_pipelines( ), ( ((postprocess, point_shadow), (terrain_directed_shadow, figure_directed_shadow)), - lod_object, + (lod_object, (terrain_directed_rain_occlusion, figure_directed_rain_occlusion)), ), ) = pool.join( || pool.join(|| pool.join(j1, j2), || pool.join(j3, j4)), @@ -795,6 +855,10 @@ fn create_ingame_and_shadow_pipelines( directed: Some(terrain_directed_shadow), figure: Some(figure_directed_shadow), }, + rain_occlusion: RainOcclusionPipelines { + terrain: Some(terrain_directed_rain_occlusion), + figure: Some(figure_directed_rain_occlusion), + }, } } @@ -887,6 +951,7 @@ pub(super) fn recreate_pipelines( ( Pipelines, ShadowPipelines, + RainOcclusionPipelines, Arc, ), RenderError, @@ -952,14 +1017,18 @@ pub(super) fn recreate_pipelines( let interface = create_interface_pipelines(needs, pool, interface_tasks); // Create the rest of the pipelines - let IngameAndShadowPipelines { ingame, shadow } = - create_ingame_and_shadow_pipelines(needs, pool, ingame_and_shadow_tasks); + let IngameAndShadowPipelines { + ingame, + shadow, + rain_occlusion, + } = create_ingame_and_shadow_pipelines(needs, pool, ingame_and_shadow_tasks); // Send them result_send .send(Ok(( Pipelines::consolidate(interface, ingame), shadow, + rain_occlusion, layouts.postprocess, ))) .expect("Channel disconnected"); diff --git a/voxygen/src/render/renderer/rain_occlusion_map.rs b/voxygen/src/render/renderer/rain_occlusion_map.rs new file mode 100644 index 0000000000..124deb4a72 --- /dev/null +++ b/voxygen/src/render/renderer/rain_occlusion_map.rs @@ -0,0 +1,227 @@ +use crate::{render::pipelines::rain_occlusion, scene::terrain::RAIN_OCCLUSION_CHUNKS}; + +use super::{ + super::{texture::Texture, RenderError, ShadowMapMode}, + Renderer, +}; +use common::{terrain::TerrainChunkSize, vol::RectVolSize}; +use vek::*; + +/// A type that holds rain occlusion map data. Since rain occlusion mapping may +/// not be supported on all platforms, we try to keep it separate. +pub struct RainOcclusionMapRenderer { + pub depth: Texture, + + pub terrain_pipeline: rain_occlusion::RainOcclusionPipeline, + pub figure_pipeline: rain_occlusion::RainOcclusionFigurePipeline, + pub layout: rain_occlusion::RainOcclusionLayout, +} + +pub enum RainOcclusionMap { + Enabled(RainOcclusionMapRenderer), + /// Dummy texture + Disabled(Texture), +} + +impl RainOcclusionMap { + pub fn new( + device: &wgpu::Device, + queue: &wgpu::Queue, + directed: Option, + figure: Option, + view: Option, + ) -> Self { + if let (Some(terrain_pipeline), Some(figure_pipeline), Some(depth)) = + (directed, figure, view) + { + let layout = rain_occlusion::RainOcclusionLayout::new(device); + + Self::Enabled(RainOcclusionMapRenderer { + depth, + terrain_pipeline, + figure_pipeline, + layout, + }) + } else { + Self::Disabled(Self::create_dummy_tex(device, queue)) + } + } + + fn create_dummy_tex(device: &wgpu::Device, queue: &wgpu::Queue) -> Texture { + let tex = { + let tex = wgpu::TextureDescriptor { + label: None, + size: wgpu::Extent3d { + width: 4, + height: 4, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Depth24Plus, + usage: wgpu::TextureUsage::SAMPLED | wgpu::TextureUsage::RENDER_ATTACHMENT, + }; + + let view = wgpu::TextureViewDescriptor { + label: None, + format: Some(wgpu::TextureFormat::Depth24Plus), + dimension: Some(wgpu::TextureViewDimension::D2), + aspect: wgpu::TextureAspect::DepthOnly, + base_mip_level: 0, + mip_level_count: None, + base_array_layer: 0, + array_layer_count: None, + }; + + let sampler_info = wgpu::SamplerDescriptor { + label: None, + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::FilterMode::Nearest, + compare: Some(wgpu::CompareFunction::LessEqual), + ..Default::default() + }; + + Texture::new_raw(device, &tex, &view, &sampler_info) + }; + + // Clear to 1.0 + let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("Dummy rain occlusion tex clearing encoder"), + }); + + encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("Clear dummy rain occlusion texture"), + color_attachments: &[], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &tex.view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Clear(1.0), + store: true, + }), + stencil_ops: None, + }), + }); + + queue.submit(std::iter::once(encoder.finish())); + + tex + } + + /// Create texture and view for rain ocllusion maps. + /// Returns (point, directed) + pub(super) fn create_view( + device: &wgpu::Device, + mode: &ShadowMapMode, + ) -> Result { + // (Attempt to) apply resolution factor to rain occlusion map resolution. + let resolution_factor = mode.resolution.clamped(0.25, 4.0); + + let max_texture_size = Renderer::max_texture_size_raw(device); + let size = + (RAIN_OCCLUSION_CHUNKS as f32).sqrt().ceil() as u32 * TerrainChunkSize::RECT_SIZE * 2; + + // Limit to max texture size, rather than erroring. + let size = size.map(|e| { + let size = e as f32 * resolution_factor; + // NOTE: We know 0 <= e since we clamped the resolution factor to be between + // 0.25 and 4.0. + if size <= max_texture_size as f32 { + size as u32 + } else { + max_texture_size + } + }); + + let levels = 1; + // Limit to max texture size rather than erroring. + let two_size = size.map(|e| { + u32::checked_next_power_of_two(e) + .filter(|&e| e <= max_texture_size) + .unwrap_or(max_texture_size) + }); + let min_size = size.reduce_min(); + let max_size = size.reduce_max(); + let _min_two_size = two_size.reduce_min(); + let _max_two_size = two_size.reduce_max(); + // For rotated shadow maps, the maximum size of a pixel along any axis is the + // size of a diagonal along that axis. + let diag_size = size.map(f64::from).magnitude(); + let diag_cross_size = f64::from(min_size) / f64::from(max_size) * diag_size; + let (diag_size, _diag_cross_size) = + if 0.0 < diag_size && diag_size <= f64::from(max_texture_size) { + // NOTE: diag_cross_size must be non-negative, since it is the ratio of a + // non-negative and a positive number (if max_size were zero, + // diag_size would be 0 too). And it must be <= diag_size, + // since min_size <= max_size. Therefore, if diag_size fits in a + // u16, so does diag_cross_size. + (diag_size as u32, diag_cross_size as u32) + } else { + // Limit to max texture resolution rather than error. + (max_texture_size as u32, max_texture_size as u32) + }; + let diag_two_size = u32::checked_next_power_of_two(diag_size) + .filter(|&e| e <= max_texture_size) + // Limit to max texture resolution rather than error. + .unwrap_or(max_texture_size) + // Make sure we don't try to create a zero sized texture (divided by 4 below) + .max(4); + + let rain_occlusion_tex = wgpu::TextureDescriptor { + label: None, + size: wgpu::Extent3d { + width: diag_two_size, + height: diag_two_size, + depth_or_array_layers: 1, + }, + mip_level_count: levels, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Depth24Plus, + usage: wgpu::TextureUsage::SAMPLED | wgpu::TextureUsage::RENDER_ATTACHMENT, + }; + + let rain_occlusion_view = wgpu::TextureViewDescriptor { + label: None, + format: Some(wgpu::TextureFormat::Depth24Plus), + dimension: Some(wgpu::TextureViewDimension::D2), + aspect: wgpu::TextureAspect::DepthOnly, + base_mip_level: 0, + mip_level_count: None, + base_array_layer: 0, + array_layer_count: None, + }; + + let sampler_info = wgpu::SamplerDescriptor { + label: None, + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::FilterMode::Nearest, + compare: Some(wgpu::CompareFunction::LessEqual), + ..Default::default() + }; + + let rain_occlusion_tex = Texture::new_raw( + device, + &rain_occlusion_tex, + &rain_occlusion_view, + &sampler_info, + ); + + Ok(rain_occlusion_tex) + } + + pub fn texture(&self) -> &Texture { + match self { + Self::Enabled(renderer) => &renderer.depth, + Self::Disabled(dummy) => dummy, + } + } +} diff --git a/voxygen/src/render/renderer/shaders.rs b/voxygen/src/render/renderer/shaders.rs index 8ee49843d8..6bb1b31d54 100644 --- a/voxygen/src/render/renderer/shaders.rs +++ b/voxygen/src/render/renderer/shaders.rs @@ -34,6 +34,7 @@ impl assets::Compound for Shaders { "include.random", "include.lod", "include.shadows", + "include.rain_occlusion", "include.point_glow", "antialias.none", "antialias.fxaa", @@ -45,6 +46,8 @@ impl assets::Compound for Shaders { "figure-vert", "light-shadows-figure-vert", "light-shadows-directed-vert", + "rain-occlusion-figure-vert", + "rain-occlusion-directed-vert", "point-light-shadows-vert", "skybox-vert", "skybox-frag", diff --git a/voxygen/src/scene/figure/mod.rs b/voxygen/src/scene/figure/mod.rs index 42d2017765..b7a0ec5f5e 100644 --- a/voxygen/src/scene/figure/mod.rs +++ b/voxygen/src/scene/figure/mod.rs @@ -17,7 +17,7 @@ use crate::{ camera::{Camera, CameraMode, Dependents}, math, terrain::Terrain, - SceneData, TrailMgr, + SceneData, TrailMgr, RAIN_THRESHOLD, }, }; use anim::{ @@ -620,6 +620,7 @@ impl FigureMgr { scene_data: &SceneData, // Visible chunk data. visible_psr_bounds: math::Aabr, + visible_por_bounds: math::Aabr, camera: &Camera, terrain: Option<&Terrain>, ) -> anim::vek::Aabb { @@ -637,25 +638,27 @@ impl FigureMgr { // of the image rendered from the light). If the position projected // with the ray_mat matrix is valid, and shadows are otherwise enabled, // we mark can_shadow. - let can_shadow_sun = { - let ray_direction = scene_data.get_sun_dir(); - let is_daylight = ray_direction.z < 0.0/*0.6*/; - // Are shadows enabled at all? - let can_shadow_sun = renderer.pipeline_modes().shadow.is_map() && is_daylight; + // Rain occlusion is very similar to sun shadows, but using a different ray_mat, + // and only if it's raining. + let (can_shadow_sun, can_occlude_rain) = { let Dependents { proj_mat: _, view_mat: _, cam_pos, .. } = camera.dependents(); - let cam_pos = math::Vec3::from(cam_pos); - let ray_direction = math::Vec3::from(ray_direction); - // Transform (semi) world space to light space. - let ray_mat: math::Mat4 = - math::Mat4::look_at_rh(cam_pos, cam_pos + ray_direction, math::Vec3::unit_y()); + let sun_dir = scene_data.get_sun_dir(); + let is_daylight = sun_dir.z < 0.0/*0.6*/; + // Are shadows enabled at all? + let can_shadow_sun = renderer.pipeline_modes().shadow.is_map() && is_daylight; + + let weather = scene_data.state.weather_at(cam_pos.xy()); + + let cam_pos = math::Vec3::from(cam_pos); + let focus_off = math::Vec3::from(camera.get_focus_pos().map(f32::trunc)); - let ray_mat = ray_mat * math::Mat4::translation_3d(-focus_off); + let focus_off_mat = math::Mat4::translation_3d(-focus_off); let collides_with_aabr = |a: math::Aabr, b: math::Aabr| { let min = math::Vec4::new(a.min.x, a.min.y, b.min.x, b.min.y); @@ -665,22 +668,40 @@ impl FigureMgr { #[cfg(not(feature = "simd"))] return min.partial_cmple(&max).reduce_and(); }; - move |pos: (anim::vek::Vec3,), radius: f32| { - // Short circuit when there are no shadows to cast. - if !can_shadow_sun { - return false; + + let can_shadow = |ray_direction: Vec3, + enabled: bool, + visible_bounds: math::Aabr| { + let ray_direction = math::Vec3::from(ray_direction); + // Transform (semi) world space to light space. + let ray_mat: math::Mat4 = + math::Mat4::look_at_rh(cam_pos, cam_pos + ray_direction, math::Vec3::unit_y()); + let ray_mat = ray_mat * focus_off_mat; + move |pos: (anim::vek::Vec3,), radius: f32| { + // Short circuit when there are no shadows to cast. + if !enabled { + return false; + } + // First project center onto shadow map. + let center = (ray_mat * math::Vec4::new(pos.0.x, pos.0.y, pos.0.z, 1.0)).xy(); + // Then, create an approximate bounding box (± radius). + let figure_box = math::Aabr { + min: center - radius, + max: center + radius, + }; + // Quick intersection test for membership in the PSC (potential shader caster) + // list. + collides_with_aabr(figure_box, visible_bounds) } - // First project center onto shadow map. - let center = (ray_mat * math::Vec4::new(pos.0.x, pos.0.y, pos.0.z, 1.0)).xy(); - // Then, create an approximate bounding box (± radius). - let figure_box = math::Aabr { - min: center - radius, - max: center + radius, - }; - // Quick intersection test for membership in the PSC (potential shader caster) - // list. - collides_with_aabr(figure_box, visible_psr_bounds) - } + }; + ( + can_shadow(sun_dir, can_shadow_sun, visible_psr_bounds), + can_shadow( + weather.rain_vel(), + weather.rain > RAIN_THRESHOLD, + visible_por_bounds, + ), + ) }; // Get player position. @@ -812,6 +833,8 @@ impl FigureMgr { } else if vd_frac > 1.0 { state.as_mut().map(|state| state.visible = false); // Keep processing if this might be a shadow caster. + // NOTE: Not worth to do for rain_occlusion, since that only happens in closeby + // chunks. if !can_shadow_prev { continue; } @@ -841,6 +864,7 @@ impl FigureMgr { } else { // Check whether we can shadow. meta.can_shadow_sun = can_shadow_sun(pos, radius); + meta.can_occlude_rain = can_occlude_rain(pos, radius); } (in_frustum, lpindex) } else { @@ -5551,17 +5575,16 @@ impl FigureMgr { visible_aabb } - pub fn render_shadows<'a>( + fn render_shadow_mapping<'a>( &'a self, drawer: &mut FigureShadowDrawer<'_, 'a>, state: &State, tick: u64, (camera, figure_lod_render_distance): CameraData, + filter_state: impl Fn(&FigureStateMeta) -> bool, ) { - span!(_guard, "render_shadows", "FigureManager::render_shadows"); let ecs = state.ecs(); let items = ecs.read_storage::(); - ( &ecs.entities(), &ecs.read_storage::(), @@ -5590,7 +5613,7 @@ impl FigureMgr { Some(Collider::Volume(vol)) => vol.mut_count, _ => 0, }, - |state| state.can_shadow_sun(), + &filter_state, if matches!(body, Body::ItemDrop(_)) { items.get(entity).map(ItemKey::from) } else { None }, ) { drawer.draw(model, bound); @@ -5598,6 +5621,36 @@ impl FigureMgr { }); } + pub fn render_shadows<'a>( + &'a self, + drawer: &mut FigureShadowDrawer<'_, 'a>, + state: &State, + tick: u64, + camera_data: CameraData, + ) { + span!(_guard, "render_shadows", "FigureManager::render_shadows"); + self.render_shadow_mapping(drawer, state, tick, camera_data, |state| { + state.can_shadow_sun() + }) + } + + pub fn render_rain_occlusion<'a>( + &'a self, + drawer: &mut FigureShadowDrawer<'_, 'a>, + state: &State, + tick: u64, + camera_data: CameraData, + ) { + span!( + _guard, + "render_rain_occlusion", + "FigureManager::render_rain_occlusion" + ); + self.render_shadow_mapping(drawer, state, tick, camera_data, |state| { + state.can_occlude_rain() + }) + } + pub fn render<'a>( &'a self, drawer: &mut FigureDrawer<'_, 'a>, @@ -6237,6 +6290,7 @@ pub struct FigureStateMeta { last_ori: anim::vek::Quaternion, lpindex: u8, can_shadow_sun: bool, + can_occlude_rain: bool, visible: bool, last_pos: Option>, avg_vel: anim::vek::Vec3, @@ -6253,6 +6307,11 @@ impl FigureStateMeta { // Either visible, or explicitly a shadow caster. self.visible || self.can_shadow_sun } + + pub fn can_occlude_rain(&self) -> bool { + // Either visible, or explicitly a rain occluder. + self.visible || self.can_occlude_rain + } } pub struct FigureState { @@ -6311,6 +6370,7 @@ impl FigureState { lpindex: 0, visible: false, can_shadow_sun: false, + can_occlude_rain: false, last_pos: None, avg_vel: anim::vek::Vec3::zero(), last_light: 1.0, diff --git a/voxygen/src/scene/lod.rs b/voxygen/src/scene/lod.rs index d8ce54672a..dddfb0bcfd 100644 --- a/voxygen/src/scene/lod.rs +++ b/voxygen/src/scene/lod.rs @@ -13,6 +13,7 @@ use common::{ lod, spiral::Spiral2d, util::srgba_to_linear, + weather, }; use hashbrown::HashMap; use std::ops::Range; @@ -52,6 +53,7 @@ impl Lod { client.world_data().lod_base.raw(), client.world_data().lod_alt.raw(), client.world_data().lod_horizon.raw(), + client.world_data().chunk_size().as_() / weather::CHUNKS_PER_CELL, settings.graphics.lod_detail.max(100).min(2500), /* TODO: figure out how we want to do this without color borders? * water_color().into_array().into(), */ @@ -180,6 +182,20 @@ impl Lod { } } } + // Update weather texture + // NOTE: consider moving the lerping to a shader if the overhead of uploading to + // the gpu each frame becomes an issue. + let weather = client.state().weather_grid(); + let size = weather.size().as_::(); + renderer.update_texture( + &self.data.weather, + [0, 0], + [size.x, size.y], + &weather + .iter() + .map(|(_, w)| [(w.cloud * 255.0) as u8, (w.rain * 255.0) as u8, 0, 0]) + .collect::>(), + ); } pub fn render<'a>(&'a self, drawer: &mut FirstPassDrawer<'a>) { diff --git a/voxygen/src/scene/mod.rs b/voxygen/src/scene/mod.rs index 9041035479..76ff9aaeaf 100644 --- a/voxygen/src/scene/mod.rs +++ b/voxygen/src/scene/mod.rs @@ -19,11 +19,11 @@ pub use self::{ trail::TrailMgr, }; use crate::{ - audio::{ambient::AmbientMgr, music::MusicMgr, sfx::SfxMgr, AudioFrontend}, + audio::{ambient, ambient::AmbientMgr, music::MusicMgr, sfx::SfxMgr, AudioFrontend}, render::{ create_skybox_mesh, CloudsLocals, Consts, Drawer, GlobalModel, Globals, GlobalsBindGroup, - Light, Model, PointLightMatrix, PostProcessLocals, Renderer, Shadow, ShadowLocals, - SkyboxVertex, + Light, Model, PointLightMatrix, PostProcessLocals, RainOcclusionLocals, Renderer, Shadow, + ShadowLocals, SkyboxVertex, }, settings::Settings, window::{AnalogGameInput, Event}, @@ -65,6 +65,9 @@ const SHADOW_FAR: f32 = 128.0; // Far plane for shadow map point light rendering /// Used for first person camera effects const RUNNING_THRESHOLD: f32 = 0.7; +/// The threashold for starting calculations with rain. +const RAIN_THRESHOLD: f32 = 0.0; + /// is_daylight, array of active lights. pub type LightData<'a> = (bool, &'a [Light]); @@ -103,6 +106,8 @@ pub struct Scene { pub sfx_mgr: SfxMgr, music_mgr: MusicMgr, ambient_mgr: AmbientMgr, + + integrated_rain_vel: f32, } pub struct SceneData<'a> { @@ -280,6 +285,8 @@ impl Scene { lights: renderer.create_consts(&[Light::default(); MAX_LIGHT_COUNT]), shadows: renderer.create_consts(&[Shadow::default(); MAX_SHADOW_COUNT]), shadow_mats: renderer.create_shadow_bound_locals(&[ShadowLocals::default()]), + rain_occlusion_mats: renderer + .create_rain_occlusion_bound_locals(&[RainOcclusionLocals::default()]), point_light_matrices: Box::new([PointLightMatrix::default(); MAX_LIGHT_COUNT * 6 + 6]), }; @@ -314,7 +321,10 @@ impl Scene { figure_mgr: FigureMgr::new(renderer), sfx_mgr: SfxMgr::default(), music_mgr: MusicMgr::default(), - ambient_mgr: AmbientMgr::default(), + ambient_mgr: AmbientMgr { + ambience: ambient::load_ambience_items(), + }, + integrated_rain_vel: 0.0, } } @@ -402,11 +412,18 @@ impl Scene { outcome: &Outcome, scene_data: &SceneData, audio: &mut AudioFrontend, + state: &State, + cam_pos: Vec3, ) { span!(_guard, "handle_outcome", "Scene::handle_outcome"); + let underwater = state + .terrain() + .get(cam_pos.map(|e| e.floor() as i32)) + .map(|b| b.is_liquid()) + .unwrap_or(false); self.particle_mgr.handle_outcome(outcome, scene_data); self.sfx_mgr - .handle_outcome(outcome, audio, scene_data.client); + .handle_outcome(outcome, audio, scene_data.client, underwater); match outcome { Outcome::Explosion { @@ -464,6 +481,8 @@ impl Scene { // Get player position. let ecs = scene_data.state.ecs(); + let dt = ecs.fetch::().0; + let player_pos = ecs .read_storage::() .get(scene_data.player_entity) @@ -527,11 +546,8 @@ impl Scene { }; // Tick camera for interpolation. - self.camera.update( - scene_data.state.get_time(), - scene_data.state.get_delta_time(), - scene_data.mouse_smoothing, - ); + self.camera + .update(scene_data.state.get_time(), dt, scene_data.mouse_smoothing); // Compute camera matrices. self.camera.compute_dependents(&*scene_data.state.terrain()); @@ -602,7 +618,6 @@ impl Scene { renderer.update_consts(&mut self.data.lights, lights); // Update event lights - let dt = ecs.fetch::().0; self.event_lights.drain_filter(|el| { el.timeout -= dt; el.timeout <= 0.0 @@ -688,7 +703,13 @@ impl Scene { self.debug.maintain(renderer); // Maintain the terrain. - let (_visible_bounds, visible_light_volume, visible_psr_bounds) = self.terrain.maintain( + let ( + _visible_bounds, + visible_light_volume, + visible_psr_bounds, + visible_occlusion_volume, + visible_por_bounds, + ) = self.terrain.maintain( renderer, scene_data, focus_pos, @@ -702,17 +723,320 @@ impl Scene { &mut self.trail_mgr, scene_data, visible_psr_bounds, + visible_por_bounds, &self.camera, Some(&self.terrain), ); + let fov = self.camera.get_effective_fov(); + let aspect_ratio = self.camera.get_aspect_ratio(); + let view_dir = ((focus_pos.map(f32::fract)) - cam_pos).normalized(); + + // We need to compute these offset matrices to transform world space coordinates + // to the translated ones we use when multiplying by the light space + // matrix; this helps avoid precision loss during the + // multiplication. + let look_at = math::Vec3::from(cam_pos); + let new_dir = math::Vec3::from(view_dir); + let new_dir = new_dir.normalized(); + let up: math::Vec3 = math::Vec3::unit_y(); + + // Optimal warping for directed lights: + // + // n_opt = 1 / sin y (z_n + √(z_n + (f - n) sin y)) + // + // where n is near plane, f is far plane, y is the tilt angle between view and + // light direction, and n_opt is the optimal near plane. + // We also want a way to transform and scale this matrix (* 0.5 + 0.5) in order + // to transform it correctly into texture coordinates, as well as + // OpenGL coordinates. Note that the matrix for directional light + // is *already* linear in the depth buffer. + // + // Also, observe that we flip the texture sampling matrix in order to account + // for the fact that DirectX renders top-down. + let texture_mat = Mat4::::scaling_3d::>(Vec3::new(0.5, -0.5, 1.0)) + * Mat4::translation_3d(Vec3::new(1.0, -1.0, 0.0)); + + let directed_mats = |d_view_mat: math::Mat4, + d_dir: math::Vec3, + volume: &Vec>| + -> (Mat4, Mat4) { + // NOTE: Light view space, right-handed. + let v_p_orig = math::Vec3::from(d_view_mat * math::Vec4::from_direction(new_dir)); + let mut v_p = v_p_orig.normalized(); + let cos_gamma = new_dir.map(f64::from).dot(d_dir.map(f64::from)); + let sin_gamma = (1.0 - cos_gamma * cos_gamma).sqrt(); + let gamma = sin_gamma.asin(); + let view_mat = math::Mat4::from_col_array(view_mat.into_col_array()); + // coordinates are transformed from world space (right-handed) to view space + // (right-handed). + let bounds1 = math::fit_psr( + view_mat.map_cols(math::Vec4::from), + volume.iter().copied(), + math::Vec4::homogenized, + ); + let n_e = f64::from(-bounds1.max.z); + let factor = compute_warping_parameter_perspective( + gamma, + n_e, + f64::from(fov), + f64::from(aspect_ratio), + ); + + v_p.z = 0.0; + v_p.normalize(); + let l_r: math::Mat4 = if factor > EPSILON_UPSILON { + // NOTE: Our coordinates are now in left-handed space, but v_p isn't; however, + // v_p has no z component, so we don't have to adjust it for left-handed + // spaces. + math::Mat4::look_at_lh(math::Vec3::zero(), math::Vec3::unit_z(), v_p) + } else { + math::Mat4::identity() + }; + // Convert from right-handed to left-handed coordinates. + let directed_proj_mat = math::Mat4::new( + 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 1.0, + ); + + let light_all_mat = l_r * directed_proj_mat * d_view_mat; + // coordinates are transformed from world space (right-handed) to rotated light + // space (left-handed). + let bounds0 = math::fit_psr( + light_all_mat, + volume.iter().copied(), + math::Vec4::homogenized, + ); + // Vague idea: project z_n from the camera view to the light view (where it's + // tilted by γ). + // + // NOTE: To transform a normal by M, we multiply by the transpose of the inverse + // of M. For the cases below, we are transforming by an + // already-inverted matrix, so the transpose of its inverse is + // just the transpose of the original matrix. + let (z_0, z_1) = { + let f_e = f64::from(-bounds1.min.z).max(n_e); + // view space, right-handed coordinates. + let p_z = bounds1.max.z; + // rotated light space, left-handed coordinates. + let p_y = bounds0.min.y; + let p_x = bounds0.center().x; + // moves from view-space (right-handed) to world space (right-handed) + let view_inv = view_mat.inverted(); + // moves from rotated light space (left-handed) to world space (right-handed). + let light_all_inv = light_all_mat.inverted(); + + // moves from view-space (right-handed) to world-space (right-handed). + let view_point = view_inv + * math::Vec4::from_point( + -math::Vec3::unit_z() * p_z, /* + math::Vec4::unit_w() */ + ); + let view_plane = view_mat.transposed() * -math::Vec4::unit_z(); + + // moves from rotated light space (left-handed) to world space (right-handed). + let light_point = light_all_inv + * math::Vec4::from_point( + math::Vec3::unit_y() * p_y, /* + math::Vec4::unit_w() */ + ); + let light_plane = light_all_mat.transposed() * math::Vec4::unit_y(); + + // moves from rotated light space (left-handed) to world space (right-handed). + let shadow_point = light_all_inv + * math::Vec4::from_point( + math::Vec3::unit_x() * p_x, /* + math::Vec4::unit_w() */ + ); + let shadow_plane = light_all_mat.transposed() * math::Vec4::unit_x(); + + // Find the point at the intersection of the three planes; note that since the + // equations are already in right-handed world space, we don't need to negate + // the z coordinates. + let solve_p0 = math::Mat4::new( + view_plane.x, + view_plane.y, + view_plane.z, + 0.0, + light_plane.x, + light_plane.y, + light_plane.z, + 0.0, + shadow_plane.x, + shadow_plane.y, + shadow_plane.z, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + ); + + // in world-space (right-handed). + let plane_dist = math::Vec4::new( + view_plane.dot(view_point), + light_plane.dot(light_point), + shadow_plane.dot(shadow_point), + 1.0, + ); + let p0_world = solve_p0.inverted() * plane_dist; + // in rotated light-space (left-handed). + let p0 = light_all_mat * p0_world; + let mut p1 = p0; + // in rotated light-space (left-handed). + p1.y = bounds0.max.y; + + // transforms from rotated light-space (left-handed) to view space + // (right-handed). + let view_from_light_mat = view_mat * light_all_inv; + // z0 and z1 are in view space (right-handed). + let z0 = view_from_light_mat * p0; + let z1 = view_from_light_mat * p1; + + // Extract the homogenized forward component (right-handed). + // + // NOTE: I don't think the w component should be anything but 1 here, but + // better safe than sorry. + ( + f64::from(z0.homogenized().dot(-math::Vec4::unit_z())).clamp(n_e, f_e), + f64::from(z1.homogenized().dot(-math::Vec4::unit_z())).clamp(n_e, f_e), + ) + }; + + // all of this is in rotated light-space (left-handed). + let mut light_focus_pos: math::Vec3 = math::Vec3::zero(); + light_focus_pos.x = bounds0.center().x; + light_focus_pos.y = bounds0.min.y; + light_focus_pos.z = bounds0.center().z; + + let d = f64::from(bounds0.max.y - bounds0.min.y).abs(); + + let w_l_y = d; + + // NOTE: See section 5.1.2.2 of Lloyd's thesis. + // NOTE: Since z_1 and z_0 are in the same coordinate space, we don't have to + // worry about the handedness of their ratio. + let alpha = z_1 / z_0; + let alpha_sqrt = alpha.sqrt(); + let directed_near_normal = if factor < 0.0 { + // Standard shadow map to LiSPSM + (1.0 + alpha_sqrt - factor * (alpha - 1.0)) / ((alpha - 1.0) * (factor + 1.0)) + } else { + // LiSPSM to PSM + ((alpha_sqrt - 1.0) * (factor * alpha_sqrt + 1.0)).recip() + }; + + // Equation 5.14 - 5.16 + let y_ = |v: f64| w_l_y * (v + directed_near_normal).abs(); + let directed_near = y_(0.0) as f32; + let directed_far = y_(1.0) as f32; + light_focus_pos.y = if factor > EPSILON_UPSILON { + light_focus_pos.y - directed_near + } else { + light_focus_pos.y + }; + // Left-handed translation. + let w_v: math::Mat4 = math::Mat4::translation_3d(-math::Vec3::new( + light_focus_pos.x, + light_focus_pos.y, + light_focus_pos.z, + )); + let shadow_view_mat: math::Mat4 = w_v * light_all_mat; + let w_p: math::Mat4 = { + if factor > EPSILON_UPSILON { + // Projection for y + let near = directed_near; + let far = directed_far; + let left = -1.0; + let right = 1.0; + let bottom = -1.0; + let top = 1.0; + let s_x = 2.0 * near / (right - left); + let o_x = (right + left) / (right - left); + let s_z = 2.0 * near / (top - bottom); + let o_z = (top + bottom) / (top - bottom); + + let s_y = (far + near) / (far - near); + let o_y = -2.0 * far * near / (far - near); + + math::Mat4::new( + s_x, o_x, 0.0, 0.0, 0.0, s_y, 0.0, o_y, 0.0, o_z, s_z, 0.0, 0.0, 1.0, 0.0, + 0.0, + ) + } else { + math::Mat4::identity() + } + }; + + let shadow_all_mat: math::Mat4 = w_p * shadow_view_mat; + // coordinates are transformed from world space (right-handed) + // to post-warp light space (left-handed), then homogenized. + let math::Aabb:: { + min: + math::Vec3 { + x: xmin, + y: ymin, + z: zmin, + }, + max: + math::Vec3 { + x: xmax, + y: ymax, + z: zmax, + }, + } = math::fit_psr( + shadow_all_mat, + volume.iter().copied(), + math::Vec4::homogenized, + ); + let s_x = 2.0 / (xmax - xmin); + let s_y = 2.0 / (ymax - ymin); + let s_z = 1.0 / (zmax - zmin); + let o_x = -(xmax + xmin) / (xmax - xmin); + let o_y = -(ymax + ymin) / (ymax - ymin); + let o_z = -zmin / (zmax - zmin); + let directed_proj_mat = Mat4::new( + s_x, 0.0, 0.0, o_x, 0.0, s_y, 0.0, o_y, 0.0, 0.0, s_z, o_z, 0.0, 0.0, 0.0, 1.0, + ); + + let shadow_all_mat: Mat4 = Mat4::from_col_arrays(shadow_all_mat.into_col_arrays()); + + let directed_texture_proj_mat = texture_mat * directed_proj_mat; + ( + directed_proj_mat * shadow_all_mat, + directed_texture_proj_mat * shadow_all_mat, + ) + }; + + let weather = client + .state() + .max_weather_near(focus_off.xy() + cam_pos.xy()); + if weather.rain > RAIN_THRESHOLD { + let weather = client.state().weather_at(focus_off.xy() + cam_pos.xy()); + let rain_vel = weather.rain_vel(); + let rain_view_mat = math::Mat4::look_at_rh(look_at, look_at + rain_vel, up); + + self.integrated_rain_vel += rain_vel.magnitude() * dt; + let rain_dir_mat = Mat4::rotation_from_to_3d(-Vec3::unit_z(), rain_vel); + + let (shadow_mat, texture_mat) = + directed_mats(rain_view_mat, rain_vel.into(), &visible_occlusion_volume); + + let rain_occlusion_locals = RainOcclusionLocals::new( + shadow_mat, + texture_mat, + rain_dir_mat, + weather.rain, + self.integrated_rain_vel, + ); + + renderer.update_consts(&mut self.data.rain_occlusion_mats, &[rain_occlusion_locals]); + } else if self.integrated_rain_vel > 0.0 { + self.integrated_rain_vel = 0.0; + // Need to set rain to zero + let rain_occlusion_locals = RainOcclusionLocals::default(); + renderer.update_consts(&mut self.data.rain_occlusion_mats, &[rain_occlusion_locals]); + } + let sun_dir = scene_data.get_sun_dir(); let is_daylight = sun_dir.z < 0.0; if renderer.pipeline_modes().shadow.is_map() && (is_daylight || !lights.is_empty()) { - let fov = self.camera.get_effective_fov(); - let aspect_ratio = self.camera.get_aspect_ratio(); - - let view_dir = ((focus_pos.map(f32::fract)) - cam_pos).normalized(); let (point_shadow_res, _directed_shadow_res) = renderer.get_shadow_resolution(); // NOTE: The aspect ratio is currently always 1 for our cube maps, since they // are equal on all sides. @@ -721,282 +1045,18 @@ impl Scene { // and moon. let directed_light_dir = math::Vec3::from(sun_dir); - // Optimal warping for directed lights: - // - // n_opt = 1 / sin y (z_n + √(z_n + (f - n) sin y)) - // - // where n is near plane, f is far plane, y is the tilt angle between view and - // light direction, and n_opt is the optimal near plane. - // We also want a way to transform and scale this matrix (* 0.5 + 0.5) in order - // to transform it correctly into texture coordinates, as well as - // OpenGL coordinates. Note that the matrix for directional light - // is *already* linear in the depth buffer. - // - // Also, observe that we flip the texture sampling matrix in order to account - // for the fact that DirectX renders top-down. - let texture_mat = Mat4::::scaling_3d::>(Vec3::new(0.5, -0.5, 1.0)) - * Mat4::translation_3d(Vec3::new(1.0, -1.0, 0.0)); - // We need to compute these offset matrices to transform world space coordinates - // to the translated ones we use when multiplying by the light space - // matrix; this helps avoid precision loss during the - // multiplication. - let look_at = math::Vec3::from(cam_pos); // We upload view matrices as well, to assist in linearizing vertex positions. // (only for directional lights, so far). let mut directed_shadow_mats = Vec::with_capacity(6); - let new_dir = math::Vec3::from(view_dir); - let new_dir = new_dir.normalized(); - let up: math::Vec3 = math::Vec3::unit_y(); + let light_view_mat = math::Mat4::look_at_rh(look_at, look_at + directed_light_dir, up); - { - // NOTE: Light view space, right-handed. - let v_p_orig = - math::Vec3::from(light_view_mat * math::Vec4::from_direction(new_dir)); - let mut v_p = v_p_orig.normalized(); - let cos_gamma = new_dir - .map(f64::from) - .dot(directed_light_dir.map(f64::from)); - let sin_gamma = (1.0 - cos_gamma * cos_gamma).sqrt(); - let gamma = sin_gamma.asin(); - let view_mat = math::Mat4::from_col_array(view_mat.into_col_array()); - // coordinates are transformed from world space (right-handed) to view space - // (right-handed). - let bounds1 = math::fit_psr( - view_mat.map_cols(math::Vec4::from), - visible_light_volume.iter().copied(), - math::Vec4::homogenized, - ); - let n_e = f64::from(-bounds1.max.z); - let factor = compute_warping_parameter_perspective( - gamma, - n_e, - f64::from(fov), - f64::from(aspect_ratio), - ); + let (shadow_mat, texture_mat) = + directed_mats(light_view_mat, directed_light_dir, &visible_light_volume); - v_p.z = 0.0; - v_p.normalize(); - let l_r: math::Mat4 = if factor > EPSILON_UPSILON { - // NOTE: Our coordinates are now in left-handed space, but v_p isn't; however, - // v_p has no z component, so we don't have to adjust it for left-handed - // spaces. - math::Mat4::look_at_lh(math::Vec3::zero(), math::Vec3::unit_z(), v_p) - } else { - math::Mat4::identity() - }; - // Convert from right-handed to left-handed coordinates. - let directed_proj_mat = math::Mat4::new( - 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 1.0, - ); + let shadow_locals = ShadowLocals::new(shadow_mat, texture_mat); - let light_all_mat = l_r * directed_proj_mat * light_view_mat; - // coordinates are transformed from world space (right-handed) to rotated light - // space (left-handed). - let bounds0 = math::fit_psr( - light_all_mat, - visible_light_volume.iter().copied(), - math::Vec4::homogenized, - ); - // Vague idea: project z_n from the camera view to the light view (where it's - // tilted by γ). - // - // NOTE: To transform a normal by M, we multiply by the transpose of the inverse - // of M. For the cases below, we are transforming by an - // already-inverted matrix, so the transpose of its inverse is - // just the transpose of the original matrix. - let (z_0, z_1) = { - let f_e = f64::from(-bounds1.min.z).max(n_e); - // view space, right-handed coordinates. - let p_z = bounds1.max.z; - // rotated light space, left-handed coordinates. - let p_y = bounds0.min.y; - let p_x = bounds0.center().x; - // moves from view-space (right-handed) to world space (right-handed) - let view_inv = view_mat.inverted(); - // moves from rotated light space (left-handed) to world space (right-handed). - let light_all_inv = light_all_mat.inverted(); + renderer.update_consts(&mut self.data.shadow_mats, &[shadow_locals]); - // moves from view-space (right-handed) to world-space (right-handed). - let view_point = view_inv - * math::Vec4::from_point( - -math::Vec3::unit_z() * p_z, /* + math::Vec4::unit_w() */ - ); - let view_plane = view_mat.transposed() * -math::Vec4::unit_z(); - - // moves from rotated light space (left-handed) to world space (right-handed). - let light_point = light_all_inv - * math::Vec4::from_point( - math::Vec3::unit_y() * p_y, /* + math::Vec4::unit_w() */ - ); - let light_plane = light_all_mat.transposed() * math::Vec4::unit_y(); - - // moves from rotated light space (left-handed) to world space (right-handed). - let shadow_point = light_all_inv - * math::Vec4::from_point( - math::Vec3::unit_x() * p_x, /* + math::Vec4::unit_w() */ - ); - let shadow_plane = light_all_mat.transposed() * math::Vec4::unit_x(); - - // Find the point at the intersection of the three planes; note that since the - // equations are already in right-handed world space, we don't need to negate - // the z coordinates. - let solve_p0 = math::Mat4::new( - view_plane.x, - view_plane.y, - view_plane.z, - 0.0, - light_plane.x, - light_plane.y, - light_plane.z, - 0.0, - shadow_plane.x, - shadow_plane.y, - shadow_plane.z, - 0.0, - 0.0, - 0.0, - 0.0, - 1.0, - ); - - // in world-space (right-handed). - let plane_dist = math::Vec4::new( - view_plane.dot(view_point), - light_plane.dot(light_point), - shadow_plane.dot(shadow_point), - 1.0, - ); - let p0_world = solve_p0.inverted() * plane_dist; - // in rotated light-space (left-handed). - let p0 = light_all_mat * p0_world; - let mut p1 = p0; - // in rotated light-space (left-handed). - p1.y = bounds0.max.y; - - // transforms from rotated light-space (left-handed) to view space - // (right-handed). - let view_from_light_mat = view_mat * light_all_inv; - // z0 and z1 are in view space (right-handed). - let z0 = view_from_light_mat * p0; - let z1 = view_from_light_mat * p1; - - // Extract the homogenized forward component (right-handed). - // - // NOTE: I don't think the w component should be anything but 1 here, but - // better safe than sorry. - ( - f64::from(z0.homogenized().dot(-math::Vec4::unit_z())).clamp(n_e, f_e), - f64::from(z1.homogenized().dot(-math::Vec4::unit_z())).clamp(n_e, f_e), - ) - }; - - // all of this is in rotated light-space (left-handed). - let mut light_focus_pos: math::Vec3 = math::Vec3::zero(); - light_focus_pos.x = bounds0.center().x; - light_focus_pos.y = bounds0.min.y; - light_focus_pos.z = bounds0.center().z; - - let d = f64::from(bounds0.max.y - bounds0.min.y).abs(); - - let w_l_y = d; - - // NOTE: See section 5.1.2.2 of Lloyd's thesis. - // NOTE: Since z_1 and z_0 are in the same coordinate space, we don't have to - // worry about the handedness of their ratio. - let alpha = z_1 / z_0; - let alpha_sqrt = alpha.sqrt(); - let directed_near_normal = if factor < 0.0 { - // Standard shadow map to LiSPSM - (1.0 + alpha_sqrt - factor * (alpha - 1.0)) / ((alpha - 1.0) * (factor + 1.0)) - } else { - // LiSPSM to PSM - ((alpha_sqrt - 1.0) * (factor * alpha_sqrt + 1.0)).recip() - }; - - // Equation 5.14 - 5.16 - let y_ = |v: f64| w_l_y * (v + directed_near_normal).abs(); - let directed_near = y_(0.0) as f32; - let directed_far = y_(1.0) as f32; - light_focus_pos.y = if factor > EPSILON_UPSILON { - light_focus_pos.y - directed_near - } else { - light_focus_pos.y - }; - // Left-handed translation. - let w_v: math::Mat4 = math::Mat4::translation_3d(-math::Vec3::new( - light_focus_pos.x, - light_focus_pos.y, - light_focus_pos.z, - )); - let shadow_view_mat: math::Mat4 = w_v * light_all_mat; - let w_p: math::Mat4 = { - if factor > EPSILON_UPSILON { - // Projection for y - let near = directed_near; - let far = directed_far; - let left = -1.0; - let right = 1.0; - let bottom = -1.0; - let top = 1.0; - let s_x = 2.0 * near / (right - left); - let o_x = (right + left) / (right - left); - let s_z = 2.0 * near / (top - bottom); - let o_z = (top + bottom) / (top - bottom); - - let s_y = (far + near) / (far - near); - let o_y = -2.0 * far * near / (far - near); - - math::Mat4::new( - s_x, o_x, 0.0, 0.0, 0.0, s_y, 0.0, o_y, 0.0, o_z, s_z, 0.0, 0.0, 1.0, - 0.0, 0.0, - ) - } else { - math::Mat4::identity() - } - }; - - let shadow_all_mat: math::Mat4 = w_p * shadow_view_mat; - // coordinates are transformed from world space (right-handed) - // to post-warp light space (left-handed), then homogenized. - let math::Aabb:: { - min: - math::Vec3 { - x: xmin, - y: ymin, - z: zmin, - }, - max: - math::Vec3 { - x: xmax, - y: ymax, - z: zmax, - }, - } = math::fit_psr( - shadow_all_mat, - visible_light_volume.iter().copied(), - math::Vec4::homogenized, - ); - let s_x = 2.0 / (xmax - xmin); - let s_y = 2.0 / (ymax - ymin); - let s_z = 1.0 / (zmax - zmin); - let o_x = -(xmax + xmin) / (xmax - xmin); - let o_y = -(ymax + ymin) / (ymax - ymin); - let o_z = -zmin / (zmax - zmin); - let directed_proj_mat = Mat4::new( - s_x, 0.0, 0.0, o_x, 0.0, s_y, 0.0, o_y, 0.0, 0.0, s_z, o_z, 0.0, 0.0, 0.0, 1.0, - ); - - let shadow_all_mat: Mat4 = - Mat4::from_col_arrays(shadow_all_mat.into_col_arrays()); - - let directed_texture_proj_mat = texture_mat * directed_proj_mat; - let shadow_locals = ShadowLocals::new( - directed_proj_mat * shadow_all_mat, - directed_texture_proj_mat * shadow_all_mat, - ); - - renderer.update_consts(&mut self.data.shadow_mats, &[shadow_locals]); - } directed_shadow_mats.push(light_view_mat); // This leaves us with five dummy slots, which we push as defaults. directed_shadow_mats @@ -1064,9 +1124,11 @@ impl Scene { &self.terrain, client, ); - self.music_mgr.maintain(audio, scene_data.state, client); + self.ambient_mgr .maintain(audio, scene_data.state, client, &self.camera); + + self.music_mgr.maintain(audio, scene_data.state, client); } pub fn global_bind_group(&self) -> &GlobalsBindGroup { &self.globals_bind_group } @@ -1085,6 +1147,7 @@ impl Scene { let is_daylight = sun_dir.z < 0.0; let focus_pos = self.camera.get_focus_pos(); let cam_pos = self.camera.dependents().cam_pos + focus_pos.map(|e| e.trunc()); + let is_rain = state.max_weather_near(cam_pos.xy()).rain > RAIN_THRESHOLD; let camera_data = (&self.camera, scene_data.figure_lod_render_distance); @@ -1116,6 +1179,21 @@ impl Scene { ) } } + // Render rain occlusion texture + if is_rain { + prof_span!("rain occlusion"); + if let Some(mut occlusion_pass) = drawer.rain_occlusion_pass() { + self.terrain + .render_rain_occlusion(&mut occlusion_pass.draw_terrain_shadows(), cam_pos); + + self.figure_mgr.render_rain_occlusion( + &mut occlusion_pass.draw_figure_shadows(), + state, + tick, + camera_data, + ); + } + } prof_span!(guard, "main pass"); if let Some(mut first_pass) = drawer.first_pass() { diff --git a/voxygen/src/scene/simple.rs b/voxygen/src/scene/simple.rs index 4bda1f2d92..7e5d284e30 100644 --- a/voxygen/src/scene/simple.rs +++ b/voxygen/src/scene/simple.rs @@ -2,8 +2,8 @@ use crate::{ mesh::{greedy::GreedyMesh, segment::generate_mesh_base_vol_terrain}, render::{ create_skybox_mesh, BoneMeshes, Consts, FigureModel, FirstPassDrawer, GlobalModel, Globals, - GlobalsBindGroup, Light, LodData, Mesh, Model, PointLightMatrix, Renderer, Shadow, - ShadowLocals, SkyboxVertex, TerrainVertex, + GlobalsBindGroup, Light, LodData, Mesh, Model, PointLightMatrix, RainOcclusionLocals, + Renderer, Shadow, ShadowLocals, SkyboxVertex, TerrainVertex, }, scene::{ camera::{self, Camera, CameraMode}, @@ -113,6 +113,8 @@ impl Scene { lights: renderer.create_consts(&[Light::default(); 20]), shadows: renderer.create_consts(&[Shadow::default(); 24]), shadow_mats: renderer.create_shadow_bound_locals(&[ShadowLocals::default()]), + rain_occlusion_mats: renderer + .create_rain_occlusion_bound_locals(&[RainOcclusionLocals::default()]), point_light_matrices: Box::new([PointLightMatrix::default(); 126]), }; let lod = LodData::dummy(renderer); diff --git a/voxygen/src/scene/terrain.rs b/voxygen/src/scene/terrain.rs index 35f5cee20b..ee5d729783 100644 --- a/voxygen/src/scene/terrain.rs +++ b/voxygen/src/scene/terrain.rs @@ -18,7 +18,7 @@ use crate::{ use super::{ camera::{self, Camera}, - math, SceneData, + math, SceneData, RAIN_THRESHOLD, }; use common::{ assets::{self, AssetExt, DotVoxAsset}, @@ -46,6 +46,10 @@ use vek::*; const SPRITE_SCALE: Vec3 = Vec3::new(1.0 / 11.0, 1.0 / 11.0, 1.0 / 11.0); const SPRITE_LOD_LEVELS: usize = 5; +// For rain occlusion we only need to render the closest chunks. +/// How many chunks are maximally rendered for rain occlusion. +pub const RAIN_OCCLUSION_CHUNKS: usize = 9; + #[derive(Clone, Copy, Debug)] struct Visibility { in_range: bool, @@ -812,10 +816,17 @@ impl Terrain { focus_pos: Vec3, loaded_distance: f32, camera: &Camera, - ) -> (Aabb, Vec>, math::Aabr) { + ) -> ( + Aabb, + Vec>, + math::Aabr, + Vec>, + math::Aabr, + ) { let camera::Dependents { view_mat, proj_mat_treeculler, + cam_pos, .. } = camera.dependents(); @@ -1289,6 +1300,10 @@ impl Terrain { min: focus_pos - 2.0, max: focus_pos + 2.0, }); + let inv_proj_view = + math::Mat4::from_col_arrays((proj_mat_treeculler * view_mat).into_col_arrays()) + .as_::() + .inverted(); // PSCs: Potential shadow casters let ray_direction = scene_data.get_sun_dir(); @@ -1300,6 +1315,7 @@ impl Terrain { #[cfg(not(feature = "simd"))] return min.partial_cmple(&max).reduce_and(); }; + let (visible_light_volume, visible_psr_bounds) = if ray_direction.z < 0.0 && renderer.pipeline_modes().shadow.is_map() { @@ -1309,10 +1325,6 @@ impl Terrain { }; let focus_off = math::Vec3::from(focus_off); let visible_bounds_fine = visible_bounding_box.as_::(); - let inv_proj_view = - math::Mat4::from_col_arrays((proj_mat_treeculler * view_mat).into_col_arrays()) - .as_::() - .inverted(); let ray_direction = math::Vec3::::from(ray_direction); // NOTE: We use proj_mat_treeculler here because // calc_focused_light_volume_points makes the assumption that the @@ -1326,9 +1338,8 @@ impl Terrain { .map(|v| v.as_::()) .collect::>(); - let cam_pos = math::Vec4::from(view_mat.inverted() * Vec4::unit_w()).xyz(); let up: math::Vec3 = { math::Vec3::unit_y() }; - + let cam_pos = math::Vec3::from(cam_pos); let ray_mat = math::Mat4::look_at_rh(cam_pos, cam_pos + ray_direction, up); let visible_bounds = math::Aabr::from(math::fit_psr( ray_mat, @@ -1395,11 +1406,55 @@ impl Terrain { }) }; drop(guard); + span!(guard, "Rain occlusion magic"); + // Check if there is rain near the camera + let max_weather = scene_data + .state + .max_weather_near(focus_off.xy() + cam_pos.xy()); + let (visible_occlusion_volume, visible_por_bounds) = if max_weather.rain > RAIN_THRESHOLD { + let visible_bounding_box = math::Aabb:: { + min: math::Vec3::from(visible_bounding_box.min - focus_off), + max: math::Vec3::from(visible_bounding_box.max - focus_off), + }; + let visible_bounds_fine = math::Aabb { + min: visible_bounding_box.min.as_::(), + max: visible_bounding_box.max.as_::(), + }; + let weather = scene_data.state.weather_at(focus_off.xy() + cam_pos.xy()); + let ray_direction = math::Vec3::::from(weather.rain_vel().normalized()); + // NOTE: We use proj_mat_treeculler here because + // calc_focused_light_volume_points makes the assumption that the + // near plane lies before the far plane. + let visible_volume = math::calc_focused_light_volume_points( + inv_proj_view, + ray_direction.as_::(), + visible_bounds_fine, + 1e-6, + ) + .map(|v| v.as_::()) + .collect::>(); + let cam_pos = math::Vec3::from(cam_pos); + let ray_mat = + math::Mat4::look_at_rh(cam_pos, cam_pos + ray_direction, math::Vec3::unit_y()); + let visible_bounds = math::Aabr::from(math::fit_psr( + ray_mat, + visible_volume.iter().copied(), + |p| p, + )); + + (visible_volume, visible_bounds) + } else { + (Vec::new(), math::Aabr::default()) + }; + + drop(guard); ( visible_bounding_box, visible_light_volume, visible_psr_bounds, + visible_occlusion_volume, + visible_por_bounds, ) } @@ -1452,6 +1507,34 @@ impl Terrain { .for_each(|(model, locals)| drawer.draw(model, locals)); } + pub fn render_rain_occlusion<'a>( + &'a self, + drawer: &mut TerrainShadowDrawer<'_, 'a>, + focus_pos: Vec3, + ) { + span!(_guard, "render_occlusion", "Terrain::render_occlusion"); + let focus_chunk = Vec2::from(focus_pos).map2(TerrainChunk::RECT_SIZE, |e: f32, sz| { + (e as i32).div_euclid(sz as i32) + }); + let chunk_iter = Spiral2d::new() + .filter_map(|rpos| { + let pos = focus_chunk + rpos; + self.chunks.get(&pos) + }) + .take(self.chunks.len().min(RAIN_OCCLUSION_CHUNKS)); + + chunk_iter + // Find a way to keep this? + // .filter(|chunk| chunk.can_shadow_sun()) + .filter_map(|chunk| { + chunk + .opaque_model + .as_ref() + .map(|model| (model, &chunk.locals)) + }) + .for_each(|(model, locals)| drawer.draw(model, locals)); + } + pub fn chunks_for_point_shadows( &self, focus_pos: Vec3, diff --git a/voxygen/src/session/mod.rs b/voxygen/src/session/mod.rs index 7176bc8786..7a3ccf5add 100644 --- a/voxygen/src/session/mod.rs +++ b/voxygen/src/session/mod.rs @@ -175,6 +175,21 @@ impl SessionState { self.scene .maintain_debug_hitboxes(&client, &global_state.settings, &mut self.hitboxes); + // All this camera code is just to determine if it's underwater for the sfx + // filter + let camera = self.scene.camera_mut(); + camera.compute_dependents(&*client.state().terrain()); + let camera::Dependents { cam_pos, .. } = self.scene.camera().dependents(); + let focus_pos = self.scene.camera().get_focus_pos(); + let focus_off = focus_pos.map(|e| e.trunc()); + let cam_pos = cam_pos + focus_off; + let underwater = client + .state() + .terrain() + .get(cam_pos.map(|e| e.floor() as i32)) + .map(|b| b.is_liquid()) + .unwrap_or(false); + #[cfg(not(target_os = "macos"))] { // Update mumble positional audio @@ -245,7 +260,24 @@ impl SessionState { let sfx_triggers = self.scene.sfx_mgr.triggers.read(); let sfx_trigger_item = sfx_triggers.get_key_value(&SfxEvent::from(&inv_event)); - global_state.audio.emit_sfx_item(sfx_trigger_item); + + match inv_event { + InventoryUpdateEvent::Dropped + | InventoryUpdateEvent::Swapped + | InventoryUpdateEvent::Given + | InventoryUpdateEvent::Collected(_) + | InventoryUpdateEvent::EntityCollectFailed { .. } + | InventoryUpdateEvent::BlockCollectFailed { .. } + | InventoryUpdateEvent::Craft => { + global_state.audio.emit_ui_sfx(sfx_trigger_item, Some(1.0)); + }, + _ => global_state.audio.emit_sfx( + sfx_trigger_item, + client.position().unwrap_or_default(), + Some(1.0), + underwater, + ), + } match inv_event { InventoryUpdateEvent::BlockCollectFailed { pos, reason } => { @@ -358,6 +390,7 @@ impl PlayState for SessionState { let client = self.client.borrow(); (client.presence(), client.registered()) }; + if client_presence.is_some() { let camera = self.scene.camera_mut(); @@ -1172,7 +1205,11 @@ impl PlayState for SessionState { }, HudEvent::Logout => { self.client.borrow_mut().logout(); + // Stop all sounds + // TODO: Abstract this behavior to all instances of PlayStateResult::Pop + // somehow global_state.audio.stop_ambient_sounds(); + global_state.audio.stop_all_sfx(); return PlayStateResult::Pop; }, HudEvent::Quit => { @@ -1577,8 +1614,13 @@ impl PlayState for SessionState { // Process outcomes from client for outcome in outcomes { - self.scene - .handle_outcome(&outcome, &scene_data, &mut global_state.audio); + self.scene.handle_outcome( + &outcome, + &scene_data, + &mut global_state.audio, + client.state(), + cam_pos, + ); self.hud .handle_outcome(&outcome, scene_data.client, global_state); } diff --git a/voxygen/src/session/settings_change.rs b/voxygen/src/session/settings_change.rs index b56e3f81c6..825933532a 100644 --- a/voxygen/src/session/settings_change.rs +++ b/voxygen/src/session/settings_change.rs @@ -22,6 +22,7 @@ pub enum Audio { AdjustInactiveMasterVolume(f32), AdjustMusicVolume(f32), AdjustSfxVolume(f32), + AdjustAmbienceVolume(f32), //ChangeAudioDevice(String), ResetAudioSettings, } @@ -202,6 +203,11 @@ impl SettingsChange { settings.audio.sfx_volume = sfx_volume; }, + Audio::AdjustAmbienceVolume(ambience_volume) => { + global_state.audio.set_ambience_volume(ambience_volume); + + settings.audio.ambience_volume = ambience_volume; + }, //Audio::ChangeAudioDevice(name) => { // global_state.audio.set_device(name.clone()); diff --git a/voxygen/src/settings/audio.rs b/voxygen/src/settings/audio.rs index 7c2603fb4e..36f2230c17 100644 --- a/voxygen/src/settings/audio.rs +++ b/voxygen/src/settings/audio.rs @@ -25,7 +25,9 @@ pub struct AudioSettings { pub inactive_master_volume_perc: f32, pub music_volume: f32, pub sfx_volume: f32, + pub ambience_volume: f32, pub num_sfx_channels: usize, + pub num_ui_channels: usize, /// Audio Device that Voxygen will use to play audio. pub output: AudioOutput, @@ -36,9 +38,11 @@ impl Default for AudioSettings { Self { master_volume: 1.0, inactive_master_volume_perc: 0.5, - music_volume: 0.4, + music_volume: 0.3, sfx_volume: 0.6, + ambience_volume: 0.6, num_sfx_channels: 60, + num_ui_channels: 10, output: AudioOutput::Automatic, } } diff --git a/world/examples/turb.rs b/world/examples/turb.rs index 1eae349bc9..fe3d8924c5 100644 --- a/world/examples/turb.rs +++ b/world/examples/turb.rs @@ -1,4 +1,4 @@ -use noise::{Seedable, SuperSimplex}; +use noise::{NoiseFn, Seedable, SuperSimplex, Turbulence}; use vek::*; @@ -8,32 +8,40 @@ const H: usize = 640; fn main() { let mut win = minifb::Window::new("Turb", W, H, minifb::WindowOptions::default()).unwrap(); + let nz = Turbulence::new( + Turbulence::new(SuperSimplex::new()) + .set_frequency(0.2) + .set_power(1.5), + ) + .set_frequency(2.0) + .set_power(0.2); + let _nz_x = SuperSimplex::new().set_seed(0); let _nz_y = SuperSimplex::new().set_seed(1); let mut _time = 0.0f64; + let mut scale = 50.0; + while win.is_open() { let mut buf = vec![0; W * H]; for i in 0..W { for j in 0..H { - let pos = Vec2::new(i as f64 / W as f64, j as f64 / H as f64) * 0.5 - 0.25; + let pos = Vec2::new(i as f64, j as f64) / scale; - let pos = pos * 10.0; - - let pos = (0..10).fold(pos, |pos, _| pos.map(|e| e.powi(3) - 1.0)); - - let val = if pos.map(|e| e.abs() < 0.5).reduce_and() { - 1.0f32 - } else { - 0.0 - }; + let val = nz.get(pos.into_array()); buf[j * W + i] = u32::from_le_bytes([(val.max(0.0).min(1.0) * 255.0) as u8; 4]); } } + if win.is_key_pressed(minifb::Key::Right, minifb::KeyRepeat::No) { + scale *= 1.5; + } else if win.is_key_pressed(minifb::Key::Left, minifb::KeyRepeat::No) { + scale /= 1.5; + } + win.update_with_buffer(&buf, W, H).unwrap(); _time += 1.0 / 60.0;