mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge branch 'isse/weather' into 'master'
Weather See merge request veloren/veloren!3183
This commit is contained in:
commit
b79d1aae55
@ -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
|
||||
|
29
Cargo.lock
generated
29
Cargo.lock
generated
@ -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",
|
||||
|
BIN
assets/voxygen/audio/ambience/leaves.ogg
(Stored with Git LFS)
Normal file
BIN
assets/voxygen/audio/ambience/leaves.ogg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/voxygen/audio/ambience/rain.ogg
(Stored with Git LFS)
Normal file
BIN
assets/voxygen/audio/ambience/rain.ogg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/voxygen/audio/ambience/thunder.ogg
(Stored with Git LFS)
Normal file
BIN
assets/voxygen/audio/ambience/thunder.ogg
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -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,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
@ -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
|
||||
|
BIN
assets/voxygen/audio/sfx/ambient/bees_1.ogg
(Stored with Git LFS)
BIN
assets/voxygen/audio/sfx/ambient/bees_1.ogg
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/audio/sfx/ambient/birdcall_1.ogg
(Stored with Git LFS)
BIN
assets/voxygen/audio/sfx/ambient/birdcall_1.ogg
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/audio/sfx/ambient/birdcall_2.ogg
(Stored with Git LFS)
BIN
assets/voxygen/audio/sfx/ambient/birdcall_2.ogg
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/audio/sfx/ambient/frog_croak_1.ogg
(Stored with Git LFS)
BIN
assets/voxygen/audio/sfx/ambient/frog_croak_1.ogg
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/audio/sfx/character/dive_roll_1.ogg
(Stored with Git LFS)
BIN
assets/voxygen/audio/sfx/character/dive_roll_1.ogg
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/audio/sfx/character/dive_roll_2.ogg
(Stored with Git LFS)
BIN
assets/voxygen/audio/sfx/character/dive_roll_2.ogg
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/audio/sfx/crafting/hammer.ogg
(Stored with Git LFS)
BIN
assets/voxygen/audio/sfx/crafting/hammer.ogg
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/audio/sfx/footsteps/snow_step_1.ogg
(Stored with Git LFS)
BIN
assets/voxygen/audio/sfx/footsteps/snow_step_1.ogg
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/audio/sfx/footsteps/snow_step_2.ogg
(Stored with Git LFS)
BIN
assets/voxygen/audio/sfx/footsteps/snow_step_2.ogg
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/audio/sfx/footsteps/snow_step_3.ogg
(Stored with Git LFS)
BIN
assets/voxygen/audio/sfx/footsteps/snow_step_3.ogg
(Stored with Git LFS)
Binary file not shown.
@ -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: [
|
||||
|
BIN
assets/voxygen/audio/soundtrack/overworld/a_heros_sorrow.ogg
(Stored with Git LFS)
Normal file
BIN
assets/voxygen/audio/soundtrack/overworld/a_heros_sorrow.ogg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/voxygen/audio/soundtrack/overworld/the_heavens_weep.ogg
(Stored with Git LFS)
Normal file
BIN
assets/voxygen/audio/soundtrack/overworld/the_heavens_weep.ogg
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -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",
|
||||
|
||||
|
@ -28,31 +28,29 @@
|
||||
// This *MUST* come after `cloud.glsl`: it contains a function that depends on `cloud.glsl` when clouds are enabled
|
||||
#include <point_glow.glsl>
|
||||
|
||||
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);
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -5,19 +5,11 @@
|
||||
#include <sky.glsl>
|
||||
#include <srgb.glsl>
|
||||
|
||||
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;
|
||||
|
||||
|
31
assets/voxygen/shaders/include/rain_occlusion.glsl
Normal file
31
assets/voxygen/shaders/include/rain_occlusion.glsl
Normal file
@ -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
|
@ -5,6 +5,7 @@
|
||||
#include <srgb.glsl>
|
||||
#include <shadows.glsl>
|
||||
#include <globals.glsl>
|
||||
#include <rain_occlusion.glsl>
|
||||
|
||||
// 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) {
|
||||
|
@ -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;
|
||||
|
65
assets/voxygen/shaders/rain-occlusion-directed-vert.glsl
Normal file
65
assets/voxygen/shaders/rain-occlusion-directed-vert.glsl
Normal file
@ -0,0 +1,65 @@
|
||||
#version 420 core
|
||||
// #extension ARB_texture_storage : enable
|
||||
|
||||
#include <constants.glsl>
|
||||
|
||||
#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 <globals.glsl>
|
||||
// For shadow locals.
|
||||
// #include <shadows.glsl>
|
||||
|
||||
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);
|
||||
}
|
86
assets/voxygen/shaders/rain-occlusion-figure-vert.glsl
Normal file
86
assets/voxygen/shaders/rain-occlusion-figure-vert.glsl
Normal file
@ -0,0 +1,86 @@
|
||||
#version 420 core
|
||||
// #extension ARB_texture_storage : enable
|
||||
|
||||
#define FIGURE_SHADER
|
||||
|
||||
#include <constants.glsl>
|
||||
|
||||
#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 <globals.glsl>
|
||||
// For shadow locals.
|
||||
// #include <shadows.glsl>
|
||||
|
||||
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);
|
||||
}
|
@ -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),
|
||||
|
@ -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);
|
||||
|
@ -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<EconomyInfo>,
|
||||
}
|
||||
|
||||
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<PresenceKind>,
|
||||
runtime: Arc<Runtime>,
|
||||
server_info: ServerInfo,
|
||||
world_data: WorldData,
|
||||
weather: WeatherLerp,
|
||||
player_list: HashMap<Uid, PlayerInfo>,
|
||||
character_list: CharacterList,
|
||||
sites: HashMap<SiteId, SiteInfoRich>,
|
||||
@ -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<Arc<TerrainChunk>> {
|
||||
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(())
|
||||
|
@ -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
|
||||
|
@ -125,6 +125,13 @@ lazy_static! {
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
|
||||
static ref WEATHERS: Vec<String> = vec![
|
||||
"clear", "cloudy", "rain", "wind", "storm"
|
||||
]
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
|
||||
pub static ref BUFF_PARSER: HashMap<String, BuffKind> = {
|
||||
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",
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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<f32>,
|
||||
},
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
161
common/src/weather.rs
Normal file
161
common/src/weather.rs
Normal file
@ -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<f32>,
|
||||
}
|
||||
|
||||
impl Weather {
|
||||
pub fn new(cloud: f32, rain: f32, wind: Vec2<f32>) -> 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::<f32>::lerp_unclamped(from.wind, to.wind, t),
|
||||
}
|
||||
}
|
||||
|
||||
// Get the rain velocity for this weather
|
||||
pub fn rain_vel(&self) -> Vec3<f32> {
|
||||
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<Weather>,
|
||||
}
|
||||
|
||||
/// 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<f32>) -> Vec2<f32> { wpos / CELL_SIZE as f32 - 0.5 }
|
||||
|
||||
// TODO: Move consts from world to common to avoid duplication
|
||||
const LOCALITY: [Vec2<i32>; 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<u32>) -> 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<Item = (Vec2<i32>, &Weather)> { self.weather.iter() }
|
||||
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = (Vec2<i32>, &mut Weather)> {
|
||||
self.weather.iter_mut()
|
||||
}
|
||||
|
||||
pub fn size(&self) -> Vec2<u32> { 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<f32>) -> 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_::<i32>();
|
||||
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<f32>) -> Weather {
|
||||
let cell_pos: Vec2<i32> = 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()
|
||||
}
|
||||
}
|
@ -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<TerrainChanges> { self.ecs.read_resource() }
|
||||
|
||||
/// Get a reference the current in-game weather grid.
|
||||
pub fn weather_grid(&self) -> Fetch<WeatherGrid> { self.ecs.read_resource() }
|
||||
|
||||
/// Get a mutable reference the current in-game weather grid.
|
||||
pub fn weather_grid_mut(&mut self) -> FetchMut<WeatherGrid> { self.ecs.write_resource() }
|
||||
|
||||
/// Get the current weather at a position in worldspace.
|
||||
pub fn weather_at(&self, pos: Vec2<f32>) -> Weather {
|
||||
self.weather_grid().get_interpolated(pos)
|
||||
}
|
||||
|
||||
/// Get the max weather near a position in worldspace.
|
||||
pub fn max_weather_near(&self, pos: Vec2<f32>) -> 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::<TimeOfDay>().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() }
|
||||
|
||||
|
@ -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"] }
|
||||
|
@ -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
|
||||
|
@ -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<String>,
|
||||
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::<WeatherSim>()
|
||||
.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())
|
||||
}
|
||||
}
|
||||
|
@ -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::<Agent>().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<Outcome>,
|
||||
) {
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -186,16 +186,13 @@ pub fn handle_mine_block(
|
||||
) {
|
||||
let skill_group = SkillGroupKind::Weapon(tool);
|
||||
let outcome_bus = state.ecs().read_resource::<EventBus<Outcome>>();
|
||||
let positions = state.ecs().read_component::<comp::Pos>();
|
||||
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 {
|
||||
|
@ -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,
|
||||
);
|
||||
|
40
server/src/weather/mod.rs
Normal file
40
server/src/weather/mod.rs
Normal file
@ -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::<tick::Sys>(dispatch_builder, &[]);
|
||||
dispatch::<sync::Sys>(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::<tick::Sys>::every(Duration::from_secs_f32(
|
||||
WEATHER_DT,
|
||||
)));
|
||||
state
|
||||
.ecs_mut()
|
||||
.insert(SysScheduler::<sync::Sys>::every(Duration::from_secs_f32(
|
||||
WEATHER_DT,
|
||||
)));
|
||||
}
|
131
server/src/weather/sim.rs
Normal file
131
server/src/weather/sim.rs
Normal file
@ -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<i32>) -> Vec2<i32> { 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<u32>,
|
||||
consts: Grid<CellConsts>,
|
||||
zones: Grid<Option<WeatherZone>>,
|
||||
}
|
||||
|
||||
impl WeatherSim {
|
||||
pub fn new(size: Vec2<u32>, 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::<Vec<_>>(),
|
||||
),
|
||||
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<f32>, radius: f32, time: f32) {
|
||||
let min: Vec2<i32> = (pos - radius).as_::<i32>().map(|e| e.max(0));
|
||||
let max: Vec2<i32> = (pos + radius)
|
||||
.ceil()
|
||||
.as_::<i32>()
|
||||
.map2(self.size.as_::<i32>(), |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_::<f32>();
|
||||
|
||||
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_::<f64>() + 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<u32> { self.size }
|
||||
}
|
37
server/src/weather/sync.rs
Normal file
37
server/src/weather/sync.rs
Normal file
@ -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<Self>>,
|
||||
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<Self>,
|
||||
(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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
35
server/src/weather/tick.rs
Normal file
35
server/src/weather/tick.rs
Normal file
@ -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<Self>>,
|
||||
);
|
||||
|
||||
const NAME: &'static str = "weather::tick";
|
||||
const ORIGIN: Origin = Origin::Server;
|
||||
const PHASE: Phase = Phase::Create;
|
||||
|
||||
fn run(
|
||||
_job: &mut common_ecs::Job<Self>,
|
||||
(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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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"}
|
||||
|
@ -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<AmbientItem>,
|
||||
}
|
||||
|
||||
/// 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<AmbientCollection>,
|
||||
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<AmbientCollection>,
|
||||
}
|
||||
|
||||
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> {
|
||||
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> {
|
||||
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 {
|
||||
|
@ -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<f32>) { 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<S>(&mut self, source: S)
|
||||
where
|
||||
S: Source + Send + 'static,
|
||||
S::Item: Sample,
|
||||
S::Item: Send,
|
||||
<S as std::iter::Iterator>::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() }
|
||||
}
|
||||
|
@ -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<MusicChannel>,
|
||||
ambient_channels: Vec<AmbientChannel>,
|
||||
sfx_channels: Vec<SfxChannel>,
|
||||
ui_channels: Vec<UiChannel>,
|
||||
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::<usize>() % 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<f32>,
|
||||
vol: Option<f32>,
|
||||
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<f32>,
|
||||
) {
|
||||
// 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::<usize>() % 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<usize> {
|
||||
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) {
|
||||
|
@ -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<DayPeriod>,
|
||||
/// Whether this track should play during a certain weather
|
||||
weather: Option<WeatherKind>,
|
||||
/// 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<DayPeriod>,
|
||||
weather: Option<WeatherKind>,
|
||||
biomes: Vec<(BiomeKind, u8)>,
|
||||
site: Option<SitesKind>,
|
||||
segments: Vec<(String, f32, MusicState, Option<MusicActivity>)>,
|
||||
@ -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<SoundtrackItem> {
|
||||
RawSoundtrackItem::Segmented {
|
||||
title,
|
||||
timing,
|
||||
weather,
|
||||
biomes,
|
||||
site,
|
||||
segments,
|
||||
@ -467,6 +481,7 @@ impl assets::Compound for SoundtrackCollection<SoundtrackItem> {
|
||||
path,
|
||||
length,
|
||||
timing: timing.clone(),
|
||||
weather,
|
||||
biomes: biomes.clone(),
|
||||
site,
|
||||
music_state,
|
||||
|
@ -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::<Uid>();
|
||||
|
||||
// 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 { .. }
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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"]
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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]),
|
||||
};
|
||||
|
||||
|
@ -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<ExperimentalShader>,
|
||||
@ -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,
|
||||
}
|
||||
|
@ -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<f32>, view_mat_inv: Mat4<f32>) -> 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 {
|
||||
|
@ -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<u32>,
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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::<Globals>() % 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<Light>,
|
||||
pub shadows: Consts<Shadow>,
|
||||
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),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
231
voxygen/src/render/pipelines/rain_occlusion.rs
Normal file
231
voxygen/src/render/pipelines/rain_occlusion.rs
Normal file
@ -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::<Locals>() % 16 == 0);
|
||||
|
||||
impl Locals {
|
||||
pub fn new(
|
||||
rain_occlusion_matrices: Mat4<f32>,
|
||||
rain_occlusion_texture_mat: Mat4<f32>,
|
||||
rain_dir_mat: Mat4<f32>,
|
||||
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<Consts<Locals>>;
|
||||
|
||||
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<Locals>) -> 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,
|
||||
}
|
||||
}
|
||||
}
|
@ -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(),
|
||||
},
|
||||
]);
|
||||
|
@ -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<Texture>,
|
||||
// In progress creation of the remaining pipelines in the background
|
||||
creating: PipelineCreation<IngameAndShadowPipelines>,
|
||||
},
|
||||
@ -117,6 +123,7 @@ enum State {
|
||||
(
|
||||
Pipelines,
|
||||
ShadowPipelines,
|
||||
RainOcclusionPipelines,
|
||||
Arc<postprocess::PostProcessLayout>,
|
||||
),
|
||||
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
|
||||
|
@ -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<figure::Locals> {
|
||||
self.layouts.global.bind_col_light(&self.device, col_light)
|
||||
}
|
||||
|
@ -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<RainOcclusionPassDrawer> {
|
||||
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<ShadowPassDrawer> {
|
||||
@ -216,6 +257,8 @@ impl<'frame> Drawer<'frame> {
|
||||
/// Returns None if the clouds pipeline is not available
|
||||
pub fn second_pass(&mut self) -> Option<SecondPassDrawer> {
|
||||
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::<terrain::Vertex>(&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::<terrain::Vertex>(&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::<terrain::Vertex>(&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::<terrain::Vertex>(&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);
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
use crate::render::pipelines::rain_occlusion;
|
||||
|
||||
use super::{
|
||||
super::{
|
||||
pipelines::{
|
||||
@ -60,9 +62,16 @@ pub struct ShadowPipelines {
|
||||
pub figure: Option<shadow::ShadowFigurePipeline>,
|
||||
}
|
||||
|
||||
pub struct RainOcclusionPipelines {
|
||||
pub terrain: Option<rain_occlusion::RainOcclusionPipeline>,
|
||||
pub figure: Option<rain_occlusion::RainOcclusionFigurePipeline>,
|
||||
}
|
||||
|
||||
// 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<postprocess::PostProcessLayout>,
|
||||
),
|
||||
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");
|
||||
|
227
voxygen/src/render/renderer/rain_occlusion_map.rs
Normal file
227
voxygen/src/render/renderer/rain_occlusion_map.rs
Normal file
@ -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<rain_occlusion::RainOcclusionPipeline>,
|
||||
figure: Option<rain_occlusion::RainOcclusionFigurePipeline>,
|
||||
view: Option<Texture>,
|
||||
) -> 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<Texture, RenderError> {
|
||||
// (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,
|
||||
}
|
||||
}
|
||||
}
|
@ -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",
|
||||
|
@ -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<f32>,
|
||||
visible_por_bounds: math::Aabr<f32>,
|
||||
camera: &Camera,
|
||||
terrain: Option<&Terrain>,
|
||||
) -> anim::vek::Aabb<f32> {
|
||||
@ -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<f32> =
|
||||
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<f32>, b: math::Aabr<f32>| {
|
||||
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<f32>,), radius: f32| {
|
||||
// Short circuit when there are no shadows to cast.
|
||||
if !can_shadow_sun {
|
||||
return false;
|
||||
|
||||
let can_shadow = |ray_direction: Vec3<f32>,
|
||||
enabled: bool,
|
||||
visible_bounds: math::Aabr<f32>| {
|
||||
let ray_direction = math::Vec3::from(ray_direction);
|
||||
// Transform (semi) world space to light space.
|
||||
let ray_mat: math::Mat4<f32> =
|
||||
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<f32>,), 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::<Item>();
|
||||
|
||||
(
|
||||
&ecs.entities(),
|
||||
&ecs.read_storage::<Pos>(),
|
||||
@ -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<f32>,
|
||||
lpindex: u8,
|
||||
can_shadow_sun: bool,
|
||||
can_occlude_rain: bool,
|
||||
visible: bool,
|
||||
last_pos: Option<anim::vek::Vec3<f32>>,
|
||||
avg_vel: anim::vek::Vec3<f32>,
|
||||
@ -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<S> {
|
||||
@ -6311,6 +6370,7 @@ impl<S: Skeleton> FigureState<S> {
|
||||
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,
|
||||
|
@ -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_::<u32>();
|
||||
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::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn render<'a>(&'a self, drawer: &mut FirstPassDrawer<'a>) {
|
||||
|
@ -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<f32>,
|
||||
) {
|
||||
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::<DeltaTime>().0;
|
||||
|
||||
let player_pos = ecs
|
||||
.read_storage::<comp::Pos>()
|
||||
.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::<DeltaTime>().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<f32> = 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::<f32>::scaling_3d::<Vec3<f32>>(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<f32>,
|
||||
d_dir: math::Vec3<f32>,
|
||||
volume: &Vec<math::Vec3<f32>>|
|
||||
-> (Mat4<f32>, Mat4<f32>) {
|
||||
// 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<f32> = 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<f32> = 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<f32> = 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<f32> = w_v * light_all_mat;
|
||||
let w_p: math::Mat4<f32> = {
|
||||
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<f32> = 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::<f32> {
|
||||
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<f32> = 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::<f32>::scaling_3d::<Vec3<f32>>(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<f32> = 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<f32> = 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<f32> = 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<f32> = 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<f32> = w_v * light_all_mat;
|
||||
let w_p: math::Mat4<f32> = {
|
||||
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<f32> = 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::<f32> {
|
||||
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<f32> =
|
||||
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() {
|
||||
|
@ -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);
|
||||
|
@ -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<f32> = 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<V: RectRasterableVol> Terrain<V> {
|
||||
focus_pos: Vec3<f32>,
|
||||
loaded_distance: f32,
|
||||
camera: &Camera,
|
||||
) -> (Aabb<f32>, Vec<math::Vec3<f32>>, math::Aabr<f32>) {
|
||||
) -> (
|
||||
Aabb<f32>,
|
||||
Vec<math::Vec3<f32>>,
|
||||
math::Aabr<f32>,
|
||||
Vec<math::Vec3<f32>>,
|
||||
math::Aabr<f32>,
|
||||
) {
|
||||
let camera::Dependents {
|
||||
view_mat,
|
||||
proj_mat_treeculler,
|
||||
cam_pos,
|
||||
..
|
||||
} = camera.dependents();
|
||||
|
||||
@ -1289,6 +1300,10 @@ impl<V: RectRasterableVol> Terrain<V> {
|
||||
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_::<f64>()
|
||||
.inverted();
|
||||
|
||||
// PSCs: Potential shadow casters
|
||||
let ray_direction = scene_data.get_sun_dir();
|
||||
@ -1300,6 +1315,7 @@ impl<V: RectRasterableVol> Terrain<V> {
|
||||
#[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<V: RectRasterableVol> Terrain<V> {
|
||||
};
|
||||
let focus_off = math::Vec3::from(focus_off);
|
||||
let visible_bounds_fine = visible_bounding_box.as_::<f64>();
|
||||
let inv_proj_view =
|
||||
math::Mat4::from_col_arrays((proj_mat_treeculler * view_mat).into_col_arrays())
|
||||
.as_::<f64>()
|
||||
.inverted();
|
||||
let ray_direction = math::Vec3::<f32>::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<V: RectRasterableVol> Terrain<V> {
|
||||
.map(|v| v.as_::<f32>())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let cam_pos = math::Vec4::from(view_mat.inverted() * Vec4::unit_w()).xyz();
|
||||
let up: math::Vec3<f32> = { 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<V: RectRasterableVol> Terrain<V> {
|
||||
})
|
||||
};
|
||||
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::<f32> {
|
||||
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_::<f64>(),
|
||||
max: visible_bounding_box.max.as_::<f64>(),
|
||||
};
|
||||
let weather = scene_data.state.weather_at(focus_off.xy() + cam_pos.xy());
|
||||
let ray_direction = math::Vec3::<f32>::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_::<f64>(),
|
||||
visible_bounds_fine,
|
||||
1e-6,
|
||||
)
|
||||
.map(|v| v.as_::<f32>())
|
||||
.collect::<Vec<_>>();
|
||||
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<V: RectRasterableVol> Terrain<V> {
|
||||
.for_each(|(model, locals)| drawer.draw(model, locals));
|
||||
}
|
||||
|
||||
pub fn render_rain_occlusion<'a>(
|
||||
&'a self,
|
||||
drawer: &mut TerrainShadowDrawer<'_, 'a>,
|
||||
focus_pos: Vec3<f32>,
|
||||
) {
|
||||
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<f32>,
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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());
|
||||
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user