Skip to content

Commit c4b52e8

Browse files
committed
とりあえず、VRMを表示できるように。(VRM→glTFはRust側で行う)
1 parent 9e997c9 commit c4b52e8

9 files changed

Lines changed: 609 additions & 41 deletions

File tree

backend/src/command.rs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ use vrm2sl_tauri_lib::{
77
LogLevel,
88
convert::{AnalysisReport, ConversionReport},
99
ipc::{
10-
AnalyzeRequest, ConvertRequest, LoadSettingsRequest, SaveSettingsRequest, analyze_vrm_ipc,
11-
convert_vrm_to_gdb_ipc, load_project_settings_ipc, save_project_settings_ipc,
10+
AnalyzeRequest, ConvertRequest, LoadSettingsRequest, PreviewRequest, SaveSettingsRequest,
11+
analyze_vrm_ipc, build_preview_glb_ipc, convert_vrm_to_gdb_ipc, load_project_settings_ipc,
12+
save_project_settings_ipc,
1213
},
1314
project::ProjectSettings,
1415
send_log_with_handle,
@@ -60,6 +61,23 @@ pub async fn convert_vrm_command(
6061
result
6162
}
6263

64+
#[tauri::command]
65+
pub async fn build_preview_glb_command(
66+
request: PreviewRequest,
67+
app: AppHandle,
68+
) -> Result<String, String> {
69+
send_log_with_handle(
70+
&app,
71+
LogLevel::Info,
72+
&format!("Build preview GLB request: {}", request.input_path),
73+
);
74+
let result = build_preview_glb_ipc(request);
75+
if result.is_ok() {
76+
send_log_with_handle(&app, LogLevel::Info, "Build preview GLB completed");
77+
}
78+
result
79+
}
80+
6381
#[tauri::command]
6482
pub async fn save_project_settings_command(
6583
request: SaveSettingsRequest,

backend/src/convert.rs

Lines changed: 95 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
553593
fn 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(
651694
fn 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"]

backend/src/ipc.rs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
use std::path::PathBuf;
1+
use std::{
2+
fs,
3+
path::PathBuf,
4+
time::{SystemTime, UNIX_EPOCH},
5+
};
26

37
use serde::{Deserialize, Serialize};
48

@@ -38,6 +42,13 @@ pub struct LoadSettingsRequest {
3842
pub path: String,
3943
}
4044

45+
/// IPC payload for generating a backend-side preview GLB.
46+
#[derive(Debug, Clone, Serialize, Deserialize)]
47+
pub struct PreviewRequest {
48+
pub input_path: String,
49+
pub options: ConvertOptions,
50+
}
51+
4152
/// Analyze a source model through the IPC boundary.
4253
pub fn analyze_vrm_ipc(request: AnalyzeRequest) -> Result<AnalysisReport, String> {
4354
let input = PathBuf::from(&request.input_path);
@@ -65,6 +76,16 @@ pub fn convert_vrm_to_gdb_ipc(request: ConvertRequest) -> Result<ConversionRepor
6576
Ok(report)
6677
}
6778

79+
/// Build a preview GLB file through the IPC boundary and return its path.
80+
pub fn build_preview_glb_ipc(request: PreviewRequest) -> Result<String, String> {
81+
let input = PathBuf::from(&request.input_path);
82+
let output = create_preview_output_path().map_err(|err| err.to_string())?;
83+
84+
convert_vrm_to_gdb(&input, &output, request.options).map_err(|err| err.to_string())?;
85+
86+
Ok(output.to_string_lossy().to_string())
87+
}
88+
6889
/// Save project settings through the IPC boundary.
6990
pub fn save_project_settings_ipc(request: SaveSettingsRequest) -> Result<(), String> {
7091
let path = PathBuf::from(request.path);
@@ -76,3 +97,14 @@ pub fn load_project_settings_ipc(request: LoadSettingsRequest) -> Result<Project
7697
let path = PathBuf::from(request.path);
7798
load_project_settings(&path).map_err(|err| err.to_string())
7899
}
100+
101+
fn create_preview_output_path() -> anyhow::Result<PathBuf> {
102+
let mut dir = std::env::temp_dir();
103+
dir.push("vrm2sl-preview");
104+
fs::create_dir_all(&dir)?;
105+
106+
let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis();
107+
let file_name = format!("preview-{}-{}.glb", std::process::id(), timestamp);
108+
109+
Ok(dir.join(file_name))
110+
}

backend/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ fn main() {
1616
.invoke_handler(tauri::generate_handler![
1717
command::analyze_vrm_command,
1818
command::convert_vrm_command,
19+
command::build_preview_glb_command,
1920
command::save_project_settings_command,
2021
command::load_project_settings_command,
2122
command::get_app_version,

frontend/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"@tauri-apps/plugin-os": "^2.3.2",
4444
"pinia": "^3.0.4",
4545
"pinia-plugin-persistedstate": "^4.7.1",
46+
"three": "^0.183.1",
4647
"unified-network": "^0.6.4",
4748
"vue": "^3.5.29",
4849
"vue-i18n": "^11.2.8",
@@ -55,6 +56,7 @@
5556
"@tauri-apps/cli": "^2.10.0",
5657
"@tsconfig/node-lts": "^24.0.0",
5758
"@types/node": "^25.3.0",
59+
"@types/three": "^0.183.1",
5860
"@vitejs/plugin-vue": "^6.0.4",
5961
"@vue/eslint-config-prettier": "^10.2.0",
6062
"@vue/eslint-config-typescript": "^14.7.0",

frontend/src/Meta.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@ import type MetaInterface from '@/interfaces/MetaInterface';
33
// This file is auto-generated by the build system.
44
const meta: MetaInterface = {
55
version: '0.0.0',
6-
date: '2026-02-26T02:00:50.275Z'
6+
date: '2026-02-26T04:28:40.837Z',
77
};
88
export default meta;

0 commit comments

Comments
 (0)