This commit is contained in:
jingyun
2025-08-22 15:13:34 +08:00
parent 961b2ae1ee
commit 1c07d576d3
44 changed files with 8049 additions and 16 deletions

View File

@@ -0,0 +1,284 @@
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,
),
),
],
),
),
],
),
);
}
}