fix:3的模型动态修改模型文件

This commit is contained in:
jingyun
2025-08-25 15:09:07 +08:00
parent 1c07d576d3
commit d16474784d
8 changed files with 346 additions and 121 deletions

BIN
examples/assets/cup/new.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 720 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 MiB

View File

@@ -0,0 +1,114 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:thermion_flutter/thermion_flutter.dart';
class Cup extends StatefulWidget {
const Cup({super.key});
@override
State<Cup> createState() => _CupState();
}
class _CupState extends State<Cup> {
bool _isLoading = true;
String? _modelPath, _modelUri;
String _statusMessage = "正在加载本地模型...";
@override
void initState() {
super.initState();
_loadLocalModel();
}
// 从应用支持目录加载模型
Future<void> _loadLocalModel() async {
try {
setState(() {
_statusMessage = "获取本地模型路径...";
});
final supportDir = await getApplicationSupportDirectory();
final modelFile = File(p.join(supportDir.path, 'cup', 'model.gltf'));
String modelPath =
File(p.join(supportDir.path, 'cup', 'model.gltf')).absolute.path;
final cupDir = Directory('${supportDir.path}/cup');
final modelUri = File('${cupDir.path}/model.gltf').uri.toString();
print('加载本地模modelPath: $modelPath');
print('加载本地模modelUri: $modelUri');
if (await modelFile.exists()) {
setState(() {
_modelPath = modelPath;
_modelUri = modelUri;
_isLoading = false;
_statusMessage = "本地模型加载完成";
});
} else {
setState(() {
_isLoading = false;
_statusMessage = "本地模型文件不存在";
});
}
} catch (e) {
print('加载本地模型失败: $e');
setState(() {
_isLoading = false;
_statusMessage = "加载失败: $e";
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('本地模型查看'),
),
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),
Text(_statusMessage),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _loadLocalModel,
child: const Text('重试'),
),
],
),
)
: ViewerWidget(
// assetPath: "file://$_modelPath",
assetPath: _modelUri,
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(),
),
),
);
}
}

View File

@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:thermion_flutter/thermion_flutter.dart';
// import 'package:thermion_dart/src/viewer/src/thermion_viewer_base.dart';
class CupPage1 extends StatelessWidget {
const CupPage1({super.key});
@@ -42,7 +43,7 @@ class CupPage extends StatefulWidget {
class _CupPageState extends State<CupPage> {
bool _isLoading = true;
String? _modelPath;
String? _modelPath, modelUri;
String _statusMessage = "正在加载本地模型...";
@override
@@ -61,9 +62,17 @@ class _CupPageState extends State<CupPage> {
final supportDir = await getApplicationSupportDirectory();
final modelFile = File(p.join(supportDir.path, 'cup', 'model.gltf'));
String modelPath =
File(p.join(supportDir.path, 'cup', 'model.gltf')).absolute.path;
final cupDir = Directory('${supportDir.path}/cup');
final modelUri = File('${cupDir.path}/model.gltf').uri;
print('加载本地模modelPath: $modelPath');
print('加载本地模modelUri: $modelUri');
if (await modelFile.exists()) {
setState(() {
_modelPath = modelFile.path;
_modelPath = modelPath;
_isLoading = false;
_statusMessage = "本地模型加载完成";
});
@@ -117,6 +126,7 @@ class _CupPageState extends State<CupPage> {
)
: ViewerWidget(
assetPath: "file://$_modelPath",
// assetPath: modelUri,
skyboxPath: "assets/default_env_skybox.ktx",
iblPath: "assets/default_env_ibl.ktx",
transformToUnitCube: true,

View File

@@ -1,10 +1,13 @@
import 'dart:convert';
import 'dart:io';
import 'package:animations/utils/binary_manager.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'model_viewer.dart';
import 'cup.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
@@ -31,7 +34,7 @@ class MyApp extends StatelessWidget {
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: const ModelViewerWithTextureReplace(),
home: ModelViewerWithTextureReplace(),
);
}
}
@@ -52,11 +55,11 @@ class _ModelViewerWithTextureReplaceState
int _keyValue = 0;
String _statusMessage = "正在加载模型...";
bool _isReplacingTexture = false;
File? _selectedImage;
@override
void initState() {
super.initState();
BinaryManager.readLocalJson();
_load3DModels();
// _loadModel();
}
@@ -117,69 +120,101 @@ class _ModelViewerWithTextureReplaceState
});
try {
// 使用FilePicker选择文件
// 选择图片文件
FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.image,
allowMultiple: false,
);
if (result != null &&
result.files.isNotEmpty &&
result.files.single.path != null) {
setState(() {
_statusMessage = "正在应用新纹理...";
});
// 获取选中的文件
PlatformFile file = result.files.first;
File newTexture = File(file.path!);
// 检查文件是否存在
if (await newTexture.exists()) {
// 先备份旧纹理文件(可选)
final backupTexturePath = '${_texturePath!}.backup';
final oldTexture = File(_texturePath!);
if (await oldTexture.exists()) {
await oldTexture.copy(backupTexturePath);
}
// 复制选中的图片到纹理路径
await newTexture.copy(_texturePath!);
// 给渲染器一点时间来处理文件变化
await Future.delayed(const Duration(milliseconds: 3000));
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => LocalModelViewer(),
),
);
// // 强制重建ViewerWidget以应用新纹理
// setState(() {
// _keyValue++;
// _statusMessage = "纹理已更新";
// });
// ScaffoldMessenger.of(context).showSnackBar(
// const SnackBar(content: Text('纹理已成功更新')),
// );
} else {
setState(() {
_statusMessage = "文件不存在";
});
}
} else {
if (result == null ||
result.files.isEmpty ||
result.files.single.path == null) {
setState(() {
_statusMessage = "取消选择";
});
return;
}
_selectedImage = File(result.files.single.path!);
final selectedFile = result.files.single;
final fileExtension = p.extension(selectedFile.path!).toLowerCase();
// 可选:仅允许特定图片格式
if (!['.jpg', '.jpeg', '.png'].contains(fileExtension)) {
setState(() {
_statusMessage = "仅支持 JPG / PNG 格式纹理";
});
return;
}
final newTexture = File(selectedFile.path!);
final supportDir = await getApplicationSupportDirectory();
final modelDir = p.join(supportDir.path, 'cup');
final modelPath = p.join(modelDir, 'model.gltf');
final modelFile = File(modelPath);
if (!await newTexture.exists()) {
setState(() {
_statusMessage = "所选纹理文件不存在";
});
return;
}
// 复制新纹理为固定名 new.jpg 到模型目录
final newTexturePath = p.join(modelDir, 'new.jpg');
await newTexture.copy(newTexturePath);
_texturePath = newTexturePath;
// 修改 GLTF 文件
if (await modelFile.exists()) {
String gltfContent = await modelFile.readAsString();
// ✅ 修复 GLTF 中非法负号格式(例如 "- 0.123" => "-0.123"
gltfContent = gltfContent.replaceAllMapped(
RegExp(r'-\s+(\d+(\.\d+)?)'),
(match) => '-${match.group(1)}',
);
final gltfJson = jsonDecode(gltfContent);
// 安全修改 images[0].uri
if (gltfJson['images'] != null && gltfJson['images'].isNotEmpty) {
gltfJson['images'][0]['uri'] = './new.jpg';
}
// 可选:修改 textures[0].name
if (gltfJson['textures'] != null && gltfJson['textures'].isNotEmpty) {
gltfJson['textures'][0]['name'] = 'new.jpg';
}
// 写回更新后的内容
final updatedContent =
const JsonEncoder.withIndent(' ').convert(gltfJson);
await modelFile.writeAsString(updatedContent);
print('GLTF文件已成功更新');
// 重建模型视图
setState(() {
_keyValue++;
_statusMessage = "纹理已更新为 new.jpg";
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('纹理已成功更新为 new.jpg')),
);
} else {
print('GLTF文件不存在: $modelPath');
setState(() {
_statusMessage = "GLTF文件不存在";
});
}
} catch (e) {
print('更换纹理失败: $e');
setState(() {
_statusMessage = "更换纹理失败: $e";
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('更换纹理失败: $e')),
);
@@ -192,6 +227,15 @@ class _ModelViewerWithTextureReplaceState
// 重置纹理到原始状态
Future<void> _resetTexture() async {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => Cup(),
// builder: (context) => CupPage(),
),
);
return;
if (_isReplacingTexture || _texturePath == null) return;
setState(() {
@@ -285,6 +329,22 @@ class _ModelViewerWithTextureReplaceState
)
: Column(
children: [
if (_selectedImage != null)
Container(
width: 300,
height: 300,
decoration: BoxDecoration(
border: Border.all(color: Colors.blue, width: 2),
borderRadius: BorderRadius.circular(12),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.file(
_selectedImage!,
fit: BoxFit.cover,
),
),
),
// Expanded(
// child: ViewerWidget(
// key: ValueKey(_keyValue),
@@ -317,21 +377,10 @@ class _ModelViewerWithTextureReplaceState
),
ElevatedButton.icon(
icon: const Icon(Icons.refresh),
label: const Text('重置纹理'),
label: const Text('3d查看'),
onPressed:
_isReplacingTexture ? null : _resetTexture,
),
ElevatedButton.icon(
icon: const Icon(Icons.camera),
label: const Text('重置视图'),
onPressed: _isReplacingTexture
? null
: () {
setState(() {
_keyValue++;
});
},
),
],
),
const SizedBox(height: 8),

View File

@@ -1,6 +1,6 @@
import 'dart:convert';
import 'dart:io';
import 'package:animations/utils/binary_manager.dart';
import 'package:flutter/material.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
@@ -21,66 +21,84 @@ class _LocalModelViewerState extends State<LocalModelViewer> {
@override
void initState() {
super.initState();
_initializeViewerAndLoadModel();
ThermionFlutterPlugin.createViewer().then((viewer) async {
_viewer = viewer;
_loadLocalModel(); // 在 viewer 初始化之后再加载模型
});
}
Future<void> _initializeViewerAndLoadModel() async {
Future<void> _loadLocalModel() async {
try {
// 获取应用支持目录并构建模型文件路径
setState(() {
_isLoading = true;
_statusMessage = "获取本地模型路径...";
});
final supportDir = await getApplicationSupportDirectory();
final modelFile = File(p.join(supportDir.path, 'cup', 'model.gltf'));
final modelDir = p.join(supportDir.path, 'cup');
final modelPath = p.join(modelDir, 'model.gltf');
final modelFile = File(modelPath);
// 检查文件是否存在
if (!await modelFile.exists()) {
await BinaryManager.migrate3DModels();
await Future.delayed(const Duration(milliseconds: 3000));
if (await modelFile.exists()) {
// 检查二进制文件
final binPath = p.join(modelDir, 'model.bin');
final binFile = File(binPath);
if (!await binFile.exists()) {
throw Exception("二进制文件不存在: $binPath");
}
// throw Exception("模型文件不存在于路径: ${modelFile.path}");
// 检查文件大小
final binSize = await binFile.length();
if (binSize == 0) {
throw Exception("二进制文件为空");
}
// 检查纹理文件
final texturePath = p.join(modelDir, 'texture.jpg');
final textureFile = File(texturePath);
if (!await textureFile.exists()) {
throw Exception("纹理文件不存在: $texturePath");
}
setState(() {
_statusMessage = "读取和修改模型数据...";
});
// 读取GLTF文件内容
String gltfContent = await modelFile.readAsString();
// 修改纹理路径(去掉"./"前缀)
gltfContent = gltfContent.replaceAll('"./texture.jpg"', '"new.jpg"');
// 转换为字节数组
Uint8List gltfBytes = Uint8List.fromList(utf8.encode(gltfContent));
// 使用绝对路径作为资源URI
final resourceUri = "file://${Directory(modelDir).absolute.path}";
setState(() {
_statusMessage = "加载模型中...";
});
final asset = await _viewer?.loadGltfFromBuffer(
gltfBytes,
resourceUri: resourceUri,
addToScene: true,
);
setState(() {
_asset = asset;
_isLoading = false;
_statusMessage = "模型加载完成";
});
} else {
setState(() {
_isLoading = false;
_statusMessage = "本地模型文件不存在";
});
}
setState(() {
_statusMessage = "创建3D查看器...";
});
// 创建ThermionViewer实例
_viewer = await ThermionFlutterPlugin.createViewer();
setState(() {
_statusMessage = "获取模型路径...";
});
setState(() {
_statusMessage = "加载环境光和天空盒...";
});
// 加载环境光和天空盒确保在pubspec.yaml中注册
await _viewer!.loadSkybox("assets/default_env_skybox.ktx");
await _viewer!.loadIbl("assets/default_env_ibl.ktx");
setState(() {
_statusMessage = "加载3D模型...";
});
// 加载glTF模型 - 使用文件URI格式
_asset = await _viewer!.loadGltf("file://${modelFile.path}");
// 可选:将模型缩放到单位立方体
await _asset!.transformToUnitCube();
// 配置场景
final camera = await _viewer!.getActiveCamera();
await camera.lookAt(Vector3(0, 0, 6));
// 开始渲染
await _viewer!.setRendering(true);
setState(() {
_isLoading = false;
_hasError = false;
_statusMessage = "加载完成";
});
} catch (e) {
print("加载模型失败: $e");
print('加载本地模型失败: $e');
setState(() {
_isLoading = false;
_hasError = true;
@@ -115,7 +133,7 @@ class _LocalModelViewerState extends State<LocalModelViewer> {
Text(_statusMessage, style: TextStyle(color: Colors.red)),
SizedBox(height: 16),
ElevatedButton(
onPressed: _initializeViewerAndLoadModel,
onPressed: _loadLocalModel,
child: Text("重试"),
),
],
@@ -164,7 +182,7 @@ class _LocalModelViewerState extends State<LocalModelViewer> {
@override
void dispose() {
deleteModelFile();
// deleteModelFile();
// 清理资源
_viewer?.dispose();
super.dispose();

View File

@@ -114,10 +114,19 @@ class BinaryManager {
// 迁移所有3D模型文件
for (final modelFile in model3dFiles) {
try {
// if (modelFile == 'model.gltf') {
// // 对于GLTF文件我们需要修改内容
// await _extractAndModifyGltfAsset(
// 'assets/cup/$modelFile',
// p.join(models3dDir.path, modelFile),
// );
// } else {
// 对于其他文件,直接复制
await _extractAsset(
'assets/cup/$modelFile',
p.join(models3dDir.path, modelFile),
);
// }
} catch (e) {
print('迁移3D模型文件失败 $modelFile: $e');
// 继续迁移其他模型文件
@@ -127,6 +136,30 @@ class BinaryManager {
print('3D模型文件迁移完成');
}
static Future<void> _extractAndModifyGltfAsset(
String assetPath, String targetPath) async {
try {
// 读取资源文件内容
final byteData = await rootBundle.load(assetPath);
final buffer = byteData.buffer;
final gltfContent = utf8.decode(
buffer.asUint8List(byteData.offsetInBytes, byteData.lengthInBytes));
// 修改纹理路径(去掉"./"前缀)
final modifiedGltfContent =
gltfContent.replaceAll('"./texture.jpg"', '"texture.jpg"');
// 写入修改后的内容到目标文件
final file = File(targetPath);
await file.writeAsBytes(utf8.encode(modifiedGltfContent));
print('已修改并迁移: $assetPath -> $targetPath');
} catch (e) {
print('提取并修改GLTF资源失败: $e');
rethrow;
}
}
static Future<void> _migrateShellScript() async {
print('开始迁移脚本文件...');

View File

@@ -47,6 +47,7 @@ flutter:
- assets/BusterDrone/
- assets/BusterDrone/textures/
- assets/cup/
- assets/image/
- assets/bin/mac/
- assets/bin/windows/
- assets/models/