Compare commits

...
Author SHA1 Message Date
Horizon agentandClaude Opus 5.5 27740e2e8f geometry.rs: GbuffersInputs only in its tests; lockfiles: the pipeline crate takes naga
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 16:46:06 +01:00
Horizon agentandClaude Opus 5.5 a7d211a1fc Translated programs' types move to horizon-render-pipeline
A translated program as the renderer builds pipelines from it (Program, ComputeProgram,
Interface, Uniform, Sampler, Image, Target, Error, DRAW_UNIFORMS) and the vertex inputs the
renderer offers it (GbuffersInputs, GeometryInputs, AlphaTest, WATER_MASK, WATER_PATCHES) move
unchanged from horizon-shaders' translator, which re-exports them. The renderer's library code
takes them from the engine crate. Layer plan M6.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 16:42:43 +01:00
Ludovic Marie 7ae28b9d3e Merge branch 'split/render-pipeline-stages' into split/render-pipeline-textures 2026-11-09 16:35:58 +01:00
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 950c368a3c Custom textures and render stages move to horizon-render-pipeline
The textures a pipeline's programs read (CustomTexture, Texture, Texels, TextureSource, Dim,
EVERY_STAGE) and the render stages they are told they draw in (RENDER_STAGES, render_stage)
move unchanged from horizon-shaders, which still reads them from a pack and re-exports them.
The renderer takes them from the engine crate. Layer plan M6.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 16:24:07 +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
36 changed files with 1677 additions and 1589 deletions

No files matched your search

Generated
+5 -1
View File
@@ -1856,6 +1856,10 @@ dependencies = [
[[package]]
name = "horizon-render-pipeline"
version = "0.1.0"
dependencies = [
"bytemuck",
"naga",
]
[[package]]
name = "horizon-server"
@@ -1905,7 +1909,7 @@ dependencies = [
name = "horizon-shader-api"
version = "0.1.0"
dependencies = [
"bytemuck",
"horizon-render-pipeline",
"naga",
"png",
"serde",
+8 -8
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)
@@ -7714,7 +7714,7 @@ impl Renderer {
inputs.declarations.push_str(
"layout(set = 5, binding = 0) readonly buffer HzIndices { uint hz_indices[]; };\n",
);
horizon_shaders::GeometryInputs {
horizon_render_pipeline::GeometryInputs {
inputs,
pull: pack_atlas::PULL.to_owned(),
index: "hz_indices[@]".to_owned(),
@@ -7737,7 +7737,7 @@ impl Renderer {
}
}
fn pack_terrain_inputs(&mut self) -> Option<horizon_shaders::GbuffersInputs> {
fn pack_terrain_inputs(&mut self) -> Option<horizon_render_pipeline::GbuffersInputs> {
// Flat draws whenever the device has them (`render` keeps them on with a pack).
let flat = self.flat.is_some() && self.flat_on;
// Flat draws read each sprite's place from a table: the atlas is stitched as the game's.
@@ -10410,7 +10410,7 @@ impl Renderer {
self.own_timing.as_mut(),
);
pass.set_bind_group(0, &self.group, &[]);
pass.set_bind_group(horizon_shader_api::stages::GROUP, &shadow.group, &[]);
pass.set_bind_group(horizon_render_pipeline::stages::GROUP, &shadow.group, &[]);
if flat_frame
&& let Some(flat) = &self.flat
&& let Some(pipeline) = &shadow.flat
@@ -10443,7 +10443,7 @@ impl Renderer {
self.own_timing.as_mut(),
);
pass.set_bind_group(0, &self.group, &[]);
pass.set_bind_group(horizon_shader_api::stages::GROUP, &shadow.group, &[]);
pass.set_bind_group(horizon_render_pipeline::stages::GROUP, &shadow.group, &[]);
if flat_frame
&& let Some(flat) = &self.flat
&& let Some(pipeline) = &shadow.flat_translucent
@@ -10475,7 +10475,7 @@ impl Renderer {
.begin_entities(&mut encoder, "Native shader pack entity shadow");
pass.set_pipeline(&shadow.entity);
pass.set_bind_group(0, &self.entity_group, &[]);
pass.set_bind_group(horizon_shader_api::stages::GROUP, &shadow.group, &[]);
pass.set_bind_group(horizon_render_pipeline::stages::GROUP, &shadow.group, &[]);
pass.set_vertex_buffer(0, self.entity_buffer.slice(..));
pass.set_index_buffer(self.indices.slice(..), wgpu::IndexFormat::Uint32);
pass.draw_indexed(0..entity_quads * 6, 0, 0..1);
@@ -10696,7 +10696,7 @@ impl Renderer {
let gpu_occluding = gpu_frame && self.gpu_culling.as_ref().is_some_and(|g| g.occluding());
let pack_frame = |pass: &mut wgpu::RenderPass<'_>| {
if let Some((_, group)) = material {
pass.set_bind_group(horizon_shader_api::stages::GROUP, group, &[]);
pass.set_bind_group(horizon_render_pipeline::stages::GROUP, group, &[]);
}
};
// A pack's solid terrain that calls `set_surface` also writes the surface target, in
@@ -12084,7 +12084,7 @@ impl Renderer {
&& self
.native
.as_ref()
.is_none_or(|n| n.frame.api == horizon_shader_api::Api::V0_1);
.is_none_or(|n| n.frame.api == horizon_render_pipeline::Api::V0_1);
match (&self.hdr, &surface_frame) {
(Some(hdr), Some(f)) => hdr.present(
&self.queue,
@@ -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,
@@ -597,9 +597,9 @@ const FACE: &str = " uint hz_q = (uint(gl_VertexIndex) & ~3u) * 6u;\n \
/// quad lands whole in one part. The colour's alpha can't decide it, as with `separateAo` it
/// holds each vertex's own ambient occlusion.
pub fn terrain_parts(
terrain: &horizon_shaders::GbuffersInputs,
terrain: &horizon_render_pipeline::GbuffersInputs,
lava: &[u16],
) -> [horizon_shaders::GbuffersInputs; 2] {
) -> [horizon_render_pipeline::GbuffersInputs; 2] {
let opaque = crate::mesher::OPAQUE_LAYER;
let lava: String = lava
.iter()
@@ -619,7 +619,7 @@ pub fn terrain_parts(
impl PackAtlas {
/// The vertex inputs of a gbuffers program drawing terrain (`TerrainVertex`, terrain.wgsl's
/// `vs_terrain` or, for `flat` draws, `vs_terrain_flat`), in atlas coordinates.
pub fn terrain_inputs(&self, flat: bool) -> horizon_shaders::GbuffersInputs {
pub fn terrain_inputs(&self, flat: bool) -> horizon_render_pipeline::GbuffersInputs {
let mut declarations = String::from(
"layout(location = 0) in vec3 hz_v_pos;\nlayout(location = 1) in uvec2 hz_v_uv;\n\
layout(location = 2) in uint hz_v_layer;\nlayout(location = 3) in vec4 hz_v_color;\n\
@@ -753,7 +753,7 @@ impl PackAtlas {
("vaColor", "hz_Color"),
("vaNormal", "hz_Normal"),
];
horizon_shaders::GbuffersInputs {
horizon_render_pipeline::GbuffersInputs {
declarations,
body,
attributes: attributes
@@ -2,7 +2,7 @@
//! translator's `DRAW_UNIFORMS`): one slot per render stage of a uniform buffer, picked with a
//! dynamic offset at each draw.
use horizon_shaders::{RENDER_STAGES, render_stage};
use horizon_render_pipeline::{RENDER_STAGES, render_stage};
/// Where a run of `Frame::entities` begins, and what it is (slice 7): the run lasts until the
/// next span. Recorded only while a shader pack draws entities, so Horizon's own frames carry
@@ -6,7 +6,7 @@
use std::ops::Range;
use horizon_shaders::render_stage;
use horizon_render_pipeline::render_stage;
use super::Vertex;
use super::pack_draw::EntitySpan;
@@ -31,7 +31,7 @@ pub fn buffers() -> [Option<wgpu::VertexBufferLayout<'static>>; 1] {
/// position, the shared atlas's UV, colour, light (block, sky in 16ths) and the world-oriented
/// normal. The quad's corners, read from the same vertices at set 1, give the face normal where
/// the vertex has none (custom geometry) and `at_tangent` along the texture's u, as for terrain.
pub fn inputs() -> horizon_shaders::GbuffersInputs {
pub fn inputs() -> horizon_render_pipeline::GbuffersInputs {
let words = std::mem::size_of::<Vertex>() / 4;
let mut declarations = "layout(location = 0) in vec3 hz_v_pos;\nlayout(location = 1) in vec2 hz_v_uv;\n\
layout(location = 2) in uint hz_v_overlay;\nlayout(location = 3) in vec4 hz_v_color;\n\
@@ -112,7 +112,7 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
// bright.
("horizonTier", "float((hz_v_overlay >> 16u) & 255u)"),
];
horizon_shaders::GbuffersInputs {
horizon_render_pipeline::GbuffersInputs {
declarations,
body,
attributes: attributes
@@ -121,7 +121,7 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
.collect(),
after: String::new(),
// Cutout, as vanilla's entity types mostly are (OQ-23): the loader's test at 0.1.
alpha_test: Some(horizon_shaders::AlphaTest {
alpha_test: Some(horizon_render_pipeline::AlphaTest {
op: ">",
reference: 0.1,
}),
@@ -138,7 +138,7 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
/// inputs of [`inputs`], each vertex read from set 1 (`hz_entity_vertices`), as `Vertex` lays
/// it out. The draw's corners walk its quads two triangles each, as `quad_indices` does: corner
/// `c` of a draw from vertex `first` (a whole quad) is drawn as vertex `first / 4 * 6 + c`.
pub fn geometry_inputs() -> horizon_shaders::GeometryInputs {
pub fn geometry_inputs() -> horizon_render_pipeline::GeometryInputs {
let words = std::mem::size_of::<Vertex>() / 4;
let pull = format!(
" uint hz_pw = hz_vertex_index * {words}u;\n \
@@ -150,7 +150,7 @@ pub fn geometry_inputs() -> horizon_shaders::GeometryInputs {
hz_v_light = uvec4(hz_pl & 255u, (hz_pl >> 8u) & 255u, (hz_pl >> 16u) & 255u, hz_pl >> 24u);\n \
hz_v_normal = unpackSnorm4x8(hz_entity_vertices[hz_pw + 8u]);\n"
);
horizon_shaders::GeometryInputs {
horizon_render_pipeline::GeometryInputs {
inputs: inputs(),
pull,
// Quad `c / 6`, corner 0, 1, 2, 0, 2, 3 of it.
@@ -163,7 +163,7 @@ pub fn geometry_inputs() -> horizon_shaders::GeometryInputs {
/// What `gbuffers_clouds` reads from the clouds' mesh (entity vertices, `vs_cloud`): the
/// position moved by the clouds' offset, the colour of the face's shade from their palette,
/// full light, the face's normal. Set 1 holds the clouds' uniform (`clouds::Uniform`).
pub fn cloud_inputs() -> horizon_shaders::GbuffersInputs {
pub fn cloud_inputs() -> horizon_render_pipeline::GbuffersInputs {
let declarations = "layout(location = 0) in vec3 hz_v_pos;\nlayout(location = 1) in vec2 hz_v_uv;\n\
layout(location = 2) in uint hz_v_overlay;\nlayout(location = 3) in vec4 hz_v_color;\n\
layout(location = 4) in uvec4 hz_v_light;\nlayout(location = 5) in vec4 hz_v_normal;\n\
@@ -183,7 +183,7 @@ pub fn cloud_inputs() -> horizon_shaders::GbuffersInputs {
("vaColor", "hz_Color"),
("vaNormal", "hz_Normal"),
];
horizon_shaders::GbuffersInputs {
horizon_render_pipeline::GbuffersInputs {
declarations,
body,
attributes: attributes
@@ -17,7 +17,7 @@ const UNIFORM: u64 = 2 * 64;
const SLOT: u64 = 32;
/// What `gbuffers_hand` reads: the entity inputs, and the hand's projection from set 1.
pub fn inputs() -> horizon_shaders::GbuffersInputs {
pub fn inputs() -> horizon_render_pipeline::GbuffersInputs {
let mut inputs = super::pack_entities::inputs();
// No last frame's hand to measure from: the hand's `at_velocity` reads zero (and its set 1
// has no previous vertices).
@@ -270,7 +270,7 @@ fn translucent_quad(quad: &[Vertex]) -> bool {
/// outside block entities, and `currentRenderedItemId`.
fn draw_slot(id: i32, translucent: bool) -> [u8; SLOT as usize] {
let mut bytes = [0u8; SLOT as usize];
let stage = horizon_shaders::render_stage(if translucent {
let stage = horizon_render_pipeline::render_stage(if translucent {
"HAND_TRANSLUCENT"
} else {
"HAND_SOLID"
@@ -8,7 +8,7 @@
/// width in pixels. The strip's corners 0 and 1 are at the start, 2 and 3 at the end (six
/// vertices per edge: pack pipelines draw triangle lists); the
/// width is applied in screen space after the pack's transform.
pub fn inputs() -> horizon_shaders::GbuffersInputs {
pub fn inputs() -> horizon_render_pipeline::GbuffersInputs {
let declarations =
"layout(location = 0) in vec3 hz_l_start;\nlayout(location = 1) in vec3 hz_l_end;\n\
layout(location = 2) in vec4 hz_l_argb;\nlayout(location = 3) in float hz_l_width;\n\
@@ -41,7 +41,7 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
if ((hz_l_c & 1) != 0) hz_l_o = -hz_l_o;\n \
gl_Position.xy += hz_l_o * gl_Position.w;\n }\n"
.to_owned();
horizon_shaders::GbuffersInputs {
horizon_render_pipeline::GbuffersInputs {
declarations,
body,
attributes: attributes
@@ -125,7 +125,7 @@ enum Source {
ShadowDepth(usize, bool),
/// `shadowcolor0/1` read outside the shadow programs (in them they are `Color`).
ShadowColor(usize),
/// The boats' water patches' depth (`horizon_shaders::WATER_MASK`), for translucent terrain.
/// The boats' water patches' depth (`horizon_render_pipeline::WATER_MASK`), for translucent terrain.
WaterMask,
}
@@ -883,7 +883,7 @@ fn fits(ty: &str, sample_type: wgpu::TextureSampleType) -> bool {
/// `hz_depth_linear` for a program: 1 for each of shadowtex0, shadowtex1, `shadow` and
/// `watershadow` whose depth filters (`linear`, bit n: shadowtex n). `shadow` is shadowtex1 in a
/// program that also declares `watershadow` (6.3).
fn depth_linear(interface: &horizon_shaders::Interface, linear: u8) -> [f32; 4] {
fn depth_linear(interface: &horizon_render_pipeline::Interface, linear: u8) -> [f32; 4] {
let watershadow = interface.samplers.iter().any(|s| s.name == "watershadow");
[0, 1, usize::from(watershadow), 0].map(|n| f32::from(linear >> n & 1))
}
@@ -921,7 +921,7 @@ fn source(name: &str, shadow_space: bool, shadow_pass: bool, watershadow: bool)
"lightmap" => Source::Lightmap,
"normals" => Source::Normals,
"specular" => Source::Specular,
horizon_shaders::WATER_MASK => Source::WaterMask,
horizon_render_pipeline::WATER_MASK => Source::WaterMask,
n => horizon_render_pipeline::buffer_index(n).map_or(Source::Dummy, Source::Color),
}
}
@@ -1503,7 +1503,7 @@ fn owned_buffers(
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
fn bindings(
features: wgpu::Features,
interface: &horizon_shaders::Interface,
interface: &horizon_render_pipeline::Interface,
name: &str,
group: Group,
buffers: &[u32],
@@ -1574,7 +1574,7 @@ fn bindings(
// pack of ours (#9064) bound it over `gaux2` and `noisetex` but not over names of its
// own, where a raw texture was bound; this rule is a fit to those cases.
let unbound = t.png
&& t.stage != horizon_shaders::EVERY_STAGE
&& t.stage != horizon_render_pipeline::textures::EVERY_STAGE
&& source(&s.name, false, true, false) == Source::Dummy;
!unbound
&& (t.stage != pack_textures::stage(group)
@@ -2283,7 +2283,7 @@ fn build_compute(
device: &wgpu::Device,
name: String,
group: Group,
c: horizon_shaders::ComputeProgram,
c: horizon_render_pipeline::ComputeProgram,
formats: &[wgpu::TextureFormat; BUFFERS],
shadow: Option<&[wgpu::TextureFormat; SHADOW_COLORS]>,
notes: &mut Vec<String>,
@@ -6065,7 +6065,7 @@ mod tests {
texture("composite", "tr", false),
texture("composite", "gaux2", true),
texture("composite", "noisetex", true),
texture(horizon_shaders::EVERY_STAGE, "cp", true),
texture(horizon_render_pipeline::textures::EVERY_STAGE, "cp", true),
];
let names = ["tp", "tr", "gaux2", "noisetex", "cp"];
let interface = horizon_shaders::Interface {
@@ -58,7 +58,7 @@ pub(super) const ATTRIBUTES: [wgpu::VertexAttribute; 2] =
wgpu::vertex_attr_array![0 => Float32x3, 1 => Uint32];
/// What `gbuffers_skybasic` reads from these vertices.
pub fn inputs() -> horizon_shaders::GbuffersInputs {
pub fn inputs() -> horizon_render_pipeline::GbuffersInputs {
let declarations =
"layout(location = 0) in vec3 hz_v_pos;\nlayout(location = 1) in uint hz_v_kind;\n\
layout(set = 1, binding = 0) uniform HzSky { mat4 hz_sky_stars; vec4 hz_sky_color; \
@@ -90,7 +90,7 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
hz_MultiTexCoord2 = hz_MultiTexCoord1;\n \
hz_Normal = vec3(0.0, 0.0, 1.0);\n"
);
horizon_shaders::GbuffersInputs {
horizon_render_pipeline::GbuffersInputs {
declarations,
body,
attributes: vec![
@@ -111,8 +111,8 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
}
/// What `gbuffers_skytextured` reads from the sun and moon's vertices.
pub fn celestial_inputs() -> horizon_shaders::GbuffersInputs {
horizon_shaders::GbuffersInputs {
pub fn celestial_inputs() -> horizon_render_pipeline::GbuffersInputs {
horizon_render_pipeline::GbuffersInputs {
declarations:
"layout(location = 0) in vec3 hz_v_pos;\nlayout(location = 1) in vec2 hz_v_uv;\n\
layout(location = 2) in vec4 hz_v_color;\n"
@@ -2,7 +2,10 @@
//! which of them a program's sampler reads.
use horizon_render_pipeline::Kind;
use horizon_shaders::{CustomTexture, Group, Texels, Texture, TextureDim, TextureSource};
use horizon_render_pipeline::textures::{
CustomTexture, Dim as TextureDim, EVERY_STAGE, Texels, Texture, TextureSource,
};
use horizon_shaders::Group;
/// What a custom texture is once uploaded: an image of ours, or one the game already has.
pub(super) enum Image {
@@ -127,16 +130,16 @@ pub(super) fn find(textures: &[PackTexture], group: Group, ty: &str, name: &str)
|| buffer.is_some() && horizon_render_pipeline::buffer_index(&t.name) == buffer)
});
staged.or_else(|| {
textures.iter().position(|t| {
t.stage == horizon_shaders::EVERY_STAGE && t.dimension == dimension && t.name == name
})
textures
.iter()
.position(|t| t.stage == EVERY_STAGE && t.dimension == dimension && t.name == name)
})
}
/// The `shaders.properties` key a texture came from, for notes.
fn key(stage: &str, name: &str) -> String {
match stage {
horizon_shaders::EVERY_STAGE => format!("customTexture.{name}"),
EVERY_STAGE => format!("customTexture.{name}"),
_ => format!("texture.{stage}.{name}"),
}
}
@@ -351,7 +354,7 @@ mod tests {
#[test]
fn custom_textures_reach_every_program() {
use wgpu::TextureViewDimension as D;
let every = horizon_shaders::EVERY_STAGE;
let every = EVERY_STAGE;
let textures = [
lightmap(every, "texClouds", D::D3),
lightmap("composite", "texClouds", D::D3),
@@ -2717,7 +2717,7 @@ pub(super) mod initial_history_tests {
/// `horizonWaterPatch<k>`'s `k`: patch `k / 2`, its corner and y range (even) or edges (odd).
fn water_patch_index(name: &str) -> Option<usize> {
let k: usize = name.strip_prefix("horizonWaterPatch")?.parse().ok()?;
(k < 2 * horizon_shaders::WATER_PATCHES).then_some(k)
(k < 2 * horizon_render_pipeline::WATER_PATCHES).then_some(k)
}
/// The boats' water patches among the frame's entity quads (`WATER_MASK_OVERLAY`), the first
@@ -2729,7 +2729,7 @@ pub(super) fn water_patches(entities: &[super::Vertex]) -> Vec<[[f32; 4]; 2]> {
let mut points: Vec<[f32; 3]> = Vec::new();
let flush = |points: &mut Vec<[f32; 3]>, out: &mut Vec<[[f32; 4]; 2]>| {
if let Some(b) = patch_box(points)
&& out.len() < horizon_shaders::WATER_PATCHES
&& out.len() < horizon_render_pipeline::WATER_PATCHES
{
out.push(b);
}
@@ -11,7 +11,7 @@ use super::pack_post::Weather;
/// What `gbuffers_weather` reads from a weather vertex: camera-relative position, the UV in
/// repeats of the sprite, colour (with vanilla's fade) and light. Weather has no normal; the
/// pack sees straight up.
pub fn inputs() -> horizon_shaders::GbuffersInputs {
pub fn inputs() -> horizon_render_pipeline::GbuffersInputs {
let declarations =
"layout(location = 0) in vec3 hz_v_pos;\nlayout(location = 1) in vec2 hz_v_uv;\n\
layout(location = 2) in uint hz_v_overlay;\nlayout(location = 3) in vec4 hz_v_color;\n\
@@ -31,7 +31,7 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
("vaColor", "hz_Color"),
("vaNormal", "hz_Normal"),
];
horizon_shaders::GbuffersInputs {
horizon_render_pipeline::GbuffersInputs {
declarations,
body,
attributes: attributes
@@ -40,7 +40,7 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
.collect(),
after: String::new(),
// Vanilla's weather drops texels below 0.1, as Horizon's own weather shader does.
alpha_test: Some(horizon_shaders::AlphaTest {
alpha_test: Some(horizon_render_pipeline::AlphaTest {
op: ">",
reference: 0.1,
}),
@@ -158,7 +158,7 @@ impl Weather {
match native {
Some((pipeline, frame)) => {
pass.set_pipeline(pipeline);
pass.set_bind_group(horizon_shader_api::stages::GROUP, frame, &[]);
pass.set_bind_group(horizon_render_pipeline::stages::GROUP, frame, &[]);
}
None => pass.set_pipeline(&self.pipeline),
}
@@ -225,7 +225,7 @@ struct HorizonWeatherOut {
";
/// The shader of a native pack's weather stage `source`.
fn native_source(source: &str, api: horizon_shader_api::Api) -> String {
fn native_source(source: &str, api: horizon_render_pipeline::Api) -> String {
format!(
"{source}\n// Horizon's weather (entity.wgsl)\n{}{}{NATIVE_ENTRIES}",
super::wgsl_prefix::prefixed(&super::entity_shader(), "horizon_entity_"),
@@ -245,7 +245,7 @@ pub(super) fn native_pipeline(
stage: &horizon_shader_api::check::CheckedStage,
constants: &[(String, f64)],
) -> Result<wgpu::RenderPipeline, String> {
use horizon_shader_api::stages::GROUP;
use horizon_render_pipeline::stages::GROUP;
let text = native_source(&stage.source, frame.api);
let module = naga::front::wgsl::parse_str(&text)
.map_err(|e| format!("{}: {}", stage.file, e.emit_to_string(&text)))?;
@@ -322,7 +322,7 @@ mod tests {
#[test]
fn a_native_weather_stage_validates() {
use horizon_shader_api::{Api, stages::StageKind};
use horizon_render_pipeline::{Api, StageKind};
for api in [Api::V0_1, Api::V0_2] {
let source = StageKind::Weather.prelude(api)
+ "fn weather(input: WeatherInput) -> vec4<f32> {\n \
+9 -1
View File
@@ -493,11 +493,19 @@ dependencies = [
]
[[package]]
name = "horizon-shader-api"
name = "horizon-render-pipeline"
version = "0.1.0"
dependencies = [
"bytemuck",
"naga",
]
[[package]]
name = "horizon-shader-api"
version = "0.1.0"
dependencies = [
"horizon-render-pipeline",
"naga",
"png",
"serde",
"serde_json",
@@ -7,6 +7,10 @@ rust-version.workspace = true
description = "What an external shader pipeline gives the renderer: its buffers, shadow map and programs. Engine layer; the pack formats that fill it are platform crates"
[dependencies]
# The frame uniform is uploaded as bytes (inputs.rs).
bytemuck = { version = "1.24", features = ["derive"] }
# A translated program's stages are naga modules (program.rs).
naga = "30.0.1"
[lints]
workspace = true
+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")
);
}
}
+20
View File
@@ -5,10 +5,30 @@
//! `shaders/` packs). No wgpu and no pack file here.
//!
//! - [`config`]: the colortex buffers and the shadow map: formats, clearing, mipmaps, settings.
//! - [`program`]: a translated program (naga modules and their interface) and the vertex
//! inputs the renderer offers it.
//! - [`textures`]: the custom textures the programs read; [`RENDER_STAGES`]: the stages they draw in.
//! - [`stages`], [`inputs`], [`Api`]: the WGSL stage interface Horizon's own packs are written
//! against (`horizon-shader-api` reads and checks those packs): the stages' functions, preludes,
//! bindings and entry points, and the frame uniform the renderer fills.
mod api;
pub mod config;
pub mod inputs;
pub mod program;
mod render_stage;
pub mod stages;
pub mod textures;
pub use api::Api;
pub use config::{
BUFFERS, Buffer, Config, Format, HALF_LIVES, Kind, SHADOW_COLORS, Shadow, buffer_index,
shadow_color,
};
pub use inputs::Frame;
pub use program::{
AlphaTest, ComputeProgram, GbuffersInputs, GeometryInputs, Interface, Program, WATER_MASK,
WATER_PATCHES,
};
pub use render_stage::{RENDER_STAGES, render_stage};
pub use stages::StageKind;
@@ -0,0 +1,199 @@
//! A translated program as the renderer builds its pipelines from it: its naga modules and the
//! interface they expect (uniforms, samplers, images, outputs), and what the renderer offers a
//! program's vertex stage (`GbuffersInputs`, `GeometryInputs`). `horizon-shaders` translates a
//! `shaders/` pack's GLSL into these.
/// What a program's stages need from the game.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Interface {
/// The uniform block at binding 0: its size and each value's name, GLSL type and offset.
/// Ours come first: `hz_target` (the target's width, height, 1/width, 1/height), and the
/// fixed-function fog the compatibility `gl_Fog` reads (OQ-28): `hz_fog_color` (vanilla's fog
/// colour) and `hz_fog` (density, start, end, 1 / (end - start)), then `hz_depth_linear`:
/// 1 where `shadowtex0`, `shadowtex1`, `shadow` and `watershadow` (x to w) read through a
/// plain sampler filter linearly, 0 where they take the nearest texel.
pub uniform_size: u32,
pub uniforms: Vec<Uniform>,
/// std140 initial contents; runtime values overwrite their members each frame.
pub initial_values: Vec<u8>,
/// Each sampler: texture at `binding`, sampler object at `binding + 1`.
pub samplers: Vec<Sampler>,
/// The fragment stage's output locations.
pub outputs: Vec<u32>,
/// A gbuffers program reads `DRAW_UNIFORMS`: the block at set 2, binding 0, set per draw.
pub draw: bool,
/// Storage images (`colorimg4`...), after the samplers.
pub images: Vec<Image>,
/// The SSBOs' bindings in set 4 (`bufferObject.<n>`).
pub buffers: Vec<u32>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Image {
pub name: String,
/// `image2D`, `uimage3D`...
pub ty: String,
/// The format qualifier (`rgba16f`).
pub format: String,
/// `readonly`, `writeonly`, empty for both, or `atomic` for an image an atomic function
/// uses.
pub access: String,
pub binding: u32,
/// A compute program's copy of image `before` as it was before the dispatch, which its
/// loads read (see `ordering`).
pub before: Option<String>,
}
/// A compute program (`deferred4_a.csh`): its module, what it reads and writes, and how many
/// work groups it runs: `workGroups`, or `workGroupsRender` times the target's size over the
/// local size (1, 1 when neither is set).
#[derive(Clone)]
pub struct ComputeProgram {
pub module: naga::Module,
pub interface: Interface,
/// Non-fatal translation diagnostics, located in the authored source.
pub notes: Vec<Error>,
pub work_groups: Option<[u32; 3]>,
pub work_groups_render: [f32; 2],
}
/// The uniforms a gbuffers program reads per draw (5.3's "D"), in a block of their own
/// (`HzDraw`, std140: four ints, a vec4; 32 bytes) the game sets for each draw.
pub const DRAW_UNIFORMS: [(&str, &str); 5] = [
("renderStage", "int"),
("entityId", "int"),
("blockEntityId", "int"),
("currentRenderedItemId", "int"),
("entityColor", "vec4"),
];
#[derive(Clone, Debug, PartialEq)]
pub struct Uniform {
pub name: String,
/// The type the pack declared (`float`, `ivec2`, `mat4`; `bool` is stored as an `int`).
pub ty: String,
pub offset: u32,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Sampler {
pub name: String,
/// The type the pack declared: `sampler2D`, `sampler2DShadow`, `usampler2D`, `sampler3D`...
pub ty: String,
pub binding: u32,
}
/// A translation diagnostic at a pack file and line.
#[derive(Clone, Debug, PartialEq)]
pub struct Error {
pub file: String,
pub line: u32,
pub message: String,
}
#[derive(Clone)]
pub struct Program {
pub vertex: naga::Module,
pub fragment: naga::Module,
pub interface: Interface,
/// Non-fatal translation diagnostics, located in the authored source.
pub notes: Vec<Error>,
}
/// Where the program draws, which decides the conventions of 5.5.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Target {
/// Pack buffers: rows stored as GL stores them.
Buffers,
/// The window (`final`).
Window,
}
/// What a gbuffers-style program's vertex stage reads. The game writes it: it knows its vertex
/// formats, the translator only places it.
#[derive(Clone, Debug, Default)]
pub struct GbuffersInputs {
/// GLSL declarations: the vertex inputs and any bindings of set 1.
pub declarations: String,
/// GLSL statements that set `hz_Vertex`, `hz_Color`, `hz_MultiTexCoord0`,
/// `hz_MultiTexCoord1` and `hz_Normal` (the compatibility inputs), run first in `main`.
pub body: String,
/// The value of each attribute a pack may declare (`mc_Entity`, `vaPosition`...), as a GLSL
/// expression of the declared type's size; an attribute not listed reads zero. Terrain
/// also supplies `mc_chunkFade`, an implicit float input rather than a declared attribute.
pub attributes: Vec<(String, String)>,
/// GLSL statements run last in `main`, with `gl_Position` in wgpu's clip space.
pub after: String,
/// The alpha test applied to output 0 (`gl_FragData[0]`, `gl_FragColor`), the loader's
/// (3.2 `alphaTest.<program>`): `None` keeps every fragment.
pub alpha_test: Option<AlphaTest>,
/// The program may read `currentRenderedItemId` without declaring it, as Iris 1.11.7
/// lets the hand's (observed with probe packs; terrain's may not).
pub item_id_declared: bool,
/// The depth test sees the far plane (window depth 1) unless the program writes
/// `gl_FragDepth`, while `gl_FragCoord.z` stays the geometry's own: the sky shows only where
/// nothing else drew, and reads its own depth as in Iris 1.11.7.
pub far_depth: bool,
/// `normals` and `specular` are bound as paged maps of the atlas `texture` reads
/// (`horizon-client-render`'s `paged_maps.rs`), not as images of its layout: the program's
/// texture calls on them read through `hz_pagedTexture`.
pub paged_maps: bool,
/// Translucent terrain: a fragment behind the boats' water patches (`horizonWaterMask`, the
/// patches' depth, 1 where there are none) is discarded, so no water shows inside a hull
/// (`RenderTypes.waterMask`), while the frame's depth stays the pack's own.
pub water_mask: bool,
/// The shadow pass's translucent terrain: a fragment inside a boat's water patch
/// (`horizonWaterPatch*`, camera-relative boxes) is discarded, so no water casts its light
/// into a hull. Not with a geometry stage.
pub water_patches: bool,
}
/// The sampler of the boats' water patches' depth (`GbuffersInputs::water_mask`).
pub const WATER_MASK: &str = "horizonWaterMask";
/// How many boats' water patches the shadow pass keeps water out of (`GbuffersInputs::water_patches`).
pub const WATER_PATCHES: usize = 16;
/// A fragment stays when its output 0's alpha compares to `reference` as `op` says (a GLSL
/// comparison operator, `true` for GL's `ALWAYS`, `false` for `NEVER`).
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AlphaTest {
pub op: &'static str,
pub reference: f32,
}
impl AlphaTest {
/// `alphaTest.<program>`'s value: `off`, or a GL comparison and a reference.
pub fn parse(value: &str) -> Option<Option<Self>> {
let mut words = value.split_whitespace();
let func = words.next()?;
if func == "off" {
return Some(None);
}
let op = match func.trim_start_matches("GL_") {
"NEVER" => "false",
"LESS" => "<",
"EQUAL" => "==",
"LEQUAL" => "<=",
"GREATER" => ">",
"NOTEQUAL" => "!=",
"GEQUAL" => ">=",
"ALWAYS" => return Some(None),
_ => return None,
};
let reference = words.next()?.parse().ok()?;
Some(Some(Self { op, reference }))
}
}
/// How a geometry program's vertex invocations reach their triangle's vertices.
#[derive(Clone, Debug, Default)]
pub struct GeometryInputs {
/// The game's vertex inputs, as `GbuffersInputs`; their `layout(location = n) in`
/// declarations become plain globals the pull sets.
pub inputs: GbuffersInputs,
/// GLSL statements that set the vertex inputs from vertex `hz_vertex_index` (a `uint`).
pub pull: String,
/// The vertex of the `@`th corner of the draw (a `uint` expression, `hz_indices[@]`).
pub index: String,
}
@@ -0,0 +1,38 @@
//! The render stages a pipeline's programs are told they draw in (`renderStage`, and the
//! `MC_RENDER_STAGE_*` macros `horizon-shaders` defines from them).
/// `MC_RENDER_STAGE_*` in the order the stages draw (spec 5.4); each is its index (OQ-7).
pub const RENDER_STAGES: [&str; 24] = [
"NONE",
"SKY",
"SUNSET",
"CUSTOM_SKY",
"SUN",
"MOON",
"STARS",
"VOID",
"TERRAIN_SOLID",
"TERRAIN_CUTOUT_MIPPED",
"TERRAIN_CUTOUT",
"ENTITIES",
"BLOCK_ENTITIES",
"DESTROY",
"OUTLINE",
"DEBUG",
"HAND_SOLID",
"TERRAIN_TRANSLUCENT",
"TRIPWIRE",
"PARTICLES",
"CLOUDS",
"RAIN_SNOW",
"WORLD_BORDER",
"HAND_TRANSLUCENT",
];
/// The value of `MC_RENDER_STAGE_<name>` (OQ-7: its index in `RENDER_STAGES`).
pub fn render_stage(name: &str) -> u32 {
RENDER_STAGES
.iter()
.position(|s| *s == name)
.unwrap_or_else(|| panic!("no render stage {name}")) as u32
}
@@ -0,0 +1,803 @@
//! The stages a WGSL pack can fill, each a WGSL function with a fixed signature
//! that the renderer calls inside its own passes, and the WGSL put around a
//! pack's files to build a stage: the prelude before (the inputs and the
//! resources), the entry points after. `horizon-shader-api` composes a pack's
//! files between them.
use crate::Api;
use crate::inputs::{
DISPLACEMENT_INPUT, Frame, MATERIAL_INPUT, POST_INPUT, SKY_INPUT, SURFACE, Struct,
TONEMAP_INPUT, VERTEX_INPUT, WEATHER_INPUT,
};
/// The bind group of a pack's resources; groups 0 to 2 are the renderer's.
pub const GROUP: u32 = 3;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StageKind {
/// The sky's colour along each view ray, before the sun, moon and stars.
Sky,
/// The colour of block surfaces (terrain), before the alpha test and fog.
Material,
/// Where each corner of a block's surface is drawn (waving plants, rolling water), for
/// the world and the shadow map.
Vertex,
/// The colour of rain and snow streaks.
Weather,
/// A full-screen pass over the world's picture, under the hand and HUD.
Post,
/// 0.2: the picture for an output (the display), once per pixel of it after the post chain,
/// before the camera overlays and the HUD.
Tonemap,
}
/// A resource a stage reads, declared by its prelude.
#[derive(Debug)]
pub struct Resource {
pub binding: u32,
pub name: &'static str,
pub ty: &'static str,
pub doc: &'static str,
}
const FRAME: Resource = Resource {
binding: 0,
name: "frame",
ty: "var<uniform> frame: Frame",
doc: "The frame (`Frame`).",
};
/// The shadow map at `binding`.
const fn shadow_map(binding: u32) -> Resource {
Resource {
binding,
name: "shadow_map",
ty: "var shadow_map: texture_depth_2d",
doc: "This frame's shadow map: the depth of the solid terrain and entities the light sees, 0 at the light, 1 away from it (and where nothing is), stored at `shadow_position`'s mapping. Valid while `frame.has_shadow` is 1 (the pack asks for it with `[shadow]`), else a 1×1 map that lights everything.",
}
}
/// The shadow map's comparison sampler at `binding`.
const fn shadow_sampler(binding: u32) -> Resource {
Resource {
binding,
name: "shadow_sampler",
ty: "var shadow_sampler: sampler_comparison",
doc: "Depth comparison, bilinear (2×2 percentage-closer filtering): `textureSampleCompareLevel(shadow_map, shadow_sampler, p.xy, p.z)` is 1 where the point is lit, 0 in shadow.",
}
}
/// The shadow map with translucent terrain at `binding`.
const fn shadow_map_translucent(binding: u32) -> Resource {
Resource {
binding,
name: "shadow_map_translucent",
ty: "var shadow_map_translucent: texture_depth_2d",
doc: "`shadow_map` with the translucent terrain (water, stained glass, ice) the light passes through drawn in too, read with `shadow_sampler` the same way: a point lit in `shadow_map` but not here lies under such a surface, which `shadow_color` gives the colour of.",
}
}
/// The colour of the translucent terrain in the shadow map at `binding`.
const fn shadow_color(binding: u32) -> Resource {
Resource {
binding,
name: "shadow_color",
ty: "var shadow_color: texture_2d<f32>",
doc: "The texture colour times the vertex colour (rgb: the tint, the face's directional shade and ambient occlusion) of the nearest translucent surface the light passes through at each texel of `shadow_map_translucent`, white where there is none; alpha is 1 on a fluid (water) and the texture's alpha, at most 0.99, on anything else. Read it with `textureLoad` at `shadow_position`'s xy times `textureDimensions(shadow_color)`.",
}
}
/// A function the material and post preludes give packs.
pub const SHADOW_HELPERS: &str = "\
// The shadow map's distortion toward its centre, of a point at shadow clip `xy`.
fn shadow_distort(xy: vec2<f32>) -> vec2<f32> {
let k = frame.shadow_distortion;
return xy / (length(xy) * k + 1.0 - k);
}
// Where the camera-relative point `position` is stored in the shadow map: xy its 0..1 texture
// coordinate (from the top left), z its depth from the light, for `textureSampleCompareLevel`.
fn shadow_position(position: vec3<f32>) -> vec3<f32> {
let clip = frame.shadow_view_proj * vec4<f32>(position, 1.0);
let xy = shadow_distort(clip.xy);
// At most 1, the depth where nothing is: past the map's far end a point stays lit.
return vec3<f32>(xy.x * 0.5 + 0.5, 0.5 - xy.y * 0.5, min(clip.z, 1.0));
}
";
const LINEAR_SAMPLER: Resource = Resource {
binding: 4,
name: "linear_sampler",
ty: "var linear_sampler: sampler",
doc: "Bilinear filtering, clamped to the edges.",
};
const NEAREST_SAMPLER: Resource = Resource {
binding: 5,
name: "nearest_sampler",
ty: "var nearest_sampler: sampler",
doc: "Nearest texel, clamped to the edges.",
};
/// The tonemap stage's resources, at the post stage's bindings for the same names.
const TONEMAP_RESOURCES: [Resource; 4] = [
FRAME,
Resource {
binding: 3,
name: "previous",
ty: "var previous: texture_2d<f32>",
doc: "The post chain's picture at the window's size (without a post chain, the world's at the world's size), linear light: what `input.color` samples, for a curve that reads around the pixel.",
},
LINEAR_SAMPLER,
NEAREST_SAMPLER,
];
/// 0.2's colour boundary, in every 0.2 prelude: vanilla's 2.2 power both ways.
const COLOR_HELPERS: &str = "\
// Negatives and NaN made 0 (a comparison with NaN is false), so no `pow` sees them.
fn horizon_sanitize(c: vec3<f32>) -> vec3<f32> {
return select(vec3<f32>(0.0), c, c > vec3<f32>(0.0));
}
fn horizon_decode(c: vec3<f32>) -> vec3<f32> {
return pow(horizon_sanitize(c), vec3<f32>(2.2));
}
fn horizon_encode(c: vec3<f32>) -> vec3<f32> {
return pow(horizon_sanitize(c), vec3<f32>(1.0 / 2.2));
}
// Alpha is coverage: it stays as it is.
fn horizon_decode_rgba(c: vec4<f32>) -> vec4<f32> {
return vec4<f32>(horizon_decode(c.rgb), c.a);
}
fn horizon_encode_rgba(c: vec4<f32>) -> vec4<f32> {
return vec4<f32>(horizon_encode(c.rgb), c.a);
}
";
const POST_RESOURCES: [Resource; 12] = [
FRAME,
Resource {
binding: 1,
name: "scene_color",
ty: "var scene_color: texture_2d<f32>",
doc: "The world's picture before any pass.",
},
Resource {
binding: 2,
name: "scene_depth",
ty: "var scene_depth: texture_depth_2d",
doc: "The world's depth, reversed (1 near, 0 far and sky).",
},
Resource {
binding: 3,
name: "previous",
ty: "var previous: texture_2d<f32>",
doc: "The previous pass's output (`scene_color` for the first pass).",
},
LINEAR_SAMPLER,
NEAREST_SAMPLER,
shadow_map(6),
shadow_sampler(7),
Resource {
binding: 8,
name: "history",
ty: "var history: texture_2d<f32>",
doc: "Last frame's input to the last pass (what the pass before it wrote), for temporal passes (TAA, denoising): put such a pass second to last and it reads its own previous output; reproject with `frame.previous_view_projection`. Black on the first frame, after a gap of over half a second, and with a single pass.",
},
shadow_map_translucent(9),
shadow_color(10),
Resource {
binding: 11,
name: "scene_surface",
ty: "var scene_surface: texture_2d<u32>",
doc: "What the material stage's `set_surface` stored for the solid terrain at each pixel, unfiltered (`textureLoad`; its texels are the bits of floats, so `bitcast<vec4<f32>>` them): read it through the prelude's `surface_at`, which knows its layout and where it no longer shows. Blank without a material stage that calls `set_surface`.",
},
];
/// The material stage's resources: the frame, and the world as it was before translucent
/// terrain drew (for water and glass to see what lies behind them).
const MATERIAL_RESOURCES: [Resource; 11] = [
FRAME,
Resource {
binding: 1,
name: "reflection",
ty: "var reflection: texture_2d<f32>",
doc: "The water mirror: the world as a camera mirrored about the horizontal water plane around the camera (`frame.water_plane`) sees it, at half resolution, mapped like the world's picture, so water samples it at `input.screen`. Solid terrain, cut away below the plane, over the vanilla sky gradient: not the pack's `sky` stage, nor the sun, moon and stars. Drawn only for a pack whose material reads it, while `frame.has_reflection` is 1; else a 1×1 blank.",
},
Resource {
binding: 2,
name: "reflection_sampler",
ty: "var reflection_sampler: sampler",
doc: "Bilinear filtering, clamped to the edges.",
},
shadow_map(3),
shadow_sampler(4),
shadow_map_translucent(7),
shadow_color(8),
Resource {
binding: 6,
name: "opaque_depth",
ty: "var opaque_depth: texture_depth_2d",
doc: "The world's depth before translucent terrain (reversed, like `scene_depth`), for the translucent pass: `textureLoad(opaque_depth, vec2<i32>(input.screen * frame.resolution), 0)` is what lies behind water or glass. Stale while solid terrain draws.",
},
Resource {
binding: 9,
name: "block_light_colors",
ty: "var block_light_colors: texture_3d<f32>",
doc: "The colour of the block light around the camera, from the `[[lights]]` of the light sources nearby: rgb their colours averaged by how near each is, over 4; a how strongly they reach the cell. Read it through `block_light_color`.",
},
Resource {
binding: 10,
name: "block_light_grid",
ty: "var<uniform> block_light_grid: vec4<f32>",
doc: "Where `block_light_colors` lies: xyz its corner, camera-relative, w the side of one of its cells in blocks.",
},
Resource {
binding: 11,
name: "opaque_color",
ty: "var opaque_color: texture_2d<f32>",
doc: "The world's picture before translucent terrain (sky, solid terrain, entities), the size of `opaque_depth`, for the translucent pass: what a surface refracts. Read it with `textureLoad`. Stale while solid terrain draws; black when the world draws straight to the window (a pack without `[[post]]` passes, at full resolution).",
},
];
/// Functions the post prelude gives packs.
const POST_HELPERS: &str = "\
// The world's depth at `uv` (reversed: 0 is the sky).
fn depth_at(uv: vec2<f32>) -> f32 {
let size = vec2<f32>(textureDimensions(scene_depth));
let texel = vec2<i32>(clamp(uv * size, vec2<f32>(0.0), size - 1.0));
return textureLoad(scene_depth, texel, 0);
}
// The camera-relative world position, in blocks, of the point at `uv` and `depth`.
fn view_position(uv: vec2<f32>, depth: f32) -> vec3<f32> {
let clip = vec4<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, depth, 1.0);
let view = frame.projection_inverse * clip;
return (frame.view_inverse * vec4<f32>(view.xyz / view.w, 0.0)).xyz;
}
";
/// What the material entries (the checker's below and the renderer's) share: the
/// surface's normal and tangent from the derivatives of its position and uv. The arguments are
/// the line's return statements of `block_light_color`, `block_texture` and
/// `block_texture_level`, the helpers that read colour.
macro_rules! material_helpers {
($light:literal, $texture:literal, $texture_level:literal) => {
concat!(
"\
// The colour of the block light at the camera-relative `position` (rgb, from the `[[lights]]`
// of the sources nearby) and how strongly they reach it (a, 0 where none does). Block light
// keeps its own brightness (`input.light.x`): this is its colour only.
fn block_light_color(position: vec3<f32>) -> vec4<f32> {
let size = vec3<f32>(textureDimensions(block_light_colors));
let uvw = (position - block_light_grid.xyz) / (block_light_grid.w * size);
if any(uvw < vec3<f32>(0.0)) || any(uvw > vec3<f32>(1.0)) {
return vec4<f32>(0.0);
}
let c = textureSampleLevel(block_light_colors, reflection_sampler, uvw, 0.0);
",
$light,
"}
// What the post stage's `surface_at` reads for this point of solid terrain: its `normal` (a
// unit vector in world axes), `smoothness` (0..1, say for reflections) and `data`, a value of
// the pack's own passed through. Unset, the point keeps the face's own normal and 0 for both;
// translucent terrain stores none.
var<private> horizon_surface: vec4<f32>;
fn set_surface(normal: vec3<f32>, smoothness: f32, data: f32) {
// Octahedral, y up: the upper half folds onto xz, the lower half unfolds around it.
let n = normal / (abs(normal.x) + abs(normal.y) + abs(normal.z));
var e = n.xz;
if n.y < 0.0 {
e = (1.0 - abs(n.zx)) * select(vec2<f32>(-1.0), vec2<f32>(1.0), n.xz >= vec2<f32>(0.0));
}
horizon_surface = vec4<f32>(bitcast<f32>(pack2x16snorm(e)), smoothness, data, 1.0);
}
// The point being shaded: its sprite's layer and mip level (the entries set them).
var<private> horizon_block_layer: u32;
var<private> horizon_block_lod: f32;
// The surface's block texture at `uv` (the sprite's space, as `input.uv`), without the tint:
// what `input.albedo` holds at the point itself, readable anywhere on the sprite, for normals
// or detail generated from the texture.
fn block_texture(uv: vec2<f32>) -> vec4<f32> {
",
$texture,
"}
// `block_texture` at mip level `lod` (0 the full-size texture).
fn block_texture_level(uv: vec2<f32>, lod: f32) -> vec4<f32> {
",
$texture_level,
"}
// The block texture's size, in texels (16 x 16 for vanilla's).
fn block_texture_size() -> vec2<f32> {
return horizon_block_size();
}
// The mip level `block_texture` reads at this point (0 up close, more far away).
fn block_texture_lod() -> f32 {
return horizon_block_lod;
}
// LabPBR: whether the resource packs ship normal (`_n`) or specular (`_s`) maps beside their
// block textures. Without them `block_normal_map` and `block_specular_map` give the defaults
// below.
fn block_pbr_maps() -> bool {
return horizon_block_pbr_present();
}
// The surface's LabPBR normal map at `uv` (the sprite's space, as `input.uv`): rg the normal's
// x and y in the texture's space (x along `input.tangent`, y down the texture; 0..1 for
// -1..1), b ambient occlusion (1 none), a height (1 the top). (0.5, 0.5, 1, 1) where a texture
// has none.
fn block_normal_map(uv: vec2<f32>) -> vec4<f32> {
return horizon_block_pbr(uv, horizon_block_layer, horizon_block_lod, 0u);
}
// `block_normal_map` at the block texture's mip level `lod` (the same footprint on the surface,
// where the map is smaller than the texture).
fn block_normal_map_level(uv: vec2<f32>, lod: f32) -> vec4<f32> {
return horizon_block_pbr(uv, horizon_block_layer, lod, 0u);
}
// The surface's LabPBR specular map at `uv`: r perceptual smoothness, g F0 up to 229/255 and
// a metal's id above, b porosity or subsurface scattering, a emission (up to 254/255; 1
// none). (0, 0, 0, 0) where a texture has none.
fn block_specular_map(uv: vec2<f32>) -> vec4<f32> {
return horizon_block_pbr(uv, horizon_block_layer, horizon_block_lod, 1u);
}
// `block_specular_map` at the block texture's mip level `lod`.
fn block_specular_map_level(uv: vec2<f32>, lod: f32) -> vec4<f32> {
return horizon_block_pbr(uv, horizon_block_layer, lod, 1u);
}
// The LabPBR maps' size, in texels (they may be smaller than the block's texture).
fn block_pbr_size() -> vec2<f32> {
return horizon_block_pbr_size();
}
// Block faces are flat: the derivatives of the position give the face's normal, turned
// toward the camera. Far away they lose precision, so a normal within about 3.6 degrees of
// an axis (every full block face) is that axis exactly: no shimmer on distant faces. The
// gentlest slope of flowing water, one level over a block, tilts about 6 degrees.
fn horizon_material_normal(position: vec3<f32>) -> vec3<f32> {
let n = normalize(cross(dpdx(position), dpdy(position)));
let a = abs(n);
let m = max(a.x, max(a.y, a.z));
let axis = select(select(vec3(0.0, 0.0, sign(n.z)), vec3(0.0, sign(n.y), 0.0), a.y == m),
vec3(sign(n.x), 0.0, 0.0), a.x == m);
let flat_n = select(n, axis, m > 0.998);
return select(flat_n, -flat_n, dot(flat_n, position) > 0.0);
}
// The direction `uv.x` grows along on the surface, and the sign of the one `uv.y` grows
// along against `cross(normal, tangent)`.
fn horizon_material_tangent(position: vec3<f32>, uv: vec2<f32>, normal: vec3<f32>) -> vec4<f32> {
let dp1 = dpdx(position);
let dp2 = dpdy(position);
let duv1 = dpdx(uv);
let duv2 = dpdy(uv);
let dp2perp = cross(dp2, normal);
let dp1perp = cross(normal, dp1);
let t = dp2perp * duv1.x + dp1perp * duv2.x;
let b = dp2perp * duv1.y + dp1perp * duv2.y;
if length(t) < 1e-12 {
return vec4<f32>(1.0, 0.0, 0.0, 1.0);
}
let tangent = normalize(t - normal * dot(normal, t));
return vec4<f32>(tangent, select(-1.0, 1.0, dot(cross(normal, tangent), b) >= 0.0));
}
"
)
};
}
const MATERIAL_HELPERS: &str = material_helpers!(
" return vec4<f32>(c.rgb * 4.0, c.a);\n",
" return horizon_block_sample(uv, horizon_block_layer, horizon_block_lod);\n",
" return horizon_block_sample(uv, horizon_block_layer, lod);\n"
);
/// 0.2's: the `[[lights]]` colours and the block texture decoded (the LabPBR maps are data).
const MATERIAL_HELPERS_0_2: &str = material_helpers!(
" return vec4<f32>(horizon_decode(c.rgb * 4.0), c.a);\n",
" return horizon_decode_rgba(horizon_block_sample(uv, horizon_block_layer, horizon_block_lod));\n",
" return horizon_decode_rgba(horizon_block_sample(uv, horizon_block_layer, lod));\n"
);
/// The post prelude's `surface_at`, for the post helpers' end.
const POST_SURFACE: &str = "\
// The solid terrain's surface at a pixel, as the material stage's `set_surface` left it.
struct SurfaceData {
// False where the sky, an entity, water, glass or a particle shows instead.
present: bool,
// A unit vector in world axes.
normal: vec3<f32>,
smoothness: f32,
// The pack's own value, passed through.
data: f32,
}
fn surface_at(texel: vec2<i32>) -> SurfaceData {
let size = vec2<i32>(textureDimensions(scene_surface));
let t = clamp(texel, vec2<i32>(0), size - 1);
let s = bitcast<vec4<f32>>(textureLoad(scene_surface, t, 0));
let depth_size = vec2<i32>(textureDimensions(scene_depth));
let depth = textureLoad(scene_depth, clamp(texel, vec2<i32>(0), depth_size - 1), 0);
// The terrain's own depth is stored with it: anything drawn nearer since hides it.
let present = s.w > 0.0 && abs(s.w - depth) <= s.w * 1e-5;
let e = unpack2x16snorm(bitcast<u32>(s.x));
var n = vec3<f32>(e.x, 1.0 - abs(e.x) - abs(e.y), e.y);
if n.y < 0.0 {
n = vec3<f32>((1.0 - abs(n.z)) * sign(n.x), n.y, (1.0 - abs(n.x)) * sign(n.z));
}
return SurfaceData(present, select(vec3<f32>(0.0), normalize(n), present),
select(0.0, s.y, present), select(0.0, s.z, present));
}
";
/// The post stage's entry points, as the renderer builds them: a full-screen
/// triangle, and a fragment that calls the pack's `post` for each pixel.
const POST_ENTRIES: &str = "\
struct HorizonPostOut {
@builtin(position) position: vec4<f32>,
@location(0) uv: vec2<f32>,
}
@vertex fn horizon_vs_post(@builtin(vertex_index) i: u32) -> HorizonPostOut {
let p = vec2<f32>(f32((i << 1u) & 2u), f32(i & 2u));
var out: HorizonPostOut;
out.position = vec4<f32>(p * 2.0 - 1.0, 0.0, 1.0);
out.uv = vec2<f32>(p.x, 1.0 - p.y);
return out;
}
@fragment fn horizon_fs_post(in: HorizonPostOut) -> @location(0) vec4<f32> {
let color = textureSampleLevel(previous, linear_sampler, in.uv, 0.0);
return post(PostInput(in.uv, in.position.xy, color, depth_at(in.uv)));
}
";
/// What the renderer gives the material prelude's LabPBR functions (`native_material.rs`); the
/// check and the entries below put a stand-in in its place.
macro_rules! pbr_stub {
() => {
"fn horizon_block_sample(uv: vec2<f32>, layer: u32, lod: f32) -> vec4<f32> {
return vec4<f32>(1.0);
}
fn horizon_block_size() -> vec2<f32> {
return vec2<f32>(16.0);
}
fn horizon_block_pbr(uv: vec2<f32>, layer: u32, lod: f32, map: u32) -> vec4<f32> {
return select(vec4<f32>(0.5, 0.5, 1.0, 1.0), vec4<f32>(0.0), map == 1u);
}
fn horizon_block_pbr_present() -> bool {
return false;
}
fn horizon_block_pbr_size() -> vec2<f32> {
return vec2<f32>(16.0);
}
"
};
}
pub const PBR_STUB: &str = pbr_stub!();
/// Entry points with the material stage's inputs: the renderer calls
/// `material` from its own terrain shader in the same way. The arguments are the line's
/// `MaterialInput` and return statements.
macro_rules! material_entries {
($input:literal, $result:literal) => {
concat!(
pbr_stub!(),
"\
struct HorizonMaterialIn {
@builtin(position) position: vec4<f32>,
@location(0) albedo: vec4<f32>,
@location(1) tint: vec4<f32>,
@location(2) light: vec2<f32>,
@location(3) lightmap: vec3<f32>,
@location(4) world: vec3<f32>,
@location(5) uv: vec2<f32>,
@location(6) @interpolate(flat) translucent: u32,
@location(7) @interpolate(flat) tags: u32,
@location(8) @interpolate(flat) face: u32,
@location(9) @interpolate(flat) fluid_quad: u32,
@location(10) @interpolate(flat) emission: f32,
@location(11) @interpolate(flat) material: u32,
}
@fragment fn horizon_fs_material(in: HorizonMaterialIn) -> @location(0) vec4<f32> {
let fluid = select(0u, select(2u, 1u, in.translucent != 0u), in.fluid_quad != 0u);
let normal = horizon_material_normal(in.world);
let tangent = horizon_material_tangent(in.world, in.uv, normal);
",
$input,
$result,
"}
"
)
};
}
const MATERIAL_ENTRIES: &str = material_entries!(
" let input = MaterialInput(in.albedo, in.tint, in.light, in.lightmap, in.world, in.uv, in.translucent != 0u, in.tags, in.material,
in.position.xy / frame.resolution, in.face, fluid, in.emission, normal, tangent, vec3<f32>(0.0), 0.0);
",
" return material(input).color;\n"
);
/// 0.2's: the colours decoded on the way in, and the result encoded for the frame
/// (`[frame] blend = "gamma"`; under `"linear"` the renderer stores it as it is).
const MATERIAL_ENTRIES_0_2: &str = material_entries!(
" let input = MaterialInput(horizon_decode_rgba(in.albedo), horizon_decode_rgba(in.tint), in.light,
horizon_decode(in.lightmap), in.world, in.uv, in.translucent != 0u, in.tags, in.material,
in.position.xy / frame.resolution, in.face, fluid, in.emission, normal, tangent, vec3<f32>(0.0), 0.0);
",
" return horizon_encode_rgba(material(input).color);\n"
);
/// Entry points with the sky stage's inputs: the renderer calls `sky` from
/// its own sky shader in the same way.
const SKY_ENTRIES: &str = "\
struct HorizonSkyIn {
@builtin(position) position: vec4<f32>,
@location(0) direction: vec3<f32>,
@location(1) color: vec4<f32>,
}
@fragment fn horizon_fs_sky(in: HorizonSkyIn) -> @location(0) vec4<f32> {
return sky(SkyInput(normalize(in.direction), in.color));
}
";
const SKY_ENTRIES_0_2: &str = "\
struct HorizonSkyIn {
@builtin(position) position: vec4<f32>,
@location(0) direction: vec3<f32>,
@location(1) color: vec4<f32>,
}
@fragment fn horizon_fs_sky(in: HorizonSkyIn) -> @location(0) vec4<f32> {
return horizon_encode_rgba(sky(SkyInput(normalize(in.direction), horizon_decode_rgba(in.color))));
}
";
/// An entry point with the weather stage's inputs: the renderer calls `weather` from its own
/// weather shader in the same way.
const VERTEX_ENTRIES: &str = "\
@vertex fn horizon_vs_vertex(
@location(0) position: vec3<f32>, @location(1) uv: vec2<f32>, @location(2) light: vec2<f32>,
@location(3) tint: vec4<f32>, @location(4) material: u32, @location(5) flags: u32,
) -> @builtin(position) vec4<f32> {
let input = VertexInput(position, uv, material, light, tint, flags & 7u, flags >> 31u);
return frame.projection * frame.view * vec4<f32>(vertex(input), 1.0);
}
";
/// 0.2's: the tint is a colour, so it is decoded too.
const VERTEX_ENTRIES_0_2: &str = "\
@vertex fn horizon_vs_vertex(
@location(0) position: vec3<f32>, @location(1) uv: vec2<f32>, @location(2) light: vec2<f32>,
@location(3) tint: vec4<f32>, @location(4) material: u32, @location(5) flags: u32,
) -> @builtin(position) vec4<f32> {
let input = VertexInput(position, uv, material, light, horizon_decode_rgba(tint), flags & 7u, flags >> 31u);
return frame.projection * frame.view * vec4<f32>(vertex(input), 1.0);
}
";
/// `fn displacement(input: DisplacementInput) -> f32`, the material stage's function
/// under `[displacement]`.
pub const DISPLACEMENT_SIGNATURE: &str = "fn displacement(input: DisplacementInput) -> f32";
/// An entry point calling the material stage's `displacement`, after its entries under
/// `[displacement]`: the renderer calls it from its own terrain vertex shader in the same way.
pub const DISPLACEMENT_ENTRIES: &str = "@vertex fn horizon_vs_displacement(
@location(0) position: vec3<f32>, @location(1) normal: vec3<f32>, @location(2) uv: vec2<f32>,
@location(3) material: u32, @location(4) height: f32,
) -> @builtin(position) vec4<f32> {
let depth = displacement(DisplacementInput(position, normal, uv, material, height));
return frame.projection * frame.view * vec4<f32>(position - normal * depth, 1.0);
}
";
const WEATHER_ENTRIES: &str = "\
struct HorizonWeatherIn {
@builtin(position) position: vec4<f32>,
@location(0) color: vec4<f32>,
@location(1) light: vec2<f32>,
@location(2) world: vec3<f32>,
@location(3) @interpolate(flat) snow: u32,
}
@fragment fn horizon_fs_weather(in: HorizonWeatherIn) -> @location(0) vec4<f32> {
return weather(WeatherInput(in.color, in.light, in.world, in.snow != 0u));
}
";
const WEATHER_ENTRIES_0_2: &str = "\
struct HorizonWeatherIn {
@builtin(position) position: vec4<f32>,
@location(0) color: vec4<f32>,
@location(1) light: vec2<f32>,
@location(2) world: vec3<f32>,
@location(3) @interpolate(flat) snow: u32,
}
@fragment fn horizon_fs_weather(in: HorizonWeatherIn) -> @location(0) vec4<f32> {
let input = WeatherInput(horizon_decode_rgba(in.color), in.light, in.world, in.snow != 0u);
return horizon_encode_rgba(weather(input));
}
";
/// The post stage's entry points under 0.2: the chain's targets hold linear light, so nothing
/// converts, and the pass's alpha is ignored.
const POST_ENTRIES_0_2: &str = "\
struct HorizonPostOut {
@builtin(position) position: vec4<f32>,
@location(0) uv: vec2<f32>,
}
@vertex fn horizon_vs_post(@builtin(vertex_index) i: u32) -> HorizonPostOut {
let p = vec2<f32>(f32((i << 1u) & 2u), f32(i & 2u));
var out: HorizonPostOut;
out.position = vec4<f32>(p * 2.0 - 1.0, 0.0, 1.0);
out.uv = vec2<f32>(p.x, 1.0 - p.y);
return out;
}
@fragment fn horizon_fs_post(in: HorizonPostOut) -> @location(0) vec4<f32> {
let color = textureSampleLevel(previous, linear_sampler, in.uv, 0.0);
return vec4<f32>(post(PostInput(in.uv, in.position.xy, color, depth_at(in.uv))).rgb, 1.0);
}
";
/// The tonemap stage's entry points: a full-screen triangle at the window's size, and a fragment
/// that holds the result to what the display pass takes.
const TONEMAP_ENTRIES: &str = "\
struct HorizonTonemapOut {
@builtin(position) position: vec4<f32>,
@location(0) uv: vec2<f32>,
}
@vertex fn horizon_vs_tonemap(@builtin(vertex_index) i: u32) -> HorizonTonemapOut {
let p = vec2<f32>(f32((i << 1u) & 2u), f32(i & 2u));
var out: HorizonTonemapOut;
out.position = vec4<f32>(p * 2.0 - 1.0, 0.0, 1.0);
out.uv = vec2<f32>(p.x, 1.0 - p.y);
return out;
}
@fragment fn horizon_fs_tonemap(in: HorizonTonemapOut) -> @location(0) vec4<f32> {
let color = horizon_sanitize(textureSampleLevel(previous, linear_sampler, in.uv, 0.0).rgb);
let out = tonemap(TonemapInput(in.uv, in.position.xy, color));
return vec4<f32>(min(horizon_sanitize(out), vec3<f32>(frame.headroom)), 1.0);
}
";
impl Api {
/// The stages the line has, in `StageKind::ALL`'s order.
pub fn stages(self) -> &'static [StageKind] {
match self {
Api::V0_1 => &StageKind::ALL[..5],
Api::V0_2 => &StageKind::ALL,
}
}
}
impl StageKind {
pub const ALL: [StageKind; 6] = [
StageKind::Sky,
StageKind::Vertex,
StageKind::Material,
StageKind::Weather,
StageKind::Post,
StageKind::Tonemap,
];
/// The manifest's name for the stage (`[sky]`, `[vertex]`, `[material]`, `[weather]`,
/// `[[post]]`, `[tonemap]`).
pub fn name(self) -> &'static str {
match self {
StageKind::Sky => "sky",
StageKind::Material => "material",
StageKind::Vertex => "vertex",
StageKind::Weather => "weather",
StageKind::Post => "post",
StageKind::Tonemap => "tonemap",
}
}
/// The function a pack defines: it has the stage's name.
pub fn function(self) -> &'static str {
self.name()
}
pub fn input(self) -> &'static Struct {
match self {
StageKind::Sky => &SKY_INPUT,
StageKind::Material => &MATERIAL_INPUT,
StageKind::Vertex => &VERTEX_INPUT,
StageKind::Weather => &WEATHER_INPUT,
StageKind::Post => &POST_INPUT,
StageKind::Tonemap => &TONEMAP_INPUT,
}
}
/// The type the function returns.
pub fn output(self) -> &'static str {
match self {
StageKind::Material => SURFACE.name,
StageKind::Vertex | StageKind::Tonemap => "vec3<f32>",
StageKind::Sky | StageKind::Weather | StageKind::Post => "vec4<f32>",
}
}
/// `fn <name>(input: <Input>) -> <Output>`.
pub fn signature(self) -> String {
format!(
"fn {}(input: {}) -> {}",
self.function(),
self.input().name,
self.output()
)
}
pub fn resources(self) -> &'static [Resource] {
match self {
StageKind::Post => &POST_RESOURCES,
StageKind::Sky | StageKind::Vertex | StageKind::Weather => std::slice::from_ref(&FRAME),
StageKind::Material => &MATERIAL_RESOURCES,
StageKind::Tonemap => &TONEMAP_RESOURCES,
}
}
/// Everything the pack's files may use under the line `api`, declared before them.
pub fn prelude(self, api: Api) -> String {
let mut out = match api {
Api::V0_1 => {
String::from("// Horizon shader API prelude (docs/api/shaders-reference.md)\n")
}
Api::V0_2 => String::from(
"// Horizon shader API 0.2 prelude (docs/api/shaders-reference-0.2.md)\n",
),
};
out += &Frame::line(api).wgsl();
out += &self.input().wgsl();
if self == StageKind::Material {
out += &SURFACE.wgsl();
// The vertex stage's file joins the material's module (`compose`).
out += &VERTEX_INPUT.wgsl();
out += &DISPLACEMENT_INPUT.wgsl();
}
for r in self.resources() {
out += &format!("@group({GROUP}) @binding({}) {};\n", r.binding, r.ty);
}
if api == Api::V0_2 {
out += COLOR_HELPERS;
}
match self {
StageKind::Post => {
out += POST_HELPERS;
out += POST_SURFACE;
out += SHADOW_HELPERS;
}
StageKind::Material => {
out += match api {
Api::V0_1 => MATERIAL_HELPERS,
Api::V0_2 => MATERIAL_HELPERS_0_2,
};
out += SHADOW_HELPERS;
}
StageKind::Vertex => out += SHADOW_HELPERS,
StageKind::Sky | StageKind::Weather | StageKind::Tonemap => {}
}
out
}
/// The entry points put after the pack's files under the line `api`.
pub fn entries(self, api: Api) -> &'static str {
match (api, self) {
(Api::V0_1, StageKind::Sky) => SKY_ENTRIES,
(Api::V0_1, StageKind::Material) => MATERIAL_ENTRIES,
(Api::V0_1, StageKind::Vertex) => VERTEX_ENTRIES,
(Api::V0_1, StageKind::Weather) => WEATHER_ENTRIES,
(Api::V0_1, StageKind::Post) => POST_ENTRIES,
(Api::V0_2, StageKind::Sky) => SKY_ENTRIES_0_2,
(Api::V0_2, StageKind::Material) => MATERIAL_ENTRIES_0_2,
(Api::V0_2, StageKind::Vertex) => VERTEX_ENTRIES_0_2,
(Api::V0_2, StageKind::Weather) => WEATHER_ENTRIES_0_2,
(Api::V0_2, StageKind::Post) => POST_ENTRIES_0_2,
// 0.1 has no tonemap stage: a manifest with one is refused before it is built.
(_, StageKind::Tonemap) => TONEMAP_ENTRIES,
}
}
}
/// The first binding of a pack's `[[textures]]` in group 3; their samplers follow at
/// `TEXTURE_SAMPLER_BINDING`.
pub const TEXTURE_BINDING: u32 = 16;
pub const TEXTURE_SAMPLER_BINDING: u32 = 24;
/// The binding of a pack's first post target (`[[post]] output`) in group 3, the others after.
pub const POST_TARGET_BINDING: u32 = 32;
/// Whether a material stage's `source` (with its prelude) calls `set_surface` itself, and so
/// needs the surface target, which a pack without it doesn't pay for.
pub fn calls_set_surface(source: &str) -> bool {
let call = "set_surface(";
source.matches(call).count() > MATERIAL_HELPERS.matches(call).count()
}
@@ -0,0 +1,71 @@
//! The custom textures a pipeline's programs read: an image of the pack (a PNG or a raw dump,
//! decoded) or a resource location the game resolves, bound to a sampler name in the programs of
//! a stage (`horizon-shaders` reads them from `shaders.properties`, spec 3.4).
use crate::Format;
/// The stage of a `customTexture.<name>`: every program's.
pub const EVERY_STAGE: &str = "*";
/// A texture bound to sampler `name` in the programs of `stage` (`gbuffers`, `deferred`,
/// `composite`...; [`EVERY_STAGE`] for a `customTexture`), or the noise texture (stage and
/// name `noise`).
#[derive(Clone, Debug)]
pub struct CustomTexture {
pub stage: String,
pub name: String,
pub source: TextureSource,
}
#[derive(Clone, Debug)]
pub enum TextureSource {
Image(Texture),
/// A resource location for the game to resolve.
Resource(String),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Dim {
D1,
D2,
D3,
}
#[derive(Clone, Debug)]
pub struct Texture {
pub dim: Dim,
/// Width, height and depth (1 for the dimensions the texture lacks).
pub size: [u32; 3],
/// The internal format the pack asked for.
pub format: Format,
pub texels: Texels,
/// Linear filtering, and clamping instead of repeating: on for a raw texture and off for a
/// PNG, unless the `.mcmeta` beside it says otherwise.
pub blur: bool,
pub clamp: bool,
/// A raw dump rather than a PNG.
pub raw: bool,
}
/// Texel values, `components` per texel: 1, 2 or 4 (three widen to four, alpha 1).
#[derive(Clone, Debug, PartialEq)]
pub enum Texels {
Float(Vec<f32>),
Int(Vec<i32>),
Uint(Vec<u32>),
}
impl Texture {
pub fn components(&self) -> usize {
components(self.format.channels)
}
}
/// The components per texel a texture of `channels` channels stores: three widen to four.
pub fn components(channels: u8) -> usize {
match channels {
1 => 1,
2 => 2,
_ => 4,
}
}
+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()
}
+2 -36
View File
@@ -3,6 +3,8 @@
use std::collections::BTreeMap;
pub use horizon_render_pipeline::{RENDER_STAGES, render_stage};
/// The feature flags of `iris.features.*` (spec 3.2) Horizon has. A pack that requires one of
/// them loads; one it lacks is noted. Horizon defines no `IRIS_FEATURE_<name>` macro for them
/// (spec OQ-2, docs/cleanroom/README.md): that macro family names another loader, so packs take
@@ -59,34 +61,6 @@ struct Macro {
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Macros(BTreeMap<String, Macro>);
/// `MC_RENDER_STAGE_*` in the order the stages draw (spec 5.4); each is its index (OQ-7).
pub const RENDER_STAGES: [&str; 24] = [
"NONE",
"SKY",
"SUNSET",
"CUSTOM_SKY",
"SUN",
"MOON",
"STARS",
"VOID",
"TERRAIN_SOLID",
"TERRAIN_CUTOUT_MIPPED",
"TERRAIN_CUTOUT",
"ENTITIES",
"BLOCK_ENTITIES",
"DESTROY",
"OUTLINE",
"DEBUG",
"HAND_SOLID",
"TERRAIN_TRANSLUCENT",
"TRIPWIRE",
"PARTICLES",
"CLOUDS",
"RAIN_SNOW",
"WORLD_BORDER",
"HAND_TRANSLUCENT",
];
/// `DH_BLOCK_*`, each its index: Distant Horizons' block materials.
pub const DH_BLOCK_MATERIALS: [&str; 16] = [
"UNKNOWN",
@@ -107,14 +81,6 @@ pub const DH_BLOCK_MATERIALS: [&str; 16] = [
"ILLUMINATED",
];
/// The value of `MC_RENDER_STAGE_<name>` (OQ-7: its index in `RENDER_STAGES`).
pub fn render_stage(name: &str) -> u32 {
RENDER_STAGES
.iter()
.position(|s| *s == name)
.unwrap_or_else(|| panic!("no render stage {name}")) as u32
}
impl Macros {
/// The standard macros that need no renderer (spec 1.5.1): `MC_VERSION`, this system's
/// `MC_OS_*`, the GL and GLSL versions Iris reports, the render stages and the host
+3 -65
View File
@@ -4,71 +4,9 @@
//! location the game resolves (`minecraft:textures/...`, `minecraft:dynamic/lightmap_1`).
use crate::{Format, Kind, Pack, Properties};
/// The stage of a `customTexture.<name>`: every program's.
pub const EVERY_STAGE: &str = "*";
/// A texture bound to sampler `name` in the programs of `stage` (`gbuffers`, `deferred`,
/// `composite`...; [`EVERY_STAGE`] for a `customTexture`), or the noise texture (stage and
/// name `noise`).
#[derive(Clone, Debug)]
pub struct CustomTexture {
pub stage: String,
pub name: String,
pub source: TextureSource,
}
#[derive(Clone, Debug)]
pub enum TextureSource {
Image(Texture),
/// A resource location for the game to resolve.
Resource(String),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Dim {
D1,
D2,
D3,
}
#[derive(Clone, Debug)]
pub struct Texture {
pub dim: Dim,
/// Width, height and depth (1 for the dimensions the texture lacks).
pub size: [u32; 3],
/// The internal format the pack asked for.
pub format: Format,
pub texels: Texels,
/// Linear filtering, and clamping instead of repeating: on for a raw texture and off for a
/// PNG, unless the `.mcmeta` beside it says otherwise.
pub blur: bool,
pub clamp: bool,
/// A raw dump rather than a PNG.
pub raw: bool,
}
/// Texel values, `components` per texel: 1, 2 or 4 (three widen to four, alpha 1).
#[derive(Clone, Debug, PartialEq)]
pub enum Texels {
Float(Vec<f32>),
Int(Vec<i32>),
Uint(Vec<u32>),
}
impl Texture {
pub fn components(&self) -> usize {
components(self.format.channels)
}
}
fn components(channels: u8) -> usize {
match channels {
1 => 1,
2 => 2,
_ => 4,
}
}
pub use horizon_render_pipeline::textures::{
CustomTexture, Dim, EVERY_STAGE, Texels, Texture, TextureSource, components,
};
/// Every `texture.*` and `customTexture.*` entry, read and decoded, and a note for each that
/// failed.
@@ -13,21 +13,11 @@ use std::collections::BTreeSet;
use std::fmt::Write;
use super::scan::{QUALIFIERS, STORAGE, significant};
use super::{Error, GbuffersInputs, Program, Target};
use super::{Error, Program, Target};
use crate::Preprocessed;
use crate::macros::lex;
/// How a geometry program's vertex invocations reach their triangle's vertices.
#[derive(Clone, Debug, Default)]
pub struct GeometryInputs {
/// The game's vertex inputs, as `GbuffersInputs`; their `layout(location = n) in`
/// declarations become plain globals the pull sets.
pub inputs: GbuffersInputs,
/// GLSL statements that set the vertex inputs from vertex `hz_vertex_index` (a `uint`).
pub pull: String,
/// The vertex of the `@`th corner of the draw (a `uint` expression, `hz_indices[@]`).
pub index: String,
}
pub use horizon_render_pipeline::program::GeometryInputs;
/// Translates `vertex`, `geometry` and `fragment` (a shadow program): the program, and how many
/// triangles the geometry stage can emit (the instances to draw).
@@ -601,6 +591,7 @@ fn strip_storage(text: &mut [String], tokens: &[&str], range: std::ops::Range<us
#[cfg(test)]
mod tests {
use super::*;
use crate::GbuffersInputs;
use crate::pack::tests::zip;
use crate::{Macros, Pack, preprocess};
+4 -182
View File
@@ -44,188 +44,10 @@ pub use geometry::{GeometryInputs, translate_geometry};
use scan::Stage;
/// What a program's stages need from the game.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Interface {
/// The uniform block at binding 0: its size and each value's name, GLSL type and offset.
/// Ours come first: `hz_target` (the target's width, height, 1/width, 1/height), and the
/// fixed-function fog the compatibility `gl_Fog` reads (OQ-28): `hz_fog_color` (vanilla's fog
/// colour) and `hz_fog` (density, start, end, 1 / (end - start)), then `hz_depth_linear`:
/// 1 where `shadowtex0`, `shadowtex1`, `shadow` and `watershadow` (x to w) read through a
/// plain sampler filter linearly, 0 where they take the nearest texel.
pub uniform_size: u32,
pub uniforms: Vec<Uniform>,
/// std140 initial contents; runtime values overwrite their members each frame.
pub initial_values: Vec<u8>,
/// Each sampler: texture at `binding`, sampler object at `binding + 1`.
pub samplers: Vec<Sampler>,
/// The fragment stage's output locations.
pub outputs: Vec<u32>,
/// A gbuffers program reads `DRAW_UNIFORMS`: the block at set 2, binding 0, set per draw.
pub draw: bool,
/// Storage images (`colorimg4`...), after the samplers.
pub images: Vec<Image>,
/// The SSBOs' bindings in set 4 (`bufferObject.<n>`).
pub buffers: Vec<u32>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Image {
pub name: String,
/// `image2D`, `uimage3D`...
pub ty: String,
/// The format qualifier (`rgba16f`).
pub format: String,
/// `readonly`, `writeonly`, empty for both, or `atomic` for an image an atomic function
/// uses.
pub access: String,
pub binding: u32,
/// A compute program's copy of image `before` as it was before the dispatch, which its
/// loads read (see `ordering`).
pub before: Option<String>,
}
/// A compute program (`deferred4_a.csh`): its module, what it reads and writes, and how many
/// work groups it runs: `workGroups`, or `workGroupsRender` times the target's size over the
/// local size (1, 1 when neither is set).
#[derive(Clone)]
pub struct ComputeProgram {
pub module: naga::Module,
pub interface: Interface,
/// Non-fatal translation diagnostics, located in the authored source.
pub notes: Vec<Error>,
pub work_groups: Option<[u32; 3]>,
pub work_groups_render: [f32; 2],
}
/// The uniforms a gbuffers program reads per draw (5.3's "D"), in a block of their own
/// (`HzDraw`, std140: four ints, a vec4; 32 bytes) the game sets for each draw.
pub const DRAW_UNIFORMS: [(&str, &str); 5] = [
("renderStage", "int"),
("entityId", "int"),
("blockEntityId", "int"),
("currentRenderedItemId", "int"),
("entityColor", "vec4"),
];
#[derive(Clone, Debug, PartialEq)]
pub struct Uniform {
pub name: String,
/// The type the pack declared (`float`, `ivec2`, `mat4`; `bool` is stored as an `int`).
pub ty: String,
pub offset: u32,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Sampler {
pub name: String,
/// The type the pack declared: `sampler2D`, `sampler2DShadow`, `usampler2D`, `sampler3D`...
pub ty: String,
pub binding: u32,
}
/// A translation diagnostic at a pack file and line.
#[derive(Clone, Debug, PartialEq)]
pub struct Error {
pub file: String,
pub line: u32,
pub message: String,
}
#[derive(Clone)]
pub struct Program {
pub vertex: naga::Module,
pub fragment: naga::Module,
pub interface: Interface,
/// Non-fatal translation diagnostics, located in the authored source.
pub notes: Vec<Error>,
}
/// Where the program draws, which decides the conventions of 5.5.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Target {
/// Pack buffers: rows stored as GL stores them.
Buffers,
/// The window (`final`).
Window,
}
/// What a gbuffers-style program's vertex stage reads. The game writes it: it knows its vertex
/// formats, the translator only places it.
#[derive(Clone, Debug, Default)]
pub struct GbuffersInputs {
/// GLSL declarations: the vertex inputs and any bindings of set 1.
pub declarations: String,
/// GLSL statements that set `hz_Vertex`, `hz_Color`, `hz_MultiTexCoord0`,
/// `hz_MultiTexCoord1` and `hz_Normal` (the compatibility inputs), run first in `main`.
pub body: String,
/// The value of each attribute a pack may declare (`mc_Entity`, `vaPosition`...), as a GLSL
/// expression of the declared type's size; an attribute not listed reads zero. Terrain
/// also supplies `mc_chunkFade`, an implicit float input rather than a declared attribute.
pub attributes: Vec<(String, String)>,
/// GLSL statements run last in `main`, with `gl_Position` in wgpu's clip space.
pub after: String,
/// The alpha test applied to output 0 (`gl_FragData[0]`, `gl_FragColor`), the loader's
/// (3.2 `alphaTest.<program>`): `None` keeps every fragment.
pub alpha_test: Option<AlphaTest>,
/// The program may read `currentRenderedItemId` without declaring it, as Iris 1.11.7
/// lets the hand's (observed with probe packs; terrain's may not).
pub item_id_declared: bool,
/// The depth test sees the far plane (window depth 1) unless the program writes
/// `gl_FragDepth`, while `gl_FragCoord.z` stays the geometry's own: the sky shows only where
/// nothing else drew, and reads its own depth as in Iris 1.11.7.
pub far_depth: bool,
/// `normals` and `specular` are bound as paged maps of the atlas `texture` reads
/// (`horizon-client-render`'s `paged_maps.rs`), not as images of its layout: the program's
/// texture calls on them read through `hz_pagedTexture`.
pub paged_maps: bool,
/// Translucent terrain: a fragment behind the boats' water patches (`horizonWaterMask`, the
/// patches' depth, 1 where there are none) is discarded, so no water shows inside a hull
/// (`RenderTypes.waterMask`), while the frame's depth stays the pack's own.
pub water_mask: bool,
/// The shadow pass's translucent terrain: a fragment inside a boat's water patch
/// (`horizonWaterPatch*`, camera-relative boxes) is discarded, so no water casts its light
/// into a hull. Not with a geometry stage.
pub water_patches: bool,
}
/// The sampler of the boats' water patches' depth (`GbuffersInputs::water_mask`).
pub const WATER_MASK: &str = "horizonWaterMask";
/// How many boats' water patches the shadow pass keeps water out of (`GbuffersInputs::water_patches`).
pub const WATER_PATCHES: usize = 16;
/// A fragment stays when its output 0's alpha compares to `reference` as `op` says (a GLSL
/// comparison operator, `true` for GL's `ALWAYS`, `false` for `NEVER`).
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AlphaTest {
pub op: &'static str,
pub reference: f32,
}
impl AlphaTest {
/// `alphaTest.<program>`'s value: `off`, or a GL comparison and a reference.
pub fn parse(value: &str) -> Option<Option<Self>> {
let mut words = value.split_whitespace();
let func = words.next()?;
if func == "off" {
return Some(None);
}
let op = match func.trim_start_matches("GL_") {
"NEVER" => "false",
"LESS" => "<",
"EQUAL" => "==",
"LEQUAL" => "<=",
"GREATER" => ">",
"NOTEQUAL" => "!=",
"GEQUAL" => ">=",
"ALWAYS" => return Some(None),
_ => return None,
};
let reference = words.next()?.parse().ok()?;
Some(Some(Self { op, reference }))
}
}
pub use horizon_render_pipeline::program::{
AlphaTest, ComputeProgram, DRAW_UNIFORMS, Error, GbuffersInputs, Image, Interface, Program,
Sampler, Target, Uniform, WATER_MASK, WATER_PATCHES,
};
/// Translates a composite-style program (`composite`, `deferred`, `final`...).
pub fn translate_composite(
+9 -1
View File
@@ -2841,11 +2841,19 @@ dependencies = [
]
[[package]]
name = "horizon-shader-api"
name = "horizon-render-pipeline"
version = "0.1.0"
dependencies = [
"bytemuck",
"naga",
]
[[package]]
name = "horizon-shader-api"
version = "0.1.0"
dependencies = [
"horizon-render-pipeline",
"naga",
"png 0.18.1",
"serde",
"serde_json",