Fix varregion scene capture and legacy rendering
Some checks failed
CI / rust-skia (Rust only) (push) Has been cancelled
CI / required (push) Has been cancelled

This commit is contained in:
2026-08-23 10:18:19 +02:00
parent 19ebee129b
commit d85f7d22f2
23 changed files with 1313 additions and 516 deletions

View File

@@ -33,6 +33,9 @@ use std::{
time::Duration,
};
const WORLD_AMBIENT_BRIGHTNESS: f32 = 400.0;
const WORLD_ENVIRONMENT_BRIGHTNESS: f32 = 600.0;
enum RenderCommand {
Frame {
camera: Camera,
@@ -291,6 +294,7 @@ struct BevyBackend {
mesh_cache: HashMap<u64, Vec<CachedGpuMesh>>,
pbr_handles: Vec<Handle<PbrBevyMaterial>>,
legacy_handles: Vec<Handle<LegacyBevyMaterial>>,
environment_map: EnvironmentMapLight,
target: Option<Handle<Image>>,
}
@@ -314,6 +318,22 @@ impl BevyBackend {
let plugins = plugins.disable::<bevy::winit::WinitPlugin>();
app.add_plugins(plugins)
.add_plugins((LegacyMaterialPlugin, PbrMaterialPlugin));
app.insert_resource(GlobalAmbientLight {
color: Color::WHITE,
brightness: WORLD_AMBIENT_BRIGHTNESS,
..default()
});
let environment_map = {
let mut images = app.world_mut().resource_mut::<Assets<Image>>();
let mut light = EnvironmentMapLight::hemispherical_gradient(
&mut images,
Color::srgb_u8(150, 180, 220),
Color::srgb_u8(175, 175, 165),
Color::srgb_u8(95, 90, 70),
);
light.intensity = WORLD_ENVIRONMENT_BRIGHTNESS;
light
};
app.finish();
app.cleanup();
if !app.world().contains_resource::<RenderDevice>() {
@@ -327,6 +347,7 @@ impl BevyBackend {
mesh_cache: HashMap::new(),
pbr_handles: Vec::new(),
legacy_handles: Vec::new(),
environment_map,
target: None,
})
}
@@ -422,6 +443,7 @@ impl BevyBackend {
..Default::default()
}),
transform,
self.environment_map.clone(),
FrameEntity,
))
.id();
@@ -630,22 +652,29 @@ fn rgba_mip_chain(texture: &Texture) -> (Vec<u8>, u32) {
let mut next = vec![0_u8; next_width * next_height * 4];
for y in 0..next_height {
for x in 0..next_width {
let mut channels = [0_u32; 4];
let mut weighted_rgb = [0_u32; 3];
let mut alpha = 0_u32;
let mut samples = 0_u32;
for source_y in y * 2..(y * 2 + 2).min(height) {
for source_x in x * 2..(x * 2 + 2).min(width) {
let source = (source_y * width + source_x) * 4;
for channel in 0..4 {
channels[channel] += u32::from(level[source + channel]);
let sample_alpha = u32::from(level[source + 3]);
alpha += sample_alpha;
for channel in 0..3 {
weighted_rgb[channel] +=
u32::from(level[source + channel]) * sample_alpha;
}
samples += 1;
}
}
let target = (y * next_width + x) * 4;
for channel in 0..4 {
next[target + channel] = u8::try_from(channels[channel] / samples)
.expect("the average of bytes fits in a byte");
for channel in 0..3 {
next[target + channel] =
u8::try_from(weighted_rgb[channel].checked_div(alpha).unwrap_or(0))
.expect("the alpha-weighted average of bytes fits in a byte");
}
next[target + 3] =
u8::try_from(alpha / samples).expect("the average of bytes fits in a byte");
}
}
output.extend_from_slice(&next);
@@ -770,10 +799,13 @@ fn legacy_as_bevy(
slot.transform.offset[1],
)
};
let cutoff = match material.alpha_mode {
SceneAlphaMode::Mask { cutoff } => cutoff.clamp(0.0, 1.0),
_ => -1.0,
};
let cutoff = material.alpha_cutoff.map_or_else(
|| match material.alpha_mode {
SceneAlphaMode::Mask { cutoff } => cutoff.clamp(0.0, 1.0),
_ => -1.0,
},
|cutoff| cutoff.clamp(0.0, 1.0),
);
LegacyBevyMaterial {
uniform: LegacyUniform {
diffuse_color: scene_color(material.diffuse_color_srgb).to_linear(),
@@ -967,6 +999,20 @@ fn validate_scene(
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shaders_use_their_native_texture_transform_conventions() {
let legacy = include_str!("legacy_material.wgsl");
assert!(legacy.contains("(centered.x * cosine + centered.y * sine) * scale_offset.x"));
assert!(legacy.contains("(-centered.x * sine + centered.y * cosine) * scale_offset.y"));
assert!(legacy.contains("vec2(uv.x, 1.0 - uv.y)"));
assert!(legacy.contains("texel.a < material.surface.w"));
let pbr = include_str!("pbr_material.wgsl");
assert!(pbr.contains("let scaled = uv * scale_offset.xy"));
assert!(pbr.contains("scaled.x * cosine - scaled.y * sine"));
assert!(pbr.contains("scaled.x * sine + scaled.y * cosine"));
}
use crate::{Mesh as SceneMesh, TextureSlot, UvTransform, Vertex};
use std::sync::Arc;
@@ -976,16 +1022,25 @@ mod tests {
width: 2,
height: 2,
rgba: vec![
0, 0, 0, 0, 100, 100, 100, 100, 200, 200, 200, 200, 255, 255, 255, 255,
0, 0, 0, 255, 100, 100, 100, 255, 200, 200, 200, 255, 255, 255, 255, 255,
],
};
let (mips, levels) = rgba_mip_chain(&texture);
assert_eq!(levels, 2);
assert_eq!(mips.len(), 20);
assert_eq!(&mips[16..], &[138, 138, 138, 138]);
assert_eq!(&mips[16..], &[138, 138, 138, 255]);
let cutout = Texture {
width: 2,
height: 2,
rgba: vec![255, 220, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
};
let (mips, _) = rgba_mip_chain(&cutout);
assert_eq!(&mips[16..], &[255, 220, 0, 63]);
}
#[test]
#[allow(clippy::too_many_lines)] // One public render sequence checks reuse and orientation.
fn bevy_headless_renders_repeated_frames_through_the_public_api() {
let renderer = match Renderer::new_blocking() {
Ok(renderer) => Arc::new(renderer),
@@ -1030,6 +1085,67 @@ mod tests {
solid_texture([255, 180, 0, 255]),
solid_texture([30, 20, 10, 255]),
];
let background = renderer
.render(
camera,
&[],
&[],
[40, 80, 120, 255],
RenderLimits {
width: 96,
height: 64,
max_triangles: 0,
max_texture_bytes: 0,
max_texture_pixels: 0,
far_distance: 64,
},
)
.unwrap();
assert!(
background
.chunks_exact(4)
.all(|pixel| pixel == [40, 80, 120, 255]),
"empty scene did not preserve its clear color"
);
let oriented = renderer
.render(
camera,
&[renderable(Material::BlinnPhong(BlinnPhongMaterial {
diffuse: TextureSlot {
texture: Some(0),
..Default::default()
},
fullbright: true,
..Default::default()
}))],
&[Texture {
width: 2,
height: 2,
rgba: vec![
255, 0, 0, 255, 255, 0, 0, 255, 0, 0, 255, 255, 0, 0, 255, 255,
],
}],
[0, 0, 0, 255],
RenderLimits {
width: 96,
height: 64,
max_triangles: 2,
max_texture_bytes: 16,
max_texture_pixels: 4,
far_distance: 64,
},
)
.unwrap();
let top = &oriented[(20 * 96 + 48) * 4..][..4];
let bottom = &oriented[(44 * 96 + 48) * 4..][..4];
assert!(
top[0] > top[2],
"legacy texture top row is vertically flipped"
);
assert!(
bottom[2] > bottom[0],
"legacy texture bottom row is vertically flipped"
);
for renderable in frames {
let rgba = renderer
.render(

View File

@@ -20,28 +20,36 @@ struct LegacyUniform {
@group(#{MATERIAL_BIND_GROUP}) @binding(6) var specular_sampler: sampler;
fn transformed_uv(uv: vec2<f32>, scale_offset: vec4<f32>, rotation: f32) -> vec2<f32> {
let centered = (uv - vec2(0.5)) * scale_offset.xy;
let centered = uv - vec2(0.5);
let sine = sin(rotation);
let cosine = cos(rotation);
return vec2(
centered.x * cosine - centered.y * sine,
centered.x * sine + centered.y * cosine,
(centered.x * cosine + centered.y * sine) * scale_offset.x,
(-centered.x * sine + centered.y * cosine) * scale_offset.y,
) + vec2(0.5) + scale_offset.zw;
}
fn legacy_sample_uv(uv: vec2<f32>) -> vec2<f32> {
return vec2(uv.x, 1.0 - uv.y);
}
@fragment
fn fragment(in: VertexOutput, @builtin(front_facing) is_front: bool) -> FragmentOutput {
let diffuse_uv = transformed_uv(in.uv, material.diffuse_scale_offset, material.rotations.x);
let texel = textureSample(diffuse_texture, diffuse_sampler, diffuse_uv);
let texel = textureSample(diffuse_texture, diffuse_sampler, legacy_sample_uv(diffuse_uv));
var base = material.diffuse_color * texel * in.color;
if material.surface.w >= 0.0 && base.a < material.surface.w {
if material.surface.w >= 0.0 && texel.a < material.surface.w {
discard;
}
var normal = normalize(in.world_normal) * select(-1.0, 1.0, is_front);
if material.surface.z > 0.5 {
let normal_uv = transformed_uv(in.uv, material.normal_scale_offset, material.rotations.y);
let tangent_normal = textureSample(normal_texture, normal_sampler, normal_uv).xyz * 2.0 - 1.0;
let tangent_normal = textureSample(
normal_texture,
normal_sampler,
legacy_sample_uv(normal_uv),
).xyz * 2.0 - 1.0;
let tangent = normalize(in.world_tangent.xyz);
let bitangent = normalize(cross(normal, tangent)) * in.world_tangent.w;
normal = normalize(mat3x3(tangent, bitangent, normal) * tangent_normal);
@@ -52,7 +60,11 @@ fn fragment(in: VertexOutput, @builtin(front_facing) is_front: bool) -> Fragment
let half_direction = normalize(light_direction + view_direction);
let diffuse_light = max(dot(normal, light_direction), 0.0);
let specular_uv = transformed_uv(in.uv, material.specular_scale_offset, material.rotations.z);
let specular_map = textureSample(specular_texture, specular_sampler, specular_uv).rgb;
let specular_map = textureSample(
specular_texture,
specular_sampler,
legacy_sample_uv(specular_uv),
).rgb;
let specular_light = pow(max(dot(normal, half_direction), 0.0), material.rotations.w);
let lit = base.rgb * (0.55 + 0.45 * diffuse_light)
+ material.specular_color.rgb * specular_map * specular_light

View File

@@ -112,6 +112,8 @@ pub struct BlinnPhongMaterial {
pub fullbright: bool,
pub double_sided: bool,
pub alpha_mode: AlphaMode,
/// Optional diffuse-texture cutout applied before face-alpha blending.
pub alpha_cutoff: Option<f32>,
}
impl Default for BlinnPhongMaterial {
@@ -127,6 +129,7 @@ impl Default for BlinnPhongMaterial {
fullbright: false,
double_sided: false,
alpha_mode: AlphaMode::Opaque,
alpha_cutoff: None,
}
}
}

View File

@@ -25,13 +25,13 @@ struct PbrExtension {
@group(#{MATERIAL_BIND_GROUP}) @binding(108) var emissive_sampler: sampler;
fn transformed_uv(uv: vec2<f32>, scale_offset: vec4<f32>, rotation: f32) -> vec2<f32> {
let centered = (uv - vec2(0.5)) * scale_offset.xy;
let scaled = uv * scale_offset.xy;
let sine = sin(rotation);
let cosine = cos(rotation);
return vec2(
centered.x * cosine - centered.y * sine,
centered.x * sine + centered.y * cosine,
) + vec2(0.5) + scale_offset.zw;
scaled.x * cosine - scaled.y * sine,
scaled.x * sine + scaled.y * cosine,
) + scale_offset.zw;
}
@fragment