Merge branch 'isse/weather' into 'master'

Weather

See merge request veloren/veloren!3183
This commit is contained in:
Joshua Yanovski 2022-07-04 19:30:43 +00:00
commit b79d1aae55
83 changed files with 3270 additions and 750 deletions

View File

@ -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
View File

@ -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

Binary file not shown.

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

Binary file not shown.

View File

@ -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,
),
]
)

View File

@ -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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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

Binary file not shown.

Binary file not shown.

View File

@ -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",

View File

@ -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);

View File

@ -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);

View File

@ -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
}

View File

@ -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;

View 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

View File

@ -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) {

View File

@ -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;

View 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);
}

View 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);
}

View File

@ -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),

View File

@ -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);

View File

@ -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(())

View File

@ -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

View File

@ -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",
}
}

View File

@ -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;

View File

@ -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
View 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()
}
}

View File

@ -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() }

View File

@ -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"] }

View File

@ -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

View File

@ -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())
}
}

View File

@ -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,
});
}
}

View File

@ -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 {

View File

@ -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
View 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
View 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 }
}

View 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));
}
}
}
}

View 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);
}
}
}

View File

@ -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"}

View File

@ -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 {

View File

@ -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() }
}

View File

@ -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) {

View File

@ -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 == &current_site,
None => true,
} && match &track.weather {
Some(weather) => weather == &current_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,

View File

@ -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 { .. }

View File

@ -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)

View File

@ -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)

View File

@ -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);

View File

@ -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"]

View File

@ -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);

View File

@ -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]),
};

View File

@ -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,
}

View File

@ -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 {

View File

@ -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,
}
}
}

View File

@ -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),
},
],
});

View 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,
}
}
}

View File

@ -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(),
},
]);

View File

@ -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

View File

@ -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)
}

View File

@ -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);
}

View File

@ -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");

View 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,
}
}
}

View File

@ -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",

View File

@ -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,

View File

@ -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>) {

View File

@ -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() {

View File

@ -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);

View File

@ -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>,

View File

@ -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);
}

View File

@ -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());

View File

@ -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,
}
}

View File

@ -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;