mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-08 17:19:10 -10:00
Merge #5928 into the shader API union
This commit is contained in:
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(),
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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.
|
||||
Reference in new issue
Block a user