@@ -216,34 +216,48 @@ pub fn analyze_vrm(input_path: &Path, options: ConvertOptions) -> Result<Analysi
216216 } )
217217 . collect ( ) ;
218218
219- let oversized_count = texture_infos
219+ let medium_oversized_count = texture_infos
220220 . iter ( )
221- . filter ( |texture| texture. width > 1024 || texture. height > 1024 )
221+ . filter ( |texture| {
222+ let max_dim = texture. width . max ( texture. height ) ;
223+ max_dim > 1024 && max_dim <= 2048
224+ } )
225+ . count ( ) ;
226+ let large_oversized_count = texture_infos
227+ . iter ( )
228+ . filter ( |texture| texture. width . max ( texture. height ) > 2048 )
222229 . count ( ) ;
223230
224- if oversized_count > 0 {
231+ if medium_oversized_count > 0 {
225232 issues. push ( ValidationIssue {
226- severity : if options. texture_auto_resize {
227- Severity :: Info
228- } else {
229- Severity :: Warning
230- } ,
231- code : "TEXTURE_OVERSIZE" . to_string ( ) ,
233+ severity : Severity :: Warning ,
234+ code : "TEXTURE_OVERSIZE_1024_2048" . to_string ( ) ,
235+ message : format ! (
236+ "⚠️ Detected {} texture(s) with max edge between 1025 and 2048. Enable the 1024px resize option if you want to downscale them" ,
237+ medium_oversized_count
238+ ) ,
239+ } ) ;
240+ }
241+
242+ if large_oversized_count > 0 {
243+ issues. push ( ValidationIssue {
244+ severity : Severity :: Warning ,
245+ code : "TEXTURE_OVERSIZE_OVER_2048" . to_string ( ) ,
232246 message : if options. texture_auto_resize {
233247 format ! (
234- "⚠️ Detected {} oversized texture(s). They will be resized to a 1024px max on export" ,
235- oversized_count
248+ "⚠️ Detected {} texture(s) larger than 2048 . They will be resized to a 1024px max on export" ,
249+ large_oversized_count
236250 )
237251 } else {
238252 format ! (
239- "⚠️ Detected {} oversized texture(s). Second Life upload cost may increase " ,
240- oversized_count
253+ "⚠️ Detected {} texture(s) larger than 2048. They will be resized to a 2048px max on export " ,
254+ large_oversized_count
241255 )
242256 } ,
243257 } ) ;
244258 }
245259
246- let fee_estimate = estimate_texture_fee ( & texture_infos) ;
260+ let fee_estimate = estimate_texture_fee ( & texture_infos, options . texture_auto_resize ) ;
247261
248262 let estimated_height_cm = estimate_height_cm ( & document, & buffers) . unwrap_or ( 170.0 ) ;
249263
@@ -521,7 +535,10 @@ fn collect_mesh_statistics(document: &Document) -> (usize, usize, Vec<Validation
521535}
522536
523537/// Estimate texture upload fees before/after resize policy.
524- fn estimate_texture_fee ( texture_infos : & [ TextureInfo ] ) -> UploadFeeEstimate {
538+ fn estimate_texture_fee (
539+ texture_infos : & [ TextureInfo ] ,
540+ auto_resize_to_1024 : bool ,
541+ ) -> UploadFeeEstimate {
525542 let before = texture_infos
526543 . iter ( )
527544 . map ( |texture| fee_per_texture ( texture. width , texture. height ) )
@@ -530,9 +547,9 @@ fn estimate_texture_fee(texture_infos: &[TextureInfo]) -> UploadFeeEstimate {
530547 let after = texture_infos
531548 . iter ( )
532549 . map ( |texture| {
533- let clamped_width = texture . width . min ( 1024 ) ;
534- let clamped_height = texture. height . min ( 1024 ) ;
535- fee_per_texture ( clamped_width , clamped_height )
550+ let ( projected_width , projected_height ) =
551+ projected_texture_size ( texture. width , texture . height , auto_resize_to_1024 ) ;
552+ fee_per_texture ( projected_width , projected_height )
536553 } )
537554 . sum :: < u32 > ( ) ;
538555
@@ -549,6 +566,29 @@ fn estimate_texture_fee(texture_infos: &[TextureInfo]) -> UploadFeeEstimate {
549566 }
550567}
551568
569+ /// Estimate texture size after export policy is applied.
570+ fn projected_texture_size ( width : u32 , height : u32 , auto_resize_to_1024 : bool ) -> ( u32 , u32 ) {
571+ let max_dim = width. max ( height) ;
572+ let target_max = if max_dim <= 1024 {
573+ 1024
574+ } else if max_dim <= 2048 {
575+ if auto_resize_to_1024 { 1024 } else { max_dim }
576+ } else if auto_resize_to_1024 {
577+ 1024
578+ } else {
579+ 2048
580+ } ;
581+
582+ if max_dim <= target_max {
583+ return ( width, height) ;
584+ }
585+
586+ let scale = target_max as f64 / max_dim as f64 ;
587+ let projected_width = ( width as f64 * scale) . round ( ) . max ( 1.0 ) as u32 ;
588+ let projected_height = ( height as f64 * scale) . round ( ) . max ( 1.0 ) as u32 ;
589+ ( projected_width, projected_height)
590+ }
591+
552592/// Estimate fee per texture based on max dimension bands.
553593fn fee_per_texture ( width : u32 , height : u32 ) -> u32 {
554594 let max_dim = width. max ( height) ;
@@ -619,9 +659,12 @@ fn transform_and_write_glb(
619659 remove_unsupported_features ( & mut json) ;
620660 apply_uniform_scale_to_scene_roots ( & mut json, scale_factor) ;
621661
622- if texture_auto_resize {
623- apply_texture_resize_to_embedded_images ( & mut json, & mut bin, texture_resize_method) ?;
624- }
662+ apply_texture_resize_to_embedded_images (
663+ & mut json,
664+ & mut bin,
665+ texture_auto_resize,
666+ texture_resize_method,
667+ ) ?;
625668
626669 let json_bytes =
627670 serde_json:: to_vec ( & json) . context ( "failed to serialize transformed glTF JSON" ) ?;
@@ -651,6 +694,7 @@ fn transform_and_write_glb(
651694fn apply_texture_resize_to_embedded_images (
652695 json : & mut Value ,
653696 bin : & mut Vec < u8 > ,
697+ auto_resize_to_1024 : bool ,
654698 interpolation : ResizeInterpolation ,
655699) -> Result < ( ) > {
656700 let Some ( buffer_views) = json. get ( "bufferViews" ) . and_then ( Value :: as_array) else {
@@ -700,11 +744,22 @@ fn apply_texture_resize_to_embedded_images(
700744 let decoded = image:: load_from_memory ( image_bytes)
701745 . with_context ( || format ! ( "failed to decode embedded texture view {view_index}" ) ) ?;
702746
703- if decoded. width ( ) <= 1024 && decoded. height ( ) <= 1024 {
704- continue ;
705- }
747+ let max_dim = decoded. width ( ) . max ( decoded. height ( ) ) ;
748+ let target_max = if max_dim <= 1024 {
749+ 1024
750+ } else if max_dim <= 2048 {
751+ if auto_resize_to_1024 { 1024 } else { max_dim }
752+ } else if auto_resize_to_1024 {
753+ 1024
754+ } else {
755+ 2048
756+ } ;
706757
707- let resized = resize_texture_to_max ( & decoded, 1024 , 1024 , interpolation) ;
758+ let resized = if max_dim > target_max {
759+ resize_texture_to_max ( & decoded, target_max, target_max, interpolation)
760+ } else {
761+ decoded
762+ } ;
708763 let mut encoded = Vec :: < u8 > :: new ( ) ;
709764 resized
710765 . write_to ( & mut Cursor :: new ( & mut encoded) , image_format)
@@ -976,11 +1031,25 @@ mod tests {
9761031 } ,
9771032 ] ;
9781033
979- let estimate = estimate_texture_fee ( & textures) ;
1034+ let estimate = estimate_texture_fee ( & textures, true ) ;
9801035 assert ! ( estimate. before_linden_dollar > estimate. after_resize_linden_dollar) ;
9811036 assert ! ( estimate. reduction_percent > 0 ) ;
9821037 }
9831038
1039+ #[ test]
1040+ fn given_large_texture_when_1024_resize_disabled_then_policy_caps_at_2048 ( ) {
1041+ let ( width, height) = projected_texture_size ( 4096 , 2048 , false ) ;
1042+ assert_eq ! ( width, 2048 ) ;
1043+ assert_eq ! ( height, 1024 ) ;
1044+ }
1045+
1046+ #[ test]
1047+ fn given_mid_texture_when_1024_resize_disabled_then_size_is_kept ( ) {
1048+ let ( width, height) = projected_texture_size ( 1800 , 900 , false ) ;
1049+ assert_eq ! ( width, 1800 ) ;
1050+ assert_eq ! ( height, 900 ) ;
1051+ }
1052+
9841053 #[ test]
9851054 fn given_required_hierarchy_when_parent_mismatch_then_error_is_reported ( ) {
9861055 let humanoid_bone_nodes = [ "hips" , "spine" , "chest" ]
0 commit comments