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

相关推荐
子兮曰2 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰2 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万2 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝2 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋2 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁2 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
汉堡大王95272 天前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端
梦想很大很大2 天前
从运行事实到回归证据:Workrun 的 Telemetry 与 Evaluation 实践
前端·人工智能·后端
计算机魔术师2 天前
Meta Muse agent 接入 Shopify 的 Shop Pay 实现代理式购物
前端
沙蒿同学2 天前
我用 Go 搭了一条 AI Agent 流水线:从 1 张商品图到一整套淘宝详情页
前端·javascript·后端