Flutter加载3D方案-WebGL

前言

目前 Flutter 官方没有提供可用的 3D 接口,所以加载 3D 只能借助其他方式,如:WebGL、Unity、原生混合等。从实现效率及简易程度上考虑 WebGL 无疑是最简单的方案。

官方 webview_flutter 插件加载本地网页没有合适的 API 和防止跨域问题,利用 local_assets_server 插件启动本地 HTTP 服务器。

dart 复制代码
  void _initServer() async {
    final server = LocalAssetsServer(
      address: InternetAddress.loopbackIPv4,
      assetsBasePath: 'assets',// 代理本地资源目录
      logger: const DebugLogger(),
    );
    final address = await server.serve();
    String url = 'http://${address.address}:${server.boundPort}';
  }

Android 平台需满足 webview_flutter 插件的最低版本要求,打开 android/app/build.gradle 文件,设置 minSdkVersion >= 19。

xml 复制代码
android {
    ...
    defaultConfig {
        ...
        minSdk = 24
    }
    ...
}

默认情况下,移动端网络请求应通过 HTTPS 进行,以确保数据传输的安全性,需要配置应用允许 HTTP 请求。

Android 平台:打开 android/app/src/main/AndroidManifest.xml 文件,在 application 标签内添加 android:usesCleartextTraffic="true" 属性。

xml 复制代码
<application
    android:usesCleartextTraffic="true"
    ... >
    ...
</application>

IOS 平台: 打开 ios/Runner/Info.plist 文件,添加 io.flutter.embedded_views_preview 键并设置为 true。

xml 复制代码
<plist version="1.0">
    <dict>
    ...
    <key>io.flutter.embedded_views_preview</key>
    <true/>
    </dict>
</plist>

通过 WebGL 流行库 three.js 加载 3D,将相关代码和模型放入本地服务器代理的资源目录中。

Web 端默认情况下 dart:ui 和 dart:html 库是 web-only 的,不允许外部调用,需动态引入该方法。

dart 复制代码
// 新建 dart_ui_web_fake.dart
final PlatformViewRegistry platformViewRegistry = PlatformViewRegistry();

class PlatformViewRegistry {

  bool registerViewFactory(
    String viewType,
    Function viewFactory, {
    bool isVisible = true,
  }) =>
      false;
}

// 新建 dart_web_fake.dart
class IFrameElement {
  dynamic get style => null;
  dynamic src;
}

移动端使用 webview_flutter,web 端使用 IFrameElement 加载本地网页

dart 复制代码
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'shim/dart_ui_web_fake.dart' if (dart.library.ui_web) 'dart:ui_web' as ui_web;
import 'shim/dart_web_fake.dart' if (dart.library.js_interop) 'dart:html';
import 'package:local_assets_server/local_assets_server.dart';

class WebViewer extends StatefulWidget {
  const WebViewer({super.key});

  @override
  State<StatefulWidget> createState() {
    return WebViewerState();
  }
}

class WebViewerState extends State<WebViewer> {
  final String _uniqueViewType = 'web-viewer-${UniqueKey().toString()}';
  bool _isListening = false;
  String _webUrl = 'assets/web/index.html?name=model.glb';

  @override
  void initState() {
    super.initState();
    if (kIsWeb) {
      _initWeb();
    } else {
      _initServer();
    }
  }

  void _initWeb() {
    ui_web.platformViewRegistry.registerViewFactory(
        _uniqueViewType,
        (viewId) => IFrameElement()
          ..style.border = 'none'
          ..style.height = '100%'
          ..style.width = '100%'
          ..src = _webUrl);
  }

  void _initServer() async {
    final server = LocalAssetsServer(
      address: InternetAddress.loopbackIPv4,
      assetsBasePath: 'assets',
      logger: const DebugLogger(),
    );
    final address = await server.serve();
    setState(() {
      _isListening = true;
      _webUrl = _webUrl.replaceAll('assets','http://${address.address}:${server.boundPort}');
    });
  }

  @override
  Widget build(BuildContext context) {
    if (kIsWeb) {
      return HtmlElementView(viewType: _uniqueViewType);
    } else {
      if (_isListening) {
        return WebViewWidget(
            controller: WebViewController()
              ..setJavaScriptMode(JavaScriptMode.unrestricted)
              ..setBackgroundColor(const Color(0x00000000))
              ..setNavigationDelegate(
                NavigationDelegate(
                  onProgress: (int progress) {
                    print(progress);
                  },
                  onPageStarted: (String url) {
                    print(url);
                  },
                  onPageFinished: (String url) {
                    print(url);
                  },
                  onHttpError: (HttpResponseError error) {
                    print(error.response);
                  },
                  onWebResourceError: (WebResourceError error) {
                    print(error);
                  },
                  onNavigationRequest: (NavigationRequest request) {
                    return NavigationDecision.navigate;
                  },
                ),
              )
              ..loadRequest(Uri.parse(_webUrl)));
      } else {
        return const Center(child: CircularProgressIndicator());
      }
    }
  }
}

在 pub 上有不少类似实现原理的插件,如 model_viewer_plus , o3d 等,还发现个基于 dart 和 three.js 编写的 three_dart 但因 gradle 版本过低应用不兼容报错。若简单展示模型可以看看。

该方案在性能方面只能说仁者见仁智者见智了。

完活。需要完整代码示例后台回复 Flutter 3D,后续有机会用 unity 实现一下,有用的话点个赞呗!!!

相关推荐
卸任1 天前
Vibe coding工具使用心得
前端·ai编程
brien91 天前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance1 天前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M1 天前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写1 天前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李1 天前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易1 天前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
风骏时光牛马1 天前
前沿视界
前端
kyriewen1 天前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员
陈随易1 天前
3000 万人在用的翻译插件,重写一遍就是一门生意
前端·后端·程序员