Merge #5928 into the shader API union

This commit is contained in:
Ludovic Marie committed 2026-11-01 14:27:09 +01:00
commit 47b5a77bce
13 files changed
+248 -30

No files matched your search

@@ -42,7 +42,7 @@ struct HorizonMaterialOut {
}
fn horizon_material_vertex(position: vec3<f32>, uv: vec2<f32>, layer: u32, color: vec4<f32>,
light: vec2<u32>, origin: vec3<f32>) -> HorizonMaterialOut {
let pos = position + origin;
let pos = horizon_material_displace(position + origin, uv, layer, color, light);
var out: HorizonMaterialOut;
out.clip = horizon_terrain_globals.view_proj * vec4(pos, 1.0);
out.uv = uv;
@@ -220,7 +220,7 @@ fn vanilla_terrain() -> String {
/// The function `name(layer)`, the value of each sprite layer in `table` and 0 for the rest:
/// `horizon_sprite_tags` (`MaterialInput.tags`) and `horizon_sprite_material`
/// (`MaterialInput.material`).
fn layer_table(name: &str, table: &[(u32, u32)]) -> String {
pub(super) fn layer_table(name: &str, table: &[(u32, u32)]) -> String {
let mut out = format!("fn {name}(layer: u32) -> u32 {{\n switch layer {{\n");
let mut table = table.to_vec();
table.sort_unstable_by_key(|&(layer, value)| (value, layer));
@@ -235,16 +235,47 @@ fn layer_table(name: &str, table: &[(u32, u32)]) -> String {
out + " default: { return 0u; }\n }\n}\n"
}
/// The terrain shader for the pack's material stage `source`, with its sprite `tags` and
/// `materials`.
/// Where a corner of terrain is drawn without a vertex stage.
pub(super) const DISPLACE_VANILLA: &str = "
fn horizon_material_displace(pos: vec3<f32>, uv: vec2<f32>, layer: u32, color: vec4<f32>,
light: vec2<u32>) -> vec3<f32> {
return pos;
}
";
/// The vertex stage's call, with its inputs from terrain's vertex.
pub(super) const DISPLACE_PACK: &str = "
fn horizon_material_displace(pos: vec3<f32>, uv: vec2<f32>, layer: u32, color: vec4<f32>,
light: vec2<u32>) -> vec3<f32> {
let flags = u32(round(color.a * 255.0));
let high = flags >> 4u;
let fluid = high >= 8u && (high & 7u) < 6u;
let lit = min(vec2(f32(light.y), f32(light.x)) / 240.0, vec2(1.0));
return vertex(VertexInput(pos, uv, horizon_sprite_material(layer & 0x7fffu), lit,
vec4(color.rgb, 1.0), select(0u, high & 7u, fluid), u32(fluid)));
}
";
/// The surface target (the post stage's `scene_surface`): x the normal (two snorm16 in its
/// bits), y smoothness, z the pack's data, w the fragment's depth.
pub const SURFACE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba32Float;
fn shader_source(source: &str, tags: &[(u32, u32)], materials: &[(u32, u32)]) -> String {
/// The terrain shader for the pack's material stage `source`, with its sprite `tags` and
/// `materials`, and the vertex stage in its module when `vertex`.
fn shader_source(
source: &str,
tags: &[(u32, u32)],
materials: &[(u32, u32)],
vertex: bool,
) -> String {
format!(
"{source}\n// Horizon's terrain (terrain.wgsl)\n{}{}{}{ENTRIES}",
"{source}\n// Horizon's terrain (terrain.wgsl)\n{}{}{}{}{ENTRIES}",
vanilla_terrain(),
if vertex {
DISPLACE_PACK
} else {
DISPLACE_VANILLA
},
layer_table("horizon_sprite_tags", tags),
layer_table("horizon_sprite_material", materials)
)
@@ -255,8 +286,9 @@ fn module(
stage: &CheckedStage,
tags: &[(u32, u32)],
materials: &[(u32, u32)],
vertex: bool,
) -> Result<naga::Module, String> {
let text = shader_source(&stage.source, tags, materials);
let text = shader_source(&stage.source, tags, materials, vertex);
naga::front::wgsl::parse_str(&text)
.map_err(|e| format!("{}: {}", stage.file, e.emit_to_string(&text)))
}
@@ -270,9 +302,10 @@ impl NativeMaterial {
stage: &CheckedStage,
constants: &[(String, f64)],
fog: bool,
vertex: bool,
) -> Result<Self, String> {
let writes_surface = horizon_shader_api::stages::calls_set_surface(&stage.source);
let module = module(stage, shape.sprite_tags, shape.sprite_materials)?;
let module = module(stage, shape.sprite_tags, shape.sprite_materials, vertex)?;
let mut constants = present_constants(&module, constants);
let sprite_names = [
"horizon_terrain_SPRITE_ARRAYS",
@@ -459,7 +492,7 @@ mod tests {
let tags = [(7, 1), (3, 1), (9, 2)];
let materials = [(5, 1000)];
assert!(layer_table("t", &tags).contains("case 3u, 7u: { return 1u; }"));
let module = module(&stage, &tags, &materials).unwrap();
let module = module(&stage, &tags, &materials, false).unwrap();
let info = naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
naga::valid::Capabilities::all(),
@@ -468,7 +501,7 @@ mod tests {
.unwrap_or_else(|e| {
panic!(
"{}",
e.emit_to_string(&shader_source(&stage.source, &tags, &materials))
e.emit_to_string(&shader_source(&stage.source, &tags, &materials, false))
)
});
let entries: Vec<&str> = module
@@ -48,7 +48,17 @@ impl NativePack {
let shadow = report
.shadow
.as_ref()
.map(|s| NativeShadow::new(device, terrain, &frame, &s.solid_materials))
.map(|s| {
let vertex = report.stages.iter().find(|s| s.kind == StageKind::Vertex);
NativeShadow::new(
device,
terrain,
&frame,
&s.solid_materials,
vertex,
constants,
)
})
.transpose()?;
// The mirror is drawn for a material that reads it, while `[reflection] option` is on.
let option = report.reflection.as_ref().and_then(|r| r.option.as_ref());
@@ -91,6 +101,7 @@ impl NativePack {
.iter()
.find(|s| s.kind == StageKind::Weather)
.map(|s| s.label.clone());
let has_vertex = report.stages.iter().any(|s| s.kind == StageKind::Vertex);
let (post, others): (Vec<_>, Vec<_>) = report
.stages
.into_iter()
@@ -103,7 +114,9 @@ impl NativePack {
let material = others
.iter()
.find(|s| s.kind == StageKind::Material)
.map(|stage| NativeMaterial::new(device, terrain, &frame, stage, constants, fog))
.map(|stage| {
NativeMaterial::new(device, terrain, &frame, stage, constants, fog, has_vertex)
})
.transpose()?;
let post = (!post.is_empty())
.then(|| NativePost::new(device, post, &report.manifest, constants, format, &frame))
@@ -41,17 +41,21 @@ fn horizon_shadow_vertex(pos: vec3<f32>, uv: vec2<f32>, layer: u32, color: vec4<
}
@vertex fn horizon_shadow_vs(
@location(0) position: vec3<f32>, @location(1) uv: vec2<u32>,
@location(2) layer: u32, @location(3) color: vec4<f32>,
@location(2) layer: u32, @location(3) color: vec4<f32>, @location(4) light: vec2<u32>,
@location(5) origin: vec4<f32>) -> HorizonShadowOut {
return horizon_shadow_vertex(position + origin.xyz, horizon_terrain_terrain_uv(uv, origin.w), layer, color);
let atlas = horizon_terrain_terrain_uv(uv, origin.w);
let pos = horizon_material_displace(position + origin.xyz, atlas, layer, color, light);
return horizon_shadow_vertex(pos, atlas, layer, color);
}
@vertex fn horizon_shadow_vs_flat(
@builtin(vertex_index) index: u32,
@location(0) position: vec3<f32>, @location(1) uv: vec2<u32>, @location(2) layer: u32,
@location(3) color: vec4<f32>
@location(3) color: vec4<f32>, @location(4) light: vec2<u32>
) -> HorizonShadowOut {
let origin = horizon_terrain_slot_origins[horizon_terrain_block_slots[index >> 8u]];
return horizon_shadow_vertex(position + origin.xyz, horizon_terrain_terrain_uv(uv, origin.w), layer, color);
let atlas = horizon_terrain_terrain_uv(uv, origin.w);
let pos = horizon_material_displace(position + origin.xyz, atlas, layer, color, light);
return horizon_shadow_vertex(pos, atlas, layer, color);
}
@fragment fn horizon_shadow_fs(in: HorizonShadowOut) {
// Bit 15 of the layer marks opaque leaves (forceOpaque): no alpha cutout.
@@ -410,12 +414,24 @@ fn shadow_solid(materials: &[(u32, u32)], solid: &[u32]) -> String {
/// The shadow pass's shader: the frame, Horizon's terrain, and the entries, with the layers
/// `solid` of `shadow_solid`.
fn shader_source(solid: &str) -> String {
fn shader_source(solid: &str, vertex: Option<&str>, materials: &[(u32, u32)]) -> String {
let (head, displace) = match vertex {
Some(stage) => (
stage.to_owned()
+ &super::native_material::layer_table("horizon_sprite_material", materials),
super::native_material::DISPLACE_PACK,
),
None => (
format!(
"{}@group({GROUP}) @binding(0) var<uniform> frame: Frame;\n{}",
FrameInputs::WGSL.wgsl(),
horizon_shader_api::stages::SHADOW_HELPERS,
),
super::native_material::DISPLACE_VANILLA,
),
};
format!(
"{}@group({GROUP}) @binding(0) var<uniform> frame: Frame;\n{}\
// Horizon's terrain (terrain.wgsl)\n{}{solid}{ENTRIES}",
FrameInputs::WGSL.wgsl(),
horizon_shader_api::stages::SHADOW_HELPERS,
"{head}// Horizon's terrain (terrain.wgsl)\n{}{solid}{displace}{ENTRIES}",
prefixed(
section(TERRAIN, "struct Globals {", "// Sky (SkyRenderer)"),
PREFIX
@@ -429,14 +445,21 @@ impl NativeShadow {
shape: &TerrainShape<'_>,
frame: &super::native_post::NativeFrame,
solid_materials: &[u32],
vertex: Option<&horizon_shader_api::check::CheckedStage>,
constants: &[(String, f64)],
) -> Result<Self, String> {
let text = shader_source(&shadow_solid(shape.sprite_materials, solid_materials));
let text = shader_source(
&shadow_solid(shape.sprite_materials, solid_materials),
vertex.map(|s| s.source.as_str()),
shape.sprite_materials,
);
let module = naga::front::wgsl::parse_str(&text)
.map_err(|e| format!("shadow pass: {}", e.emit_to_string(&text)))?;
let constants = [
let mut constants = super::native_post::present_constants(&module, constants);
constants.extend([
("horizon_terrain_SPRITE_ARRAYS", shape.sprites[0]),
("horizon_terrain_SPRITE_SPLIT", shape.sprites[1]),
];
]);
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Native shader pack shadow"),
@@ -588,7 +611,7 @@ mod tests {
#[test]
fn the_shadow_shader_validates() {
for materials in [&[][..], &[(3, 7), (4, 8), (9, 7)][..]] {
let text = shader_source(&shadow_solid(materials, &[7]));
let text = shader_source(&shadow_solid(materials, &[7]), None, materials);
let module = naga::front::wgsl::parse_str(&text).unwrap();
naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
+1
View File
@@ -160,6 +160,7 @@ pub fn check(pack: &Pack) -> Result<Report, Vec<Problem>> {
.sky
.iter()
.map(|s| (StageKind::Sky, s))
.chain(m.vertex.iter().map(|s| (StageKind::Vertex, s)))
.chain(m.material.iter().map(|s| (StageKind::Material, s)))
.chain(m.weather.iter().map(|s| (StageKind::Weather, s)))
.chain(m.post.iter().map(|s| (StageKind::Post, s)));
+15
View File
@@ -265,6 +265,21 @@ pub const SKY_INPUT: Struct = Struct {
},
};
/// 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",
+20 -1
View File
@@ -43,6 +43,8 @@ pub struct Manifest {
pub shared: Vec<String>,
pub sky: Option<Stage>,
pub material: Option<Stage>,
/// Where corners of terrain are drawn: needs `material`, and shares its module.
pub vertex: Option<Stage>,
/// The colour of rain and snow streaks (Minecraft's own when absent).
pub weather: Option<Stage>,
/// Full-screen passes, in the order they run.
@@ -324,6 +326,8 @@ struct Raw {
sky: Option<Stage>,
material: Option<Stage>,
#[serde(default)]
vertex: Option<Stage>,
#[serde(default)]
weather: Option<Stage>,
#[serde(default)]
post: Vec<Stage>,
@@ -474,13 +478,25 @@ impl Manifest {
"no stage: give it a [sky], a [material], a [weather] or a [[post]]".into(),
));
}
if raw.sky.iter().chain(&raw.post).any(|s| s.fog.is_some()) {
if raw.vertex.is_some() && raw.material.is_none() {
return Err(err(
"[vertex] needs a [material]: they draw terrain together".into(),
));
}
if raw
.sky
.iter()
.chain(&raw.vertex)
.chain(&raw.post)
.any(|s| s.fog.is_some())
{
return Err(err("`fog` is a key of [material] only".into()));
}
if raw
.sky
.iter()
.chain(&raw.material)
.chain(&raw.vertex)
.any(|s| s.scale.is_some() || s.output.is_some())
{
return Err(err("`scale` and `output` are keys of [[post]] only".into()));
@@ -496,6 +512,7 @@ impl Manifest {
.sky
.iter()
.chain(&raw.material)
.chain(&raw.vertex)
.chain(&raw.weather)
.chain(&raw.post);
for file in raw
@@ -702,6 +719,7 @@ impl Manifest {
shared: raw.shared,
sky: raw.sky,
material: raw.material,
vertex: raw.vertex,
weather: raw.weather,
post: raw.post,
options,
@@ -814,6 +832,7 @@ impl Manifest {
.sky
.iter()
.chain(&self.material)
.chain(&self.vertex)
.chain(&self.weather)
.chain(&self.post);
for file in self
@@ -45,6 +45,7 @@ pub fn markdown() -> String {
StageKind::Post => "[[post]]",
StageKind::Sky => "[sky]",
StageKind::Material => "[material]",
StageKind::Vertex => "[vertex]",
StageKind::Weather => "[weather]",
};
out += &format!("| {} | `{key}` | `{}` |\n", kind.name(), kind.signature());
+39 -4
View File
@@ -2,7 +2,9 @@
//! 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::{Frame, MATERIAL_INPUT, POST_INPUT, SKY_INPUT, SURFACE, Struct, WEATHER_INPUT};
use crate::inputs::{
Frame, MATERIAL_INPUT, POST_INPUT, SKY_INPUT, SURFACE, Struct, VERTEX_INPUT, WEATHER_INPUT,
};
use crate::package::Pack;
/// The bind group of a pack's resources; groups 0 to 2 are the renderer's.
@@ -14,6 +16,9 @@ pub enum StageKind {
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.
@@ -439,6 +444,16 @@ struct HorizonSkyIn {
/// 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);
}
";
const WEATHER_ENTRIES: &str = "\
struct HorizonWeatherIn {
@builtin(position) position: vec4<f32>,
@@ -453,18 +468,20 @@ struct HorizonWeatherIn {
";
impl StageKind {
pub const ALL: [StageKind; 4] = [
pub const ALL: [StageKind; 5] = [
StageKind::Sky,
StageKind::Vertex,
StageKind::Material,
StageKind::Weather,
StageKind::Post,
];
/// The manifest's name for the stage (`[sky]`, `[material]`, `[weather]`, `[[post]]`).
/// The manifest's name for the stage (`[sky]`, `[vertex]`, `[material]`, `[weather]`, `[[post]]`).
pub fn name(self) -> &'static str {
match self {
StageKind::Sky => "sky",
StageKind::Material => "material",
StageKind::Vertex => "vertex",
StageKind::Weather => "weather",
StageKind::Post => "post",
}
@@ -479,6 +496,7 @@ impl StageKind {
match self {
StageKind::Sky => &SKY_INPUT,
StageKind::Material => &MATERIAL_INPUT,
StageKind::Vertex => &VERTEX_INPUT,
StageKind::Weather => &WEATHER_INPUT,
StageKind::Post => &POST_INPUT,
}
@@ -488,6 +506,7 @@ impl StageKind {
pub fn output(self) -> &'static str {
match self {
StageKind::Material => SURFACE.name,
StageKind::Vertex => "vec3<f32>",
StageKind::Sky | StageKind::Weather | StageKind::Post => "vec4<f32>",
}
}
@@ -505,7 +524,7 @@ impl StageKind {
pub fn resources(self) -> &'static [Resource] {
match self {
StageKind::Post => &POST_RESOURCES,
StageKind::Sky | StageKind::Weather => std::slice::from_ref(&FRAME),
StageKind::Sky | StageKind::Vertex | StageKind::Weather => std::slice::from_ref(&FRAME),
StageKind::Material => &MATERIAL_RESOURCES,
}
}
@@ -518,6 +537,8 @@ impl StageKind {
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();
}
for r in self.resources() {
out += &format!("@group({GROUP}) @binding({}) {};\n", r.binding, r.ty);
@@ -532,6 +553,7 @@ impl StageKind {
out += MATERIAL_HELPERS;
out += SHADOW_HELPERS;
}
StageKind::Vertex => out += SHADOW_HELPERS,
StageKind::Sky | StageKind::Weather => {}
}
out
@@ -542,6 +564,7 @@ impl StageKind {
match self {
StageKind::Sky => SKY_ENTRIES,
StageKind::Material => MATERIAL_ENTRIES,
StageKind::Vertex => VERTEX_ENTRIES,
StageKind::Weather => WEATHER_ENTRIES,
StageKind::Post => POST_ENTRIES,
}
@@ -641,6 +664,7 @@ pub fn compose(pack: &Pack, kind: StageKind, file: &str) -> Composed {
let stage = match kind {
StageKind::Sky => m.sky.as_ref(),
StageKind::Material => m.material.as_ref(),
StageKind::Vertex => m.vertex.as_ref(),
StageKind::Weather => m.weather.as_ref(),
StageKind::Post => m.post.iter().find(|s| s.file == file),
};
@@ -650,6 +674,17 @@ pub fn compose(pack: &Pack, kind: StageKind, file: &str) -> Composed {
}
}
push(file, pack.source(file).unwrap_or_default());
// They draw terrain together: the vertex stage's function is in the material's module.
if kind == StageKind::Material
&& let Some(vertex) = &m.vertex
{
for extra in vertex.files.iter().chain(std::iter::once(&vertex.file)) {
let own = stage.is_some_and(|s| s.files.contains(extra));
if !own && !m.shared.contains(extra) {
push(extra, pack.source(extra).unwrap_or_default());
}
}
}
push("<entries>", kind.entries());
composed
}
+25
View File
@@ -193,6 +193,31 @@ fn a_stages_files_are_its_own() {
check(&pack).unwrap();
}
#[test]
fn a_vertex_stage_checks_alone_and_joins_the_material() {
let mut files = sample().files;
let manifest = String::from_utf8(files[MANIFEST].clone()).unwrap();
files.insert(
MANIFEST.into(),
format!("{manifest}\n[vertex]\nfile = \"wave.wgsl\"\n").into_bytes(),
);
files.insert(
"wave.wgsl".into(),
b"fn vertex(input: VertexInput) -> vec3<f32> {
return input.position + vec3(0.0, sin(frame.time) * f32(input.material), 0.0);
}
"
.to_vec(),
);
let report = check(&Pack::from_files(files).unwrap()).unwrap();
let source = |kind| {
let stage = report.stages.iter().find(|s| s.kind == kind).unwrap();
stage.source.contains("fn vertex(input: VertexInput)")
};
assert!(source(StageKind::Vertex));
assert!(source(StageKind::Material));
}
#[test]
fn a_material_reads_its_block_texture() {
let mut files = sample().files;
@@ -31,6 +31,9 @@ fn vanilla(kind: StageKind) -> &'static str {
"fn material(input: MaterialInput) -> Surface {\n \
return Surface(input.albedo * input.tint * vec4<f32>(input.lightmap, 1.0));\n}\n"
}
StageKind::Vertex => {
"fn vertex(input: VertexInput) -> vec3<f32> { return input.position; }\n"
}
StageKind::Weather => {
"fn weather(input: WeatherInput) -> vec4<f32> { return input.color; }\n"
}
+15
View File
@@ -612,6 +612,21 @@ fn post_outputs_are_named_scaled_and_never_the_last_pass() {
}
}
#[test]
fn a_vertex_stage_needs_a_material() {
let head = "schema = 1\nid = \"a\"\nname = \"A\"\nversion = \"1\"\napi = \"0.1\"\n";
let e = Manifest::parse(&format!(
"{head}[vertex]\nfile = \"v.wgsl\"\n[[post]]\nfile = \"p.wgsl\"\n"
))
.unwrap_err();
assert!(e.to_string().contains("needs a [material]"), "{e}");
let m = Manifest::parse(&format!(
"{head}[material]\nfile = \"m.wgsl\"\n[vertex]\nfile = \"v.wgsl\"\n"
))
.unwrap();
assert_eq!(m.vertex.unwrap().file, "v.wgsl");
}
#[test]
fn materials_name_blocks_and_move_sprites_to_the_translucent_pass() {
let m = Manifest::parse(concat!(
+31
View File
@@ -12,6 +12,7 @@ bindings and no entry points, and no names starting with `horizon_`.
| Stage | In `horizon-shaders.toml` | Your function |
| --- | --- | --- |
| sky | `[sky]` | `fn sky(input: SkyInput) -> vec4<f32>` |
| vertex | `[vertex]` | `fn vertex(input: VertexInput) -> vec3<f32>` |
| material | `[material]` | `fn material(input: MaterialInput) -> Surface` |
| weather | `[weather]` | `fn weather(input: WeatherInput) -> vec4<f32>` |
| post | `[[post]]` | `fn post(input: PostInput) -> vec4<f32>` |
@@ -35,6 +36,36 @@ Resources (bind group 3):
| --- | --- | --- |
| 0 | `var<uniform> frame: Frame` | The frame (`Frame`). |
## vertex
`fn vertex(input: VertexInput) -> vec3<f32>`
### `VertexInput`
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).
| Field | Type | Meaning |
| --- | --- | --- |
| `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. |
Resources (bind group 3):
| Binding | Declaration | Meaning |
| --- | --- | --- |
| 0 | `var<uniform> frame: Frame` | The frame (`Frame`). |
Helpers:
- `fn shadow_position(position: vec3<f32>) -> vec3<f32>`: where the camera-relative point `position` is
stored in the shadow map, for `textureSampleCompareLevel`.
- `fn shadow_distort(xy: vec2<f32>) -> vec2<f32>`: the map's distortion of a point at shadow clip `xy`.
## material
`fn material(input: MaterialInput) -> Surface`
+4
View File
@@ -60,6 +60,9 @@ file = "material.wgsl"
files = ["pbr.wgsl"] # optional, on any stage: WGSL put after `shared`, before this stage's file
fog = false # optional: the pack fogs its surfaces itself (Horizon does when absent or true)
[vertex] # needs [material]: where corners of terrain are drawn, for the world and the shadow map; shares the material's module
file = "vertex.wgsl"
[weather] # the colour of rain and snow streaks
file = "weather.wgsl"
@@ -201,3 +204,4 @@ stages.
- 2026-11-01: API 0.1 (additions): the material stage's `block_texture`, `block_texture_level`, `block_texture_size` and `block_texture_lod`, the surface's own sprite anywhere on it. The LabPBR `_level` functions now count the block texture's mip levels, the same footprint where a map is smaller.
- 2026-11-01: API 0.1 (additions): the material stage's `set_surface` and the post stage's `surface_at` and `scene_surface`: a normal, a smoothness and a value of the pack's own per pixel of solid terrain. Only a material stage that calls `set_surface` pays for the target.
- 2026-11-01: API 0.1 (additions): `[[materials]] blocks`, ids by block, and `translucent`, moving an id's sprites into the translucent pass. `sprites` may be left out when `blocks` is there.
- 2026-11-01: API 0.1 (additions): the `[vertex]` stage, `fn vertex(input: VertexInput) -> vec3<f32>`: waving plants and rolling water, in the world and the shadow map.