mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Share map render resources and release uploaded CPU data
This commit is contained in:
1 parent
5159538b85
commit
acbaa9ccda
7 files changed
+384
-56
No files matched your search
@@ -1,8 +1,36 @@
|
||||
//! Opt-in repeatable frame/physics timing: SKATE_PERF_REPORT=path.json.
|
||||
use bevy::prelude::*;
|
||||
use std::{path::PathBuf, time::Instant};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use bevy::render::{Render, RenderApp, RenderSystems};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::{collections::BTreeMap, sync::OnceLock};
|
||||
use std::{path::PathBuf, time::Instant};
|
||||
|
||||
static SECTIONS: Mutex<BTreeMap<&'static str, (f64, u64)>> = Mutex::new(BTreeMap::new());
|
||||
pub(crate) struct Scope {
|
||||
name: &'static str,
|
||||
start: Option<Instant>,
|
||||
}
|
||||
impl Scope {
|
||||
pub(crate) fn new(name: &'static str) -> Self {
|
||||
static ENABLED: OnceLock<bool> = OnceLock::new();
|
||||
Self {
|
||||
name,
|
||||
start: ENABLED
|
||||
.get_or_init(|| std::env::var_os("SKATE_PERF_REPORT").is_some())
|
||||
.then(Instant::now),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Drop for Scope {
|
||||
fn drop(&mut self) {
|
||||
if let Some(start) = self.start {
|
||||
let mut sections = SECTIONS.lock().unwrap();
|
||||
let entry = sections.entry(self.name).or_default();
|
||||
entry.0 += start.elapsed().as_secs_f64() * 1000.;
|
||||
entry.1 += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
pub(crate) struct Performance {
|
||||
@@ -13,7 +41,7 @@ pub(crate) struct Performance {
|
||||
physics_ms: f64,
|
||||
ticks: u32,
|
||||
samples: Vec<[f64; 4]>,
|
||||
render: Arc<Mutex<Vec<[f64; 2]>>>,
|
||||
render: Arc<Mutex<Vec<[f64; 5]>>>,
|
||||
}
|
||||
impl Performance {
|
||||
pub(crate) fn physics(&mut self, elapsed: std::time::Duration) {
|
||||
@@ -25,21 +53,63 @@ impl Performance {
|
||||
pub(crate) struct PerformancePlugin;
|
||||
impl Plugin for PerformancePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
let Some(path) = std::env::var_os("SKATE_PERF_REPORT") else { return; };
|
||||
let Some(path) = std::env::var_os("SKATE_PERF_REPORT") else {
|
||||
return;
|
||||
};
|
||||
let now = Instant::now();
|
||||
let render = Arc::new(Mutex::new(Vec::new()));
|
||||
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
||||
render_app.insert_resource(RenderPerformance { start: None, frame: now, prepared: now, samples: render.clone() })
|
||||
render_app
|
||||
.insert_resource(RenderPerformance {
|
||||
start: None,
|
||||
frame: now,
|
||||
prepared: now,
|
||||
mark: now,
|
||||
phases: [0.; 3],
|
||||
samples: render.clone(),
|
||||
})
|
||||
.add_systems(Render, render_begin.before(RenderSystems::ExtractCommands))
|
||||
.add_systems(Render, render_prepared.after(RenderSystems::Prepare).before(RenderSystems::Render))
|
||||
.add_systems(
|
||||
Render,
|
||||
render_assets_done
|
||||
.after(RenderSystems::PrepareMeshes)
|
||||
.after(RenderSystems::PrepareAssets)
|
||||
.before(RenderSystems::ManageViews),
|
||||
)
|
||||
.add_systems(
|
||||
Render,
|
||||
render_views_done
|
||||
.after(RenderSystems::ManageViews)
|
||||
.before(RenderSystems::Queue),
|
||||
)
|
||||
.add_systems(
|
||||
Render,
|
||||
render_queue_done
|
||||
.after(RenderSystems::Queue)
|
||||
.before(RenderSystems::PhaseSort),
|
||||
)
|
||||
.add_systems(
|
||||
Render,
|
||||
render_prepared
|
||||
.after(RenderSystems::Prepare)
|
||||
.before(RenderSystems::Render),
|
||||
)
|
||||
.add_systems(Render, render_finish.after(RenderSystems::PostCleanup));
|
||||
}
|
||||
// Benchmark measurements must not depend on whether the window has focus.
|
||||
app.insert_resource(bevy::winit::WinitSettings::continuous());
|
||||
app.insert_resource(Performance {
|
||||
path: path.into(), start: None, frame_start: now, previous: now,
|
||||
physics_ms: 0., ticks: 0, samples: Vec::new(), render,
|
||||
}).add_systems(First, begin).add_systems(Last, finish);
|
||||
path: path.into(),
|
||||
start: None,
|
||||
frame_start: now,
|
||||
previous: now,
|
||||
physics_ms: 0.,
|
||||
ticks: 0,
|
||||
samples: Vec::new(),
|
||||
render,
|
||||
})
|
||||
.add_systems(First, begin)
|
||||
.add_systems(Last, finish);
|
||||
}
|
||||
}
|
||||
fn begin(mut p: ResMut<Performance>) {
|
||||
@@ -49,21 +119,32 @@ fn begin(mut p: ResMut<Performance>) {
|
||||
}
|
||||
fn finish(mut p: ResMut<Performance>, mut exit: MessageWriter<AppExit>) {
|
||||
let now = Instant::now();
|
||||
let elapsed = now.duration_since(*p.start.get_or_insert(now)).as_secs_f64();
|
||||
let elapsed = now
|
||||
.duration_since(*p.start.get_or_insert(now))
|
||||
.as_secs_f64();
|
||||
let frame = now.duration_since(p.previous).as_secs_f64() * 1000.;
|
||||
p.previous = now;
|
||||
// Exclude initialization and shader warmup. Frame interval includes render
|
||||
// synchronization; CPU schedule time is First..Last of the main world.
|
||||
if elapsed > 10. {
|
||||
let sample = [frame, now.duration_since(p.frame_start).as_secs_f64() * 1000., p.physics_ms, f64::from(p.ticks)];
|
||||
let sample = [
|
||||
frame,
|
||||
now.duration_since(p.frame_start).as_secs_f64() * 1000.,
|
||||
p.physics_ms,
|
||||
f64::from(p.ticks),
|
||||
];
|
||||
p.samples.push(sample);
|
||||
}
|
||||
if elapsed > 25. && !p.samples.is_empty() {
|
||||
let mean = |column: usize| p.samples.iter().map(|s| s[column]).sum::<f64>() / p.samples.len() as f64;
|
||||
let mean = |column: usize| {
|
||||
p.samples.iter().map(|s| s[column]).sum::<f64>() / p.samples.len() as f64
|
||||
};
|
||||
let mut frames: Vec<_> = p.samples.iter().map(|s| s[0]).collect();
|
||||
frames.sort_by(f64::total_cmp);
|
||||
let render = p.render.lock().unwrap();
|
||||
let render_mean = |column: usize| render.iter().map(|s| s[column]).sum::<f64>() / render.len().max(1) as f64;
|
||||
let render_mean = |column: usize| {
|
||||
render.iter().map(|s| s[column]).sum::<f64>() / render.len().max(1) as f64
|
||||
};
|
||||
let report = serde_json::json!({
|
||||
"frames": frames.len(), "fps": 1000. / mean(0),
|
||||
"frame_ms_mean": mean(0), "frame_ms_median": frames[frames.len()/2],
|
||||
@@ -72,32 +153,65 @@ fn finish(mut p: ResMut<Performance>, mut exit: MessageWriter<AppExit>) {
|
||||
"physics_ticks_per_frame": mean(3),
|
||||
"physics_ms_per_tick": mean(2) / mean(3).max(f64::EPSILON),
|
||||
"render_prepare_ms_mean": render_mean(0), "render_submit_ms_mean": render_mean(1),
|
||||
"render_assets_ms_mean": render_mean(2), "render_views_ms_mean": render_mean(3), "render_queue_ms_mean": render_mean(4),
|
||||
"sections_ms_per_call": SECTIONS.lock().unwrap().iter().map(|(&k, &(ms, n))| (k, ms / n as f64)).collect::<BTreeMap<_,_>>(),
|
||||
"samples": p.samples,
|
||||
});
|
||||
match std::fs::write(&p.path, serde_json::to_vec_pretty(&report).unwrap()) {
|
||||
Ok(()) => { eprintln!("SKATE_PERF_REPORT {}", p.path.display()); exit.write(AppExit::Success); }
|
||||
Err(e) => { eprintln!("Performance report: {e}"); exit.write(AppExit::error()); }
|
||||
Ok(()) => {
|
||||
eprintln!("SKATE_PERF_REPORT {}", p.path.display());
|
||||
exit.write(AppExit::Success);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Performance report: {e}");
|
||||
exit.write(AppExit::error());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
struct RenderPerformance {
|
||||
start: Option<Instant>, frame: Instant, prepared: Instant,
|
||||
samples: Arc<Mutex<Vec<[f64; 2]>>>,
|
||||
start: Option<Instant>,
|
||||
frame: Instant,
|
||||
prepared: Instant,
|
||||
mark: Instant,
|
||||
phases: [f64; 3],
|
||||
samples: Arc<Mutex<Vec<[f64; 5]>>>,
|
||||
}
|
||||
fn render_begin(mut p: ResMut<RenderPerformance>) {
|
||||
p.frame = Instant::now();
|
||||
p.mark = p.frame;
|
||||
}
|
||||
fn render_assets_done(mut p: ResMut<RenderPerformance>) {
|
||||
let now = Instant::now();
|
||||
p.phases[0] = now.duration_since(p.mark).as_secs_f64() * 1000.;
|
||||
p.mark = now;
|
||||
}
|
||||
fn render_views_done(mut p: ResMut<RenderPerformance>) {
|
||||
let now = Instant::now();
|
||||
p.phases[1] = now.duration_since(p.mark).as_secs_f64() * 1000.;
|
||||
p.mark = now;
|
||||
}
|
||||
fn render_queue_done(mut p: ResMut<RenderPerformance>) {
|
||||
p.phases[2] = p.mark.elapsed().as_secs_f64() * 1000.;
|
||||
}
|
||||
fn render_prepared(mut p: ResMut<RenderPerformance>) {
|
||||
p.prepared = Instant::now();
|
||||
}
|
||||
fn render_finish(mut p: ResMut<RenderPerformance>) {
|
||||
let now = Instant::now();
|
||||
if now.duration_since(*p.start.get_or_insert(now)).as_secs_f64() > 10. {
|
||||
if now
|
||||
.duration_since(*p.start.get_or_insert(now))
|
||||
.as_secs_f64()
|
||||
> 10.
|
||||
{
|
||||
p.samples.lock().unwrap().push([
|
||||
p.prepared.duration_since(p.frame).as_secs_f64() * 1000.,
|
||||
now.duration_since(p.prepared).as_secs_f64() * 1000.,
|
||||
p.phases[0],
|
||||
p.phases[1],
|
||||
p.phases[2],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -29,12 +29,15 @@ pub(super) fn advance(
|
||||
//input reset82DB8998, which would immediately clear that mode again.
|
||||
}
|
||||
physics.board.clear_forces();
|
||||
let query_timer = crate::performance::Scope::new("wheel_and_foot_queries");
|
||||
//World8275EA20 starts both batches before actor SetUpPhysics. Foot
|
||||
//results stay pending while PlayerInput consumes the preceding records.
|
||||
physics
|
||||
.riding
|
||||
.start_wheel_queries(&physics.board, &physics.world)?;
|
||||
let skeleton_queries = super::foot_ik_queries::query(&physics.world, &skater.skeleton)?;
|
||||
drop(query_timer);
|
||||
let graph_timer = crate::performance::Scope::new("animation_graph");
|
||||
let animation = animation_phase::advance(
|
||||
physics,
|
||||
skater,
|
||||
@@ -43,6 +46,8 @@ pub(super) fn advance(
|
||||
&physics.animation_profile,
|
||||
)
|
||||
.map_err(|e| format!("Animation tick{}: {e}", physics.ticks))?;
|
||||
drop(graph_timer);
|
||||
let input_timer = crate::performance::Scope::new("input_and_state");
|
||||
//ForcePhysics Begin82BB2868 writes the live Skeleton16420 mode once.
|
||||
//Consume the graph request so a later physical reset can retain its own0.
|
||||
if let Some(mode) = skater.animation.motion.riding.force_mode.take() {
|
||||
@@ -118,7 +123,11 @@ pub(super) fn advance(
|
||||
//World8275ECA4 ends skeleton tests after state/forces and before solving.
|
||||
//Teleport resets previous observations, but preserves this pending batch.
|
||||
skeleton_queries.publish(&mut skater.player_input.player);
|
||||
drop(input_timer);
|
||||
let solve_timer = crate::performance::Scope::new("solve_total");
|
||||
solve::advance(physics, skater, skater.ground.steering.targets)?;
|
||||
drop(solve_timer);
|
||||
let _output_timer = crate::performance::Scope::new("outputs_and_camera");
|
||||
//ProcessOutput82DB6EE8 resets the packet before its component publishers.
|
||||
//All consumers of the preceding output have completed this frame's input.
|
||||
super::player_input::reset_outputs(&mut skater.player_input.physical);
|
||||
|
||||
@@ -21,10 +21,13 @@ pub(super) fn advance(
|
||||
// Skeleton82BE5094 passes false to82768728: its edge threshold is -1,
|
||||
// whereas the board requests .999. GroundPipeline supplies the remaining
|
||||
// shared values. Do not let the second query overwrite the first's rows.
|
||||
let board_timer = crate::performance::Scope::new("board_world_contacts");
|
||||
let mut contacts = physics
|
||||
.world
|
||||
.query_primitives(&board_volumes, physics.query, physics.retention)
|
||||
.to_vec();
|
||||
drop(board_timer);
|
||||
let skeleton_timer = crate::performance::Scope::new("skeleton_world_contacts");
|
||||
let mut skeleton_query = physics.query;
|
||||
skeleton_query.edge_cos_bend_normal_threshold = -1.0;
|
||||
contacts.extend_from_slice(physics.world.query_primitives(
|
||||
@@ -32,6 +35,7 @@ pub(super) fn advance(
|
||||
skeleton_query,
|
||||
physics.retention,
|
||||
));
|
||||
drop(skeleton_timer);
|
||||
assembly_contacts::append(
|
||||
&mut contacts,
|
||||
&board_volumes,
|
||||
|
||||
@@ -341,6 +341,77 @@ pub(crate) fn collision_world(
|
||||
BoardWorld::with_query_metadata(triangles, metadata).map_err(str::to_owned)
|
||||
}
|
||||
|
||||
/// Reuse identical render resources without changing authored triangles.
|
||||
fn render_texture_ids(textures: &[skate_data::skate_map::Texture]) -> Vec<u32> {
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut buckets: HashMap<(u32, u32, u32, u64), Vec<usize>> = HashMap::new();
|
||||
let mut ids = vec![0];
|
||||
for (i, t) in textures.iter().enumerate() {
|
||||
let mut hash = std::collections::hash_map::DefaultHasher::new();
|
||||
t.rgba.hash(&mut hash);
|
||||
let bucket = buckets
|
||||
.entry((t.width, t.height, t.color_space, hash.finish()))
|
||||
.or_default();
|
||||
let same = bucket.iter().copied().find(|&j| textures[j].rgba == t.rgba);
|
||||
let canonical = same.unwrap_or_else(|| {
|
||||
bucket.push(i);
|
||||
i
|
||||
});
|
||||
ids.push(canonical as u32 + 1);
|
||||
}
|
||||
ids
|
||||
}
|
||||
|
||||
fn render_material_ids(
|
||||
materials: &[skate_data::skate_map::Material],
|
||||
texture_ids: &[u32],
|
||||
) -> Vec<usize> {
|
||||
let mut unique = HashMap::new();
|
||||
materials
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, m)| {
|
||||
// Only fields consumed by this renderer belong in the identity. Source
|
||||
// names and collision surface metadata do not change a PBR material.
|
||||
let textures = m.textures.map(|id| texture_ids[id as usize]);
|
||||
let key = [
|
||||
textures[0],
|
||||
textures[1],
|
||||
textures[2],
|
||||
textures[3],
|
||||
textures[4],
|
||||
m.color[0].to_bits(),
|
||||
m.color[1].to_bits(),
|
||||
m.color[2].to_bits(),
|
||||
m.roughness.to_bits(),
|
||||
m.emissive.to_bits(),
|
||||
m.indirect_strength.to_bits(),
|
||||
m.alpha_mode,
|
||||
m.alpha_cutoff.to_bits(),
|
||||
];
|
||||
*unique.entry(key).or_insert(i)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn render_groups(
|
||||
geometry: &skate_data::skate_map::Geometry,
|
||||
material_ids: &[usize],
|
||||
) -> Vec<(usize, Vec<u32>)> {
|
||||
let mut lookup = HashMap::new();
|
||||
let mut groups: Vec<(usize, Vec<u32>)> = Vec::new();
|
||||
for tri in geometry.indices.chunks_exact(3) {
|
||||
let material = material_ids[geometry.vertices[tri[0] as usize].material as usize - 1];
|
||||
let group = *lookup.entry(material).or_insert_with(|| {
|
||||
let index = groups.len();
|
||||
groups.push((material, Vec::new()));
|
||||
index
|
||||
});
|
||||
groups[group].1.extend_from_slice(tri);
|
||||
}
|
||||
groups
|
||||
}
|
||||
|
||||
pub(crate) fn spawn(
|
||||
map: &SkateMap,
|
||||
commands: &mut Commands,
|
||||
@@ -349,8 +420,10 @@ pub(crate) fn spawn(
|
||||
images: &mut Assets<Image>,
|
||||
) {
|
||||
// Texture roles have different transfer functions even when sharing a record.
|
||||
let texture_ids = render_texture_ids(&map.textures);
|
||||
let mut cache = HashMap::<(u32, u8), Handle<Image>>::new();
|
||||
let mut texture = |id: u32, role: u8| -> Option<Handle<Image>> {
|
||||
let id = texture_ids[id as usize];
|
||||
if id == 0 {
|
||||
return None;
|
||||
}
|
||||
@@ -390,7 +463,7 @@ pub(crate) fn spawn(
|
||||
TextureDimension::D2,
|
||||
bytes,
|
||||
format,
|
||||
RenderAssetUsages::default(),
|
||||
RenderAssetUsages::RENDER_WORLD,
|
||||
);
|
||||
let mut sampler = bevy::image::ImageSamplerDescriptor::linear();
|
||||
if role != 1 {
|
||||
@@ -403,15 +476,18 @@ pub(crate) fn spawn(
|
||||
.clone(),
|
||||
)
|
||||
};
|
||||
let mut groups = vec![Vec::<u32>::new(); map.materials.len()];
|
||||
for tri in map.geometry.indices.chunks_exact(3) {
|
||||
groups[map.geometry.vertices[tri[0] as usize].material as usize - 1].extend_from_slice(tri);
|
||||
}
|
||||
for (m, indices) in map.materials.iter().zip(groups) {
|
||||
if indices.is_empty() {
|
||||
continue;
|
||||
}
|
||||
// Reindex each material once, preserving authored normals and both UV sets.
|
||||
let material_ids = render_material_ids(&map.materials, &texture_ids);
|
||||
let groups = render_groups(&map.geometry, &material_ids);
|
||||
eprintln!(
|
||||
"SKATE_RENDER_BATCHES count={} triangles={}",
|
||||
groups.len(),
|
||||
map.geometry.indices.len() / 3
|
||||
);
|
||||
let mut material_handles: Vec<Option<Handle<StandardMaterial>>> =
|
||||
vec![None; map.materials.len()];
|
||||
for (material_index, indices) in groups {
|
||||
let m = &map.materials[material_index];
|
||||
// Reindex each batch, preserving authored normals and both UV sets.
|
||||
let mut remap = HashMap::new();
|
||||
let mut vertices = Vec::new();
|
||||
let local: Vec<u32> = indices
|
||||
@@ -426,7 +502,7 @@ pub(crate) fn spawn(
|
||||
.collect();
|
||||
let mut mesh = Mesh::new(
|
||||
bevy::mesh::PrimitiveTopology::TriangleList,
|
||||
RenderAssetUsages::default(),
|
||||
RenderAssetUsages::RENDER_WORLD,
|
||||
)
|
||||
.with_inserted_attribute(
|
||||
Mesh::ATTRIBUTE_POSITION,
|
||||
@@ -464,29 +540,33 @@ pub(crate) fn spawn(
|
||||
warn!("SKATE material {} tangent generation: {error}", m.name);
|
||||
}
|
||||
}
|
||||
let orm = texture(m.textures[3], 2);
|
||||
let material = materials.add(StandardMaterial {
|
||||
base_color: Color::linear_rgb(m.color[0], m.color[1], m.color[2]),
|
||||
base_color_texture: texture(m.textures[0], 0),
|
||||
normal_map_texture: texture(m.textures[2], 2),
|
||||
metallic_roughness_texture: orm.clone(),
|
||||
occlusion_texture: orm,
|
||||
metallic: if m.textures[3] != 0 { 1. } else { 0. },
|
||||
perceptual_roughness: m.roughness,
|
||||
emissive: LinearRgba::rgb(
|
||||
m.color[0] * m.emissive,
|
||||
m.color[1] * m.emissive,
|
||||
m.color[2] * m.emissive,
|
||||
),
|
||||
emissive_texture: texture(m.textures[4], 0),
|
||||
alpha_mode: match m.alpha_mode {
|
||||
1 => AlphaMode::Mask(m.alpha_cutoff),
|
||||
2 => AlphaMode::Blend,
|
||||
_ => AlphaMode::Opaque,
|
||||
},
|
||||
lightmap_exposure: m.indirect_strength,
|
||||
..default()
|
||||
});
|
||||
let material = material_handles[material_index]
|
||||
.get_or_insert_with(|| {
|
||||
let orm = texture(m.textures[3], 2);
|
||||
materials.add(StandardMaterial {
|
||||
base_color: Color::linear_rgb(m.color[0], m.color[1], m.color[2]),
|
||||
base_color_texture: texture(m.textures[0], 0),
|
||||
normal_map_texture: texture(m.textures[2], 2),
|
||||
metallic_roughness_texture: orm.clone(),
|
||||
occlusion_texture: orm,
|
||||
metallic: if m.textures[3] != 0 { 1. } else { 0. },
|
||||
perceptual_roughness: m.roughness,
|
||||
emissive: LinearRgba::rgb(
|
||||
m.color[0] * m.emissive,
|
||||
m.color[1] * m.emissive,
|
||||
m.color[2] * m.emissive,
|
||||
),
|
||||
emissive_texture: texture(m.textures[4], 0),
|
||||
alpha_mode: match m.alpha_mode {
|
||||
1 => AlphaMode::Mask(m.alpha_cutoff),
|
||||
2 => AlphaMode::Blend,
|
||||
_ => AlphaMode::Opaque,
|
||||
},
|
||||
lightmap_exposure: m.indirect_strength,
|
||||
..default()
|
||||
})
|
||||
})
|
||||
.clone();
|
||||
let mut entity = commands.spawn((
|
||||
Name::new(m.name.clone()),
|
||||
Mesh3d(meshes.add(mesh)),
|
||||
@@ -563,6 +643,64 @@ mod tests {
|
||||
fn demo() -> SkateMap {
|
||||
SkateMap::parse(include_bytes!("../../../maps/format-demo.skate")).unwrap()
|
||||
}
|
||||
#[test]
|
||||
fn render_batches_preserve_complete_triangles_and_materials() {
|
||||
let map = demo();
|
||||
let material_ids = render_material_ids(&map.materials, &render_texture_ids(&map.textures));
|
||||
let chunks = render_groups(&map.geometry, &material_ids);
|
||||
assert_eq!(chunks.len(), 1);
|
||||
let mut original: Vec<_> = map
|
||||
.geometry
|
||||
.indices
|
||||
.chunks_exact(3)
|
||||
.map(|t| t.to_vec())
|
||||
.collect();
|
||||
let mut partitioned = Vec::new();
|
||||
for (material, indices) in chunks {
|
||||
for tri in indices.chunks_exact(3) {
|
||||
assert_eq!(
|
||||
material + 1,
|
||||
map.geometry.vertices[tri[0] as usize].material as usize
|
||||
);
|
||||
partitioned.push(tri.to_vec());
|
||||
}
|
||||
}
|
||||
original.sort();
|
||||
partitioned.sort();
|
||||
assert_eq!(partitioned, original);
|
||||
}
|
||||
#[test]
|
||||
fn render_material_sharing_requires_identical_rendered_fields() {
|
||||
let mut map = demo();
|
||||
let mut copy = demo().materials.remove(0);
|
||||
copy.name = "another source object".into();
|
||||
copy.audio = 42;
|
||||
map.materials.push(copy);
|
||||
let texture_ids = render_texture_ids(&map.textures);
|
||||
assert_eq!(render_material_ids(&map.materials, &texture_ids), [0, 0]);
|
||||
map.materials[1].roughness = 0.1234;
|
||||
assert_eq!(render_material_ids(&map.materials, &texture_ids), [0, 1]);
|
||||
map.materials[1].roughness = map.materials[0].roughness;
|
||||
map.materials[1].textures[1] = 0;
|
||||
assert_eq!(render_material_ids(&map.materials, &texture_ids), [0, 1]);
|
||||
}
|
||||
#[test]
|
||||
fn texture_sharing_preserves_pixels_dimensions_and_color_space() {
|
||||
let texture = |name: &str, color_space, pixel| skate_data::skate_map::Texture {
|
||||
name: name.into(),
|
||||
width: 1,
|
||||
height: 1,
|
||||
color_space,
|
||||
rgba: vec![pixel, 0, 0, 255],
|
||||
};
|
||||
let textures = vec![
|
||||
texture("first", 1, 30),
|
||||
texture("duplicate", 1, 30),
|
||||
texture("linear", 0, 30),
|
||||
texture("different", 1, 31),
|
||||
];
|
||||
assert_eq!(render_texture_ids(&textures), [0, 1, 1, 3, 4]);
|
||||
}
|
||||
fn material() -> RetailContactMaterial {
|
||||
RetailContactMaterial {
|
||||
static_friction: 0.,
|
||||
|
||||
@@ -24,7 +24,7 @@ fn spawn(
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
mut images: ResMut<Assets<Image>>,
|
||||
config: Res<crate::config::Config>,
|
||||
mut config: ResMut<crate::config::Config>,
|
||||
) {
|
||||
commands
|
||||
.spawn((PlayerRoot, Transform::default(), Visibility::default()))
|
||||
@@ -33,8 +33,10 @@ fn spawn(
|
||||
GltfAssetLabel::Scene(0).from_asset(manifest.0.character_scene.clone()),
|
||||
)));
|
||||
});
|
||||
if let Some(map) = &config.map {
|
||||
crate::skate_world::spawn(map, &mut commands, &mut meshes, &mut materials, &mut images);
|
||||
if let Some(map) = config.map.take() {
|
||||
// Physics has already consumed the package. Render assets own their
|
||||
// uploaded data; keeping another full city package wastes gigabytes.
|
||||
crate::skate_world::spawn(&map, &mut commands, &mut meshes, &mut materials, &mut images);
|
||||
return;
|
||||
}
|
||||
let colors = [
|
||||
|
||||
Reference in new issue
Block a user