mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Restore authored stock surface detail and specular masks after model swaps
This commit is contained in:
1 parent
e6a1dfe79c
commit
983771f99b
5 files changed
+76
-27
No files matched your search
@@ -430,6 +430,9 @@ impl Parts {
|
||||
retail: material.extension.retail.clone(), retail_mask: material.extension.retail_mask.clone(),
|
||||
enabled: if source.opacity.is_some() {Vec4::Y} else {Vec4::ZERO}, ..default()
|
||||
};
|
||||
if extension.retail.tint.w > 0. {
|
||||
extension.retail.tint = Vec4::from_array(material.base.base_color.to_linear().to_f32_array());
|
||||
}
|
||||
if let Some((tid,t)) = stamp["id"].as_str().and_then(|id| self.library.tattoos.get(id).map(|t|(id,t))) {
|
||||
let key=match stamp["side"].as_u64().unwrap_or(0) {1=>"StampUVConstraintQ4",2=>"StampUVConstraintQ1",3=>"StampUVConstraintQ2",_=>"StampUVConstraintQ3"};
|
||||
let bounds:Vec<f32>=part.flag(key).split(',').filter_map(|n|n.parse().ok()).collect();
|
||||
@@ -472,16 +475,16 @@ impl Parts {
|
||||
let opacity = m.opacity.as_ref().map(|p| load(p, true));
|
||||
let retail_mask = m.lighting.as_ref().and_then(|l| l.specular.as_ref()).map(|p| load(p, true));
|
||||
let retail = m.lighting.as_ref().filter(|l| l.params.len() == 9).map(|l| crate::retail_character::CharacterParams {
|
||||
tint: Vec4::ONE,
|
||||
options: Vec4::new(0., f32::from(retail_mask.is_some()), -1., f32::from(l.shader.starts_with("character.hair"))),
|
||||
tint: Vec4::from_array(Color::srgb(m.tint[0],m.tint[1],m.tint[2]).to_linear().to_f32_array()),
|
||||
options: Vec4::new(f32::from(normal.is_some()), f32::from(retail_mask.is_some()), -1., f32::from(l.shader.starts_with("character.hair"))),
|
||||
rows: std::array::from_fn(|i| Vec4::from_array(l.params[i])),
|
||||
..default()
|
||||
}).unwrap_or_default();
|
||||
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,
|
||||
base_color_texture: Some(diffuse.clone()),
|
||||
normal_map_texture: normal.clone(),
|
||||
metallic_roughness_texture: rough,
|
||||
double_sided: m.opacity.is_some(),
|
||||
cull_mode: if m.opacity.is_some() {
|
||||
@@ -814,11 +817,21 @@ mod online_tests {
|
||||
parts.library.tattoos.insert("ink".into(),Tattoo{name:"ink".into(),texture:String::new(),bounds:[0.,0.,1.,1.]});
|
||||
let mut images=Assets::<Image>::default();let image=images.add(Image::default());parts.tattoos.insert("ink".into(),image.clone());
|
||||
let mut materials=Assets::<SkaterMaterial>::default();
|
||||
let handle=materials.add(SkaterMaterial{base:StandardMaterial::default(),extension:SkinStamp::default()});
|
||||
let handle=materials.add(SkaterMaterial{base:StandardMaterial{normal_map_texture:Some(image.clone()),base_color_texture:Some(image.clone()),..default()},extension:SkinStamp {
|
||||
retail_mask:Some(image.clone()),
|
||||
retail:crate::retail_character::CharacterParams{tint:Vec4::ONE,options:Vec4::new(1.,1.,-1.,0.),..default()},..default()
|
||||
}});
|
||||
parts.materials.insert("skin".into(),(handle.clone(),vec![]));
|
||||
let red=parts.profile_material("body","skin",&serde_json::json!({"skin_tint":[1,0,0],"tattoos":{"Arm":{"id":"ink","side":0}}}),&materials).unwrap();
|
||||
let blue=parts.profile_material("body","skin",&serde_json::json!({"skin_tint":[0,0,1]}),&materials).unwrap();
|
||||
assert_eq!(red.base.base_color,Color::srgb(1.,0.,0.));assert_eq!(blue.base.base_color,Color::srgb(0.,0.,1.));
|
||||
for material in [&red,&blue] {
|
||||
assert_eq!(material.base.normal_map_texture,Some(image.clone()));
|
||||
assert_eq!(material.base.base_color_texture,Some(image.clone()));
|
||||
assert_eq!(material.extension.retail_mask,Some(image.clone()));
|
||||
assert_eq!(material.extension.retail.options,Vec4::new(1.,1.,-1.,0.));
|
||||
assert_eq!(material.extension.retail.tint,Vec4::from_array(material.base.base_color.to_linear().to_f32_array()));
|
||||
}
|
||||
assert_eq!(red.extension.texture,Some(image));assert!(blue.extension.texture.is_none());
|
||||
assert_eq!(materials.get(&handle).unwrap().base.base_color,Color::WHITE);
|
||||
assert!(materials.get(&handle).unwrap().extension.texture.is_none());
|
||||
|
||||
@@ -23,7 +23,8 @@ struct SkinStamp {
|
||||
pbr_deferred_functions::deferred_output,
|
||||
}
|
||||
#else
|
||||
#import skate_character_lighting::{CharacterParams, shade_character}
|
||||
#import skate_character_lighting::{CharacterParams, shade_character, character_normal, character_albedo}
|
||||
#import bevy_pbr::pbr_bindings
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(105) var retail_mask: texture_2d<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(106) var retail_mask_sampler: sampler;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(107) var<uniform> retail: CharacterParams;
|
||||
@@ -80,6 +81,17 @@ fn fragment(
|
||||
|
||||
// generate a PbrInput struct from the StandardMaterial bindings
|
||||
var pbr_input = pbr_input_from_standard_material(in, is_front);
|
||||
#ifndef PREPASS_PIPELINE
|
||||
// Preserve authored alpha as specular data before opacity/tattoos/discard.
|
||||
let retail_albedo=textureSample(pbr_bindings::base_color_texture,pbr_bindings::base_color_sampler,in.uv);
|
||||
let retail_mapped=character_normal(in.world_position,in.world_normal,in.uv,
|
||||
textureSample(pbr_bindings::normal_map_texture,pbr_bindings::normal_map_sampler,in.uv).xy);
|
||||
let retail_n=select(normalize(in.world_normal),retail_mapped,retail.options.x>0.0);
|
||||
if retail.light.w>0.0 && retail.tint.w>0.0 {
|
||||
pbr_input.material.base_color=vec4<f32>(character_albedo(retail_albedo.rgb)*retail.tint.rgb,retail_albedo.a);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
#ifdef VERTEX_UVS_B
|
||||
@@ -113,9 +125,9 @@ fn fragment(
|
||||
var out: FragmentOutput;
|
||||
if retail.light.w > 0.0 && retail.tint.w > 0.0 {
|
||||
let mask = textureSample(retail_mask, retail_mask_sampler, in.uv).r;
|
||||
out.color = shade_character(retail, pbr_input.N, pbr_input.world_position,
|
||||
out.color = shade_character(retail, retail_n, pbr_input.world_position,
|
||||
pbr_input.material.base_color.rgb,
|
||||
select(pbr_input.material.base_color.a, mask, retail.options.y > 0.0),
|
||||
select(retail_albedo.a, mask, retail.options.y > 0.0),
|
||||
pbr_input.material.base_color.a);
|
||||
} else if (pbr_input.material.flags & STANDARD_MATERIAL_FLAGS_UNLIT_BIT) == 0u {
|
||||
out.color = apply_pbr_lighting(pbr_input);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#import bevy_pbr::{forward_io::VertexOutput, mesh_view_bindings as frame}
|
||||
#import skate_character_lighting::{CharacterParams, shade_character}
|
||||
#import skate_character_lighting::{CharacterParams, shade_character, character_normal, character_albedo}
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(0) var<uniform> p: CharacterParams;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(1) var diffuse: texture_2d<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(2) var diffuse_sampler: sampler;
|
||||
@@ -11,24 +11,9 @@
|
||||
@fragment
|
||||
fn fragment(i: VertexOutput) -> @location(0) vec4<f32> {
|
||||
let albedo = textureSample(diffuse,diffuse_sampler,i.uv);
|
||||
// GLTF diffuse is sampled as sRGB; recover retail's gamma-two decode.
|
||||
let gamma = select(albedo.rgb*12.92, 1.055*pow(max(albedo.rgb,vec3<f32>(0.0)),vec3<f32>(1.0/2.4))-0.055, albedo.rgb>vec3<f32>(0.0031308));
|
||||
let d = gamma*gamma*p.tint.rgb;
|
||||
let n = normalize(i.world_normal);
|
||||
let rpos = i.world_position.xyz-frame::view.world_position;
|
||||
let vd = -normalize(rpos);
|
||||
let dp1=dpdx(rpos); let dp2=dpdy(rpos);
|
||||
let du1=dpdx(i.uv); let du2=dpdy(i.uv);
|
||||
var tt=cross(dp2,n)*du1.x+cross(n,dp1)*du2.x;
|
||||
var bb=cross(dp2,n)*du1.y+cross(n,dp1)*du2.y;
|
||||
tt *= -inverseSqrt(max(dot(tt,tt),1e-12));
|
||||
bb *= inverseSqrt(max(dot(bb,bb),1e-12));
|
||||
var vn=n;
|
||||
if p.options.x>0.0 {
|
||||
// Installed normal PNGs already unpack DXT5nm into RGB.
|
||||
let nm=textureSample(normal_map,normal_sampler,i.uv).xy*2.0-1.0;
|
||||
vn=normalize(nm.x*tt+nm.y*bb+n*sqrt(saturate(1.0-dot(nm,nm))));
|
||||
}
|
||||
let d=character_albedo(albedo.rgb)*p.tint.rgb;
|
||||
let mapped=character_normal(i.world_position,i.world_normal,i.uv,textureSample(normal_map,normal_sampler,i.uv).xy);
|
||||
let vn=select(normalize(i.world_normal),mapped,p.options.x>0.0);
|
||||
var smask=albedo.a;
|
||||
if p.options.y>0.0 { smask=textureSample(mask_map,mask_sampler,i.uv).r; }
|
||||
return shade_character(p, vn, i.world_position, d, smask, albedo.a);
|
||||
|
||||
@@ -6,6 +6,26 @@ struct CharacterParams {
|
||||
rows: array<vec4<f32>,9>, sh: array<vec4<f32>,9>,
|
||||
}
|
||||
|
||||
// Both original and modular stock meshes use the authored derivative basis.
|
||||
// Modular GLBs intentionally omit tangents, so StandardMaterial's tangent-only
|
||||
// normal mapping cannot supply this detail after an outfit/model swap.
|
||||
fn character_normal(position: vec4<f32>, normal: vec3<f32>, uv: vec2<f32>, sample: vec2<f32>) -> vec3<f32> {
|
||||
let n=normalize(normal);
|
||||
let rpos=position.xyz-frame::view.world_position;
|
||||
let dp1=dpdx(rpos); let dp2=dpdy(rpos);
|
||||
let du1=dpdx(uv); let du2=dpdy(uv);
|
||||
var tt=cross(dp2,n)*du1.x+cross(n,dp1)*du2.x;
|
||||
var bb=cross(dp2,n)*du1.y+cross(n,dp1)*du2.y;
|
||||
tt *= -inverseSqrt(max(dot(tt,tt),1e-12));
|
||||
bb *= inverseSqrt(max(dot(bb,bb),1e-12));
|
||||
let nm=sample*2.0-1.0;
|
||||
return normalize(nm.x*tt+nm.y*bb+n*sqrt(saturate(1.0-dot(nm,nm))));
|
||||
}
|
||||
fn character_albedo(linear: vec3<f32>) -> vec3<f32> {
|
||||
let gamma=select(linear*12.92,1.055*pow(max(linear,vec3<f32>(0.0)),vec3<f32>(1.0/2.4))-0.055,linear>vec3<f32>(0.0031308));
|
||||
return gamma*gamma;
|
||||
}
|
||||
|
||||
fn shade_character(p: CharacterParams, vn: vec3<f32>, position: vec4<f32>, d: vec3<f32>, smask: f32, alpha: f32) -> vec4<f32> {
|
||||
let vd = -normalize(position.xyz-frame::view.world_position);
|
||||
if p.options.w>0.0 {
|
||||
|
||||
@@ -181,3 +181,22 @@ launcher records stdout/stderr, backtraces and the exit code in
|
||||
See [visual-parity-status.md](visual-parity-status.md) for the subsequent SH
|
||||
transition smoothing, second-UV blended hair coverage, GPU exposure meter and
|
||||
remaining shadow/CAC fidelity gaps. Earlier implementation sections are historical.
|
||||
|
||||
## Stock surface restoration, September 9
|
||||
|
||||
Returning from an imported/native model reapplies the modular stock outfit. Its
|
||||
geometry-only GLBs have normal maps in the material library but no tangent
|
||||
attributes, so the StandardMaterial input path silently omitted their detail.
|
||||
The customiser shader also used alpha after `alpha_discard` as the clothing
|
||||
specular mask; opaque coverage had already replaced that authored mask with 1.
|
||||
|
||||
Both original and modular retail shaders now share the derivative normal basis
|
||||
and gamma-two albedo decode. Modular materials retain the source diffuse alpha
|
||||
for highlights, independently of opacity and tattoos, and use the dedicated skin
|
||||
specular mask where authored. Profile updates retain texture handles and apply
|
||||
colour tint separately. This shared material path covers local and remote stock
|
||||
outfits without changing imported PBR materials or globally lowering gloss.
|
||||
|
||||
Fourteen focused tests pass with the prepared library, including offline shader
|
||||
compilation, stock restoration and per-profile material preservation. No game was
|
||||
launched; visual verification of stock -> imported/pro -> stock remains manual.
|
||||
Reference in new issue
Block a user