// A far tree: one quad that turns about the trunk to face the camera and shows
// the baked view nearest to where the camera looks from, lit by the scene's own lights. The Rust
// side is `impostor/mod.rs` (the quad, the atlases) and `impostor/material.rs`; where the quad
// stands is `impostor_common.wesl`, which the shadow pass (`impostor_prepass.wesl`) shares.
import bevy_pbr::render::{
mesh_functions,
forward_io::{Vertex, VertexOutput, FragmentOutput, decompress_vertex},
view_transformations::position_world_to_clip,
mesh_view_bindings::{view, globals},
pbr_types,
pbr_functions::{apply_pbr_lighting, main_pass_post_lighting_processing, calculate_view},
mesh_types::MESH_FLAGS_SHADOW_RECEIVER_BIT,
};
import package::shading::common::{
flora, flora_wind, wind_bend, shake_at, oct_decode, transmitted_light, leaf_gloss_toward_light, toon_lighting, toon_rim,
snow_on, snow_shelter, wet_on, LEAF_FROST, tree_condition, condition_defoliation, condition_withering,
condition_own_withering, tags_read,
};
import package::impostor::impostor_common::impostor_corner;
@if(VISIBILITY_RANGE_DITHER)
import bevy_pbr::render::pbr_functions::visibility_range_dither;
@if(FLORA_SEE_THROUGH)
import package::shading::common::see_through;
@if(FLORA_FIRE)
import package::shading::common::{
fire_at, leaf_burn_front, ember_flicker, ember_glow, LEAF_GLOW, LEAF_SCORCH, SCORCHED, CHARCOAL, EMBER, EMBER_NITS,
};
@if(FLORA_GLIDE)
import package::shading::common::{gliding, glide_tint};
@group(constants::MATERIAL_BIND_GROUP) @binding(0) var albedo: texture_2d<f32>;
@group(constants::MATERIAL_BIND_GROUP) @binding(1) var atlas_sampler: sampler;
@group(constants::MATERIAL_BIND_GROUP) @binding(2) var surface: texture_2d<f32>;
// x: views in the atlas, y: the render style, w: 1 for an unlit look.
@group(constants::MATERIAL_BIND_GROUP) @binding(3) var<uniform> params: vec4<f32>;
struct ImpostorTint {
wood: vec4<f32>,
leaf: vec4<f32>,
}
// Twice the look's linear colour of the wood and of the leaves.
@group(constants::MATERIAL_BIND_GROUP) @binding(4) var<uniform> look: ImpostorTint;
@vertex
fn vertex(vertex_no_morph: Vertex) -> VertexOutput {
var out: VertexOutput;
let vertex = decompress_vertex(vertex_no_morph, vertex_no_morph.instance_index);
let world_from_local = mesh_functions::get_world_from_local(vertex_no_morph.instance_index);
let origin = world_from_local[3].xyz;
// The quad faces the camera (its view's backward axis when it is orthographic) and is drawn
// at the front of the crown (`params.z`, the crown's reach), so the crown's near edge seen
// from above stands on the ground instead of under it.
let orthographic = view.clip_from_view[3].w == 1.0;
let corner = impostor_corner(
vertex.position.xy, world_from_local, view.world_position, view.world_from_view[2].xyz, orthographic, params.x,
params.z,
);
// The tree sways with the wind, and swings when shaken, like its meshes do.
let bent = wind_bend(corner.world, origin, globals.time, shake_at(origin, globals.time));
out.world_position = vec4<f32>(bent, 1.0);
out.position = position_world_to_clip(bent);
// The tree's own +Z in the world (its yaw): what the baked normals are relative to.
out.world_normal = normalize(vec3<f32>(world_from_local[2].x, 0.0, world_from_local[2].z));
@if(VERTEX_UVS_A)
out.uv = vec2<f32>((corner.tile + vertex.uv.x) / params.x, vertex.uv.y);
@if(VERTEX_OUTPUT_INSTANCE_INDEX)
out.instance_index = vertex_no_morph.instance_index;
@if(VISIBILITY_RANGE_DITHER)
out.visibility_range_dither = mesh_functions::get_visibility_range_dither_level(
vertex_no_morph.instance_index, world_from_local[3]);
return out;
}
// How much of a leafy texel a crown's open flowers cover, on a bare tree: a blossoming crown is
// a little more open than a leafy one.
const BLOSSOM_COVER: f32 = 0.8;
@fragment
fn fragment(in: VertexOutput) -> FragmentOutput {
@if(VISIBILITY_RANGE_DITHER)
visibility_range_dither(in.position, in.visibility_range_dither);
@if(FLORA_SEE_THROUGH)
see_through(in.world_position.xyz, in.position.xy, view.world_position);
var color = textureSample(albedo, atlas_sampler, in.uv);
let baked = textureSample(surface, atlas_sampler, in.uv);
// The picture is neutral grey (0.5): the look's colour, twice over, multiplies it into the
// wood's or the leaves' (baked translucency 1; wood 0), before the season recolours the leaves.
color = vec4<f32>(color.rgb * select(look.wood.rgb, look.leaf.rgb, baked.a > 0.5), color.a);
// The season (`FloraSeason`): the baked picture is summer. A deciduous tree's leaves (baked
// translucency 1, wood 0) turn their autumn colour in patches a few texels wide, each at its
// own moment, and fallen leaves thin the crown's coverage, so a bare tree keeps its wood.
let season = flora_wind[5];
let leafy = saturate(baked.a) * flora.autumn.w;
let spot = in.uv * vec2<f32>(textureDimensions(albedo)) / 3.0;
let moment = fract(sin(dot(floor(spot), vec2<f32>(12.9898, 78.233))) * 43758.547) * 0.8;
// Where one pixel spans several patches, one patch's moment would flicker as the view moves:
// take the share of the patches that have turned (their moments are spread evenly) instead.
let spread = saturate(max(fwidth(spot.x), fwidth(spot.y)) - 0.5);
let turned = mix(smoothstep(moment, moment + 0.2, season.x), saturate((season.x - 0.1) / 0.8), spread);
let turn = turned * leafy;
let summer = color;
// A gliding look (`RecolorPool::glide`): the wood, and the leaves that have not turned, take
// the look's blend; turned leaves keep the species' autumn colour.
var glide = vec3<f32>(1.0);
var tag = 0u;
if tags_read() {
tag = mesh_functions::get_tag(in.instance_index);
}
@if(FLORA_GLIDE)
if gliding() {
glide = glide_tint(tag, baked.a > 0.5);
}
// A sick or dying tree (`TreeCondition`, in the mesh's tag): its leafy texels wither toward the
// dry leaf colour (from whatever colour the season gives them) and the crown thins by the share
// of its leaves the class has lost, in patches like the season's (and, where a pixel spans
// several, the share of them), so a far crown shows the class the meshes show. A hull crown
// keeps its facets (`flora.tree.y`).
var wither = 0.0;
var thin = 0.0;
// Patches twice the season's width, about a leaf: a crown thinned in single leaf-sized gaps
// reads as leaves lost, where finer ones read as noise (`fwidth` stays out of the branch below).
let cell = spot / 2.0;
let cell_spread = saturate(max(fwidth(cell.x), fwidth(cell.y)) - 0.5);
let condition = tree_condition(tag);
if condition != 0u {
// Where the picture is leaf: the colour moves only where it is mostly leaf (a soft wood
// texel next to a leaf would flare, the ratio is large), the coverage thins wherever there
// is a trace of one, so the soft edge of a lost leaf goes with it.
let leaf_core = smoothstep(0.4, 0.9, baked.a);
let leaf_edge = saturate(baked.a * 4.0);
let own = fract(sin(dot(floor(cell), vec2<f32>(21.19, 47.83))) * 17317.371);
wither = condition_own_withering(condition_withering(condition), mix(own, 0.5, cell_spread)) * leaf_core;
let lost = condition_defoliation(condition) * select(1.0, 0.0, flora.tree.y > 0.5);
let draw = fract(sin(dot(floor(cell), vec2<f32>(63.1, 17.7))) * 9741.3);
thin = mix(step(draw, lost), lost, cell_spread) * leaf_edge;
}
let tint = mix(mix(glide, flora.autumn.rgb, turn), flora.withered.rgb, wither * flora.withered.w);
color = vec4<f32>(color.rgb * tint, color.a * (1.0 - season.y * leafy) * (1.0 - thin));
// A flowering tree (`FoliageParams::blossom`) wears its flowers in patches as they open, over
// its leaves or on the bare crown in early spring; the picture was baked without them.
let flower_spot = fract(sin(dot(floor(spot), vec2<f32>(39.346, 11.135))) * 24634.634);
let bloomed = mix(step(flower_spot, season.w), season.w, spread) * saturate(baked.a) * flora.blossom.w;
var flower_share = 0.0;
if bloomed > 0.0 {
let leaves = 1.0 - season.y * leafy;
flower_share = bloomed / max(bloomed + leaves, 1.0e-3);
color = vec4<f32>(
mix(color.rgb, summer.rgb * flora.blossom.rgb, flower_share),
max(color.a, summer.a * bloomed * BLOSSOM_COVER),
);
}
// The fire (`FloraFire`), as on the meshes: the leaves scorch and burn away in patches (each at
// its own moment, the share of them where a pixel spans several), the wood chars, and it glows
// while it burns.
var ember = vec3<f32>(0.0);
@if(FLORA_FIRE) {
let fire = fire_at(in.world_position.xyz);
if fire.x > 0.0 {
let leaves = saturate(baked.a);
let front = leaf_burn_front(fire.x);
let ahead = fract(sin(dot(floor(spot), vec2<f32>(63.7264, 10.873))) * 43758.547) - front;
let burnt = mix(step(ahead, 0.0), saturate(front), spread);
let scorched = mix(
1.0 - smoothstep(LEAF_GLOW, LEAF_GLOW + LEAF_SCORCH, ahead),
saturate((front + LEAF_GLOW + LEAF_SCORCH) / (LEAF_GLOW + LEAF_SCORCH)),
spread,
);
let charred = smoothstep(0.0, 0.4, fire.x) * (1.0 - leaves);
let rgb = mix(color.rgb, SCORCHED, scorched * leaves);
color = vec4<f32>(mix(rgb, CHARCOAL, charred), color.a * (1.0 - burnt * leaves));
// The meshes glow in their char's seams, about a tenth of the wood: as much on average.
let flicker = ember_flicker(in.world_position.xyz, globals.time);
ember = EMBER * EMBER_NITS * 0.1 * ember_glow(fire.y) * charred * flicker;
}
}
// Alpha is the share of the texel the tree covers, averaged down the mipmaps. With MSAA it
// becomes the share of the pixel's samples drawn (alpha to coverage), so a far crown is as
// airy as the leaves it was baked from: the sky shows through its gaps in the same
// proportion. It is not sharpened the way Bevy's materials sharpen it, which would turn a
// far mip's partial coverage back into all or nothing. Without MSAA it is cut at one half.
@if(ALPHA_TO_COVERAGE)
if color.a < 1.0 / 255.0 {
discard;
}
@else
if color.a < 0.5 {
discard;
}
// The baked normal is in the tree's frame: x across (its local +X in the world), y up,
// z along its local +Z.
let n = oct_decode(baked.rg * 2.0 - 1.0);
let z = normalize(in.world_normal);
let x = vec3<f32>(z.z, 0.0, -z.x);
let N = normalize(x * n.x + vec3<f32>(0.0, 1.0, 0.0) * n.y + z * n.z);
var p = pbr_types::pbr_input_new();
p.material.base_color = vec4<f32>(color.rgb, 1.0);
p.material.perceptual_roughness = 0.9;
p.material.flags = p.material.flags | pbr_types::STANDARD_MATERIAL_FLAGS_FOG_ENABLED_BIT;
p.frag_coord = in.position;
p.world_position = in.world_position;
p.world_normal = N;
p.N = N;
p.is_orthographic = view.clip_from_view[3].w == 1.0;
p.V = calculate_view(in.world_position, p.is_orthographic);
// Leaves (baked translucency 1; wood 0) lose their gloss facing the sun as the meshes' do; a
// hull crown's facets (`flora.tree.y`) are matte and have none to lose.
let leaf_gloss = select(saturate(baked.a), 0.0, flora.tree.y > 0.5);
p.material.reflectance = p.material.reflectance * mix(1.0, leaf_gloss_toward_light(p.V), leaf_gloss);
p.flags = MESH_FLAGS_SHADOW_RECEIVER_BIT;
// What the crown's own leaves keep from the sky and the sun.
p.diffuse_occlusion = vec3<f32>(baked.b);
p.directional_shadow_factor = baked.b;
// The rain, wood or leaf by the texel's baked translucency (wood 0; a leaf's own translucency,
// below 1, so anything above a trace is a leaf), then the season's snow on what faces up, by
// the baked (crown-bent) normal, as much as the texel sees of the sky (wood: 1), as the meshes
// take them.
wet_on(&p, saturate(baked.a * 20.0));
let snowed = snow_on(&p, N, snow_shelter(baked.b), LEAF_FROST * saturate(baked.a));
var out: FragmentOutput;
if params.w > 0.5 {
// An unlit look (`PoolColors::unlit`): the colour as the weather and the fire left it.
out.color = vec4<f32>(p.material.base_color.rgb, color.a);
} else {
// The material's `params.y` is the render style: 2 is toon.
let toon = params.y > 1.5;
if toon {
out.color = toon_lighting(p);
} else {
out.color = apply_pbr_lighting(p);
}
// Light through a flower takes its own colour, as on the meshes (`leaf_transmission`).
let tint = mix(flora.tint.rgb, vec3<f32>(1.0), flower_share);
let through = color.rgb * tint * flora.leaf.x * baked.a * (1.0 - snowed);
out.color = vec4<f32>(
out.color.rgb + view.exposure * transmitted_light(
p.world_position, N, N, p.V, p.frag_coord, p.flags, baked.b, through, baked.b
),
color.a,
);
if toon {
out.color = vec4<f32>(out.color.rgb * toon_rim(N, p.V), out.color.a);
}
}
@if(FLORA_FIRE)
out.color = vec4<f32>(out.color.rgb + view.exposure * ember, out.color.a);
out.color = main_pass_post_lighting_processing(p, out.color);
return out;
}