mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Fix native hair coverage and verify customiser gameplay effects
This commit is contained in:
1 parent
6e686edb77
commit
61dbdfce8e
11 files changed
+438
-54
No files matched your search
@@ -972,12 +972,19 @@ fn preferences(
|
||||
state: Res<Customiser>,
|
||||
mut physics: ResMut<crate::physics::GamePhysics>,
|
||||
mut skater: ResMut<crate::physics::SkaterRuntime>,
|
||||
) {
|
||||
apply_preferences(&state.draft, &mut physics, &mut skater.animation);
|
||||
}
|
||||
pub(crate) fn apply_preferences(
|
||||
profile: &Value,
|
||||
physics: &mut crate::physics::GamePhysics,
|
||||
animation: &mut crate::skater_animation::SkaterAnimation,
|
||||
) {
|
||||
physics.set_equipment_preferences(
|
||||
scalar(&state.draft, "truck", 0.7) as f32,
|
||||
scalar(&state.draft, "wheel", 0.7) as f32,
|
||||
scalar(profile, "truck", 0.7) as f32,
|
||||
scalar(profile, "wheel", 0.7) as f32,
|
||||
);
|
||||
physics.set_gesture_preferences(state.draft["gestures"].as_object().map(|g| {
|
||||
physics.set_gesture_preferences(profile["gestures"].as_object().map(|g| {
|
||||
// ResetGestureSet824FA730 marks all 37 entries available and selects
|
||||
// the first four in table order for Up,Down,Left,Right.
|
||||
std::array::from_fn(|i| {
|
||||
@@ -986,16 +993,15 @@ fn preferences(
|
||||
.unwrap_or(i as u64) as u32
|
||||
})
|
||||
}));
|
||||
skater.animation.set_customisation(
|
||||
scalar(&state.draft, "stance", 1.) as u32,
|
||||
scalar(&state.draft, "style", 0.) as u32,
|
||||
animation.set_customisation(
|
||||
scalar(profile, "stance", 1.) as u32,
|
||||
scalar(profile, "style", 0.) as u32,
|
||||
);
|
||||
skater
|
||||
.animation
|
||||
animation
|
||||
.motion
|
||||
.animation
|
||||
.posture
|
||||
.set_profile(scalar(&state.draft, "posture", 0.) as u32);
|
||||
.set_profile(scalar(profile, "posture", 0.) as u32);
|
||||
}
|
||||
fn draw(
|
||||
mut commands: Commands,
|
||||
|
||||
@@ -14,6 +14,10 @@ pub(crate) struct SkinStamp {
|
||||
#[texture(100)]
|
||||
#[sampler(101)]
|
||||
pub texture: Option<Handle<Image>>,
|
||||
/// cac_hair_defaultPS samples opacity.r on TEXCOORD1, independently of diffuse alpha.
|
||||
#[texture(103)]
|
||||
#[sampler(104)]
|
||||
pub hair_opacity: Option<Handle<Image>>,
|
||||
/// Scale.xy, translation.zw in the authored secondary UV stream.
|
||||
#[uniform(102)]
|
||||
pub transform: Vec4,
|
||||
@@ -27,12 +31,16 @@ impl MaterialExtension for SkinStamp {
|
||||
fn fragment_shader() -> ShaderRef {
|
||||
"embedded://skate3rust/customiser_stamp.wgsl".into()
|
||||
}
|
||||
fn prepass_fragment_shader() -> ShaderRef {
|
||||
"embedded://skate3rust/customiser_prepass.wgsl".into()
|
||||
}
|
||||
fn deferred_fragment_shader() -> ShaderRef {
|
||||
Self::fragment_shader()
|
||||
}
|
||||
}
|
||||
pub(crate) fn register(app: &mut App) {
|
||||
embedded_asset!(app, "customiser_stamp.wgsl");
|
||||
embedded_asset!(app, "customiser_prepass.wgsl");
|
||||
app.add_plugins(MaterialPlugin::<SkaterMaterial>::default());
|
||||
}
|
||||
|
||||
@@ -118,28 +126,57 @@ mod tests {
|
||||
let h = assets.add(shader.clone());
|
||||
cache.set_shader(h.id(), shader);
|
||||
}
|
||||
let shader = Shader::from_wgsl(
|
||||
include_str!("customiser_stamp.wgsl"),
|
||||
"customiser_stamp.wgsl",
|
||||
);
|
||||
let h = assets.add(shader.clone());
|
||||
cache.set_shader(h.id(), shader);
|
||||
for secondary_uv in [false, true] {
|
||||
let mut d = defs.clone();
|
||||
for name in [
|
||||
"VERTEX_UVS_A",
|
||||
"VERTEX_OUTPUT_INSTANCE_INDEX",
|
||||
"VERTEX_NORMALS",
|
||||
"MESH_PIPELINE",
|
||||
"STANDARD_MATERIAL",
|
||||
"STANDARD_MATERIAL_BASE_COLOR_TEXTURE",
|
||||
] {
|
||||
d.push(ShaderDefVal::Bool(name.into(), true));
|
||||
for (source, path, prepass) in [
|
||||
(
|
||||
include_str!("customiser_stamp.wgsl"),
|
||||
"customiser_stamp.wgsl",
|
||||
false,
|
||||
),
|
||||
(
|
||||
include_str!("customiser_prepass.wgsl"),
|
||||
"customiser_prepass.wgsl",
|
||||
true,
|
||||
),
|
||||
] {
|
||||
let shader = Shader::from_wgsl(source, path);
|
||||
let h = assets.add(shader.clone());
|
||||
cache.set_shader(h.id(), shader);
|
||||
for secondary_uv in [false, true] {
|
||||
let mut d = defs.clone();
|
||||
for name in [
|
||||
"VERTEX_UVS",
|
||||
"MAY_DISCARD",
|
||||
"VERTEX_UVS_A",
|
||||
"VERTEX_OUTPUT_INSTANCE_INDEX",
|
||||
"VERTEX_NORMALS",
|
||||
"MESH_PIPELINE",
|
||||
"STANDARD_MATERIAL",
|
||||
"STANDARD_MATERIAL_BASE_COLOR_TEXTURE",
|
||||
] {
|
||||
d.push(ShaderDefVal::Bool(name.into(), true));
|
||||
}
|
||||
if secondary_uv {
|
||||
d.push(ShaderDefVal::Bool("VERTEX_UVS_B".into(), true));
|
||||
}
|
||||
if prepass {
|
||||
d.push(ShaderDefVal::Bool("PREPASS_PIPELINE".into(), true));
|
||||
}
|
||||
cache
|
||||
.get(&(), 0, h.id(), &d)
|
||||
.unwrap_or_else(|e| panic!("{path} secondary UV {secondary_uv}: {e:?}"));
|
||||
if prepass {
|
||||
for name in [
|
||||
"PREPASS_FRAGMENT",
|
||||
"NORMAL_PREPASS",
|
||||
"NORMAL_PREPASS_OR_DEFERRED_PREPASS",
|
||||
] {
|
||||
d.push(ShaderDefVal::Bool(name.into(), true));
|
||||
}
|
||||
cache
|
||||
.get(&(), 0, h.id(), &d)
|
||||
.unwrap_or_else(|e| panic!("Normal {path} UV {secondary_uv}: {e:?}"));
|
||||
}
|
||||
}
|
||||
if secondary_uv {
|
||||
d.push(ShaderDefVal::Bool("VERTEX_UVS_B".into(), true));
|
||||
}
|
||||
cache.get(&(), 0, h.id(), &d).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,8 @@ pub(crate) struct Material {
|
||||
normal: Option<String>,
|
||||
rough: Option<String>,
|
||||
alpha: bool,
|
||||
#[serde(default)]
|
||||
opacity: Option<String>,
|
||||
tint: [f32; 3],
|
||||
metallic: f32,
|
||||
roughness: f32,
|
||||
@@ -413,12 +415,19 @@ impl Parts {
|
||||
let diffuse = load(&m.diffuse, false);
|
||||
let normal = m.normal.as_ref().map(|p| load(p, true));
|
||||
let rough = m.rough.as_ref().map(|p| load(p, true));
|
||||
let opacity = m.opacity.as_ref().map(|p| load(p, true));
|
||||
let material = materials.add(SkaterMaterial {
|
||||
base: StandardMaterial {
|
||||
base_color: Color::srgb(m.tint[0], m.tint[1], m.tint[2]),
|
||||
base_color_texture: Some(diffuse),
|
||||
normal_map_texture: normal,
|
||||
metallic_roughness_texture: rough,
|
||||
double_sided: m.opacity.is_some(),
|
||||
cull_mode: if m.opacity.is_some() {
|
||||
None
|
||||
} else {
|
||||
Some(bevy::render::render_resource::Face::Back)
|
||||
},
|
||||
metallic: m.metallic,
|
||||
perceptual_roughness: if m.rough.is_some() { 1. } else { m.roughness },
|
||||
alpha_mode: if m.alpha {
|
||||
@@ -428,7 +437,15 @@ impl Parts {
|
||||
},
|
||||
..default()
|
||||
},
|
||||
extension: SkinStamp::default(),
|
||||
extension: SkinStamp {
|
||||
hair_opacity: opacity,
|
||||
enabled: if m.opacity.is_some() {
|
||||
Vec4::Y
|
||||
} else {
|
||||
Vec4::ZERO
|
||||
},
|
||||
..default()
|
||||
},
|
||||
});
|
||||
self.materials.insert(id.into(), (material, images));
|
||||
}
|
||||
@@ -438,10 +455,15 @@ pub(crate) fn setup(
|
||||
config: Res<crate::config::Config>,
|
||||
server: Res<AssetServer>,
|
||||
) {
|
||||
let library = std::fs::read(config.asset_root.join("private/customisation/library.json"))
|
||||
.ok()
|
||||
.and_then(|b| serde_json::from_slice::<Library>(&b).ok())
|
||||
.unwrap_or_default();
|
||||
let library = std::fs::read(
|
||||
config
|
||||
.asset_root
|
||||
.join("private/customisation/library-v3.json"),
|
||||
)
|
||||
.or_else(|_| std::fs::read(config.asset_root.join("private/customisation/library.json")))
|
||||
.ok()
|
||||
.and_then(|b| serde_json::from_slice::<Library>(&b).ok())
|
||||
.unwrap_or_default();
|
||||
let geometry = library
|
||||
.models
|
||||
.iter()
|
||||
@@ -638,7 +660,17 @@ pub(crate) fn update(
|
||||
let selected = stamp["id"]
|
||||
.as_str()
|
||||
.and_then(|id| parts.library.tattoos.get(id).map(|t| (id, t)));
|
||||
let mut extension = SkinStamp::default();
|
||||
let mut extension = SkinStamp {
|
||||
hair_opacity: materials
|
||||
.get(&handle)
|
||||
.and_then(|m| m.extension.hair_opacity.clone()),
|
||||
enabled: if source.opacity.is_some() {
|
||||
Vec4::Y
|
||||
} else {
|
||||
Vec4::ZERO
|
||||
},
|
||||
..default()
|
||||
};
|
||||
if let Some((tid, t)) = selected {
|
||||
let key = match stamp["side"].as_u64().unwrap_or(0) {
|
||||
1 => "StampUVConstraintQ4",
|
||||
@@ -659,6 +691,7 @@ pub(crate) fn update(
|
||||
transform,
|
||||
rectangle,
|
||||
enabled: Vec4::X,
|
||||
..default()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
// Bevy 0.18.1 pbr_prepass.wgsl, MIT/Apache-2.0, copyright Bevy contributors.
|
||||
// Match native hair coverage in colour, depth and shadow passes.
|
||||
struct SkinStamp {
|
||||
transform: vec4<f32>,
|
||||
rectangle: vec4<f32>,
|
||||
enabled: vec4<f32>,
|
||||
}
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(103) var hair_opacity: texture_2d<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(104) var hair_sampler: sampler;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(102) var<uniform> stamp: SkinStamp;
|
||||
#import bevy_pbr::{
|
||||
pbr_prepass_functions,
|
||||
pbr_bindings,
|
||||
pbr_bindings::material,
|
||||
pbr_types,
|
||||
pbr_functions,
|
||||
pbr_functions::SampleBias,
|
||||
prepass_io,
|
||||
mesh_bindings::mesh,
|
||||
mesh_view_bindings::view,
|
||||
}
|
||||
|
||||
#import bevy_render::bindless::{bindless_samplers_filtering, bindless_textures_2d}
|
||||
|
||||
#ifdef MESHLET_MESH_MATERIAL_PASS
|
||||
#import bevy_pbr::meshlet_visibility_buffer_resolve::resolve_vertex_output
|
||||
#endif
|
||||
|
||||
#ifdef BINDLESS
|
||||
#import bevy_pbr::pbr_bindings::material_indices
|
||||
#endif // BINDLESS
|
||||
|
||||
fn customiser_alpha_discard(in: prepass_io::VertexOutput) {
|
||||
#ifdef VERTEX_UVS_B
|
||||
let alpha = textureSample(hair_opacity, hair_sampler, in.uv_b).r;
|
||||
if stamp.enabled.y > 0.5 {
|
||||
let coverage = pbr_functions::alpha_discard(pbr_bindings::material, vec4<f32>(1.0, 1.0, 1.0, alpha));
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
pbr_prepass_functions::prepass_alpha_discard(in);
|
||||
}
|
||||
|
||||
#ifdef PREPASS_FRAGMENT
|
||||
@fragment
|
||||
fn fragment(
|
||||
#ifdef MESHLET_MESH_MATERIAL_PASS
|
||||
@builtin(position) frag_coord: vec4<f32>,
|
||||
#else
|
||||
in: prepass_io::VertexOutput,
|
||||
@builtin(front_facing) is_front: bool,
|
||||
#endif
|
||||
) -> prepass_io::FragmentOutput {
|
||||
#ifdef MESHLET_MESH_MATERIAL_PASS
|
||||
let in = resolve_vertex_output(frag_coord);
|
||||
let is_front = true;
|
||||
#else // MESHLET_MESH_MATERIAL_PASS
|
||||
|
||||
#ifdef BINDLESS
|
||||
let slot = mesh[in.instance_index].material_and_lightmap_bind_group_slot & 0xffffu;
|
||||
let flags = pbr_bindings::material_array[material_indices[slot].material].flags;
|
||||
let uv_transform = pbr_bindings::material_array[material_indices[slot].material].uv_transform;
|
||||
#else // BINDLESS
|
||||
let flags = pbr_bindings::material.flags;
|
||||
let uv_transform = pbr_bindings::material.uv_transform;
|
||||
#endif // BINDLESS
|
||||
|
||||
// If we're in the crossfade section of a visibility range, conditionally
|
||||
// discard the fragment according to the visibility pattern.
|
||||
#ifdef VISIBILITY_RANGE_DITHER
|
||||
pbr_functions::visibility_range_dither(in.position, in.visibility_range_dither);
|
||||
#endif // VISIBILITY_RANGE_DITHER
|
||||
|
||||
customiser_alpha_discard(in);
|
||||
#endif // MESHLET_MESH_MATERIAL_PASS
|
||||
|
||||
var out: prepass_io::FragmentOutput;
|
||||
|
||||
#ifdef UNCLIPPED_DEPTH_ORTHO_EMULATION
|
||||
out.frag_depth = in.unclipped_depth;
|
||||
#endif // UNCLIPPED_DEPTH_ORTHO_EMULATION
|
||||
|
||||
#ifdef NORMAL_PREPASS
|
||||
// NOTE: Unlit bit not set means == 0 is true, so the true case is if lit
|
||||
if (flags & pbr_types::STANDARD_MATERIAL_FLAGS_UNLIT_BIT) == 0u {
|
||||
let double_sided = (flags & pbr_types::STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT) != 0u;
|
||||
|
||||
let world_normal = pbr_functions::prepare_world_normal(
|
||||
in.world_normal,
|
||||
double_sided,
|
||||
is_front,
|
||||
);
|
||||
|
||||
var normal = world_normal;
|
||||
|
||||
#ifdef VERTEX_UVS
|
||||
#ifdef VERTEX_TANGENTS
|
||||
#ifdef STANDARD_MATERIAL_NORMAL_MAP
|
||||
|
||||
// TODO: Transforming UVs mean we need to apply derivative chain rule for meshlet mesh material pass
|
||||
#ifdef STANDARD_MATERIAL_NORMAL_MAP_UV_B
|
||||
let uv = (uv_transform * vec3(in.uv_b, 1.0)).xy;
|
||||
#else
|
||||
let uv = (uv_transform * vec3(in.uv, 1.0)).xy;
|
||||
#endif
|
||||
|
||||
// Fill in the sample bias so we can sample from textures.
|
||||
var bias: SampleBias;
|
||||
#ifdef MESHLET_MESH_MATERIAL_PASS
|
||||
bias.ddx_uv = in.ddx_uv;
|
||||
bias.ddy_uv = in.ddy_uv;
|
||||
#else // MESHLET_MESH_MATERIAL_PASS
|
||||
bias.mip_bias = view.mip_bias;
|
||||
#endif // MESHLET_MESH_MATERIAL_PASS
|
||||
|
||||
let Nt =
|
||||
#ifdef MESHLET_MESH_MATERIAL_PASS
|
||||
textureSampleGrad(
|
||||
#else // MESHLET_MESH_MATERIAL_PASS
|
||||
textureSampleBias(
|
||||
#endif // MESHLET_MESH_MATERIAL_PASS
|
||||
#ifdef BINDLESS
|
||||
bindless_textures_2d[material_indices[slot].normal_map_texture],
|
||||
bindless_samplers_filtering[material_indices[slot].normal_map_sampler],
|
||||
#else // BINDLESS
|
||||
pbr_bindings::normal_map_texture,
|
||||
pbr_bindings::normal_map_sampler,
|
||||
#endif // BINDLESS
|
||||
uv,
|
||||
#ifdef MESHLET_MESH_MATERIAL_PASS
|
||||
bias.ddx_uv,
|
||||
bias.ddy_uv,
|
||||
#else // MESHLET_MESH_MATERIAL_PASS
|
||||
bias.mip_bias,
|
||||
#endif // MESHLET_MESH_MATERIAL_PASS
|
||||
).rgb;
|
||||
let TBN = pbr_functions::calculate_tbn_mikktspace(normal, in.world_tangent);
|
||||
|
||||
normal = pbr_functions::apply_normal_mapping(
|
||||
flags,
|
||||
TBN,
|
||||
double_sided,
|
||||
is_front,
|
||||
Nt,
|
||||
);
|
||||
|
||||
#endif // STANDARD_MATERIAL_NORMAL_MAP
|
||||
#endif // VERTEX_TANGENTS
|
||||
#endif // VERTEX_UVS
|
||||
|
||||
out.normal = vec4(normal * 0.5 + vec3(0.5), 1.0);
|
||||
} else {
|
||||
out.normal = vec4(in.world_normal * 0.5 + vec3(0.5), 1.0);
|
||||
}
|
||||
#endif // NORMAL_PREPASS
|
||||
|
||||
#ifdef MOTION_VECTOR_PREPASS
|
||||
#ifdef MESHLET_MESH_MATERIAL_PASS
|
||||
out.motion_vector = in.motion_vector;
|
||||
#else
|
||||
out.motion_vector = pbr_prepass_functions::calculate_motion_vector(in.world_position, in.previous_world_position);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
return out;
|
||||
}
|
||||
#else
|
||||
@fragment
|
||||
fn fragment(in: prepass_io::VertexOutput) {
|
||||
customiser_alpha_discard(in);
|
||||
}
|
||||
#endif // PREPASS_FRAGMENT
|
||||
@@ -7,6 +7,8 @@ struct SkinStamp {
|
||||
}
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(100) var stamp_texture: texture_2d<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(101) var stamp_sampler: sampler;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(103) var hair_opacity: texture_2d<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(104) var hair_sampler: sampler;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(102) var<uniform> stamp: SkinStamp;
|
||||
#import bevy_pbr::{
|
||||
pbr_types,
|
||||
@@ -77,6 +79,10 @@ fn fragment(
|
||||
|
||||
|
||||
#ifdef VERTEX_UVS_B
|
||||
// The diffuse alpha is NOT coverage. Hair has a separate red-channel
|
||||
// opacity map on secondary UVs (cac_hair_defaultVS/PS).
|
||||
let hair_alpha = textureSample(hair_opacity, hair_sampler, in.uv_b).r;
|
||||
pbr_input.material.base_color.a = mix(pbr_input.material.base_color.a, hair_alpha, stamp.enabled.y);
|
||||
// cacstamp_skin_defaultPS: skin * (1 + (decal.rgb - 1) * decal.a).
|
||||
// Its VS transforms TEXCOORD1 by the two i_customGraphic rows.
|
||||
let stamp_uv = in.uv_b * stamp.transform.xy + stamp.transform.zw;
|
||||
|
||||
@@ -293,3 +293,21 @@ mod tests {
|
||||
super::super::GroundState::load(&data, "default", true).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[test]
|
||||
#[ignore = "requires extracted private stock collections"]
|
||||
fn customiser_truck_tightness_changes_authored_steering() {
|
||||
use skate_core::riding::steering::{SteeringInput, calculate_tilt};
|
||||
let root = std::path::PathBuf::from(std::env::var_os("SKATE3_ASSET_ROOT").unwrap());
|
||||
let data = Collections::load(&root).unwrap();
|
||||
let settings = GroundSettings::load(&data, "default", "default").unwrap();
|
||||
let samples: Vec<_> = [0.0, 0.7, 1.0].into_iter().map(|tightness| calculate_tilt(
|
||||
&settings.steering,
|
||||
SteeringInput { turn: 0.6, absolute_body_speed: 5.0, flipped_controls_scalar: 1.0,
|
||||
truck_tightness: tightness, ..Default::default() }, None, None,
|
||||
)).collect();
|
||||
assert!(samples[0].abs() > samples[2].abs());
|
||||
assert!((samples[2] / samples[0] - settings.steering.tight_trucks_scalar).abs() < 0.00001);
|
||||
eprintln!("Authored truck tightness tilt samples: {samples:?}; tight scalar {}", settings.steering.tight_trucks_scalar);
|
||||
}
|
||||
@@ -356,7 +356,7 @@ fn stock_skater_startup_builds_the_graphs_and_physical_body_from_the_same_rig()
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires private stock skater, animation banks and collections"]
|
||||
fn stock_wheel_hardness_reaches_ground_force_and_torque() {
|
||||
fn customiser_equipment_reaches_ground_force_and_torque() {
|
||||
let root = std::env::var_os("SKATE3_ASSET_ROOT").expect("set SKATE3_ASSET_ROOT");
|
||||
let root = std::path::Path::new(&root);
|
||||
let manifest = skate_data::GameAssets::load(root).unwrap();
|
||||
@@ -365,6 +365,10 @@ fn stock_wheel_hardness_reaches_ground_force_and_torque() {
|
||||
for hardness in [0.0_f32, 0.7, 1.0] {
|
||||
let mut physics = GamePhysics::load_with_difficulty(root, None, crate::difficulty::Difficulty::Normal).unwrap();
|
||||
let mut skater = SkaterRuntime::load(root, &graphs, &physics, "normal").unwrap();
|
||||
crate::customiser::apply_preferences(
|
||||
&serde_json::json!({"truck": hardness, "wheel": hardness}),
|
||||
&mut physics, &mut skater.animation,
|
||||
);
|
||||
let mut controls = PlayerControls::default();
|
||||
let mut camera = crate::camera::CameraRuntime::load(root).unwrap();
|
||||
let input = crate::input::ControllerInput::default();
|
||||
@@ -382,7 +386,8 @@ fn stock_wheel_hardness_reaches_ground_force_and_torque() {
|
||||
// Replay identical observed sideways travel at the Ground input boundary.
|
||||
// Only the published profile input changes; run the actual Ground adapter.
|
||||
let p = &mut skater.player_input.processed;
|
||||
p.scalar_2764 = hardness;
|
||||
assert_eq!(p.scalar_2764, hardness);
|
||||
assert_eq!(p.truck_tightness_2760, hardness);
|
||||
p.vectors_400_416 = [[1.0_f32, 0.0, 3.0, 0.0].map(f32::to_bits); 2];
|
||||
p.scalar_2656 = 10.0_f32.sqrt();
|
||||
skater.ground.state.elapsed_2648 = 0.25;
|
||||
@@ -454,3 +459,48 @@ fn stock_geometry_loads_and_reaches_the_live_board_solver() {
|
||||
let deck = physics.board.part_transforms()[BodyId::Deck.index()];
|
||||
assert!(deck.translation.y > ground::HEIGHT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires private stock animation banks and collections; no window or renderer"]
|
||||
fn customiser_styles_and_postures_change_stock_pose() {
|
||||
use skate_core::animation::playback::{PlayAnimation, PlayAnimationInstance, TransitionSettings};
|
||||
use skate_core::animation::playback_tree::{Evaluation, PoseCommand};
|
||||
let root = std::path::PathBuf::from(std::env::var_os("SKATE3_ASSET_ROOT").unwrap());
|
||||
let manifest = skate_data::GameAssets::load(&root).unwrap();
|
||||
let graphs = crate::graph_runtime::StockGraphs::load(&root, &manifest).unwrap();
|
||||
let mut physics = GamePhysics::load(&root).unwrap();
|
||||
let mut skater = SkaterRuntime::load(&root, &graphs, &physics, "normal").unwrap();
|
||||
let operation = PlayAnimation {
|
||||
animation: "S_R_PUSHLSP_HSTR_N_0_CYC1".into(), switch_animation: None, mirror_animation: None,
|
||||
no_board_animation: None, playback_speed: 1.0, apply_posture: true,
|
||||
transition: TransitionSettings { kind: 1, seconds: 0.0, under: 0, matching: 0, use_channels_from_weights: false },
|
||||
parameters: vec![],
|
||||
};
|
||||
let mut samples = Vec::new();
|
||||
for (style, posture) in [(0,0), (1,0), (2,0), (3,0), (0,1), (0,2), (0,3)] {
|
||||
crate::customiser::apply_preferences(
|
||||
&serde_json::json!({"style":style, "posture":posture}),
|
||||
&mut physics, &mut skater.animation,
|
||||
);
|
||||
let motion = &mut skater.animation.motion;
|
||||
PlayAnimationInstance::default().begin(&operation, &mut motion.playback_context, &mut motion.animation).unwrap();
|
||||
motion.animation.apply_parameters().unwrap();
|
||||
motion.animation.seek_current_fraction(0.35);
|
||||
let commands = motion.animation.evaluate_pose(Evaluation { cull_threshold: 0.0001, update_history: false }).unwrap();
|
||||
let pose = skater.animation.evaluator.evaluate(&commands).unwrap();
|
||||
let names: Vec<_> = commands.iter().filter_map(|c| match c {
|
||||
PoseCommand::Clip { name, .. } | PoseCommand::Pose { name } => Some(name.clone()), _ => None,
|
||||
}).collect();
|
||||
eprintln!("Style {style}, posture {posture}: {names:?}");
|
||||
if let Some(baseline) = samples.first() {
|
||||
assert!(&pose != baseline, "Style {style}, posture {posture} did not change the evaluated stock pose");
|
||||
}
|
||||
samples.push(pose);
|
||||
}
|
||||
skater.animation.set_customisation(1,0);
|
||||
let stance = skater.animation.stance();
|
||||
skater.animation.set_customisation(0,0);
|
||||
assert_ne!(skater.animation.stance(), stance);
|
||||
skater.animation.set_customisation(1,0);
|
||||
assert_eq!(skater.animation.stance(), stance);
|
||||
}
|
||||
@@ -1,10 +1,54 @@
|
||||
# Character customiser V2
|
||||
# Character customiser V3
|
||||
|
||||
The dedicated local test build uses `Play Character Customiser V2.bat` and
|
||||
`skate3rust-customiser-v2.exe`. Open Escape/Start, then Character customiser.
|
||||
The dedicated local test build uses `Play Character Customiser V3.bat` and
|
||||
`skate3rust-customiser-v3.exe`. Open Escape/Start, then Character customiser.
|
||||
The game and launcher have not been executed for validation; the build is ready
|
||||
for user playtesting. No existing game executable is replaced.
|
||||
|
||||
## Hair coverage and gameplay verification (V3)
|
||||
|
||||
Hair now uses its independent opacity texture's **red channel on TEXCOORD1**.
|
||||
The owned `cac_hair_defaultVS` copies primary/secondary UVs into XY/ZW, and
|
||||
`cac_hair_defaultPS` samples diffuse RGB from XY and opacity R from ZW. Output
|
||||
alpha is opacity R times `i_params.x`. The old combined diffuse-alpha texture
|
||||
was incorrect: on the moussed-up example only 0.29% of diffuse pixels exceed the
|
||||
0.5 cutoff, versus 42.18% of the opacity red channel. That discarded most raised
|
||||
hair geometry. All 31 opacity-bearing hair materials now retain separate maps.
|
||||
Thin hair cards render from both sides. The colour and depth/shadow passes use
|
||||
the same corrected coverage; existing PBR lighting formulas remain unchanged.
|
||||
|
||||
V3 uses `library-v3.json` when present, with a legacy `library.json` fallback.
|
||||
For side-by-side preparation set `"library_index":"library-v3.json"` in the
|
||||
private preparation config. V2's index, executable and launcher are retained.
|
||||
|
||||
Offline checks use the same `apply_preferences` function called by the menu,
|
||||
then the real stock actor publication, processed physics input and consumers:
|
||||
|
||||
- Truck tightness reaches processed field 2760. At identical steering input,
|
||||
loose/default/tight produce tilt 0.08955247 / 0.07074646 / 0.062686734. The
|
||||
tight/loose ratio is the loaded native `tight_trucks_scalar`, 0.7.
|
||||
- Wheel hardness reaches processed field 2764. At identical sideways travel,
|
||||
soft/default/hard produce side force -23.31247 / -15.153105 / -11.656235.
|
||||
Ratios match `physicswheels/default/SoftestWheelSideFrictionScalar` from the
|
||||
extracted collection data. Straightening torque also changes, from -1.113991
|
||||
to 1.1697853 at the endpoints. These are controlled test samples, not universal
|
||||
steering angles, grip percentages or speed ratings.
|
||||
- Standard/Loose/Gonzo/Aggressive select distinct stock push clips through the
|
||||
`PROSKATER` selector: default, Hsu, Gonzales and PJ respectively. Evaluated bone
|
||||
poses differ. Stiff/Slouch/Buff add their respective authored posture poses
|
||||
and change the evaluated result. Switching natural stance changes its basis
|
||||
and switching back restores it.
|
||||
|
||||
The stock riding idle is shared between styles. Style differences appear in
|
||||
specific authored movements (including pushes and selected tricks), and posture
|
||||
is applied on eligible motion-tree construction. Wheel/truck merchandise models
|
||||
are appearance choices; **Wheel hardness** and **Truck tightness** are the native
|
||||
handling controls. No invented brand-dependent statistics are added.
|
||||
|
||||
Shader validation covers colour, depth/shadow and normal prepass paths, each
|
||||
with and without secondary UVs. Tests run without a game window or GPU. The hair
|
||||
fix still needs the user's visual playtest; no game process was launched here.
|
||||
|
||||
## Menu and controls
|
||||
|
||||
Four main sections: **Body, Clothes, Board, Style**. Item lists are alphabetical,
|
||||
@@ -75,9 +119,9 @@ python -m tools.asset_pipeline.customisation_library <private-worker.json>
|
||||
python -m tools.asset_pipeline.customisation_profiles <private-customisation-directory>
|
||||
```
|
||||
|
||||
The old worker remains an offline compatibility tool. It is not the V2 runtime.
|
||||
The old worker remains an offline compatibility tool. It is not the runtime.
|
||||
|
||||
Validation: 21 Python tests and five Rust customiser tests pass. Owned-data checks
|
||||
Validation: 21 Python tests and eight Rust customiser tests pass. Owned-data checks
|
||||
cover all 480 GLBs, indices, skin weights, 22 GPU target slots, texture references,
|
||||
both default outfits, all selectable model/material combinations against those
|
||||
outfits, presets, search, colour selection, JSON round trips, and temporary tattoo
|
||||
|
||||
@@ -107,7 +107,7 @@ def prepare(config):
|
||||
mat=catalog['materials'][mid];tex={t['channel']:t['id'] for t in mat['textures']}
|
||||
if 'diffuse' not in tex:continue
|
||||
diffuse=texture(tex['diffuse'])
|
||||
if 'alpha' in tex:
|
||||
if 'alpha' in tex and slot!='Hair':
|
||||
alpha_path=assets/texture(tex['alpha']);combined=out/'textures'/(tex['diffuse']+'_'+tex['alpha']+'.png')
|
||||
if not combined.exists():
|
||||
im=Image.open(assets/diffuse).convert('RGBA');alpha=Image.open(alpha_path).convert('RGBA')
|
||||
@@ -118,6 +118,7 @@ def prepare(config):
|
||||
mat_data[mid]=dict(name=variant_label(slot,model['name'],v['name']),flags={k[4:]:val for k,val in mat['flags'].items() if val},
|
||||
diffuse=diffuse,normal=texture(tex['normal'],'normal') if 'normal' in tex else None,
|
||||
rough=texture(tex['specular'],'rough') if 'specular' in tex else None,
|
||||
opacity=texture(tex['alpha']) if slot=='Hair' and 'alpha' in tex else None,
|
||||
alpha='alpha' in tex,tint=defaults.get(mid,{}).get('tint',
|
||||
[0.72,0.57,0.49] if mat['flags'].get('cas.SkinTone')=='light' else
|
||||
[0.33,0.26,0.23] if mat['flags'].get('cas.SkinTone')=='dark' else
|
||||
@@ -148,7 +149,7 @@ def prepare(config):
|
||||
colours.sort(key=lambda c:c['name'])
|
||||
library=dict(version=3,colours=colours,tattoos=tattoos,models=model_data,materials=mat_data,defaults={'male':male,'female':female},
|
||||
morphs=recipe_base['morph_assembly']['live_targets'],errors=errors)
|
||||
(cache/'library.json').write_text(json.dumps(library,separators=(',',':')))
|
||||
(cache/config.get('library_index','library.json')).write_text(json.dumps(library,separators=(',',':')))
|
||||
print('LIBRARY_READY',len(model_data),len(mat_data),'errors',errors,flush=True)
|
||||
return library
|
||||
|
||||
|
||||
@@ -61,6 +61,9 @@ class LibraryTests(unittest.TestCase):
|
||||
for target in primitive['targets']:
|
||||
self.assertEqual(len(array(target['POSITION'])),count)
|
||||
self.assertEqual(len(array(target['NORMAL'])),count)
|
||||
if part['slot'] == 'Hair':
|
||||
self.assertIn('TEXCOORD_1', a)
|
||||
self.assertEqual(len(array(a['TEXCOORD_1'])), count)
|
||||
if (part['slot']=='Arm' and part['flags'].get('ArmModelType')=='shoulder') or (part['slot']=='Leg' and part['flags'].get('LegModelType') in ['thigh','thighdown'] and part['flags'].get('RequiresSockStyle','none') in ['','none']):
|
||||
uv = array(a['TEXCOORD_1'])
|
||||
for side in ['Q3','Q4']:
|
||||
@@ -68,8 +71,22 @@ class LibraryTests(unittest.TestCase):
|
||||
inside=(uv[:,0]>=left)&(uv[:,0]<=right)&(uv[:,1]>=1-top)&(uv[:,1]<=1-bottom)
|
||||
self.assertTrue(inside.any(),part['name']+' '+side)
|
||||
for material in library['materials'].values():
|
||||
for channel in ['diffuse','normal','rough']:
|
||||
if material[channel]: self.assertTrue((assets/material[channel]).is_file())
|
||||
for channel in ['diffuse','normal','rough','opacity']:
|
||||
if material.get(channel): self.assertTrue((assets/material[channel]).is_file())
|
||||
# V3 preserves the native hair opacity texture independently of diffuse.
|
||||
hair = [m for m in library['materials'].values() if m.get('opacity')]
|
||||
self.assertEqual(len(hair), 31)
|
||||
for material in hair:
|
||||
self.assertNotEqual(material['diffuse'], material['opacity'])
|
||||
self.assertIn('/decoded/', material['diffuse'])
|
||||
self.assertTrue(material['alpha'])
|
||||
# Native moussed-up hair demonstrates why diffuse A cannot be coverage.
|
||||
from PIL import Image
|
||||
sample = library['materials']['00000c5603e38817']
|
||||
diffuse = np.asarray(Image.open(assets/sample['diffuse']).convert('RGBA'))
|
||||
opacity = np.asarray(Image.open(assets/sample['opacity']).convert('RGBA'))
|
||||
self.assertLess(np.mean(diffuse[:,:,3] >= 128), .01)
|
||||
self.assertGreater(np.mean(opacity[:,:,0] >= 128), .4)
|
||||
for tattoo in library['tattoos'].values():
|
||||
self.assertTrue((assets/tattoo['texture']).is_file())
|
||||
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
@echo off
|
||||
setlocal
|
||||
cd /d "%~dp0"
|
||||
if not exist "%~dp0skate3rust-customiser-v2.exe" (
|
||||
echo The V2 customiser executable is missing.
|
||||
if not exist "%~dp0skate3rust-customiser-v3.exe" (
|
||||
echo The V3 customiser executable is missing.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
if not exist "%~dp0assets\private\customisation\library.json" (
|
||||
if not exist "%~dp0assets\private\customisation\library-v3.json" (
|
||||
echo The prepared character library is missing.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo Skate 3 Character Customiser V2
|
||||
echo Skate 3 Character Customiser V3
|
||||
echo Open Escape / Start, then Character customiser.
|
||||
echo Browse with arrows / D-pad. Change with left-right or the minus-plus buttons.
|
||||
echo Type to search an item list. Escape / B goes back. Done saves and resumes.
|
||||
echo Diagnostic output: customiser-v2-session.log
|
||||
"%~dp0skate3rust-customiser-v2.exe" --assets "%~dp0assets" --map "%~dp0maps\SkateSchool.skate" > "%~dp0customiser-v2-session.log" 2>&1
|
||||
echo Diagnostic output: customiser-v3-session.log
|
||||
"%~dp0skate3rust-customiser-v3.exe" --assets "%~dp0assets" --map "%~dp0maps\SkateSchool.skate" > "%~dp0customiser-v3-session.log" 2>&1
|
||||
set "skate_customiser_exit=%errorlevel%"
|
||||
if not "%skate_customiser_exit%"=="0" (
|
||||
echo The test exited with code %skate_customiser_exit%.
|
||||
echo See %~dp0customiser-v2-session.log
|
||||
echo See %~dp0customiser-v3-session.log
|
||||
pause
|
||||
)
|
||||
exit /b %skate_customiser_exit%
|
||||
Reference in new issue
Block a user