Restore authored stock surface detail and specular masks after model swaps

This commit is contained in:
chazmm committed 2026-09-09 17:56:21 +01:00
1 parent e6a1dfe79c
commit 983771f99b
5 files changed
+76 -27

No files matched your search

+18 -5
View File
@@ -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());
+15 -3
View File
@@ -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);
+4 -19
View File
@@ -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 {