using System; using System.Collections; using System.Collections.Generic; using System.Runtime.InteropServices; using Kill.UI.Components; using UnityEngine; using UnityEngine.UI; namespace Kill.UI.Pages { /// /// 二维码扫描器 - 调用摄像头扫描二维码 /// public class ScanQRcode : MonoBehaviour { [Serializable] public enum ScanType { ConnectDevice, // 连接设备 Other // 其他用途 } [Header("UI组件")] public RawImage cameraPreview; // 摄像头预览 [Header("扫描设置")] float scanInterval = 0.05f; // 扫描间隔(秒),密集扫描提升远距离识别率 public ScanType scanType = ScanType.ConnectDevice; // 扫描类型 [Header("性能优化")] [Tooltip("扫描处理线程数")] bool useAsyncScan = true; [Header("中心裁切放大")] [Tooltip("截取画面中心区域的比例(0-1),值越小放越大。\n推荐:近距离 0.55~0.65;远距离扫码 0.8~0.9")] [Range(0.3f, 1.0f)] float centerCropRatio = 1f; #if UNITY_IOS && !UNITY_EDITOR [DllImport("__Internal")] private static extern void _EnableContinuousAutoFocus(); [DllImport("__Internal")] private static extern void _TriggerAutoFocus(float normalizedX, float normalizedY); [DllImport("__Internal")] private static extern void _TriggerContinuousRefocus(); #endif #if UNITY_ANDROID && !UNITY_EDITOR // 通过反射拿到的 WebCamTexture 内部 Camera 实例(Unity 2022 LTS 一般拿不到,详见 EnableAndroidContinuousAutoFocus) private AndroidJavaObject androidCameraInstance; // 标记是否成功接管对焦控制 private bool androidFocusControlAvailable = false; #endif // 摄像头相关 private WebCamTexture webCamTexture; private WebCamDevice currentDevice; private bool isScanning = false; private bool isProcessing = false; // 扫描结果回调 public event Action OnQRCodeScanned; void OnEnable() { StartScan(); } void OnDisable() { StopScan(); } // 手动重新对焦防抖时间:0.6s 内的重复点击只触发一次,避免过于频繁触发对焦 private float lastManualRefocusTime = -10f; private const float ManualRefocusCooldown = 0.6f; /// /// 用户手动点击预览时调用:触发一次软重新对焦(无黑屏,体验好)。 /// 已接管 Camera 的设备走点对焦;未接管(Unity 2022 LTS 默认情况)走 autoFocusPoint 重新设置。 /// public void TriggerRefocus() { if (!isScanning) return; if (Time.unscaledTime - lastManualRefocusTime < ManualRefocusCooldown) return; lastManualRefocusTime = Time.unscaledTime; #if UNITY_IOS && !UNITY_EDITOR // iOS:调用原生插件触发一次点对焦(1.5s 后原生自动恢复连续对焦,无黑屏) _TriggerContinuousRefocus(); #elif UNITY_ANDROID && !UNITY_EDITOR if (androidFocusControlAvailable && androidCameraInstance != null) { // 已接管:走正常的点对焦 TriggerAndroidAutoFocus(0.5f, 0.5f); } else { // 未接管:走软触发(无黑屏) Debug.Log("[ScanQRcode] 用户点击预览,触发软重新对焦"); SoftTriggerAndroidFocus(); } #else // 编辑器/其他平台:无操作 #endif } /// /// 开始扫描 /// public void StartScan() { if (isScanning) return; StartCoroutine(InitializeCamera()); } /// /// 停止扫描 /// public void StopScan() { isScanning = false; StopAllCoroutines(); if (webCamTexture != null && webCamTexture.isPlaying) { webCamTexture.Stop(); webCamTexture = null; } // 清理预览纹理 if (previewTexture != null) { Destroy(previewTexture); previewTexture = null; } #if UNITY_ANDROID && !UNITY_EDITOR // 反射拿到的 Camera 实例由 WebCamTexture 内部管理,仅清空引用即可 if (androidCameraInstance != null) { androidCameraInstance.Dispose(); androidCameraInstance = null; } androidFocusControlAvailable = false; #endif } /// /// 关闭扫码界面 /// public void Close() { StopScan(); gameObject.SetActive(false); } /// /// 初始化摄像头 /// private IEnumerator InitializeCamera() { // Android 使用 Permission API 请求权限 #if UNITY_ANDROID && !UNITY_EDITOR if (!UnityEngine.Android.Permission.HasUserAuthorizedPermission(UnityEngine.Android.Permission.Camera)) { UnityEngine.Android.Permission.RequestUserPermission(UnityEngine.Android.Permission.Camera); // 等待权限请求结果 yield return new WaitUntil(() => UnityEngine.Android.Permission.HasUserAuthorizedPermission(UnityEngine.Android.Permission.Camera)); } if (!UnityEngine.Android.Permission.HasUserAuthorizedPermission(UnityEngine.Android.Permission.Camera)) { UpdateStatus("100085"); yield break; } #else // 其他平台使用传统方式 yield return Application.RequestUserAuthorization(UserAuthorization.WebCam); if (!Application.HasUserAuthorization(UserAuthorization.WebCam)) { UpdateStatus("100085"); yield break; } #endif // 获取后置摄像头 WebCamDevice[] devices = WebCamTexture.devices; if (devices.Length == 0) { UpdateStatus("100086"); yield break; } // 优先使用后置摄像头,并选择支持自动对焦的摄像头 string deviceName = devices[0].name; for (int i = 0; i < devices.Length; i++) { if (!devices[i].isFrontFacing) { deviceName = devices[i].name; currentDevice = devices[i]; break; } } RectTransform cameraPreviewRect=cameraPreview.GetComponent(); // 尝试更高分辨率(4K);WebCamTexture 在不支持时会自动回退到设备最大值 // 注意:高分辨率下解码 CPU 压力大,需要配合性能较好的设备 webCamTexture = new WebCamTexture(deviceName, 3840, 2160, 30); // 开始播放 webCamTexture.Play(); // 等待摄像头启动 yield return new WaitUntil(() => webCamTexture.width > 100); // 输出实际分辨率,方便确认设备用的最大值 Debug.Log($"[ScanQRcode] 摄像头实际分辨率: {webCamTexture.width}x{webCamTexture.height}"); // 等 0.3s 再触发对焦,避免系统未准备好 yield return new WaitForSeconds(0.3f); #if UNITY_IOS && !UNITY_EDITOR // iOS: 通过原生插件开启连续自动对焦(autoFocusPoint 在 iOS 无效) // iOS 系统会自动维持对焦状态,无需周期触发 _EnableContinuousAutoFocus(); #elif UNITY_ANDROID && !UNITY_EDITOR // Android:WebCamTexture 默认不会自动对焦,需主动通过 Camera API 触发 EnableAndroidContinuousAutoFocus(); // 仅当我们拿到 Camera 实例时才启动对焦维护协程(Unity 2022 LTS Camera2 后端一般拿不到) if (androidFocusControlAvailable) { StartCoroutine(AndroidFocusMaintainCoroutine()); } else { // 兜底:未接管到 Camera 时,周期性触发重新对焦(部分只对焦一次的设备) StartCoroutine(AndroidFallbackFocusMaintainCoroutine()); } #endif webCamTexture.autoFocusPoint = new Vector2(0.5f, 0.5f); // 调整预览画面比例 AdjustPreviewAspect(); isScanning = true; // 开始预览更新和扫描 StartCoroutine(PreviewUpdateCoroutine()); StartCoroutine(ScanCoroutine()); } // 预览纹理长边基准(按 RawImage 实际宽高比生成,避免变形) // 保持 UI RectTransform 比例,内部纹理按比例放大到接近 1080 长边 // 但不超过相机实际分辨率的 1.5 倍,避免低端设备过度放大导致画质损失 private const int PreviewTextureLongSide = 1080; private const float MaxUpscaleRatio = 1.5f; /// /// 调整预览画面比例 - 从相机画面中心截取并旋转,生成与 UI 框同比例的高清纹理。 /// 手机竖着拿,摄像头默认横向输出,需要旋转 90 度。 /// 预览纹理宽高比跟随 RawImage RectTransform 宽高比,长边固定 1080, /// 既保证远距离扫码识别率,又避免画面变形。 /// 对低端设备(720p 摄像头)会自适应降低纹理尺寸,避免过度放大导致画质损失。 /// private void AdjustPreviewAspect() { if (webCamTexture == null || cameraPreview == null) return; // 获取相机分辨率(摄像头默认横向,如 3840x2160 / 1920x1080 / 1280x720) int cameraWidth = webCamTexture.width; int cameraHeight = webCamTexture.height; // 获取预览区域尺寸(用于按比例生成纹理,避免变形) RectTransform previewRect = cameraPreview.GetComponent(); float rectW = Mathf.Max(1, previewRect.rect.width); float rectH = Mathf.Max(1, previewRect.rect.height); // 相机短边(旋转后对应预览宽边),用于限制过度放大 int cameraShortSide = Mathf.Min(cameraWidth, cameraHeight); // 按 RectTransform 比例计算纹理尺寸(长边固定 1080) int previewWidth, previewHeight; if (rectW >= rectH) { previewWidth = PreviewTextureLongSide; previewHeight = Mathf.Max(1, Mathf.RoundToInt(PreviewTextureLongSide * (rectH / rectW))); } else { previewHeight = PreviewTextureLongSide; previewWidth = Mathf.Max(1, Mathf.RoundToInt(PreviewTextureLongSide * (rectW / rectH))); } // 自适应:如果纹理长边超过相机短边的 1.5 倍,按相机分辨率等比缩小(保护低端设备画质) int previewLongSide = Mathf.Max(previewWidth, previewHeight); int maxAllowedLong = Mathf.RoundToInt(cameraShortSide * MaxUpscaleRatio); if (previewLongSide > maxAllowedLong) { float scale = (float)maxAllowedLong / previewLongSide; previewWidth = Mathf.Max(1, Mathf.RoundToInt(previewWidth * scale)); previewHeight = Mathf.Max(1, Mathf.RoundToInt(previewHeight * scale)); } // 创建预览纹理(按 UI 比例) if (previewTexture == null || previewTexture.width != previewWidth || previewTexture.height != previewHeight) { if (previewTexture != null) { Destroy(previewTexture); } previewTexture = new Texture2D(previewWidth, previewHeight, TextureFormat.RGB24, false); } // 从相机画面中裁切并旋转 CutAndRotateCameraFrame(cameraWidth, cameraHeight, previewWidth, previewHeight); // 设置预览纹理 cameraPreview.texture = previewTexture; cameraPreview.uvRect = new Rect(0, 0, 1, 1); // 重置旋转(因为已经在像素层面旋转了) previewRect.localEulerAngles = Vector3.zero; Debug.Log($"[ScanQRcode] 预览调整 - 相机: {cameraWidth}x{cameraHeight}, 预览: {previewWidth}x{previewHeight}"); } #if UNITY_ANDROID && !UNITY_EDITOR /// /// 启用 Android 自动对焦。 /// Unity 2022 LTS 在 Android 上使用 Camera2 API 内部创建 CameraDevice, /// 无法直接反射拿到 Camera 对象,本方法仅尝试拿到旧 Camera API 的实例。 /// 失败时仅打印日志,不阻塞扫码流程(依赖系统默认对焦)。 /// private void EnableAndroidContinuousAutoFocus() { androidCameraInstance = GetAndroidCameraFromWebCamTexture(webCamTexture); if (androidCameraInstance != null) { try { SetCameraFocusMode(androidCameraInstance, "FOCUS_MODE_CONTINUOUS_PICTURE"); androidFocusControlAvailable = true; Debug.Log("[ScanQRcode] Android 自动对焦已接管(FOCUS_MODE_CONTINUOUS_PICTURE)"); } catch (Exception e) { Debug.LogWarning($"[ScanQRcode] 设置 Android 自动对焦模式失败: {e.Message}"); androidFocusControlAvailable = false; } } else { androidFocusControlAvailable = false; // 尝试让 WebCamTexture 自身使用自动对焦(部分 ROM 支持) try { webCamTexture.autoFocusPoint = new Vector2(0.5f, 0.5f); } catch { } } } /// /// 触发 Android 单次自动对焦(指定点) /// private void TriggerAndroidAutoFocus(float normX, float normY) { if (!androidFocusControlAvailable || androidCameraInstance == null) { // 没接管到对焦,触摸对焦无效(保持安静,不打日志避免刷屏) return; } try { // 屏幕坐标 -> camera 坐标系(Camera API 的 focusAreas 使用 -1000~1000 坐标系) int focusX = Mathf.RoundToInt((normX - 0.5f) * 2000f); int focusY = Mathf.RoundToInt((normY - 0.5f) * 2000f); using (var rectClass = new AndroidJavaClass("android.graphics.Rect")) using (var area = rectClass.CallStatic("new", focusX - 100, focusY - 100, focusX + 100, focusY + 100)) using (var areasList = new AndroidJavaObject("java.util.ArrayList")) { areasList.Call("add", area); androidCameraInstance.Call("setFocusAreas", areasList); androidCameraInstance.Call("setMeteringAreas", areasList); } // 先切到 AUTO 模式才能执行单次对焦,对焦完再切回 CONTINUOUS SetCameraFocusMode(androidCameraInstance, "FOCUS_MODE_AUTO"); // AndroidJavaProxy 不是 IDisposable,不能 using;也不需要 Dispose,GC 即可回收 var callback = new AutoFocusCallbackProxy(this); androidCameraInstance.Call("autoFocus", callback); // 1.5s 后切回连续模式(对焦回调可能未触发,需要兜底) StartCoroutine(RestoreContinuousFocusAfter(1.5f)); } catch (Exception e) { Debug.LogWarning($"[ScanQRcode] Android 触摸对焦失败: {e.Message}"); } } private IEnumerator RestoreContinuousFocusAfter(float seconds) { yield return new WaitForSeconds(seconds); if (androidFocusControlAvailable && androidCameraInstance != null) { SetCameraFocusMode(androidCameraInstance, "FOCUS_MODE_CONTINUOUS_PICTURE"); } } /// /// 周期性重新设置连续对焦,防止 Android 镜头因失焦后停止自动对焦 /// private IEnumerator AndroidFocusMaintainCoroutine() { while (isScanning) { yield return new WaitForSeconds(2.5f); if (androidFocusControlAvailable && androidCameraInstance != null) { try { SetCameraFocusMode(androidCameraInstance, "FOCUS_MODE_CONTINUOUS_PICTURE"); } catch { /* 实例可能已被释放,下次循环重试 */ } } } } /// /// 兜底对焦策略:未接管到 Camera 实例时(Unity 2022 LTS 默认情况), /// 每 3s 通过 softTrigger(autoFocusPoint 重新设置,几乎无感)让镜头重新对焦。 /// 用户也可通过调用 TriggerRefocus() 主动触发。 /// private IEnumerator AndroidFallbackFocusMaintainCoroutine() { // 首次等待 3s 让初始对焦完成 yield return new WaitForSeconds(3f); while (isScanning && webCamTexture != null) { // 如果在运行中已经接管到对焦控制(理论上不会变),停止兜底 if (androidFocusControlAvailable) break; // 软触发:重新设置 autoFocusPoint(无黑屏) SoftTriggerAndroidFocus(); yield return new WaitForSeconds(3f); } } /// /// 软触发对焦:通过重新设置 autoFocusPoint 让手机做一次快速对焦,无黑屏。 /// 部分手机响应后会重新对焦,部分手机忽略此属性(需要走硬重启兜底)。 /// private void SoftTriggerAndroidFocus() { if (webCamTexture == null || !webCamTexture.isPlaying) return; try { // 在 (0.5, 0.5) 和 (0.5, 0.51) 之间切换,强制触发 setter // 相同值部分 ROM 不响应,需要做点变化 webCamTexture.autoFocusPoint = new Vector2(0.5f, 0.51f); webCamTexture.autoFocusPoint = new Vector2(0.5f, 0.5f); } catch (Exception e) { Debug.LogWarning($"[ScanQRcode] softTrigger 失败: {e.Message}"); } } /// /// 通过停止+重新播放 WebCamTexture 强制镜头重新对焦。 /// 注意:会有约 200~500ms 的预览黑屏闪烁,权衡使用。 /// private void RestartWebCamTextureForFocus() { try { if (webCamTexture == null) return; webCamTexture.Stop(); webCamTexture.Play(); } catch (Exception e) { Debug.LogWarning($"[ScanQRcode] 重启 WebCamTexture 失败: {e.Message}"); } } /// /// 通过反射从 WebCamTexture 拿到内部的 android.hardware.Camera 实例。 /// 注意:Unity 2022 LTS 在 Android 上使用 Camera2 API,此反射在 2022 LTS 上几乎拿不到值。 /// private AndroidJavaObject GetAndroidCameraFromWebCamTexture(WebCamTexture tex) { try { string[] candidates = { "m_Camera", "m_NativeCamera", "camera", "mCamera" }; foreach (var name in candidates) { var f = typeof(WebCamTexture).GetField(name, System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); if (f != null) { var v = f.GetValue(tex); if (v is AndroidJavaObject ajo) return ajo; } } } catch (Exception e) { Debug.LogWarning($"[ScanQRcode] 反射获取 Camera 失败: {e.Message}"); } return null; } /// /// 设置 Camera 对焦模式(Camera API 官方推荐通过 setParameters 设置) /// private void SetCameraFocusMode(AndroidJavaObject camera, string modeName) { if (camera == null) return; using (var parameters = camera.Call("getParameters")) { parameters.Call("setFocusMode", modeName); camera.Call("setParameters", parameters); } // 部分旧设备直接调用 setFocusMode(String) 也生效,兼容一下 camera.Call("setFocusMode", modeName); } /// /// AutoFocusCallback 的 C# 实现(AndroidJavaProxy) /// private class AutoFocusCallbackProxy : AndroidJavaProxy { private readonly ScanQRcode _owner; public AutoFocusCallbackProxy(ScanQRcode owner) : base("android.hardware.Camera$AutoFocusCallback") { _owner = owner; } // Java 签名: void onAutoFocus(boolean success, Camera camera) public void onAutoFocus(bool success, AndroidJavaObject camera) { try { if (_owner != null && _owner.androidFocusControlAvailable && _owner.androidCameraInstance != null) { _owner.SetCameraFocusMode(_owner.androidCameraInstance, "FOCUS_MODE_CONTINUOUS_PICTURE"); } } catch { } } } #endif private Texture2D previewTexture; /// /// 从相机画面裁切并旋转,生成预览画面 /// 支持中心裁切放大:只截取画面中心区域,放大到预览尺寸 /// 性能优化:按行复用 float 增量,避免重复乘法 /// private void CutAndRotateCameraFrame(int camW, int camH, int targetW, int targetH) { // 计算中心裁切区域大小 int sourceCropH = Mathf.Max(1, Mathf.RoundToInt(targetH * centerCropRatio)); int sourceCropW = Mathf.Max(1, Mathf.RoundToInt(targetW * centerCropRatio)); int cropY = (camW - sourceCropH) / 2; int cropX = (camH - sourceCropW) / 2; // 两个方向缩放一致,保证不变形 float scale = centerCropRatio; // 获取相机像素数据 Color32[] cameraPixels = webCamTexture.GetPixels32(); Color32[] targetPixels = new Color32[targetW * targetH]; // 顺时针旋转90度 + 中心裁切 + 缩放 for (int y = 0; y < targetH; y++) { float srcXBase = cropY + (targetH - 1 - y) * scale; // 整行的 srcX float srcYBase = cropX; // 起始 srcY int rowStart = y * targetW; for (int x = 0; x < targetW; x++) { float srcX = srcXBase; float srcY = srcYBase + x * scale; int sx = (int)srcX; int sy = (int)srcY; float fx = srcX - sx; float fy = srcY - sy; int sx1 = Mathf.Min(sx + 1, camW - 1); int sy1 = Mathf.Min(sy + 1, camH - 1); int idx00 = sy * camW + sx; int idx10 = sy * camW + sx1; int idx01 = sy1 * camW + sx; int idx11 = sy1 * camW + sx1; Color32 c00 = cameraPixels[idx00]; Color32 c10 = cameraPixels[idx10]; Color32 c01 = cameraPixels[idx01]; Color32 c11 = cameraPixels[idx11]; float r = (1-fx)*(1-fy)*c00.r + fx*(1-fy)*c10.r + (1-fx)*fy*c01.r + fx*fy*c11.r; float g = (1-fx)*(1-fy)*c00.g + fx*(1-fy)*c10.g + (1-fx)*fy*c01.g + fx*fy*c11.g; float b = (1-fx)*(1-fy)*c00.b + fx*(1-fy)*c10.b + (1-fx)*fy*c01.b + fx*fy*c11.b; targetPixels[rowStart + x] = new Color32( (byte)Mathf.Clamp(r, 0, 255), (byte)Mathf.Clamp(g, 0, 255), (byte)Mathf.Clamp(b, 0, 255), 255 ); } } // 设置像素到预览纹理 previewTexture.SetPixels32(targetPixels); previewTexture.Apply(); } [SerializeField] private float previewUpdateInterval = 0.03f; // 预览更新间隔(20fps) /// /// 预览更新协程 - 降低更新频率避免卡顿 /// private IEnumerator PreviewUpdateCoroutine() { while (isScanning && webCamTexture != null && webCamTexture.isPlaying) { // 更新预览画面(限制频率) if (previewTexture != null) { CutAndRotateCameraFrame(webCamTexture.width, webCamTexture.height, previewTexture.width, previewTexture.height); } yield return new WaitForSeconds(previewUpdateInterval); } } /// /// 扫描协程 /// private IEnumerator ScanCoroutine() { // 等待一帧确保摄像头已准备好 yield return null; while (isScanning && webCamTexture != null && webCamTexture.isPlaying) { // 避免同时处理多帧 if (isProcessing) { yield return null; continue; } isProcessing = true; // 使用预览纹理进行扫描 if (previewTexture != null) { // 异步解析二维码以避免卡顿 if (useAsyncScan) { // 直接获取 RGB24 原始字节,无需手动转换 var rawData = previewTexture.GetRawTextureData(); byte[] rgbBytes = rawData.ToArray(); int texWidth = previewTexture.width; int texHeight = previewTexture.height; string result = null; bool scanComplete = false; // 在后台线程执行 ZXing 解码(纯计算,不涉及 Unity API) System.Threading.ThreadPool.QueueUserWorkItem(_ => { try { result = DecodeQRCodeFromBytes(rgbBytes, texWidth, texHeight); } catch (Exception e) { Debug.LogError($"[ScanQRcode] 解码异常: {e.Message}"); } finally { scanComplete = true; } }); // 等待解码完成 yield return new WaitUntil(() => scanComplete); // 处理结果 if (!string.IsNullOrEmpty(result)) { ProcessScanResult(result); } } else { // 同步解码 string result = DecodeQRCode(previewTexture); if (!string.IsNullOrEmpty(result)) { ProcessScanResult(result); } } } isProcessing = false; yield return new WaitForSeconds(scanInterval); } } /// /// 处理扫描结果 /// private void ProcessScanResult(string result) { Debug.Log($"扫描结果: {result}"); // 根据扫描类型处理结果 if (scanType == ScanType.ConnectDevice) { // 从结果中提取 MAC 地址 string macAddress = ExtractMacAddress(result); if (!string.IsNullOrEmpty(macAddress)) { // 扫描成功,仅回传 MAC 地址 OnQRCodeScanned?.Invoke(macAddress); OnQRCodeScanned-=OnQRCodeScanned; } else { // 二维码不符合要求,显示提示 UpdateStatus("100084"); } } else { if (!string.IsNullOrEmpty(result)) { // 其他用途,直接回传结果 OnQRCodeScanned?.Invoke(result); } else { // 二维码不符合要求,显示提示 UpdateStatus("100084"); } } } /// /// 获取摄像头当前帧 /// private Texture2D GetSnapshot() { if (webCamTexture == null || !webCamTexture.isPlaying) return null; try { Texture2D snapshot = new Texture2D(webCamTexture.width, webCamTexture.height); snapshot.SetPixels(webCamTexture.GetPixels()); snapshot.Apply(); return snapshot; } catch { return null; } } /// /// 放大纹理(使用双线性插值)- 优化版本 /// private Texture2D UpscaleTexture(Texture2D source, float factor) { // 限制放大倍数,避免过度消耗性能 factor = Mathf.Clamp(factor, 1f, 3f); int newWidth = Mathf.RoundToInt(source.width * factor); int newHeight = Mathf.RoundToInt(source.height * factor); // 限制最大尺寸,避免内存问题 newWidth = Mathf.Min(newWidth, 1280); newHeight = Mathf.Min(newHeight, 720); // 如果尺寸没有变化,直接返回原图 if (newWidth <= source.width || newHeight <= source.height) { return source; } Texture2D result = new Texture2D(newWidth, newHeight, TextureFormat.RGB24, false); // 使用 RenderTexture 进行高质量缩放 RenderTexture rt = RenderTexture.GetTemporary(newWidth, newHeight, 0, RenderTextureFormat.RGB565); RenderTexture.active = rt; Graphics.Blit(source, rt); result.ReadPixels(new Rect(0, 0, newWidth, newHeight), 0, 0); result.Apply(); RenderTexture.active = null; RenderTexture.ReleaseTemporary(rt); // 销毁原始纹理 Destroy(source); return result; } /// /// 解析二维码 /// private string DecodeQRCode(Texture2D texture) { try { // 全图扫描 string result = TryDecodeRegion(texture, 0, 0, texture.width, texture.height); if (!string.IsNullOrEmpty(result)) return result; return null; } catch (Exception ex) { Debug.LogError($"二维码解析失败: {ex.Message}"); return null; } } /// /// 从原始字节数据解析二维码(不依赖 Unity API,可在后台线程调用) /// private string DecodeQRCodeFromBytes(byte[] rgbBytes, int width, int height) { try { var luminanceSource = new ZXing.RGBLuminanceSource(rgbBytes, width, height); string result = TryDecodeWithBinarizer(luminanceSource); if (!string.IsNullOrEmpty(result)) return result; return null; } catch (Exception ex) { Debug.LogError($"二维码解析失败: {ex.Message}"); return null; } } /// /// 尝试解码指定区域 /// private string TryDecodeRegion(Texture2D texture, int startX, int startY, int width, int height) { try { // 确保区域在有效范围内 startX = Mathf.Max(0, startX); startY = Mathf.Max(0, startY); width = Mathf.Min(width, texture.width - startX); height = Mathf.Min(height, texture.height - startY); // 获取全图像素数据 Color32[] allPixels = texture.GetPixels32(); // 提取指定区域的像素 Color32[] regionPixels = new Color32[width * height]; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int srcIndex = (startY + y) * texture.width + (startX + x); int dstIndex = y * width + x; if (srcIndex < allPixels.Length) { regionPixels[dstIndex] = allPixels[srcIndex]; } } } // 转换为 RGB 字节数组(ZXing 内部会自动处理灰度转换) byte[] rgbBytes = new byte[regionPixels.Length * 3]; for (int i = 0; i < regionPixels.Length; i++) { rgbBytes[i * 3] = regionPixels[i].r; rgbBytes[i * 3 + 1] = regionPixels[i].g; rgbBytes[i * 3 + 2] = regionPixels[i].b; } // 使用 RGBLuminanceSource,让 ZXing 内部处理灰度转换 var luminanceSource = new ZXing.RGBLuminanceSource(rgbBytes, width, height); // 尝试解码 string result = TryDecodeWithBinarizer(luminanceSource); if (!string.IsNullOrEmpty(result)) return result; return null; } catch { return null; } } /// /// 尝试使用不同的二值化算法解码 /// private string TryDecodeWithBinarizer(ZXing.LuminanceSource luminanceSource) { var hints = new Dictionary { { ZXing.DecodeHintType.TRY_HARDER, true }, { ZXing.DecodeHintType.POSSIBLE_FORMATS, new List { ZXing.BarcodeFormat.QR_CODE } } }; var reader = new ZXing.QrCode.QRCodeReader(); // 尝试 HybridBinarizer try { var binarizer = new ZXing.Common.HybridBinarizer(luminanceSource); var binaryBitmap = new ZXing.BinaryBitmap(binarizer); var result = reader.decode(binaryBitmap, hints); if (result != null) return result.Text; } catch { } // 尝试 GlobalHistogramBinarizer try { var binarizer = new ZXing.Common.GlobalHistogramBinarizer(luminanceSource); var binaryBitmap = new ZXing.BinaryBitmap(binarizer); var result = reader.decode(binaryBitmap, hints); if (result != null) return result.Text; } catch { } return null; } /// /// 应用锐化滤镜(拉普拉斯算子) /// 增强图像边缘,提高模糊二维码识别率 /// private byte[] ApplySharpenFilter(byte[] input, int width, int height) { byte[] output = new byte[input.Length]; // 拉普拉斯锐化核 // 0 -1 0 // -1 5 -1 // 0 -1 0 int[] kernel = { 0, -1, 0, -1, 5, -1, 0, -1, 0 }; int kernelSize = 3; int halfKernel = kernelSize / 2; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int sum = 0; // 应用卷积核 for (int ky = -halfKernel; ky <= halfKernel; ky++) { for (int kx = -halfKernel; kx <= halfKernel; kx++) { int py = Mathf.Clamp(y + ky, 0, height - 1); int px = Mathf.Clamp(x + kx, 0, width - 1); int pixelIndex = py * width + px; int kernelIndex = (ky + halfKernel) * kernelSize + (kx + halfKernel); sum += input[pixelIndex] * kernel[kernelIndex]; } } // 限制在0-255范围内 output[y * width + x] = (byte)Mathf.Clamp(sum, 0, 255); } } return output; } /// /// 从字符串中提取 MAC 地址 /// 支持格式: /// 1. 带冒号格式:98:EA:A0:02:4E:06 /// 2. 不带冒号格式:98eaa002658e /// 3. 从键值对格式提取:Name:xxx;MAC:98eaa002658e /// private string ExtractMacAddress(string input) { if (string.IsNullOrEmpty(input)) return null; string macAddress = null; // 尝试从键值对格式提取 MAC:xxx // 匹配 MAC: 后面跟着 12 位十六进制字符(可能带冒号或不带) System.Text.RegularExpressions.Regex macKeyValueRegex = new System.Text.RegularExpressions.Regex(@"MAC:([0-9A-Fa-f]{2}:?[0-9A-Fa-f]{2}:?[0-9A-Fa-f]{2}:?[0-9A-Fa-f]{2}:?[0-9A-Fa-f]{2}:?[0-9A-Fa-f]{2})"); System.Text.RegularExpressions.Match kvMatch = macKeyValueRegex.Match(input); if (kvMatch.Success) { macAddress = kvMatch.Groups[1].Value; } else { // 尝试匹配带冒号的标准 MAC 格式 System.Text.RegularExpressions.Regex macColonRegex = new System.Text.RegularExpressions.Regex(@"[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}"); System.Text.RegularExpressions.Match colonMatch = macColonRegex.Match(input); if (colonMatch.Success) { macAddress = colonMatch.Value; } else { // 尝试匹配不带冒号的 12 位十六进制字符 System.Text.RegularExpressions.Regex macNoColonRegex = new System.Text.RegularExpressions.Regex(@"[0-9A-Fa-f]{12}"); System.Text.RegularExpressions.Match noColonMatch = macNoColonRegex.Match(input); if (noColonMatch.Success) { macAddress = noColonMatch.Value; } } } if (!string.IsNullOrEmpty(macAddress)) { // 统一转换为带冒号的大写格式 return FormatMacAddress(macAddress); } return null; } /// /// 将 MAC 地址统一格式化为带冒号的大写格式 /// 输入: 98eaa002658e 或 98:EA:A0:02:65:8E /// 输出: 98:EA:A0:02:65:8E /// private string FormatMacAddress(string mac) { if (string.IsNullOrEmpty(mac)) return null; // 移除所有冒号 string cleanMac = mac.Replace(":", "").Replace("-", "").ToUpper(); // 检查长度是否为 12 if (cleanMac.Length != 12) return null; // 格式化为 XX:XX:XX:XX:XX:XX return string.Format("{0}:{1}:{2}:{3}:{4}:{5}", cleanMac.Substring(0, 2), cleanMac.Substring(2, 2), cleanMac.Substring(4, 2), cleanMac.Substring(6, 2), cleanMac.Substring(8, 2), cleanMac.Substring(10, 2)); } /// /// 更新状态文字 /// private void UpdateStatus(string code) { ToastUI.Show(code); } void OnDestroy() { StopScan(); gameObject.SetActive(false); } } }