吃了吗您?Flutter渲染 3D 月饼模型

·  阅读 2153
吃了吗您?Flutter渲染 3D 月饼模型

我正在参加中秋创意投稿大赛,详情请看:中秋创意投稿大赛

中秋节马上就要到来,往年看着大厂里发放的月饼都特别精致,再看看自己公司发的月饼,就真的只是月饼。想要在 Flutter 中渲染 3D 模型,第一时间想到了去 pub.dev中看看相关的实现方案, 我找到了 model_viewer并有了如下的效果。

渲染月饼的3D模型,模型来自sketchfab

⚠️注意: 本文中思路实际上是采用的 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.

从中我们可以看到这个控件只支持 glTFGLB 两种格式的模型,还能从中了解到他的原理就是通过在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服务提供页面。

image-20210908094505508

ModelViewer 中对明文请求的解决方案

再次查看控件的主页,发现作者也给了简单粗暴的解决方案:

image-20210908094846801

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的创建部分修改一下,证书验证异常的时候依然能够显示页面。

WebView创建修改后

针对 Assets 目录的静态文件服务

这里基本上就可以显示模型了, 但是有个条件你的模型不能有贴图。我看可以看到官方的所有模型都是在一个文件中的,但是我下载的🥮模型是有贴图的。

月饼模型

本着说不定行呢的心态运行了一下, 不出所料白屏给你看。其实 gltf 文件本身是个 json 文件,当我打开后看到在 gltf 中是有对贴图的定义的。

image-20210908111800629

我们看到 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

感谢大家看到这里,临近中秋提前祝大家中秋节快乐。不过! 咸月饼好吃,还是甜月饼好吃呢?

分类:
Android
标签:
分类:
Android
标签: