mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-09 10:39:46 -10:00
Compare commits
102
Commits
No files matched your search
@@ -25,8 +25,10 @@ Feature → files → harness for the most-changed areas: [docs/architecture.md]
|
||||
first line `> 🤖 Opened by Ludovic's AI agent (Claude), not by Ludovic himself.`, commits
|
||||
`Co-Authored-By: Claude <model> <[email protected]>`, last line
|
||||
`🤖 Generated with [Claude Code](https://claude.com/claude-code)`.
|
||||
- **Layers** ([docs/layers.md](docs/layers.md)): engine < platform < Horizon; a crate depends only on its own layer or a
|
||||
lower one (`tools/check_layers.py`, crate layers in `tools/layers.json`). New Horizon code goes behind `horizon-extras`.
|
||||
- **Layers** ([docs/layers.md](docs/layers.md)): engine uses engine; platform uses engine and platform; Horizon (its extras,
|
||||
cosmetics, shop, telemetry, launcher) uses platform and Horizon only, never the engine: it moves to its own repository
|
||||
later. `tools/check_layers.py` checks it. New Horizon code goes behind `horizon-extras`; built-in addresses go in
|
||||
`horizon-addresses`.
|
||||
- Everything players run is Rust. Python/Java only for dev tools and harness drivers in `tools/`.
|
||||
- Horizon-only features follow [docs/design-system.md](docs/design-system.md) and are off by default where
|
||||
they would change the vanilla look.
|
||||
@@ -39,6 +41,8 @@ Feature → files → harness for the most-changed areas: [docs/architecture.md]
|
||||
issue is taken, and so is one labelled `in progress` (claimed on Discord): pick another. `/unclaim` when you stop;
|
||||
a claim with no open PR linking it is released after 5 days. The issues free to claim, without reading threads:
|
||||
`gh issue list --search 'is:open no:assignee -label:"in progress" -label:tracker -label:proposal -label:"not claimable" -linked:pr'`
|
||||
- **Machine translations stay unreviewed.** `tools/lang_review/` records only text a person who speaks the language
|
||||
wrote or checked; never `lang_coverage.py --mark-reviewed` machine text ([docs/translations.md](docs/translations.md)).
|
||||
- **Controllers reach what registers.** Draw controls with a registering helper (`ui::button`, `settings_shell::foot_button`); `graphics_style::button` and `screen_draw::button` only paint, and registering one part of a screen ends its crosshair, so register all of it.
|
||||
|
||||
## Crates
|
||||
|
||||
+15
-4
@@ -45,9 +45,13 @@ and compares their outputs.
|
||||
[The parity workflow](docs/parity-workflow.md) shows how harnesses run; copy a nearby one.
|
||||
- **Horizon's own features** (the launcher, the new menus) follow [the design system](docs/design-system.md) and are
|
||||
off by default wherever they would change the vanilla look.
|
||||
- **Three layers: engine, platform, Horizon.** Vanilla code never depends on the platform (mods, shader packs,
|
||||
Horizon-only settings) or on Horizon (cosmetics, the shop, official servers, telemetry), and the platform never
|
||||
depends on Horizon. [docs/layers.md](docs/layers.md) says where a new feature goes; CI checks the dependencies.
|
||||
- **Three layers: engine, platform, Horizon.**
|
||||
- Vanilla code never depends on the platform (mods, shader packs, HDR, the settings system) or on Horizon
|
||||
(its extras, cosmetics, the shop, official servers, telemetry).
|
||||
- The platform never depends on Horizon.
|
||||
- Horizon uses only the platform, never the engine: it will live in its own repository.
|
||||
|
||||
[docs/layers.md](docs/layers.md) says where a new feature goes; CI checks the dependencies.
|
||||
- **Everything players run is Rust.** Python and Java are fine for dev tools and harness drivers in `tools/`, never
|
||||
for player-facing code.
|
||||
- **No game files in the repo.** Never commit game files, decompiled source or prepared resources (`.local/`):
|
||||
@@ -93,7 +97,8 @@ names its sources: the sections of that specification, the documentation pages a
|
||||
that downloads NVIDIA's files itself, on the player's machine. No NVIDIA binaries, headers or SDK files in the
|
||||
repository.
|
||||
- **The JVM bridge** (running Java mods) is only ever a separate, optional download, never installed or on by
|
||||
default. The game may offer to turn it on when it finds a Java mod; the player decides.
|
||||
default. The game may offer to turn it on when it finds a Java mod; the player decides. It is promoted out of beta
|
||||
only after MSPT and RAM benchmarks against the Java client with the same mods ([#13914](https://github.com/horizon-rust/horizon/issues/13914)).
|
||||
|
||||
## Before asking for review
|
||||
|
||||
@@ -113,6 +118,12 @@ including examples. The dependency scope includes optional, platform, build and
|
||||
library, binary, integration and doc tests without building unrelated example executables. Shared Cargo
|
||||
configuration and toolchain changes select the whole workspace; CI also keeps its workspace-wide checks.
|
||||
|
||||
### Translations
|
||||
|
||||
Most of Horizon's translations were written by machine and stay marked unreviewed until someone who speaks the
|
||||
language checks them. To report a wrong string or review your language, see [docs/translations.md](docs/translations.md)
|
||||
and #translations on Horizon's Discord. Never mark machine-written text as reviewed.
|
||||
|
||||
## Claim before you start
|
||||
|
||||
Work is tracked in GitHub issues, and an issue that is assigned is taken (so is one with the `in progress` label:
|
||||
|
||||
Generated
+11
-1
@@ -1593,6 +1593,7 @@ dependencies = [
|
||||
"horizon-optifine",
|
||||
"horizon-polytone",
|
||||
"horizon-protocol",
|
||||
"horizon-render-pipeline",
|
||||
"horizon-shader-api",
|
||||
"horizon-shaders",
|
||||
"horizon-system",
|
||||
@@ -1852,6 +1853,14 @@ dependencies = [
|
||||
"ring",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "horizon-render-pipeline"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"naga",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "horizon-server"
|
||||
version = "0.1.0"
|
||||
@@ -1900,7 +1909,7 @@ dependencies = [
|
||||
name = "horizon-shader-api"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"horizon-render-pipeline",
|
||||
"naga",
|
||||
"png",
|
||||
"serde",
|
||||
@@ -1916,6 +1925,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"half",
|
||||
"horizon-core",
|
||||
"horizon-render-pipeline",
|
||||
"naga",
|
||||
"png",
|
||||
"zip",
|
||||
|
||||
@@ -39,6 +39,7 @@ members = [
|
||||
"crates/cargo-horizon-mod",
|
||||
"crates/horizon-port-pack",
|
||||
"crates/horizon-shaders",
|
||||
"crates/horizon-render-pipeline",
|
||||
"crates/horizon-optifine",
|
||||
"crates/horizon-polytone",
|
||||
"crates/horizon-shader-api",
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
//! The `MobEffects` and `Potions` registries (code in vanilla, ported as
|
||||
//! tables): each effect's colour and category, each potion's effects, and the
|
||||
//! potion colour mix. horizon-client re-exports them from `brewing`.
|
||||
use std::{collections::HashMap, sync::OnceLock};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{OnceLock, RwLock},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Category {
|
||||
@@ -18,6 +21,22 @@ pub struct EffectDef {
|
||||
pub category: Category,
|
||||
pub instant: bool,
|
||||
}
|
||||
/// The colours resource packs give effects (Polytone's `effect.<id>`), by the table's id.
|
||||
static PACK_COLORS: RwLock<Option<HashMap<String, u32>>> = RwLock::new(None);
|
||||
/// Takes the packs' effect colours (none: vanilla's).
|
||||
pub fn set_pack_colors(colors: Vec<(String, u32)>) {
|
||||
let colors = (!colors.is_empty()).then(|| colors.into_iter().collect());
|
||||
*PACK_COLORS.write().unwrap_or_else(|e| e.into_inner()) = colors;
|
||||
}
|
||||
impl EffectDef {
|
||||
/// `MobEffect.getColor` as the packs show it: potions, tipped arrows and effect particles.
|
||||
pub fn shown_color(&self) -> u32 {
|
||||
let pack = PACK_COLORS.read().unwrap_or_else(|e| e.into_inner());
|
||||
pack.as_ref()
|
||||
.and_then(|colors| colors.get(self.id))
|
||||
.map_or(self.color, |c| c & 0xFF_FFFF)
|
||||
}
|
||||
}
|
||||
const fn fx(id: &'static str, color: u32, category: Category) -> EffectDef {
|
||||
EffectDef {
|
||||
id,
|
||||
@@ -309,9 +328,10 @@ pub fn effects_color<'a>(effects: impl Iterator<Item = (&'a str, i32)>) -> u32 {
|
||||
for (e, amp) in effects {
|
||||
let Some(e) = effect(e) else { continue };
|
||||
let w = amp as u32 + 1;
|
||||
r += w * (e.color >> 16 & 255);
|
||||
g += w * (e.color >> 8 & 255);
|
||||
b += w * (e.color & 255);
|
||||
let color = e.shown_color();
|
||||
r += w * (color >> 16 & 255);
|
||||
g += w * (color >> 8 & 255);
|
||||
b += w * (color & 255);
|
||||
total += w;
|
||||
}
|
||||
match (
|
||||
|
||||
@@ -24,6 +24,7 @@ horizon-narrator = { path = "../horizon-narrator" }
|
||||
horizon-optifine = { path = "../horizon-optifine" }
|
||||
horizon-polytone = { path = "../horizon-polytone" }
|
||||
horizon-protocol = { path = "../horizon-protocol" }
|
||||
horizon-render-pipeline = { path = "../horizon-render-pipeline" }
|
||||
horizon-shader-api = { path = "../horizon-shader-api" }
|
||||
horizon-shaders = { path = "../horizon-shaders" }
|
||||
horizon-system = { path = "../horizon-system" }
|
||||
|
||||
@@ -575,14 +575,14 @@ fn classify(name: &str, prop: &dyn Fn(&str) -> Option<String>) -> Kind {
|
||||
"banner" => {
|
||||
let segment: f32 = prop("rotation").and_then(|r| r.parse().ok()).unwrap_or(0.0);
|
||||
return Kind::Banner {
|
||||
color: crate::pack_dyes::diffuse(dye),
|
||||
color: crate::pack_palette::diffuse(dye),
|
||||
wall: false,
|
||||
angle: segment * 22.5,
|
||||
};
|
||||
}
|
||||
"wall_banner" => {
|
||||
return Kind::Banner {
|
||||
color: crate::pack_dyes::diffuse(dye),
|
||||
color: crate::pack_palette::diffuse(dye),
|
||||
wall: true,
|
||||
angle: y_rot(facing()),
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ use std::{collections::HashSet, path::Path};
|
||||
pub use horizon_client_core::potions::{
|
||||
BASE_POTION_COLOR, Category, EFFECTS, EffectDef, POTIONS, PotionDef, all_effects,
|
||||
description_id, effect, is_mod_effect, mod_effects, namespaced, potion, potion_color,
|
||||
register_mod_effects,
|
||||
register_mod_effects, set_pack_colors,
|
||||
};
|
||||
/// `MobEffectUtil.formatDuration` at 20 TPS.
|
||||
pub fn format_duration(ticks: i32) -> String {
|
||||
|
||||
@@ -110,10 +110,15 @@ impl FogState {
|
||||
}
|
||||
/// `LocalPlayer.getWaterVision`.
|
||||
pub fn water_vision(&self, eye_in_water: bool) -> f32 {
|
||||
self.water_vision_over(eye_in_water, 600.0)
|
||||
}
|
||||
/// The water vision a pack's `water_fog_brightening_time` gives: vanilla's ramp over that
|
||||
/// many ticks, of the 600 the game counts (more never reaches full).
|
||||
pub fn water_vision_over(&self, eye_in_water: bool, ticks: f32) -> f32 {
|
||||
if !eye_in_water {
|
||||
return 0.0;
|
||||
}
|
||||
let t = self.water_vision_time as f32;
|
||||
let t = self.water_vision_time as f32 * 600.0 / ticks;
|
||||
if t >= 600.0 {
|
||||
return 1.0;
|
||||
}
|
||||
@@ -226,6 +231,8 @@ pub struct FogInputs<'a> {
|
||||
/// `GameRenderer.nightVisionScale` while Night Vision is on without
|
||||
/// Darkness, else 0 (`FogRenderer.computeFogColor`'s brightenFactor).
|
||||
pub night_vision: f32,
|
||||
/// The packs' Polytone `environment` fog settings.
|
||||
pub environment: horizon_polytone::colors::Environment,
|
||||
}
|
||||
|
||||
/// `FogRenderer.setupFog` + `computeFogColor`.
|
||||
@@ -241,7 +248,7 @@ pub fn setup_fog(tables: &FogTables, state: &FogState, input: &FogInputs) -> Fog
|
||||
// Base colour of the applicable environment.
|
||||
let mut color = match input.fog_type {
|
||||
FogType::Lava => rgb(0x99_1A00),
|
||||
FogType::PowderSnow => rgb(0x9F_BACC),
|
||||
FogType::PowderSnow => rgb(input.environment.powder_snow_fog.unwrap_or(0x9F_BACC)),
|
||||
FogType::Water => input.water_fog_color,
|
||||
FogType::Atmospheric => {
|
||||
let mut fog = sky.fog_color;
|
||||
@@ -299,8 +306,11 @@ pub fn setup_fog(tables: &FogTables, state: &FogState, input: &FogInputs) -> Fog
|
||||
color[1] += (color[1] * 0.6 - color[1]) * t;
|
||||
color[2] += (color[2] * 0.6 - color[2]) * t;
|
||||
}
|
||||
let brighten = if eye_in_water {
|
||||
let env = &input.environment;
|
||||
let brighten = if eye_in_water && input.full_water_vision {
|
||||
water_vision
|
||||
} else if eye_in_water {
|
||||
state.water_vision_over(true, env.water_fog_brightening_time) * env.water_fog_brightening
|
||||
} else {
|
||||
input.night_vision
|
||||
};
|
||||
@@ -342,10 +352,11 @@ pub fn setup_fog(tables: &FogTables, state: &FogState, input: &FogInputs) -> Fog
|
||||
}
|
||||
_ if input.blindness.is_some() => {
|
||||
let (duration, infinite) = input.blindness.unwrap_or((0, true));
|
||||
let end = env.blindness_fog_distance;
|
||||
let distance = if infinite {
|
||||
5.0
|
||||
end
|
||||
} else {
|
||||
rd + (5.0 - rd) * (duration as f32 / 20.0).min(1.0)
|
||||
rd + (end - rd) * (duration as f32 / 20.0).min(1.0)
|
||||
};
|
||||
fog.environmental_start = distance * 0.25;
|
||||
fog.environmental_end = distance;
|
||||
@@ -355,7 +366,7 @@ pub fn setup_fog(tables: &FogTables, state: &FogState, input: &FogInputs) -> Fog
|
||||
_ if input.darkness.is_some() => {
|
||||
// DarknessFogEnvironment.setupFog: closing in to 15 blocks.
|
||||
let blend = input.darkness.unwrap_or(0.0);
|
||||
let distance = rd + (15.0 - rd) * blend;
|
||||
let distance = rd + (env.darkness_fog_distance - rd) * blend;
|
||||
fog.environmental_start = distance * 0.75;
|
||||
fog.environmental_end = distance;
|
||||
fog.sky_end = distance;
|
||||
@@ -380,7 +391,7 @@ pub fn setup_fog(tables: &FogTables, state: &FogState, input: &FogInputs) -> Fog
|
||||
fog.cloud_end = ((input.cloud_range_chunks * 16) as f32).min(sky.cloud_fog_end);
|
||||
if input.boss_world_fog {
|
||||
fog.environmental_start = fog.environmental_start.min(10.0);
|
||||
fog.environmental_end = fog.environmental_end.min(96.0);
|
||||
fog.environmental_end = fog.environmental_end.min(env.boss_fog_distance);
|
||||
fog.sky_end = fog.environmental_end;
|
||||
fog.cloud_end = fog.environmental_end;
|
||||
}
|
||||
@@ -483,6 +494,7 @@ mod tests {
|
||||
fire_resistance: false,
|
||||
full_water_vision: false,
|
||||
night_vision: 0.0,
|
||||
environment: Default::default(),
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
@@ -560,6 +572,55 @@ mod tests {
|
||||
assert!(fog.environmental_start >= 1e9 && fog.render_distance_start >= 1e9);
|
||||
}
|
||||
#[test]
|
||||
fn a_packs_environment_moves_effect_fog_and_its_colours() {
|
||||
let sky = Sky {
|
||||
fog_start: 10.0,
|
||||
fog_end: 96.0,
|
||||
..Sky::default()
|
||||
};
|
||||
let tables = FogTables {
|
||||
water_fog_end: vec![1.0],
|
||||
};
|
||||
let environment = horizon_polytone::colors::Environment {
|
||||
powder_snow_fog: Some(0xFF0000),
|
||||
blindness_fog_distance: 40.0,
|
||||
water_fog_brightening: 0.0,
|
||||
..Default::default()
|
||||
};
|
||||
let mut input = inputs(&sky);
|
||||
input.blindness = Some((-1, true));
|
||||
input.environment = environment;
|
||||
assert_eq!(
|
||||
setup_fog(&tables, &FogState::default(), &input).environmental_end,
|
||||
40.0
|
||||
);
|
||||
input.blindness = None;
|
||||
input.fog_type = FogType::PowderSnow;
|
||||
let fog = setup_fog(&tables, &FogState::default(), &input);
|
||||
assert_eq!(fog.color, [1.0, 0.0, 0.0, 1.0]);
|
||||
// Under water for the full 600 ticks: vanilla washes the fog out to a full channel; a
|
||||
// brightening of 0 keeps the colour given.
|
||||
let mut state = FogState::default();
|
||||
for _ in 0..600 {
|
||||
state.tick(true, false);
|
||||
}
|
||||
input.fog_type = FogType::Water;
|
||||
input.water_fog_color = [0.1, 0.2, 0.4];
|
||||
let washed = setup_fog(&tables, &state, &inputs_with(&input, Default::default())).color;
|
||||
assert_eq!(washed[2], 1.0);
|
||||
let kept = setup_fog(&tables, &state, &input).color;
|
||||
assert_eq!(&kept[..3], &[0.1, 0.2, 0.4]);
|
||||
}
|
||||
fn inputs_with<'a>(
|
||||
input: &FogInputs<'a>,
|
||||
environment: horizon_polytone::colors::Environment,
|
||||
) -> FogInputs<'a> {
|
||||
FogInputs {
|
||||
environment,
|
||||
..*input
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn fog_off_keeps_fluid_and_effect_fog() {
|
||||
let sky = Sky {
|
||||
fog_start: 10.0,
|
||||
|
||||
@@ -7661,7 +7661,7 @@ impl Renderer {
|
||||
) -> std::result::Result<Vec<String>, String> {
|
||||
let linear = pack
|
||||
.as_ref()
|
||||
.is_some_and(|p| p.0.manifest.api == horizon_shader_api::Api::V0_2);
|
||||
.is_some_and(|p| p.0.manifest.api == horizon_render_pipeline::Api::V0_2);
|
||||
if (linear || self.output.frame.format == LINEAR_FRAME)
|
||||
&& linear != (self.output.frame.format == LINEAR_FRAME)
|
||||
&& !std::mem::replace(&mut self.frame_kept_logged, true)
|
||||
@@ -7714,7 +7714,7 @@ impl Renderer {
|
||||
inputs.declarations.push_str(
|
||||
"layout(set = 5, binding = 0) readonly buffer HzIndices { uint hz_indices[]; };\n",
|
||||
);
|
||||
horizon_shaders::GeometryInputs {
|
||||
horizon_render_pipeline::GeometryInputs {
|
||||
inputs,
|
||||
pull: pack_atlas::PULL.to_owned(),
|
||||
index: "hz_indices[@]".to_owned(),
|
||||
@@ -7737,7 +7737,7 @@ impl Renderer {
|
||||
}
|
||||
}
|
||||
|
||||
fn pack_terrain_inputs(&mut self) -> Option<horizon_shaders::GbuffersInputs> {
|
||||
fn pack_terrain_inputs(&mut self) -> Option<horizon_render_pipeline::GbuffersInputs> {
|
||||
// Flat draws whenever the device has them (`render` keeps them on with a pack).
|
||||
let flat = self.flat.is_some() && self.flat_on;
|
||||
// Flat draws read each sprite's place from a table: the atlas is stitched as the game's.
|
||||
@@ -10410,7 +10410,7 @@ impl Renderer {
|
||||
self.own_timing.as_mut(),
|
||||
);
|
||||
pass.set_bind_group(0, &self.group, &[]);
|
||||
pass.set_bind_group(horizon_shader_api::stages::GROUP, &shadow.group, &[]);
|
||||
pass.set_bind_group(horizon_render_pipeline::stages::GROUP, &shadow.group, &[]);
|
||||
if flat_frame
|
||||
&& let Some(flat) = &self.flat
|
||||
&& let Some(pipeline) = &shadow.flat
|
||||
@@ -10443,7 +10443,7 @@ impl Renderer {
|
||||
self.own_timing.as_mut(),
|
||||
);
|
||||
pass.set_bind_group(0, &self.group, &[]);
|
||||
pass.set_bind_group(horizon_shader_api::stages::GROUP, &shadow.group, &[]);
|
||||
pass.set_bind_group(horizon_render_pipeline::stages::GROUP, &shadow.group, &[]);
|
||||
if flat_frame
|
||||
&& let Some(flat) = &self.flat
|
||||
&& let Some(pipeline) = &shadow.flat_translucent
|
||||
@@ -10475,7 +10475,7 @@ impl Renderer {
|
||||
.begin_entities(&mut encoder, "Native shader pack entity shadow");
|
||||
pass.set_pipeline(&shadow.entity);
|
||||
pass.set_bind_group(0, &self.entity_group, &[]);
|
||||
pass.set_bind_group(horizon_shader_api::stages::GROUP, &shadow.group, &[]);
|
||||
pass.set_bind_group(horizon_render_pipeline::stages::GROUP, &shadow.group, &[]);
|
||||
pass.set_vertex_buffer(0, self.entity_buffer.slice(..));
|
||||
pass.set_index_buffer(self.indices.slice(..), wgpu::IndexFormat::Uint32);
|
||||
pass.draw_indexed(0..entity_quads * 6, 0, 0..1);
|
||||
@@ -10696,7 +10696,7 @@ impl Renderer {
|
||||
let gpu_occluding = gpu_frame && self.gpu_culling.as_ref().is_some_and(|g| g.occluding());
|
||||
let pack_frame = |pass: &mut wgpu::RenderPass<'_>| {
|
||||
if let Some((_, group)) = material {
|
||||
pass.set_bind_group(horizon_shader_api::stages::GROUP, group, &[]);
|
||||
pass.set_bind_group(horizon_render_pipeline::stages::GROUP, group, &[]);
|
||||
}
|
||||
};
|
||||
// A pack's solid terrain that calls `set_surface` also writes the surface target, in
|
||||
@@ -12084,7 +12084,7 @@ impl Renderer {
|
||||
&& self
|
||||
.native
|
||||
.as_ref()
|
||||
.is_none_or(|n| n.frame.api == horizon_shader_api::Api::V0_1);
|
||||
.is_none_or(|n| n.frame.api == horizon_render_pipeline::Api::V0_1);
|
||||
match (&self.hdr, &surface_frame) {
|
||||
(Some(hdr), Some(f)) => hdr.present(
|
||||
&self.queue,
|
||||
@@ -15830,11 +15830,11 @@ mod shadow_caster_tests {
|
||||
#[test]
|
||||
fn the_shadow_pass_draws_terrain_outside_the_pack_s_planes() {
|
||||
let kept = |near: Option<f32>, far: Option<f32>, y: f64, min: [f32; 3], depth: bool| {
|
||||
let shadow = horizon_shaders::Shadow {
|
||||
let shadow = horizon_render_pipeline::Shadow {
|
||||
distance: 128.0,
|
||||
near,
|
||||
far,
|
||||
..horizon_shaders::Shadow::default()
|
||||
..horizon_render_pipeline::Shadow::default()
|
||||
};
|
||||
let camera = DVec3::new(13.5, y + 1.62, -13.5);
|
||||
let view = pack_post::ShadowView {
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
//! colour. They read the terrain's group 0, the flat draws' group 1 where those run, and the
|
||||
//! pack's frame at group 3.
|
||||
|
||||
use horizon_shader_api::{check::CheckedStage, stages::GROUP};
|
||||
use horizon_render_pipeline::stages::GROUP;
|
||||
use horizon_shader_api::check::CheckedStage;
|
||||
|
||||
use super::native_post::{NativeFrame, present_constants};
|
||||
use super::wgsl_prefix::{TERRAIN, prefixed, section};
|
||||
@@ -369,7 +370,7 @@ fn shader_source(
|
||||
tags: &[(u32, u32)],
|
||||
materials: &[(u32, u32)],
|
||||
(vertex, displaced): (bool, bool),
|
||||
api: horizon_shader_api::Api,
|
||||
api: horizon_render_pipeline::Api,
|
||||
) -> String {
|
||||
format!(
|
||||
"{source}\n// Horizon's terrain (terrain.wgsl)\n{}{}{}{}{}{ENTRIES}{}{}",
|
||||
@@ -398,7 +399,7 @@ fn module(
|
||||
materials: &[(u32, u32)],
|
||||
vertex: bool,
|
||||
displaced: bool,
|
||||
api: horizon_shader_api::Api,
|
||||
api: horizon_render_pipeline::Api,
|
||||
) -> Result<naga::Module, String> {
|
||||
let text = shader_source(&stage.source, tags, materials, (vertex, displaced), api);
|
||||
naga::front::wgsl::parse_str(&text)
|
||||
@@ -417,7 +418,7 @@ impl NativeMaterial {
|
||||
vertex: bool,
|
||||
displaced: Option<&DisplacedShape<'_>>,
|
||||
) -> Result<Self, String> {
|
||||
let writes_surface = horizon_shader_api::stages::calls_set_surface(&stage.source);
|
||||
let writes_surface = horizon_render_pipeline::stages::calls_set_surface(&stage.source);
|
||||
let module = module(
|
||||
stage,
|
||||
shape.sprite_tags,
|
||||
@@ -586,7 +587,7 @@ impl NativeMaterial {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn coverage_test_shader(api: horizon_shader_api::Api) -> String {
|
||||
pub(super) fn coverage_test_shader(api: horizon_render_pipeline::Api) -> String {
|
||||
use horizon_shader_api::{MANIFEST, Pack, StageKind, check};
|
||||
let manifest = format!(
|
||||
"schema = 1\nid = \"coverage\"\nname = \"Coverage\"\nversion = \"1\"\napi = \"{api}\"\n[material]\nfile = \"material.wgsl\"\n[displacement]\n"
|
||||
@@ -829,7 +830,7 @@ mod tests {
|
||||
.into();
|
||||
let report = check(&Pack::from_files(files).unwrap()).unwrap();
|
||||
let source = report.stages[0].source.clone()
|
||||
+ horizon_shader_api::stages::PBR_STUB
|
||||
+ horizon_render_pipeline::stages::PBR_STUB
|
||||
+ super::super::native_post::line_helpers(api)
|
||||
+ entry;
|
||||
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
//! One of Horizon's own shader packs while it runs (docs/shaders.md): the frame its stages
|
||||
//! read, its sky, material and post stages, and the timer of their passes.
|
||||
|
||||
use horizon_shader_api::{Report, StageKind};
|
||||
use horizon_render_pipeline::StageKind;
|
||||
use horizon_shader_api::Report;
|
||||
|
||||
use super::displace::Displacement;
|
||||
use super::native_material::{DisplacedShape, NativeMaterial, TerrainShape};
|
||||
@@ -253,7 +254,7 @@ impl NativePack {
|
||||
.transpose()?;
|
||||
// A 0.2 pack always has one: its decode and tonemap run without a chain too.
|
||||
let tonemap = others.iter().find(|s| s.kind == StageKind::Tonemap);
|
||||
let post = (!post.is_empty() || api == horizon_shader_api::Api::V0_2)
|
||||
let post = (!post.is_empty() || api == horizon_render_pipeline::Api::V0_2)
|
||||
.then(|| {
|
||||
let manifest = &report.manifest;
|
||||
NativePost::new(device, post, manifest, constants, format, &frame, tonemap)
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
//! (`NativeFrame`).
|
||||
|
||||
use glam::{Mat4, Vec3};
|
||||
use horizon_shader_api::{
|
||||
Api, Frame as FrameInputs, StageKind, check::CheckedStage, stages::GROUP,
|
||||
};
|
||||
use horizon_render_pipeline::stages::{GROUP, POST_TARGET_BINDING};
|
||||
use horizon_render_pipeline::{Api, Frame as FrameInputs, StageKind};
|
||||
use horizon_shader_api::check::CheckedStage;
|
||||
|
||||
use super::Frame;
|
||||
|
||||
@@ -338,7 +338,7 @@ pub struct NativeFrame {
|
||||
reflection_sampler: wgpu::Sampler,
|
||||
/// The shadow map the stages read, and the camera that draws it when the pack asks for one.
|
||||
pub shadow_map: super::native_shadow::ShadowMap,
|
||||
shadow_camera: Option<horizon_shaders::Shadow>,
|
||||
shadow_camera: Option<horizon_render_pipeline::Shadow>,
|
||||
/// Whether the pack's material reads `reflection` (`NativePack::new`): else no mirror
|
||||
/// is drawn and `has_reflection` stays 0.
|
||||
pub reflects: bool,
|
||||
@@ -1014,10 +1014,9 @@ impl NativePost {
|
||||
texture_entry(11, wgpu::TextureSampleType::Uint),
|
||||
];
|
||||
entries.extend(pack_texture_entries(frame.textures.len()));
|
||||
entries
|
||||
.extend((0..targets.len() as u32).map(|j| {
|
||||
texture_entry(horizon_shader_api::stages::POST_TARGET_BINDING + j, float)
|
||||
}));
|
||||
entries.extend(
|
||||
(0..targets.len() as u32).map(|j| texture_entry(POST_TARGET_BINDING + j, float)),
|
||||
);
|
||||
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("Native shader pack post"),
|
||||
entries: &entries,
|
||||
@@ -1346,7 +1345,7 @@ impl NativePost {
|
||||
view
|
||||
};
|
||||
wgpu::BindGroupEntry {
|
||||
binding: horizon_shader_api::stages::POST_TARGET_BINDING + j as u32,
|
||||
binding: POST_TARGET_BINDING + j as u32,
|
||||
resource: wgpu::BindingResource::TextureView(view),
|
||||
}
|
||||
}));
|
||||
@@ -1497,7 +1496,7 @@ pub fn frame_inputs(
|
||||
size: [u32; 2],
|
||||
seconds: f32,
|
||||
index: u32,
|
||||
shadow: Option<&horizon_shaders::Shadow>,
|
||||
shadow: Option<&horizon_render_pipeline::Shadow>,
|
||||
) -> FrameInputs {
|
||||
let aspect = size[0] as f32 / size[1].max(1) as f32;
|
||||
let view = crate::camera::view_matrix(frame.yaw, frame.pitch);
|
||||
@@ -1655,7 +1654,7 @@ pub fn pack_textures(
|
||||
|
||||
/// Layout entries of `count` pack textures and their samplers.
|
||||
pub(super) fn pack_texture_entries(count: usize) -> Vec<wgpu::BindGroupLayoutEntry> {
|
||||
use horizon_shader_api::stages::{TEXTURE_BINDING, TEXTURE_SAMPLER_BINDING};
|
||||
use horizon_render_pipeline::stages::{TEXTURE_BINDING, TEXTURE_SAMPLER_BINDING};
|
||||
(0..count as u32)
|
||||
.flat_map(|i| {
|
||||
[
|
||||
@@ -1684,7 +1683,7 @@ pub(super) fn pack_texture_entries(count: usize) -> Vec<wgpu::BindGroupLayoutEnt
|
||||
pub(super) fn pack_texture_group_entries(
|
||||
textures: &[(wgpu::TextureView, wgpu::Sampler)],
|
||||
) -> Vec<wgpu::BindGroupEntry<'_>> {
|
||||
use horizon_shader_api::stages::{TEXTURE_BINDING, TEXTURE_SAMPLER_BINDING};
|
||||
use horizon_render_pipeline::stages::{TEXTURE_BINDING, TEXTURE_SAMPLER_BINDING};
|
||||
textures
|
||||
.iter()
|
||||
.enumerate()
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
//! draw lists (`ShadowView`) serve both; the map's texels are distorted toward the camera as the
|
||||
//! prelude's `shadow_position` undoes.
|
||||
|
||||
use horizon_shader_api::{Api, Frame as FrameInputs, manifest::Shadow as Settings, stages::GROUP};
|
||||
use horizon_shaders::Shadow;
|
||||
use horizon_render_pipeline::Shadow;
|
||||
use horizon_render_pipeline::{Api, Frame as FrameInputs, stages::GROUP};
|
||||
use horizon_shader_api::manifest::Shadow as Settings;
|
||||
|
||||
use super::native_material::TerrainShape;
|
||||
use super::wgsl_prefix::{TERRAIN, prefixed, section};
|
||||
@@ -387,7 +388,7 @@ fn entity_source() -> String {
|
||||
format!(
|
||||
"{}@group({GROUP}) @binding(0) var<uniform> frame: Frame;\n{}{ENTITY_ENTRIES}",
|
||||
FrameInputs::WGSL.wgsl(),
|
||||
horizon_shader_api::stages::SHADOW_HELPERS,
|
||||
horizon_render_pipeline::stages::SHADOW_HELPERS,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -486,7 +487,7 @@ fn shader_source(
|
||||
(Some(material), _) => (
|
||||
format!(
|
||||
"{material}{}{table}{}{}{GRID_ENTRIES}",
|
||||
horizon_shader_api::stages::PBR_STUB,
|
||||
horizon_render_pipeline::stages::PBR_STUB,
|
||||
super::displace::RELIEF,
|
||||
super::displace::GRID,
|
||||
),
|
||||
@@ -504,7 +505,7 @@ fn shader_source(
|
||||
format!(
|
||||
"{}@group({GROUP}) @binding(0) var<uniform> frame: Frame;\n{}{UNTAKEN}",
|
||||
FrameInputs::WGSL.wgsl(),
|
||||
horizon_shader_api::stages::SHADOW_HELPERS,
|
||||
horizon_render_pipeline::stages::SHADOW_HELPERS,
|
||||
),
|
||||
super::native_material::DISPLACE_VANILLA,
|
||||
),
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
//! names). It reads the renderer's globals (group 0) and the pack's frame (group 3), and
|
||||
//! draws where vanilla's sky draws; the sun, moon, stars and the dark disc are drawn over it.
|
||||
|
||||
use horizon_shader_api::{check::CheckedStage, stages::GROUP};
|
||||
use horizon_render_pipeline::stages::GROUP;
|
||||
use horizon_shader_api::check::CheckedStage;
|
||||
|
||||
use super::native_post::{NativeFrame, present_constants};
|
||||
use super::wgsl_prefix::{TERRAIN, prefixed, section};
|
||||
@@ -40,7 +41,7 @@ fn vanilla_sky() -> String {
|
||||
}
|
||||
|
||||
/// The sky shader for the pack's sky stage `source`.
|
||||
fn shader_source(source: &str, api: horizon_shader_api::Api) -> String {
|
||||
fn shader_source(source: &str, api: horizon_render_pipeline::Api) -> String {
|
||||
format!(
|
||||
"{source}\n// Horizon's sky (terrain.wgsl)\n{}{}{PACK_ENTRY}",
|
||||
vanilla_sky(),
|
||||
@@ -198,7 +199,7 @@ mod tests {
|
||||
// A pack may use the renderer's names for its own things.
|
||||
let own = "override gain: f32;\nconst PI: f32 = 3.0;\nfn view_ray() -> f32 { return PI; }\n\
|
||||
fn sky(input: SkyInput) -> vec4<f32> { return input.color * gain * view_ray(); }\n";
|
||||
let text = shader_source(&stage(own).source, horizon_shader_api::Api::V0_1);
|
||||
let text = shader_source(&stage(own).source, horizon_render_pipeline::Api::V0_1);
|
||||
let module = naga::front::wgsl::parse_str(&text).unwrap_or_else(|e| {
|
||||
panic!("{}", e.emit_to_string(&text));
|
||||
});
|
||||
@@ -261,7 +262,7 @@ mod tests {
|
||||
Vec::new(),
|
||||
Default::default(),
|
||||
None,
|
||||
horizon_shader_api::Api::V0_1,
|
||||
horizon_render_pipeline::Api::V0_1,
|
||||
);
|
||||
queue.write_buffer(
|
||||
&frame.buffer,
|
||||
|
||||
@@ -597,9 +597,9 @@ const FACE: &str = " uint hz_q = (uint(gl_VertexIndex) & ~3u) * 6u;\n \
|
||||
/// quad lands whole in one part. The colour's alpha can't decide it, as with `separateAo` it
|
||||
/// holds each vertex's own ambient occlusion.
|
||||
pub fn terrain_parts(
|
||||
terrain: &horizon_shaders::GbuffersInputs,
|
||||
terrain: &horizon_render_pipeline::GbuffersInputs,
|
||||
lava: &[u16],
|
||||
) -> [horizon_shaders::GbuffersInputs; 2] {
|
||||
) -> [horizon_render_pipeline::GbuffersInputs; 2] {
|
||||
let opaque = crate::mesher::OPAQUE_LAYER;
|
||||
let lava: String = lava
|
||||
.iter()
|
||||
@@ -619,7 +619,7 @@ pub fn terrain_parts(
|
||||
impl PackAtlas {
|
||||
/// The vertex inputs of a gbuffers program drawing terrain (`TerrainVertex`, terrain.wgsl's
|
||||
/// `vs_terrain` or, for `flat` draws, `vs_terrain_flat`), in atlas coordinates.
|
||||
pub fn terrain_inputs(&self, flat: bool) -> horizon_shaders::GbuffersInputs {
|
||||
pub fn terrain_inputs(&self, flat: bool) -> horizon_render_pipeline::GbuffersInputs {
|
||||
let mut declarations = String::from(
|
||||
"layout(location = 0) in vec3 hz_v_pos;\nlayout(location = 1) in uvec2 hz_v_uv;\n\
|
||||
layout(location = 2) in uint hz_v_layer;\nlayout(location = 3) in vec4 hz_v_color;\n\
|
||||
@@ -753,7 +753,7 @@ impl PackAtlas {
|
||||
("vaColor", "hz_Color"),
|
||||
("vaNormal", "hz_Normal"),
|
||||
];
|
||||
horizon_shaders::GbuffersInputs {
|
||||
horizon_render_pipeline::GbuffersInputs {
|
||||
declarations,
|
||||
body,
|
||||
attributes: attributes
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//! translator's `DRAW_UNIFORMS`): one slot per render stage of a uniform buffer, picked with a
|
||||
//! dynamic offset at each draw.
|
||||
|
||||
use horizon_shaders::{RENDER_STAGES, render_stage};
|
||||
use horizon_render_pipeline::{RENDER_STAGES, render_stage};
|
||||
|
||||
/// Where a run of `Frame::entities` begins, and what it is (slice 7): the run lasts until the
|
||||
/// next span. Recorded only while a shader pack draws entities, so Horizon's own frames carry
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
use std::ops::Range;
|
||||
|
||||
use horizon_shaders::render_stage;
|
||||
use horizon_render_pipeline::render_stage;
|
||||
|
||||
use super::Vertex;
|
||||
use super::pack_draw::EntitySpan;
|
||||
@@ -31,7 +31,7 @@ pub fn buffers() -> [Option<wgpu::VertexBufferLayout<'static>>; 1] {
|
||||
/// position, the shared atlas's UV, colour, light (block, sky in 16ths) and the world-oriented
|
||||
/// normal. The quad's corners, read from the same vertices at set 1, give the face normal where
|
||||
/// the vertex has none (custom geometry) and `at_tangent` along the texture's u, as for terrain.
|
||||
pub fn inputs() -> horizon_shaders::GbuffersInputs {
|
||||
pub fn inputs() -> horizon_render_pipeline::GbuffersInputs {
|
||||
let words = std::mem::size_of::<Vertex>() / 4;
|
||||
let mut declarations = "layout(location = 0) in vec3 hz_v_pos;\nlayout(location = 1) in vec2 hz_v_uv;\n\
|
||||
layout(location = 2) in uint hz_v_overlay;\nlayout(location = 3) in vec4 hz_v_color;\n\
|
||||
@@ -112,7 +112,7 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
|
||||
// bright.
|
||||
("horizonTier", "float((hz_v_overlay >> 16u) & 255u)"),
|
||||
];
|
||||
horizon_shaders::GbuffersInputs {
|
||||
horizon_render_pipeline::GbuffersInputs {
|
||||
declarations,
|
||||
body,
|
||||
attributes: attributes
|
||||
@@ -121,7 +121,7 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
|
||||
.collect(),
|
||||
after: String::new(),
|
||||
// Cutout, as vanilla's entity types mostly are (OQ-23): the loader's test at 0.1.
|
||||
alpha_test: Some(horizon_shaders::AlphaTest {
|
||||
alpha_test: Some(horizon_render_pipeline::AlphaTest {
|
||||
op: ">",
|
||||
reference: 0.1,
|
||||
}),
|
||||
@@ -138,7 +138,7 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
|
||||
/// inputs of [`inputs`], each vertex read from set 1 (`hz_entity_vertices`), as `Vertex` lays
|
||||
/// it out. The draw's corners walk its quads two triangles each, as `quad_indices` does: corner
|
||||
/// `c` of a draw from vertex `first` (a whole quad) is drawn as vertex `first / 4 * 6 + c`.
|
||||
pub fn geometry_inputs() -> horizon_shaders::GeometryInputs {
|
||||
pub fn geometry_inputs() -> horizon_render_pipeline::GeometryInputs {
|
||||
let words = std::mem::size_of::<Vertex>() / 4;
|
||||
let pull = format!(
|
||||
" uint hz_pw = hz_vertex_index * {words}u;\n \
|
||||
@@ -150,7 +150,7 @@ pub fn geometry_inputs() -> horizon_shaders::GeometryInputs {
|
||||
hz_v_light = uvec4(hz_pl & 255u, (hz_pl >> 8u) & 255u, (hz_pl >> 16u) & 255u, hz_pl >> 24u);\n \
|
||||
hz_v_normal = unpackSnorm4x8(hz_entity_vertices[hz_pw + 8u]);\n"
|
||||
);
|
||||
horizon_shaders::GeometryInputs {
|
||||
horizon_render_pipeline::GeometryInputs {
|
||||
inputs: inputs(),
|
||||
pull,
|
||||
// Quad `c / 6`, corner 0, 1, 2, 0, 2, 3 of it.
|
||||
@@ -163,7 +163,7 @@ pub fn geometry_inputs() -> horizon_shaders::GeometryInputs {
|
||||
/// What `gbuffers_clouds` reads from the clouds' mesh (entity vertices, `vs_cloud`): the
|
||||
/// position moved by the clouds' offset, the colour of the face's shade from their palette,
|
||||
/// full light, the face's normal. Set 1 holds the clouds' uniform (`clouds::Uniform`).
|
||||
pub fn cloud_inputs() -> horizon_shaders::GbuffersInputs {
|
||||
pub fn cloud_inputs() -> horizon_render_pipeline::GbuffersInputs {
|
||||
let declarations = "layout(location = 0) in vec3 hz_v_pos;\nlayout(location = 1) in vec2 hz_v_uv;\n\
|
||||
layout(location = 2) in uint hz_v_overlay;\nlayout(location = 3) in vec4 hz_v_color;\n\
|
||||
layout(location = 4) in uvec4 hz_v_light;\nlayout(location = 5) in vec4 hz_v_normal;\n\
|
||||
@@ -183,7 +183,7 @@ pub fn cloud_inputs() -> horizon_shaders::GbuffersInputs {
|
||||
("vaColor", "hz_Color"),
|
||||
("vaNormal", "hz_Normal"),
|
||||
];
|
||||
horizon_shaders::GbuffersInputs {
|
||||
horizon_render_pipeline::GbuffersInputs {
|
||||
declarations,
|
||||
body,
|
||||
attributes: attributes
|
||||
|
||||
@@ -17,7 +17,7 @@ const UNIFORM: u64 = 2 * 64;
|
||||
const SLOT: u64 = 32;
|
||||
|
||||
/// What `gbuffers_hand` reads: the entity inputs, and the hand's projection from set 1.
|
||||
pub fn inputs() -> horizon_shaders::GbuffersInputs {
|
||||
pub fn inputs() -> horizon_render_pipeline::GbuffersInputs {
|
||||
let mut inputs = super::pack_entities::inputs();
|
||||
// No last frame's hand to measure from: the hand's `at_velocity` reads zero (and its set 1
|
||||
// has no previous vertices).
|
||||
@@ -270,7 +270,7 @@ fn translucent_quad(quad: &[Vertex]) -> bool {
|
||||
/// outside block entities, and `currentRenderedItemId`.
|
||||
fn draw_slot(id: i32, translucent: bool) -> [u8; SLOT as usize] {
|
||||
let mut bytes = [0u8; SLOT as usize];
|
||||
let stage = horizon_shaders::render_stage(if translucent {
|
||||
let stage = horizon_render_pipeline::render_stage(if translucent {
|
||||
"HAND_TRANSLUCENT"
|
||||
} else {
|
||||
"HAND_SOLID"
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use horizon_shaders::{BufferObject, CustomImage, Format, Kind};
|
||||
use horizon_render_pipeline::{Format, Kind};
|
||||
use horizon_shaders::{BufferObject, CustomImage};
|
||||
|
||||
pub(super) struct PackImage {
|
||||
pub(super) def: CustomImage,
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
/// width in pixels. The strip's corners 0 and 1 are at the start, 2 and 3 at the end (six
|
||||
/// vertices per edge: pack pipelines draw triangle lists); the
|
||||
/// width is applied in screen space after the pack's transform.
|
||||
pub fn inputs() -> horizon_shaders::GbuffersInputs {
|
||||
pub fn inputs() -> horizon_render_pipeline::GbuffersInputs {
|
||||
let declarations =
|
||||
"layout(location = 0) in vec3 hz_l_start;\nlayout(location = 1) in vec3 hz_l_end;\n\
|
||||
layout(location = 2) in vec4 hz_l_argb;\nlayout(location = 3) in float hz_l_width;\n\
|
||||
@@ -41,7 +41,7 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
|
||||
if ((hz_l_c & 1) != 0) hz_l_o = -hz_l_o;\n \
|
||||
gl_Position.xy += hz_l_o * gl_Position.w;\n }\n"
|
||||
.to_owned();
|
||||
horizon_shaders::GbuffersInputs {
|
||||
horizon_render_pipeline::GbuffersInputs {
|
||||
declarations,
|
||||
body,
|
||||
attributes: attributes
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use glam::{DMat4, DVec3};
|
||||
use horizon_shaders::{Format, Group, Kind, Post, SHADOW_COLORS};
|
||||
use horizon_render_pipeline::{BUFFERS, Format, Kind, SHADOW_COLORS};
|
||||
use horizon_shaders::{Group, Post};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use super::pack_draw::DrawUniforms;
|
||||
@@ -28,8 +29,6 @@ use super::pack_sky::PackSky;
|
||||
use super::pack_textures;
|
||||
use super::pack_uniforms::{self, FrameUniforms, PackInputs};
|
||||
|
||||
const BUFFERS: usize = horizon_shaders::BUFFERS;
|
||||
|
||||
mod packed;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -126,7 +125,7 @@ enum Source {
|
||||
ShadowDepth(usize, bool),
|
||||
/// `shadowcolor0/1` read outside the shadow programs (in them they are `Color`).
|
||||
ShadowColor(usize),
|
||||
/// The boats' water patches' depth (`horizon_shaders::WATER_MASK`), for translucent terrain.
|
||||
/// The boats' water patches' depth (`horizon_render_pipeline::WATER_MASK`), for translucent terrain.
|
||||
WaterMask,
|
||||
}
|
||||
|
||||
@@ -691,7 +690,7 @@ pub struct PackPost {
|
||||
/// `depthtex0` at the screen's centre, read back for `centerDepthSmooth`.
|
||||
center_depth: Option<super::pack_center_depth::CenterDepth>,
|
||||
/// `shaders.properties`' custom uniforms, after the frame's in `uniforms.values`.
|
||||
custom: horizon_shaders::Custom,
|
||||
custom: Box<dyn horizon_render_pipeline::CustomUniforms>,
|
||||
atlas: Option<wgpu::TextureView>,
|
||||
material_maps: Option<[wgpu::TextureView; 2]>,
|
||||
/// The shared atlas's normal and specular paged maps (`set_paged_maps`).
|
||||
@@ -884,13 +883,13 @@ fn fits(ty: &str, sample_type: wgpu::TextureSampleType) -> bool {
|
||||
/// `hz_depth_linear` for a program: 1 for each of shadowtex0, shadowtex1, `shadow` and
|
||||
/// `watershadow` whose depth filters (`linear`, bit n: shadowtex n). `shadow` is shadowtex1 in a
|
||||
/// program that also declares `watershadow` (6.3).
|
||||
fn depth_linear(interface: &horizon_shaders::Interface, linear: u8) -> [f32; 4] {
|
||||
fn depth_linear(interface: &horizon_render_pipeline::Interface, linear: u8) -> [f32; 4] {
|
||||
let watershadow = interface.samplers.iter().any(|s| s.name == "watershadow");
|
||||
[0, 1, usize::from(watershadow), 0].map(|n| f32::from(linear >> n & 1))
|
||||
}
|
||||
|
||||
fn source(name: &str, shadow_space: bool, shadow_pass: bool, watershadow: bool) -> Source {
|
||||
let shadow_color = horizon_shaders::shadow_color(name);
|
||||
let shadow_color = horizon_render_pipeline::shadow_color(name);
|
||||
let shadow_depth = match name {
|
||||
"shadowtex0" | "watershadow" | "shadowtex0HW" => Some(0),
|
||||
"shadowtex1" | "shadowtex1HW" => Some(1),
|
||||
@@ -909,7 +908,7 @@ fn source(name: &str, shadow_space: bool, shadow_pass: bool, watershadow: bool)
|
||||
};
|
||||
}
|
||||
}
|
||||
if shadow_space && horizon_shaders::buffer_index(name).is_some() {
|
||||
if shadow_space && horizon_render_pipeline::buffer_index(name).is_some() {
|
||||
// colortex buffers are not this pass's.
|
||||
return Source::Dummy;
|
||||
}
|
||||
@@ -922,8 +921,8 @@ fn source(name: &str, shadow_space: bool, shadow_pass: bool, watershadow: bool)
|
||||
"lightmap" => Source::Lightmap,
|
||||
"normals" => Source::Normals,
|
||||
"specular" => Source::Specular,
|
||||
horizon_shaders::WATER_MASK => Source::WaterMask,
|
||||
n => horizon_shaders::buffer_index(n).map_or(Source::Dummy, Source::Color),
|
||||
horizon_render_pipeline::WATER_MASK => Source::WaterMask,
|
||||
n => horizon_render_pipeline::buffer_index(n).map_or(Source::Dummy, Source::Color),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1504,7 +1503,7 @@ fn owned_buffers(
|
||||
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
|
||||
fn bindings(
|
||||
features: wgpu::Features,
|
||||
interface: &horizon_shaders::Interface,
|
||||
interface: &horizon_render_pipeline::Interface,
|
||||
name: &str,
|
||||
group: Group,
|
||||
buffers: &[u32],
|
||||
@@ -1575,12 +1574,12 @@ fn bindings(
|
||||
// pack of ours (#9064) bound it over `gaux2` and `noisetex` but not over names of its
|
||||
// own, where a raw texture was bound; this rule is a fit to those cases.
|
||||
let unbound = t.png
|
||||
&& t.stage != horizon_shaders::EVERY_STAGE
|
||||
&& t.stage != horizon_render_pipeline::textures::EVERY_STAGE
|
||||
&& source(&s.name, false, true, false) == Source::Dummy;
|
||||
!unbound
|
||||
&& (t.stage != pack_textures::stage(group)
|
||||
|| released.0 != t.stage
|
||||
|| horizon_shaders::buffer_index(&t.name)
|
||||
|| horizon_render_pipeline::buffer_index(&t.name)
|
||||
.is_none_or(|b| released.1 & 1 << b == 0))
|
||||
});
|
||||
let image = pack
|
||||
@@ -2284,7 +2283,7 @@ fn build_compute(
|
||||
device: &wgpu::Device,
|
||||
name: String,
|
||||
group: Group,
|
||||
c: horizon_shaders::ComputeProgram,
|
||||
c: horizon_render_pipeline::ComputeProgram,
|
||||
formats: &[wgpu::TextureFormat; BUFFERS],
|
||||
shadow: Option<&[wgpu::TextureFormat; SHADOW_COLORS]>,
|
||||
notes: &mut Vec<String>,
|
||||
@@ -2522,24 +2521,22 @@ impl PackPost {
|
||||
let shadow_config = post.config.shadow.clone();
|
||||
let noise_size = post.config.noise_resolution.unwrap_or(256);
|
||||
let half_lives = post.config.half_lives;
|
||||
for (name, ticks) in horizon_shaders::HALF_LIVES.iter().zip(half_lives) {
|
||||
for (name, ticks) in horizon_render_pipeline::HALF_LIVES.iter().zip(half_lives) {
|
||||
if let Some(t) = ticks {
|
||||
notes.push(format!("`{name}` is {t} ticks"));
|
||||
}
|
||||
}
|
||||
// shadowcolor2 to 7 only for a pack that names `HIGHER_SHADOWCOLOR`.
|
||||
let shadow_colors = (1u16 << post.shadow_colors) - 1;
|
||||
let mut custom = post.custom;
|
||||
let custom_names: Vec<(String, String)> = (custom.uniforms())
|
||||
.map(|(name, ty)| (name.to_owned(), ty.to_owned()))
|
||||
.collect();
|
||||
let mut custom: Box<dyn horizon_render_pipeline::CustomUniforms> = Box::new(post.custom);
|
||||
let custom_names = custom.names();
|
||||
// A first run finds the names neither the frame nor the pack defines.
|
||||
custom.eval(0.0, &pack_uniforms::zero);
|
||||
// Its zeros are no start for `smooth`: the first frame's values are, as in Iris.
|
||||
custom.restart_smoothing();
|
||||
if !custom.notes.is_empty() {
|
||||
notes.push(format!("custom uniforms: {}", custom.notes.join("; ")));
|
||||
custom.notes.clear();
|
||||
let custom_notes = custom.take_notes();
|
||||
if !custom_notes.is_empty() {
|
||||
notes.push(format!("custom uniforms: {}", custom_notes.join("; ")));
|
||||
}
|
||||
let mask =
|
||||
|flips: &[(u32, bool)]| flips.iter().filter(|f| f.1).fold(0u16, |m, f| m | 1 << f.0);
|
||||
@@ -5395,7 +5392,7 @@ impl PackPost {
|
||||
self.uniforms.update(frame, inputs, size);
|
||||
// `smooth` starts on the first frame that knows the camera's biome.
|
||||
self.custom.world_frame(inputs.biome_known);
|
||||
self.uniforms.eval_custom(&mut self.custom);
|
||||
self.uniforms.eval_custom(self.custom.as_mut());
|
||||
if self.targets.is_none() {
|
||||
return;
|
||||
}
|
||||
@@ -6066,7 +6063,7 @@ mod tests {
|
||||
texture("composite", "tr", false),
|
||||
texture("composite", "gaux2", true),
|
||||
texture("composite", "noisetex", true),
|
||||
texture(horizon_shaders::EVERY_STAGE, "cp", true),
|
||||
texture(horizon_render_pipeline::textures::EVERY_STAGE, "cp", true),
|
||||
];
|
||||
let names = ["tp", "tr", "gaux2", "noisetex", "cp"];
|
||||
let interface = horizon_shaders::Interface {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
//! a fit to one driver's values, not a rule Iris states. `RGBA4` and `RGBA2` read back the `RGBA8`
|
||||
//! bytes in the same probe, as Horizon's 8-bit buffers already do.
|
||||
|
||||
use horizon_shaders::{Format, Kind};
|
||||
use horizon_render_pipeline::{Format, Kind};
|
||||
use naga::{BinaryOperator, Expression, Literal, MathFunction, Statement, VectorSize};
|
||||
|
||||
/// The bits each channel keeps in a buffer of format `f`, for the formats that keep fewer than
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! with two sides that `shadowcomp` programs flip as composite programs flip colortex (6.1.6).
|
||||
//! Square, `shadowMapResolution` pixels.
|
||||
|
||||
use horizon_shaders::SHADOW_COLORS;
|
||||
use horizon_render_pipeline::SHADOW_COLORS;
|
||||
|
||||
type Ops<T> = wgpu::Operations<T>;
|
||||
type Color = wgpu::Color;
|
||||
@@ -97,7 +97,11 @@ pub(super) fn stand_in(device: &wgpu::Device) -> wgpu::TextureView {
|
||||
}
|
||||
|
||||
impl ShadowTargets {
|
||||
pub fn reset_history(&mut self, device: &wgpu::Device, shadow: &horizon_shaders::Shadow) {
|
||||
pub fn reset_history(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
shadow: &horizon_render_pipeline::Shadow,
|
||||
) {
|
||||
let mut colors = 0;
|
||||
let mut doubled = 0;
|
||||
let formats = std::array::from_fn(|n| match &self.color[n] {
|
||||
@@ -125,7 +129,7 @@ impl ShadowTargets {
|
||||
/// resolution is kept within the device's limit.
|
||||
pub fn new(
|
||||
device: &wgpu::Device,
|
||||
shadow: &horizon_shaders::Shadow,
|
||||
shadow: &horizon_render_pipeline::Shadow,
|
||||
formats: [wgpu::TextureFormat; SHADOW_COLORS],
|
||||
colors: u16,
|
||||
doubled: u16,
|
||||
|
||||
@@ -58,7 +58,7 @@ pub(super) const ATTRIBUTES: [wgpu::VertexAttribute; 2] =
|
||||
wgpu::vertex_attr_array![0 => Float32x3, 1 => Uint32];
|
||||
|
||||
/// What `gbuffers_skybasic` reads from these vertices.
|
||||
pub fn inputs() -> horizon_shaders::GbuffersInputs {
|
||||
pub fn inputs() -> horizon_render_pipeline::GbuffersInputs {
|
||||
let declarations =
|
||||
"layout(location = 0) in vec3 hz_v_pos;\nlayout(location = 1) in uint hz_v_kind;\n\
|
||||
layout(set = 1, binding = 0) uniform HzSky { mat4 hz_sky_stars; vec4 hz_sky_color; \
|
||||
@@ -90,7 +90,7 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
|
||||
hz_MultiTexCoord2 = hz_MultiTexCoord1;\n \
|
||||
hz_Normal = vec3(0.0, 0.0, 1.0);\n"
|
||||
);
|
||||
horizon_shaders::GbuffersInputs {
|
||||
horizon_render_pipeline::GbuffersInputs {
|
||||
declarations,
|
||||
body,
|
||||
attributes: vec![
|
||||
@@ -111,8 +111,8 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
|
||||
}
|
||||
|
||||
/// What `gbuffers_skytextured` reads from the sun and moon's vertices.
|
||||
pub fn celestial_inputs() -> horizon_shaders::GbuffersInputs {
|
||||
horizon_shaders::GbuffersInputs {
|
||||
pub fn celestial_inputs() -> horizon_render_pipeline::GbuffersInputs {
|
||||
horizon_render_pipeline::GbuffersInputs {
|
||||
declarations:
|
||||
"layout(location = 0) in vec3 hz_v_pos;\nlayout(location = 1) in vec2 hz_v_uv;\n\
|
||||
layout(location = 2) in vec4 hz_v_color;\n"
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
//! A shader pack's custom textures (`texture.<stage>.<name>`, `texture.noise`) on the GPU, and
|
||||
//! which of them a program's sampler reads.
|
||||
|
||||
use horizon_shaders::{CustomTexture, Group, Kind, Texels, Texture, TextureDim, TextureSource};
|
||||
use horizon_render_pipeline::Kind;
|
||||
use horizon_render_pipeline::textures::{
|
||||
CustomTexture, Dim as TextureDim, EVERY_STAGE, Texels, Texture, TextureSource,
|
||||
};
|
||||
use horizon_shaders::Group;
|
||||
|
||||
/// What a custom texture is once uploaded: an image of ours, or one the game already has.
|
||||
pub(super) enum Image {
|
||||
@@ -118,24 +122,24 @@ pub(super) fn find(textures: &[PackTexture], group: Group, ty: &str, name: &str)
|
||||
} else {
|
||||
wgpu::TextureViewDimension::D2
|
||||
};
|
||||
let buffer = horizon_shaders::buffer_index(name);
|
||||
let buffer = horizon_render_pipeline::buffer_index(name);
|
||||
let staged = textures.iter().position(|t| {
|
||||
t.stage == stage(group)
|
||||
&& t.dimension == dimension
|
||||
&& (t.name == name
|
||||
|| buffer.is_some() && horizon_shaders::buffer_index(&t.name) == buffer)
|
||||
|| buffer.is_some() && horizon_render_pipeline::buffer_index(&t.name) == buffer)
|
||||
});
|
||||
staged.or_else(|| {
|
||||
textures.iter().position(|t| {
|
||||
t.stage == horizon_shaders::EVERY_STAGE && t.dimension == dimension && t.name == name
|
||||
})
|
||||
textures
|
||||
.iter()
|
||||
.position(|t| t.stage == EVERY_STAGE && t.dimension == dimension && t.name == name)
|
||||
})
|
||||
}
|
||||
|
||||
/// The `shaders.properties` key a texture came from, for notes.
|
||||
fn key(stage: &str, name: &str) -> String {
|
||||
match stage {
|
||||
horizon_shaders::EVERY_STAGE => format!("customTexture.{name}"),
|
||||
EVERY_STAGE => format!("customTexture.{name}"),
|
||||
_ => format!("texture.{stage}.{name}"),
|
||||
}
|
||||
}
|
||||
@@ -350,7 +354,7 @@ mod tests {
|
||||
#[test]
|
||||
fn custom_textures_reach_every_program() {
|
||||
use wgpu::TextureViewDimension as D;
|
||||
let every = horizon_shaders::EVERY_STAGE;
|
||||
let every = EVERY_STAGE;
|
||||
let textures = [
|
||||
lightmap(every, "texClouds", D::D3),
|
||||
lightmap("composite", "texClouds", D::D3),
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
//! [-w, w], and the depth buffers packs read hold GL window depth.
|
||||
|
||||
use glam::{DMat4, DVec3, Mat4, Vec3, Vec4};
|
||||
use horizon_shaders::Shadow;
|
||||
use horizon_render_pipeline::{CustomUniforms, Shadow, Val};
|
||||
|
||||
use super::Frame;
|
||||
|
||||
@@ -440,7 +440,7 @@ pub struct FrameUniforms {
|
||||
center_depth: Option<f32>,
|
||||
/// The pack's shadow settings (7.1).
|
||||
pub shadow: Shadow,
|
||||
/// The pack's smoothing half-lives in ticks (`horizon_shaders::HALF_LIVES`).
|
||||
/// The pack's smoothing half-lives in ticks (`horizon_render_pipeline::HALF_LIVES`).
|
||||
pub half_lives: [Option<f32>; 4],
|
||||
}
|
||||
|
||||
@@ -1050,8 +1050,8 @@ impl FrameUniforms {
|
||||
|
||||
impl FrameUniforms {
|
||||
/// Evaluates the pack's custom uniforms with this frame's values and puts them after
|
||||
/// `NAMES`' values, in `custom.uniforms()` order.
|
||||
pub fn eval_custom(&mut self, custom: &mut horizon_shaders::Custom) {
|
||||
/// `NAMES`' values, in `custom.names()` order.
|
||||
pub fn eval_custom(&mut self, custom: &mut dyn CustomUniforms) {
|
||||
if let Some(tick) = self.custom_restart {
|
||||
custom.restart_at(tick);
|
||||
}
|
||||
@@ -1061,8 +1061,8 @@ impl FrameUniforms {
|
||||
let v = values.get(index(name)?)?;
|
||||
let n = components(name);
|
||||
Some(match *v {
|
||||
Value::F(f) => horizon_shaders::Val::vector(&f.map(f64::from)[..n]),
|
||||
Value::I(i) => horizon_shaders::Val::vector(&i.map(f64::from)[..n.min(3)]),
|
||||
Value::F(f) => Val::vector(&f.map(f64::from)[..n]),
|
||||
Value::I(i) => Val::vector(&i.map(f64::from)[..n.min(3)]),
|
||||
})
|
||||
};
|
||||
let dt = match index("frameTime").and_then(|i| values.get(i)) {
|
||||
@@ -1144,9 +1144,9 @@ fn components(name: &str) -> usize {
|
||||
|
||||
/// What `name` gives custom uniforms before the first frame: zeros, at its size, so their
|
||||
/// shapes check out as on a frame (#9086).
|
||||
pub fn zero(name: &str) -> Option<horizon_shaders::Val> {
|
||||
pub fn zero(name: &str) -> Option<Val> {
|
||||
index(name)?;
|
||||
Some(horizon_shaders::Val::vector(&[0.0; 16][..components(name)]))
|
||||
Some(Val::vector(&[0.0; 16][..components(name)]))
|
||||
}
|
||||
|
||||
/// The position of `name` in [`NAMES`].
|
||||
@@ -2717,7 +2717,7 @@ pub(super) mod initial_history_tests {
|
||||
/// `horizonWaterPatch<k>`'s `k`: patch `k / 2`, its corner and y range (even) or edges (odd).
|
||||
fn water_patch_index(name: &str) -> Option<usize> {
|
||||
let k: usize = name.strip_prefix("horizonWaterPatch")?.parse().ok()?;
|
||||
(k < 2 * horizon_shaders::WATER_PATCHES).then_some(k)
|
||||
(k < 2 * horizon_render_pipeline::WATER_PATCHES).then_some(k)
|
||||
}
|
||||
|
||||
/// The boats' water patches among the frame's entity quads (`WATER_MASK_OVERLAY`), the first
|
||||
@@ -2729,7 +2729,7 @@ pub(super) fn water_patches(entities: &[super::Vertex]) -> Vec<[[f32; 4]; 2]> {
|
||||
let mut points: Vec<[f32; 3]> = Vec::new();
|
||||
let flush = |points: &mut Vec<[f32; 3]>, out: &mut Vec<[[f32; 4]; 2]>| {
|
||||
if let Some(b) = patch_box(points)
|
||||
&& out.len() < horizon_shaders::WATER_PATCHES
|
||||
&& out.len() < horizon_render_pipeline::WATER_PATCHES
|
||||
{
|
||||
out.push(b);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use super::pack_post::Weather;
|
||||
/// What `gbuffers_weather` reads from a weather vertex: camera-relative position, the UV in
|
||||
/// repeats of the sprite, colour (with vanilla's fade) and light. Weather has no normal; the
|
||||
/// pack sees straight up.
|
||||
pub fn inputs() -> horizon_shaders::GbuffersInputs {
|
||||
pub fn inputs() -> horizon_render_pipeline::GbuffersInputs {
|
||||
let declarations =
|
||||
"layout(location = 0) in vec3 hz_v_pos;\nlayout(location = 1) in vec2 hz_v_uv;\n\
|
||||
layout(location = 2) in uint hz_v_overlay;\nlayout(location = 3) in vec4 hz_v_color;\n\
|
||||
@@ -31,7 +31,7 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
|
||||
("vaColor", "hz_Color"),
|
||||
("vaNormal", "hz_Normal"),
|
||||
];
|
||||
horizon_shaders::GbuffersInputs {
|
||||
horizon_render_pipeline::GbuffersInputs {
|
||||
declarations,
|
||||
body,
|
||||
attributes: attributes
|
||||
@@ -40,7 +40,7 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
|
||||
.collect(),
|
||||
after: String::new(),
|
||||
// Vanilla's weather drops texels below 0.1, as Horizon's own weather shader does.
|
||||
alpha_test: Some(horizon_shaders::AlphaTest {
|
||||
alpha_test: Some(horizon_render_pipeline::AlphaTest {
|
||||
op: ">",
|
||||
reference: 0.1,
|
||||
}),
|
||||
|
||||
@@ -158,7 +158,7 @@ impl Weather {
|
||||
match native {
|
||||
Some((pipeline, frame)) => {
|
||||
pass.set_pipeline(pipeline);
|
||||
pass.set_bind_group(horizon_shader_api::stages::GROUP, frame, &[]);
|
||||
pass.set_bind_group(horizon_render_pipeline::stages::GROUP, frame, &[]);
|
||||
}
|
||||
None => pass.set_pipeline(&self.pipeline),
|
||||
}
|
||||
@@ -225,7 +225,7 @@ struct HorizonWeatherOut {
|
||||
";
|
||||
|
||||
/// The shader of a native pack's weather stage `source`.
|
||||
fn native_source(source: &str, api: horizon_shader_api::Api) -> String {
|
||||
fn native_source(source: &str, api: horizon_render_pipeline::Api) -> String {
|
||||
format!(
|
||||
"{source}\n// Horizon's weather (entity.wgsl)\n{}{}{NATIVE_ENTRIES}",
|
||||
super::wgsl_prefix::prefixed(&super::entity_shader(), "horizon_entity_"),
|
||||
@@ -245,7 +245,7 @@ pub(super) fn native_pipeline(
|
||||
stage: &horizon_shader_api::check::CheckedStage,
|
||||
constants: &[(String, f64)],
|
||||
) -> Result<wgpu::RenderPipeline, String> {
|
||||
use horizon_shader_api::stages::GROUP;
|
||||
use horizon_render_pipeline::stages::GROUP;
|
||||
let text = native_source(&stage.source, frame.api);
|
||||
let module = naga::front::wgsl::parse_str(&text)
|
||||
.map_err(|e| format!("{}: {}", stage.file, e.emit_to_string(&text)))?;
|
||||
@@ -322,7 +322,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn a_native_weather_stage_validates() {
|
||||
use horizon_shader_api::{Api, stages::StageKind};
|
||||
use horizon_render_pipeline::{Api, StageKind};
|
||||
for api in [Api::V0_1, Api::V0_2] {
|
||||
let source = StageKind::Weather.prelude(api)
|
||||
+ "fn weather(input: WeatherInput) -> vec4<f32> {\n \
|
||||
|
||||
@@ -86,8 +86,8 @@ pub mod options_fixes;
|
||||
pub mod options_ui;
|
||||
pub mod overlay;
|
||||
pub mod pack_browser;
|
||||
pub mod pack_dyes;
|
||||
pub mod pack_options_ui;
|
||||
pub mod pack_palette;
|
||||
pub mod pack_screen;
|
||||
pub mod pack_tags;
|
||||
pub mod packs;
|
||||
|
||||
@@ -766,7 +766,10 @@ fn stem_color(age: u8) -> u32 {
|
||||
(a * 32) << 16 | (255 - a * 8) << 8 | (a * 4)
|
||||
}
|
||||
fn redstone_color(power: u8) -> u32 {
|
||||
// RedstoneWireBlock.COLORS
|
||||
// RedstoneWireBlock.COLORS, or a pack's.
|
||||
if let Some(color) = crate::pack_palette::redstone(power) {
|
||||
return color;
|
||||
}
|
||||
let f = f32::from(power) / 15.0;
|
||||
let r = f * 0.6 + if f > 0.0 { 0.4 } else { 0.3 };
|
||||
let g = (f * f * 0.7 - 0.5).clamp(0.0, 1.0);
|
||||
|
||||
+14
-4
@@ -1,6 +1,7 @@
|
||||
//! Polytone's dye and sheep colours (`dye.<name>.diffuse|text`, `sheep.<name>`), for drawing only:
|
||||
//! the pack recolours collars, tropical fish, banners, wool and sign text, while gameplay (crafting,
|
||||
//! item data, the beacon) keeps vanilla's `DyeColor`. Lock-free: the draw paths read it per entity.
|
||||
//! Polytone's dye, sheep and redstone colours (`dye.<name>.diffuse|text`, `sheep.<name>`,
|
||||
//! `redstone.<power>`), for drawing only: the pack recolours collars, tropical fish, banners, wool,
|
||||
//! sign text and redstone wire and dust, while gameplay (crafting, item data, the beacon) keeps
|
||||
//! vanilla's `DyeColor`. Lock-free: the draw paths and the mesher's workers read it per face.
|
||||
use std::sync::atomic::{AtomicU64, Ordering::Relaxed};
|
||||
|
||||
use crate::block_entity_render::DYES;
|
||||
@@ -11,6 +12,7 @@ use horizon_polytone::colors::PackColors;
|
||||
static DIFFUSE: [AtomicU64; 16] = [const { AtomicU64::new(0) }; 16];
|
||||
static TEXT: [AtomicU64; 16] = [const { AtomicU64::new(0) }; 16];
|
||||
static SHEEP: [AtomicU64; 16] = [const { AtomicU64::new(0) }; 16];
|
||||
static REDSTONE: [AtomicU64; 16] = [const { AtomicU64::new(0) }; 16];
|
||||
|
||||
/// Takes the colours of the packs just loaded (vanilla's where they set none).
|
||||
pub fn set(colors: &PackColors) {
|
||||
@@ -24,6 +26,7 @@ pub fn set(colors: &PackColors) {
|
||||
store(&DIFFUSE, i, colors.dye(name, "diffuse"));
|
||||
store(&TEXT, i, colors.dye(name, "text"));
|
||||
store(&SHEEP, i, colors.sheep(name));
|
||||
store(&REDSTONE, i, colors.redstone(i as u8));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +45,11 @@ pub fn text(dye: usize) -> u32 {
|
||||
read(&TEXT, dye).unwrap_or(DYE_TEXT[dye & 15].1)
|
||||
}
|
||||
|
||||
/// A pack's `RedStoneWireBlock.COLORS` entry for this power (RGB), when it sets one.
|
||||
pub fn redstone(power: u8) -> Option<u32> {
|
||||
read(&REDSTONE, usize::from(power))
|
||||
}
|
||||
|
||||
/// `ColorLerper.Type.SHEEP` wool colour: a pack's `sheep.<name>`, else white 0xE6E6E6 and the
|
||||
/// others 75% of vanilla's dye (a pack's `dye.*.diffuse` doesn't reach wool).
|
||||
pub fn sheep_wool(dye: u8) -> u32 {
|
||||
@@ -69,7 +77,7 @@ mod tests {
|
||||
std::fs::create_dir_all(&polytone).unwrap();
|
||||
std::fs::write(
|
||||
polytone.join("colors.json"),
|
||||
r#"{"dye": {"cyan": {"diffuse": "00ff00", "text": "0000ff"}}, "sheep": {"pink": "2040ff"}}"#,
|
||||
r#"{"dye": {"cyan": {"diffuse": "00ff00", "text": "0000ff"}}, "sheep": {"pink": "2040ff"}, "redstone": {"7": "0x123456"}}"#,
|
||||
)
|
||||
.unwrap();
|
||||
set(&PackColors::read(&dir));
|
||||
@@ -81,7 +89,9 @@ mod tests {
|
||||
assert_eq!(sheep_wool(cyan as u8), 0x107575);
|
||||
assert_eq!(diffuse(purple), 0x8932B8);
|
||||
assert_eq!(text(purple), 0xA020F0);
|
||||
assert_eq!((redstone(7), redstone(15)), (Some(0x123456), None));
|
||||
set(&PackColors::default());
|
||||
assert_eq!(redstone(7), None);
|
||||
assert_eq!(diffuse(cyan), 0x169C9C);
|
||||
assert_eq!(sheep_wool(pink as u8), 0xB6687F);
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
@@ -168,6 +168,15 @@ impl Registry {
|
||||
let mut modifiers = Vec::new();
|
||||
let mut entity_modifiers = Vec::new();
|
||||
let mut particle_modifiers = Vec::new();
|
||||
// Custom colours' `particle.<id>`: a modifier that sets only the colour, below the files'.
|
||||
for (id, color) in horizon_polytone::colors::PackColors::read(pack_root).particles() {
|
||||
let json = serde_json::json!({"targets": [id], "color": color});
|
||||
match ParticleModifier::parse(&json, None) {
|
||||
Ok(Some(m)) => particle_modifiers.push((m, None, None)),
|
||||
Ok(None) => {}
|
||||
Err(e) => log::warn!("colors particle.{id}: {e}"),
|
||||
}
|
||||
}
|
||||
let namespaces = std::fs::read_dir(pack_root.join("assets"))
|
||||
.into_iter()
|
||||
.flatten()
|
||||
@@ -2104,6 +2113,23 @@ mod tests {
|
||||
std::fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_colours_recolour_their_particle_type() {
|
||||
let root = pack(&[(
|
||||
"assets/obs/polytone/colors.json",
|
||||
serde_json::json!({"particle": {"minecraft:flame": "0x0000ff"}}),
|
||||
)]);
|
||||
let registry = Registry::read(&root, Default::default(), blocks());
|
||||
let colour = |kind: &str| {
|
||||
let mut p = Particle::blank(ParticleType::Flame, [0.0; 3]);
|
||||
modify(®istry, kind, None, &mut p);
|
||||
p.rgba
|
||||
};
|
||||
assert_eq!(colour("minecraft:flame"), [0.0, 0.0, 1.0, 1.0]);
|
||||
assert_eq!(colour("minecraft:smoke"), [1.0; 4]);
|
||||
std::fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn particle_modifiers_scale_power_and_recolour_their_particles_once() {
|
||||
let root = pack(&[
|
||||
|
||||
@@ -85,7 +85,7 @@ impl SignText {
|
||||
}
|
||||
/// `DyeColor.getTextColor` of this side.
|
||||
pub fn text_color(&self) -> u32 {
|
||||
crate::pack_dyes::text(usize::from(self.color.min(15)))
|
||||
crate::pack_palette::text(usize::from(self.color.min(15)))
|
||||
}
|
||||
/// `SignText.hasAnyClickCommands`: a line whose style has a `run_command`
|
||||
/// click event.
|
||||
|
||||
@@ -51,6 +51,7 @@ pub fn main() -> horizon_client::Result<()> {
|
||||
.ok_or("missing cauldron")?,
|
||||
)?;
|
||||
let mut item = ItemEntity {
|
||||
random: horizon_core::random::LegacyRandom::new(0),
|
||||
rotation: [0.0; 2],
|
||||
join: Default::default(),
|
||||
health: 5,
|
||||
@@ -73,6 +74,7 @@ pub fn main() -> horizon_client::Result<()> {
|
||||
no_gravity: true,
|
||||
vertical_collision: false,
|
||||
horizontal_collision: false,
|
||||
fall_distance: 0.0,
|
||||
};
|
||||
output.push_str(f[0]);
|
||||
for _ in 0..2 {
|
||||
|
||||
@@ -276,8 +276,11 @@ fn concrete_powder_color(dye: &str) -> u32 {
|
||||
_ => 0x191919,
|
||||
}
|
||||
}
|
||||
/// `RedstoneWireBlock.COLORS`.
|
||||
/// `RedstoneWireBlock.COLORS`, or a pack's.
|
||||
fn redstone_color(power: u8) -> [f32; 3] {
|
||||
if let Some(c) = crate::pack_palette::redstone(power) {
|
||||
return [16, 8, 0].map(|shift| ((c >> shift) & 255) as f32 / 255.0);
|
||||
}
|
||||
let f = f32::from(power) / 15.0;
|
||||
let r = f * 0.6 + if f > 0.0 { 0.4 } else { 0.3 };
|
||||
let g = (f * f * 0.7 - 0.5).clamp(0.0, 1.0);
|
||||
|
||||
@@ -190,12 +190,12 @@ pub fn banner_color(item: &str) -> Option<&'static str> {
|
||||
DYES.iter().map(|d| d.0).find(|d| *d == name)
|
||||
}
|
||||
/// `DyeColor.getTextureDiffuseColor` (RGB) by dye name, as the resource
|
||||
/// packs draw it (`pack_dyes`): banner layers, banner and shield items and the
|
||||
/// packs draw it (`pack_palette`): banner layers, banner and shield items and the
|
||||
/// loom follow a pack's dye colours as the banner base does.
|
||||
pub fn dye_rgb(name: &str) -> u32 {
|
||||
DYES.iter()
|
||||
.position(|d| d.0 == name)
|
||||
.map_or(0xFFFFFF, crate::pack_dyes::diffuse)
|
||||
.map_or(0xFFFFFF, crate::pack_palette::diffuse)
|
||||
}
|
||||
/// The layers of a banner or shield stack.
|
||||
pub fn layers(stack: &ItemStack) -> &[BannerLayer] {
|
||||
|
||||
@@ -19,8 +19,8 @@ pub use horizon_client_render::{
|
||||
hdr_calibration, hud, hud_kit, hud_layout, hud_overlays, inventory_effects, java_text,
|
||||
jigsaw_block, json_folder, level_loading, light, lod, logging, menu_blur, mesher, minimap,
|
||||
mipmap, mod_browser, mod_list, model_library, models, multiline_edit, native_emissive,
|
||||
options_file, options_fixes, options_ui, overlay, pack_browser, pack_dyes, pack_screen, packs,
|
||||
pad_cursor, pad_glyph, par, particles, poi_sections, polytone_colors, polytone_post,
|
||||
options_file, options_fixes, options_ui, overlay, pack_browser, pack_palette, pack_screen,
|
||||
packs, pad_cursor, pad_glyph, par, particles, poi_sections, polytone_colors, polytone_post,
|
||||
post_chain, potent_sulfur, preedit, providers, pvp, pvp_hud, random_source, screen_draw,
|
||||
screen_effects, sculk, section_mask, server_terrain, settings, shader_options,
|
||||
shader_pack_list, signs, skins, social, sound_engine, sound_events, sound_preview, spans,
|
||||
|
||||
@@ -82,6 +82,7 @@ fn slime_at(world: &World, facts: &BlockFacts, position: BlockPos) -> bool {
|
||||
}
|
||||
|
||||
mod fluids;
|
||||
mod item_fall;
|
||||
mod item_no_physics;
|
||||
pub use fluids::{fluid_flow, update_entity_fluids, water_clip};
|
||||
|
||||
@@ -234,6 +235,12 @@ pub struct ItemEntity {
|
||||
pub vertical_collision: bool,
|
||||
/// `Entity.horizontalCollision`: the last move was stopped on x or z.
|
||||
pub horizontal_collision: bool,
|
||||
/// `Entity.random`, after the constructor's draws: `moveTowardsClosestSpace`
|
||||
/// takes an item's escape speed from it.
|
||||
pub random: horizon_core::random::LegacyRandom,
|
||||
/// `Entity.fallDistance` (`fall_distance` in its save): what the item has
|
||||
/// fallen since it last touched ground or water.
|
||||
pub fall_distance: f64,
|
||||
}
|
||||
/// What `Entity.doWaterSplashEffect` asks the level to do for a dropped item.
|
||||
/// Particles are drawn on the client (`Level.addParticle` is empty on the
|
||||
@@ -365,6 +372,10 @@ pub struct ItemStep {
|
||||
did_move: bool,
|
||||
/// The contact block for a clipped vertical move; idle ticks have none.
|
||||
pub vertical_contact: Option<BlockPos>,
|
||||
/// `Entity.move` with physics: where it started, what it moved, and
|
||||
/// whether the move was applied. `None` while idle or without physics,
|
||||
/// where `checkFallDamage` does not run.
|
||||
pub fall_move: Option<(Vec3, Vec3, bool)>,
|
||||
}
|
||||
#[cfg(test)]
|
||||
impl ItemEntity {
|
||||
@@ -384,6 +395,7 @@ impl ItemEntity {
|
||||
) -> bool {
|
||||
let (water, lava) = self.update_fluids(world, fluids);
|
||||
self.water_entry(water > 0.0, self.ticks == 0, &mut |_| {});
|
||||
self.lava_fall(lava > 0.0);
|
||||
let step = self.tick_move(
|
||||
world,
|
||||
facts,
|
||||
@@ -547,14 +559,17 @@ impl ItemEntity {
|
||||
false,
|
||||
)
|
||||
}
|
||||
/// `Entity.updateFluidInteraction`: entering water after the first tick
|
||||
/// runs `doWaterSplashEffect`.
|
||||
/// `Entity.updateFluidInteraction`: water resets the fall, and entering
|
||||
/// it after the first tick runs `doWaterSplashEffect`.
|
||||
pub fn water_entry<F: FnMut(ItemSplash)>(
|
||||
&mut self,
|
||||
in_water: bool,
|
||||
first_tick: bool,
|
||||
splash: &mut F,
|
||||
) {
|
||||
if in_water {
|
||||
self.fall_distance = 0.0;
|
||||
}
|
||||
if in_water && !self.was_touching_water && !first_tick {
|
||||
let mut random = horizon_core::random::LegacyRandom::new(
|
||||
(self.id as i64)
|
||||
@@ -684,6 +699,7 @@ impl ItemEntity {
|
||||
binding: collision,
|
||||
subject: None,
|
||||
};
|
||||
let from = self.position;
|
||||
let mut wanted = self.velocity;
|
||||
// Entity.move: a block the item sits in (cobweb, powder snow)
|
||||
// scales this move once and stops it.
|
||||
@@ -713,7 +729,8 @@ impl ItemEntity {
|
||||
)
|
||||
.unwrap_or(Vec3::ZERO)
|
||||
};
|
||||
if move_applies(wanted, moved) {
|
||||
let applied = move_applies(wanted, moved);
|
||||
if applied {
|
||||
self.position = Vec3::new(
|
||||
self.position.x + moved.x,
|
||||
self.position.y + moved.y,
|
||||
@@ -755,6 +772,7 @@ impl ItemEntity {
|
||||
old_velocity,
|
||||
did_move: true,
|
||||
vertical_contact,
|
||||
fall_move: (!no_physics).then_some((from, moved, applied)),
|
||||
};
|
||||
}
|
||||
// applyEffectsFromBlocksForLastMovements: still lift/drag while idle.
|
||||
@@ -765,6 +783,7 @@ impl ItemEntity {
|
||||
old_velocity,
|
||||
did_move: false,
|
||||
vertical_contact: None,
|
||||
fall_move: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -828,7 +847,14 @@ impl ItemEntity {
|
||||
horizon_world::entity_move::scale_by_block_speed(self.velocity, factor);
|
||||
}
|
||||
self.apply_honey_slide(world, facts);
|
||||
self.stuck = self.stuck_multiplier(world, facts).unwrap_or(Vec3::ZERO);
|
||||
self.stuck = match self.stuck_multiplier(world, facts) {
|
||||
// Entity.makeStuckInBlock resets the fall.
|
||||
Some(stuck) => {
|
||||
self.fall_distance = 0.0;
|
||||
stuck
|
||||
}
|
||||
None => Vec3::ZERO,
|
||||
};
|
||||
}
|
||||
}
|
||||
/// The rest of `ItemEntity.tick`, after the bubble column's push: slime,
|
||||
@@ -2766,35 +2792,7 @@ impl ArmorStand {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod experience_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn death_reward_uses_levels_and_caps_the_recoverable_points() {
|
||||
for (level, reward) in [(0, 0), (1, 7), (14, 98), (15, 100), (30, 100)] {
|
||||
let xp = Experience {
|
||||
level,
|
||||
progress: 0.75,
|
||||
total: 10_000,
|
||||
};
|
||||
assert_eq!(xp.death_reward(false), reward);
|
||||
assert_eq!(xp.death_reward(true), 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn respawn_clears_all_xp_unless_it_is_kept() {
|
||||
let mut xp = Experience::default();
|
||||
xp.give(155);
|
||||
let before = (xp.level, xp.progress, xp.total);
|
||||
xp.respawn(true);
|
||||
assert_eq!((xp.level, xp.progress, xp.total), before);
|
||||
xp.respawn(false);
|
||||
assert_eq!((xp.level, xp.progress, xp.total), (0, 0.0, 0));
|
||||
xp.give(7);
|
||||
assert_eq!((xp.level, xp.progress, xp.total), (1, 0.0, 7));
|
||||
}
|
||||
}
|
||||
mod experience_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod fire_environment_tests {
|
||||
@@ -2823,34 +2821,19 @@ mod fire_environment_tests {
|
||||
assets.registry.default_state("minecraft:lava").unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let make = |id: &str| ItemEntity {
|
||||
rotation: [0.0, 0.0],
|
||||
join: Default::default(),
|
||||
no_gravity: false,
|
||||
id: 1,
|
||||
thrower: None,
|
||||
target: None,
|
||||
stack: ItemStack {
|
||||
item: std::sync::Arc::from(id),
|
||||
count: 1,
|
||||
damage: 0,
|
||||
extra: None,
|
||||
},
|
||||
health: 5,
|
||||
fire_ticks: 0,
|
||||
position: at,
|
||||
previous: at,
|
||||
velocity: Vec3::ZERO,
|
||||
on_ground: false,
|
||||
vertical_collision: false,
|
||||
horizontal_collision: false,
|
||||
age: 0,
|
||||
pickup_delay: 0,
|
||||
bob_offset: 0.0,
|
||||
ticks: 0,
|
||||
needs_sync: false,
|
||||
stuck: Vec3::ZERO,
|
||||
was_touching_water: false,
|
||||
let make = |id: &str| {
|
||||
ItemEntity::new(
|
||||
1,
|
||||
ItemStack {
|
||||
item: std::sync::Arc::from(id),
|
||||
count: 1,
|
||||
damage: 0,
|
||||
extra: None,
|
||||
},
|
||||
at,
|
||||
Vec3::ZERO,
|
||||
0.0,
|
||||
)
|
||||
};
|
||||
let mut wood = make("minecraft:oak_planks");
|
||||
let mut metal = make("minecraft:netherite_ingot");
|
||||
@@ -2958,30 +2941,7 @@ mod destroyed_bundle_environment_tests {
|
||||
ItemStack::new("minecraft:stone", 3),
|
||||
ItemStack::new("minecraft:diamond", 1),
|
||||
];
|
||||
let mut bundle = ItemEntity {
|
||||
rotation: [0.0, 0.0],
|
||||
join: Default::default(),
|
||||
no_gravity: false,
|
||||
id: 1,
|
||||
thrower: None,
|
||||
target: None,
|
||||
stack,
|
||||
health: 5,
|
||||
fire_ticks: 0,
|
||||
position: at,
|
||||
previous: at,
|
||||
velocity: Vec3::ZERO,
|
||||
on_ground: false,
|
||||
vertical_collision: false,
|
||||
horizontal_collision: false,
|
||||
age: 0,
|
||||
pickup_delay: 0,
|
||||
bob_offset: 0.0,
|
||||
ticks: 0,
|
||||
needs_sync: false,
|
||||
stuck: Vec3::ZERO,
|
||||
was_touching_water: false,
|
||||
};
|
||||
let mut bundle = ItemEntity::new(1, stack, at, Vec3::ZERO, 0.0);
|
||||
// 5 health against 4 a tick: the first tick leaves 1 and nothing out.
|
||||
let first = bundle.environment_tick(&world, &facts, assets.registry, &items);
|
||||
assert!(first.alive && first.spilled.is_empty());
|
||||
@@ -3165,6 +3125,7 @@ mod merge_tests {
|
||||
fn item(id: &str, count: u32, x: f64, age: i32) -> ItemEntity {
|
||||
let at = Vec3::new(x, 64.0, 0.5);
|
||||
ItemEntity {
|
||||
random: horizon_core::random::LegacyRandom::new(0),
|
||||
rotation: [0.0, 0.0],
|
||||
join: Default::default(),
|
||||
no_gravity: false,
|
||||
@@ -3185,6 +3146,7 @@ mod merge_tests {
|
||||
on_ground: true,
|
||||
vertical_collision: false,
|
||||
horizontal_collision: false,
|
||||
fall_distance: 0.0,
|
||||
age,
|
||||
pickup_delay: 0,
|
||||
bob_offset: 0.0,
|
||||
@@ -3684,6 +3646,7 @@ mod honey_slide_tests {
|
||||
pub(super) fn falling_item(x: f64, y: f64, z: f64, vx: f64, vy: f64, vz: f64) -> ItemEntity {
|
||||
let at = Vec3::new(x, y, z);
|
||||
ItemEntity {
|
||||
random: horizon_core::random::LegacyRandom::new(0),
|
||||
rotation: [0.0, 0.0],
|
||||
join: Default::default(),
|
||||
no_gravity: false,
|
||||
@@ -3704,6 +3667,7 @@ mod honey_slide_tests {
|
||||
on_ground: false,
|
||||
vertical_collision: false,
|
||||
horizontal_collision: false,
|
||||
fall_distance: 0.0,
|
||||
age: 10,
|
||||
pickup_delay: 0,
|
||||
bob_offset: 0.0,
|
||||
@@ -3835,6 +3799,7 @@ mod slime_bounce_tests {
|
||||
fn grounded_item() -> ItemEntity {
|
||||
let at = Vec3::new(0.5, 2.0, 0.5);
|
||||
ItemEntity {
|
||||
random: horizon_core::random::LegacyRandom::new(0),
|
||||
rotation: [0.0, 0.0],
|
||||
join: Default::default(),
|
||||
no_gravity: false,
|
||||
@@ -3855,6 +3820,7 @@ mod slime_bounce_tests {
|
||||
on_ground: true,
|
||||
vertical_collision: false,
|
||||
horizontal_collision: false,
|
||||
fall_distance: 0.0,
|
||||
age: 10,
|
||||
pickup_delay: 0,
|
||||
bob_offset: 0.0,
|
||||
@@ -3906,6 +3872,7 @@ mod slime_bounce_tests {
|
||||
old_velocity: item.velocity,
|
||||
did_move: true,
|
||||
vertical_contact: None,
|
||||
fall_move: None,
|
||||
},
|
||||
);
|
||||
assert_eq!(item.velocity.x.to_bits(), 0x3fb9_1687_3333_3334);
|
||||
@@ -4747,6 +4714,7 @@ mod item_destroyed_tests {
|
||||
.map(|(id, count)| ItemStack::new(id, *count))
|
||||
.collect();
|
||||
ItemEntity {
|
||||
random: horizon_core::random::LegacyRandom::new(0),
|
||||
rotation: [0.0, 0.0],
|
||||
join: Default::default(),
|
||||
no_gravity: false,
|
||||
@@ -4762,6 +4730,7 @@ mod item_destroyed_tests {
|
||||
on_ground: false,
|
||||
vertical_collision: false,
|
||||
horizontal_collision: false,
|
||||
fall_distance: 0.0,
|
||||
age: 0,
|
||||
pickup_delay: 0,
|
||||
bob_offset: 0.0,
|
||||
@@ -4982,6 +4951,7 @@ mod dropped_item_light_tests {
|
||||
|
||||
fn item(previous_y: f64, y: f64) -> ItemEntity {
|
||||
ItemEntity {
|
||||
random: horizon_core::random::LegacyRandom::new(0),
|
||||
rotation: [0.0, 0.0],
|
||||
join: Default::default(),
|
||||
no_gravity: false,
|
||||
@@ -5002,6 +4972,7 @@ mod dropped_item_light_tests {
|
||||
on_ground: false,
|
||||
vertical_collision: false,
|
||||
horizontal_collision: false,
|
||||
fall_distance: 0.0,
|
||||
age: 0,
|
||||
pickup_delay: 0,
|
||||
bob_offset: 0.0,
|
||||
@@ -5044,6 +5015,7 @@ mod dropped_item_light_tests {
|
||||
assert_eq!(falling.light_probe(1.0)[1].floor(), 65.0);
|
||||
// Across x and z too, not just down.
|
||||
let sliding = ItemEntity {
|
||||
random: horizon_core::random::LegacyRandom::new(0),
|
||||
position: Vec3::new(1.5, 64.0, 0.5),
|
||||
previous: Vec3::new(0.5, 64.0, 0.5),
|
||||
..item(64.0, 64.0)
|
||||
@@ -5061,6 +5033,7 @@ mod dropped_item_light_tests {
|
||||
assert_eq!(cold.render_light(light(15, 0)), (15 * 16) as u32);
|
||||
assert_eq!(cold.render_light(light(0, 9)), ((9 * 16) << 8) as u32);
|
||||
let burning = ItemEntity {
|
||||
random: horizon_core::random::LegacyRandom::new(0),
|
||||
fire_ticks: 160,
|
||||
..item(64.0, 64.0)
|
||||
};
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn death_reward_uses_levels_and_caps_the_recoverable_points() {
|
||||
for (level, reward) in [(0, 0), (1, 7), (14, 98), (15, 100), (30, 100)] {
|
||||
let xp = Experience {
|
||||
level,
|
||||
progress: 0.75,
|
||||
total: 10_000,
|
||||
};
|
||||
assert_eq!(xp.death_reward(false), reward);
|
||||
assert_eq!(xp.death_reward(true), 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn respawn_clears_all_xp_unless_it_is_kept() {
|
||||
let mut xp = Experience::default();
|
||||
xp.give(155);
|
||||
let before = (xp.level, xp.progress, xp.total);
|
||||
xp.respawn(true);
|
||||
assert_eq!((xp.level, xp.progress, xp.total), before);
|
||||
xp.respawn(false);
|
||||
assert_eq!((xp.level, xp.progress, xp.total), (0, 0.0, 0));
|
||||
xp.give(7);
|
||||
assert_eq!((xp.level, xp.progress, xp.total), (1, 0.0, 7));
|
||||
}
|
||||
@@ -338,6 +338,7 @@ mod tests {
|
||||
}
|
||||
let at = Vec3::new(20.5, 4.0, 20.5);
|
||||
let item = ItemEntity {
|
||||
random: horizon_core::random::LegacyRandom::new(0),
|
||||
rotation: [0.0, 0.0],
|
||||
join: Default::default(),
|
||||
no_gravity: false,
|
||||
@@ -358,6 +359,7 @@ mod tests {
|
||||
on_ground: false,
|
||||
vertical_collision: false,
|
||||
horizontal_collision: false,
|
||||
fall_distance: 0.0,
|
||||
age: 0,
|
||||
pickup_delay: 0,
|
||||
bob_offset: 0.0,
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
//! A dropped item's construction and its fall distance (`Entity.fallDistance`).
|
||||
use super::{ItemEntity, ItemStep};
|
||||
use crate::items::ItemStack;
|
||||
use horizon_core::geometry::Vec3;
|
||||
|
||||
impl ItemEntity {
|
||||
/// A fresh `ItemEntity` at `position` moving at `velocity`: full health,
|
||||
/// no age, delay or owner, not yet ticked.
|
||||
pub fn new(id: u32, stack: ItemStack, position: Vec3, velocity: Vec3, bob_offset: f32) -> Self {
|
||||
Self {
|
||||
rotation: [0.0, 0.0],
|
||||
join: Default::default(),
|
||||
no_gravity: false,
|
||||
health: 5,
|
||||
fire_ticks: 0,
|
||||
thrower: None,
|
||||
target: None,
|
||||
id,
|
||||
stack,
|
||||
position,
|
||||
previous: position,
|
||||
velocity,
|
||||
on_ground: false,
|
||||
vertical_collision: false,
|
||||
horizontal_collision: false,
|
||||
fall_distance: 0.0,
|
||||
age: 0,
|
||||
pickup_delay: 0,
|
||||
bob_offset,
|
||||
ticks: 0,
|
||||
needs_sync: false,
|
||||
stuck: Vec3::ZERO,
|
||||
was_touching_water: false,
|
||||
// The caller gives it the own random that drew its UUID and bobOffs.
|
||||
random: horizon_core::random::LegacyRandom::new(0),
|
||||
}
|
||||
}
|
||||
/// `Entity.move`'s fall bookkeeping after the item's move: `reset_clip`
|
||||
/// is the cast of a move of a block or more over fall-resetting blocks
|
||||
/// and water (the fall it leaves), then `checkFallDamage` adds the drop,
|
||||
/// narrowed to a float, outside water and ends the fall on the ground.
|
||||
pub fn check_fall(&mut self, step: &ItemStep, reset_clip: impl FnOnce(f64, Vec3, Vec3) -> f64) {
|
||||
let Some((from, moved, applied)) = step.fall_move else {
|
||||
return;
|
||||
};
|
||||
if applied {
|
||||
self.fall_distance = reset_clip(self.fall_distance, from, moved);
|
||||
}
|
||||
if !self.was_touching_water && moved.y < 0.0 {
|
||||
self.fall_distance -= f64::from(moved.y as f32);
|
||||
}
|
||||
if self.on_ground {
|
||||
self.fall_distance = 0.0;
|
||||
}
|
||||
}
|
||||
/// `Entity.baseTick`: lava halves the fall each tick.
|
||||
pub fn lava_fall(&mut self, in_lava: bool) {
|
||||
if in_lava {
|
||||
self.fall_distance *= 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,8 @@ use horizon_world::{
|
||||
registry::{BlockRegistry, BlockStateId},
|
||||
};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
mod projectile_fields;
|
||||
use projectile_fields::{load_projectile_entity_fields, put_projectile_entity_fields};
|
||||
mod saved_attributes;
|
||||
|
||||
/// Saved data carried by `ConversionType.convertCommon` outside the modelled
|
||||
@@ -1362,14 +1364,7 @@ pub fn mob_to_nbt(
|
||||
map.insert("StunTick".into(), Tag::Int(m.extra.counter2));
|
||||
map.insert("RoarTick".into(), Tag::Int(m.extra.anim_ticks));
|
||||
}
|
||||
"minecraft:vex" => {
|
||||
if let Some(h) = m.extra.home {
|
||||
map.insert("bound_pos".into(), Tag::IntArray(h.to_vec()));
|
||||
}
|
||||
if m.extra.egg_time > 0 {
|
||||
map.insert("life_ticks".into(), Tag::Int(m.extra.egg_time));
|
||||
}
|
||||
}
|
||||
"minecraft:vex" => crate::mobs::vex::write_nbt(m, map, &mut uuid_of),
|
||||
"minecraft:endermite" => {
|
||||
map.insert("Lifetime".into(), Tag::Int(m.extra.counter));
|
||||
}
|
||||
@@ -1562,11 +1557,9 @@ pub fn mob_to_nbt(
|
||||
.iter_mut()
|
||||
.find(|t| text(t.get("id")).as_deref() == Some(attr_id.as_str()))
|
||||
{
|
||||
// The model owns these bases: a ravager's speed, a wolf's health.
|
||||
if matches!(
|
||||
(species, attr),
|
||||
(S::Ravager, Attr::MovementSpeed) | (S::Wolf, Attr::MaxHealth)
|
||||
) && let Tag::Compound(c) = existing
|
||||
// The model owns a wolf's health base.
|
||||
if (species, attr) == (S::Wolf, Attr::MaxHealth)
|
||||
&& let Tag::Compound(c) = existing
|
||||
{
|
||||
c.insert("base".into(), Tag::Double(base.to_bits()));
|
||||
}
|
||||
@@ -2642,22 +2635,12 @@ pub fn mob_from_nbt_with_random(
|
||||
m.extra.counter = number(tag.get("AttackTick")).unwrap_or(0) as i32;
|
||||
m.extra.counter2 = number(tag.get("StunTick")).unwrap_or(0) as i32;
|
||||
m.extra.anim_ticks = number(tag.get("RoarTick")).unwrap_or(0) as i32;
|
||||
if crate::mobs::ravager::immobile(&m)
|
||||
&& !matches!(tag.get("attributes"), Some(Tag::List(l)) if l.iter().any(|a| text(a.get("id")).as_deref() == Some("minecraft:movement_speed")))
|
||||
{
|
||||
m.attrs.speed = 0.0;
|
||||
}
|
||||
}
|
||||
if m.species() == crate::mobs::Species::Endermite {
|
||||
m.extra.counter = number(tag.get("Lifetime")).unwrap_or(0) as i32;
|
||||
}
|
||||
if m.species() == crate::mobs::Species::Vex {
|
||||
if let Some(Tag::IntArray(v)) = tag.get("bound_pos")
|
||||
&& v.len() == 3
|
||||
{
|
||||
m.extra.home = Some([v[0], v[1], v[2]]);
|
||||
}
|
||||
m.extra.egg_time = number(tag.get("life_ticks")).map_or(-1, |v| v as i32);
|
||||
crate::mobs::vex::read_nbt(&mut m, tag);
|
||||
}
|
||||
// Bogged.readAdditionalSaveData: unsheared when absent.
|
||||
if m.species() == crate::mobs::Species::Bogged {
|
||||
@@ -2822,7 +2805,7 @@ fn item_entity_to_nbt_saved(e: &ItemEntity, uuid: [i32; 4], raw: Option<&Tag>, i
|
||||
motion: e.velocity,
|
||||
rotation: e.rotation,
|
||||
on_ground: e.on_ground,
|
||||
fall_distance: 0.0,
|
||||
fall_distance: e.fall_distance,
|
||||
};
|
||||
let mut tag = base.non_mob_tag(raw);
|
||||
for (key, value) in [
|
||||
@@ -2998,6 +2981,8 @@ pub fn item_entity_from_nbt(tag: &Tag, id: u32, bob_offset: f32) -> Option<ItemE
|
||||
on_ground: flag(tag, "OnGround"),
|
||||
vertical_collision: false,
|
||||
horizontal_collision: false,
|
||||
// Entity.load: getDoubleOr, any number widened.
|
||||
fall_distance: float(tag.get("fall_distance")).unwrap_or(0.0),
|
||||
age: number(tag.get("Age")).unwrap_or(0) as i32,
|
||||
pickup_delay: number(tag.get("PickupDelay")).unwrap_or(0),
|
||||
bob_offset,
|
||||
@@ -3005,6 +2990,8 @@ pub fn item_entity_from_nbt(tag: &Tag, id: u32, bob_offset: f32) -> Option<ItemE
|
||||
needs_sync: false,
|
||||
stuck: Vec3::ZERO,
|
||||
was_touching_water: false,
|
||||
// The caller gives it the own random that drew its UUID and bobOffs.
|
||||
random: horizon_core::random::LegacyRandom::new(0),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3285,7 +3272,7 @@ pub fn fireball_to_nbt(
|
||||
motion: p.velocity,
|
||||
rotation: [p.yaw, p.pitch],
|
||||
on_ground: p.on_ground,
|
||||
fall_distance: 0.0,
|
||||
fall_distance: p.fall_distance,
|
||||
};
|
||||
let mut tag = base.tag(raw);
|
||||
put(
|
||||
@@ -3312,27 +3299,6 @@ pub fn fireball_to_nbt(
|
||||
Some(tag)
|
||||
}
|
||||
|
||||
/// `Entity.load`: fire is read as a short, air as an int and the visual
|
||||
/// fire flag as a byte-valued boolean. Portal state lives on the level.
|
||||
fn load_projectile_entity_fields(p: &mut Projectile, tag: &Tag) {
|
||||
p.fire_ticks = i32::from(tag.get("Fire").and_then(int_value).unwrap_or(0) as i16);
|
||||
p.air_supply = tag.get("Air").and_then(int_value).unwrap_or(p.air_supply);
|
||||
p.has_visual_fire = flag(tag, "HasVisualFire");
|
||||
}
|
||||
|
||||
/// `Entity.saveWithoutId`: save the current fire and air as shorts, and
|
||||
/// only write the optional visual fire flag when it is true.
|
||||
fn put_projectile_entity_fields(tag: &mut Tag, p: &Projectile) {
|
||||
put(tag, "Fire", Tag::Short(p.fire_ticks as i16));
|
||||
put(tag, "Air", Tag::Short(p.air_supply as i16));
|
||||
if let Tag::Compound(map) = tag {
|
||||
map.remove(&"HasVisualFire".into());
|
||||
}
|
||||
if p.has_visual_fire {
|
||||
put(tag, "HasVisualFire", Tag::Byte(1));
|
||||
}
|
||||
}
|
||||
|
||||
/// `Projectile.addAdditionalSaveData`: Owner, LeftOwner only when set, and
|
||||
/// the explicit HasBeenShot flag.
|
||||
fn put_projectile(tag: &mut Tag, p: &Projectile, owner: Option<[i32; 4]>) {
|
||||
@@ -3436,7 +3402,7 @@ pub fn throwable_to_nbt(
|
||||
motion: p.velocity,
|
||||
rotation: [p.yaw, p.pitch],
|
||||
on_ground: p.on_ground,
|
||||
fall_distance: 0.0,
|
||||
fall_distance: p.fall_distance,
|
||||
};
|
||||
let mut tag = base.tag(raw);
|
||||
put_projectile(&mut tag, p, owner);
|
||||
@@ -3622,6 +3588,7 @@ pub fn fireball_from_nbt(tag: &Tag, player: Option<[i32; 4]>) -> Option<Projecti
|
||||
load_projectile_entity_fields(&mut p, tag);
|
||||
}
|
||||
p.on_ground = flag(tag, "OnGround");
|
||||
p.fall_distance = float(tag.get("fall_distance")).unwrap_or(0.0);
|
||||
p.acceleration = float(tag.get("acceleration_power")).unwrap_or(0.1);
|
||||
if let Some(item) = tag.get("Item").and_then(item_from_nbt) {
|
||||
p.set_item(item);
|
||||
@@ -4541,74 +4508,7 @@ mod tnt_power_tests {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod spell_ticks_nbt_tests {
|
||||
use super::*;
|
||||
use crate::mobs::Species;
|
||||
|
||||
fn caster(species: Species, name: &str) -> Mob {
|
||||
let mut kind = crate::mobs::sensor_scan_tests::kind();
|
||||
let ty = Arc::make_mut(&mut kind);
|
||||
ty.species = species;
|
||||
ty.id = format!("minecraft:{name}");
|
||||
Mob::new(1, kind, Vec3::new(0.5, 64.0, 0.5), 0.0)
|
||||
}
|
||||
|
||||
/// `SpellcasterIllager.readAdditionalSaveData`: bind loaded values to the
|
||||
/// counter already used by casting goals and customServerAiStep.
|
||||
#[test]
|
||||
fn spell_ticks_load_into_the_live_counter() {
|
||||
for (species, name) in [
|
||||
(Species::Evoker, "evoker"),
|
||||
(Species::Illusioner, "illusioner"),
|
||||
] {
|
||||
let m = caster(species, name);
|
||||
let types = HashMap::from([(m.kind.id.clone(), m.kind.clone())]);
|
||||
for value in [None, Some(30), Some(0), Some(-2)] {
|
||||
let mut raw = Tag::compound([
|
||||
("id", string(&m.kind.id)),
|
||||
("Pos", doubles(&[0.5, 64.0, 0.5])),
|
||||
]);
|
||||
if let Some(value) = value {
|
||||
put(&mut raw, "SpellTicks", Tag::Int(value));
|
||||
}
|
||||
let loaded = mob_from_nbt(&raw, &types, 2, None).unwrap();
|
||||
assert_eq!(
|
||||
loaded.extra.counter,
|
||||
value.unwrap_or(0),
|
||||
"{name}: {value:?}"
|
||||
);
|
||||
let saved = mob_to_nbt(&loaded, [1, 2, 3, 4], None, None, |_| None);
|
||||
assert_eq!(saved.get("SpellTicks"), Some(&Tag::Int(value.unwrap_or(0))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `SpellcasterIllager.addAdditionalSaveData`: live state wins even when
|
||||
/// no raw input exists or the original raw counter has become stale.
|
||||
#[test]
|
||||
fn spell_ticks_save_the_mutated_model_without_a_raw_fallback() {
|
||||
for (species, name) in [
|
||||
(Species::Evoker, "evoker"),
|
||||
(Species::Illusioner, "illusioner"),
|
||||
] {
|
||||
let mut m = caster(species, name);
|
||||
let raw = Tag::compound([
|
||||
("SpellTicks", Tag::Int(30)),
|
||||
("audit:unrelated", Tag::Int(7)),
|
||||
]);
|
||||
for value in [29, 27, 20, 0, -2] {
|
||||
m.extra.counter = value;
|
||||
for raw in [None, Some(&raw)] {
|
||||
let saved = mob_to_nbt(&m, [1, 2, 3, 4], raw, None, |_| None);
|
||||
assert_eq!(saved.get("SpellTicks"), Some(&Tag::Int(value)), "{name}");
|
||||
if raw.is_some() {
|
||||
assert_eq!(saved.get("audit:unrelated"), Some(&Tag::Int(7)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mod spell_ticks_nbt_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
@@ -5721,33 +5621,25 @@ mod tests {
|
||||
#[test]
|
||||
fn item_entities_round_trip() {
|
||||
let e = ItemEntity {
|
||||
rotation: [0.0, 0.0],
|
||||
join: Default::default(),
|
||||
no_gravity: false,
|
||||
health: 2,
|
||||
fire_ticks: 73,
|
||||
thrower: Some([5, 6, 7, 8]),
|
||||
target: None,
|
||||
id: 7,
|
||||
stack: ItemStack {
|
||||
item: Arc::from("minecraft:diamond_pickaxe"),
|
||||
count: 1,
|
||||
damage: 5,
|
||||
extra: None,
|
||||
},
|
||||
position: Vec3::new(100.5, 64.0, -31.5),
|
||||
previous: Vec3::ZERO,
|
||||
velocity: Vec3::new(0.0, -0.04, 0.0),
|
||||
on_ground: true,
|
||||
vertical_collision: false,
|
||||
horizontal_collision: false,
|
||||
age: 120,
|
||||
pickup_delay: 10,
|
||||
ticks: 0,
|
||||
bob_offset: 0.0,
|
||||
needs_sync: false,
|
||||
stuck: Vec3::ZERO,
|
||||
was_touching_water: false,
|
||||
..ItemEntity::new(
|
||||
7,
|
||||
ItemStack {
|
||||
item: Arc::from("minecraft:diamond_pickaxe"),
|
||||
count: 1,
|
||||
damage: 5,
|
||||
extra: None,
|
||||
},
|
||||
Vec3::new(100.5, 64.0, -31.5),
|
||||
Vec3::new(0.0, -0.04, 0.0),
|
||||
0.0,
|
||||
)
|
||||
};
|
||||
let tag = item_entity_to_nbt(&e, [1, 2, 3, 4], None);
|
||||
assert_eq!(kind(&tag).as_deref(), Some("minecraft:item"));
|
||||
@@ -10511,25 +10403,21 @@ mod equine_temper_preservation_tests {
|
||||
);
|
||||
Arc::make_mut(&mut kind).species = Species::Ravager;
|
||||
let types = [("minecraft:ravager".into(), kind)].into();
|
||||
let mut ravager = mob_from_nbt(
|
||||
&Tag::compound([
|
||||
("id", string("minecraft:ravager")),
|
||||
("Pos", doubles(&[0.0, 0.0, 0.0])),
|
||||
("RoarTick", Tag::Int(10)),
|
||||
]),
|
||||
&types,
|
||||
2,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
ravager.age = 1;
|
||||
let saved = mob_to_nbt(&ravager, [0; 4], None, None, |_| None);
|
||||
assert_eq!(
|
||||
saved.get("attributes"),
|
||||
Some(&Tag::List(vec![Tag::compound([
|
||||
("id", string("minecraft:movement_speed")),
|
||||
("base", Tag::Double(0.0f64.to_bits()))
|
||||
])]))
|
||||
);
|
||||
// Before its first aiStep a roaring ravager has no movement_speed
|
||||
// instance; a saved base (mid-ramp after a roar) loads back as is.
|
||||
let speed = Tag::List(vec![Tag::compound([
|
||||
("id", string("minecraft:movement_speed")),
|
||||
("base", Tag::Double(0.10317f64.to_bits())),
|
||||
])]);
|
||||
for (key, value, saved) in [
|
||||
("RoarTick", Tag::Int(10), Tag::List(vec![])),
|
||||
("attributes", speed.clone(), speed),
|
||||
] {
|
||||
let pos = ("Pos", doubles(&[0.0, 0.0, 0.0]));
|
||||
let tag = Tag::compound([("id", string("minecraft:ravager")), pos, (key, value)]);
|
||||
let ravager = mob_from_nbt(&tag, &types, 2, None).unwrap();
|
||||
let nbt = mob_to_nbt(&ravager, [0; 4], None, None, |_| None);
|
||||
assert_eq!(nbt.get("attributes"), Some(&saved));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
//! The `Entity` fields a projectile keeps beside `Projectile`'s own: fire,
|
||||
//! air, the visual fire flag and the fall distance.
|
||||
use super::{flag, float, int_value, put};
|
||||
use crate::projectiles::Projectile;
|
||||
use horizon_world::nbt::Tag;
|
||||
|
||||
/// `Entity.load`: fire is read as a short, air as an int and the visual
|
||||
/// fire flag as a byte-valued boolean. Portal state lives on the level.
|
||||
pub(super) fn load_projectile_entity_fields(p: &mut Projectile, tag: &Tag) {
|
||||
p.fire_ticks = i32::from(tag.get("Fire").and_then(int_value).unwrap_or(0) as i16);
|
||||
p.air_supply = tag.get("Air").and_then(int_value).unwrap_or(p.air_supply);
|
||||
p.has_visual_fire = flag(tag, "HasVisualFire");
|
||||
// Entity.load: getDoubleOr, any number widened.
|
||||
p.fall_distance = float(tag.get("fall_distance")).unwrap_or(0.0);
|
||||
}
|
||||
|
||||
/// `Entity.saveWithoutId`: save the current fire and air as shorts, and
|
||||
/// only write the optional visual fire flag when it is true.
|
||||
pub(super) fn put_projectile_entity_fields(tag: &mut Tag, p: &Projectile) {
|
||||
put(tag, "Fire", Tag::Short(p.fire_ticks as i16));
|
||||
put(tag, "Air", Tag::Short(p.air_supply as i16));
|
||||
if let Tag::Compound(map) = tag {
|
||||
map.remove(&"HasVisualFire".into());
|
||||
}
|
||||
if p.has_visual_fire {
|
||||
put(tag, "HasVisualFire", Tag::Byte(1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
use super::*;
|
||||
use crate::mobs::Species;
|
||||
|
||||
fn caster(species: Species, name: &str) -> Mob {
|
||||
let mut kind = crate::mobs::sensor_scan_tests::kind();
|
||||
let ty = Arc::make_mut(&mut kind);
|
||||
ty.species = species;
|
||||
ty.id = format!("minecraft:{name}");
|
||||
Mob::new(1, kind, Vec3::new(0.5, 64.0, 0.5), 0.0)
|
||||
}
|
||||
|
||||
/// `SpellcasterIllager.readAdditionalSaveData`: bind loaded values to the
|
||||
/// counter already used by casting goals and customServerAiStep.
|
||||
#[test]
|
||||
fn spell_ticks_load_into_the_live_counter() {
|
||||
for (species, name) in [
|
||||
(Species::Evoker, "evoker"),
|
||||
(Species::Illusioner, "illusioner"),
|
||||
] {
|
||||
let m = caster(species, name);
|
||||
let types = HashMap::from([(m.kind.id.clone(), m.kind.clone())]);
|
||||
for value in [None, Some(30), Some(0), Some(-2)] {
|
||||
let mut raw = Tag::compound([
|
||||
("id", string(&m.kind.id)),
|
||||
("Pos", doubles(&[0.5, 64.0, 0.5])),
|
||||
]);
|
||||
if let Some(value) = value {
|
||||
put(&mut raw, "SpellTicks", Tag::Int(value));
|
||||
}
|
||||
let loaded = mob_from_nbt(&raw, &types, 2, None).unwrap();
|
||||
assert_eq!(
|
||||
loaded.extra.counter,
|
||||
value.unwrap_or(0),
|
||||
"{name}: {value:?}"
|
||||
);
|
||||
let saved = mob_to_nbt(&loaded, [1, 2, 3, 4], None, None, |_| None);
|
||||
assert_eq!(saved.get("SpellTicks"), Some(&Tag::Int(value.unwrap_or(0))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `SpellcasterIllager.addAdditionalSaveData`: live state wins even when
|
||||
/// no raw input exists or the original raw counter has become stale.
|
||||
#[test]
|
||||
fn spell_ticks_save_the_mutated_model_without_a_raw_fallback() {
|
||||
for (species, name) in [
|
||||
(Species::Evoker, "evoker"),
|
||||
(Species::Illusioner, "illusioner"),
|
||||
] {
|
||||
let mut m = caster(species, name);
|
||||
let raw = Tag::compound([
|
||||
("SpellTicks", Tag::Int(30)),
|
||||
("audit:unrelated", Tag::Int(7)),
|
||||
]);
|
||||
for value in [29, 27, 20, 0, -2] {
|
||||
m.extra.counter = value;
|
||||
for raw in [None, Some(&raw)] {
|
||||
let saved = mob_to_nbt(&m, [1, 2, 3, 4], raw, None, |_| None);
|
||||
assert_eq!(saved.get("SpellTicks"), Some(&Tag::Int(value)), "{name}");
|
||||
if raw.is_some() {
|
||||
assert_eq!(saved.get("audit:unrelated"), Some(&Tag::Int(7)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3893,8 +3893,7 @@ impl Game<'_> {
|
||||
velocity: Option<[f64; 3]>,
|
||||
pickup_delay: u32,
|
||||
) -> [f64; 3] {
|
||||
let item = stack.item.clone();
|
||||
let count = stack.count as i32;
|
||||
let (item, count) = (stack.item.clone(), stack.count as i32);
|
||||
self.next_entity += 1;
|
||||
let p = Vec3::new(position[0], position[1], position[2]);
|
||||
// The item's own random, in vanilla's order: Entity's UUID, ItemEntity's
|
||||
@@ -3910,27 +3909,15 @@ impl Game<'_> {
|
||||
});
|
||||
self.dropped.push(ItemEntity {
|
||||
rotation: [yaw, 0.0],
|
||||
join: Default::default(),
|
||||
no_gravity: false,
|
||||
health: 5,
|
||||
fire_ticks: 0,
|
||||
thrower: None,
|
||||
target: None,
|
||||
id: self.next_entity,
|
||||
stack,
|
||||
position: p,
|
||||
previous: p,
|
||||
velocity: Vec3::new(velocity[0], velocity[1], velocity[2]),
|
||||
on_ground: false,
|
||||
vertical_collision: false,
|
||||
horizontal_collision: false,
|
||||
age: 0,
|
||||
pickup_delay: i64::from(pickup_delay),
|
||||
bob_offset: bob,
|
||||
ticks: 0,
|
||||
needs_sync: false,
|
||||
stuck: Vec3::ZERO,
|
||||
was_touching_water: false,
|
||||
random: own,
|
||||
..ItemEntity::new(
|
||||
self.next_entity,
|
||||
stack,
|
||||
p,
|
||||
Vec3::new(velocity[0], velocity[1], velocity[2]),
|
||||
bob,
|
||||
)
|
||||
});
|
||||
// ServerLevel.addFreshEntity.
|
||||
if self.native_joins_flush(false, self.next_entity) {
|
||||
@@ -5583,20 +5570,10 @@ impl Game<'_> {
|
||||
}
|
||||
self.trigger_bee_nest_destroyed(broken, held.as_ref(), bees);
|
||||
}
|
||||
// InfestedBlock.spawnAfterBreak: its silverfish comes out unless the
|
||||
// tool has silk touch (#prevents_infested_spawns) or blocks don't drop.
|
||||
if facts.name.starts_with("minecraft:infested_")
|
||||
&& !silk
|
||||
&& self.ui.rules.bool("block_drops")
|
||||
{
|
||||
let at = [
|
||||
f64::from(pos[0]) + 0.5,
|
||||
f64::from(pos[1]),
|
||||
f64::from(pos[2]) + 0.5,
|
||||
];
|
||||
if let Some(id) = self.spawn_mob_raw("minecraft:silverfish", at, false) {
|
||||
self.mob_poof(id);
|
||||
}
|
||||
// InfestedBlock.spawnAfterBreak, unless the tool has silk touch
|
||||
// (#prevents_infested_spawns).
|
||||
if facts.name.starts_with("minecraft:infested_") && !silk {
|
||||
self.spawn_infestation(pos);
|
||||
}
|
||||
// Block.tryDropExperience samples before Silk Touch removes the XP.
|
||||
// SpawnerBlock.spawnAfterBreak ignores Silk Touch. Both sample even
|
||||
|
||||
@@ -677,7 +677,7 @@ impl Game<'_> {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
/// `Entity.move`'s fall reset for a vehicle that keeps a fall and moves on
|
||||
/// `Entity.move`'s fall reset for an entity that keeps a fall and moves on
|
||||
/// by `moved` from `from`: a move of a block or more casts up to 8 blocks
|
||||
/// of it (`ClipContext.Block.FALLDAMAGE_RESETTING`, `ClipContext.Fluid.WATER`)
|
||||
/// and a hit resets the fall, before the move's own drop counts.
|
||||
|
||||
@@ -7,8 +7,15 @@ use std::sync::{Arc, Mutex};
|
||||
|
||||
/// Exercise the actual Game keyboard/mouse handlers and generated mod dispatch.
|
||||
#[test]
|
||||
#[ignore = "needs prepared resources and a fresh HORIZON_HOME; run in isolation"]
|
||||
#[ignore = "needs prepared resources and a fresh HORIZON_HOME"]
|
||||
fn chat_completion_reaches_mods_from_keyboard_and_mouse() {
|
||||
crate::mod_api::in_own_process(
|
||||
"game::chat::suggestion_completion_tests::chat_completion_reaches_mods_from_keyboard_and_mouse",
|
||||
chat_completion_reaches_mods_from_keyboard_and_mouse_in_own_process,
|
||||
);
|
||||
}
|
||||
|
||||
fn chat_completion_reaches_mods_from_keyboard_and_mouse_in_own_process() {
|
||||
let root = PathBuf::from(std::env::var_os("HORIZON_TEST_RESOURCES").unwrap());
|
||||
let scratch = PathBuf::from(std::env::var_os("HORIZON_HOME").unwrap()).join("suggestion-game");
|
||||
std::fs::create_dir_all(&scratch).unwrap();
|
||||
|
||||
@@ -471,9 +471,10 @@ impl Game<'_> {
|
||||
Target::Item(id) => {
|
||||
let new = entity_nbt::item_entity_from_nbt(&tag, id, 0.0).ok_or_else(stale)?;
|
||||
if let Some(slot) = self.dropped.iter_mut().find(|e| e.id == id) {
|
||||
let bob = slot.bob_offset;
|
||||
*slot = new;
|
||||
slot.bob_offset = bob;
|
||||
// `/data` loads into the same entity: its bobOffs and own random stay.
|
||||
let kept = std::mem::replace(slot, new);
|
||||
slot.bob_offset = kept.bob_offset;
|
||||
slot.random = kept.random;
|
||||
}
|
||||
}
|
||||
Target::Fangs(id) => {
|
||||
|
||||
@@ -13,6 +13,7 @@ use std::sync::Arc;
|
||||
|
||||
fn item(id: u32, at: Vec3) -> ItemEntity {
|
||||
ItemEntity {
|
||||
random: horizon_core::random::LegacyRandom::new(0),
|
||||
rotation: [0.0, 0.0],
|
||||
join: Default::default(),
|
||||
no_gravity: true,
|
||||
@@ -33,6 +34,7 @@ fn item(id: u32, at: Vec3) -> ItemEntity {
|
||||
on_ground: false,
|
||||
vertical_collision: false,
|
||||
horizontal_collision: false,
|
||||
fall_distance: 0.0,
|
||||
age: 97,
|
||||
pickup_delay: 0,
|
||||
bob_offset: 0.0,
|
||||
|
||||
@@ -592,6 +592,7 @@ impl Game<'_> {
|
||||
let heights = item.push_by_fluid(&self.world, &self.res.motion().fluids);
|
||||
// Entity.baseTick splashes before moving, except on the first tick.
|
||||
item.water_entry(heights.0 > 0.0, item.ticks == 0, splash);
|
||||
item.lava_fall(heights.1 > 0.0);
|
||||
let step = item.tick_move_before_block_effects(
|
||||
&self.world,
|
||||
self.res.facts(),
|
||||
@@ -603,6 +604,9 @@ impl Game<'_> {
|
||||
Some(self.border.geometry()),
|
||||
Some(&self.res.motion().blocks),
|
||||
);
|
||||
item.check_fall(&step, |fall, from, moved| {
|
||||
self.vehicle_fall_reset(fall, from, moved)
|
||||
});
|
||||
if let Some(pos) = step.vertical_contact {
|
||||
let state = self.world.block(pos.x, pos.y, pos.z);
|
||||
let position = item.position;
|
||||
|
||||
@@ -303,6 +303,43 @@ impl Game<'_> {
|
||||
}
|
||||
hits as f32 / count as f32
|
||||
}
|
||||
/// What a ray's step through the block at `pos` takes from its power
|
||||
/// (`ExplosionDamageCalculator.getBlockExplosionResistance`), and whether
|
||||
/// the block is spared.
|
||||
fn blast_step<S: std::hash::BuildHasher>(
|
||||
&self,
|
||||
pos: [i32; 3],
|
||||
resists: Option<([i32; 3], f32)>,
|
||||
cap: Option<f32>,
|
||||
wither_immune: Option<&std::collections::HashSet<String, S>>,
|
||||
) -> (f32, bool) {
|
||||
let state = self.world.block(pos[0], pos[1], pos[2]);
|
||||
let f = self.res.facts().get(state);
|
||||
let fluid = if f.fluid != Fluid::None {
|
||||
100.0f32
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
// A primed TNT minecart's rails and the blocks they
|
||||
// stand on: no resistance, and never blown up.
|
||||
let spared = self.combat.rail_proof_blast
|
||||
&& (self.rail(pos[0], pos[1], pos[2]).is_some()
|
||||
|| self.rail(pos[0], pos[1] + 1, pos[2]).is_some());
|
||||
let taken = match resists {
|
||||
Some((at, resistance)) if at == pos => (resistance + 0.3) * 0.3,
|
||||
_ if !self.res.facts().is_air(state) || f.fluid != Fluid::None => {
|
||||
let base = if spared {
|
||||
0.0
|
||||
} else {
|
||||
f.explosion_resistance.max(fluid)
|
||||
};
|
||||
let immune = wither_immune.is_some_and(|t| t.contains(&*f.name));
|
||||
(blast_resistance(base, cap, immune) + 0.3) * 0.3
|
||||
}
|
||||
_ => 0.0,
|
||||
};
|
||||
(taken, spared)
|
||||
}
|
||||
/// `ServerExplosion.calculateExplodedPositions`; `resists` overrides one
|
||||
/// block's resistance (`ExplosionDamageCalculator.getBlockExplosionResistance`).
|
||||
fn exploded_positions(
|
||||
@@ -341,6 +378,11 @@ impl Game<'_> {
|
||||
zd /= d;
|
||||
let mut power = radius * (0.7 + self.world.random.next_float() * 0.6);
|
||||
let (mut xp, mut yp, mut zp) = (center[0], center[1], center[2]);
|
||||
// A ray's steps are 0.3 long, about three to a block: a
|
||||
// step in the block the last one was in takes what that
|
||||
// one took (the level does not change while rays cast),
|
||||
// and that block is in `set` already if it is to be.
|
||||
let mut last: Option<([i32; 3], f32, bool)> = None;
|
||||
while power > 0.0 {
|
||||
let pos = [xp.floor() as i32, yp.floor() as i32, zp.floor() as i32];
|
||||
if pos[1] < min_y
|
||||
@@ -350,35 +392,17 @@ impl Game<'_> {
|
||||
{
|
||||
break;
|
||||
}
|
||||
let state = self.world.block(pos[0], pos[1], pos[2]);
|
||||
let f = self.res.facts().get(state);
|
||||
let fluid = if f.fluid != Fluid::None {
|
||||
100.0f32
|
||||
} else {
|
||||
0.0
|
||||
let (taken, spared, again) = match last {
|
||||
Some((at, taken, spared)) if at == pos => (taken, spared, true),
|
||||
_ => {
|
||||
let (taken, spared) =
|
||||
self.blast_step(pos, resists, cap, wither_immune.as_ref());
|
||||
last = Some((pos, taken, spared));
|
||||
(taken, spared, false)
|
||||
}
|
||||
};
|
||||
// A primed TNT minecart's rails and the blocks they
|
||||
// stand on: no resistance, and never blown up.
|
||||
let spared = self.combat.rail_proof_blast
|
||||
&& (self.rail(pos[0], pos[1], pos[2]).is_some()
|
||||
|| self.rail(pos[0], pos[1] + 1, pos[2]).is_some());
|
||||
match resists {
|
||||
Some((at, resistance)) if at == pos => {
|
||||
power -= (resistance + 0.3) * 0.3;
|
||||
}
|
||||
_ if !self.res.facts().is_air(state) || f.fluid != Fluid::None => {
|
||||
let base = if spared {
|
||||
0.0
|
||||
} else {
|
||||
f.explosion_resistance.max(fluid)
|
||||
};
|
||||
let immune =
|
||||
wither_immune.as_ref().is_some_and(|t| t.contains(&*f.name));
|
||||
power -= (blast_resistance(base, cap, immune) + 0.3) * 0.3;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if power > 0.0 && !spared {
|
||||
power -= taken;
|
||||
if power > 0.0 && !spared && !again {
|
||||
set.put(Key(pos), ());
|
||||
}
|
||||
xp += xd * f64::from(0.3f32);
|
||||
@@ -1047,6 +1071,22 @@ impl Game<'_> {
|
||||
self.lan_explode(center, small, radius, block_count, pushed);
|
||||
Ok(())
|
||||
}
|
||||
/// `InfestedBlock.spawnAfterBreak` without a tool that prevents it: while
|
||||
/// blocks drop, its silverfish (`spawnInfestation`) comes out at the
|
||||
/// bottom centre of the block, with its spawn animation.
|
||||
pub(super) fn spawn_infestation(&mut self, pos: [i32; 3]) {
|
||||
if !self.ui.rules.bool("block_drops") {
|
||||
return;
|
||||
}
|
||||
let at = [
|
||||
f64::from(pos[0]) + 0.5,
|
||||
f64::from(pos[1]),
|
||||
f64::from(pos[2]) + 0.5,
|
||||
];
|
||||
if let Some(id) = self.spawn_mob_raw("minecraft:silverfish", at, false) {
|
||||
self.mob_poof(id);
|
||||
}
|
||||
}
|
||||
/// `BlockBehaviour.onExplosionHit` (TNT: `wasExploded` primes a short fuse).
|
||||
fn explosion_hit_block(
|
||||
&mut self,
|
||||
@@ -1091,6 +1131,10 @@ impl Game<'_> {
|
||||
} else if let Some(tag) = super::spill::saved_entity(&self.world, pos) {
|
||||
self.pending.record_saved(pos, &tag);
|
||||
}
|
||||
// Block.onExplosionHit runs spawnAfterBreak before the drops.
|
||||
if !is_tnt && name.starts_with("minecraft:infested_") {
|
||||
self.spawn_infestation(pos);
|
||||
}
|
||||
if !is_tnt {
|
||||
let dynamic = self.pending.dynamic(pos);
|
||||
// LootContextParams.EXPLOSION_RADIUS under DESTROY_WITH_DECAY: the
|
||||
@@ -1260,6 +1304,107 @@ fn blast_resistance(base: f32, cap: Option<f32>, wither_immune: bool) -> f32 {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// `exploded_positions` before rays reused their last block's lookup:
|
||||
/// every step looked its block up.
|
||||
fn exploded_positions_stepwise(
|
||||
game: &mut Game<'_>,
|
||||
center: [f64; 3],
|
||||
radius: f32,
|
||||
resists: Option<([i32; 3], f32)>,
|
||||
) -> Vec<[i32; 3]> {
|
||||
let (min_y, max_y) = game.with_level(|_, level, _| {
|
||||
use horizon_world::level::LevelAccess;
|
||||
(level.min_y(), level.max_y())
|
||||
});
|
||||
let mut set: JavaHashMap<Key, ()> = JavaHashMap::default();
|
||||
for xx in 0..16 {
|
||||
for yy in 0..16 {
|
||||
for zz in 0..16 {
|
||||
if !(xx == 0 || xx == 15 || yy == 0 || yy == 15 || zz == 0 || zz == 15) {
|
||||
continue;
|
||||
}
|
||||
let mut xd = f64::from(xx as f32 / 15.0 * 2.0 - 1.0);
|
||||
let mut yd = f64::from(yy as f32 / 15.0 * 2.0 - 1.0);
|
||||
let mut zd = f64::from(zz as f32 / 15.0 * 2.0 - 1.0);
|
||||
let d = (xd * xd + yd * yd + zd * zd).sqrt();
|
||||
(xd, yd, zd) = (xd / d, yd / d, zd / d);
|
||||
let mut power = radius * (0.7 + game.world.random.next_float() * 0.6);
|
||||
let (mut xp, mut yp, mut zp) = (center[0], center[1], center[2]);
|
||||
while power > 0.0 {
|
||||
let pos = [xp.floor() as i32, yp.floor() as i32, zp.floor() as i32];
|
||||
if pos[1] < min_y || pos[1] >= max_y {
|
||||
break;
|
||||
}
|
||||
let state = game.world.block(pos[0], pos[1], pos[2]);
|
||||
let f = game.res.facts().get(state);
|
||||
let fluid = if f.fluid != Fluid::None {
|
||||
100.0f32
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
match resists {
|
||||
Some((at, resistance)) if at == pos => {
|
||||
power -= (resistance + 0.3) * 0.3;
|
||||
}
|
||||
_ if !game.res.facts().is_air(state) || f.fluid != Fluid::None => {
|
||||
let base = f.explosion_resistance.max(fluid);
|
||||
power -= (blast_resistance(base, None, false) + 0.3) * 0.3;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if power > 0.0 {
|
||||
set.put(Key(pos), ());
|
||||
}
|
||||
xp += xd * f64::from(0.3f32);
|
||||
yp += yd * f64::from(0.3f32);
|
||||
zp += zd * f64::from(0.3f32);
|
||||
power -= 0.225_000_01;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
set.entries().map(|(k, _)| k.0).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "needs prepared resources in HORIZON_TEST_RESOURCES"]
|
||||
fn rays_reusing_their_last_block_explode_what_each_step_did() {
|
||||
let mut game = crate::game::native_machines::tests::server_game();
|
||||
let registry = &game.pack.assets.registry;
|
||||
let states: Vec<BlockStateId> = [
|
||||
"minecraft:stone",
|
||||
"minecraft:air",
|
||||
"minecraft:water",
|
||||
"minecraft:dirt",
|
||||
"minecraft:obsidian",
|
||||
"minecraft:air",
|
||||
"minecraft:oak_planks",
|
||||
"minecraft:glass",
|
||||
]
|
||||
.iter()
|
||||
.map(|n| registry.default_state(n).unwrap())
|
||||
.collect();
|
||||
// A jumble of blocks of every resistance around the blast, in the
|
||||
// loaded chunk at the origin.
|
||||
for x in 0..16 {
|
||||
for z in 0..16 {
|
||||
for y in 56..74 {
|
||||
let i = ((x * 7 + y * 3 + z * 5) as usize) % states.len();
|
||||
game.world.set(x, y, z, states[i]).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
let center = [8.3, 64.6, 7.9];
|
||||
for (radius, resists) in [(4.0, None), (6.0, None), (4.0, Some(([8, 64, 8], 1200.0)))] {
|
||||
game.world.random = horizon_core::random::LegacyRandom::new(7);
|
||||
let expected = exploded_positions_stepwise(&mut game, center, radius, resists);
|
||||
game.world.random = horizon_core::random::LegacyRandom::new(7);
|
||||
let got = game.exploded_positions(center, radius, resists);
|
||||
assert!(expected.len() > 20, "{radius}: {}", expected.len());
|
||||
assert_eq!(got, expected, "radius {radius}, {resists:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires prepared resources; tools/test_explosion_items_parity.py"]
|
||||
fn explosion_items_parity_fixture() {
|
||||
|
||||
@@ -105,8 +105,10 @@ impl Game<'_> {
|
||||
);
|
||||
let on_ground = wanted.y < 0.0 && movement.y != wanted.y;
|
||||
f.on_ground = on_ground;
|
||||
if next.y < f.position.y {
|
||||
f.fall_distance += f.position.y - next.y;
|
||||
// Entity.checkFallDamage: each step down, narrowed to a float (a
|
||||
// falling block never runs baseTick, so it is never in water).
|
||||
if movement.y < 0.0 {
|
||||
f.fall_distance -= f64::from(movement.y as f32);
|
||||
}
|
||||
let from = f.position;
|
||||
f.position = next;
|
||||
|
||||
@@ -2176,6 +2176,7 @@ mod tests {
|
||||
#[test]
|
||||
fn reeling_an_item_requests_an_immediate_motion_update() {
|
||||
let mut item = ItemEntity {
|
||||
random: horizon_core::random::LegacyRandom::new(0),
|
||||
rotation: [0.0, 0.0],
|
||||
join: Default::default(),
|
||||
no_gravity: false,
|
||||
@@ -2191,6 +2192,7 @@ mod tests {
|
||||
on_ground: true,
|
||||
vertical_collision: false,
|
||||
horizontal_collision: false,
|
||||
fall_distance: 0.0,
|
||||
age: 0,
|
||||
pickup_delay: 0,
|
||||
bob_offset: 0.0,
|
||||
|
||||
@@ -37,8 +37,15 @@ const FISH: [&str; 4] = [
|
||||
];
|
||||
|
||||
#[test]
|
||||
#[ignore = "needs prepared resources in HORIZON_TEST_RESOURCES and an isolated mod registry"]
|
||||
#[ignore = "needs prepared resources in HORIZON_TEST_RESOURCES"]
|
||||
fn a_bobber_floats_bites_and_rolls_the_table_of_a_mods_medium() {
|
||||
crate::mod_api::in_own_process(
|
||||
"game::fishing::medium_tests::a_bobber_floats_bites_and_rolls_the_table_of_a_mods_medium",
|
||||
a_bobber_floats_bites_and_rolls_the_table_of_a_mods_medium_in_own_process,
|
||||
);
|
||||
}
|
||||
|
||||
fn a_bobber_floats_bites_and_rolls_the_table_of_a_mods_medium_in_own_process() {
|
||||
let root = PathBuf::from(std::env::var_os("HORIZON_TEST_RESOURCES").unwrap());
|
||||
let settings = Settings::default();
|
||||
let loaded = load_resources(
|
||||
|
||||
@@ -26,8 +26,15 @@ impl Hooks for Arc<Probe> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "needs prepared resources in HORIZON_TEST_RESOURCES and an isolated mod registry"]
|
||||
#[ignore = "needs prepared resources in HORIZON_TEST_RESOURCES"]
|
||||
fn a_changed_inventory_is_heard_once_with_its_41_slots() {
|
||||
crate::mod_api::in_own_process(
|
||||
"game::advancements::inventory_changed_tests::a_changed_inventory_is_heard_once_with_its_41_slots",
|
||||
a_changed_inventory_is_heard_once_with_its_41_slots_in_own_process,
|
||||
);
|
||||
}
|
||||
|
||||
fn a_changed_inventory_is_heard_once_with_its_41_slots_in_own_process() {
|
||||
let root = PathBuf::from(std::env::var_os("HORIZON_TEST_RESOURCES").unwrap());
|
||||
let settings = Settings::default();
|
||||
let loaded = load_resources(
|
||||
|
||||
@@ -168,6 +168,7 @@ mod tests {
|
||||
fn item_at(x: f64, y: f64, z: f64) -> ItemEntity {
|
||||
let p = Vec3::new(x, y, z);
|
||||
ItemEntity {
|
||||
random: horizon_core::random::LegacyRandom::new(0),
|
||||
rotation: [0.0, 0.0],
|
||||
join: Default::default(),
|
||||
health: 5,
|
||||
@@ -190,6 +191,7 @@ mod tests {
|
||||
no_gravity: false,
|
||||
vertical_collision: false,
|
||||
horizontal_collision: false,
|
||||
fall_distance: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,8 +32,15 @@ impl Hooks for Arc<Probe> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "needs prepared resources in HORIZON_TEST_RESOURCES and an isolated mod registry"]
|
||||
#[ignore = "needs prepared resources in HORIZON_TEST_RESOURCES"]
|
||||
fn the_active_stack_survives_a_partial_contact_and_consumption_removes_the_entity() {
|
||||
crate::mod_api::in_own_process(
|
||||
"game::item_contact_tests::the_active_stack_survives_a_partial_contact_and_consumption_removes_the_entity",
|
||||
the_active_stack_survives_a_partial_contact_and_consumption_removes_the_entity_in_own_process,
|
||||
);
|
||||
}
|
||||
|
||||
fn the_active_stack_survives_a_partial_contact_and_consumption_removes_the_entity_in_own_process() {
|
||||
let root = PathBuf::from(std::env::var_os("HORIZON_TEST_RESOURCES").unwrap());
|
||||
let settings = Settings::default();
|
||||
let loaded = load_resources(
|
||||
|
||||
@@ -82,7 +82,8 @@ impl Game<'_> {
|
||||
&nearby,
|
||||
Some(self.border.geometry()),
|
||||
);
|
||||
let escape_speed = no_physics.then(|| self.world.random.next_float() * 0.2 + 0.1);
|
||||
// `moveTowardsClosestSpace` draws the speed from the item's own random.
|
||||
let escape_speed = no_physics.then(|| item.random.next_float() * 0.2 + 0.1);
|
||||
let mut entered = Vec::new();
|
||||
let (alive, discarded) = self.tick_item_with_block_contacts(
|
||||
item,
|
||||
@@ -573,3 +574,47 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod escape_tests {
|
||||
use super::super::native_machines::tests::server_game;
|
||||
use horizon_client::chunk::Chunk;
|
||||
use horizon_client::items::ItemStack;
|
||||
use horizon_core::random::RandomSource;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// `Entity.moveTowardsClosestSpace` takes an item's escape speed from the
|
||||
/// item's own random, the one its constructor drew from, and leaves the
|
||||
/// level's random as it was (#13043).
|
||||
#[test]
|
||||
#[ignore = "needs prepared resources in HORIZON_TEST_RESOURCES"]
|
||||
fn an_item_escapes_a_block_at_its_own_randoms_speed() {
|
||||
let mut game = server_game();
|
||||
let feet = horizon_core::geometry::Vec3::new(8.5, 70.0, 8.5);
|
||||
game.player = Some(horizon_client::motion::PlayerMotion::new(game.motion, feet).unwrap());
|
||||
game.world
|
||||
.chunks
|
||||
.insert((0, 0), Arc::new(Chunk::empty(0, 0)));
|
||||
game.update_chunk_tickets();
|
||||
assert!(game.entity_ticking_area()(8, 8));
|
||||
let stone = game
|
||||
.pack
|
||||
.assets
|
||||
.registry
|
||||
.default_state("minecraft:stone")
|
||||
.unwrap();
|
||||
game.world.set(8, 64, 8, stone).unwrap();
|
||||
let stack = ItemStack::new("minecraft:stone", 2);
|
||||
game.spawn_item(stack, [8.5, 64.4, 8.5], [0.0, 0.0, 0.0], 10);
|
||||
let item = game.dropped.last().unwrap();
|
||||
let expected = f64::from(item.random.clone().next_float() * 0.2 + 0.1);
|
||||
let mut level = game.world.random.clone();
|
||||
game.ticked_entities_through = u32::MAX;
|
||||
game.tick_items();
|
||||
let item = game.dropped.last().unwrap();
|
||||
// Up is the nearest open side from the middle of the block.
|
||||
// The push replaces the fall's velocity; then the 0.98 drag.
|
||||
assert_eq!(item.velocity.y, expected * 0.98);
|
||||
assert_eq!(game.world.random.next_float(), level.next_float());
|
||||
}
|
||||
}
|
||||
@@ -75,8 +75,15 @@ fn target(id: i32) -> Option<InteractTarget> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "private prepared resources; process-wide mods, run this test alone"]
|
||||
#[ignore = "private prepared resources in HORIZON_TEST_RESOURCES"]
|
||||
fn actual_item_interact_mob_fallback_priority_and_filters() {
|
||||
crate::mod_api::in_own_process(
|
||||
"game::item_interact_mob_tests::actual_item_interact_mob_fallback_priority_and_filters",
|
||||
actual_item_interact_mob_fallback_priority_and_filters_in_own_process,
|
||||
);
|
||||
}
|
||||
|
||||
fn actual_item_interact_mob_fallback_priority_and_filters_in_own_process() {
|
||||
let root = PathBuf::from(std::env::var_os("HORIZON_TEST_RESOURCES").unwrap());
|
||||
let settings = Settings::default();
|
||||
let loaded = load_resources(
|
||||
|
||||
@@ -35,8 +35,15 @@ fn porkchops(game: &Game<'_>) -> usize {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "run alone with prepared resources in HORIZON_TEST_RESOURCES"]
|
||||
#[ignore = "needs prepared resources in HORIZON_TEST_RESOURCES"]
|
||||
fn a_vetoed_kill_keeps_the_mob_and_a_later_kill_dies_once() {
|
||||
crate::mod_api::in_own_process(
|
||||
"game::kill_die_cancel_tests::a_vetoed_kill_keeps_the_mob_and_a_later_kill_dies_once",
|
||||
a_vetoed_kill_keeps_the_mob_and_a_later_kill_dies_once_in_own_process,
|
||||
);
|
||||
}
|
||||
|
||||
fn a_vetoed_kill_keeps_the_mob_and_a_later_kill_dies_once_in_own_process() {
|
||||
let probe = Arc::new(Probe::default());
|
||||
assert!(
|
||||
mod_api::MODS
|
||||
|
||||
@@ -485,10 +485,93 @@ impl Game<'_> {
|
||||
e.pitch = o.pitch;
|
||||
e.head_yaw = o.yaw;
|
||||
if let Some(tag) = self.persist.raw.get(&o.id) {
|
||||
self.lan_display_data(tag, hub, e);
|
||||
self.lan_display_data_kept(o.id, tag, hub, e);
|
||||
}
|
||||
}
|
||||
// Only this level's displays: the others are kept for their own level's pass.
|
||||
KEPT_DISPLAYS.with(|kept| {
|
||||
let mut kept = kept.borrow_mut();
|
||||
let Some(kept) = kept.get_mut(&self.dim) else {
|
||||
return;
|
||||
};
|
||||
if kept.len() > self.others.len() + 64 {
|
||||
let ids: horizon_client::fast_hash::FxHashSet<u32> =
|
||||
self.others.iter().map(|o| o.id).collect();
|
||||
kept.retain(|id, _| ids.contains(id));
|
||||
}
|
||||
});
|
||||
}
|
||||
/// [`Self::lan_display_data`] for the display `id`, built again only when
|
||||
/// its compound changed: a server's holograms and menus are text displays
|
||||
/// whose text, item and transformation were parsed again every tick.
|
||||
fn lan_display_data_kept(&self, id: u32, tag: &Tag, hub: &mut Hub, e: &mut EntityState) {
|
||||
if !matches!(
|
||||
e.kind.as_str(),
|
||||
"minecraft:text_display" | "minecraft:block_display" | "minecraft:item_display"
|
||||
) {
|
||||
return self.lan_display_data(tag, hub, e);
|
||||
}
|
||||
// Block states are read through the pack's registry: a kept build is
|
||||
// only the same for the same one.
|
||||
let registry = std::ptr::from_ref(self.pack.assets.registry) as usize;
|
||||
KEPT_DISPLAYS.with(|kept| {
|
||||
let mut kept = kept.borrow_mut();
|
||||
let kept = kept.entry(self.dim).or_default();
|
||||
let same =
|
||||
|k: &KeptDisplay| k.registry == registry && k.kind == e.kind && k.tag == *tag;
|
||||
if let Some(k) = kept.get(&id).filter(|k| same(k)) {
|
||||
e.meta.clone_from(&k.meta);
|
||||
e.defaults.clone_from(&k.defaults);
|
||||
// The stack's ids are the hub's: asked each time, as a build does.
|
||||
if let Some((at, item)) = &k.item
|
||||
&& let Some(slot) = e.meta.get_mut(*at)
|
||||
{
|
||||
slot.2 = Value::Item(hub.stack(item));
|
||||
}
|
||||
return;
|
||||
}
|
||||
self.lan_display_data(tag, hub, e);
|
||||
let item = (e.kind == "minecraft:item_display").then(|| {
|
||||
let at = e
|
||||
.meta
|
||||
.iter()
|
||||
.position(|m| m.0 == 23 && m.1 == serializer::ITEM_STACK);
|
||||
let item = tag
|
||||
.get("item")
|
||||
.and_then(horizon_client::save::item_from_nbt);
|
||||
(at.unwrap_or(usize::MAX), item)
|
||||
});
|
||||
kept.insert(
|
||||
id,
|
||||
KeptDisplay {
|
||||
registry,
|
||||
kind: e.kind.clone(),
|
||||
tag: tag.clone(),
|
||||
meta: e.meta.clone(),
|
||||
defaults: e.defaults.clone(),
|
||||
item,
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// A display's synched data as [`Game::lan_display_data`] built it from
|
||||
/// `tag`, kept for the display's id on the thread that tracks entities, per
|
||||
/// level: a server thread tracks each dimension's `others` in turn.
|
||||
struct KeptDisplay {
|
||||
registry: usize,
|
||||
kind: String,
|
||||
tag: Tag,
|
||||
meta: Vec<(u8, i32, Value)>,
|
||||
defaults: Vec<(u8, i32, Value)>,
|
||||
/// An item display's item and the index in `meta` its stack goes in.
|
||||
item: Option<(usize, Option<horizon_client::items::ItemStack>)>,
|
||||
}
|
||||
thread_local! {
|
||||
static KEPT_DISPLAYS: std::cell::RefCell<
|
||||
horizon_client::fast_hash::FxHashMap<super::Dim, horizon_client::fast_hash::FxHashMap<u32, KeptDisplay>>,
|
||||
> = Default::default();
|
||||
}
|
||||
|
||||
impl Game<'_> {
|
||||
@@ -935,6 +1018,85 @@ fn push_arrow_meta(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[ignore = "needs prepared resources in HORIZON_TEST_RESOURCES"]
|
||||
fn kept_display_data_is_what_a_build_gives() {
|
||||
let game = crate::game::native_machines::tests::server_game();
|
||||
let sync = std::sync::Arc::new(horizon_client::lan::data::SyncData::empty());
|
||||
let net = horizon_client::lan::NetConfig {
|
||||
listen: false,
|
||||
..horizon_client::lan::NetConfig::loopback(0, String::new(), 2)
|
||||
};
|
||||
let lan = horizon_client::lan::Lan::start_with(sync.clone(), net).unwrap();
|
||||
let encoder = horizon_client::lan::chunks::ChunkEncoder::new(
|
||||
[] as [(std::sync::Arc<str>, bool); 0],
|
||||
&horizon_protocol::registry::Builtin::default(),
|
||||
&[],
|
||||
&[],
|
||||
);
|
||||
let config = horizon_client::lan::hub::HubConfig {
|
||||
default_game_mode: 0,
|
||||
guest_commands: false,
|
||||
force_game_mode: false,
|
||||
hardcore: false,
|
||||
view_distance: 10,
|
||||
simulation_distance: 10,
|
||||
dimension: "minecraft:overworld".into(),
|
||||
allow_flight: false,
|
||||
seed: 0,
|
||||
max_players: 20,
|
||||
integrated: true,
|
||||
levels: Default::default(),
|
||||
};
|
||||
let mut hub = Hub::new(lan, sync, encoder, config);
|
||||
let snbt = |s: &str| horizon_client::command::snbt::parse_fully(s).unwrap();
|
||||
let displays: [(&str, Tag, Tag); 3] = [
|
||||
(
|
||||
"minecraft:text_display",
|
||||
snbt(r#"{text:{text:"Shop",color:"gold"},billboard:"center",line_width:120}"#),
|
||||
snbt(r#"{text:{text:"Sold out",bold:1b},billboard:"center",background:0}"#),
|
||||
),
|
||||
(
|
||||
"minecraft:block_display",
|
||||
snbt(r#"{block_state:{Name:"minecraft:oak_stairs",Properties:{facing:"east"}}}"#),
|
||||
snbt(r#"{block_state:{Name:"minecraft:glass"},brightness:{sky:15,block:3}}"#),
|
||||
),
|
||||
(
|
||||
"minecraft:item_display",
|
||||
snbt(r#"{item:{id:"minecraft:diamond",count:3},item_display:"gui"}"#),
|
||||
snbt(
|
||||
r#"{item:{id:"minecraft:apple",count:1},item_display:"head",glow_color_override:255}"#,
|
||||
),
|
||||
),
|
||||
];
|
||||
let state = |kind: &str| EntityState {
|
||||
kind: kind.into(),
|
||||
..Default::default()
|
||||
};
|
||||
for (id, (kind, first, second)) in displays.iter().enumerate() {
|
||||
let id = 900 + id as u32;
|
||||
// Ticks with the first compound, then the second, then the first
|
||||
// again: each answer is a fresh build's.
|
||||
for tag in [first, first, first, second, second, first] {
|
||||
let mut built = state(kind);
|
||||
game.lan_display_data(tag, &mut hub, &mut built);
|
||||
assert!(!built.meta.is_empty(), "{kind}");
|
||||
let mut kept = state(kind);
|
||||
game.lan_display_data_kept(id, tag, &mut hub, &mut kept);
|
||||
assert_eq!(
|
||||
format!("{:?}", kept.meta),
|
||||
format!("{:?}", built.meta),
|
||||
"{kind}"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{:?}", kept.defaults),
|
||||
format!("{:?}", built.defaults),
|
||||
"{kind}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn summoned_ids_are_unique_and_survive_the_tracking_pass() {
|
||||
let mut next = 40;
|
||||
|
||||
@@ -12,12 +12,31 @@
|
||||
use horizon_client::fast_hash::FxHashMap;
|
||||
use horizon_client::lan::{hub::Hub, tracking};
|
||||
|
||||
/// Per level, each idle entity's place and the tracking pass it was last described in.
|
||||
/// Per level, the idle entities the last pass noted (`Kept`).
|
||||
#[derive(Default)]
|
||||
pub(super) struct IdleStates {
|
||||
levels: FxHashMap<String, FxHashMap<i32, (u32, Place)>>,
|
||||
levels: FxHashMap<String, Kept>,
|
||||
}
|
||||
|
||||
/// One level's idle entities as the last tracking pass noted them, in the
|
||||
/// order it met them, and that pass. The game meets its entities in the same
|
||||
/// order every tick (its lists keep their order), so a pass finds each one's
|
||||
/// note at or just past where it found the last one: no lookup by id. One it
|
||||
/// does not find there is described in full, which is always right.
|
||||
#[derive(Default)]
|
||||
struct Kept {
|
||||
pass: u32,
|
||||
ids: Vec<i32>,
|
||||
places: Vec<Place>,
|
||||
/// This pass's notes, which replace them when it finishes.
|
||||
next_ids: Vec<i32>,
|
||||
next_places: Vec<Place>,
|
||||
}
|
||||
|
||||
/// How many notes past the last one found a pass looks: entities that left
|
||||
/// the level since are skipped over.
|
||||
const LOOK_AHEAD: usize = 4;
|
||||
|
||||
/// Where an entity is: its position, rotation (yaw, pitch, head yaw), whether
|
||||
/// it is on the ground, and its velocity.
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
@@ -30,16 +49,24 @@ pub(super) struct Place {
|
||||
|
||||
/// The idle entities of the level in place, for one pass.
|
||||
pub(super) struct IdlePass<'a> {
|
||||
kept: &'a mut FxHashMap<i32, (u32, Place)>,
|
||||
kept: &'a mut Kept,
|
||||
/// Whether the kept notes are the last pass's.
|
||||
last: bool,
|
||||
/// Where the next note is looked for.
|
||||
cursor: usize,
|
||||
/// The level's tracking passes so far (`Hub::tracking_ticks`). An entity
|
||||
/// described one pass earlier was in the tracker's last pass.
|
||||
/// noted one pass earlier was in the tracker's last pass.
|
||||
now: u32,
|
||||
/// The players the level's tracker runs for (`Hub::watches`: their
|
||||
/// index), where they are horizontally, and their view distance in blocks:
|
||||
/// the clients that could pair.
|
||||
/// the clients that could pair. Sorted by x, so an entity looks only at
|
||||
/// the ones within its range along x.
|
||||
viewers: Vec<(usize, [f64; 2], i32)>,
|
||||
/// `entity-broadcast-range-percentage`, as the tracker scales ranges.
|
||||
percent: i32,
|
||||
/// The last type asked about and its scaled tracking range: the
|
||||
/// entities of a type come in runs (a farm, a pile of items).
|
||||
last_kind: Option<(String, i32)>,
|
||||
}
|
||||
|
||||
impl IdleStates {
|
||||
@@ -51,16 +78,46 @@ impl IdleStates {
|
||||
.map(|(i, g)| (i, [g.position[0], g.position[2]], g.view_distance * 16))
|
||||
.collect();
|
||||
let kept = self.levels.entry(hub.config.dimension.clone()).or_default();
|
||||
Some(IdlePass {
|
||||
kept,
|
||||
now,
|
||||
viewers,
|
||||
percent,
|
||||
})
|
||||
Some(IdlePass::new(kept, now, viewers, percent))
|
||||
}
|
||||
}
|
||||
|
||||
impl IdlePass<'_> {
|
||||
impl<'a> IdlePass<'a> {
|
||||
fn new(
|
||||
kept: &'a mut Kept,
|
||||
now: u32,
|
||||
mut viewers: Vec<(usize, [f64; 2], i32)>,
|
||||
percent: i32,
|
||||
) -> Self {
|
||||
// A player somewhere not finite is within no range: the distance
|
||||
// test below fails for it.
|
||||
viewers.retain(|(_, [x, z], _)| x.is_finite() && z.is_finite());
|
||||
viewers.sort_by(|a, b| a.1[0].total_cmp(&b.1[0]));
|
||||
kept.next_ids.clear();
|
||||
kept.next_places.clear();
|
||||
IdlePass {
|
||||
last: kept.pass.wrapping_add(1) == now,
|
||||
kept,
|
||||
cursor: 0,
|
||||
now,
|
||||
viewers,
|
||||
percent,
|
||||
last_kind: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The type's tracking range in blocks, scaled as the tracker scales it.
|
||||
fn range(&mut self, kind: &str) -> i32 {
|
||||
match &mut self.last_kind {
|
||||
Some((last, range)) if last.as_str() == kind => *range,
|
||||
last => {
|
||||
let range = self.percent * (i32::from(tracking::settings(kind).0) * 16) / 100;
|
||||
*last = Some((kind.to_owned(), range));
|
||||
range
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the idle entity `id` of type `kind`, now at `at`, can be
|
||||
/// described bare: it was in the tracker's last pass at the same place,
|
||||
/// carries no passengers (whose ranges widen its own), and every client
|
||||
@@ -70,6 +127,16 @@ impl IdlePass<'_> {
|
||||
&& hub.riding.passengers(id).is_empty()
|
||||
}
|
||||
|
||||
/// The place the last pass noted for `id`, if it is found where the
|
||||
/// pass's order says it would be.
|
||||
fn noted(&mut self, id: i32) -> Option<Place> {
|
||||
let ids = &self.kept.ids;
|
||||
let end = ids.len().min(self.cursor + LOOK_AHEAD);
|
||||
let k = (self.cursor..end).find(|&k| ids[k] == id)?;
|
||||
self.cursor = k + 1;
|
||||
Some(self.kept.places[k])
|
||||
}
|
||||
|
||||
/// [`Self::bare`] but for the passengers, with whether the player `i`
|
||||
/// tracks the entity.
|
||||
fn unchanged(
|
||||
@@ -79,27 +146,36 @@ impl IdlePass<'_> {
|
||||
at: &Place,
|
||||
tracks: impl Fn(usize) -> bool,
|
||||
) -> bool {
|
||||
// A new one reads as noted two passes ago.
|
||||
let now = self.now;
|
||||
let noted = self.kept.entry(id).or_insert((now.wrapping_sub(2), *at));
|
||||
let still = noted.0.wrapping_add(1) == now && noted.1 == *at;
|
||||
*noted = (now, *at);
|
||||
if !still {
|
||||
let noted = if self.last { self.noted(id) } else { None };
|
||||
self.kept.next_ids.push(id);
|
||||
self.kept.next_places.push(*at);
|
||||
if noted != Some(*at) {
|
||||
return false;
|
||||
}
|
||||
// The tracker's `sees`, but for the chunk it also asks the client to have.
|
||||
let range = self.percent * (i32::from(tracking::settings(kind).0) * 16) / 100;
|
||||
!self.viewers.iter().any(|&(i, [x, z], view)| {
|
||||
let range = f64::from(range.min(view));
|
||||
let (dx, dz) = (x - at.position[0], z - at.position[2]);
|
||||
dx * dx + dz * dz <= range * range && !tracks(i)
|
||||
})
|
||||
let range = self.range(kind);
|
||||
// Only a client within `range` along x can be within it: a block
|
||||
// more spares the rounding of the squares.
|
||||
let reach = f64::from(range) + 1.0;
|
||||
let x = at.position[0];
|
||||
let from = self.viewers.partition_point(|v| v.1[0] < x - reach);
|
||||
!self.viewers[from..]
|
||||
.iter()
|
||||
.take_while(|v| v.1[0] <= x + reach)
|
||||
.any(|&(i, [vx, vz], view)| {
|
||||
let range = f64::from(range.min(view));
|
||||
let (dx, dz) = (vx - at.position[0], vz - at.position[2]);
|
||||
dx * dx + dz * dz <= range * range && !tracks(i)
|
||||
})
|
||||
}
|
||||
|
||||
/// Forget the entities this pass did not note: they left, or tick again.
|
||||
/// The notes of this pass replace the last one's: the entities it did not
|
||||
/// note left, or tick again.
|
||||
pub(super) fn finish(self) {
|
||||
let now = self.now;
|
||||
self.kept.retain(|_, (stamp, _)| *stamp == now);
|
||||
let kept = self.kept;
|
||||
std::mem::swap(&mut kept.ids, &mut kept.next_ids);
|
||||
std::mem::swap(&mut kept.places, &mut kept.next_places);
|
||||
kept.pass = self.now;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,14 +192,22 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn pass(kept: &mut FxHashMap<i32, (u32, Place)>, now: u32) -> IdlePass<'_> {
|
||||
/// One pass `now` over `entities` (id, type, place), each answered with
|
||||
/// whether the player `i` tracks it; what each could be described as.
|
||||
fn pass(
|
||||
kept: &mut Kept,
|
||||
now: u32,
|
||||
entities: &[(i32, &str, Place)],
|
||||
tracks: impl Fn(i32, usize) -> bool,
|
||||
) -> Vec<bool> {
|
||||
// One client at the origin, one 400 blocks away; both see 10 chunks.
|
||||
IdlePass {
|
||||
kept,
|
||||
now,
|
||||
viewers: vec![(0, [0.0, 0.0], 160), (1, [400.0, 0.0], 160)],
|
||||
percent: 100,
|
||||
}
|
||||
let viewers = vec![(1, [400.0, 0.0], 160), (0, [0.0, 0.0], 160)];
|
||||
let mut pass = IdlePass::new(kept, now, viewers, 100);
|
||||
let bare = (entities.iter())
|
||||
.map(|(id, kind, at)| pass.unchanged(*id, kind, at, |i| tracks(*id, i)))
|
||||
.collect();
|
||||
pass.finish();
|
||||
bare
|
||||
}
|
||||
|
||||
/// An idle cow (tracked 10 chunks away) is described bare while it stays
|
||||
@@ -131,24 +215,54 @@ mod tests {
|
||||
/// there could pair with it, when it moved, and after a pass it was not in.
|
||||
#[test]
|
||||
fn an_idle_entity_is_bare_until_a_client_could_pair_or_it_moves() {
|
||||
let mut kept = FxHashMap::default();
|
||||
let cow = "minecraft:cow";
|
||||
let mut kept = Kept::default();
|
||||
let cow = |at| [(7, "minecraft:cow", at)];
|
||||
let at = place(100.0);
|
||||
// Its first pass describes it in full.
|
||||
assert!(!pass(&mut kept, 5).unchanged(7, cow, &at, |_| true));
|
||||
assert_eq!(pass(&mut kept, 5, &cow(at), |_, _| true), [false]);
|
||||
// Tracked by the client in range; the far one can't pair.
|
||||
let tracked = |i| i == 0;
|
||||
assert!(pass(&mut kept, 6).unchanged(7, cow, &at, tracked));
|
||||
let tracked = |_, i| i == 0;
|
||||
assert_eq!(pass(&mut kept, 6, &cow(at), tracked), [true]);
|
||||
// The client in range doesn't track it yet: it could pair.
|
||||
assert!(!pass(&mut kept, 7).unchanged(7, cow, &at, |_| false));
|
||||
assert_eq!(pass(&mut kept, 7, &cow(at), |_, _| false), [false]);
|
||||
// 100 blocks is past an item's 6 chunks: no client could pair.
|
||||
assert!(pass(&mut kept, 8).unchanged(7, "minecraft:item", &at, |_| false));
|
||||
let item = [(7, "minecraft:item", at)];
|
||||
assert_eq!(pass(&mut kept, 8, &item, |_, _| false), [true]);
|
||||
// It moved.
|
||||
assert!(!pass(&mut kept, 9).unchanged(7, cow, &place(100.5), tracked));
|
||||
assert_eq!(pass(&mut kept, 9, &cow(place(100.5)), tracked), [false]);
|
||||
// A pass went by without it.
|
||||
assert!(!pass(&mut kept, 11).unchanged(7, cow, &place(100.5), tracked));
|
||||
// Only what a pass noted stays.
|
||||
pass(&mut kept, 12).finish();
|
||||
assert!(kept.is_empty());
|
||||
assert_eq!(pass(&mut kept, 11, &cow(place(100.5)), tracked), [false]);
|
||||
// The client 400 blocks away is in range of a cow next to it.
|
||||
let far = [(7, "minecraft:cow", place(300.0))];
|
||||
pass(&mut kept, 12, &far, |_, _| true);
|
||||
assert_eq!(pass(&mut kept, 13, &far, |_, i| i == 0), [false]);
|
||||
assert_eq!(pass(&mut kept, 14, &far, |_, i| i == 1), [true]);
|
||||
}
|
||||
|
||||
/// Each entity's note is found in the last pass's order, past the ones
|
||||
/// that left since and after one that joined.
|
||||
#[test]
|
||||
fn notes_are_found_in_the_last_pass_order() {
|
||||
let mut kept = Kept::default();
|
||||
let item = |id| (id, "minecraft:item", place(1000.0 + f64::from(id)));
|
||||
let all = |ids: &[i32]| ids.iter().map(|&id| item(id)).collect::<Vec<_>>();
|
||||
let first = all(&(1..=20).collect::<Vec<_>>());
|
||||
pass(&mut kept, 1, &first, |_, _| false);
|
||||
// 2 to 4 left, 30 joined in the middle, 12 to 14 left.
|
||||
let ids = [1, 5, 6, 30, 7, 8, 9, 10, 11, 15, 16, 17, 18, 19, 20];
|
||||
let bare = pass(&mut kept, 2, &all(&ids), |_, _| false);
|
||||
let full: Vec<i32> = ids
|
||||
.iter()
|
||||
.zip(&bare)
|
||||
.filter(|(_, b)| !**b)
|
||||
.map(|(id, _)| *id)
|
||||
.collect();
|
||||
assert_eq!(full, [30]);
|
||||
// One that moved out of order beyond the look-ahead is described in
|
||||
// full, which is always right, and the next pass finds it again.
|
||||
let moved = [20, 1, 5, 6, 7];
|
||||
let bare = pass(&mut kept, 3, &all(&moved), |_, _| false);
|
||||
assert_eq!(bare, [false, true, true, true, true]);
|
||||
assert_eq!(pass(&mut kept, 4, &all(&moved), |_, _| false), [true; 5]);
|
||||
}
|
||||
}
|
||||
@@ -1732,6 +1732,7 @@ mod item_column_tests {
|
||||
fn item(id: u32, x: f64, y: f64, z: f64) -> ItemEntity {
|
||||
let at = Vec3::new(x, y, z);
|
||||
ItemEntity {
|
||||
random: horizon_core::random::LegacyRandom::new(0),
|
||||
rotation: [0.0, 0.0],
|
||||
join: Default::default(),
|
||||
no_gravity: false,
|
||||
@@ -1747,6 +1748,7 @@ mod item_column_tests {
|
||||
on_ground: true,
|
||||
vertical_collision: false,
|
||||
horizontal_collision: false,
|
||||
fall_distance: 0.0,
|
||||
age: 0,
|
||||
pickup_delay: 0,
|
||||
bob_offset: 0.0,
|
||||
|
||||
@@ -731,8 +731,15 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[ignore = "prepared resources in HORIZON_TEST_RESOURCES; run alone to isolate the mod registry"]
|
||||
#[ignore = "prepared resources in HORIZON_TEST_RESOURCES"]
|
||||
fn the_standard_helpers_route_to_native_owned_storage() {
|
||||
crate::mod_api::in_own_process(
|
||||
"game::machines::native_fluids::tests::the_standard_helpers_route_to_native_owned_storage",
|
||||
the_standard_helpers_route_to_native_owned_storage_in_own_process,
|
||||
);
|
||||
}
|
||||
|
||||
fn the_standard_helpers_route_to_native_owned_storage_in_own_process() {
|
||||
use crate::game::{Audio, Options, Progress, Settings, World, load_resources};
|
||||
use crate::mod_api::{
|
||||
self, CustomBlockFluidsEvent, CustomFluidDrainEvent, CustomFluidFillEvent,
|
||||
|
||||
@@ -1334,6 +1334,21 @@ impl Game<'_> {
|
||||
);
|
||||
}
|
||||
}
|
||||
// `LivingEntity.tick` ticks `EntityEquipment` (the armour slots) as
|
||||
// well as the inventory, so a compass worn there loses its target too.
|
||||
let gone = self.armor.each_ref().map(|stack| {
|
||||
stack
|
||||
.as_ref()
|
||||
.is_some_and(|s| lodestone_target_gone(self, s, dim))
|
||||
});
|
||||
for (stack, gone) in self.armor.iter_mut().zip(gone) {
|
||||
if gone && let Some(stack) = stack {
|
||||
stack.data_mut().set_other(
|
||||
"minecraft:lodestone_tracker",
|
||||
Some(Tag::Compound(Default::default())),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn give_or_drop_stack(&mut self, stack: ItemStack) {
|
||||
if let Some(rest) = self.give(stack)
|
||||
|
||||
@@ -24,8 +24,15 @@ impl Hooks for Arc<Probe> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "needs prepared resources in HORIZON_TEST_RESOURCES and an isolated mod registry"]
|
||||
#[ignore = "needs prepared resources in HORIZON_TEST_RESOURCES"]
|
||||
fn a_mod_toast_queues_unless_a_mod_cancels_it() {
|
||||
crate::mod_api::in_own_process(
|
||||
"game::advancements::mod_toast_tests::a_mod_toast_queues_unless_a_mod_cancels_it",
|
||||
a_mod_toast_queues_unless_a_mod_cancels_it_in_own_process,
|
||||
);
|
||||
}
|
||||
|
||||
fn a_mod_toast_queues_unless_a_mod_cancels_it_in_own_process() {
|
||||
let root = PathBuf::from(std::env::var_os("HORIZON_TEST_RESOURCES").unwrap());
|
||||
let settings = Settings::default();
|
||||
let loaded = load_resources(
|
||||
|
||||
@@ -969,10 +969,16 @@ mod tests {
|
||||
assert_eq!(verdict(&ruby, &manifest, true), switches_on_restart);
|
||||
assert_eq!(verdict(&ruby, &manifest, false), Some(Ok(())));
|
||||
let later = std::time::SystemTime::now() + std::time::Duration::from_secs(5);
|
||||
std::fs::File::open(&ruby)
|
||||
.unwrap()
|
||||
.set_modified(later)
|
||||
.unwrap();
|
||||
let mut folder = std::fs::OpenOptions::new();
|
||||
folder.read(true);
|
||||
// Windows opens a folder only with FILE_FLAG_BACKUP_SEMANTICS, and sets its time only
|
||||
// with write access.
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::fs::OpenOptionsExt;
|
||||
folder.write(true).custom_flags(0x0200_0000);
|
||||
}
|
||||
folder.open(&ruby).unwrap().set_modified(later).unwrap();
|
||||
assert_eq!(verdict(&ruby, &manifest, false), switches_on_restart);
|
||||
std::fs::remove_dir_all(&dir).unwrap();
|
||||
}
|
||||
|
||||
@@ -655,13 +655,13 @@ impl Game<'_> {
|
||||
/// A saved dropped item.
|
||||
pub(super) fn load_item(&mut self, tag: Tag, uuid: Option<[i32; 4]>) {
|
||||
self.next_entity += 1;
|
||||
// `ItemEntity(EntityType, Level)` before the load: the item's own random
|
||||
// makes its UUID (kept unless the tag has one) and bobOffs, not the level's.
|
||||
// Its yaw, the next draw, is always replaced by the saved rotation.
|
||||
// `ItemEntity(EntityType, Level)` first: its own random draws UUID, bobOffs and yaw.
|
||||
let mut own = random_source::entity();
|
||||
let fresh = horizon_client::save::insecure_uuid(&mut own);
|
||||
let bob = own.next_float() * std::f32::consts::PI * 2.0;
|
||||
if let Some(e) = entity_nbt::item_entity_from_nbt(&tag, self.next_entity, bob) {
|
||||
own.next_float();
|
||||
if let Some(mut e) = entity_nbt::item_entity_from_nbt(&tag, self.next_entity, bob) {
|
||||
e.random = own;
|
||||
self.remember(e.id, Some(uuid.unwrap_or(fresh)), tag);
|
||||
self.dropped.push(e);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Needs private prepared resources: installs process-wide trait mods, so it runs in a process of its
|
||||
//! own (`in_own_process`) and passes whatever else runs beside it.
|
||||
//! own (`mod_api::in_own_process`) and passes whatever else runs beside it.
|
||||
use super::super::*;
|
||||
use crate::mod_api::{
|
||||
self, Hooks, Host, NativeRecipeCategory, NativeRecipeLookup, NativeStack,
|
||||
@@ -45,36 +45,11 @@ fn stack(item: &str) -> NativeStack {
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs `body` in a fresh process of this test binary: `mod_api::MODS` is set once per process, and
|
||||
/// another test that set it first would make this one's `set` fail. The parent starts the binary again
|
||||
/// for just this test (`--exact`, `--ignored`) and requires that exactly one test ran and passed; the
|
||||
/// child sees `OWN_PROCESS` and runs `body` itself.
|
||||
fn in_own_process(name: &str, body: impl FnOnce()) {
|
||||
const OWN_PROCESS: &str = "HORIZON_TEST_OWN_PROCESS";
|
||||
if std::env::var_os(OWN_PROCESS).is_some_and(|v| v == name) {
|
||||
return body();
|
||||
}
|
||||
let out = std::process::Command::new(std::env::current_exe().unwrap())
|
||||
.args([name, "--exact", "--ignored", "--test-threads=1"])
|
||||
.env(OWN_PROCESS, name)
|
||||
.output()
|
||||
.unwrap();
|
||||
let report = format!(
|
||||
"{}{}",
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
assert!(
|
||||
out.status.success() && report.contains("test result: ok. 1 passed;"),
|
||||
"{name} failed in its own process:\n{report}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "needs prepared resources in HORIZON_TEST_RESOURCES"]
|
||||
fn lookups_wait_for_the_next_frame_and_stop_at_the_viewer_that_takes_them() {
|
||||
const NAME: &str = "game::native_recipes::recipe_viewer_tests::lookups_wait_for_the_next_frame_and_stop_at_the_viewer_that_takes_them";
|
||||
in_own_process(NAME, lookups_wait_for_the_next_frame);
|
||||
mod_api::in_own_process(NAME, lookups_wait_for_the_next_frame);
|
||||
}
|
||||
|
||||
fn lookups_wait_for_the_next_frame() {
|
||||
|
||||
@@ -6256,32 +6256,11 @@ impl Game<'_> {
|
||||
};
|
||||
let at = Vec3::new(p[0], p[1], p[2]);
|
||||
// ItemEntity's constructor draws bobOffs from the item's own random.
|
||||
let bob_offset = horizon_client::mobs::entity_random(id as u32, at).next_float()
|
||||
* std::f32::consts::PI
|
||||
* 2.0;
|
||||
let mut random = horizon_client::mobs::entity_random(id as u32, at);
|
||||
let bob_offset = random.next_float() * std::f32::consts::PI * 2.0;
|
||||
self.dropped.push(ItemEntity {
|
||||
rotation: [0.0, 0.0],
|
||||
join: Default::default(),
|
||||
no_gravity: false,
|
||||
health: 5,
|
||||
fire_ticks: 0,
|
||||
thrower: None,
|
||||
target: None,
|
||||
id: id as u32,
|
||||
stack,
|
||||
position: at,
|
||||
previous: at,
|
||||
velocity,
|
||||
on_ground: false,
|
||||
vertical_collision: false,
|
||||
horizontal_collision: false,
|
||||
age: 0,
|
||||
pickup_delay: 0,
|
||||
bob_offset,
|
||||
ticks: 0,
|
||||
needs_sync: false,
|
||||
stuck: Vec3::ZERO,
|
||||
was_touching_water: false,
|
||||
random,
|
||||
..ItemEntity::new(id as u32, stack, at, velocity, bob_offset)
|
||||
});
|
||||
}
|
||||
/// `Entity.lerpMotion` (and `recreateFromPacket`'s motion) for the
|
||||
|
||||
@@ -167,6 +167,7 @@ impl Game<'_> {
|
||||
|| self.settings.extras.pvp.tweaks.clear_lava,
|
||||
full_water_vision: self.settings.extras.pvp.tweaks.clear_water,
|
||||
night_vision: self.fog_night_vision(),
|
||||
environment: self.pack.polytone_colors.env,
|
||||
};
|
||||
let mut fog = horizon_client::fog::setup_fog(tables, &self.fog, &input);
|
||||
if !self.settings.horizon.fog {
|
||||
|
||||
@@ -294,6 +294,7 @@ fn pack_particles(
|
||||
/// The packs' custom colours; their dye and sheep colours go to the draw paths' table.
|
||||
fn read_colors(root: &std::path::Path) -> horizon_client::polytone_colors::PackColors {
|
||||
let colors = horizon_client::polytone_colors::PackColors::read(root);
|
||||
horizon_client::pack_dyes::set(&colors);
|
||||
horizon_client::pack_palette::set(&colors);
|
||||
horizon_client::brewing::set_pack_colors(colors.effects());
|
||||
colors
|
||||
}
|
||||
@@ -952,7 +952,7 @@ pub struct Entities {
|
||||
/// moves. One the entities' own ticks added waits for the next pass, as
|
||||
/// vanilla's waits for the next tick.
|
||||
pub synced: Vec<(i32, i32)>,
|
||||
states: crate::fast_hash::FxHashMap<i32, ServerEntity>,
|
||||
states: slab::ServerEntities,
|
||||
/// The level tick, stamped on every entity still in the level.
|
||||
tick: u32,
|
||||
/// This tick's entities as the range checks read them, the chunks they
|
||||
@@ -1072,7 +1072,7 @@ impl Entities {
|
||||
self.states.len()
|
||||
}
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.states.is_empty()
|
||||
self.states.len() == 0
|
||||
}
|
||||
|
||||
/// How many passes the level's tracking has run.
|
||||
@@ -1160,12 +1160,29 @@ impl Entities {
|
||||
self.quiet.resize(entities.len(), false);
|
||||
// The states stamped this tick: when that is all of them, none left.
|
||||
let mut stamped = 0;
|
||||
self.states.begin();
|
||||
for (index, e) in entities.iter().enumerate() {
|
||||
let mut added = false;
|
||||
let s = self.states.entry(e.id).or_insert_with(|| {
|
||||
added = true;
|
||||
ServerEntity::new(e, kind(&e.kind), riding)
|
||||
});
|
||||
let s = match self.states.meet(e.id) {
|
||||
Some((slot, by_id)) => {
|
||||
let s = self.states.at(slot);
|
||||
// Found out of order: the next one is looked for after
|
||||
// where this one was in the last tick's order.
|
||||
if by_id && s.seen == now.wrapping_sub(1) {
|
||||
let last = s.index;
|
||||
self.states.resume_after(last);
|
||||
}
|
||||
self.states.at_mut(slot)
|
||||
}
|
||||
None => {
|
||||
added = true;
|
||||
let slot = self
|
||||
.states
|
||||
.insert(e.id, ServerEntity::new(e, kind(&e.kind), riding));
|
||||
self.states.met_new(e.id, slot);
|
||||
self.states.at_mut(slot)
|
||||
}
|
||||
};
|
||||
if s.seen != now {
|
||||
stamped += 1;
|
||||
}
|
||||
@@ -1217,6 +1234,7 @@ impl Entities {
|
||||
hidden: e.hidden,
|
||||
});
|
||||
}
|
||||
self.states.end();
|
||||
// TrackedEntity.getEffectiveRange: a vehicle is tracked as far as its
|
||||
// widest indirect passenger, so a rider never leaves before its mount.
|
||||
for (&vehicle, passengers) in &riding.passengers {
|
||||
@@ -1390,9 +1408,7 @@ impl Entities {
|
||||
if self.quiet[index] && !self.pairs[index] {
|
||||
continue;
|
||||
}
|
||||
let Some(s) = self.states.get_mut(&e.id) else {
|
||||
continue;
|
||||
};
|
||||
let s = self.states.at_mut(self.states.slot_of_index(index));
|
||||
self.packets.clear();
|
||||
// Paired as it joined, then its first changes go to everyone
|
||||
// tracking it, those just paired with it included. Arrows wait for the
|
||||
@@ -1466,13 +1482,14 @@ impl Entities {
|
||||
}
|
||||
// ChunkMap.removeEntity for the ones no longer in the level.
|
||||
if self.states.len() > stamped {
|
||||
self.states.retain(|_, s| s.seen == now);
|
||||
self.states.retain(|s| s.seen == now);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod late_pair_tests;
|
||||
mod slab;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
@@ -42,7 +42,7 @@ fn late_pairing_does_not_duplicate_or_spend_the_next_arrow_update() {
|
||||
let mut e = trident();
|
||||
level.pair_after_tick(std::slice::from_ref(&e), &riding, &mut viewers, |_| Some(0));
|
||||
assert_eq!(level.tick, 1);
|
||||
assert_eq!(level.states[&37].tick, 0);
|
||||
assert_eq!(level.states.get(&37).unwrap().tick, 0);
|
||||
assert!(viewers[0].tracker.is_tracking(37));
|
||||
assert_eq!(
|
||||
viewers[0]
|
||||
@@ -57,14 +57,14 @@ fn late_pairing_does_not_duplicate_or_spend_the_next_arrow_update() {
|
||||
level.pair_after_tick(std::slice::from_ref(&e), &riding, &mut viewers, |_| Some(0));
|
||||
assert!(viewers[0].out.is_empty());
|
||||
assert!(viewers[0].tracker.take_events().is_empty());
|
||||
assert_eq!(level.states[&37].tick, 0);
|
||||
assert_eq!(level.states.get(&37).unwrap().tick, 0);
|
||||
// It hits a block between its introduction and the next tracking pass.
|
||||
e.position[0] += 0.25;
|
||||
e.velocity = [0.0; 3];
|
||||
e.meta.push((10, serializer::BOOLEAN, Value::Bool(true)));
|
||||
level.tick(&[&e], &riding, &mut viewers, |_| Some(0));
|
||||
assert_eq!(level.tick, 2);
|
||||
assert_eq!(level.states[&37].tick, 1);
|
||||
assert_eq!(level.states.get(&37).unwrap().tick, 1);
|
||||
assert!(
|
||||
!viewers[0]
|
||||
.out
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
//! A level's `ServerEntity`s by id (`ChunkMap.entityMap`), found without hashing on most ticks.
|
||||
//!
|
||||
//! A level tracks tens of thousands of entities, most of them items and mobs frozen past the
|
||||
//! simulation distance, and looked each one up by id on every tick: one probe of a large table
|
||||
//! for each, missing the cache nearly every time. The game describes its entities in the same
|
||||
//! order every tick (its lists keep their order), so each tick notes the order it met them in,
|
||||
//! and the next one finds each entity at or just past where it found the one before. Only an
|
||||
//! entity that joined, or one met out of order, is looked up by id.
|
||||
|
||||
use super::ServerEntity;
|
||||
use crate::fast_hash::FxHashMap;
|
||||
|
||||
/// How many entries past the last one found an entity is looked for: the entities that left
|
||||
/// the level since are skipped over.
|
||||
const LOOK_AHEAD: usize = 4;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct ServerEntities {
|
||||
/// The entities and their ids; None in a slot that is free.
|
||||
slots: Vec<Option<(i32, ServerEntity)>>,
|
||||
free: Vec<u32>,
|
||||
by_id: FxHashMap<i32, u32>,
|
||||
/// The last tick's entities in the order it met them (id, slot), this tick's so far, and
|
||||
/// where the next one is looked for.
|
||||
order: Vec<(i32, u32)>,
|
||||
next_order: Vec<(i32, u32)>,
|
||||
cursor: usize,
|
||||
}
|
||||
|
||||
impl ServerEntities {
|
||||
pub(super) fn len(&self) -> usize {
|
||||
self.by_id.len()
|
||||
}
|
||||
|
||||
pub(super) fn contains_key(&self, id: &i32) -> bool {
|
||||
self.by_id.contains_key(id)
|
||||
}
|
||||
|
||||
pub(super) fn get(&self, id: &i32) -> Option<&ServerEntity> {
|
||||
self.by_id.get(id).map(|&slot| self.at(slot))
|
||||
}
|
||||
|
||||
pub(super) fn at(&self, slot: u32) -> &ServerEntity {
|
||||
&self.slots[slot as usize].as_ref().expect("a live slot").1
|
||||
}
|
||||
|
||||
pub(super) fn at_mut(&mut self, slot: u32) -> &mut ServerEntity {
|
||||
&mut self.slots[slot as usize].as_mut().expect("a live slot").1
|
||||
}
|
||||
|
||||
/// Adds the entity `id` (not tracked yet), returning its slot.
|
||||
pub(super) fn insert(&mut self, id: i32, s: ServerEntity) -> u32 {
|
||||
debug_assert!(!self.by_id.contains_key(&id));
|
||||
let slot = match self.free.pop() {
|
||||
Some(slot) => {
|
||||
self.slots[slot as usize] = Some((id, s));
|
||||
slot
|
||||
}
|
||||
None => {
|
||||
self.slots.push(Some((id, s)));
|
||||
(self.slots.len() - 1) as u32
|
||||
}
|
||||
};
|
||||
self.by_id.insert(id, slot);
|
||||
slot
|
||||
}
|
||||
|
||||
/// Starts meeting a tick's entities in order.
|
||||
pub(super) fn begin(&mut self) {
|
||||
self.cursor = 0;
|
||||
self.next_order.clear();
|
||||
}
|
||||
|
||||
/// The slot of the entity `id`, met next in this tick's order, and whether it was found by
|
||||
/// id (not where the last tick's order had it). Notes it in this tick's order.
|
||||
pub(super) fn meet(&mut self, id: i32) -> Option<(u32, bool)> {
|
||||
let end = self.order.len().min(self.cursor + LOOK_AHEAD);
|
||||
let found = match (self.cursor..end).find(|&k| self.order[k].0 == id && self.holds(k, id)) {
|
||||
Some(k) => {
|
||||
self.cursor = k + 1;
|
||||
Some((self.order[k].1, false))
|
||||
}
|
||||
None => self.by_id.get(&id).map(|&slot| (slot, true)),
|
||||
};
|
||||
if let Some((slot, _)) = found {
|
||||
self.next_order.push((id, slot));
|
||||
}
|
||||
found
|
||||
}
|
||||
|
||||
/// Whether the last tick's `k`-th slot still holds the entity `id`.
|
||||
fn holds(&self, k: usize, id: i32) -> bool {
|
||||
(self.slots[self.order[k].1 as usize].as_ref()).is_some_and(|(held, _)| *held == id)
|
||||
}
|
||||
|
||||
/// Notes an entity met this tick that was not tracked before it.
|
||||
pub(super) fn met_new(&mut self, id: i32, slot: u32) {
|
||||
self.next_order.push((id, slot));
|
||||
}
|
||||
|
||||
/// The next entity is looked for after the last tick's `index`-th: where the one just
|
||||
/// found by id was then.
|
||||
pub(super) fn resume_after(&mut self, index: usize) {
|
||||
self.cursor = index + 1;
|
||||
}
|
||||
|
||||
/// This tick's order is the one the next tick looks in; the slot of the tick's
|
||||
/// `index`-th entity, until then.
|
||||
pub(super) fn end(&mut self) {
|
||||
std::mem::swap(&mut self.order, &mut self.next_order);
|
||||
}
|
||||
|
||||
pub(super) fn slot_of_index(&self, index: usize) -> u32 {
|
||||
self.order[index].1
|
||||
}
|
||||
|
||||
/// Keeps only the entities `keep` says to.
|
||||
pub(super) fn retain(&mut self, mut keep: impl FnMut(&ServerEntity) -> bool) {
|
||||
for (slot, entry) in self.slots.iter_mut().enumerate() {
|
||||
if let Some((id, s)) = entry
|
||||
&& !keep(s)
|
||||
{
|
||||
self.by_id.remove(id);
|
||||
*entry = None;
|
||||
self.free.push(slot as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::{EntityState, Riding};
|
||||
use super::*;
|
||||
|
||||
fn entity(id: i32) -> ServerEntity {
|
||||
let e = EntityState {
|
||||
id,
|
||||
kind: "minecraft:item".into(),
|
||||
..Default::default()
|
||||
};
|
||||
ServerEntity::new(&e, Some(1), &Riding::default())
|
||||
}
|
||||
|
||||
/// One tick meeting `ids` in order: each one's slot and whether it was
|
||||
/// found by id; the new ones are added.
|
||||
fn tick(all: &mut ServerEntities, ids: &[i32]) -> Vec<(u32, bool)> {
|
||||
all.begin();
|
||||
let met = (ids.iter())
|
||||
.map(|&id| {
|
||||
all.meet(id).unwrap_or_else(|| {
|
||||
let slot = all.insert(id, entity(id));
|
||||
all.met_new(id, slot);
|
||||
(slot, true)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
all.end();
|
||||
met
|
||||
}
|
||||
|
||||
/// The entities met in last tick's order are found there, past the ones
|
||||
/// that left; the rest by id, each in its own slot, freed ones reused.
|
||||
#[test]
|
||||
fn entities_are_found_in_last_ticks_order_or_by_id() {
|
||||
let mut all = ServerEntities::default();
|
||||
let first = tick(&mut all, &[10, 11, 12, 13, 14, 15]);
|
||||
assert_eq!(
|
||||
first.iter().map(|m| m.0).collect::<Vec<_>>(),
|
||||
[0, 1, 2, 3, 4, 5]
|
||||
);
|
||||
// 11 and 12 left; 20 joined between 13 and 14.
|
||||
let met = tick(&mut all, &[10, 13, 20, 14, 15]);
|
||||
assert_eq!(
|
||||
met,
|
||||
[(0, false), (3, false), (6, true), (4, false), (5, false)]
|
||||
);
|
||||
// 15 out of order is found by id; the next ones in order again.
|
||||
let met = tick(&mut all, &[15, 10, 20]);
|
||||
assert_eq!(met, [(5, true), (0, false), (6, false)]);
|
||||
// Only those three are left in the level: the others' slots go to
|
||||
// the next ones to join.
|
||||
for (slot, _) in &met {
|
||||
all.at_mut(*slot).seen = 1;
|
||||
}
|
||||
all.retain(|s| s.seen == 1);
|
||||
assert_eq!(all.len(), 3);
|
||||
assert!(!all.contains_key(&13) && all.get(&20).is_some());
|
||||
let met = tick(&mut all, &[15, 10, 20, 30, 31, 32]);
|
||||
let slots: Vec<u32> = met.iter().map(|m| m.0).collect();
|
||||
assert_eq!(slots, [5, 0, 6, 4, 3, 2]);
|
||||
assert_eq!(all.slot_of_index(4), 3);
|
||||
}
|
||||
}
|
||||
@@ -3318,7 +3318,7 @@ impl Hub {
|
||||
continue;
|
||||
};
|
||||
let alpha = if ambient { 38 } else { 255 };
|
||||
let argb = (alpha << 24) | def.color as i32;
|
||||
let argb = (alpha << 24) | def.shown_color() as i32;
|
||||
("entity_effect", Some(ParticleData::Color(argb)))
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1173,7 +1173,7 @@ pub struct Extra {
|
||||
pub sitting: bool,
|
||||
/// `Parrot.partyParrot` and `jukebox`: the playing jukebox it dances to.
|
||||
pub party_jukebox: Option<[i32; 3]>,
|
||||
/// The owner's UUID (tamed animals).
|
||||
/// The owner's UUID: a tamed animal's, or the reference a vex was loaded with.
|
||||
pub owner: Option<[i32; 4]>,
|
||||
/// `Vindicator.isJohnny`: a custom name that was ever `Johnny`, which
|
||||
/// `setCustomName` latches and nothing clears.
|
||||
|
||||
@@ -510,9 +510,6 @@ pub fn base_value(m: &Mob, a: Attr) -> f64 {
|
||||
/// [`base_value`] with the type's default given (`default_value`), for a
|
||||
/// caller that already looked it up for the mob's type.
|
||||
pub fn base_value_with(m: &Mob, a: Attr, default: impl FnOnce() -> Option<f64>) -> f64 {
|
||||
if a == Attr::MovementSpeed && m.species() == Species::Ravager {
|
||||
return m.attrs.speed;
|
||||
}
|
||||
m.extra
|
||||
.attributes
|
||||
.as_ref()
|
||||
@@ -532,8 +529,7 @@ pub fn base_value_with(m: &Mob, a: Attr, default: impl FnOnce() -> Option<f64>)
|
||||
/// growing up) is saved apart; these are the rest:
|
||||
/// - `movement_speed`: Zombie and Piglin.setBaby (the load calls it even for an
|
||||
/// adult), `Enderman.setTarget` (the load ends in `readPersistentAngerSaveData`,
|
||||
/// which calls it), a Ravager whose load leaves it immobile, and the first tick of
|
||||
/// any mob (`LivingEntity.removeFrost`).
|
||||
/// which calls it), and the first tick of any mob (`LivingEntity.removeFrost`).
|
||||
/// - `max_health`: `Wolf.applyTamingSideEffects`, through `setTame(false, true)`
|
||||
/// when the load finds no owner (a wolf that is tamed in game makes it too).
|
||||
/// - `attack_damage`: `Rabbit.setVariant`, and a baby Goat or Hoglin, whose load's
|
||||
@@ -541,11 +537,7 @@ pub fn base_value_with(m: &Mob, a: Attr, default: impl FnOnce() -> Option<f64>)
|
||||
pub fn made_at_load(m: &Mob) -> Vec<Attr> {
|
||||
let s = m.species();
|
||||
let mut made = Vec::new();
|
||||
if super::doors::is_zombie(s)
|
||||
|| matches!(s, Species::Piglin | Species::Enderman)
|
||||
|| (s == Species::Ravager && super::ravager::immobile(m))
|
||||
|| m.age > 0
|
||||
{
|
||||
if super::doors::is_zombie(s) || matches!(s, Species::Piglin | Species::Enderman) || m.age > 0 {
|
||||
made.push(Attr::MovementSpeed);
|
||||
}
|
||||
if s == Species::Wolf {
|
||||
|
||||
@@ -95,18 +95,16 @@ pub fn ai_step(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
|
||||
if !m.alive() {
|
||||
return;
|
||||
}
|
||||
let max = if immobile(m) {
|
||||
0.0
|
||||
} else if m.ai.target.is_some() {
|
||||
0.35
|
||||
} else {
|
||||
0.3
|
||||
};
|
||||
m.attrs.speed = if max == 0.0 {
|
||||
// The speed lives in the attribute's base, which the save writes and the
|
||||
// load restores (AttributeMap.pack and apply).
|
||||
let speed = if immobile(m) {
|
||||
0.0
|
||||
} else {
|
||||
m.attrs.speed + (max - m.attrs.speed) * 0.1
|
||||
let max = if m.ai.target.is_some() { 0.35 } else { 0.3 };
|
||||
let base = super::finalize::base_value(m, super::finalize::Attr::MovementSpeed);
|
||||
base + (max - base) * 0.1
|
||||
};
|
||||
super::finalize::set_base(m, super::finalize::Attr::MovementSpeed, speed);
|
||||
if m.horizontal_collision && w.mob_griefing {
|
||||
let b = m
|
||||
.bounds()
|
||||
|
||||
@@ -152,7 +152,7 @@ pub fn armor_tint(undyed: Option<u32>, stack: Option<&crate::items::ItemStack>)
|
||||
.unwrap_or(undyed);
|
||||
abgr(dyed)
|
||||
}
|
||||
pub use crate::pack_dyes::{diffuse as dye, sheep_wool as sheep_wool_rgb};
|
||||
pub use crate::pack_palette::{diffuse as dye, sheep_wool as sheep_wool_rgb};
|
||||
/// `ColorLerper.getLerpedColor(ColorLerper.Type.SHEEP, ageInTicks)`: the 16
|
||||
/// wool colours in DyeColor order, one per 25 ticks, lerped in between. The
|
||||
/// colour a `jeb_` sheep's wool takes.
|
||||
|
||||
@@ -143,6 +143,21 @@ impl Goal for RandomMove {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// `Vex.getOwner`: the mob the vex is bound to. `counter2` holds its id while
|
||||
/// the evoker that summoned it is in the level; a vex loaded from a save knows
|
||||
/// only the UUID it was saved with, and vanilla's `EntityReference.getEntity`
|
||||
/// asks the level for that entity every time it is called
|
||||
/// (`MobWorld::who_by_uuid`), so an owner that loaded later is found too.
|
||||
pub fn owner(m: &Mob, lookup: Option<&dyn super::WhoLookup>) -> Option<u32> {
|
||||
if m.extra.counter2 != 0 {
|
||||
return Some(m.extra.counter2 as u32);
|
||||
}
|
||||
match lookup?.who_by_uuid(m.extra.owner?)? {
|
||||
Who::Mob(id) => Some(id),
|
||||
// `Vex.setOwner` takes a `Mob`: a UUID naming the player is no owner.
|
||||
Who::Player => None,
|
||||
}
|
||||
}
|
||||
/// `VexCopyOwnerTargetGoal`: the evoker's target.
|
||||
#[derive(Clone, Debug)]
|
||||
struct CopyOwnerTarget;
|
||||
@@ -152,26 +167,24 @@ impl Goal for CopyOwnerTarget {
|
||||
super::goals::TARGET
|
||||
}
|
||||
fn can_use(&mut self, m: &mut Mob, cx: &mut Cx) -> bool {
|
||||
let owner = m.extra.counter2 as u32;
|
||||
owner != 0
|
||||
&& cx
|
||||
.w
|
||||
.others
|
||||
let seen = owner(m, cx.w.who_lookup).and_then(|owner| {
|
||||
cx.w.others
|
||||
.iter()
|
||||
.find(|s| s.id == owner)
|
||||
.and_then(|s| s.target)
|
||||
.is_some_and(|t| t != Who::Mob(m.id))
|
||||
});
|
||||
seen.is_some_and(|t| t != Who::Mob(m.id))
|
||||
}
|
||||
fn can_continue(&mut self, m: &mut Mob, cx: &mut Cx) -> bool {
|
||||
m.target_info(cx.w).is_some_and(|t| t.alive)
|
||||
}
|
||||
fn start(&mut self, m: &mut Mob, cx: &mut Cx) {
|
||||
let owner = m.extra.counter2 as u32;
|
||||
m.ai.target =
|
||||
m.ai.target = owner(m, cx.w.who_lookup).and_then(|owner| {
|
||||
cx.w.others
|
||||
.iter()
|
||||
.find(|s| s.id == owner)
|
||||
.and_then(|s| s.target);
|
||||
.and_then(|s| s.target)
|
||||
});
|
||||
}
|
||||
}
|
||||
/// `VexMoveControl`: accelerate straight at the wanted point.
|
||||
@@ -275,3 +288,149 @@ pub fn texture(m: &Mob) -> &'static str {
|
||||
"entity/illager/vex"
|
||||
}
|
||||
}
|
||||
|
||||
/// `Vex.addAdditionalSaveData`: the bound origin, the limited life and the
|
||||
/// owner reference's UUID (`EntityReference.store`, left out when there is no
|
||||
/// owner). The level answers for an owner still in it; a reference the vex was
|
||||
/// loaded with stands when the owner is gone.
|
||||
pub fn write_nbt(
|
||||
m: &Mob,
|
||||
map: &mut horizon_world::nbt::Compound,
|
||||
uuid_of: &mut impl FnMut(u32) -> Option<[i32; 4]>,
|
||||
) {
|
||||
use horizon_world::nbt::Tag;
|
||||
if let Some(h) = m.extra.home {
|
||||
map.insert("bound_pos".into(), Tag::IntArray(h.to_vec()));
|
||||
}
|
||||
if m.extra.egg_time > 0 {
|
||||
map.insert("life_ticks".into(), Tag::Int(m.extra.egg_time));
|
||||
}
|
||||
let resolved = if m.extra.counter2 != 0 {
|
||||
crate::entity_nbt::who_uuid(Who::Mob(m.extra.counter2 as u32), uuid_of)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
match resolved.or(m.extra.owner) {
|
||||
Some(uuid) => {
|
||||
map.insert("owner".into(), Tag::IntArray(uuid.to_vec()));
|
||||
}
|
||||
None => {
|
||||
map.remove(&"owner".into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `Vex.readAdditionalSaveData`: the bound origin, the life left (-1 when
|
||||
/// absent) and the owner's UUID, kept as the UUID it is - an id would name
|
||||
/// whatever mob has it now - and resolved through the level when the vex asks
|
||||
/// for its owner (`owner`).
|
||||
pub fn read_nbt(m: &mut Mob, tag: &horizon_world::nbt::Tag) {
|
||||
use horizon_world::nbt::Tag;
|
||||
if let Some(Tag::IntArray(v)) = tag.get("bound_pos")
|
||||
&& v.len() == 3
|
||||
{
|
||||
m.extra.home = Some([v[0], v[1], v[2]]);
|
||||
}
|
||||
m.extra.egg_time = crate::save::number(tag.get("life_ticks")).map_or(-1, |v| v as i32);
|
||||
m.extra.owner = crate::save::uuid_from_tag(tag.get("owner"));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod owner_save_tests {
|
||||
use super::*;
|
||||
use crate::entity_nbt::{mob_from_nbt, mob_to_nbt};
|
||||
use crate::mobs::WhoLookup;
|
||||
use horizon_world::nbt::Tag;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A vex with the id `EVOKER` in the level: what `Vex.setOwner` records
|
||||
/// when an evoker summons it.
|
||||
const EVOKER: u32 = 7;
|
||||
|
||||
fn vex() -> Mob {
|
||||
let mut kind = crate::mobs::sensor_scan_tests::kind();
|
||||
let ty = Arc::make_mut(&mut kind);
|
||||
ty.id = "minecraft:vex".into();
|
||||
ty.species = Species::Vex;
|
||||
Mob::new(1, kind, Vec3::ZERO, 0.0)
|
||||
}
|
||||
|
||||
/// `Level.getEntity(uuid)` for the test: `Game` answers this with
|
||||
/// `Game::who_by_uuid`.
|
||||
struct Level(HashMap<[i32; 4], Who>);
|
||||
impl WhoLookup for Level {
|
||||
fn who_by_uuid(&self, uuid: [i32; 4]) -> Option<Who> {
|
||||
self.0.get(&uuid).copied()
|
||||
}
|
||||
}
|
||||
|
||||
/// `Vex.addAdditionalSaveData` stores its owner reference's UUID
|
||||
/// (`EntityReference.store`, nothing at all without an owner) and
|
||||
/// `readAdditionalSaveData` reads it back (`EntityReference.read`), so a
|
||||
/// summoned vex keeps the evoker whose target it copies across a chunk
|
||||
/// save. Horizon models the owner as the owner's mob id and wrote neither,
|
||||
/// so a reloaded vex had no owner to copy a target from.
|
||||
#[test]
|
||||
fn a_vexs_owner_uuid_survives_the_chunk_save() {
|
||||
let evoker_uuid = [3, 1, 4, 1];
|
||||
let mut summoned = vex();
|
||||
assert_eq!(owner(&summoned, None), None, "a vex nobody summoned");
|
||||
summoned.extra.counter2 = EVOKER as i32;
|
||||
assert_eq!(owner(&summoned, None), Some(EVOKER), "the evoker's id");
|
||||
let uuid_of = |id: u32| (id == EVOKER).then_some(evoker_uuid);
|
||||
let saved = mob_to_nbt(&summoned, [0; 4], None, None, uuid_of);
|
||||
assert_eq!(
|
||||
saved.get("owner"),
|
||||
Some(&Tag::IntArray(evoker_uuid.to_vec()))
|
||||
);
|
||||
// The loaded vex knows the UUID: no id survives a save, and its owner
|
||||
// is found again only through the level (`EntityReference.getEntity`).
|
||||
let types = [("minecraft:vex".into(), summoned.kind.clone())].into();
|
||||
let back = mob_from_nbt(&saved, &types, 2, None).unwrap();
|
||||
assert_eq!(back.extra.owner, Some(evoker_uuid));
|
||||
assert_eq!(back.extra.counter2, 0);
|
||||
let level = Level(HashMap::from([(evoker_uuid, Who::Mob(42))]));
|
||||
assert_eq!(owner(&back, Some(&level)), Some(42), "the evoker, found");
|
||||
// An owner the level does not have, and a UUID naming the player, are
|
||||
// no owner: `Vex.setOwner` takes a `Mob`.
|
||||
assert_eq!(owner(&back, Some(&Level(HashMap::new()))), None);
|
||||
let player = Level(HashMap::from([(evoker_uuid, Who::Player)]));
|
||||
assert_eq!(owner(&back, Some(&player)), None);
|
||||
assert_eq!(owner(&back, None), None, "a level with no UUIDs");
|
||||
// A save read again keeps the reference even when nothing in the level
|
||||
// answers for it, as `EntityReference` does.
|
||||
let again = mob_to_nbt(&back, [0; 4], None, None, |_| None);
|
||||
assert_eq!(
|
||||
again.get("owner"),
|
||||
Some(&Tag::IntArray(evoker_uuid.to_vec()))
|
||||
);
|
||||
}
|
||||
|
||||
/// `EntityReference.store` writes nothing for a vex with no owner, and a
|
||||
/// save that names none loads with none - neither the `owner` key a loaded
|
||||
/// compound carried nor an id left over may stand.
|
||||
#[test]
|
||||
fn a_vex_with_no_owner_saves_no_owner_key() {
|
||||
let mut loose = vex();
|
||||
let stale = Tag::compound([
|
||||
("id", Tag::String("minecraft:vex".into())),
|
||||
(
|
||||
"Pos",
|
||||
Tag::List(vec![
|
||||
Tag::Double(0.5_f64.to_bits()),
|
||||
Tag::Double(64.0_f64.to_bits()),
|
||||
Tag::Double(0.5_f64.to_bits()),
|
||||
]),
|
||||
),
|
||||
("owner", Tag::IntArray(vec![9, 9, 9, 9])),
|
||||
]);
|
||||
let types = [("minecraft:vex".into(), loose.kind.clone())].into();
|
||||
let loaded = mob_from_nbt(&stale, &types, 3, None).unwrap();
|
||||
assert_eq!(loaded.extra.owner, Some([9, 9, 9, 9]));
|
||||
loose.extra.owner = None;
|
||||
loose.extra.counter2 = 0;
|
||||
let saved = mob_to_nbt(&loose, [0; 4], Some(&stale), None, |_| None);
|
||||
assert_eq!(saved.get("owner"), None, "no owner, no key");
|
||||
}
|
||||
}
|
||||
@@ -1221,6 +1221,32 @@ pub use generated::Hooks;
|
||||
#[doc(hidden)]
|
||||
pub use generated::*;
|
||||
|
||||
/// Runs `body` in a fresh process of this test binary: `mod_api::MODS` is set once per process, and
|
||||
/// another test that set it first would make this one's `set` fail. The parent starts the binary again
|
||||
/// for just this test (`--exact`, `--include-ignored`) and requires that exactly one test ran and passed; the
|
||||
/// child sees `OWN_PROCESS` and runs `body` itself.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn in_own_process(name: &str, body: impl FnOnce()) {
|
||||
const OWN_PROCESS: &str = "HORIZON_TEST_OWN_PROCESS";
|
||||
if std::env::var_os(OWN_PROCESS).is_some_and(|v| v == name) {
|
||||
return body();
|
||||
}
|
||||
let out = std::process::Command::new(std::env::current_exe().unwrap())
|
||||
.args([name, "--exact", "--include-ignored", "--test-threads=1"])
|
||||
.env(OWN_PROCESS, name)
|
||||
.output()
|
||||
.unwrap();
|
||||
let report = format!(
|
||||
"{}{}",
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
assert!(
|
||||
out.status.success() && report.contains("test result: ok. 1 passed;"),
|
||||
"{name} failed in its own process:\n{report}"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -1233,6 +1259,13 @@ mod tests {
|
||||
/// turned on; a test's own list is never switched.
|
||||
#[test]
|
||||
fn a_mod_turned_off_is_skipped_until_turned_on() {
|
||||
crate::mod_api::in_own_process(
|
||||
"mod_api::tests::a_mod_turned_off_is_skipped_until_turned_on",
|
||||
a_mod_turned_off_is_skipped_until_turned_on_in_own_process,
|
||||
);
|
||||
}
|
||||
|
||||
fn a_mod_turned_off_is_skipped_until_turned_on_in_own_process() {
|
||||
name_mods(&["quiet", "broken"]);
|
||||
let _ = MODS.set(Arc::from(vec![
|
||||
Box::new(Quiet) as Box<dyn Hooks>,
|
||||
|
||||
@@ -255,6 +255,9 @@ pub struct Projectile {
|
||||
pub air_supply: i32,
|
||||
/// `Entity.hasVisualFire`: an optional saved flag independent of burning.
|
||||
pub has_visual_fire: bool,
|
||||
/// `Entity.fallDistance` (`fall_distance` in its save): a projectile sets
|
||||
/// its position without `Entity.move`, so it keeps what it was given.
|
||||
pub fall_distance: f64,
|
||||
/// `Entity.DATA_SHARED_FLAGS_ID`'s fire bit, set by the server's base tick.
|
||||
pub synced_fire: bool,
|
||||
/// `Fireball.DATA_ITEM_STACK`: the item a fireball is drawn as, a fire
|
||||
@@ -473,6 +476,7 @@ impl Projectile {
|
||||
fire_ticks: 0,
|
||||
air_supply: 300,
|
||||
has_visual_fire: false,
|
||||
fall_distance: 0.0,
|
||||
synced_fire: false,
|
||||
item,
|
||||
arrow: kind_is_arrow.then(|| {
|
||||
@@ -675,7 +679,7 @@ impl Projectile {
|
||||
return -1;
|
||||
}
|
||||
let rgb = match (effect, &self.arrow) {
|
||||
(Some((id, _)), _) => crate::brewing::effect(id).map(|e| e.color),
|
||||
(Some((id, _)), _) => crate::brewing::effect(id).map(|e| e.shown_color()),
|
||||
(None, Some(a)) if &*a.pickup_item.item == "minecraft:tipped_arrow" => a
|
||||
.pickup_item
|
||||
.potion()
|
||||
|
||||
Generated
+9
-1
@@ -493,11 +493,19 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "horizon-shader-api"
|
||||
name = "horizon-render-pipeline"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"naga",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "horizon-shader-api"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"horizon-render-pipeline",
|
||||
"naga",
|
||||
"png",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
||||
@@ -8,10 +8,41 @@ use std::path::Path;
|
||||
|
||||
use serde_json::Value as Json;
|
||||
|
||||
/// The `environment` fog settings vanilla hardcodes, as the packs set them (vanilla's by default).
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Environment {
|
||||
/// `powder_snow_fog`: the fog colour inside powder snow (RGB).
|
||||
pub powder_snow_fog: Option<u32>,
|
||||
/// `blindness_fog_distance`, `darkness_fog_distance`, `boss_fog_distance`: the fog ends of
|
||||
/// Blindness (5), Darkness (15) and bosses that add world fog (96).
|
||||
pub blindness_fog_distance: f32,
|
||||
pub darkness_fog_distance: f32,
|
||||
pub boss_fog_distance: f32,
|
||||
/// `water_fog_brightening`: how much the underwater fog colour washes out (1 vanilla, 0 none).
|
||||
pub water_fog_brightening: f32,
|
||||
/// `water_fog_brightening_time`: the ticks under water it takes (vanilla's 600).
|
||||
pub water_fog_brightening_time: f32,
|
||||
}
|
||||
|
||||
impl Default for Environment {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
powder_snow_fog: None,
|
||||
blindness_fog_distance: 5.0,
|
||||
darkness_fog_distance: 15.0,
|
||||
boss_fog_distance: 96.0,
|
||||
water_fog_brightening: 1.0,
|
||||
water_fog_brightening_time: 600.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Every pack's custom colours, by dotted key.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct PackColors {
|
||||
entries: BTreeMap<String, Json>,
|
||||
/// [`Self::environment`], read once with the packs: the fog reads it every frame.
|
||||
pub env: Environment,
|
||||
}
|
||||
|
||||
impl PackColors {
|
||||
@@ -38,6 +69,7 @@ impl PackColors {
|
||||
}
|
||||
}
|
||||
}
|
||||
colors.env = colors.environment();
|
||||
colors
|
||||
}
|
||||
|
||||
@@ -93,6 +125,91 @@ impl PackColors {
|
||||
self.color(&format!("sheep.{name}"))
|
||||
}
|
||||
|
||||
/// `redstone.<power>`: the wire's and its dust particles' colour at that power (0 to 15).
|
||||
pub fn redstone(&self, power: u8) -> Option<u32> {
|
||||
self.color(&format!("redstone.{power}"))
|
||||
}
|
||||
|
||||
/// `effect.<id>` or `effect.<id>.color` (the deprecated `potion.<id>` too): mob effects'
|
||||
/// colours by id without vanilla's namespace, as the effects table names them. `water` and
|
||||
/// `empty` are left out: they change nothing on the real mod.
|
||||
pub fn effects(&self) -> Vec<(String, u32)> {
|
||||
let mut out = Vec::new();
|
||||
for (key, value) in &self.entries {
|
||||
let Some(rest) = key
|
||||
.strip_prefix("effect.")
|
||||
.or_else(|| key.strip_prefix("potion."))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let id = rest.strip_suffix(".color").unwrap_or(rest);
|
||||
if id.ends_with(".particle") || matches!(id, "water" | "empty" | "minecraft:water") {
|
||||
continue;
|
||||
}
|
||||
if let Some(color) = parse_color(value) {
|
||||
let id = id.strip_prefix("minecraft:").unwrap_or(id);
|
||||
// `effect.<id>.color` wins over the plain key whichever comes first.
|
||||
out.retain(|(other, _)| other != id || !key.ends_with(".color"));
|
||||
if !out.iter().any(|(other, _)| other == id) {
|
||||
out.push((id.to_owned(), color));
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// `particle.<id>`: a colour or an expression each new particle of that type takes, by
|
||||
/// namespaced type id.
|
||||
pub fn particles(&self) -> Vec<(String, String)> {
|
||||
self.entries
|
||||
.iter()
|
||||
.filter_map(|(key, value)| {
|
||||
let id = key.strip_prefix("particle.")?;
|
||||
let id = if id.contains(':') {
|
||||
id.to_owned()
|
||||
} else {
|
||||
format!("minecraft:{id}")
|
||||
};
|
||||
let value = match value {
|
||||
Json::String(s) => s.trim().to_owned(),
|
||||
Json::Number(n) => n.to_string(),
|
||||
_ => return None,
|
||||
};
|
||||
Some((id, value))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// `environment.*`: the fog settings a pack replaces.
|
||||
pub fn environment(&self) -> Environment {
|
||||
let number = |key: &str, default: f32| {
|
||||
match self.entries.get(&format!("environment.{key}")) {
|
||||
Some(Json::Number(n)) => n.as_f64().map(|v| v as f32),
|
||||
Some(Json::String(s)) => s.trim().parse().ok(),
|
||||
_ => None,
|
||||
}
|
||||
.unwrap_or(default)
|
||||
};
|
||||
let vanilla = Environment::default();
|
||||
Environment {
|
||||
powder_snow_fog: self
|
||||
.color("environment.powder_snow_fog")
|
||||
.map(|c| c & 0xFF_FFFF),
|
||||
blindness_fog_distance: number(
|
||||
"blindness_fog_distance",
|
||||
vanilla.blindness_fog_distance,
|
||||
),
|
||||
darkness_fog_distance: number("darkness_fog_distance", vanilla.darkness_fog_distance),
|
||||
boss_fog_distance: number("boss_fog_distance", vanilla.boss_fog_distance),
|
||||
water_fog_brightening: number("water_fog_brightening", vanilla.water_fog_brightening),
|
||||
water_fog_brightening_time: number(
|
||||
"water_fog_brightening_time",
|
||||
vanilla.water_fog_brightening_time,
|
||||
)
|
||||
.max(1.0),
|
||||
}
|
||||
}
|
||||
|
||||
/// `entity_shadows`: false hides every entity's round shadow, whatever the video option says.
|
||||
pub fn entity_shadows(&self) -> bool {
|
||||
match self.entries.get("entity_shadows") {
|
||||
@@ -161,6 +278,55 @@ mod tests {
|
||||
assert_eq!(p.xp_bar(), [Some(0x00ff22), Some(u32::MAX)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effects_particles_and_redstone_by_id() {
|
||||
let mut c = PackColors::default();
|
||||
c.add_json(
|
||||
"",
|
||||
&json!({
|
||||
"effect": {"minecraft:speed": {"color": "ff0000", "particle": "minecraft:poof"}, "water": "00ff00", "poison": 7},
|
||||
"particle": {"minecraft:flame": "0x0000ff", "poof": 0},
|
||||
"redstone": {"15": "00ff00"}
|
||||
}),
|
||||
);
|
||||
let mut effects = c.effects();
|
||||
effects.sort();
|
||||
assert_eq!(
|
||||
effects,
|
||||
[("poison".to_owned(), 7), ("speed".to_owned(), 0xff0000)]
|
||||
);
|
||||
let mut particles = c.particles();
|
||||
particles.sort();
|
||||
assert_eq!(
|
||||
particles,
|
||||
[
|
||||
("minecraft:flame".to_owned(), "0x0000ff".to_owned()),
|
||||
("minecraft:poof".to_owned(), "0".to_owned())
|
||||
]
|
||||
);
|
||||
assert_eq!((c.redstone(15), c.redstone(0)), (Some(0x00ff00), None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn environment_keeps_vanillas_where_unset() {
|
||||
let mut c = PackColors::default();
|
||||
assert_eq!(c.environment(), Environment::default());
|
||||
c.add_json(
|
||||
"",
|
||||
&json!({"environment": {"powder_snow_fog": "#c0c0d0", "blindness_fog_distance": 40, "water_fog_brightening_time": "200"}}),
|
||||
);
|
||||
let e = c.environment();
|
||||
assert_eq!(e.powder_snow_fog, Some(0xc0c0d0));
|
||||
assert_eq!(
|
||||
(e.blindness_fog_distance, e.darkness_fog_distance),
|
||||
(40.0, 15.0)
|
||||
);
|
||||
assert_eq!(
|
||||
(e.water_fog_brightening, e.water_fog_brightening_time),
|
||||
(1.0, 200.0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dyes_name_their_colour_kind() {
|
||||
let mut c = PackColors::default();
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "horizon-render-pipeline"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
description = "What an external shader pipeline gives the renderer: its buffers, shadow map and programs. Engine layer; the pack formats that fill it are platform crates"
|
||||
|
||||
[dependencies]
|
||||
# The frame uniform is uploaded as bytes (inputs.rs).
|
||||
bytemuck = { version = "1.24", features = ["derive"] }
|
||||
# A translated program's stages are naga modules (program.rs).
|
||||
naga = "30.0.1"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,33 @@
|
||||
//! The lines of the WGSL stage interface ([`crate::stages`], [`crate::inputs`]).
|
||||
|
||||
/// A shader API line: the stages, their signatures and the inputs a pack's WGSL may read.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum Api {
|
||||
/// Vanilla's gamma values.
|
||||
V0_1,
|
||||
/// Linear light (docs/design/hdr-linear-light.md). Experimental: checked, not yet drawn.
|
||||
V0_2,
|
||||
}
|
||||
|
||||
impl Api {
|
||||
/// Every line this build reads and checks, oldest first.
|
||||
pub const SERVED: [Api; 2] = [Api::V0_1, Api::V0_2];
|
||||
|
||||
/// The manifest's `api` value.
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Api::V0_1 => "0.1",
|
||||
Api::V0_2 => "0.2",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(line: &str) -> Option<Api> {
|
||||
Self::SERVED.into_iter().find(|a| a.as_str() == line)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Api {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
//! The pipeline's buffers and shadow map (spec 1.7, 6.1, 6.3, 7.1): each colortex buffer's
|
||||
//! format, clearing and mipmaps, the shadow map's settings, and the names packs give them. A pack
|
||||
//! format's reader fills a [`Config`] (`horizon-shaders` reads them from a `shaders/` pack's stage
|
||||
//! files); the renderer makes its targets from it.
|
||||
|
||||
/// colortex buffers (6.1.1).
|
||||
pub const BUFFERS: usize = 16;
|
||||
|
||||
/// The shadowcolor buffers there are: two, and `shadowcolor2` to `shadowcolor7` for a pack that
|
||||
/// names the `HIGHER_SHADOWCOLOR` feature.
|
||||
pub const SHADOW_COLORS: usize = 8;
|
||||
|
||||
/// A colour format of 6.7, as channels, bits per channel and kind. Packed formats are described
|
||||
/// by their widest channel: `RGB10_A2` is 4 × 10 bits, `R11F_G11F_B10F` 3 × 11 bits float.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Format {
|
||||
pub channels: u8,
|
||||
pub bits: u8,
|
||||
pub kind: Kind,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Kind {
|
||||
Unorm,
|
||||
Snorm,
|
||||
Float,
|
||||
Int,
|
||||
Uint,
|
||||
}
|
||||
|
||||
impl Format {
|
||||
/// The default `RGBA` (6.1.3).
|
||||
pub const RGBA8: Format = Format {
|
||||
channels: 4,
|
||||
bits: 8,
|
||||
kind: Kind::Unorm,
|
||||
};
|
||||
|
||||
/// A format name of 6.7, in any case.
|
||||
pub fn parse(name: &str) -> Option<Format> {
|
||||
let name = name.trim().to_ascii_uppercase();
|
||||
let packed = |channels, bits, kind| {
|
||||
Some(Format {
|
||||
channels,
|
||||
bits,
|
||||
kind,
|
||||
})
|
||||
};
|
||||
match name.as_str() {
|
||||
"RGBA" => return Some(Self::RGBA8),
|
||||
"R3_G3_B2" => return packed(3, 3, Kind::Unorm),
|
||||
"RGB5_A1" => return packed(4, 5, Kind::Unorm),
|
||||
"RGB10_A2" => return packed(4, 10, Kind::Unorm),
|
||||
"RGB10_A2UI" => return packed(4, 10, Kind::Uint),
|
||||
"R11F_G11F_B10F" => return packed(3, 11, Kind::Float),
|
||||
"RGB9_E5" => return packed(3, 9, Kind::Float),
|
||||
"RGBA4" => return packed(4, 4, Kind::Unorm),
|
||||
"RGB565" => return packed(3, 6, Kind::Unorm),
|
||||
"RGBA2" => return packed(4, 2, Kind::Unorm),
|
||||
_ => {}
|
||||
}
|
||||
let letters = name.bytes().take_while(u8::is_ascii_alphabetic).count();
|
||||
let channels = match &name[..letters] {
|
||||
"R" => 1,
|
||||
"RG" => 2,
|
||||
"RGB" => 3,
|
||||
"RGBA" => 4,
|
||||
_ => return None,
|
||||
};
|
||||
let rest = &name[letters..];
|
||||
let digits = rest.bytes().take_while(u8::is_ascii_digit).count();
|
||||
let bits: u8 = rest[..digits].parse().ok()?;
|
||||
let kind = match &rest[digits..] {
|
||||
"" => Kind::Unorm,
|
||||
"_SNORM" => Kind::Snorm,
|
||||
"F" => Kind::Float,
|
||||
"I" => Kind::Int,
|
||||
"UI" => Kind::Uint,
|
||||
_ => return None,
|
||||
};
|
||||
let valid = match kind {
|
||||
Kind::Unorm | Kind::Snorm => matches!(bits, 8 | 16),
|
||||
Kind::Float => matches!(bits, 16 | 32),
|
||||
Kind::Int | Kind::Uint => matches!(bits, 8 | 16 | 32),
|
||||
};
|
||||
valid.then_some(Format {
|
||||
channels,
|
||||
bits,
|
||||
kind,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The settings of one colortex buffer.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Buffer {
|
||||
pub format: Format,
|
||||
/// Cleared every frame (`<buffer>Clear`, default true).
|
||||
pub clear: bool,
|
||||
/// `<buffer>ClearColor`, else 6.1.5's default: colortex0 the fog colour, colortex1 white,
|
||||
/// the others transparent black.
|
||||
pub clear_color: Option<[f32; 4]>,
|
||||
/// `<buffer>MipmapEnabled`.
|
||||
pub mipmap: bool,
|
||||
}
|
||||
|
||||
impl Default for Buffer {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
format: Format::RGBA8,
|
||||
clear: true,
|
||||
clear_color: None,
|
||||
mipmap: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The shadow map's settings (6.3, 7.1), from `const` lines of the stage files.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Shadow {
|
||||
/// `shadowMapResolution`: the map's side in pixels.
|
||||
pub resolution: u32,
|
||||
/// `shadowDistance`: half the side of the area the map covers, in blocks.
|
||||
pub distance: f32,
|
||||
/// `shadowDistanceRenderMul`: when > 0, geometry beyond `distance × mul` is not drawn.
|
||||
pub render_mul: f32,
|
||||
/// `voxelDistance`: with `shadow.culling=reversed`, nothing within it is culled.
|
||||
pub voxel_distance: f32,
|
||||
/// `shadowIntervalSize`: the shadow camera moves in steps of this many blocks (0: none).
|
||||
pub interval: f32,
|
||||
/// Native packs: the snap's remainder is euclidean, so the centre is within half an interval
|
||||
/// of the player at negative coordinates too. `shaders/` packs keep Iris's.
|
||||
pub euclidean_snap: bool,
|
||||
/// `sunPathRotation`, degrees.
|
||||
pub sun_path_rotation: f32,
|
||||
/// `shadowMapFov`: a perspective projection of this many degrees instead of orthographic.
|
||||
pub fov: Option<f32>,
|
||||
/// `shadowNearPlane`, `shadowFarPlane`: the orthographic projection's planes, as JOML's
|
||||
/// `zNear`/`zFar`. None when the pack doesn't set one: Iris 1.11.7 then uses -100.05 and
|
||||
/// 156 (measured with a probe pack), each on its own.
|
||||
pub near: Option<f32>,
|
||||
pub far: Option<f32>,
|
||||
/// `shadowHardwareFiltering(0|1)`: shadowtex0/1 sampled with depth comparison.
|
||||
pub hardware_filtering: [bool; 2],
|
||||
/// `shadowtex0Nearest` (or `shadowtexNearest`, `shadow0MinMagNearest`) and
|
||||
/// `shadowtex1Nearest` (or `shadow1MinMagNearest`): shadowtex0/1 read through a plain sampler
|
||||
/// take the nearest texel. Otherwise Iris 1.11.7 filters them linearly (measured).
|
||||
pub nearest: [bool; 2],
|
||||
/// `shadowcolor0` to `shadowcolor7`: format, cleared every frame, clear colour (default
|
||||
/// white).
|
||||
pub colors: [Buffer; SHADOW_COLORS],
|
||||
}
|
||||
|
||||
impl Default for Shadow {
|
||||
fn default() -> Self {
|
||||
let white = Buffer {
|
||||
clear_color: Some([1.0; 4]),
|
||||
..Buffer::default()
|
||||
};
|
||||
Self {
|
||||
resolution: 1024,
|
||||
distance: 160.0,
|
||||
render_mul: -1.0,
|
||||
voxel_distance: 160.0,
|
||||
interval: 2.0,
|
||||
euclidean_snap: false,
|
||||
sun_path_rotation: 0.0,
|
||||
fov: None,
|
||||
near: None,
|
||||
far: None,
|
||||
hardware_filtering: [false; 2],
|
||||
nearest: [false; 2],
|
||||
colors: std::array::from_fn(|_| white.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Config {
|
||||
pub buffers: [Buffer; BUFFERS],
|
||||
pub shadow: Shadow,
|
||||
/// `noiseTextureResolution`: the side of the `noisetex` Horizon makes when the pack gives none.
|
||||
pub noise_resolution: Option<u32>,
|
||||
/// The smoothed uniforms' half-lives in ticks, in [`HALF_LIVES`] order, when the pack sets
|
||||
/// them (`const float wetnessHalflife = 30.0;`...).
|
||||
pub half_lives: [Option<f32>; 4],
|
||||
/// Conflicting or unreadable settings, as `file:line: message`.
|
||||
pub notes: Vec<String>,
|
||||
/// Each colortex setting the pack's files set so far and its value, so that its reader can
|
||||
/// report a later file changing it.
|
||||
pub settings: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
buffers: std::array::from_fn(|_| Buffer::default()),
|
||||
shadow: Shadow::default(),
|
||||
noise_resolution: None,
|
||||
half_lives: [None; 4],
|
||||
notes: Vec::new(),
|
||||
settings: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The shadowcolor buffer a sampler or image name reads: `shadowcolor` and `shadowcolor0` to
|
||||
/// `shadowcolor7`.
|
||||
pub fn shadow_color(name: &str) -> Option<usize> {
|
||||
match name.strip_prefix("shadowcolor")?.as_bytes() {
|
||||
[] => Some(0),
|
||||
&[d @ b'0'..=b'7'] => Some(usize::from(d - b'0')),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The colortex index of a buffer name or alias (6.1.1).
|
||||
pub fn buffer_index(name: &str) -> Option<usize> {
|
||||
let alias = [
|
||||
"gcolor",
|
||||
"gdepth",
|
||||
"gnormal",
|
||||
"composite",
|
||||
"gaux1",
|
||||
"gaux2",
|
||||
"gaux3",
|
||||
"gaux4",
|
||||
];
|
||||
if let Some(i) = alias.iter().position(|a| *a == name) {
|
||||
return Some(i);
|
||||
}
|
||||
let n: usize = name.strip_prefix("colortex")?.parse().ok()?;
|
||||
(n < BUFFERS).then_some(n)
|
||||
}
|
||||
|
||||
/// The `const float` half-lives (ShaderDoc, OptiFine's shaders.txt) of `wetness` rising and
|
||||
/// falling, `eyeBrightnessSmooth` and `centerDepthSmooth`, in ticks.
|
||||
pub const HALF_LIVES: [&str; 4] = [
|
||||
"wetnessHalflife",
|
||||
"drynessHalflife",
|
||||
"eyeBrightnessHalflife",
|
||||
"centerDepthHalflife",
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn formats_parse() {
|
||||
let f = |channels, bits, kind| {
|
||||
Some(Format {
|
||||
channels,
|
||||
bits,
|
||||
kind,
|
||||
})
|
||||
};
|
||||
assert_eq!(Format::parse("rgba16f"), f(4, 16, Kind::Float));
|
||||
assert_eq!(Format::parse("R32UI"), f(1, 32, Kind::Uint));
|
||||
assert_eq!(Format::parse("RG8_SNORM"), f(2, 8, Kind::Snorm));
|
||||
assert_eq!(Format::parse("R11F_G11F_B10F"), f(3, 11, Kind::Float));
|
||||
assert_eq!(Format::parse("RGB16"), f(3, 16, Kind::Unorm));
|
||||
assert_eq!(Format::parse("RGBA12"), None);
|
||||
assert_eq!(Format::parse("R16_SNORMX"), None);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
//! A pack's custom uniforms: values the pack computes on the CPU each frame from the game's own
|
||||
//! uniforms, which the renderer then hands its programs after the frame's. The renderer only
|
||||
//! drives the evaluator ([`CustomUniforms`]); the pack format supplies it (`horizon-shaders`'
|
||||
//! `Custom` reads `shaders.properties`' expressions).
|
||||
|
||||
/// A value: up to 16 components (matrices only as names, for `m.column.row`).
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Val {
|
||||
pub v: [f64; 16],
|
||||
/// Components: 1 for a scalar.
|
||||
pub n: usize,
|
||||
}
|
||||
|
||||
impl Val {
|
||||
pub fn scalar(x: f64) -> Self {
|
||||
let mut v = [0.0; 16];
|
||||
v[0] = x;
|
||||
Self { v, n: 1 }
|
||||
}
|
||||
pub fn vector(c: &[f64]) -> Self {
|
||||
let mut v = [0.0; 16];
|
||||
v[..c.len()].copy_from_slice(c);
|
||||
Self { v, n: c.len() }
|
||||
}
|
||||
}
|
||||
|
||||
/// What the renderer asks of a pack's custom uniforms. Called once per frame of a pack, never on
|
||||
/// vanilla's path.
|
||||
pub trait CustomUniforms: Send + Sync {
|
||||
/// The uniforms' names and declared types, in the order [`CustomUniforms::eval`] returns
|
||||
/// their values.
|
||||
fn names(&self) -> Vec<(String, String)>;
|
||||
|
||||
/// Evaluates everything for a frame `dt` seconds after the last; `builtin` gives the values
|
||||
/// of the game's uniforms and constants. Returns the uniforms' values, in `names` order.
|
||||
fn eval(&mut self, dt: f64, builtin: &dyn Fn(&str) -> Option<Val>) -> Vec<Val>;
|
||||
|
||||
/// Forgets what smoothing keeps: each smoothed value starts again from its next value.
|
||||
fn restart_smoothing(&mut self);
|
||||
|
||||
/// Called before each frame's `eval`, with whether the camera's biome is known.
|
||||
fn world_frame(&mut self, biome_known: bool);
|
||||
|
||||
/// Forgets the world's frames, as a pack just loaded has seen none.
|
||||
fn forget_world(&mut self);
|
||||
|
||||
/// Restarts smoothing and randomness for a frame of the world's tick `tick`, so a frame
|
||||
/// drawn again at that tick evaluates the same.
|
||||
fn restart_at(&mut self, tick: u64);
|
||||
|
||||
/// The problems found so far (expressions that did not parse, unknown names), emptied.
|
||||
fn take_notes(&mut self) -> Vec<String>;
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
//! What a pack's WGSL reads: the frame uniform every stage sees, which the
|
||||
//! renderer fills and uploads as the Rust struct below, and the stage input
|
||||
//! and output structs. The WGSL of each is generated from these definitions
|
||||
//! (`Struct::wgsl`), and so is the reference (docs/api/shaders-reference.md).
|
||||
|
||||
/// One field of a WGSL struct, for the prelude and the reference.
|
||||
#[derive(Debug)]
|
||||
pub struct Field {
|
||||
pub name: &'static str,
|
||||
pub ty: &'static str,
|
||||
/// Its meaning, unit and range.
|
||||
pub doc: &'static str,
|
||||
}
|
||||
|
||||
/// A struct a pack's WGSL can name.
|
||||
#[derive(Debug)]
|
||||
pub struct Struct {
|
||||
pub name: &'static str,
|
||||
pub doc: &'static str,
|
||||
pub fields: &'static [Field],
|
||||
}
|
||||
|
||||
impl Struct {
|
||||
pub fn wgsl(&self) -> String {
|
||||
let mut out = format!("struct {} {{\n", self.name);
|
||||
for f in self.fields {
|
||||
out += &format!(" {}: {},\n", f.name, f.ty);
|
||||
}
|
||||
out + "}\n"
|
||||
}
|
||||
}
|
||||
|
||||
/// The WGSL type of a Rust field the renderer uploads.
|
||||
pub trait Wgsl {
|
||||
const TYPE: &'static str;
|
||||
}
|
||||
|
||||
macro_rules! wgsl_types {
|
||||
($($rust:ty => $wgsl:literal),* $(,)?) => {
|
||||
$(impl Wgsl for $rust {
|
||||
const TYPE: &'static str = $wgsl;
|
||||
})*
|
||||
};
|
||||
}
|
||||
|
||||
wgsl_types! {
|
||||
f32 => "f32",
|
||||
u32 => "u32",
|
||||
i32 => "i32",
|
||||
[f32; 2] => "vec2<f32>",
|
||||
[f32; 3] => "vec3<f32>",
|
||||
[f32; 4] => "vec4<f32>",
|
||||
[[f32; 4]; 4] => "mat4x4<f32>",
|
||||
[[f32; 4]; 8] => "array<vec4<f32>, 8>",
|
||||
}
|
||||
|
||||
/// A uniform struct: the Rust type the renderer uploads, and its WGSL. `pad` names a trailing
|
||||
/// field in Rust only, which fills the struct out to WGSL's size (a multiple of its 16-byte
|
||||
/// alignment), since `Pod` allows no implicit padding.
|
||||
macro_rules! uniform {
|
||||
($(#[doc = $doc:literal])* $name:ident {
|
||||
$($(#[doc = $fdoc:literal])* $field:ident: $ty:ty,)*
|
||||
} $(pad $pad:ident: $pad_ty:ty)?) => {
|
||||
$(#[doc = $doc])*
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct $name {
|
||||
$($(#[doc = $fdoc])* pub $field: $ty,)*
|
||||
$(/// Not in the WGSL: zeros up to its size.
|
||||
pub $pad: $pad_ty,)?
|
||||
}
|
||||
|
||||
impl $name {
|
||||
pub const WGSL: Struct = Struct {
|
||||
name: stringify!($name),
|
||||
doc: concat!($($doc, "\n"),*),
|
||||
fields: &[$(Field {
|
||||
name: stringify!($field),
|
||||
ty: <$ty as Wgsl>::TYPE,
|
||||
doc: concat!($($fdoc),*),
|
||||
}),*],
|
||||
};
|
||||
/// Each field's byte offset, in `WGSL.fields`' order.
|
||||
pub const OFFSETS: &'static [usize] = &[$(std::mem::offset_of!($name, $field)),*];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
uniform! {
|
||||
/// The frame, as `frame` in every stage. Positions and directions are
|
||||
/// in world axes relative to the camera (x east, y up, z south), so they
|
||||
/// stay precise far from the world's origin; depth is reversed (1 near,
|
||||
/// 0 at the far plane and in the sky).
|
||||
Frame {
|
||||
/// Camera-relative world to view space: the camera's rotation.
|
||||
view: [[f32; 4]; 4],
|
||||
/// View to clip space (reversed depth, wgpu's clip space: z 0..1).
|
||||
projection: [[f32; 4]; 4],
|
||||
/// View to camera-relative world space.
|
||||
view_inverse: [[f32; 4]; 4],
|
||||
/// Clip to view space.
|
||||
projection_inverse: [[f32; 4]; 4],
|
||||
/// The camera's world position, in blocks (about a millimetre of precision 10 000 blocks out).
|
||||
camera_position: [f32; 3],
|
||||
/// Seconds since the pack was loaded, wrapping to 0 every hour.
|
||||
time: f32,
|
||||
/// Unit vector toward the sun (below the horizon at night).
|
||||
sun_direction: [f32; 3],
|
||||
/// The time of day, 0..1: 0 sunrise, 0.25 noon, 0.5 sunset, 0.75 midnight.
|
||||
day_time: f32,
|
||||
/// Unit vector toward the moon.
|
||||
moon_direction: [f32; 3],
|
||||
/// Vanilla's GameTime: (game time % 24000 + partial tick) / 24000, 0..1.
|
||||
game_time: f32,
|
||||
/// Vanilla's sky colour this frame, 0..1.
|
||||
sky_color: [f32; 3],
|
||||
/// Rain strength, 0..1.
|
||||
rain: f32,
|
||||
/// Vanilla's fog colour this frame (also the clear colour), 0..1.
|
||||
fog_color: [f32; 3],
|
||||
/// Thunder strength, 0..1.
|
||||
thunder: f32,
|
||||
/// The size of the world's picture, in pixels (the resolution scale applied).
|
||||
resolution: [f32; 2],
|
||||
/// Block and sky light at the camera, 0..1 (level / 15).
|
||||
eye_light: [f32; 2],
|
||||
/// Where vanilla's fog starts, in blocks from the camera.
|
||||
fog_start: f32,
|
||||
/// Where vanilla's fog is full, in blocks.
|
||||
fog_end: f32,
|
||||
/// The render distance, in blocks.
|
||||
render_distance: f32,
|
||||
/// The far plane, in blocks.
|
||||
far: f32,
|
||||
/// Night vision strength, 0..1.
|
||||
night_vision: f32,
|
||||
/// Blindness strength, 0..1.
|
||||
blindness: f32,
|
||||
/// The darkness effect's strength, 0..1.
|
||||
darkness: f32,
|
||||
/// The Brightness option, 0..1 (Moody to Bright).
|
||||
brightness: f32,
|
||||
/// What the camera is in: 0 air, 1 water, 2 lava, 3 powder snow.
|
||||
eye_in: u32,
|
||||
/// 0 the Overworld, -1 the Nether, 1 the End, 2 any other dimension.
|
||||
dimension: i32,
|
||||
/// Frames drawn since the pack was loaded (wraps).
|
||||
frame_index: u32,
|
||||
/// 1 while F1 hides the HUD, else 0.
|
||||
hide_gui: u32,
|
||||
/// What falls in the biome at the camera while it rains: 0 nothing, 1 rain, 2 snow.
|
||||
precipitation: u32,
|
||||
/// The biome at the camera's temperature (vanilla's, -0.7..2.0; it snows below 0.15).
|
||||
temperature: f32,
|
||||
/// The biome at the camera's downfall, 0..1.
|
||||
downfall: f32,
|
||||
/// `rain`, smoothed: rises over about 30 seconds and dries over about 10, 0..1.
|
||||
wetness: f32,
|
||||
/// Last frame's camera-relative world to clip space (its `projection * view`); a point
|
||||
/// at camera-relative `p` now was at `p + camera_motion` relative to last frame's camera.
|
||||
/// This frame's on the first frame and after a gap of over half a second (a pause, a
|
||||
/// world load), when last frame's camera says nothing about this one.
|
||||
previous_view_projection: [[f32; 4]; 4],
|
||||
/// How far the camera moved since last frame, in blocks (0 after such a gap).
|
||||
camera_motion: [f32; 3],
|
||||
/// Seconds since last frame (0 on the first and after such a gap).
|
||||
delta_time: f32,
|
||||
/// `eye_light` eased with a half-life of 2 seconds: it follows the light levels' steps
|
||||
/// without stepping itself (for exposure and indoor fog).
|
||||
eye_light_smooth: [f32; 2],
|
||||
/// 1 while the camera is in water (`eye_in` 1), eased with a half-life of a quarter
|
||||
/// second in and out.
|
||||
in_water: f32,
|
||||
/// The same for lava (`eye_in` 2).
|
||||
in_lava: f32,
|
||||
/// The y of the horizontal water plane `reflection` mirrors the world about, in world
|
||||
/// blocks, while `has_reflection` is 1.
|
||||
water_plane: f32,
|
||||
/// 1 while the material stage's `reflection` holds this frame's water mirror, else 0.
|
||||
has_reflection: u32,
|
||||
/// The size of `reflection`, in pixels: half the world's picture.
|
||||
reflection_resolution: [f32; 2],
|
||||
/// Eight values a native mod hands its own pack each frame (`native_shader_params`,
|
||||
/// docs/native-mods.md): what they mean is that mod's, such as where and when
|
||||
/// something happened in the world. All 0 when no mod sets them.
|
||||
mods: [[f32; 4]; 8],
|
||||
/// Unit vector toward the body casting this frame's shadows: the sun by day, the moon
|
||||
/// by night.
|
||||
light_direction: [f32; 3],
|
||||
/// 1 while `shadow_map` holds this frame's shadow map, else 0.
|
||||
has_shadow: u32,
|
||||
/// Camera-relative world to the shadow camera's clip space, before the distortion
|
||||
/// (orthographic, looking along `-light_direction`; z 0 at the light, 1 away from it).
|
||||
shadow_view_proj: [[f32; 4]; 4],
|
||||
/// Half the side of the square the shadow map covers, in blocks (the manifest's
|
||||
/// `[shadow] distance`); 0 without a shadow map.
|
||||
shadow_distance: f32,
|
||||
/// How strongly the shadow map is distorted toward its centre, 0..1: a point at
|
||||
/// shadow clip `xy` is stored at `xy / (length(xy) * k + 1 - k)`, so texels near the
|
||||
/// camera cover less ground (`shadow_position` applies it).
|
||||
shadow_distortion: f32,
|
||||
/// The shadow map's side, in texels; 0 without a shadow map.
|
||||
shadow_resolution: f32,
|
||||
/// The ground one shadow texel covers at the map's centre, in blocks (for a bias that
|
||||
/// scales with it); 0 without a shadow map.
|
||||
shadow_center_texel: f32,
|
||||
/// 1 while the output is HDR, else 0.
|
||||
hdr: u32,
|
||||
/// The peak over paper white: the brightest value the display shows (Apple's EDR
|
||||
/// headroom, Android's HDR/SDR ratio). 1 in SDR. It changes while the game runs (the
|
||||
/// system's SDR white, a calibration, another display), so read it every frame.
|
||||
headroom: f32,
|
||||
/// Paper white in nits; a nominal 80 in SDR.
|
||||
paper_white: f32,
|
||||
/// The peak brightness in nits; a nominal 80 in SDR, as `paper_white`.
|
||||
peak: f32,
|
||||
/// The display's black in nits (HDR's Black Level); 0 in SDR.
|
||||
black_level: f32,
|
||||
} pad padding: [u32; 3]
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
/// The fields 0.1's prelude declares: the struct up to `shadow_center_texel`, a prefix of
|
||||
/// the buffer the renderer uploads.
|
||||
pub const FIELDS_0_1: usize = 49;
|
||||
|
||||
/// `Frame` as the line's prelude declares it.
|
||||
pub fn line(api: crate::Api) -> Struct {
|
||||
let fields = match api {
|
||||
crate::Api::V0_1 => &Self::WGSL.fields[..Self::FIELDS_0_1],
|
||||
crate::Api::V0_2 => Self::WGSL.fields,
|
||||
};
|
||||
Struct {
|
||||
fields,
|
||||
..Self::WGSL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! fields {
|
||||
($($name:ident: $ty:literal = $doc:literal,)*) => {
|
||||
&[$(Field { name: stringify!($name), ty: $ty, doc: $doc }),*]
|
||||
};
|
||||
}
|
||||
|
||||
/// The post stage's input: one pixel of the picture.
|
||||
pub const POST_INPUT: Struct = Struct {
|
||||
name: "PostInput",
|
||||
doc: "One pixel of the picture, for `fn post(input: PostInput) -> vec4<f32>`.",
|
||||
fields: fields! {
|
||||
uv: "vec2<f32>" = "Where the pixel is, 0..1 from the top left.",
|
||||
pixel: "vec2<f32>" = "The pixel's centre, in pixels from the top left of what the pass draws (the world's size; the window's for the last pass).",
|
||||
color: "vec4<f32>" = "The previous pass's colour here (the world's picture for the first pass).",
|
||||
depth: "f32" = "The world's depth here, reversed: 0 is the sky.",
|
||||
},
|
||||
};
|
||||
|
||||
/// The material stage's input: one point of a block's surface.
|
||||
pub const MATERIAL_INPUT: Struct = Struct {
|
||||
name: "MaterialInput",
|
||||
doc: "One point of a block's surface, for `fn material(input: MaterialInput) -> Surface`.",
|
||||
fields: fields! {
|
||||
albedo: "vec4<f32>" = "The block texture's colour here (alpha 0..1).",
|
||||
tint: "vec4<f32>" = "The vertex colour: biome tint, face shade and ambient occlusion.",
|
||||
light: "vec2<f32>" = "Block and sky light, 0..1, smoothed over the face.",
|
||||
lightmap: "vec3<f32>" = "Vanilla's light colour for that light this frame.",
|
||||
position: "vec3<f32>" = "The point, camera-relative, in blocks.",
|
||||
uv: "vec2<f32>" = "The texture coordinate inside the block atlas.",
|
||||
translucent: "bool" = "Drawn in the translucent pass (water, stained glass, ice).",
|
||||
tags: "u32" = "The pack's sprite tags this surface's sprite has: bit i for the manifest's i-th `[[tags]]`.",
|
||||
material: "u32" = "The `[[materials]]` id of this surface's sprite (0 when no entry names it).",
|
||||
screen: "vec2<f32>" = "Where the point is on the screen, 0..1 from the top left (`opaque_depth` and `opaque_color` at `screen * frame.resolution`).",
|
||||
face: "u32" = "For a fluid, which face of the block the point is on, `Direction` order: 0 down, 1 up, 2 north, 3 south, 4 west, 5 east. 0 elsewhere.",
|
||||
fluid: "u32" = "0 not a fluid, 1 water, 2 lava.",
|
||||
emission: "f32" = "How strongly the block itself shines, 0..1, by its kind of light: a redstone torch or a candle 7/15, a torch or lava 14/15, glowstone 1; 0 for most blocks.",
|
||||
normal: "vec3<f32>" = "The surface's geometric normal, a unit vector in world axes, facing the camera. A normal within about 3.6 degrees of an axis is that axis exactly, so distant block faces do not shimmer; flowing water's slopes are steeper and keep theirs.",
|
||||
tangent: "vec4<f32>" = "The surface's tangent: xyz the unit direction in world axes along which `uv.x` grows, w the sign (1 or -1) that makes `cross(normal, tangent.xyz) * tangent.w` the direction `uv.y` grows along (down the texture). With `normal` it turns a normal map's normals into world axes.",
|
||||
colored_light: "vec3<f32>" = "The frame's coloured lights at this point (`[[particle_lights]]`, held and dropped items in their `[[lights]]` colour while dynamic lights are on, and mods' visual lights), 0..1, to light the surface with as the pack likes; 0 where none reaches.",
|
||||
displaced: "f32" = "How much of the relief `[displacement]` gave this point as geometry, 0..1: 1 near the camera, where the surface already lies where `displacement` put it (`position` is that point; a parallax effect would count its depth twice), easing to 0 at `[displacement] distance`; always 0 without `[displacement]`.",
|
||||
},
|
||||
};
|
||||
|
||||
/// What the material stage returns.
|
||||
pub const SURFACE: Struct = Struct {
|
||||
name: "Surface",
|
||||
doc: "What `material` returns. Vanilla's own is `albedo * tint * vec4(lightmap, 1.0)`; \
|
||||
Horizon then applies the alpha test (cutout and translucent) and fog.",
|
||||
fields: fields! {
|
||||
color: "vec4<f32>" = "The lit colour, 0..1, with its alpha.",
|
||||
},
|
||||
};
|
||||
|
||||
/// The sky stage's input: one direction of the sky.
|
||||
pub const SKY_INPUT: Struct = Struct {
|
||||
name: "SkyInput",
|
||||
doc: "One direction of the sky, for `fn sky(input: SkyInput) -> vec4<f32>`. The sun, moon, \
|
||||
stars and clouds are drawn over the result.",
|
||||
fields: fields! {
|
||||
direction: "vec3<f32>" = "Unit view ray, camera-relative world axes.",
|
||||
color: "vec4<f32>" = "Vanilla's sky colour along the ray (sky, fog and sunrise).",
|
||||
},
|
||||
};
|
||||
|
||||
/// The input of the material stage's `displacement`, under `[displacement]`.
|
||||
pub const DISPLACEMENT_INPUT: Struct = Struct {
|
||||
name: "DisplacementInput",
|
||||
doc: "One corner of the grid a block face near the camera is cut into, for `fn displacement(input: DisplacementInput) -> f32` (`[displacement]`), which returns how deep below the face, in blocks, the surface lies at `height`.",
|
||||
fields: fields! {
|
||||
position: "vec3<f32>" = "The corner on the flat face, camera-relative, in blocks.",
|
||||
normal: "vec3<f32>" = "The face's normal, a unit vector in world axes.",
|
||||
uv: "vec2<f32>" = "The texture coordinate inside the block's sprite (as `MaterialInput.uv`).",
|
||||
material: "u32" = "The `[[materials]]` id of the block texture (0 when none matches).",
|
||||
height: "f32" = "A LabPBR height, 0..1 (1 the top of the relief): the normal map's alpha here, or the texture's average height (what the face's edges are pinned to).",
|
||||
},
|
||||
};
|
||||
|
||||
/// The vertex stage's input: one corner of a block's surface.
|
||||
pub const VERTEX_INPUT: Struct = Struct {
|
||||
name: "VertexInput",
|
||||
doc: "One corner of a block's surface, for `fn vertex(input: VertexInput) -> vec3<f32>`, which returns where it is drawn (camera-relative, in blocks; `input.position` where it stays).",
|
||||
fields: fields! {
|
||||
position: "vec3<f32>" = "The corner, camera-relative, in blocks (`+ frame.camera_position` for the world's).",
|
||||
uv: "vec2<f32>" = "The texture coordinate inside the block's sprite.",
|
||||
material: "u32" = "The `[[materials]]` id of the block's sprite (0 when none matches).",
|
||||
light: "vec2<f32>" = "Block and sky light, 0..1.",
|
||||
tint: "vec4<f32>" = "The vertex colour (biome tint, face shade, ambient occlusion); alpha 1.",
|
||||
face: "u32" = "For a fluid, the face of the block the corner is on (as `MaterialInput.face`), else 0.",
|
||||
fluid: "u32" = "1 on a fluid's surface (water or lava), else 0.",
|
||||
},
|
||||
};
|
||||
|
||||
/// The weather stage's input: one point of a rain or snow streak.
|
||||
pub const WEATHER_INPUT: Struct = Struct {
|
||||
name: "WeatherInput",
|
||||
doc: "One point of a rain or snow streak, for `fn weather(input: WeatherInput) -> vec4<f32>`, \
|
||||
which returns the colour blended over the world (alpha its opacity).",
|
||||
fields: fields! {
|
||||
color: "vec4<f32>" = "The rain or snow texture's colour here times the streak's vertex colour (alpha 0.1..1).",
|
||||
light: "vec2<f32>" = "Block and sky light, 0..1.",
|
||||
position: "vec3<f32>" = "The point, camera-relative, in blocks.",
|
||||
snow: "bool" = "A snow streak (rain otherwise).",
|
||||
},
|
||||
};
|
||||
|
||||
/// The tonemap stage's input (0.2): one pixel of the window.
|
||||
pub const TONEMAP_INPUT: Struct = Struct {
|
||||
name: "TonemapInput",
|
||||
doc: "One pixel of an output (the window), for `fn tonemap(input: TonemapInput) -> vec3<f32>`, which returns its colour for the output `frame` describes: linear light, 1.0 at paper white, from 0 to `frame.headroom` (Horizon clamps it there).",
|
||||
fields: fields! {
|
||||
uv: "vec2<f32>" = "Where the pixel is, 0..1 from the top left.",
|
||||
pixel: "vec2<f32>" = "The pixel's centre, in pixels from the top left of the output (the window).",
|
||||
color: "vec3<f32>" = "The post chain's picture here (the world's without one), linear light, never negative or NaN.",
|
||||
},
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn frame_fields_carry_their_docs() {
|
||||
let wgsl = Frame::WGSL;
|
||||
assert_eq!(wgsl.fields.len(), 54);
|
||||
assert!(wgsl.fields.iter().all(|f| !f.doc.trim().is_empty()));
|
||||
// 0.1's 49 fields, then 0.2's five and the padding to a multiple of 16.
|
||||
assert_eq!(
|
||||
std::mem::size_of::<Frame>(),
|
||||
6 * 64 + 15 * 16 + 8 * 16 + 2 * 16
|
||||
);
|
||||
assert_eq!(Frame::OFFSETS[Frame::FIELDS_0_1], 6 * 64 + 15 * 16 + 8 * 16);
|
||||
assert_eq!(
|
||||
Frame::line(crate::Api::V0_1).fields.len(),
|
||||
Frame::FIELDS_0_1
|
||||
);
|
||||
assert!(
|
||||
wgsl.wgsl()
|
||||
.contains(" camera_position: vec3<f32>,\n time: f32,\n")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
//! The external shader pipeline the renderer runs (docs/layers.md, layer plan M6): what a shader
|
||||
//! pack hands the renderer once its own format is read. The renderer (`horizon-client-render`,
|
||||
//! engine) builds its targets and passes from these types and does nothing with them on
|
||||
//! vanilla's path; the pack formats that fill them are platform crates (`horizon-shaders` reads
|
||||
//! `shaders/` packs). No wgpu and no pack file here.
|
||||
//!
|
||||
//! - [`config`]: the colortex buffers and the shadow map: formats, clearing, mipmaps, settings.
|
||||
//! - [`custom`]: the pack's custom uniforms, which the renderer evaluates each frame.
|
||||
//! - [`program`]: a translated program (naga modules and their interface) and the vertex
|
||||
//! inputs the renderer offers it.
|
||||
//! - [`textures`]: the custom textures the programs read; [`RENDER_STAGES`]: the stages they draw in.
|
||||
//! - [`stages`], [`inputs`], [`Api`]: the WGSL stage interface Horizon's own packs are written
|
||||
//! against (`horizon-shader-api` reads and checks those packs): the stages' functions, preludes,
|
||||
//! bindings and entry points, and the frame uniform the renderer fills.
|
||||
|
||||
mod api;
|
||||
pub mod config;
|
||||
pub mod custom;
|
||||
pub mod inputs;
|
||||
pub mod program;
|
||||
mod render_stage;
|
||||
pub mod stages;
|
||||
pub mod textures;
|
||||
|
||||
pub use api::Api;
|
||||
pub use config::{
|
||||
BUFFERS, Buffer, Config, Format, HALF_LIVES, Kind, SHADOW_COLORS, Shadow, buffer_index,
|
||||
shadow_color,
|
||||
};
|
||||
pub use custom::{CustomUniforms, Val};
|
||||
pub use inputs::Frame;
|
||||
pub use program::{
|
||||
AlphaTest, ComputeProgram, GbuffersInputs, GeometryInputs, Interface, Program, WATER_MASK,
|
||||
WATER_PATCHES,
|
||||
};
|
||||
pub use render_stage::{RENDER_STAGES, render_stage};
|
||||
pub use stages::StageKind;
|
||||
@@ -0,0 +1,199 @@
|
||||
//! A translated program as the renderer builds its pipelines from it: its naga modules and the
|
||||
//! interface they expect (uniforms, samplers, images, outputs), and what the renderer offers a
|
||||
//! program's vertex stage (`GbuffersInputs`, `GeometryInputs`). `horizon-shaders` translates a
|
||||
//! `shaders/` pack's GLSL into these.
|
||||
|
||||
/// What a program's stages need from the game.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Interface {
|
||||
/// The uniform block at binding 0: its size and each value's name, GLSL type and offset.
|
||||
/// Ours come first: `hz_target` (the target's width, height, 1/width, 1/height), and the
|
||||
/// fixed-function fog the compatibility `gl_Fog` reads (OQ-28): `hz_fog_color` (vanilla's fog
|
||||
/// colour) and `hz_fog` (density, start, end, 1 / (end - start)), then `hz_depth_linear`:
|
||||
/// 1 where `shadowtex0`, `shadowtex1`, `shadow` and `watershadow` (x to w) read through a
|
||||
/// plain sampler filter linearly, 0 where they take the nearest texel.
|
||||
pub uniform_size: u32,
|
||||
pub uniforms: Vec<Uniform>,
|
||||
/// std140 initial contents; runtime values overwrite their members each frame.
|
||||
pub initial_values: Vec<u8>,
|
||||
/// Each sampler: texture at `binding`, sampler object at `binding + 1`.
|
||||
pub samplers: Vec<Sampler>,
|
||||
/// The fragment stage's output locations.
|
||||
pub outputs: Vec<u32>,
|
||||
/// A gbuffers program reads `DRAW_UNIFORMS`: the block at set 2, binding 0, set per draw.
|
||||
pub draw: bool,
|
||||
/// Storage images (`colorimg4`...), after the samplers.
|
||||
pub images: Vec<Image>,
|
||||
/// The SSBOs' bindings in set 4 (`bufferObject.<n>`).
|
||||
pub buffers: Vec<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Image {
|
||||
pub name: String,
|
||||
/// `image2D`, `uimage3D`...
|
||||
pub ty: String,
|
||||
/// The format qualifier (`rgba16f`).
|
||||
pub format: String,
|
||||
/// `readonly`, `writeonly`, empty for both, or `atomic` for an image an atomic function
|
||||
/// uses.
|
||||
pub access: String,
|
||||
pub binding: u32,
|
||||
/// A compute program's copy of image `before` as it was before the dispatch, which its
|
||||
/// loads read (see `ordering`).
|
||||
pub before: Option<String>,
|
||||
}
|
||||
|
||||
/// A compute program (`deferred4_a.csh`): its module, what it reads and writes, and how many
|
||||
/// work groups it runs: `workGroups`, or `workGroupsRender` times the target's size over the
|
||||
/// local size (1, 1 when neither is set).
|
||||
#[derive(Clone)]
|
||||
pub struct ComputeProgram {
|
||||
pub module: naga::Module,
|
||||
pub interface: Interface,
|
||||
/// Non-fatal translation diagnostics, located in the authored source.
|
||||
pub notes: Vec<Error>,
|
||||
pub work_groups: Option<[u32; 3]>,
|
||||
pub work_groups_render: [f32; 2],
|
||||
}
|
||||
|
||||
/// The uniforms a gbuffers program reads per draw (5.3's "D"), in a block of their own
|
||||
/// (`HzDraw`, std140: four ints, a vec4; 32 bytes) the game sets for each draw.
|
||||
pub const DRAW_UNIFORMS: [(&str, &str); 5] = [
|
||||
("renderStage", "int"),
|
||||
("entityId", "int"),
|
||||
("blockEntityId", "int"),
|
||||
("currentRenderedItemId", "int"),
|
||||
("entityColor", "vec4"),
|
||||
];
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Uniform {
|
||||
pub name: String,
|
||||
/// The type the pack declared (`float`, `ivec2`, `mat4`; `bool` is stored as an `int`).
|
||||
pub ty: String,
|
||||
pub offset: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Sampler {
|
||||
pub name: String,
|
||||
/// The type the pack declared: `sampler2D`, `sampler2DShadow`, `usampler2D`, `sampler3D`...
|
||||
pub ty: String,
|
||||
pub binding: u32,
|
||||
}
|
||||
|
||||
/// A translation diagnostic at a pack file and line.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Error {
|
||||
pub file: String,
|
||||
pub line: u32,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Program {
|
||||
pub vertex: naga::Module,
|
||||
pub fragment: naga::Module,
|
||||
pub interface: Interface,
|
||||
/// Non-fatal translation diagnostics, located in the authored source.
|
||||
pub notes: Vec<Error>,
|
||||
}
|
||||
|
||||
/// Where the program draws, which decides the conventions of 5.5.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Target {
|
||||
/// Pack buffers: rows stored as GL stores them.
|
||||
Buffers,
|
||||
/// The window (`final`).
|
||||
Window,
|
||||
}
|
||||
|
||||
/// What a gbuffers-style program's vertex stage reads. The game writes it: it knows its vertex
|
||||
/// formats, the translator only places it.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct GbuffersInputs {
|
||||
/// GLSL declarations: the vertex inputs and any bindings of set 1.
|
||||
pub declarations: String,
|
||||
/// GLSL statements that set `hz_Vertex`, `hz_Color`, `hz_MultiTexCoord0`,
|
||||
/// `hz_MultiTexCoord1` and `hz_Normal` (the compatibility inputs), run first in `main`.
|
||||
pub body: String,
|
||||
/// The value of each attribute a pack may declare (`mc_Entity`, `vaPosition`...), as a GLSL
|
||||
/// expression of the declared type's size; an attribute not listed reads zero. Terrain
|
||||
/// also supplies `mc_chunkFade`, an implicit float input rather than a declared attribute.
|
||||
pub attributes: Vec<(String, String)>,
|
||||
/// GLSL statements run last in `main`, with `gl_Position` in wgpu's clip space.
|
||||
pub after: String,
|
||||
/// The alpha test applied to output 0 (`gl_FragData[0]`, `gl_FragColor`), the loader's
|
||||
/// (3.2 `alphaTest.<program>`): `None` keeps every fragment.
|
||||
pub alpha_test: Option<AlphaTest>,
|
||||
/// The program may read `currentRenderedItemId` without declaring it, as Iris 1.11.7
|
||||
/// lets the hand's (observed with probe packs; terrain's may not).
|
||||
pub item_id_declared: bool,
|
||||
/// The depth test sees the far plane (window depth 1) unless the program writes
|
||||
/// `gl_FragDepth`, while `gl_FragCoord.z` stays the geometry's own: the sky shows only where
|
||||
/// nothing else drew, and reads its own depth as in Iris 1.11.7.
|
||||
pub far_depth: bool,
|
||||
/// `normals` and `specular` are bound as paged maps of the atlas `texture` reads
|
||||
/// (`horizon-client-render`'s `paged_maps.rs`), not as images of its layout: the program's
|
||||
/// texture calls on them read through `hz_pagedTexture`.
|
||||
pub paged_maps: bool,
|
||||
/// Translucent terrain: a fragment behind the boats' water patches (`horizonWaterMask`, the
|
||||
/// patches' depth, 1 where there are none) is discarded, so no water shows inside a hull
|
||||
/// (`RenderTypes.waterMask`), while the frame's depth stays the pack's own.
|
||||
pub water_mask: bool,
|
||||
/// The shadow pass's translucent terrain: a fragment inside a boat's water patch
|
||||
/// (`horizonWaterPatch*`, camera-relative boxes) is discarded, so no water casts its light
|
||||
/// into a hull. Not with a geometry stage.
|
||||
pub water_patches: bool,
|
||||
}
|
||||
|
||||
/// The sampler of the boats' water patches' depth (`GbuffersInputs::water_mask`).
|
||||
pub const WATER_MASK: &str = "horizonWaterMask";
|
||||
|
||||
/// How many boats' water patches the shadow pass keeps water out of (`GbuffersInputs::water_patches`).
|
||||
pub const WATER_PATCHES: usize = 16;
|
||||
|
||||
/// A fragment stays when its output 0's alpha compares to `reference` as `op` says (a GLSL
|
||||
/// comparison operator, `true` for GL's `ALWAYS`, `false` for `NEVER`).
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct AlphaTest {
|
||||
pub op: &'static str,
|
||||
pub reference: f32,
|
||||
}
|
||||
|
||||
impl AlphaTest {
|
||||
/// `alphaTest.<program>`'s value: `off`, or a GL comparison and a reference.
|
||||
pub fn parse(value: &str) -> Option<Option<Self>> {
|
||||
let mut words = value.split_whitespace();
|
||||
let func = words.next()?;
|
||||
if func == "off" {
|
||||
return Some(None);
|
||||
}
|
||||
let op = match func.trim_start_matches("GL_") {
|
||||
"NEVER" => "false",
|
||||
"LESS" => "<",
|
||||
"EQUAL" => "==",
|
||||
"LEQUAL" => "<=",
|
||||
"GREATER" => ">",
|
||||
"NOTEQUAL" => "!=",
|
||||
"GEQUAL" => ">=",
|
||||
"ALWAYS" => return Some(None),
|
||||
_ => return None,
|
||||
};
|
||||
let reference = words.next()?.parse().ok()?;
|
||||
Some(Some(Self { op, reference }))
|
||||
}
|
||||
}
|
||||
|
||||
/// How a geometry program's vertex invocations reach their triangle's vertices.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct GeometryInputs {
|
||||
/// The game's vertex inputs, as `GbuffersInputs`; their `layout(location = n) in`
|
||||
/// declarations become plain globals the pull sets.
|
||||
pub inputs: GbuffersInputs,
|
||||
/// GLSL statements that set the vertex inputs from vertex `hz_vertex_index` (a `uint`).
|
||||
pub pull: String,
|
||||
/// The vertex of the `@`th corner of the draw (a `uint` expression, `hz_indices[@]`).
|
||||
pub index: String,
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
//! The render stages a pipeline's programs are told they draw in (`renderStage`, and the
|
||||
//! `MC_RENDER_STAGE_*` macros `horizon-shaders` defines from them).
|
||||
|
||||
/// `MC_RENDER_STAGE_*` in the order the stages draw (spec 5.4); each is its index (OQ-7).
|
||||
pub const RENDER_STAGES: [&str; 24] = [
|
||||
"NONE",
|
||||
"SKY",
|
||||
"SUNSET",
|
||||
"CUSTOM_SKY",
|
||||
"SUN",
|
||||
"MOON",
|
||||
"STARS",
|
||||
"VOID",
|
||||
"TERRAIN_SOLID",
|
||||
"TERRAIN_CUTOUT_MIPPED",
|
||||
"TERRAIN_CUTOUT",
|
||||
"ENTITIES",
|
||||
"BLOCK_ENTITIES",
|
||||
"DESTROY",
|
||||
"OUTLINE",
|
||||
"DEBUG",
|
||||
"HAND_SOLID",
|
||||
"TERRAIN_TRANSLUCENT",
|
||||
"TRIPWIRE",
|
||||
"PARTICLES",
|
||||
"CLOUDS",
|
||||
"RAIN_SNOW",
|
||||
"WORLD_BORDER",
|
||||
"HAND_TRANSLUCENT",
|
||||
];
|
||||
|
||||
/// The value of `MC_RENDER_STAGE_<name>` (OQ-7: its index in `RENDER_STAGES`).
|
||||
pub fn render_stage(name: &str) -> u32 {
|
||||
RENDER_STAGES
|
||||
.iter()
|
||||
.position(|s| *s == name)
|
||||
.unwrap_or_else(|| panic!("no render stage {name}")) as u32
|
||||
}
|
||||
@@ -0,0 +1,803 @@
|
||||
//! The stages a WGSL pack can fill, each a WGSL function with a fixed signature
|
||||
//! that the renderer calls inside its own passes, and the WGSL put around a
|
||||
//! pack's files to build a stage: the prelude before (the inputs and the
|
||||
//! resources), the entry points after. `horizon-shader-api` composes a pack's
|
||||
//! files between them.
|
||||
use crate::Api;
|
||||
use crate::inputs::{
|
||||
DISPLACEMENT_INPUT, Frame, MATERIAL_INPUT, POST_INPUT, SKY_INPUT, SURFACE, Struct,
|
||||
TONEMAP_INPUT, VERTEX_INPUT, WEATHER_INPUT,
|
||||
};
|
||||
|
||||
/// The bind group of a pack's resources; groups 0 to 2 are the renderer's.
|
||||
pub const GROUP: u32 = 3;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum StageKind {
|
||||
/// The sky's colour along each view ray, before the sun, moon and stars.
|
||||
Sky,
|
||||
/// The colour of block surfaces (terrain), before the alpha test and fog.
|
||||
Material,
|
||||
/// Where each corner of a block's surface is drawn (waving plants, rolling water), for
|
||||
/// the world and the shadow map.
|
||||
Vertex,
|
||||
/// The colour of rain and snow streaks.
|
||||
Weather,
|
||||
/// A full-screen pass over the world's picture, under the hand and HUD.
|
||||
Post,
|
||||
/// 0.2: the picture for an output (the display), once per pixel of it after the post chain,
|
||||
/// before the camera overlays and the HUD.
|
||||
Tonemap,
|
||||
}
|
||||
|
||||
/// A resource a stage reads, declared by its prelude.
|
||||
#[derive(Debug)]
|
||||
pub struct Resource {
|
||||
pub binding: u32,
|
||||
pub name: &'static str,
|
||||
pub ty: &'static str,
|
||||
pub doc: &'static str,
|
||||
}
|
||||
|
||||
const FRAME: Resource = Resource {
|
||||
binding: 0,
|
||||
name: "frame",
|
||||
ty: "var<uniform> frame: Frame",
|
||||
doc: "The frame (`Frame`).",
|
||||
};
|
||||
|
||||
/// The shadow map at `binding`.
|
||||
const fn shadow_map(binding: u32) -> Resource {
|
||||
Resource {
|
||||
binding,
|
||||
name: "shadow_map",
|
||||
ty: "var shadow_map: texture_depth_2d",
|
||||
doc: "This frame's shadow map: the depth of the solid terrain and entities the light sees, 0 at the light, 1 away from it (and where nothing is), stored at `shadow_position`'s mapping. Valid while `frame.has_shadow` is 1 (the pack asks for it with `[shadow]`), else a 1×1 map that lights everything.",
|
||||
}
|
||||
}
|
||||
|
||||
/// The shadow map's comparison sampler at `binding`.
|
||||
const fn shadow_sampler(binding: u32) -> Resource {
|
||||
Resource {
|
||||
binding,
|
||||
name: "shadow_sampler",
|
||||
ty: "var shadow_sampler: sampler_comparison",
|
||||
doc: "Depth comparison, bilinear (2×2 percentage-closer filtering): `textureSampleCompareLevel(shadow_map, shadow_sampler, p.xy, p.z)` is 1 where the point is lit, 0 in shadow.",
|
||||
}
|
||||
}
|
||||
|
||||
/// The shadow map with translucent terrain at `binding`.
|
||||
const fn shadow_map_translucent(binding: u32) -> Resource {
|
||||
Resource {
|
||||
binding,
|
||||
name: "shadow_map_translucent",
|
||||
ty: "var shadow_map_translucent: texture_depth_2d",
|
||||
doc: "`shadow_map` with the translucent terrain (water, stained glass, ice) the light passes through drawn in too, read with `shadow_sampler` the same way: a point lit in `shadow_map` but not here lies under such a surface, which `shadow_color` gives the colour of.",
|
||||
}
|
||||
}
|
||||
|
||||
/// The colour of the translucent terrain in the shadow map at `binding`.
|
||||
const fn shadow_color(binding: u32) -> Resource {
|
||||
Resource {
|
||||
binding,
|
||||
name: "shadow_color",
|
||||
ty: "var shadow_color: texture_2d<f32>",
|
||||
doc: "The texture colour times the vertex colour (rgb: the tint, the face's directional shade and ambient occlusion) of the nearest translucent surface the light passes through at each texel of `shadow_map_translucent`, white where there is none; alpha is 1 on a fluid (water) and the texture's alpha, at most 0.99, on anything else. Read it with `textureLoad` at `shadow_position`'s xy times `textureDimensions(shadow_color)`.",
|
||||
}
|
||||
}
|
||||
|
||||
/// A function the material and post preludes give packs.
|
||||
pub const SHADOW_HELPERS: &str = "\
|
||||
// The shadow map's distortion toward its centre, of a point at shadow clip `xy`.
|
||||
fn shadow_distort(xy: vec2<f32>) -> vec2<f32> {
|
||||
let k = frame.shadow_distortion;
|
||||
return xy / (length(xy) * k + 1.0 - k);
|
||||
}
|
||||
// Where the camera-relative point `position` is stored in the shadow map: xy its 0..1 texture
|
||||
// coordinate (from the top left), z its depth from the light, for `textureSampleCompareLevel`.
|
||||
fn shadow_position(position: vec3<f32>) -> vec3<f32> {
|
||||
let clip = frame.shadow_view_proj * vec4<f32>(position, 1.0);
|
||||
let xy = shadow_distort(clip.xy);
|
||||
// At most 1, the depth where nothing is: past the map's far end a point stays lit.
|
||||
return vec3<f32>(xy.x * 0.5 + 0.5, 0.5 - xy.y * 0.5, min(clip.z, 1.0));
|
||||
}
|
||||
";
|
||||
|
||||
const LINEAR_SAMPLER: Resource = Resource {
|
||||
binding: 4,
|
||||
name: "linear_sampler",
|
||||
ty: "var linear_sampler: sampler",
|
||||
doc: "Bilinear filtering, clamped to the edges.",
|
||||
};
|
||||
|
||||
const NEAREST_SAMPLER: Resource = Resource {
|
||||
binding: 5,
|
||||
name: "nearest_sampler",
|
||||
ty: "var nearest_sampler: sampler",
|
||||
doc: "Nearest texel, clamped to the edges.",
|
||||
};
|
||||
|
||||
/// The tonemap stage's resources, at the post stage's bindings for the same names.
|
||||
const TONEMAP_RESOURCES: [Resource; 4] = [
|
||||
FRAME,
|
||||
Resource {
|
||||
binding: 3,
|
||||
name: "previous",
|
||||
ty: "var previous: texture_2d<f32>",
|
||||
doc: "The post chain's picture at the window's size (without a post chain, the world's at the world's size), linear light: what `input.color` samples, for a curve that reads around the pixel.",
|
||||
},
|
||||
LINEAR_SAMPLER,
|
||||
NEAREST_SAMPLER,
|
||||
];
|
||||
|
||||
/// 0.2's colour boundary, in every 0.2 prelude: vanilla's 2.2 power both ways.
|
||||
const COLOR_HELPERS: &str = "\
|
||||
// Negatives and NaN made 0 (a comparison with NaN is false), so no `pow` sees them.
|
||||
fn horizon_sanitize(c: vec3<f32>) -> vec3<f32> {
|
||||
return select(vec3<f32>(0.0), c, c > vec3<f32>(0.0));
|
||||
}
|
||||
fn horizon_decode(c: vec3<f32>) -> vec3<f32> {
|
||||
return pow(horizon_sanitize(c), vec3<f32>(2.2));
|
||||
}
|
||||
fn horizon_encode(c: vec3<f32>) -> vec3<f32> {
|
||||
return pow(horizon_sanitize(c), vec3<f32>(1.0 / 2.2));
|
||||
}
|
||||
// Alpha is coverage: it stays as it is.
|
||||
fn horizon_decode_rgba(c: vec4<f32>) -> vec4<f32> {
|
||||
return vec4<f32>(horizon_decode(c.rgb), c.a);
|
||||
}
|
||||
fn horizon_encode_rgba(c: vec4<f32>) -> vec4<f32> {
|
||||
return vec4<f32>(horizon_encode(c.rgb), c.a);
|
||||
}
|
||||
";
|
||||
|
||||
const POST_RESOURCES: [Resource; 12] = [
|
||||
FRAME,
|
||||
Resource {
|
||||
binding: 1,
|
||||
name: "scene_color",
|
||||
ty: "var scene_color: texture_2d<f32>",
|
||||
doc: "The world's picture before any pass.",
|
||||
},
|
||||
Resource {
|
||||
binding: 2,
|
||||
name: "scene_depth",
|
||||
ty: "var scene_depth: texture_depth_2d",
|
||||
doc: "The world's depth, reversed (1 near, 0 far and sky).",
|
||||
},
|
||||
Resource {
|
||||
binding: 3,
|
||||
name: "previous",
|
||||
ty: "var previous: texture_2d<f32>",
|
||||
doc: "The previous pass's output (`scene_color` for the first pass).",
|
||||
},
|
||||
LINEAR_SAMPLER,
|
||||
NEAREST_SAMPLER,
|
||||
shadow_map(6),
|
||||
shadow_sampler(7),
|
||||
Resource {
|
||||
binding: 8,
|
||||
name: "history",
|
||||
ty: "var history: texture_2d<f32>",
|
||||
doc: "Last frame's input to the last pass (what the pass before it wrote), for temporal passes (TAA, denoising): put such a pass second to last and it reads its own previous output; reproject with `frame.previous_view_projection`. Black on the first frame, after a gap of over half a second, and with a single pass.",
|
||||
},
|
||||
shadow_map_translucent(9),
|
||||
shadow_color(10),
|
||||
Resource {
|
||||
binding: 11,
|
||||
name: "scene_surface",
|
||||
ty: "var scene_surface: texture_2d<u32>",
|
||||
doc: "What the material stage's `set_surface` stored for the solid terrain at each pixel, unfiltered (`textureLoad`; its texels are the bits of floats, so `bitcast<vec4<f32>>` them): read it through the prelude's `surface_at`, which knows its layout and where it no longer shows. Blank without a material stage that calls `set_surface`.",
|
||||
},
|
||||
];
|
||||
|
||||
/// The material stage's resources: the frame, and the world as it was before translucent
|
||||
/// terrain drew (for water and glass to see what lies behind them).
|
||||
const MATERIAL_RESOURCES: [Resource; 11] = [
|
||||
FRAME,
|
||||
Resource {
|
||||
binding: 1,
|
||||
name: "reflection",
|
||||
ty: "var reflection: texture_2d<f32>",
|
||||
doc: "The water mirror: the world as a camera mirrored about the horizontal water plane around the camera (`frame.water_plane`) sees it, at half resolution, mapped like the world's picture, so water samples it at `input.screen`. Solid terrain, cut away below the plane, over the vanilla sky gradient: not the pack's `sky` stage, nor the sun, moon and stars. Drawn only for a pack whose material reads it, while `frame.has_reflection` is 1; else a 1×1 blank.",
|
||||
},
|
||||
Resource {
|
||||
binding: 2,
|
||||
name: "reflection_sampler",
|
||||
ty: "var reflection_sampler: sampler",
|
||||
doc: "Bilinear filtering, clamped to the edges.",
|
||||
},
|
||||
shadow_map(3),
|
||||
shadow_sampler(4),
|
||||
shadow_map_translucent(7),
|
||||
shadow_color(8),
|
||||
Resource {
|
||||
binding: 6,
|
||||
name: "opaque_depth",
|
||||
ty: "var opaque_depth: texture_depth_2d",
|
||||
doc: "The world's depth before translucent terrain (reversed, like `scene_depth`), for the translucent pass: `textureLoad(opaque_depth, vec2<i32>(input.screen * frame.resolution), 0)` is what lies behind water or glass. Stale while solid terrain draws.",
|
||||
},
|
||||
Resource {
|
||||
binding: 9,
|
||||
name: "block_light_colors",
|
||||
ty: "var block_light_colors: texture_3d<f32>",
|
||||
doc: "The colour of the block light around the camera, from the `[[lights]]` of the light sources nearby: rgb their colours averaged by how near each is, over 4; a how strongly they reach the cell. Read it through `block_light_color`.",
|
||||
},
|
||||
Resource {
|
||||
binding: 10,
|
||||
name: "block_light_grid",
|
||||
ty: "var<uniform> block_light_grid: vec4<f32>",
|
||||
doc: "Where `block_light_colors` lies: xyz its corner, camera-relative, w the side of one of its cells in blocks.",
|
||||
},
|
||||
Resource {
|
||||
binding: 11,
|
||||
name: "opaque_color",
|
||||
ty: "var opaque_color: texture_2d<f32>",
|
||||
doc: "The world's picture before translucent terrain (sky, solid terrain, entities), the size of `opaque_depth`, for the translucent pass: what a surface refracts. Read it with `textureLoad`. Stale while solid terrain draws; black when the world draws straight to the window (a pack without `[[post]]` passes, at full resolution).",
|
||||
},
|
||||
];
|
||||
|
||||
/// Functions the post prelude gives packs.
|
||||
const POST_HELPERS: &str = "\
|
||||
// The world's depth at `uv` (reversed: 0 is the sky).
|
||||
fn depth_at(uv: vec2<f32>) -> f32 {
|
||||
let size = vec2<f32>(textureDimensions(scene_depth));
|
||||
let texel = vec2<i32>(clamp(uv * size, vec2<f32>(0.0), size - 1.0));
|
||||
return textureLoad(scene_depth, texel, 0);
|
||||
}
|
||||
// The camera-relative world position, in blocks, of the point at `uv` and `depth`.
|
||||
fn view_position(uv: vec2<f32>, depth: f32) -> vec3<f32> {
|
||||
let clip = vec4<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, depth, 1.0);
|
||||
let view = frame.projection_inverse * clip;
|
||||
return (frame.view_inverse * vec4<f32>(view.xyz / view.w, 0.0)).xyz;
|
||||
}
|
||||
";
|
||||
|
||||
/// What the material entries (the checker's below and the renderer's) share: the
|
||||
/// surface's normal and tangent from the derivatives of its position and uv. The arguments are
|
||||
/// the line's return statements of `block_light_color`, `block_texture` and
|
||||
/// `block_texture_level`, the helpers that read colour.
|
||||
macro_rules! material_helpers {
|
||||
($light:literal, $texture:literal, $texture_level:literal) => {
|
||||
concat!(
|
||||
"\
|
||||
// The colour of the block light at the camera-relative `position` (rgb, from the `[[lights]]`
|
||||
// of the sources nearby) and how strongly they reach it (a, 0 where none does). Block light
|
||||
// keeps its own brightness (`input.light.x`): this is its colour only.
|
||||
fn block_light_color(position: vec3<f32>) -> vec4<f32> {
|
||||
let size = vec3<f32>(textureDimensions(block_light_colors));
|
||||
let uvw = (position - block_light_grid.xyz) / (block_light_grid.w * size);
|
||||
if any(uvw < vec3<f32>(0.0)) || any(uvw > vec3<f32>(1.0)) {
|
||||
return vec4<f32>(0.0);
|
||||
}
|
||||
let c = textureSampleLevel(block_light_colors, reflection_sampler, uvw, 0.0);
|
||||
",
|
||||
$light,
|
||||
"}
|
||||
// What the post stage's `surface_at` reads for this point of solid terrain: its `normal` (a
|
||||
// unit vector in world axes), `smoothness` (0..1, say for reflections) and `data`, a value of
|
||||
// the pack's own passed through. Unset, the point keeps the face's own normal and 0 for both;
|
||||
// translucent terrain stores none.
|
||||
var<private> horizon_surface: vec4<f32>;
|
||||
fn set_surface(normal: vec3<f32>, smoothness: f32, data: f32) {
|
||||
// Octahedral, y up: the upper half folds onto xz, the lower half unfolds around it.
|
||||
let n = normal / (abs(normal.x) + abs(normal.y) + abs(normal.z));
|
||||
var e = n.xz;
|
||||
if n.y < 0.0 {
|
||||
e = (1.0 - abs(n.zx)) * select(vec2<f32>(-1.0), vec2<f32>(1.0), n.xz >= vec2<f32>(0.0));
|
||||
}
|
||||
horizon_surface = vec4<f32>(bitcast<f32>(pack2x16snorm(e)), smoothness, data, 1.0);
|
||||
}
|
||||
// The point being shaded: its sprite's layer and mip level (the entries set them).
|
||||
var<private> horizon_block_layer: u32;
|
||||
var<private> horizon_block_lod: f32;
|
||||
// The surface's block texture at `uv` (the sprite's space, as `input.uv`), without the tint:
|
||||
// what `input.albedo` holds at the point itself, readable anywhere on the sprite, for normals
|
||||
// or detail generated from the texture.
|
||||
fn block_texture(uv: vec2<f32>) -> vec4<f32> {
|
||||
",
|
||||
$texture,
|
||||
"}
|
||||
// `block_texture` at mip level `lod` (0 the full-size texture).
|
||||
fn block_texture_level(uv: vec2<f32>, lod: f32) -> vec4<f32> {
|
||||
",
|
||||
$texture_level,
|
||||
"}
|
||||
// The block texture's size, in texels (16 x 16 for vanilla's).
|
||||
fn block_texture_size() -> vec2<f32> {
|
||||
return horizon_block_size();
|
||||
}
|
||||
// The mip level `block_texture` reads at this point (0 up close, more far away).
|
||||
fn block_texture_lod() -> f32 {
|
||||
return horizon_block_lod;
|
||||
}
|
||||
// LabPBR: whether the resource packs ship normal (`_n`) or specular (`_s`) maps beside their
|
||||
// block textures. Without them `block_normal_map` and `block_specular_map` give the defaults
|
||||
// below.
|
||||
fn block_pbr_maps() -> bool {
|
||||
return horizon_block_pbr_present();
|
||||
}
|
||||
// The surface's LabPBR normal map at `uv` (the sprite's space, as `input.uv`): rg the normal's
|
||||
// x and y in the texture's space (x along `input.tangent`, y down the texture; 0..1 for
|
||||
// -1..1), b ambient occlusion (1 none), a height (1 the top). (0.5, 0.5, 1, 1) where a texture
|
||||
// has none.
|
||||
fn block_normal_map(uv: vec2<f32>) -> vec4<f32> {
|
||||
return horizon_block_pbr(uv, horizon_block_layer, horizon_block_lod, 0u);
|
||||
}
|
||||
// `block_normal_map` at the block texture's mip level `lod` (the same footprint on the surface,
|
||||
// where the map is smaller than the texture).
|
||||
fn block_normal_map_level(uv: vec2<f32>, lod: f32) -> vec4<f32> {
|
||||
return horizon_block_pbr(uv, horizon_block_layer, lod, 0u);
|
||||
}
|
||||
// The surface's LabPBR specular map at `uv`: r perceptual smoothness, g F0 up to 229/255 and
|
||||
// a metal's id above, b porosity or subsurface scattering, a emission (up to 254/255; 1
|
||||
// none). (0, 0, 0, 0) where a texture has none.
|
||||
fn block_specular_map(uv: vec2<f32>) -> vec4<f32> {
|
||||
return horizon_block_pbr(uv, horizon_block_layer, horizon_block_lod, 1u);
|
||||
}
|
||||
// `block_specular_map` at the block texture's mip level `lod`.
|
||||
fn block_specular_map_level(uv: vec2<f32>, lod: f32) -> vec4<f32> {
|
||||
return horizon_block_pbr(uv, horizon_block_layer, lod, 1u);
|
||||
}
|
||||
// The LabPBR maps' size, in texels (they may be smaller than the block's texture).
|
||||
fn block_pbr_size() -> vec2<f32> {
|
||||
return horizon_block_pbr_size();
|
||||
}
|
||||
// Block faces are flat: the derivatives of the position give the face's normal, turned
|
||||
// toward the camera. Far away they lose precision, so a normal within about 3.6 degrees of
|
||||
// an axis (every full block face) is that axis exactly: no shimmer on distant faces. The
|
||||
// gentlest slope of flowing water, one level over a block, tilts about 6 degrees.
|
||||
fn horizon_material_normal(position: vec3<f32>) -> vec3<f32> {
|
||||
let n = normalize(cross(dpdx(position), dpdy(position)));
|
||||
let a = abs(n);
|
||||
let m = max(a.x, max(a.y, a.z));
|
||||
let axis = select(select(vec3(0.0, 0.0, sign(n.z)), vec3(0.0, sign(n.y), 0.0), a.y == m),
|
||||
vec3(sign(n.x), 0.0, 0.0), a.x == m);
|
||||
let flat_n = select(n, axis, m > 0.998);
|
||||
return select(flat_n, -flat_n, dot(flat_n, position) > 0.0);
|
||||
}
|
||||
// The direction `uv.x` grows along on the surface, and the sign of the one `uv.y` grows
|
||||
// along against `cross(normal, tangent)`.
|
||||
fn horizon_material_tangent(position: vec3<f32>, uv: vec2<f32>, normal: vec3<f32>) -> vec4<f32> {
|
||||
let dp1 = dpdx(position);
|
||||
let dp2 = dpdy(position);
|
||||
let duv1 = dpdx(uv);
|
||||
let duv2 = dpdy(uv);
|
||||
let dp2perp = cross(dp2, normal);
|
||||
let dp1perp = cross(normal, dp1);
|
||||
let t = dp2perp * duv1.x + dp1perp * duv2.x;
|
||||
let b = dp2perp * duv1.y + dp1perp * duv2.y;
|
||||
if length(t) < 1e-12 {
|
||||
return vec4<f32>(1.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
let tangent = normalize(t - normal * dot(normal, t));
|
||||
return vec4<f32>(tangent, select(-1.0, 1.0, dot(cross(normal, tangent), b) >= 0.0));
|
||||
}
|
||||
"
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
const MATERIAL_HELPERS: &str = material_helpers!(
|
||||
" return vec4<f32>(c.rgb * 4.0, c.a);\n",
|
||||
" return horizon_block_sample(uv, horizon_block_layer, horizon_block_lod);\n",
|
||||
" return horizon_block_sample(uv, horizon_block_layer, lod);\n"
|
||||
);
|
||||
|
||||
/// 0.2's: the `[[lights]]` colours and the block texture decoded (the LabPBR maps are data).
|
||||
const MATERIAL_HELPERS_0_2: &str = material_helpers!(
|
||||
" return vec4<f32>(horizon_decode(c.rgb * 4.0), c.a);\n",
|
||||
" return horizon_decode_rgba(horizon_block_sample(uv, horizon_block_layer, horizon_block_lod));\n",
|
||||
" return horizon_decode_rgba(horizon_block_sample(uv, horizon_block_layer, lod));\n"
|
||||
);
|
||||
|
||||
/// The post prelude's `surface_at`, for the post helpers' end.
|
||||
const POST_SURFACE: &str = "\
|
||||
// The solid terrain's surface at a pixel, as the material stage's `set_surface` left it.
|
||||
struct SurfaceData {
|
||||
// False where the sky, an entity, water, glass or a particle shows instead.
|
||||
present: bool,
|
||||
// A unit vector in world axes.
|
||||
normal: vec3<f32>,
|
||||
smoothness: f32,
|
||||
// The pack's own value, passed through.
|
||||
data: f32,
|
||||
}
|
||||
fn surface_at(texel: vec2<i32>) -> SurfaceData {
|
||||
let size = vec2<i32>(textureDimensions(scene_surface));
|
||||
let t = clamp(texel, vec2<i32>(0), size - 1);
|
||||
let s = bitcast<vec4<f32>>(textureLoad(scene_surface, t, 0));
|
||||
let depth_size = vec2<i32>(textureDimensions(scene_depth));
|
||||
let depth = textureLoad(scene_depth, clamp(texel, vec2<i32>(0), depth_size - 1), 0);
|
||||
// The terrain's own depth is stored with it: anything drawn nearer since hides it.
|
||||
let present = s.w > 0.0 && abs(s.w - depth) <= s.w * 1e-5;
|
||||
let e = unpack2x16snorm(bitcast<u32>(s.x));
|
||||
var n = vec3<f32>(e.x, 1.0 - abs(e.x) - abs(e.y), e.y);
|
||||
if n.y < 0.0 {
|
||||
n = vec3<f32>((1.0 - abs(n.z)) * sign(n.x), n.y, (1.0 - abs(n.x)) * sign(n.z));
|
||||
}
|
||||
return SurfaceData(present, select(vec3<f32>(0.0), normalize(n), present),
|
||||
select(0.0, s.y, present), select(0.0, s.z, present));
|
||||
}
|
||||
";
|
||||
|
||||
/// The post stage's entry points, as the renderer builds them: a full-screen
|
||||
/// triangle, and a fragment that calls the pack's `post` for each pixel.
|
||||
const POST_ENTRIES: &str = "\
|
||||
struct HorizonPostOut {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) uv: vec2<f32>,
|
||||
}
|
||||
@vertex fn horizon_vs_post(@builtin(vertex_index) i: u32) -> HorizonPostOut {
|
||||
let p = vec2<f32>(f32((i << 1u) & 2u), f32(i & 2u));
|
||||
var out: HorizonPostOut;
|
||||
out.position = vec4<f32>(p * 2.0 - 1.0, 0.0, 1.0);
|
||||
out.uv = vec2<f32>(p.x, 1.0 - p.y);
|
||||
return out;
|
||||
}
|
||||
@fragment fn horizon_fs_post(in: HorizonPostOut) -> @location(0) vec4<f32> {
|
||||
let color = textureSampleLevel(previous, linear_sampler, in.uv, 0.0);
|
||||
return post(PostInput(in.uv, in.position.xy, color, depth_at(in.uv)));
|
||||
}
|
||||
";
|
||||
|
||||
/// What the renderer gives the material prelude's LabPBR functions (`native_material.rs`); the
|
||||
/// check and the entries below put a stand-in in its place.
|
||||
macro_rules! pbr_stub {
|
||||
() => {
|
||||
"fn horizon_block_sample(uv: vec2<f32>, layer: u32, lod: f32) -> vec4<f32> {
|
||||
return vec4<f32>(1.0);
|
||||
}
|
||||
fn horizon_block_size() -> vec2<f32> {
|
||||
return vec2<f32>(16.0);
|
||||
}
|
||||
fn horizon_block_pbr(uv: vec2<f32>, layer: u32, lod: f32, map: u32) -> vec4<f32> {
|
||||
return select(vec4<f32>(0.5, 0.5, 1.0, 1.0), vec4<f32>(0.0), map == 1u);
|
||||
}
|
||||
fn horizon_block_pbr_present() -> bool {
|
||||
return false;
|
||||
}
|
||||
fn horizon_block_pbr_size() -> vec2<f32> {
|
||||
return vec2<f32>(16.0);
|
||||
}
|
||||
"
|
||||
};
|
||||
}
|
||||
pub const PBR_STUB: &str = pbr_stub!();
|
||||
|
||||
/// Entry points with the material stage's inputs: the renderer calls
|
||||
/// `material` from its own terrain shader in the same way. The arguments are the line's
|
||||
/// `MaterialInput` and return statements.
|
||||
macro_rules! material_entries {
|
||||
($input:literal, $result:literal) => {
|
||||
concat!(
|
||||
pbr_stub!(),
|
||||
"\
|
||||
struct HorizonMaterialIn {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) albedo: vec4<f32>,
|
||||
@location(1) tint: vec4<f32>,
|
||||
@location(2) light: vec2<f32>,
|
||||
@location(3) lightmap: vec3<f32>,
|
||||
@location(4) world: vec3<f32>,
|
||||
@location(5) uv: vec2<f32>,
|
||||
@location(6) @interpolate(flat) translucent: u32,
|
||||
@location(7) @interpolate(flat) tags: u32,
|
||||
@location(8) @interpolate(flat) face: u32,
|
||||
@location(9) @interpolate(flat) fluid_quad: u32,
|
||||
@location(10) @interpolate(flat) emission: f32,
|
||||
@location(11) @interpolate(flat) material: u32,
|
||||
}
|
||||
@fragment fn horizon_fs_material(in: HorizonMaterialIn) -> @location(0) vec4<f32> {
|
||||
let fluid = select(0u, select(2u, 1u, in.translucent != 0u), in.fluid_quad != 0u);
|
||||
let normal = horizon_material_normal(in.world);
|
||||
let tangent = horizon_material_tangent(in.world, in.uv, normal);
|
||||
",
|
||||
$input,
|
||||
$result,
|
||||
"}
|
||||
"
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
const MATERIAL_ENTRIES: &str = material_entries!(
|
||||
" let input = MaterialInput(in.albedo, in.tint, in.light, in.lightmap, in.world, in.uv, in.translucent != 0u, in.tags, in.material,
|
||||
in.position.xy / frame.resolution, in.face, fluid, in.emission, normal, tangent, vec3<f32>(0.0), 0.0);
|
||||
",
|
||||
" return material(input).color;\n"
|
||||
);
|
||||
|
||||
/// 0.2's: the colours decoded on the way in, and the result encoded for the frame
|
||||
/// (`[frame] blend = "gamma"`; under `"linear"` the renderer stores it as it is).
|
||||
const MATERIAL_ENTRIES_0_2: &str = material_entries!(
|
||||
" let input = MaterialInput(horizon_decode_rgba(in.albedo), horizon_decode_rgba(in.tint), in.light,
|
||||
horizon_decode(in.lightmap), in.world, in.uv, in.translucent != 0u, in.tags, in.material,
|
||||
in.position.xy / frame.resolution, in.face, fluid, in.emission, normal, tangent, vec3<f32>(0.0), 0.0);
|
||||
",
|
||||
" return horizon_encode_rgba(material(input).color);\n"
|
||||
);
|
||||
|
||||
/// Entry points with the sky stage's inputs: the renderer calls `sky` from
|
||||
/// its own sky shader in the same way.
|
||||
const SKY_ENTRIES: &str = "\
|
||||
struct HorizonSkyIn {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) direction: vec3<f32>,
|
||||
@location(1) color: vec4<f32>,
|
||||
}
|
||||
@fragment fn horizon_fs_sky(in: HorizonSkyIn) -> @location(0) vec4<f32> {
|
||||
return sky(SkyInput(normalize(in.direction), in.color));
|
||||
}
|
||||
";
|
||||
|
||||
const SKY_ENTRIES_0_2: &str = "\
|
||||
struct HorizonSkyIn {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) direction: vec3<f32>,
|
||||
@location(1) color: vec4<f32>,
|
||||
}
|
||||
@fragment fn horizon_fs_sky(in: HorizonSkyIn) -> @location(0) vec4<f32> {
|
||||
return horizon_encode_rgba(sky(SkyInput(normalize(in.direction), horizon_decode_rgba(in.color))));
|
||||
}
|
||||
";
|
||||
|
||||
/// An entry point with the weather stage's inputs: the renderer calls `weather` from its own
|
||||
/// weather shader in the same way.
|
||||
const VERTEX_ENTRIES: &str = "\
|
||||
@vertex fn horizon_vs_vertex(
|
||||
@location(0) position: vec3<f32>, @location(1) uv: vec2<f32>, @location(2) light: vec2<f32>,
|
||||
@location(3) tint: vec4<f32>, @location(4) material: u32, @location(5) flags: u32,
|
||||
) -> @builtin(position) vec4<f32> {
|
||||
let input = VertexInput(position, uv, material, light, tint, flags & 7u, flags >> 31u);
|
||||
return frame.projection * frame.view * vec4<f32>(vertex(input), 1.0);
|
||||
}
|
||||
";
|
||||
|
||||
/// 0.2's: the tint is a colour, so it is decoded too.
|
||||
const VERTEX_ENTRIES_0_2: &str = "\
|
||||
@vertex fn horizon_vs_vertex(
|
||||
@location(0) position: vec3<f32>, @location(1) uv: vec2<f32>, @location(2) light: vec2<f32>,
|
||||
@location(3) tint: vec4<f32>, @location(4) material: u32, @location(5) flags: u32,
|
||||
) -> @builtin(position) vec4<f32> {
|
||||
let input = VertexInput(position, uv, material, light, horizon_decode_rgba(tint), flags & 7u, flags >> 31u);
|
||||
return frame.projection * frame.view * vec4<f32>(vertex(input), 1.0);
|
||||
}
|
||||
";
|
||||
|
||||
/// `fn displacement(input: DisplacementInput) -> f32`, the material stage's function
|
||||
/// under `[displacement]`.
|
||||
pub const DISPLACEMENT_SIGNATURE: &str = "fn displacement(input: DisplacementInput) -> f32";
|
||||
|
||||
/// An entry point calling the material stage's `displacement`, after its entries under
|
||||
/// `[displacement]`: the renderer calls it from its own terrain vertex shader in the same way.
|
||||
pub const DISPLACEMENT_ENTRIES: &str = "@vertex fn horizon_vs_displacement(
|
||||
@location(0) position: vec3<f32>, @location(1) normal: vec3<f32>, @location(2) uv: vec2<f32>,
|
||||
@location(3) material: u32, @location(4) height: f32,
|
||||
) -> @builtin(position) vec4<f32> {
|
||||
let depth = displacement(DisplacementInput(position, normal, uv, material, height));
|
||||
return frame.projection * frame.view * vec4<f32>(position - normal * depth, 1.0);
|
||||
}
|
||||
";
|
||||
|
||||
const WEATHER_ENTRIES: &str = "\
|
||||
struct HorizonWeatherIn {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) color: vec4<f32>,
|
||||
@location(1) light: vec2<f32>,
|
||||
@location(2) world: vec3<f32>,
|
||||
@location(3) @interpolate(flat) snow: u32,
|
||||
}
|
||||
@fragment fn horizon_fs_weather(in: HorizonWeatherIn) -> @location(0) vec4<f32> {
|
||||
return weather(WeatherInput(in.color, in.light, in.world, in.snow != 0u));
|
||||
}
|
||||
";
|
||||
|
||||
const WEATHER_ENTRIES_0_2: &str = "\
|
||||
struct HorizonWeatherIn {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) color: vec4<f32>,
|
||||
@location(1) light: vec2<f32>,
|
||||
@location(2) world: vec3<f32>,
|
||||
@location(3) @interpolate(flat) snow: u32,
|
||||
}
|
||||
@fragment fn horizon_fs_weather(in: HorizonWeatherIn) -> @location(0) vec4<f32> {
|
||||
let input = WeatherInput(horizon_decode_rgba(in.color), in.light, in.world, in.snow != 0u);
|
||||
return horizon_encode_rgba(weather(input));
|
||||
}
|
||||
";
|
||||
|
||||
/// The post stage's entry points under 0.2: the chain's targets hold linear light, so nothing
|
||||
/// converts, and the pass's alpha is ignored.
|
||||
const POST_ENTRIES_0_2: &str = "\
|
||||
struct HorizonPostOut {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) uv: vec2<f32>,
|
||||
}
|
||||
@vertex fn horizon_vs_post(@builtin(vertex_index) i: u32) -> HorizonPostOut {
|
||||
let p = vec2<f32>(f32((i << 1u) & 2u), f32(i & 2u));
|
||||
var out: HorizonPostOut;
|
||||
out.position = vec4<f32>(p * 2.0 - 1.0, 0.0, 1.0);
|
||||
out.uv = vec2<f32>(p.x, 1.0 - p.y);
|
||||
return out;
|
||||
}
|
||||
@fragment fn horizon_fs_post(in: HorizonPostOut) -> @location(0) vec4<f32> {
|
||||
let color = textureSampleLevel(previous, linear_sampler, in.uv, 0.0);
|
||||
return vec4<f32>(post(PostInput(in.uv, in.position.xy, color, depth_at(in.uv))).rgb, 1.0);
|
||||
}
|
||||
";
|
||||
|
||||
/// The tonemap stage's entry points: a full-screen triangle at the window's size, and a fragment
|
||||
/// that holds the result to what the display pass takes.
|
||||
const TONEMAP_ENTRIES: &str = "\
|
||||
struct HorizonTonemapOut {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) uv: vec2<f32>,
|
||||
}
|
||||
@vertex fn horizon_vs_tonemap(@builtin(vertex_index) i: u32) -> HorizonTonemapOut {
|
||||
let p = vec2<f32>(f32((i << 1u) & 2u), f32(i & 2u));
|
||||
var out: HorizonTonemapOut;
|
||||
out.position = vec4<f32>(p * 2.0 - 1.0, 0.0, 1.0);
|
||||
out.uv = vec2<f32>(p.x, 1.0 - p.y);
|
||||
return out;
|
||||
}
|
||||
@fragment fn horizon_fs_tonemap(in: HorizonTonemapOut) -> @location(0) vec4<f32> {
|
||||
let color = horizon_sanitize(textureSampleLevel(previous, linear_sampler, in.uv, 0.0).rgb);
|
||||
let out = tonemap(TonemapInput(in.uv, in.position.xy, color));
|
||||
return vec4<f32>(min(horizon_sanitize(out), vec3<f32>(frame.headroom)), 1.0);
|
||||
}
|
||||
";
|
||||
|
||||
impl Api {
|
||||
/// The stages the line has, in `StageKind::ALL`'s order.
|
||||
pub fn stages(self) -> &'static [StageKind] {
|
||||
match self {
|
||||
Api::V0_1 => &StageKind::ALL[..5],
|
||||
Api::V0_2 => &StageKind::ALL,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StageKind {
|
||||
pub const ALL: [StageKind; 6] = [
|
||||
StageKind::Sky,
|
||||
StageKind::Vertex,
|
||||
StageKind::Material,
|
||||
StageKind::Weather,
|
||||
StageKind::Post,
|
||||
StageKind::Tonemap,
|
||||
];
|
||||
|
||||
/// The manifest's name for the stage (`[sky]`, `[vertex]`, `[material]`, `[weather]`,
|
||||
/// `[[post]]`, `[tonemap]`).
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
StageKind::Sky => "sky",
|
||||
StageKind::Material => "material",
|
||||
StageKind::Vertex => "vertex",
|
||||
StageKind::Weather => "weather",
|
||||
StageKind::Post => "post",
|
||||
StageKind::Tonemap => "tonemap",
|
||||
}
|
||||
}
|
||||
|
||||
/// The function a pack defines: it has the stage's name.
|
||||
pub fn function(self) -> &'static str {
|
||||
self.name()
|
||||
}
|
||||
|
||||
pub fn input(self) -> &'static Struct {
|
||||
match self {
|
||||
StageKind::Sky => &SKY_INPUT,
|
||||
StageKind::Material => &MATERIAL_INPUT,
|
||||
StageKind::Vertex => &VERTEX_INPUT,
|
||||
StageKind::Weather => &WEATHER_INPUT,
|
||||
StageKind::Post => &POST_INPUT,
|
||||
StageKind::Tonemap => &TONEMAP_INPUT,
|
||||
}
|
||||
}
|
||||
|
||||
/// The type the function returns.
|
||||
pub fn output(self) -> &'static str {
|
||||
match self {
|
||||
StageKind::Material => SURFACE.name,
|
||||
StageKind::Vertex | StageKind::Tonemap => "vec3<f32>",
|
||||
StageKind::Sky | StageKind::Weather | StageKind::Post => "vec4<f32>",
|
||||
}
|
||||
}
|
||||
|
||||
/// `fn <name>(input: <Input>) -> <Output>`.
|
||||
pub fn signature(self) -> String {
|
||||
format!(
|
||||
"fn {}(input: {}) -> {}",
|
||||
self.function(),
|
||||
self.input().name,
|
||||
self.output()
|
||||
)
|
||||
}
|
||||
|
||||
pub fn resources(self) -> &'static [Resource] {
|
||||
match self {
|
||||
StageKind::Post => &POST_RESOURCES,
|
||||
StageKind::Sky | StageKind::Vertex | StageKind::Weather => std::slice::from_ref(&FRAME),
|
||||
StageKind::Material => &MATERIAL_RESOURCES,
|
||||
StageKind::Tonemap => &TONEMAP_RESOURCES,
|
||||
}
|
||||
}
|
||||
|
||||
/// Everything the pack's files may use under the line `api`, declared before them.
|
||||
pub fn prelude(self, api: Api) -> String {
|
||||
let mut out = match api {
|
||||
Api::V0_1 => {
|
||||
String::from("// Horizon shader API prelude (docs/api/shaders-reference.md)\n")
|
||||
}
|
||||
Api::V0_2 => String::from(
|
||||
"// Horizon shader API 0.2 prelude (docs/api/shaders-reference-0.2.md)\n",
|
||||
),
|
||||
};
|
||||
out += &Frame::line(api).wgsl();
|
||||
out += &self.input().wgsl();
|
||||
if self == StageKind::Material {
|
||||
out += &SURFACE.wgsl();
|
||||
// The vertex stage's file joins the material's module (`compose`).
|
||||
out += &VERTEX_INPUT.wgsl();
|
||||
out += &DISPLACEMENT_INPUT.wgsl();
|
||||
}
|
||||
for r in self.resources() {
|
||||
out += &format!("@group({GROUP}) @binding({}) {};\n", r.binding, r.ty);
|
||||
}
|
||||
if api == Api::V0_2 {
|
||||
out += COLOR_HELPERS;
|
||||
}
|
||||
match self {
|
||||
StageKind::Post => {
|
||||
out += POST_HELPERS;
|
||||
out += POST_SURFACE;
|
||||
out += SHADOW_HELPERS;
|
||||
}
|
||||
StageKind::Material => {
|
||||
out += match api {
|
||||
Api::V0_1 => MATERIAL_HELPERS,
|
||||
Api::V0_2 => MATERIAL_HELPERS_0_2,
|
||||
};
|
||||
out += SHADOW_HELPERS;
|
||||
}
|
||||
StageKind::Vertex => out += SHADOW_HELPERS,
|
||||
StageKind::Sky | StageKind::Weather | StageKind::Tonemap => {}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The entry points put after the pack's files under the line `api`.
|
||||
pub fn entries(self, api: Api) -> &'static str {
|
||||
match (api, self) {
|
||||
(Api::V0_1, StageKind::Sky) => SKY_ENTRIES,
|
||||
(Api::V0_1, StageKind::Material) => MATERIAL_ENTRIES,
|
||||
(Api::V0_1, StageKind::Vertex) => VERTEX_ENTRIES,
|
||||
(Api::V0_1, StageKind::Weather) => WEATHER_ENTRIES,
|
||||
(Api::V0_1, StageKind::Post) => POST_ENTRIES,
|
||||
(Api::V0_2, StageKind::Sky) => SKY_ENTRIES_0_2,
|
||||
(Api::V0_2, StageKind::Material) => MATERIAL_ENTRIES_0_2,
|
||||
(Api::V0_2, StageKind::Vertex) => VERTEX_ENTRIES_0_2,
|
||||
(Api::V0_2, StageKind::Weather) => WEATHER_ENTRIES_0_2,
|
||||
(Api::V0_2, StageKind::Post) => POST_ENTRIES_0_2,
|
||||
// 0.1 has no tonemap stage: a manifest with one is refused before it is built.
|
||||
(_, StageKind::Tonemap) => TONEMAP_ENTRIES,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The first binding of a pack's `[[textures]]` in group 3; their samplers follow at
|
||||
/// `TEXTURE_SAMPLER_BINDING`.
|
||||
pub const TEXTURE_BINDING: u32 = 16;
|
||||
pub const TEXTURE_SAMPLER_BINDING: u32 = 24;
|
||||
|
||||
/// The binding of a pack's first post target (`[[post]] output`) in group 3, the others after.
|
||||
pub const POST_TARGET_BINDING: u32 = 32;
|
||||
|
||||
/// Whether a material stage's `source` (with its prelude) calls `set_surface` itself, and so
|
||||
/// needs the surface target, which a pack without it doesn't pay for.
|
||||
pub fn calls_set_surface(source: &str) -> bool {
|
||||
let call = "set_surface(";
|
||||
source.matches(call).count() > MATERIAL_HELPERS.matches(call).count()
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
//! The custom textures a pipeline's programs read: an image of the pack (a PNG or a raw dump,
|
||||
//! decoded) or a resource location the game resolves, bound to a sampler name in the programs of
|
||||
//! a stage (`horizon-shaders` reads them from `shaders.properties`, spec 3.4).
|
||||
|
||||
use crate::Format;
|
||||
|
||||
/// The stage of a `customTexture.<name>`: every program's.
|
||||
pub const EVERY_STAGE: &str = "*";
|
||||
|
||||
/// A texture bound to sampler `name` in the programs of `stage` (`gbuffers`, `deferred`,
|
||||
/// `composite`...; [`EVERY_STAGE`] for a `customTexture`), or the noise texture (stage and
|
||||
/// name `noise`).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CustomTexture {
|
||||
pub stage: String,
|
||||
pub name: String,
|
||||
pub source: TextureSource,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum TextureSource {
|
||||
Image(Texture),
|
||||
/// A resource location for the game to resolve.
|
||||
Resource(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Dim {
|
||||
D1,
|
||||
D2,
|
||||
D3,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Texture {
|
||||
pub dim: Dim,
|
||||
/// Width, height and depth (1 for the dimensions the texture lacks).
|
||||
pub size: [u32; 3],
|
||||
/// The internal format the pack asked for.
|
||||
pub format: Format,
|
||||
pub texels: Texels,
|
||||
/// Linear filtering, and clamping instead of repeating: on for a raw texture and off for a
|
||||
/// PNG, unless the `.mcmeta` beside it says otherwise.
|
||||
pub blur: bool,
|
||||
pub clamp: bool,
|
||||
/// A raw dump rather than a PNG.
|
||||
pub raw: bool,
|
||||
}
|
||||
|
||||
/// Texel values, `components` per texel: 1, 2 or 4 (three widen to four, alpha 1).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Texels {
|
||||
Float(Vec<f32>),
|
||||
Int(Vec<i32>),
|
||||
Uint(Vec<u32>),
|
||||
}
|
||||
|
||||
impl Texture {
|
||||
pub fn components(&self) -> usize {
|
||||
components(self.format.channels)
|
||||
}
|
||||
}
|
||||
|
||||
/// The components per texel a texture of `channels` channels stores: three widen to four.
|
||||
pub fn components(channels: u8) -> usize {
|
||||
match channels {
|
||||
1 => 1,
|
||||
2 => 2,
|
||||
_ => 4,
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,8 @@ rust-version.workspace = true
|
||||
description = "Horizon's own shader packs (WGSL, docs/shaders.md): the format, the stages, the check, and the horizon-shader tool"
|
||||
|
||||
[dependencies]
|
||||
bytemuck = { version = "1.24", features = ["derive"] }
|
||||
# The stage interface packs are written against (the renderer's, engine).
|
||||
horizon-render-pipeline = { path = "../horizon-render-pipeline" }
|
||||
# Parses and validates a pack's WGSL, and translates it as wgpu does for Vulkan, Metal and DirectX 12.
|
||||
png = "0.18"
|
||||
naga = { version = "30.0.1", features = ["wgsl-in", "spv-out", "msl-out", "hlsl-out"] }
|
||||
|
||||
@@ -1,380 +1,3 @@
|
||||
//! What a pack's WGSL reads: the frame uniform every stage sees, which the
|
||||
//! renderer fills and uploads as the Rust struct below, and the stage input
|
||||
//! and output structs. The WGSL of each is generated from these definitions
|
||||
//! (`Struct::wgsl`), and so is the reference (docs/api/shaders-reference.md).
|
||||
|
||||
/// One field of a WGSL struct, for the prelude and the reference.
|
||||
#[derive(Debug)]
|
||||
pub struct Field {
|
||||
pub name: &'static str,
|
||||
pub ty: &'static str,
|
||||
/// Its meaning, unit and range.
|
||||
pub doc: &'static str,
|
||||
}
|
||||
|
||||
/// A struct a pack's WGSL can name.
|
||||
#[derive(Debug)]
|
||||
pub struct Struct {
|
||||
pub name: &'static str,
|
||||
pub doc: &'static str,
|
||||
pub fields: &'static [Field],
|
||||
}
|
||||
|
||||
impl Struct {
|
||||
pub fn wgsl(&self) -> String {
|
||||
let mut out = format!("struct {} {{\n", self.name);
|
||||
for f in self.fields {
|
||||
out += &format!(" {}: {},\n", f.name, f.ty);
|
||||
}
|
||||
out + "}\n"
|
||||
}
|
||||
}
|
||||
|
||||
/// The WGSL type of a Rust field the renderer uploads.
|
||||
pub trait Wgsl {
|
||||
const TYPE: &'static str;
|
||||
}
|
||||
|
||||
macro_rules! wgsl_types {
|
||||
($($rust:ty => $wgsl:literal),* $(,)?) => {
|
||||
$(impl Wgsl for $rust {
|
||||
const TYPE: &'static str = $wgsl;
|
||||
})*
|
||||
};
|
||||
}
|
||||
|
||||
wgsl_types! {
|
||||
f32 => "f32",
|
||||
u32 => "u32",
|
||||
i32 => "i32",
|
||||
[f32; 2] => "vec2<f32>",
|
||||
[f32; 3] => "vec3<f32>",
|
||||
[f32; 4] => "vec4<f32>",
|
||||
[[f32; 4]; 4] => "mat4x4<f32>",
|
||||
[[f32; 4]; 8] => "array<vec4<f32>, 8>",
|
||||
}
|
||||
|
||||
/// A uniform struct: the Rust type the renderer uploads, and its WGSL. `pad` names a trailing
|
||||
/// field in Rust only, which fills the struct out to WGSL's size (a multiple of its 16-byte
|
||||
/// alignment), since `Pod` allows no implicit padding.
|
||||
macro_rules! uniform {
|
||||
($(#[doc = $doc:literal])* $name:ident {
|
||||
$($(#[doc = $fdoc:literal])* $field:ident: $ty:ty,)*
|
||||
} $(pad $pad:ident: $pad_ty:ty)?) => {
|
||||
$(#[doc = $doc])*
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct $name {
|
||||
$($(#[doc = $fdoc])* pub $field: $ty,)*
|
||||
$(/// Not in the WGSL: zeros up to its size.
|
||||
pub $pad: $pad_ty,)?
|
||||
}
|
||||
|
||||
impl $name {
|
||||
pub const WGSL: Struct = Struct {
|
||||
name: stringify!($name),
|
||||
doc: concat!($($doc, "\n"),*),
|
||||
fields: &[$(Field {
|
||||
name: stringify!($field),
|
||||
ty: <$ty as Wgsl>::TYPE,
|
||||
doc: concat!($($fdoc),*),
|
||||
}),*],
|
||||
};
|
||||
/// Each field's byte offset, in `WGSL.fields`' order.
|
||||
pub const OFFSETS: &'static [usize] = &[$(std::mem::offset_of!($name, $field)),*];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
uniform! {
|
||||
/// The frame, as `frame` in every stage. Positions and directions are
|
||||
/// in world axes relative to the camera (x east, y up, z south), so they
|
||||
/// stay precise far from the world's origin; depth is reversed (1 near,
|
||||
/// 0 at the far plane and in the sky).
|
||||
Frame {
|
||||
/// Camera-relative world to view space: the camera's rotation.
|
||||
view: [[f32; 4]; 4],
|
||||
/// View to clip space (reversed depth, wgpu's clip space: z 0..1).
|
||||
projection: [[f32; 4]; 4],
|
||||
/// View to camera-relative world space.
|
||||
view_inverse: [[f32; 4]; 4],
|
||||
/// Clip to view space.
|
||||
projection_inverse: [[f32; 4]; 4],
|
||||
/// The camera's world position, in blocks (about a millimetre of precision 10 000 blocks out).
|
||||
camera_position: [f32; 3],
|
||||
/// Seconds since the pack was loaded, wrapping to 0 every hour.
|
||||
time: f32,
|
||||
/// Unit vector toward the sun (below the horizon at night).
|
||||
sun_direction: [f32; 3],
|
||||
/// The time of day, 0..1: 0 sunrise, 0.25 noon, 0.5 sunset, 0.75 midnight.
|
||||
day_time: f32,
|
||||
/// Unit vector toward the moon.
|
||||
moon_direction: [f32; 3],
|
||||
/// Vanilla's GameTime: (game time % 24000 + partial tick) / 24000, 0..1.
|
||||
game_time: f32,
|
||||
/// Vanilla's sky colour this frame, 0..1.
|
||||
sky_color: [f32; 3],
|
||||
/// Rain strength, 0..1.
|
||||
rain: f32,
|
||||
/// Vanilla's fog colour this frame (also the clear colour), 0..1.
|
||||
fog_color: [f32; 3],
|
||||
/// Thunder strength, 0..1.
|
||||
thunder: f32,
|
||||
/// The size of the world's picture, in pixels (the resolution scale applied).
|
||||
resolution: [f32; 2],
|
||||
/// Block and sky light at the camera, 0..1 (level / 15).
|
||||
eye_light: [f32; 2],
|
||||
/// Where vanilla's fog starts, in blocks from the camera.
|
||||
fog_start: f32,
|
||||
/// Where vanilla's fog is full, in blocks.
|
||||
fog_end: f32,
|
||||
/// The render distance, in blocks.
|
||||
render_distance: f32,
|
||||
/// The far plane, in blocks.
|
||||
far: f32,
|
||||
/// Night vision strength, 0..1.
|
||||
night_vision: f32,
|
||||
/// Blindness strength, 0..1.
|
||||
blindness: f32,
|
||||
/// The darkness effect's strength, 0..1.
|
||||
darkness: f32,
|
||||
/// The Brightness option, 0..1 (Moody to Bright).
|
||||
brightness: f32,
|
||||
/// What the camera is in: 0 air, 1 water, 2 lava, 3 powder snow.
|
||||
eye_in: u32,
|
||||
/// 0 the Overworld, -1 the Nether, 1 the End, 2 any other dimension.
|
||||
dimension: i32,
|
||||
/// Frames drawn since the pack was loaded (wraps).
|
||||
frame_index: u32,
|
||||
/// 1 while F1 hides the HUD, else 0.
|
||||
hide_gui: u32,
|
||||
/// What falls in the biome at the camera while it rains: 0 nothing, 1 rain, 2 snow.
|
||||
precipitation: u32,
|
||||
/// The biome at the camera's temperature (vanilla's, -0.7..2.0; it snows below 0.15).
|
||||
temperature: f32,
|
||||
/// The biome at the camera's downfall, 0..1.
|
||||
downfall: f32,
|
||||
/// `rain`, smoothed: rises over about 30 seconds and dries over about 10, 0..1.
|
||||
wetness: f32,
|
||||
/// Last frame's camera-relative world to clip space (its `projection * view`); a point
|
||||
/// at camera-relative `p` now was at `p + camera_motion` relative to last frame's camera.
|
||||
/// This frame's on the first frame and after a gap of over half a second (a pause, a
|
||||
/// world load), when last frame's camera says nothing about this one.
|
||||
previous_view_projection: [[f32; 4]; 4],
|
||||
/// How far the camera moved since last frame, in blocks (0 after such a gap).
|
||||
camera_motion: [f32; 3],
|
||||
/// Seconds since last frame (0 on the first and after such a gap).
|
||||
delta_time: f32,
|
||||
/// `eye_light` eased with a half-life of 2 seconds: it follows the light levels' steps
|
||||
/// without stepping itself (for exposure and indoor fog).
|
||||
eye_light_smooth: [f32; 2],
|
||||
/// 1 while the camera is in water (`eye_in` 1), eased with a half-life of a quarter
|
||||
/// second in and out.
|
||||
in_water: f32,
|
||||
/// The same for lava (`eye_in` 2).
|
||||
in_lava: f32,
|
||||
/// The y of the horizontal water plane `reflection` mirrors the world about, in world
|
||||
/// blocks, while `has_reflection` is 1.
|
||||
water_plane: f32,
|
||||
/// 1 while the material stage's `reflection` holds this frame's water mirror, else 0.
|
||||
has_reflection: u32,
|
||||
/// The size of `reflection`, in pixels: half the world's picture.
|
||||
reflection_resolution: [f32; 2],
|
||||
/// Eight values a native mod hands its own pack each frame (`native_shader_params`,
|
||||
/// docs/native-mods.md): what they mean is that mod's, such as where and when
|
||||
/// something happened in the world. All 0 when no mod sets them.
|
||||
mods: [[f32; 4]; 8],
|
||||
/// Unit vector toward the body casting this frame's shadows: the sun by day, the moon
|
||||
/// by night.
|
||||
light_direction: [f32; 3],
|
||||
/// 1 while `shadow_map` holds this frame's shadow map, else 0.
|
||||
has_shadow: u32,
|
||||
/// Camera-relative world to the shadow camera's clip space, before the distortion
|
||||
/// (orthographic, looking along `-light_direction`; z 0 at the light, 1 away from it).
|
||||
shadow_view_proj: [[f32; 4]; 4],
|
||||
/// Half the side of the square the shadow map covers, in blocks (the manifest's
|
||||
/// `[shadow] distance`); 0 without a shadow map.
|
||||
shadow_distance: f32,
|
||||
/// How strongly the shadow map is distorted toward its centre, 0..1: a point at
|
||||
/// shadow clip `xy` is stored at `xy / (length(xy) * k + 1 - k)`, so texels near the
|
||||
/// camera cover less ground (`shadow_position` applies it).
|
||||
shadow_distortion: f32,
|
||||
/// The shadow map's side, in texels; 0 without a shadow map.
|
||||
shadow_resolution: f32,
|
||||
/// The ground one shadow texel covers at the map's centre, in blocks (for a bias that
|
||||
/// scales with it); 0 without a shadow map.
|
||||
shadow_center_texel: f32,
|
||||
/// 1 while the output is HDR, else 0.
|
||||
hdr: u32,
|
||||
/// The peak over paper white: the brightest value the display shows (Apple's EDR
|
||||
/// headroom, Android's HDR/SDR ratio). 1 in SDR. It changes while the game runs (the
|
||||
/// system's SDR white, a calibration, another display), so read it every frame.
|
||||
headroom: f32,
|
||||
/// Paper white in nits; a nominal 80 in SDR.
|
||||
paper_white: f32,
|
||||
/// The peak brightness in nits; a nominal 80 in SDR, as `paper_white`.
|
||||
peak: f32,
|
||||
/// The display's black in nits (HDR's Black Level); 0 in SDR.
|
||||
black_level: f32,
|
||||
} pad padding: [u32; 3]
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
/// The fields 0.1's prelude declares: the struct up to `shadow_center_texel`, a prefix of
|
||||
/// the buffer the renderer uploads.
|
||||
pub const FIELDS_0_1: usize = 49;
|
||||
|
||||
/// `Frame` as the line's prelude declares it.
|
||||
pub fn line(api: crate::Api) -> Struct {
|
||||
let fields = match api {
|
||||
crate::Api::V0_1 => &Self::WGSL.fields[..Self::FIELDS_0_1],
|
||||
crate::Api::V0_2 => Self::WGSL.fields,
|
||||
};
|
||||
Struct {
|
||||
fields,
|
||||
..Self::WGSL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! fields {
|
||||
($($name:ident: $ty:literal = $doc:literal,)*) => {
|
||||
&[$(Field { name: stringify!($name), ty: $ty, doc: $doc }),*]
|
||||
};
|
||||
}
|
||||
|
||||
/// The post stage's input: one pixel of the picture.
|
||||
pub const POST_INPUT: Struct = Struct {
|
||||
name: "PostInput",
|
||||
doc: "One pixel of the picture, for `fn post(input: PostInput) -> vec4<f32>`.",
|
||||
fields: fields! {
|
||||
uv: "vec2<f32>" = "Where the pixel is, 0..1 from the top left.",
|
||||
pixel: "vec2<f32>" = "The pixel's centre, in pixels from the top left of what the pass draws (the world's size; the window's for the last pass).",
|
||||
color: "vec4<f32>" = "The previous pass's colour here (the world's picture for the first pass).",
|
||||
depth: "f32" = "The world's depth here, reversed: 0 is the sky.",
|
||||
},
|
||||
};
|
||||
|
||||
/// The material stage's input: one point of a block's surface.
|
||||
pub const MATERIAL_INPUT: Struct = Struct {
|
||||
name: "MaterialInput",
|
||||
doc: "One point of a block's surface, for `fn material(input: MaterialInput) -> Surface`.",
|
||||
fields: fields! {
|
||||
albedo: "vec4<f32>" = "The block texture's colour here (alpha 0..1).",
|
||||
tint: "vec4<f32>" = "The vertex colour: biome tint, face shade and ambient occlusion.",
|
||||
light: "vec2<f32>" = "Block and sky light, 0..1, smoothed over the face.",
|
||||
lightmap: "vec3<f32>" = "Vanilla's light colour for that light this frame.",
|
||||
position: "vec3<f32>" = "The point, camera-relative, in blocks.",
|
||||
uv: "vec2<f32>" = "The texture coordinate inside the block atlas.",
|
||||
translucent: "bool" = "Drawn in the translucent pass (water, stained glass, ice).",
|
||||
tags: "u32" = "The pack's sprite tags this surface's sprite has: bit i for the manifest's i-th `[[tags]]`.",
|
||||
material: "u32" = "The `[[materials]]` id of this surface's sprite (0 when no entry names it).",
|
||||
screen: "vec2<f32>" = "Where the point is on the screen, 0..1 from the top left (`opaque_depth` and `opaque_color` at `screen * frame.resolution`).",
|
||||
face: "u32" = "For a fluid, which face of the block the point is on, `Direction` order: 0 down, 1 up, 2 north, 3 south, 4 west, 5 east. 0 elsewhere.",
|
||||
fluid: "u32" = "0 not a fluid, 1 water, 2 lava.",
|
||||
emission: "f32" = "How strongly the block itself shines, 0..1, by its kind of light: a redstone torch or a candle 7/15, a torch or lava 14/15, glowstone 1; 0 for most blocks.",
|
||||
normal: "vec3<f32>" = "The surface's geometric normal, a unit vector in world axes, facing the camera. A normal within about 3.6 degrees of an axis is that axis exactly, so distant block faces do not shimmer; flowing water's slopes are steeper and keep theirs.",
|
||||
tangent: "vec4<f32>" = "The surface's tangent: xyz the unit direction in world axes along which `uv.x` grows, w the sign (1 or -1) that makes `cross(normal, tangent.xyz) * tangent.w` the direction `uv.y` grows along (down the texture). With `normal` it turns a normal map's normals into world axes.",
|
||||
colored_light: "vec3<f32>" = "The frame's coloured lights at this point (`[[particle_lights]]`, held and dropped items in their `[[lights]]` colour while dynamic lights are on, and mods' visual lights), 0..1, to light the surface with as the pack likes; 0 where none reaches.",
|
||||
displaced: "f32" = "How much of the relief `[displacement]` gave this point as geometry, 0..1: 1 near the camera, where the surface already lies where `displacement` put it (`position` is that point; a parallax effect would count its depth twice), easing to 0 at `[displacement] distance`; always 0 without `[displacement]`.",
|
||||
},
|
||||
};
|
||||
|
||||
/// What the material stage returns.
|
||||
pub const SURFACE: Struct = Struct {
|
||||
name: "Surface",
|
||||
doc: "What `material` returns. Vanilla's own is `albedo * tint * vec4(lightmap, 1.0)`; \
|
||||
Horizon then applies the alpha test (cutout and translucent) and fog.",
|
||||
fields: fields! {
|
||||
color: "vec4<f32>" = "The lit colour, 0..1, with its alpha.",
|
||||
},
|
||||
};
|
||||
|
||||
/// The sky stage's input: one direction of the sky.
|
||||
pub const SKY_INPUT: Struct = Struct {
|
||||
name: "SkyInput",
|
||||
doc: "One direction of the sky, for `fn sky(input: SkyInput) -> vec4<f32>`. The sun, moon, \
|
||||
stars and clouds are drawn over the result.",
|
||||
fields: fields! {
|
||||
direction: "vec3<f32>" = "Unit view ray, camera-relative world axes.",
|
||||
color: "vec4<f32>" = "Vanilla's sky colour along the ray (sky, fog and sunrise).",
|
||||
},
|
||||
};
|
||||
|
||||
/// The input of the material stage's `displacement`, under `[displacement]`.
|
||||
pub const DISPLACEMENT_INPUT: Struct = Struct {
|
||||
name: "DisplacementInput",
|
||||
doc: "One corner of the grid a block face near the camera is cut into, for `fn displacement(input: DisplacementInput) -> f32` (`[displacement]`), which returns how deep below the face, in blocks, the surface lies at `height`.",
|
||||
fields: fields! {
|
||||
position: "vec3<f32>" = "The corner on the flat face, camera-relative, in blocks.",
|
||||
normal: "vec3<f32>" = "The face's normal, a unit vector in world axes.",
|
||||
uv: "vec2<f32>" = "The texture coordinate inside the block's sprite (as `MaterialInput.uv`).",
|
||||
material: "u32" = "The `[[materials]]` id of the block texture (0 when none matches).",
|
||||
height: "f32" = "A LabPBR height, 0..1 (1 the top of the relief): the normal map's alpha here, or the texture's average height (what the face's edges are pinned to).",
|
||||
},
|
||||
};
|
||||
|
||||
/// The vertex stage's input: one corner of a block's surface.
|
||||
pub const VERTEX_INPUT: Struct = Struct {
|
||||
name: "VertexInput",
|
||||
doc: "One corner of a block's surface, for `fn vertex(input: VertexInput) -> vec3<f32>`, which returns where it is drawn (camera-relative, in blocks; `input.position` where it stays).",
|
||||
fields: fields! {
|
||||
position: "vec3<f32>" = "The corner, camera-relative, in blocks (`+ frame.camera_position` for the world's).",
|
||||
uv: "vec2<f32>" = "The texture coordinate inside the block's sprite.",
|
||||
material: "u32" = "The `[[materials]]` id of the block's sprite (0 when none matches).",
|
||||
light: "vec2<f32>" = "Block and sky light, 0..1.",
|
||||
tint: "vec4<f32>" = "The vertex colour (biome tint, face shade, ambient occlusion); alpha 1.",
|
||||
face: "u32" = "For a fluid, the face of the block the corner is on (as `MaterialInput.face`), else 0.",
|
||||
fluid: "u32" = "1 on a fluid's surface (water or lava), else 0.",
|
||||
},
|
||||
};
|
||||
|
||||
/// The weather stage's input: one point of a rain or snow streak.
|
||||
pub const WEATHER_INPUT: Struct = Struct {
|
||||
name: "WeatherInput",
|
||||
doc: "One point of a rain or snow streak, for `fn weather(input: WeatherInput) -> vec4<f32>`, \
|
||||
which returns the colour blended over the world (alpha its opacity).",
|
||||
fields: fields! {
|
||||
color: "vec4<f32>" = "The rain or snow texture's colour here times the streak's vertex colour (alpha 0.1..1).",
|
||||
light: "vec2<f32>" = "Block and sky light, 0..1.",
|
||||
position: "vec3<f32>" = "The point, camera-relative, in blocks.",
|
||||
snow: "bool" = "A snow streak (rain otherwise).",
|
||||
},
|
||||
};
|
||||
|
||||
/// The tonemap stage's input (0.2): one pixel of the window.
|
||||
pub const TONEMAP_INPUT: Struct = Struct {
|
||||
name: "TonemapInput",
|
||||
doc: "One pixel of an output (the window), for `fn tonemap(input: TonemapInput) -> vec3<f32>`, which returns its colour for the output `frame` describes: linear light, 1.0 at paper white, from 0 to `frame.headroom` (Horizon clamps it there).",
|
||||
fields: fields! {
|
||||
uv: "vec2<f32>" = "Where the pixel is, 0..1 from the top left.",
|
||||
pixel: "vec2<f32>" = "The pixel's centre, in pixels from the top left of the output (the window).",
|
||||
color: "vec3<f32>" = "The post chain's picture here (the world's without one), linear light, never negative or NaN.",
|
||||
},
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn frame_fields_carry_their_docs() {
|
||||
let wgsl = Frame::WGSL;
|
||||
assert_eq!(wgsl.fields.len(), 54);
|
||||
assert!(wgsl.fields.iter().all(|f| !f.doc.trim().is_empty()));
|
||||
// 0.1's 49 fields, then 0.2's five and the padding to a multiple of 16.
|
||||
assert_eq!(
|
||||
std::mem::size_of::<Frame>(),
|
||||
6 * 64 + 15 * 16 + 8 * 16 + 2 * 16
|
||||
);
|
||||
assert_eq!(Frame::OFFSETS[Frame::FIELDS_0_1], 6 * 64 + 15 * 16 + 8 * 16);
|
||||
assert_eq!(
|
||||
Frame::line(crate::Api::V0_1).fields.len(),
|
||||
Frame::FIELDS_0_1
|
||||
);
|
||||
assert!(
|
||||
wgsl.wgsl()
|
||||
.contains(" camera_position: vec3<f32>,\n time: f32,\n")
|
||||
);
|
||||
}
|
||||
}
|
||||
//! What a pack's WGSL reads: the renderer's frame uniform and the stages' input and output
|
||||
//! structs (`horizon_render_pipeline::inputs`).
|
||||
pub use horizon_render_pipeline::inputs::*;
|
||||
@@ -9,37 +9,7 @@ pub const SCHEMA: u32 = 1;
|
||||
/// templates write, and what the game and the registry take.
|
||||
pub const API: &str = Api::V0_1.as_str();
|
||||
|
||||
/// A shader API line: the stages, their signatures and the inputs a pack's WGSL may read.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum Api {
|
||||
/// Vanilla's gamma values.
|
||||
V0_1,
|
||||
/// Linear light (docs/design/hdr-linear-light.md). Experimental: checked, not yet drawn.
|
||||
V0_2,
|
||||
}
|
||||
|
||||
impl Api {
|
||||
/// Every line this build reads and checks, oldest first.
|
||||
pub const SERVED: [Api; 2] = [Api::V0_1, Api::V0_2];
|
||||
|
||||
/// The manifest's `api` value.
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Api::V0_1 => "0.1",
|
||||
Api::V0_2 => "0.2",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(line: &str) -> Option<Api> {
|
||||
Self::SERVED.into_iter().find(|a| a.as_str() == line)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Api {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
pub use horizon_render_pipeline::Api;
|
||||
|
||||
/// `[frame] blend` (0.2): what the frame stores, and so how see-through draws blend.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
|
||||
@@ -1,797 +1,10 @@
|
||||
//! The stages a pack can fill, each a WGSL function with a fixed signature
|
||||
//! that Horizon calls inside its own passes, and the WGSL put around a
|
||||
//! pack's files to build a stage: the prelude before (the inputs and the
|
||||
//! resources), the entry points after.
|
||||
use crate::inputs::{
|
||||
DISPLACEMENT_INPUT, Frame, MATERIAL_INPUT, POST_INPUT, SKY_INPUT, SURFACE, Struct,
|
||||
TONEMAP_INPUT, VERTEX_INPUT, WEATHER_INPUT,
|
||||
};
|
||||
use crate::manifest::Api;
|
||||
//! The stages a pack can fill and the WGSL around its files: the stage interface is the
|
||||
//! renderer's (`horizon_render_pipeline::stages`, re-exported here); this module puts a pack's
|
||||
//! files and declarations between its prelude and its entry points.
|
||||
pub use horizon_render_pipeline::stages::*;
|
||||
|
||||
use crate::package::Pack;
|
||||
|
||||
/// The bind group of a pack's resources; groups 0 to 2 are the renderer's.
|
||||
pub const GROUP: u32 = 3;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum StageKind {
|
||||
/// The sky's colour along each view ray, before the sun, moon and stars.
|
||||
Sky,
|
||||
/// The colour of block surfaces (terrain), before the alpha test and fog.
|
||||
Material,
|
||||
/// Where each corner of a block's surface is drawn (waving plants, rolling water), for
|
||||
/// the world and the shadow map.
|
||||
Vertex,
|
||||
/// The colour of rain and snow streaks.
|
||||
Weather,
|
||||
/// A full-screen pass over the world's picture, under the hand and HUD.
|
||||
Post,
|
||||
/// 0.2: the picture for an output (the display), once per pixel of it after the post chain,
|
||||
/// before the camera overlays and the HUD.
|
||||
Tonemap,
|
||||
}
|
||||
|
||||
/// A resource a stage reads, declared by its prelude.
|
||||
#[derive(Debug)]
|
||||
pub struct Resource {
|
||||
pub binding: u32,
|
||||
pub name: &'static str,
|
||||
pub ty: &'static str,
|
||||
pub doc: &'static str,
|
||||
}
|
||||
|
||||
const FRAME: Resource = Resource {
|
||||
binding: 0,
|
||||
name: "frame",
|
||||
ty: "var<uniform> frame: Frame",
|
||||
doc: "The frame (`Frame`).",
|
||||
};
|
||||
|
||||
/// The shadow map at `binding`.
|
||||
const fn shadow_map(binding: u32) -> Resource {
|
||||
Resource {
|
||||
binding,
|
||||
name: "shadow_map",
|
||||
ty: "var shadow_map: texture_depth_2d",
|
||||
doc: "This frame's shadow map: the depth of the solid terrain and entities the light sees, 0 at the light, 1 away from it (and where nothing is), stored at `shadow_position`'s mapping. Valid while `frame.has_shadow` is 1 (the pack asks for it with `[shadow]`), else a 1×1 map that lights everything.",
|
||||
}
|
||||
}
|
||||
|
||||
/// The shadow map's comparison sampler at `binding`.
|
||||
const fn shadow_sampler(binding: u32) -> Resource {
|
||||
Resource {
|
||||
binding,
|
||||
name: "shadow_sampler",
|
||||
ty: "var shadow_sampler: sampler_comparison",
|
||||
doc: "Depth comparison, bilinear (2×2 percentage-closer filtering): `textureSampleCompareLevel(shadow_map, shadow_sampler, p.xy, p.z)` is 1 where the point is lit, 0 in shadow.",
|
||||
}
|
||||
}
|
||||
|
||||
/// The shadow map with translucent terrain at `binding`.
|
||||
const fn shadow_map_translucent(binding: u32) -> Resource {
|
||||
Resource {
|
||||
binding,
|
||||
name: "shadow_map_translucent",
|
||||
ty: "var shadow_map_translucent: texture_depth_2d",
|
||||
doc: "`shadow_map` with the translucent terrain (water, stained glass, ice) the light passes through drawn in too, read with `shadow_sampler` the same way: a point lit in `shadow_map` but not here lies under such a surface, which `shadow_color` gives the colour of.",
|
||||
}
|
||||
}
|
||||
|
||||
/// The colour of the translucent terrain in the shadow map at `binding`.
|
||||
const fn shadow_color(binding: u32) -> Resource {
|
||||
Resource {
|
||||
binding,
|
||||
name: "shadow_color",
|
||||
ty: "var shadow_color: texture_2d<f32>",
|
||||
doc: "The texture colour times the vertex colour (rgb: the tint, the face's directional shade and ambient occlusion) of the nearest translucent surface the light passes through at each texel of `shadow_map_translucent`, white where there is none; alpha is 1 on a fluid (water) and the texture's alpha, at most 0.99, on anything else. Read it with `textureLoad` at `shadow_position`'s xy times `textureDimensions(shadow_color)`.",
|
||||
}
|
||||
}
|
||||
|
||||
/// A function the material and post preludes give packs.
|
||||
pub const SHADOW_HELPERS: &str = "\
|
||||
// The shadow map's distortion toward its centre, of a point at shadow clip `xy`.
|
||||
fn shadow_distort(xy: vec2<f32>) -> vec2<f32> {
|
||||
let k = frame.shadow_distortion;
|
||||
return xy / (length(xy) * k + 1.0 - k);
|
||||
}
|
||||
// Where the camera-relative point `position` is stored in the shadow map: xy its 0..1 texture
|
||||
// coordinate (from the top left), z its depth from the light, for `textureSampleCompareLevel`.
|
||||
fn shadow_position(position: vec3<f32>) -> vec3<f32> {
|
||||
let clip = frame.shadow_view_proj * vec4<f32>(position, 1.0);
|
||||
let xy = shadow_distort(clip.xy);
|
||||
// At most 1, the depth where nothing is: past the map's far end a point stays lit.
|
||||
return vec3<f32>(xy.x * 0.5 + 0.5, 0.5 - xy.y * 0.5, min(clip.z, 1.0));
|
||||
}
|
||||
";
|
||||
|
||||
const LINEAR_SAMPLER: Resource = Resource {
|
||||
binding: 4,
|
||||
name: "linear_sampler",
|
||||
ty: "var linear_sampler: sampler",
|
||||
doc: "Bilinear filtering, clamped to the edges.",
|
||||
};
|
||||
|
||||
const NEAREST_SAMPLER: Resource = Resource {
|
||||
binding: 5,
|
||||
name: "nearest_sampler",
|
||||
ty: "var nearest_sampler: sampler",
|
||||
doc: "Nearest texel, clamped to the edges.",
|
||||
};
|
||||
|
||||
/// The tonemap stage's resources, at the post stage's bindings for the same names.
|
||||
const TONEMAP_RESOURCES: [Resource; 4] = [
|
||||
FRAME,
|
||||
Resource {
|
||||
binding: 3,
|
||||
name: "previous",
|
||||
ty: "var previous: texture_2d<f32>",
|
||||
doc: "The post chain's picture at the window's size (without a post chain, the world's at the world's size), linear light: what `input.color` samples, for a curve that reads around the pixel.",
|
||||
},
|
||||
LINEAR_SAMPLER,
|
||||
NEAREST_SAMPLER,
|
||||
];
|
||||
|
||||
/// 0.2's colour boundary, in every 0.2 prelude: vanilla's 2.2 power both ways.
|
||||
const COLOR_HELPERS: &str = "\
|
||||
// Negatives and NaN made 0 (a comparison with NaN is false), so no `pow` sees them.
|
||||
fn horizon_sanitize(c: vec3<f32>) -> vec3<f32> {
|
||||
return select(vec3<f32>(0.0), c, c > vec3<f32>(0.0));
|
||||
}
|
||||
fn horizon_decode(c: vec3<f32>) -> vec3<f32> {
|
||||
return pow(horizon_sanitize(c), vec3<f32>(2.2));
|
||||
}
|
||||
fn horizon_encode(c: vec3<f32>) -> vec3<f32> {
|
||||
return pow(horizon_sanitize(c), vec3<f32>(1.0 / 2.2));
|
||||
}
|
||||
// Alpha is coverage: it stays as it is.
|
||||
fn horizon_decode_rgba(c: vec4<f32>) -> vec4<f32> {
|
||||
return vec4<f32>(horizon_decode(c.rgb), c.a);
|
||||
}
|
||||
fn horizon_encode_rgba(c: vec4<f32>) -> vec4<f32> {
|
||||
return vec4<f32>(horizon_encode(c.rgb), c.a);
|
||||
}
|
||||
";
|
||||
|
||||
const POST_RESOURCES: [Resource; 12] = [
|
||||
FRAME,
|
||||
Resource {
|
||||
binding: 1,
|
||||
name: "scene_color",
|
||||
ty: "var scene_color: texture_2d<f32>",
|
||||
doc: "The world's picture before any pass.",
|
||||
},
|
||||
Resource {
|
||||
binding: 2,
|
||||
name: "scene_depth",
|
||||
ty: "var scene_depth: texture_depth_2d",
|
||||
doc: "The world's depth, reversed (1 near, 0 far and sky).",
|
||||
},
|
||||
Resource {
|
||||
binding: 3,
|
||||
name: "previous",
|
||||
ty: "var previous: texture_2d<f32>",
|
||||
doc: "The previous pass's output (`scene_color` for the first pass).",
|
||||
},
|
||||
LINEAR_SAMPLER,
|
||||
NEAREST_SAMPLER,
|
||||
shadow_map(6),
|
||||
shadow_sampler(7),
|
||||
Resource {
|
||||
binding: 8,
|
||||
name: "history",
|
||||
ty: "var history: texture_2d<f32>",
|
||||
doc: "Last frame's input to the last pass (what the pass before it wrote), for temporal passes (TAA, denoising): put such a pass second to last and it reads its own previous output; reproject with `frame.previous_view_projection`. Black on the first frame, after a gap of over half a second, and with a single pass.",
|
||||
},
|
||||
shadow_map_translucent(9),
|
||||
shadow_color(10),
|
||||
Resource {
|
||||
binding: 11,
|
||||
name: "scene_surface",
|
||||
ty: "var scene_surface: texture_2d<u32>",
|
||||
doc: "What the material stage's `set_surface` stored for the solid terrain at each pixel, unfiltered (`textureLoad`; its texels are the bits of floats, so `bitcast<vec4<f32>>` them): read it through the prelude's `surface_at`, which knows its layout and where it no longer shows. Blank without a material stage that calls `set_surface`.",
|
||||
},
|
||||
];
|
||||
|
||||
/// The material stage's resources: the frame, and the world as it was before translucent
|
||||
/// terrain drew (for water and glass to see what lies behind them).
|
||||
const MATERIAL_RESOURCES: [Resource; 11] = [
|
||||
FRAME,
|
||||
Resource {
|
||||
binding: 1,
|
||||
name: "reflection",
|
||||
ty: "var reflection: texture_2d<f32>",
|
||||
doc: "The water mirror: the world as a camera mirrored about the horizontal water plane around the camera (`frame.water_plane`) sees it, at half resolution, mapped like the world's picture, so water samples it at `input.screen`. Solid terrain, cut away below the plane, over the vanilla sky gradient: not the pack's `sky` stage, nor the sun, moon and stars. Drawn only for a pack whose material reads it, while `frame.has_reflection` is 1; else a 1×1 blank.",
|
||||
},
|
||||
Resource {
|
||||
binding: 2,
|
||||
name: "reflection_sampler",
|
||||
ty: "var reflection_sampler: sampler",
|
||||
doc: "Bilinear filtering, clamped to the edges.",
|
||||
},
|
||||
shadow_map(3),
|
||||
shadow_sampler(4),
|
||||
shadow_map_translucent(7),
|
||||
shadow_color(8),
|
||||
Resource {
|
||||
binding: 6,
|
||||
name: "opaque_depth",
|
||||
ty: "var opaque_depth: texture_depth_2d",
|
||||
doc: "The world's depth before translucent terrain (reversed, like `scene_depth`), for the translucent pass: `textureLoad(opaque_depth, vec2<i32>(input.screen * frame.resolution), 0)` is what lies behind water or glass. Stale while solid terrain draws.",
|
||||
},
|
||||
Resource {
|
||||
binding: 9,
|
||||
name: "block_light_colors",
|
||||
ty: "var block_light_colors: texture_3d<f32>",
|
||||
doc: "The colour of the block light around the camera, from the `[[lights]]` of the light sources nearby: rgb their colours averaged by how near each is, over 4; a how strongly they reach the cell. Read it through `block_light_color`.",
|
||||
},
|
||||
Resource {
|
||||
binding: 10,
|
||||
name: "block_light_grid",
|
||||
ty: "var<uniform> block_light_grid: vec4<f32>",
|
||||
doc: "Where `block_light_colors` lies: xyz its corner, camera-relative, w the side of one of its cells in blocks.",
|
||||
},
|
||||
Resource {
|
||||
binding: 11,
|
||||
name: "opaque_color",
|
||||
ty: "var opaque_color: texture_2d<f32>",
|
||||
doc: "The world's picture before translucent terrain (sky, solid terrain, entities), the size of `opaque_depth`, for the translucent pass: what a surface refracts. Read it with `textureLoad`. Stale while solid terrain draws; black when the world draws straight to the window (a pack without `[[post]]` passes, at full resolution).",
|
||||
},
|
||||
];
|
||||
|
||||
/// Functions the post prelude gives packs.
|
||||
const POST_HELPERS: &str = "\
|
||||
// The world's depth at `uv` (reversed: 0 is the sky).
|
||||
fn depth_at(uv: vec2<f32>) -> f32 {
|
||||
let size = vec2<f32>(textureDimensions(scene_depth));
|
||||
let texel = vec2<i32>(clamp(uv * size, vec2<f32>(0.0), size - 1.0));
|
||||
return textureLoad(scene_depth, texel, 0);
|
||||
}
|
||||
// The camera-relative world position, in blocks, of the point at `uv` and `depth`.
|
||||
fn view_position(uv: vec2<f32>, depth: f32) -> vec3<f32> {
|
||||
let clip = vec4<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, depth, 1.0);
|
||||
let view = frame.projection_inverse * clip;
|
||||
return (frame.view_inverse * vec4<f32>(view.xyz / view.w, 0.0)).xyz;
|
||||
}
|
||||
";
|
||||
|
||||
/// What the material entries (the checker's below and the renderer's) share: the
|
||||
/// surface's normal and tangent from the derivatives of its position and uv. The arguments are
|
||||
/// the line's return statements of `block_light_color`, `block_texture` and
|
||||
/// `block_texture_level`, the helpers that read colour.
|
||||
macro_rules! material_helpers {
|
||||
($light:literal, $texture:literal, $texture_level:literal) => {
|
||||
concat!(
|
||||
"\
|
||||
// The colour of the block light at the camera-relative `position` (rgb, from the `[[lights]]`
|
||||
// of the sources nearby) and how strongly they reach it (a, 0 where none does). Block light
|
||||
// keeps its own brightness (`input.light.x`): this is its colour only.
|
||||
fn block_light_color(position: vec3<f32>) -> vec4<f32> {
|
||||
let size = vec3<f32>(textureDimensions(block_light_colors));
|
||||
let uvw = (position - block_light_grid.xyz) / (block_light_grid.w * size);
|
||||
if any(uvw < vec3<f32>(0.0)) || any(uvw > vec3<f32>(1.0)) {
|
||||
return vec4<f32>(0.0);
|
||||
}
|
||||
let c = textureSampleLevel(block_light_colors, reflection_sampler, uvw, 0.0);
|
||||
",
|
||||
$light,
|
||||
"}
|
||||
// What the post stage's `surface_at` reads for this point of solid terrain: its `normal` (a
|
||||
// unit vector in world axes), `smoothness` (0..1, say for reflections) and `data`, a value of
|
||||
// the pack's own passed through. Unset, the point keeps the face's own normal and 0 for both;
|
||||
// translucent terrain stores none.
|
||||
var<private> horizon_surface: vec4<f32>;
|
||||
fn set_surface(normal: vec3<f32>, smoothness: f32, data: f32) {
|
||||
// Octahedral, y up: the upper half folds onto xz, the lower half unfolds around it.
|
||||
let n = normal / (abs(normal.x) + abs(normal.y) + abs(normal.z));
|
||||
var e = n.xz;
|
||||
if n.y < 0.0 {
|
||||
e = (1.0 - abs(n.zx)) * select(vec2<f32>(-1.0), vec2<f32>(1.0), n.xz >= vec2<f32>(0.0));
|
||||
}
|
||||
horizon_surface = vec4<f32>(bitcast<f32>(pack2x16snorm(e)), smoothness, data, 1.0);
|
||||
}
|
||||
// The point being shaded: its sprite's layer and mip level (the entries set them).
|
||||
var<private> horizon_block_layer: u32;
|
||||
var<private> horizon_block_lod: f32;
|
||||
// The surface's block texture at `uv` (the sprite's space, as `input.uv`), without the tint:
|
||||
// what `input.albedo` holds at the point itself, readable anywhere on the sprite, for normals
|
||||
// or detail generated from the texture.
|
||||
fn block_texture(uv: vec2<f32>) -> vec4<f32> {
|
||||
",
|
||||
$texture,
|
||||
"}
|
||||
// `block_texture` at mip level `lod` (0 the full-size texture).
|
||||
fn block_texture_level(uv: vec2<f32>, lod: f32) -> vec4<f32> {
|
||||
",
|
||||
$texture_level,
|
||||
"}
|
||||
// The block texture's size, in texels (16 x 16 for vanilla's).
|
||||
fn block_texture_size() -> vec2<f32> {
|
||||
return horizon_block_size();
|
||||
}
|
||||
// The mip level `block_texture` reads at this point (0 up close, more far away).
|
||||
fn block_texture_lod() -> f32 {
|
||||
return horizon_block_lod;
|
||||
}
|
||||
// LabPBR: whether the resource packs ship normal (`_n`) or specular (`_s`) maps beside their
|
||||
// block textures. Without them `block_normal_map` and `block_specular_map` give the defaults
|
||||
// below.
|
||||
fn block_pbr_maps() -> bool {
|
||||
return horizon_block_pbr_present();
|
||||
}
|
||||
// The surface's LabPBR normal map at `uv` (the sprite's space, as `input.uv`): rg the normal's
|
||||
// x and y in the texture's space (x along `input.tangent`, y down the texture; 0..1 for
|
||||
// -1..1), b ambient occlusion (1 none), a height (1 the top). (0.5, 0.5, 1, 1) where a texture
|
||||
// has none.
|
||||
fn block_normal_map(uv: vec2<f32>) -> vec4<f32> {
|
||||
return horizon_block_pbr(uv, horizon_block_layer, horizon_block_lod, 0u);
|
||||
}
|
||||
// `block_normal_map` at the block texture's mip level `lod` (the same footprint on the surface,
|
||||
// where the map is smaller than the texture).
|
||||
fn block_normal_map_level(uv: vec2<f32>, lod: f32) -> vec4<f32> {
|
||||
return horizon_block_pbr(uv, horizon_block_layer, lod, 0u);
|
||||
}
|
||||
// The surface's LabPBR specular map at `uv`: r perceptual smoothness, g F0 up to 229/255 and
|
||||
// a metal's id above, b porosity or subsurface scattering, a emission (up to 254/255; 1
|
||||
// none). (0, 0, 0, 0) where a texture has none.
|
||||
fn block_specular_map(uv: vec2<f32>) -> vec4<f32> {
|
||||
return horizon_block_pbr(uv, horizon_block_layer, horizon_block_lod, 1u);
|
||||
}
|
||||
// `block_specular_map` at the block texture's mip level `lod`.
|
||||
fn block_specular_map_level(uv: vec2<f32>, lod: f32) -> vec4<f32> {
|
||||
return horizon_block_pbr(uv, horizon_block_layer, lod, 1u);
|
||||
}
|
||||
// The LabPBR maps' size, in texels (they may be smaller than the block's texture).
|
||||
fn block_pbr_size() -> vec2<f32> {
|
||||
return horizon_block_pbr_size();
|
||||
}
|
||||
// Block faces are flat: the derivatives of the position give the face's normal, turned
|
||||
// toward the camera. Far away they lose precision, so a normal within about 3.6 degrees of
|
||||
// an axis (every full block face) is that axis exactly: no shimmer on distant faces. The
|
||||
// gentlest slope of flowing water, one level over a block, tilts about 6 degrees.
|
||||
fn horizon_material_normal(position: vec3<f32>) -> vec3<f32> {
|
||||
let n = normalize(cross(dpdx(position), dpdy(position)));
|
||||
let a = abs(n);
|
||||
let m = max(a.x, max(a.y, a.z));
|
||||
let axis = select(select(vec3(0.0, 0.0, sign(n.z)), vec3(0.0, sign(n.y), 0.0), a.y == m),
|
||||
vec3(sign(n.x), 0.0, 0.0), a.x == m);
|
||||
let flat_n = select(n, axis, m > 0.998);
|
||||
return select(flat_n, -flat_n, dot(flat_n, position) > 0.0);
|
||||
}
|
||||
// The direction `uv.x` grows along on the surface, and the sign of the one `uv.y` grows
|
||||
// along against `cross(normal, tangent)`.
|
||||
fn horizon_material_tangent(position: vec3<f32>, uv: vec2<f32>, normal: vec3<f32>) -> vec4<f32> {
|
||||
let dp1 = dpdx(position);
|
||||
let dp2 = dpdy(position);
|
||||
let duv1 = dpdx(uv);
|
||||
let duv2 = dpdy(uv);
|
||||
let dp2perp = cross(dp2, normal);
|
||||
let dp1perp = cross(normal, dp1);
|
||||
let t = dp2perp * duv1.x + dp1perp * duv2.x;
|
||||
let b = dp2perp * duv1.y + dp1perp * duv2.y;
|
||||
if length(t) < 1e-12 {
|
||||
return vec4<f32>(1.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
let tangent = normalize(t - normal * dot(normal, t));
|
||||
return vec4<f32>(tangent, select(-1.0, 1.0, dot(cross(normal, tangent), b) >= 0.0));
|
||||
}
|
||||
"
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
const MATERIAL_HELPERS: &str = material_helpers!(
|
||||
" return vec4<f32>(c.rgb * 4.0, c.a);\n",
|
||||
" return horizon_block_sample(uv, horizon_block_layer, horizon_block_lod);\n",
|
||||
" return horizon_block_sample(uv, horizon_block_layer, lod);\n"
|
||||
);
|
||||
|
||||
/// 0.2's: the `[[lights]]` colours and the block texture decoded (the LabPBR maps are data).
|
||||
const MATERIAL_HELPERS_0_2: &str = material_helpers!(
|
||||
" return vec4<f32>(horizon_decode(c.rgb * 4.0), c.a);\n",
|
||||
" return horizon_decode_rgba(horizon_block_sample(uv, horizon_block_layer, horizon_block_lod));\n",
|
||||
" return horizon_decode_rgba(horizon_block_sample(uv, horizon_block_layer, lod));\n"
|
||||
);
|
||||
|
||||
/// The post prelude's `surface_at`, for the post helpers' end.
|
||||
const POST_SURFACE: &str = "\
|
||||
// The solid terrain's surface at a pixel, as the material stage's `set_surface` left it.
|
||||
struct SurfaceData {
|
||||
// False where the sky, an entity, water, glass or a particle shows instead.
|
||||
present: bool,
|
||||
// A unit vector in world axes.
|
||||
normal: vec3<f32>,
|
||||
smoothness: f32,
|
||||
// The pack's own value, passed through.
|
||||
data: f32,
|
||||
}
|
||||
fn surface_at(texel: vec2<i32>) -> SurfaceData {
|
||||
let size = vec2<i32>(textureDimensions(scene_surface));
|
||||
let t = clamp(texel, vec2<i32>(0), size - 1);
|
||||
let s = bitcast<vec4<f32>>(textureLoad(scene_surface, t, 0));
|
||||
let depth_size = vec2<i32>(textureDimensions(scene_depth));
|
||||
let depth = textureLoad(scene_depth, clamp(texel, vec2<i32>(0), depth_size - 1), 0);
|
||||
// The terrain's own depth is stored with it: anything drawn nearer since hides it.
|
||||
let present = s.w > 0.0 && abs(s.w - depth) <= s.w * 1e-5;
|
||||
let e = unpack2x16snorm(bitcast<u32>(s.x));
|
||||
var n = vec3<f32>(e.x, 1.0 - abs(e.x) - abs(e.y), e.y);
|
||||
if n.y < 0.0 {
|
||||
n = vec3<f32>((1.0 - abs(n.z)) * sign(n.x), n.y, (1.0 - abs(n.x)) * sign(n.z));
|
||||
}
|
||||
return SurfaceData(present, select(vec3<f32>(0.0), normalize(n), present),
|
||||
select(0.0, s.y, present), select(0.0, s.z, present));
|
||||
}
|
||||
";
|
||||
|
||||
/// The post stage's entry points, as the renderer builds them: a full-screen
|
||||
/// triangle, and a fragment that calls the pack's `post` for each pixel.
|
||||
const POST_ENTRIES: &str = "\
|
||||
struct HorizonPostOut {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) uv: vec2<f32>,
|
||||
}
|
||||
@vertex fn horizon_vs_post(@builtin(vertex_index) i: u32) -> HorizonPostOut {
|
||||
let p = vec2<f32>(f32((i << 1u) & 2u), f32(i & 2u));
|
||||
var out: HorizonPostOut;
|
||||
out.position = vec4<f32>(p * 2.0 - 1.0, 0.0, 1.0);
|
||||
out.uv = vec2<f32>(p.x, 1.0 - p.y);
|
||||
return out;
|
||||
}
|
||||
@fragment fn horizon_fs_post(in: HorizonPostOut) -> @location(0) vec4<f32> {
|
||||
let color = textureSampleLevel(previous, linear_sampler, in.uv, 0.0);
|
||||
return post(PostInput(in.uv, in.position.xy, color, depth_at(in.uv)));
|
||||
}
|
||||
";
|
||||
|
||||
/// What the renderer gives the material prelude's LabPBR functions (`native_material.rs`); the
|
||||
/// check and the entries below put a stand-in in its place.
|
||||
macro_rules! pbr_stub {
|
||||
() => {
|
||||
"fn horizon_block_sample(uv: vec2<f32>, layer: u32, lod: f32) -> vec4<f32> {
|
||||
return vec4<f32>(1.0);
|
||||
}
|
||||
fn horizon_block_size() -> vec2<f32> {
|
||||
return vec2<f32>(16.0);
|
||||
}
|
||||
fn horizon_block_pbr(uv: vec2<f32>, layer: u32, lod: f32, map: u32) -> vec4<f32> {
|
||||
return select(vec4<f32>(0.5, 0.5, 1.0, 1.0), vec4<f32>(0.0), map == 1u);
|
||||
}
|
||||
fn horizon_block_pbr_present() -> bool {
|
||||
return false;
|
||||
}
|
||||
fn horizon_block_pbr_size() -> vec2<f32> {
|
||||
return vec2<f32>(16.0);
|
||||
}
|
||||
"
|
||||
};
|
||||
}
|
||||
pub const PBR_STUB: &str = pbr_stub!();
|
||||
|
||||
/// Entry points with the material stage's inputs: the renderer calls
|
||||
/// `material` from its own terrain shader in the same way. The arguments are the line's
|
||||
/// `MaterialInput` and return statements.
|
||||
macro_rules! material_entries {
|
||||
($input:literal, $result:literal) => {
|
||||
concat!(
|
||||
pbr_stub!(),
|
||||
"\
|
||||
struct HorizonMaterialIn {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) albedo: vec4<f32>,
|
||||
@location(1) tint: vec4<f32>,
|
||||
@location(2) light: vec2<f32>,
|
||||
@location(3) lightmap: vec3<f32>,
|
||||
@location(4) world: vec3<f32>,
|
||||
@location(5) uv: vec2<f32>,
|
||||
@location(6) @interpolate(flat) translucent: u32,
|
||||
@location(7) @interpolate(flat) tags: u32,
|
||||
@location(8) @interpolate(flat) face: u32,
|
||||
@location(9) @interpolate(flat) fluid_quad: u32,
|
||||
@location(10) @interpolate(flat) emission: f32,
|
||||
@location(11) @interpolate(flat) material: u32,
|
||||
}
|
||||
@fragment fn horizon_fs_material(in: HorizonMaterialIn) -> @location(0) vec4<f32> {
|
||||
let fluid = select(0u, select(2u, 1u, in.translucent != 0u), in.fluid_quad != 0u);
|
||||
let normal = horizon_material_normal(in.world);
|
||||
let tangent = horizon_material_tangent(in.world, in.uv, normal);
|
||||
",
|
||||
$input,
|
||||
$result,
|
||||
"}
|
||||
"
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
const MATERIAL_ENTRIES: &str = material_entries!(
|
||||
" let input = MaterialInput(in.albedo, in.tint, in.light, in.lightmap, in.world, in.uv, in.translucent != 0u, in.tags, in.material,
|
||||
in.position.xy / frame.resolution, in.face, fluid, in.emission, normal, tangent, vec3<f32>(0.0), 0.0);
|
||||
",
|
||||
" return material(input).color;\n"
|
||||
);
|
||||
|
||||
/// 0.2's: the colours decoded on the way in, and the result encoded for the frame
|
||||
/// (`[frame] blend = "gamma"`; under `"linear"` the renderer stores it as it is).
|
||||
const MATERIAL_ENTRIES_0_2: &str = material_entries!(
|
||||
" let input = MaterialInput(horizon_decode_rgba(in.albedo), horizon_decode_rgba(in.tint), in.light,
|
||||
horizon_decode(in.lightmap), in.world, in.uv, in.translucent != 0u, in.tags, in.material,
|
||||
in.position.xy / frame.resolution, in.face, fluid, in.emission, normal, tangent, vec3<f32>(0.0), 0.0);
|
||||
",
|
||||
" return horizon_encode_rgba(material(input).color);\n"
|
||||
);
|
||||
|
||||
/// Entry points with the sky stage's inputs: the renderer calls `sky` from
|
||||
/// its own sky shader in the same way.
|
||||
const SKY_ENTRIES: &str = "\
|
||||
struct HorizonSkyIn {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) direction: vec3<f32>,
|
||||
@location(1) color: vec4<f32>,
|
||||
}
|
||||
@fragment fn horizon_fs_sky(in: HorizonSkyIn) -> @location(0) vec4<f32> {
|
||||
return sky(SkyInput(normalize(in.direction), in.color));
|
||||
}
|
||||
";
|
||||
|
||||
const SKY_ENTRIES_0_2: &str = "\
|
||||
struct HorizonSkyIn {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) direction: vec3<f32>,
|
||||
@location(1) color: vec4<f32>,
|
||||
}
|
||||
@fragment fn horizon_fs_sky(in: HorizonSkyIn) -> @location(0) vec4<f32> {
|
||||
return horizon_encode_rgba(sky(SkyInput(normalize(in.direction), horizon_decode_rgba(in.color))));
|
||||
}
|
||||
";
|
||||
|
||||
/// An entry point with the weather stage's inputs: the renderer calls `weather` from its own
|
||||
/// weather shader in the same way.
|
||||
const VERTEX_ENTRIES: &str = "\
|
||||
@vertex fn horizon_vs_vertex(
|
||||
@location(0) position: vec3<f32>, @location(1) uv: vec2<f32>, @location(2) light: vec2<f32>,
|
||||
@location(3) tint: vec4<f32>, @location(4) material: u32, @location(5) flags: u32,
|
||||
) -> @builtin(position) vec4<f32> {
|
||||
let input = VertexInput(position, uv, material, light, tint, flags & 7u, flags >> 31u);
|
||||
return frame.projection * frame.view * vec4<f32>(vertex(input), 1.0);
|
||||
}
|
||||
";
|
||||
|
||||
/// 0.2's: the tint is a colour, so it is decoded too.
|
||||
const VERTEX_ENTRIES_0_2: &str = "\
|
||||
@vertex fn horizon_vs_vertex(
|
||||
@location(0) position: vec3<f32>, @location(1) uv: vec2<f32>, @location(2) light: vec2<f32>,
|
||||
@location(3) tint: vec4<f32>, @location(4) material: u32, @location(5) flags: u32,
|
||||
) -> @builtin(position) vec4<f32> {
|
||||
let input = VertexInput(position, uv, material, light, horizon_decode_rgba(tint), flags & 7u, flags >> 31u);
|
||||
return frame.projection * frame.view * vec4<f32>(vertex(input), 1.0);
|
||||
}
|
||||
";
|
||||
|
||||
/// `fn displacement(input: DisplacementInput) -> f32`, the material stage's function
|
||||
/// under `[displacement]`.
|
||||
pub const DISPLACEMENT_SIGNATURE: &str = "fn displacement(input: DisplacementInput) -> f32";
|
||||
|
||||
/// An entry point calling the material stage's `displacement`, after its entries under
|
||||
/// `[displacement]`: the renderer calls it from its own terrain vertex shader in the same way.
|
||||
const DISPLACEMENT_ENTRIES: &str = "@vertex fn horizon_vs_displacement(
|
||||
@location(0) position: vec3<f32>, @location(1) normal: vec3<f32>, @location(2) uv: vec2<f32>,
|
||||
@location(3) material: u32, @location(4) height: f32,
|
||||
) -> @builtin(position) vec4<f32> {
|
||||
let depth = displacement(DisplacementInput(position, normal, uv, material, height));
|
||||
return frame.projection * frame.view * vec4<f32>(position - normal * depth, 1.0);
|
||||
}
|
||||
";
|
||||
|
||||
const WEATHER_ENTRIES: &str = "\
|
||||
struct HorizonWeatherIn {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) color: vec4<f32>,
|
||||
@location(1) light: vec2<f32>,
|
||||
@location(2) world: vec3<f32>,
|
||||
@location(3) @interpolate(flat) snow: u32,
|
||||
}
|
||||
@fragment fn horizon_fs_weather(in: HorizonWeatherIn) -> @location(0) vec4<f32> {
|
||||
return weather(WeatherInput(in.color, in.light, in.world, in.snow != 0u));
|
||||
}
|
||||
";
|
||||
|
||||
const WEATHER_ENTRIES_0_2: &str = "\
|
||||
struct HorizonWeatherIn {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) color: vec4<f32>,
|
||||
@location(1) light: vec2<f32>,
|
||||
@location(2) world: vec3<f32>,
|
||||
@location(3) @interpolate(flat) snow: u32,
|
||||
}
|
||||
@fragment fn horizon_fs_weather(in: HorizonWeatherIn) -> @location(0) vec4<f32> {
|
||||
let input = WeatherInput(horizon_decode_rgba(in.color), in.light, in.world, in.snow != 0u);
|
||||
return horizon_encode_rgba(weather(input));
|
||||
}
|
||||
";
|
||||
|
||||
/// The post stage's entry points under 0.2: the chain's targets hold linear light, so nothing
|
||||
/// converts, and the pass's alpha is ignored.
|
||||
const POST_ENTRIES_0_2: &str = "\
|
||||
struct HorizonPostOut {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) uv: vec2<f32>,
|
||||
}
|
||||
@vertex fn horizon_vs_post(@builtin(vertex_index) i: u32) -> HorizonPostOut {
|
||||
let p = vec2<f32>(f32((i << 1u) & 2u), f32(i & 2u));
|
||||
var out: HorizonPostOut;
|
||||
out.position = vec4<f32>(p * 2.0 - 1.0, 0.0, 1.0);
|
||||
out.uv = vec2<f32>(p.x, 1.0 - p.y);
|
||||
return out;
|
||||
}
|
||||
@fragment fn horizon_fs_post(in: HorizonPostOut) -> @location(0) vec4<f32> {
|
||||
let color = textureSampleLevel(previous, linear_sampler, in.uv, 0.0);
|
||||
return vec4<f32>(post(PostInput(in.uv, in.position.xy, color, depth_at(in.uv))).rgb, 1.0);
|
||||
}
|
||||
";
|
||||
|
||||
/// The tonemap stage's entry points: a full-screen triangle at the window's size, and a fragment
|
||||
/// that holds the result to what the display pass takes.
|
||||
const TONEMAP_ENTRIES: &str = "\
|
||||
struct HorizonTonemapOut {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) uv: vec2<f32>,
|
||||
}
|
||||
@vertex fn horizon_vs_tonemap(@builtin(vertex_index) i: u32) -> HorizonTonemapOut {
|
||||
let p = vec2<f32>(f32((i << 1u) & 2u), f32(i & 2u));
|
||||
var out: HorizonTonemapOut;
|
||||
out.position = vec4<f32>(p * 2.0 - 1.0, 0.0, 1.0);
|
||||
out.uv = vec2<f32>(p.x, 1.0 - p.y);
|
||||
return out;
|
||||
}
|
||||
@fragment fn horizon_fs_tonemap(in: HorizonTonemapOut) -> @location(0) vec4<f32> {
|
||||
let color = horizon_sanitize(textureSampleLevel(previous, linear_sampler, in.uv, 0.0).rgb);
|
||||
let out = tonemap(TonemapInput(in.uv, in.position.xy, color));
|
||||
return vec4<f32>(min(horizon_sanitize(out), vec3<f32>(frame.headroom)), 1.0);
|
||||
}
|
||||
";
|
||||
|
||||
impl Api {
|
||||
/// The stages the line has, in `StageKind::ALL`'s order.
|
||||
pub fn stages(self) -> &'static [StageKind] {
|
||||
match self {
|
||||
Api::V0_1 => &StageKind::ALL[..5],
|
||||
Api::V0_2 => &StageKind::ALL,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StageKind {
|
||||
pub const ALL: [StageKind; 6] = [
|
||||
StageKind::Sky,
|
||||
StageKind::Vertex,
|
||||
StageKind::Material,
|
||||
StageKind::Weather,
|
||||
StageKind::Post,
|
||||
StageKind::Tonemap,
|
||||
];
|
||||
|
||||
/// The manifest's name for the stage (`[sky]`, `[vertex]`, `[material]`, `[weather]`,
|
||||
/// `[[post]]`, `[tonemap]`).
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
StageKind::Sky => "sky",
|
||||
StageKind::Material => "material",
|
||||
StageKind::Vertex => "vertex",
|
||||
StageKind::Weather => "weather",
|
||||
StageKind::Post => "post",
|
||||
StageKind::Tonemap => "tonemap",
|
||||
}
|
||||
}
|
||||
|
||||
/// The function a pack defines: it has the stage's name.
|
||||
pub fn function(self) -> &'static str {
|
||||
self.name()
|
||||
}
|
||||
|
||||
pub fn input(self) -> &'static Struct {
|
||||
match self {
|
||||
StageKind::Sky => &SKY_INPUT,
|
||||
StageKind::Material => &MATERIAL_INPUT,
|
||||
StageKind::Vertex => &VERTEX_INPUT,
|
||||
StageKind::Weather => &WEATHER_INPUT,
|
||||
StageKind::Post => &POST_INPUT,
|
||||
StageKind::Tonemap => &TONEMAP_INPUT,
|
||||
}
|
||||
}
|
||||
|
||||
/// The type the function returns.
|
||||
pub fn output(self) -> &'static str {
|
||||
match self {
|
||||
StageKind::Material => SURFACE.name,
|
||||
StageKind::Vertex | StageKind::Tonemap => "vec3<f32>",
|
||||
StageKind::Sky | StageKind::Weather | StageKind::Post => "vec4<f32>",
|
||||
}
|
||||
}
|
||||
|
||||
/// `fn <name>(input: <Input>) -> <Output>`.
|
||||
pub fn signature(self) -> String {
|
||||
format!(
|
||||
"fn {}(input: {}) -> {}",
|
||||
self.function(),
|
||||
self.input().name,
|
||||
self.output()
|
||||
)
|
||||
}
|
||||
|
||||
pub fn resources(self) -> &'static [Resource] {
|
||||
match self {
|
||||
StageKind::Post => &POST_RESOURCES,
|
||||
StageKind::Sky | StageKind::Vertex | StageKind::Weather => std::slice::from_ref(&FRAME),
|
||||
StageKind::Material => &MATERIAL_RESOURCES,
|
||||
StageKind::Tonemap => &TONEMAP_RESOURCES,
|
||||
}
|
||||
}
|
||||
|
||||
/// Everything the pack's files may use under the line `api`, declared before them.
|
||||
pub fn prelude(self, api: Api) -> String {
|
||||
let mut out = match api {
|
||||
Api::V0_1 => {
|
||||
String::from("// Horizon shader API prelude (docs/api/shaders-reference.md)\n")
|
||||
}
|
||||
Api::V0_2 => String::from(
|
||||
"// Horizon shader API 0.2 prelude (docs/api/shaders-reference-0.2.md)\n",
|
||||
),
|
||||
};
|
||||
out += &Frame::line(api).wgsl();
|
||||
out += &self.input().wgsl();
|
||||
if self == StageKind::Material {
|
||||
out += &SURFACE.wgsl();
|
||||
// The vertex stage's file joins the material's module (`compose`).
|
||||
out += &VERTEX_INPUT.wgsl();
|
||||
out += &DISPLACEMENT_INPUT.wgsl();
|
||||
}
|
||||
for r in self.resources() {
|
||||
out += &format!("@group({GROUP}) @binding({}) {};\n", r.binding, r.ty);
|
||||
}
|
||||
if api == Api::V0_2 {
|
||||
out += COLOR_HELPERS;
|
||||
}
|
||||
match self {
|
||||
StageKind::Post => {
|
||||
out += POST_HELPERS;
|
||||
out += POST_SURFACE;
|
||||
out += SHADOW_HELPERS;
|
||||
}
|
||||
StageKind::Material => {
|
||||
out += match api {
|
||||
Api::V0_1 => MATERIAL_HELPERS,
|
||||
Api::V0_2 => MATERIAL_HELPERS_0_2,
|
||||
};
|
||||
out += SHADOW_HELPERS;
|
||||
}
|
||||
StageKind::Vertex => out += SHADOW_HELPERS,
|
||||
StageKind::Sky | StageKind::Weather | StageKind::Tonemap => {}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The entry points put after the pack's files under the line `api`.
|
||||
pub fn entries(self, api: Api) -> &'static str {
|
||||
match (api, self) {
|
||||
(Api::V0_1, StageKind::Sky) => SKY_ENTRIES,
|
||||
(Api::V0_1, StageKind::Material) => MATERIAL_ENTRIES,
|
||||
(Api::V0_1, StageKind::Vertex) => VERTEX_ENTRIES,
|
||||
(Api::V0_1, StageKind::Weather) => WEATHER_ENTRIES,
|
||||
(Api::V0_1, StageKind::Post) => POST_ENTRIES,
|
||||
(Api::V0_2, StageKind::Sky) => SKY_ENTRIES_0_2,
|
||||
(Api::V0_2, StageKind::Material) => MATERIAL_ENTRIES_0_2,
|
||||
(Api::V0_2, StageKind::Vertex) => VERTEX_ENTRIES_0_2,
|
||||
(Api::V0_2, StageKind::Weather) => WEATHER_ENTRIES_0_2,
|
||||
(Api::V0_2, StageKind::Post) => POST_ENTRIES_0_2,
|
||||
// 0.1 has no tonemap stage: a manifest with one is refused before it is built.
|
||||
(_, StageKind::Tonemap) => TONEMAP_ENTRIES,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The first binding of a pack's `[[textures]]` in group 3; their samplers follow at
|
||||
/// `TEXTURE_SAMPLER_BINDING`.
|
||||
pub const TEXTURE_BINDING: u32 = 16;
|
||||
pub const TEXTURE_SAMPLER_BINDING: u32 = 24;
|
||||
|
||||
/// The declarations of a pack's `[[textures]]`, part of every stage's prelude.
|
||||
pub fn texture_declarations(textures: &[crate::manifest::PackTexture]) -> String {
|
||||
let mut out = String::new();
|
||||
@@ -832,9 +45,6 @@ impl Composed {
|
||||
}
|
||||
}
|
||||
|
||||
/// The binding of a pack's first post target (`[[post]] output`) in group 3, the others after.
|
||||
pub const POST_TARGET_BINDING: u32 = 32;
|
||||
|
||||
/// The declarations of a pack's post targets, part of the post stage's prelude.
|
||||
pub fn post_target_declarations(manifest: &crate::manifest::Manifest) -> String {
|
||||
manifest
|
||||
@@ -913,10 +123,3 @@ pub fn compose(pack: &Pack, kind: StageKind, file: &str) -> Composed {
|
||||
}
|
||||
composed
|
||||
}
|
||||
|
||||
/// Whether a material stage's `source` (with its prelude) calls `set_surface` itself, and so
|
||||
/// needs the surface target, which a pack without it doesn't pay for.
|
||||
pub fn calls_set_surface(source: &str) -> bool {
|
||||
let call = "set_surface(";
|
||||
source.matches(call).count() > MATERIAL_HELPERS.matches(call).count()
|
||||
}
|
||||
Loaded 100 of 137 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user