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 实现一下,有用的话点个赞呗!!!

相关推荐
墨染天姬4 分钟前
【AI】cursor提示词小技巧
前端·数据库·人工智能
烛阴8 分钟前
TEngine 入门系列(一):TEngine 是什么 & 为什么选它
前端·unity3d
转转技术团队22 分钟前
WebNN:让 AI 推理在浏览器中“零距离”运行
前端
刀法如飞43 分钟前
TypeScript 数组去重的 20 种实现方式,哪一种你还不知道?
前端·javascript·算法
IT_陈寒1 小时前
Vite热更新失效?你可能漏了这个小细节
前端·人工智能·后端
海石1 小时前
面试官:说一下你现在使用的 AI IDE,什么,JoyCode 是什么?
前端·ai编程
彩票管理中心秘书长1 小时前
一次搞懂:在Vue里用Showdown渲染Markdown+KaTeX数学公式
前端
m0_738120721 小时前
应急响应(重点)——记一次某公司流量应急溯源分析(附带下载链接)
服务器·前端·数据库·安全·web安全·网络安全
前端Hardy2 小时前
pnpm 11.0 正式登场:安装起飞、安全拉满、彻底告别 npm 依赖
前端
PILIPALAPENG2 小时前
第4周 Day 1:智能体记忆系统——给 Agent 一个"大脑"
前端·人工智能·python