Compare commits

...
Author SHA1 Message Date
Ludovic Marie 0b7d3f8105 Merge branch 'split/render-pipeline-config' into split/render-pipeline-stages 2026-11-09 16:35:02 +01:00
Horizon agentandClaude Opus 5.5 a6093def90 launcher and registry lockfiles: horizon-shader-api depends on horizon-render-pipeline
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 16:21:43 +01:00
Horizon agentandClaude Opus 5.5 80aa1a5b06 Cargo.lock: horizon-shader-api takes bytemuck through the pipeline crate
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 16:20:23 +01:00
Ludovic Marie 8da02c530e Merge branch 'split/render-pipeline-config' into split/render-pipeline-stages 2026-11-09 16:20:23 +01:00
Horizon agentandClaude Opus 5.5 a23f327048 WGSL stage interface moves to horizon-render-pipeline
The stages Horizon's own packs fill, their preludes, bindings and entry points (stages.rs),
the frame uniform the renderer uploads (inputs.rs) and the API lines (Api) are the renderer's
interface, so they move unchanged into the engine's pipeline crate. horizon-shader-api keeps
reading, composing and checking packs against them and re-exports them under the same paths
(launcher and registry unchanged). The renderer takes them from the engine crate. Layer plan M6.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 15:58:12 +01:00
19 changed files with 1297 additions and 1253 deletions

No files matched your search

Generated
+4 -1
View File
@@ -1856,6 +1856,9 @@ dependencies = [
[[package]]
name = "horizon-render-pipeline"
version = "0.1.0"
dependencies = [
"bytemuck",
]
[[package]]
name = "horizon-server"
@@ -1905,7 +1908,7 @@ dependencies = [
name = "horizon-shader-api"
version = "0.1.0"
dependencies = [
"bytemuck",
"horizon-render-pipeline",
"naga",
"png",
"serde",
+6 -6
View File
@@ -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)
@@ -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,
@@ -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;
@@ -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),
}
}));
@@ -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()
@@ -7,7 +7,8 @@
//! prelude's `shadow_position` undoes.
use horizon_render_pipeline::Shadow;
use horizon_shader_api::{Api, Frame as FrameInputs, manifest::Shadow as Settings, stages::GROUP};
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,
@@ -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 \
+8 -1
View File
@@ -493,10 +493,17 @@ dependencies = [
]
[[package]]
name = "horizon-shader-api"
name = "horizon-render-pipeline"
version = "0.1.0"
dependencies = [
"bytemuck",
]
[[package]]
name = "horizon-shader-api"
version = "0.1.0"
dependencies = [
"horizon-render-pipeline",
"naga",
"png",
"serde",
@@ -7,6 +7,8 @@ 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"] }
[lints]
workspace = true
+33
View File
@@ -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,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")
);
}
}
@@ -5,10 +5,19 @@
//! `shaders/` packs). No wgpu and no pack file here.
//!
//! - [`config`]: the colortex buffers and the shadow map: formats, clearing, mipmaps, settings.
//! - [`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 inputs;
pub mod stages;
pub use api::Api;
pub use config::{
BUFFERS, Buffer, Config, Format, HALF_LIVES, Kind, SHADOW_COLORS, Shadow, buffer_index,
shadow_color,
};
pub use inputs::Frame;
pub use stages::StageKind;
@@ -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()
}
+2 -1
View File
@@ -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"] }
+3 -380
View File
@@ -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::*;
+1 -31
View File
@@ -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)]
+5 -802
View File
@@ -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()
}
+8 -1
View File
@@ -2841,10 +2841,17 @@ dependencies = [
]
[[package]]
name = "horizon-shader-api"
name = "horizon-render-pipeline"
version = "0.1.0"
dependencies = [
"bytemuck",
]
[[package]]
name = "horizon-shader-api"
version = "0.1.0"
dependencies = [
"horizon-render-pipeline",
"naga",
"png 0.18.1",
"serde",