我正在参加中秋创意投稿大赛,详情请看:中秋创意投稿大赛
中秋节马上就要到来,往年看着大厂里发放的月饼都特别精致,再看看自己公司发的月饼,就真的只是月饼。想要在 Flutter 中渲染 3D 模型,第一时间想到了去 pub.dev中看看相关的实现方案, 我找到了 model_viewer并有了如下的效果。
⚠️注意: 本文中思路实际上是采用的 WebView + Google model-viewer.js 的方式渲染的3D模型。
model_viewer
当我们打开 pub.dev 中 model_viewer 的主页我们会看到如下的说明:
This is a Flutter widget for rendering interactive 3D models in the glTF and GLB formats.
The widget embeds Google’s
model-viewer
web component in a WebView.
从中我们可以看到这个控件只支持 glTF
和 GLB
两种格式的模型,还能从中了解到他的原理就是通过在Flutter 中植入WebView通过Goolge 的 <model-viewer>
Web组件渲染模型。
使用示例
看着案例中的使用还挺简单,于是马上就搞了个示例跑了一下。
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: ModelViewer(
backgroundColor: Colors.black,
// 本地 assets 中文件, 需要在 pubspec.yaml 中配置flutter节点中的assets
src: "assets/Astronaut.glb",
// 是否自动播放
autoPlay: true,
// 自动旋转
autoRotate: true,
// 视窗控制,能转动.
cameraControls: true,
),
);
}
}
复制代码
结果一跑发现竟然无法显示,查看日志观察到WebViewer加载出现了不允许进行明文请求。
我这模型也不是网络模型,怎么会出现不允许进行明文请求的错误呢? 翻看 model-viewer 的代码,发现 ModelViewer 在本地通过HttpServer建立了一个简单的Web服务提供页面。
ModelViewer 中对明文请求的解决方案
再次查看控件的主页,发现作者也给了简单粗暴的解决方案:
Android 通过配置 android:usesCleartextTraffic="true"
让应用支持明文请求;本着折腾的心态在想:既然应用强制我们使用https,我们何不在创建HttpServer的地方动一动手脚,在本地创建一个 HttpsServer 呢?
Https Server
通过查看 HttpServer 中的方法发现了我们的目标(HttpServer#bindSecure)。
static Future<HttpServer> bindSecure(
address, int port, SecurityContext context,
{int backlog = 0,
bool v6Only = false,
bool requestClientCertificate = false,
bool shared = false}) =>
_HttpServer.bindSecure(address, port, context, backlog, v6Only,
requestClientCertificate, shared);
复制代码
查看官方文档中对Https部分的描述, 我们只需要简单修改一下原来创建Server的地方,我们就可以得到一个 HttpsServer了。其中 server.pem
为证书, private.pem
为私钥, 创建方式请看示例项目的README。
SecurityContext securityContext = new SecurityContext();
final chain = await _readAsset("assets/ssl/server.pem");
final privateKey = await _readAsset("assets/ssl/private.pem");
securityContext.useCertificateChainBytes(chain.cast<int>());
securityContext.usePrivateKeyBytes(privateKey.cast<int>());
// 注意: 因为 443 端口在 Android 设备中是受保护的端口, 我们可以采用11443作为https服务的端口
_proxy = await HttpServer.bindSecure(InternetAddress.loopbackIPv4, 11443, securityContext);
复制代码
你以为这样结束了?我也这样认为的,当我重新运行项目。发现了一个新的问题,我用的证书是私有签发的,而 webview_flutter
好像不能够忽略 ssl 异常导致打开页面后白屏。
WebView访问使用私有签发的证书站点
我在 webview_flutter
控件的GitHub仓库中看到了一个 PR 提到了这个问题: [webview_flutter] Ignore SSL certificate errors. 虽然看着是Close了, 但是我将 web view_flutter 版本升级到最新版本后,发现修改最终没有合入到仓库中。
到这里我们有了两条路可以走:
- 本地维护 `webview_flutter `参照上述中PR中的解决方案,Android部分通过修改 `FlutterWebViewClient.java `中WebViewClient的创建解决问题。
- 再找找还有没有其他维护的WebView控件,能够解决这个问题。
复制代码
我采用了第二种方法,于是找到了这个仓库 flutter_inappwebview。将 ModelViewer 中WebView的创建部分修改一下,证书验证异常的时候依然能够显示页面。
针对 Assets 目录的静态文件服务
这里基本上就可以显示模型了, 但是有个条件你的模型不能有贴图。我看可以看到官方的所有模型都是在一个文件中的,但是我下载的?模型是有贴图的。
本着说不定行呢的心态运行了一下, 不出所料白屏给你看。其实 gltf 文件本身是个 json 文件,当我打开后看到在 gltf 中是有对贴图的定义的。
我们看到 model_viewer 在拼接 html 模版的时候,其实对于模型的加载有一个固定的链接 /model
,因为gltf文件中的贴图是相对路径, 并且其中并没有对文件访问提供服务,所以导致模型无法访问贴图文件白屏。
那怎么解决呢? 其实很简单,我们可以修改 HttpServer 让其成为一个文件服务器,模型地址我们直接使用 assets 路径,并且访问地址直接读取文件返回给页面便可以了!
Model_viewer.dart
String _buildHTML(final String htmlTemplate) {
return HTMLBuilder.build(
...
// 直接将 src 设置到页面中. 不再使用 '/model' 地址映射对象文件
src: widget.src,
...
);
}
Future<void> _initProxy() async {
...
_proxy.listen((final HttpRequest request) async {
final response = request.response;
switch (request.uri.path) {
case '/':
case '/index.html':
final htmlTemplate = await rootBundle
.loadString('packages/model_viewer/etc/assets/template.html');
final html = utf8.encode(_buildHTML(htmlTemplate));
response
..statusCode = HttpStatus.ok
..headers.add("Content-Type", "text/html;charset=UTF-8")
..headers.add("Content-Length", html.length.toString())
..add(html);
break;
case '/model-viewer.js':
final code = await _readAsset(
'packages/model_viewer/etc/assets/model-viewer.js');
response
..statusCode = HttpStatus.ok
..headers
.add("Content-Type", "application/javascript;charset=UTF-8")
..headers.add("Content-Length", code.lengthInBytes.toString())
..add(code);
await response.close();
break;
// 其他的直接通过地址在 assets 目录中查找文件.
default:
try {
final requestUrl = request.uri.path;
// /assets/xxx => assets/xxx
final assetsFilePath = requestUrl.startsWith("/") ? requestUrl.substring(1, requestUrl.length): requestUrl;
// 读取数据
final data = await _readAsset(assetsFilePath);
// 根据链接后缀获取 contentType
final mimeType = lookupMimeType(requestUrl);
response
..statusCode = HttpStatus.ok
..headers.add("Content-Type", mimeType == null ? "application/octet-stream": mimeType)
..headers.add("Content-Length", data.lengthInBytes.toString())
..headers.add("Access-Control-Allow-Origin", "*")
..add(data);
}on Exception catch (e) {
response
..statusCode = HttpStatus.notFound;
}
break;
}
await response.close();
});
}
复制代码
现在我们只需要在调用的地方修改一下就可以访问带贴图的模型了.
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
....
home: ModelViewer(
src: "assets/mooncake2_dinh_rosesally/scene.gltf",
...
),
);
}
}
复制代码
最后
官方公布的模型库: Google model-viewer models
感谢大家看到这里,临近中秋提前祝大家中秋节快乐。不过! 咸月饼好吃,还是甜月饼好吃呢?