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 天前
设计稿是 #4A7C6F,页面量出来是 #4B7C6F:HEX、HSL、透明度、canvas 来回转的七个坑
前端·javascript·css·canvas
郑州光合科技余经理1 天前
海外版外卖加盟:总站与分站配送规则怎么分开管
java·开发语言·前端·后端·uni-app·php·ai编程
小呆呆6661 天前
副业搞起来,小说,漫画,漫剧的成本优化思路
前端·后端·面试
Dovis(誓平步青云)1 天前
浇水提醒刚弹出又消失,植物状态别只存一个百分比
开发语言·前端·javascript·pdf·ecmascript·电脑
码艺-Alimjan1 天前
Vben Admin 新增维吾尔语 Vben-Modal的关键坑之一
前端·javascript·vue.js
可乐鸡翅yeah_1 天前
hls.js 手动自定义 http 请求 loader,修改请求头实战
开发语言·前端·javascript·网络协议·http·ecmascript·m3u8在线
IT_陈寒1 天前
Vite静态资源导入这个坑我帮你们踩过了
前端·人工智能·后端
广州华水科技1 天前
大坝安全监测解决方案:单北斗GNSS形变监测系统应用与维护
前端
yivifu1 天前
中文古籍电子书注释集成
前端·javascript·python·beautifulsoup·epub
默_笙1 天前
🛴 从散件到整机:DeepAgents 与 Agent 身上预留的那些"插槽"(前置介绍)
前端·javascript