makepad/apps/vj/examples/pbr_view.rs
Admin 75b295ce9a vj: synchronize decks and queue remote stem processing
Update deck and mixer synchronization, apply matched deck rates to splats, and queue remote stem extraction through the fleet. Refresh catalog and creator pipeline integration.

Validation: release VJ suite reports 890 passed, 10 failed, nine ignored. Eight mixer failures reproduce in serial execution (gain, transitions, sample alignment and video fades); two asynchronous media tests also failed in the full run. The new matched-rate splat test passes. These remaining failures are not claimed as fixed.
2026-09-05 01:45:43 +02:00

342 lines
11 KiB
Rust

//! Offscreen GPU smoke for the 3D program slot's shading.
//!
//! PBR_VIEW_GLB=<path.glb> PBR_VIEW_YAW=0,45,90 PBR_VIEW_OUT=<dir> \
//! cargo run -p makepad-vj --release --example pbr_view
//!
//! Loads one GLB through exactly the path `mesh_view.rs` uses for an
//! unskinned prop — `Renderer::load_model` + `Renderer::draw_preview` into an
//! offscreen pass — and writes one PPM per camera yaw, then exits.
//!
//! It exists because the specular lane
//! ([`makepad_render::DrawScenePbr`]) is picked by MATERIAL at load, and the
//! only honest way to check a shader is to look at what it draws. Two knobs
//! matter: the model (matte vs shiny material = the before/after) and the
//! camera yaw (a specular highlight moves with the eye; a diffuse term does
//! not). Both are inputs here, so a regression is one command away.
//!
//! PPM rather than PNG on purpose: this crate has no encoder dependency and
//! P6 is four lines of code. `sips`/`magick`/python convert it.
use makepad_render::{
preview_scene_state, set_pass_camera, DrawSceneAlpha, DrawSceneCube, DrawSceneShadow,
DrawSceneSkinned, DrawSceneSkinnedGpu, DrawSceneSky, DrawSceneTerrain, DrawSceneTexture,
ModelInstance, PreviewLook, PreviewStage, Renderer, SceneDraws,
};
use makepad_widgets::*;
app_main!(App);
const SIZE: f64 = 512.0;
script_mod! {
use mod.prelude.widgets.*
mod.widgets.PbrProbeBase = #(PbrProbe::register_widget(vm))
mod.widgets.PbrProbe = set_type_default() do mod.widgets.PbrProbeBase{
width: Fill
height: Fill
// The same key direction the VJ mesh slot lights its stage with, so
// what this renders is what that slot renders.
draw_cube +: { light_dir: vec3(0.35, 0.8, 0.45) }
draw_alpha +: { light_dir: vec3(0.35, 0.8, 0.45) }
draw_terrain +: { light_dir: vec3(0.35, 0.8, 0.45) }
draw_skinned +: { light_dir: vec3(0.35, 0.8, 0.45) }
draw_models +: { light_dir: vec3(0.35, 0.8, 0.45) }
}
load_all_resources() do #(App::script_component(vm)){
ui: Root{
main_window := Window{
window.inner_size: vec2(560, 560)
body +: {
probe := PbrProbe{}
}
}
}
}
}
/// yaw-rotation * uniform scale, translated. Copied from `mesh_view.rs` so
/// the statue sits exactly where the VJ slot puts it.
fn trs_yaw(pos: Vec3f, yaw: f32, scale: f32) -> Mat4f {
let mut m = Mat4f::rotation(vec3f(0.0, yaw, 0.0));
for k in [0usize, 1, 2, 4, 5, 6, 8, 9, 10] {
m.v[k] *= scale;
}
m.v[12] = pos.x;
m.v[13] = pos.y;
m.v[14] = pos.z;
m
}
#[derive(Script, ScriptHook, WidgetRef, WidgetRegister)]
pub struct PbrProbe {
#[uid]
uid: WidgetUid,
#[source]
source: ScriptObjectRef,
#[walk]
walk: Walk,
#[layout]
layout: Layout,
#[live]
draw_bg: DrawSceneTexture,
#[live]
draw_cube: DrawSceneCube,
#[live]
draw_alpha: DrawSceneAlpha,
#[live]
draw_sky: DrawSceneSky,
#[live]
draw_terrain: DrawSceneTerrain,
#[live]
draw_shadow: DrawSceneShadow,
#[live]
draw_skinned: DrawSceneSkinnedGpu,
#[live]
draw_models: DrawSceneSkinned,
#[new]
pass: DrawPass,
#[new]
draw_list: DrawList,
#[new]
color_texture: Texture,
#[new]
depth_texture: Texture,
#[rust]
area: Area,
#[rust(false)]
initialized: bool,
#[rust]
renderer: Renderer,
#[rust]
look: PreviewLook,
#[rust]
statue: Option<ModelInstance>,
/// Camera yaws still to shoot, in radians.
#[rust]
yaws: Vec<(f32, String)>,
/// Frames drawn at the current yaw. The readback is only meaningful once
/// the GPU has actually delivered the pass.
#[rust(0u32)]
settle: u32,
#[rust]
next_frame: NextFrame,
}
impl PbrProbe {
fn ensure_initialized(&mut self, cx: &mut Cx) {
if self.initialized {
return;
}
self.initialized = true;
self.color_texture = Texture::new_with_format(
cx,
TextureFormat::RenderBGRAu8 { size: TextureSize::Auto, initial: true },
);
self.depth_texture = Texture::new_with_format(
cx,
TextureFormat::DepthD32 { size: TextureSize::Auto, initial: true },
);
cx.passes[self.pass.draw_pass_id()].keep_camera_matrix = true;
self.next_frame = cx.new_next_frame();
let path = std::env::var("PBR_VIEW_GLB").expect("PBR_VIEW_GLB=<path.glb>");
let out = std::env::var("PBR_VIEW_OUT").unwrap_or_else(|_| ".".into());
let stem = std::path::Path::new(&path)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("model")
.to_string();
self.yaws = std::env::var("PBR_VIEW_YAW")
.unwrap_or_else(|_| "0".into())
.split(',')
.filter_map(|s| s.trim().parse::<f32>().ok())
.map(|deg| {
(
deg.to_radians(),
format!("{out}/{stem}_yaw{}.ppm", deg as i32),
)
})
.collect();
self.yaws.reverse();
let glb = std::fs::read(&path).expect("read glb");
let png = makepad_render::model::embedded_base_color_png(&glb);
let tris = self
.renderer
.load_model(cx, "probe", &glb, png.as_deref())
.expect("load model");
let (min, max) = self
.renderer
.model_bounds("probe")
.unwrap_or((vec3f(0.0, 0.0, 0.0), vec3f(0.0, 1.0, 0.0)));
// Same framing as the VJ slot: 1.75 units tall, feet on the slab.
let scale = 1.75 / (max.y - min.y).max(0.01);
self.statue = Some(ModelInstance {
model: "probe".into(),
transform: trs_yaw(vec3f(0.0, -min.y * scale, 0.0), 0.0, scale),
tint: vec4(1.0, 1.0, 1.0, 1.0),
color_adjust: vec4(0.0, 1.0, 1.0, 0.0),
dynamic: false,
depth_order: 0.0,
custom_material: None,
part_poses: Vec::new(),
});
self.look.target = vec3f(0.0, 0.9, 0.0);
self.look.distance = 4.6;
self.look.fov = 45.0;
log!("pbr_view: {path} — {tris} triangles, {} shots", self.yaws.len());
}
/// Read the pass back and write a P6 PPM. Returns false while the GPU has
/// not delivered yet, so the caller shoots the same frame again.
fn capture(&mut self, cx: &mut Cx, file: &str) -> bool {
let Some((w, h, bgra)) = cx.debug_read_render_texture(&self.color_texture) else {
return false;
};
if w == 0 || h == 0 || bgra.len() < w * h * 4 {
return false;
}
let mut out = format!("P6\n{w} {h}\n255\n").into_bytes();
for px in bgra.chunks_exact(4).take(w * h) {
out.extend_from_slice(&[px[2], px[1], px[0]]);
}
match std::fs::write(file, &out) {
Ok(()) => log!("pbr_view: wrote {file} ({w}x{h})"),
Err(e) => log!("pbr_view: cannot write {file}: {e}"),
}
true
}
}
impl Widget for PbrProbe {
fn draw_walk(&mut self, cx: &mut Cx2d, _scope: &mut Scope, walk: Walk) -> DrawStep {
let rect = cx.walk_turtle_with_area(&mut self.area, walk);
self.ensure_initialized(cx.cx);
let Some(yaw) = self.yaws.last().map(|(y, _)| *y) else {
return DrawStep::done();
};
self.look.yaw = yaw;
let pass_rect = Rect { pos: dvec2(0.0, 0.0), size: dvec2(SIZE, SIZE) };
self.pass.set_size(cx, pass_rect.size);
self.pass.set_color_texture(
cx,
&self.color_texture,
DrawPassClearColor::ClearWith(vec4(0.015, 0.02, 0.04, 1.0)),
);
self.pass
.set_depth_texture(cx, &self.depth_texture, DrawPassClearDepth::ClearWith(1.0));
cx.make_child_pass(&self.pass);
cx.begin_pass(&self.pass, None);
self.pass.set_size(cx, pass_rect.size);
self.pass.set_dpi_factor(cx, 1.0);
if let Some(scene_state) = preview_scene_state(self.look, pass_rect, cx.time()) {
set_pass_camera(cx.cx, &self.pass, &scene_state);
let cx3d = &mut Cx3d::new(cx.cx);
self.draw_list.begin_always(cx3d);
self.renderer
.set_models(self.statue.iter().cloned().collect());
let mut draws = SceneDraws {
cube: &mut self.draw_cube,
alpha: &mut self.draw_alpha,
sky: &mut self.draw_sky,
sky_analytic: None,
terrain: &mut self.draw_terrain,
shadow: Some(&mut self.draw_shadow),
shadow_sdf: None,
firework: None,
flare: None,
water: None,
screen: None,
screen_instances: &[],
view_model: None,
};
let mut stage = PreviewStage::statue();
stage.ground_half = 9.0;
stage.ground_color = vec4(0.10, 0.11, 0.14, 1.0);
self.renderer.draw_preview(
cx3d,
&mut self.draw_list,
&mut draws,
self.look,
stage,
scene_state,
None,
Some(&mut self.draw_models),
);
self.draw_list.end(cx3d);
}
cx.end_pass(&self.pass);
self.draw_bg.draw_vars.set_texture(0, &self.color_texture);
self.draw_bg.draw_abs(cx, rect);
DrawStep::done()
}
fn handle_event(&mut self, cx: &mut Cx, event: &Event, _scope: &mut Scope) {
if self.next_frame.is_event(event).is_none() {
return;
}
self.next_frame = cx.new_next_frame();
if !self.initialized {
self.area.redraw(cx);
return;
}
// Eight frames of settling before the first readback: the pass is
// pipelined and an early read returns the clear colour.
self.settle += 1;
if self.settle < 8 {
self.area.redraw(cx);
return;
}
let Some((_, file)) = self.yaws.last().cloned() else {
cx.quit();
return;
};
if self.capture(cx, &file) {
self.yaws.pop();
self.settle = 0;
}
if self.yaws.is_empty() {
cx.quit();
return;
}
self.area.redraw(cx);
}
}
impl WidgetNode for PbrProbe {
fn widget_uid(&self) -> WidgetUid {
self.uid
}
fn walk(&mut self, _cx: &mut Cx) -> Walk {
self.walk
}
fn area(&self) -> Area {
self.area
}
fn redraw(&mut self, cx: &mut Cx) {
self.area.redraw(cx);
}
}
#[derive(Script, ScriptHook)]
pub struct App {
#[live]
ui: WidgetRef,
}
impl MatchEvent for App {}
impl AppMain for App {
fn script_mod(vm: &mut ScriptVm) -> ScriptValue {
makepad_widgets::script_mod(vm);
makepad_render::script_mod(vm);
self::script_mod(vm)
}
fn handle_event(&mut self, cx: &mut Cx, event: &Event) {
self.match_event(cx, event);
self.ui.handle_event(cx, event, &mut Scope::empty());
}
}