285 lines
8.8 KiB
Dart
285 lines
8.8 KiB
Dart
import 'dart:io';
|
||
|
||
import 'package:file_picker/file_picker.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:path_provider/path_provider.dart';
|
||
import 'package:thermion_flutter/thermion_flutter.dart';
|
||
|
||
void main() {
|
||
runApp(const MyApp());
|
||
}
|
||
|
||
class MyApp extends StatelessWidget {
|
||
const MyApp({super.key});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return MaterialApp(
|
||
title: '3D模型纹理替换',
|
||
theme: ThemeData(
|
||
primarySwatch: Colors.blue,
|
||
visualDensity: VisualDensity.adaptivePlatformDensity,
|
||
),
|
||
home: const ModelViewerScreen(),
|
||
);
|
||
}
|
||
}
|
||
|
||
class ModelViewerScreen extends StatefulWidget {
|
||
const ModelViewerScreen({super.key});
|
||
|
||
@override
|
||
State<ModelViewerScreen> createState() => _ModelViewerScreenState();
|
||
}
|
||
|
||
class _ModelViewerScreenState extends State<ModelViewerScreen> {
|
||
bool _isLoading = true;
|
||
String? _modelPath;
|
||
String? _texturePath;
|
||
int _keyValue = 0;
|
||
String _statusMessage = "正在加载模型...";
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_loadModel();
|
||
}
|
||
|
||
// 加载模型文件到临时目录
|
||
Future<void> _loadModel() async {
|
||
try {
|
||
setState(() {
|
||
_statusMessage = "准备模型目录...";
|
||
});
|
||
|
||
final directory = await getApplicationDocumentsDirectory();
|
||
final modelDir = Directory('${directory.path}/model');
|
||
|
||
// 确保目录存在
|
||
if (!await modelDir.exists()) {
|
||
await modelDir.create(recursive: true);
|
||
}
|
||
|
||
setState(() {
|
||
_statusMessage = "加载GLTF文件...";
|
||
});
|
||
|
||
// 加载 .gltf 文件并写入本地
|
||
final gltfData =
|
||
await DefaultAssetBundle.of(context).load('assets/cup/model.gltf');
|
||
final gltfFile = File('${modelDir.path}/model.gltf');
|
||
await gltfFile.writeAsBytes(gltfData.buffer.asUint8List());
|
||
|
||
/// ✅ 打印本地路径(关键)
|
||
print('✅ GLTF 本地路径: ${gltfFile.path}');
|
||
print('✅ 是否存在: ${await gltfFile.exists()}');
|
||
|
||
setState(() {
|
||
_statusMessage = "加载模型数据...";
|
||
});
|
||
|
||
// 加载 .bin 文件并写入本地
|
||
final binData =
|
||
await DefaultAssetBundle.of(context).load('assets/cup/model.bin');
|
||
final binFile = File('${modelDir.path}/model.bin');
|
||
await binFile.writeAsBytes(binData.buffer.asUint8List());
|
||
|
||
setState(() {
|
||
_statusMessage = "加载纹理...";
|
||
});
|
||
|
||
// 加载纹理文件并写入本地
|
||
final textureData =
|
||
await DefaultAssetBundle.of(context).load('assets/cup/texture.jpg');
|
||
final textureFile = File('${modelDir.path}/texture.jpg');
|
||
await textureFile.writeAsBytes(textureData.buffer.asUint8List());
|
||
|
||
/// ✅ 打印纹理路径
|
||
print('✅ 纹理本地路径: ${textureFile.path}');
|
||
print('✅ 是否存在: ${await textureFile.exists()}');
|
||
|
||
setState(() {
|
||
_modelPath = gltfFile.path;
|
||
_texturePath = textureFile.path;
|
||
_isLoading = false;
|
||
_statusMessage = "加载完成";
|
||
});
|
||
} catch (e) {
|
||
print('❌ 加载模型失败: $e');
|
||
setState(() {
|
||
_isLoading = false;
|
||
_statusMessage = "加载失败: $e";
|
||
});
|
||
}
|
||
}
|
||
|
||
// 使用FilePicker选择新纹理 - 修复配置错误
|
||
Future<void> _pickNewTexture() async {
|
||
try {
|
||
setState(() {
|
||
_statusMessage = "正在选择纹理文件...";
|
||
});
|
||
|
||
// 使用FilePicker选择文件 - 修复配置
|
||
// 选项1: 使用FileType.image(不能指定allowedExtensions)
|
||
FilePickerResult? result = await FilePicker.platform.pickFiles(
|
||
type: FileType.image,
|
||
allowMultiple: false,
|
||
);
|
||
|
||
// 选项2: 如果需要特定扩展名,使用FileType.custom
|
||
/*
|
||
FilePickerResult? result = await FilePicker.platform.pickFiles(
|
||
type: FileType.custom,
|
||
allowMultiple: false,
|
||
allowedExtensions: ['jpg', 'jpeg', 'png', 'bmp', 'tga'],
|
||
);
|
||
*/
|
||
|
||
if (result != null &&
|
||
result.files.isNotEmpty &&
|
||
result.files.single.path != null &&
|
||
_texturePath != null) {
|
||
setState(() {
|
||
_statusMessage = "正在应用新纹理...";
|
||
});
|
||
|
||
// 获取选中的文件
|
||
PlatformFile file = result.files.first;
|
||
File newTexture = File(file.path!);
|
||
|
||
// 检查文件是否存在
|
||
if (await newTexture.exists()) {
|
||
// 复制选中的图片到纹理路径
|
||
await newTexture.copy(_texturePath!);
|
||
|
||
// 强制重建ViewerWidget以应用新纹理
|
||
setState(() {
|
||
_keyValue++;
|
||
_statusMessage = "纹理已更新";
|
||
});
|
||
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(content: Text('纹理已成功更新')),
|
||
);
|
||
} else {
|
||
setState(() {
|
||
_statusMessage = "文件不存在";
|
||
});
|
||
}
|
||
} else {
|
||
setState(() {
|
||
_statusMessage = "取消选择";
|
||
});
|
||
}
|
||
} catch (e) {
|
||
print('更换纹理失败: $e');
|
||
setState(() {
|
||
_statusMessage = "更换纹理失败";
|
||
});
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(content: Text('更换纹理失败: $e')),
|
||
);
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
appBar: AppBar(
|
||
title: const Text('3D模型纹理替换'),
|
||
actions: [
|
||
IconButton(
|
||
icon: const Icon(Icons.texture),
|
||
onPressed: _pickNewTexture,
|
||
tooltip: '更换纹理',
|
||
),
|
||
],
|
||
),
|
||
body: _isLoading
|
||
? Center(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
const CircularProgressIndicator(),
|
||
const SizedBox(height: 16),
|
||
Text(_statusMessage),
|
||
],
|
||
),
|
||
)
|
||
: _modelPath == null
|
||
? Center(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
const Icon(Icons.error, size: 48, color: Colors.red),
|
||
const SizedBox(height: 16),
|
||
const Text('加载模型失败'),
|
||
const SizedBox(height: 16),
|
||
ElevatedButton(
|
||
onPressed: _loadModel,
|
||
child: const Text('重试'),
|
||
),
|
||
],
|
||
),
|
||
)
|
||
: Column(
|
||
children: [
|
||
Expanded(
|
||
child: ViewerWidget(
|
||
key: ValueKey(_keyValue),
|
||
assetPath: _modelPath!,
|
||
skyboxPath: "assets/default_env_skybox.ktx",
|
||
iblPath: "assets/default_env_ibl.ktx",
|
||
transformToUnitCube: true,
|
||
initialCameraPosition: Vector3(0, 0, 6),
|
||
background: Colors.grey[200],
|
||
manipulatorType: ManipulatorType.ORBIT,
|
||
initial: const Center(
|
||
child: CircularProgressIndicator(),
|
||
),
|
||
),
|
||
),
|
||
Container(
|
||
padding: const EdgeInsets.all(16),
|
||
color: Colors.grey[100],
|
||
child: Column(
|
||
children: [
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||
children: [
|
||
ElevatedButton.icon(
|
||
icon: const Icon(Icons.texture),
|
||
label: const Text('选择纹理图片'),
|
||
onPressed: _pickNewTexture,
|
||
),
|
||
ElevatedButton.icon(
|
||
icon: const Icon(Icons.refresh),
|
||
label: const Text('重置视图'),
|
||
onPressed: () {
|
||
setState(() {
|
||
_keyValue++;
|
||
});
|
||
},
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
_statusMessage,
|
||
style: TextStyle(
|
||
color: Colors.grey[600],
|
||
fontStyle: FontStyle.italic,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|