From 630a624ff768cfd008cd0dd3766691fdc5512f05 Mon Sep 17 00:00:00 2001 From: LiRuoChen <571244399@qq.com> Date: Mon, 8 Jun 2026 09:53:51 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=95=B0=E6=8D=AE=E7=B2=BE?= =?UTF-8?q?=E5=BA=A6=E5=88=B0float?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ScreenMeshPicking.shader | 27 +- .../GeoLayerPickerRenderPassFeature.cs | 369 +++++++++++++++--- 2 files changed, 326 insertions(+), 70 deletions(-) diff --git a/Runtime/MeshDetection_Builtin/ScreenMeshPicking.shader b/Runtime/MeshDetection_Builtin/ScreenMeshPicking.shader index 2726856..1f929a7 100644 --- a/Runtime/MeshDetection_Builtin/ScreenMeshPicking.shader +++ b/Runtime/MeshDetection_Builtin/ScreenMeshPicking.shader @@ -1,9 +1,8 @@ -Shader "XericLibrary/ScreenDectection/MeshPicking" +Shader "XericLibrary/ScreenDectection/MeshPicking_HDR" { Properties { - // 传入材质ID - _MatID ("Material ID", Int) = 0 + [HideInInspector] _MatID ("Material ID", Int) = 0 } SubShader { @@ -12,9 +11,11 @@ Shader "XericLibrary/ScreenDectection/MeshPicking" Pass { - // 关闭光照和雾效,纯数据输出 Lighting Off Fog { Mode Off } + Cull Back + ZTest LEqual + ZWrite On CGPROGRAM #pragma vertex vert @@ -29,32 +30,22 @@ Shader "XericLibrary/ScreenDectection/MeshPicking" struct v2f { float4 vertex : SV_POSITION; - float3 worldPos : TEXCOORD0; // 传递世界坐标给片段着色器 + float3 worldPos : TEXCOORD0; }; - int _MatID; // 接收材质ID + int _MatID; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); - // 计算世界坐标并传递 o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; return o; } - fixed4 frag (v2f i) : SV_Target + float4 frag (v2f i) : SV_Target { - // 1. 将世界坐标编码到 RGB 通道 - // 假设你的地质模型世界坐标范围在 -1000 到 1000 之间 - // 加上 1000 变成 0~2000,再除以 2000 变成 0~1 - float3 encodedPos = (i.worldPos + 1000.0) / 2000.0; - - // 2. 将材质ID编码到 Alpha 通道 (假设ID不超过255) - float encodedID = _MatID / 255.0; - - // 输出 RGBA = (PosX, PosY, PosZ, MatID) - return fixed4(encodedPos.x, encodedPos.y, encodedPos.z, encodedID); + return float4(i.worldPos.xyz, (float)_MatID); } ENDCG } diff --git a/Runtime/MeshDetection_URP/GeoLayerPickerRenderPassFeature.cs b/Runtime/MeshDetection_URP/GeoLayerPickerRenderPassFeature.cs index aae099b..9984c0d 100644 --- a/Runtime/MeshDetection_URP/GeoLayerPickerRenderPassFeature.cs +++ b/Runtime/MeshDetection_URP/GeoLayerPickerRenderPassFeature.cs @@ -1,12 +1,16 @@ -using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; +using System.Collections.Generic; +using Unity.Collections; +using XericLibrary.Runtime.MacroLibrary; -// #if UNITY_2022_1_OR_NEWER // urp 13 +#if UNITY_2022_1_OR_NEWER namespace XericLibrary.PostProcess { - // 定义数据结构,用来存储展平后的 SubMesh 信息 + /// + /// 用于存储展平后的 SubMesh 渲染数据 + /// [System.Serializable] public struct SubMeshDrawData { @@ -16,62 +20,231 @@ namespace XericLibrary.PostProcess public int uniqueMatID; } + /// + /// 解码后的地质层拾取数据 + /// + [System.Serializable] + public struct PickedGeoData + { + /// 材质/SubMesh 的唯一 ID,0 表示背景 + public int matID; + + /// 该像素点对应的世界坐标 + public Vector3 worldPos; + + /// 是否拾取到了有效的地质层 + public bool isValid; + } + + /// + /// 地质层拾取 RenderFeature。 + /// 负责在 URP 管线中注入自定义 RenderPass,管理模型,并自动在后台降频读取、解码 GPU 数据。 + /// public class GeoLayerPickerRenderPassFeature : ScriptableRendererFeature { - - // 在 Feature 外部类中,添加 Inspector 面板需要的配置参数 - [Header("拾取设置")] + #region 静态单例与外部管理 API + + /// + /// 当前激活的 Feature 实例 + /// + public static GeoLayerPickerRenderPassFeature Instance { get; private set; } + + private readonly List m_GeoMeshes = new List(); + private readonly List m_DrawList = new List(); + private bool m_IsDirty = true; + + /// 添加需要检查的场景模型对象 + public void AddGeoMesh(MeshFilter meshFilter) + { + if (meshFilter != null && !m_GeoMeshes.Contains(meshFilter)) + { + m_GeoMeshes.Add(meshFilter); + m_IsDirty = true; + } + } + + /// 批量添加需要检查的场景模型对象 + public void AddGeoMeshes(IEnumerable meshFilters) + { + if (meshFilters == null) return; + foreach (var mf in meshFilters) + { + if (mf != null && !m_GeoMeshes.Contains(mf)) + { + m_GeoMeshes.Add(mf); + m_IsDirty = true; + } + } + } + + /// 移除指定的场景模型对象 + public void RemoveGeoMesh(MeshFilter meshFilter) + { + if (m_GeoMeshes.Remove(meshFilter)) m_IsDirty = true; + } + + /// 清空所有已添加的场景模型对象 + public void ClearGeoMeshes() + { + if (m_GeoMeshes.Count > 0) + { + m_GeoMeshes.Clear(); + m_IsDirty = true; + } + } + + /// 获取当前所有已添加的场景模型对象(只读) + public IReadOnlyList GetGeoMeshes() => m_GeoMeshes; + + #endregion + + #region 数据访问 API (供外部 UI 或逻辑调用) + + // 内部维护的解码后的数据数组 (大小等于 pickRTSize.x * pickRTSize.y) + private PickedGeoData[] m_DecodedDataArray; + + /// + /// 获取当前帧已解码的拾取数据数组。 + /// 数组按行优先排列 (Index = y * width + x)。 + /// + public IReadOnlyList GetDecodedData() => m_DecodedDataArray; + + /// + /// 获取一列的数据 + /// + /// + /// + public void GetColumnData(int column, List listData) + { + listData.Clear(); + column = MacroMath.ClampToInt(column, 0, pickRTSize.x); + for (int i = 0; i < pickRTSize.y; i++) + { + listData.Add(m_DecodedDataArray[pickRTSize.x * i + column]); + } + } + + /// + /// 根据归一化的屏幕坐标 (0~1) 获取拾取数据。 + /// 会自动映射到 sampleRect 和 pickRTSize 范围内。 + /// + /// 归一化屏幕坐标 (左下角为 0,0) + /// 输出的拾取数据 + /// 是否成功获取(坐标是否在采样区域内) + public bool TryGetPickedData(Vector2 normalizedScreenPos, out PickedGeoData data) + { + data = default; + if (m_DecodedDataArray == null || m_DecodedDataArray.Length == 0) return false; + + // 1. 将屏幕坐标转换为相对于 sampleRect 的局部坐标 (0~1) + float localX = (normalizedScreenPos.x - sampleRect.x) / sampleRect.width; + float localY = (normalizedScreenPos.y - sampleRect.y) / sampleRect.height; + + // 2. 检查是否超出了采样区域 + if (localX < 0f || localX > 1f || localY < 0f || localY > 1f) return false; + + // 3. 映射到 RT 的像素坐标 + int pixelX = Mathf.Clamp(Mathf.FloorToInt(localX * pickRTSize.x), 0, pickRTSize.x - 1); + int pixelY = Mathf.Clamp(Mathf.FloorToInt(localY * pickRTSize.y), 0, pickRTSize.y - 1); + + // 4. 获取数据 + int index = pixelY * pickRTSize.x + pixelX; + data = m_DecodedDataArray[index]; + return true; + } + + #endregion + + #region Inspector 配置参数 + + [Header("拾取材质与尺寸")] [Tooltip("用于渲染地质层 ID 和坐标的 HDR 材质球")] public Material pickMaterial; - public MeshFilter[] geoMeshes; + + [Tooltip("渲染目标纹理的尺寸,越小性能越好(例如 64x64)")] public Vector2Int pickRTSize = new Vector2Int(64, 64); - // 内部持有的资源 - private RenderTexture m_PickRT; - private List m_DrawList = new List(); + [Header("采样区域设置")] [Tooltip("定义屏幕上的采样区域 (归一化坐标 0~1)。\n例如: X=0.25, Y=0.25, W=0.5, H=0.5 表示只渲染屏幕正中间一半的区域。")] + public Rect sampleRect = new Rect(0f, 0f, 1f, 1f); - class GeoLayerPickerRenderPass : ScriptableRenderPass + [Header("读取频率控制")] [Tooltip("每隔多少秒从 GPU 读取一次数据 (例如 0.1 秒 = 10 FPS)")] [Range(0.01f, 1.0f)] + public float readbackInterval = 0.1f; + + #endregion + + private RenderTexture m_PickRT; + private float m_TimeSinceLastReadback = 0f; + private bool m_IsReadbackInProgress = false; + + /// + /// 自定义 RenderPass + /// + class CustomRenderPass : ScriptableRenderPass { public Material passMaterial; public RenderTexture passRT; public List passDrawList; - + public Rect passSampleRect; + public bool requestReadback; // 标记本帧是否需要触发异步读取 + private int m_MatIDProperty = Shader.PropertyToID("_MatID"); - + public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData) { - // 在 URP 中,必须在这里告诉管线我们要渲染到哪张 RT 上 if (passRT != null) { ConfigureTarget(new RenderTargetIdentifier(passRT)); + // 清屏为 0,0,0,0 (背景 ID 为 0) ConfigureClear(ClearFlag.All, Color.clear); } } public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { - // 安全检查 - if (passMaterial == null || passRT == null || passDrawList == null || passDrawList.Count == 0) + if (passMaterial == null || passRT == null || passDrawList == null || passDrawList.Count == 0) return; - // 从对象池获取 CommandBuffer (性能更好) CommandBuffer cmd = CommandBufferPool.Get("GeoLayerPickBuffer"); - // 在 URP 13 中,绝对不能在 Execute 里用 cmd.SetRenderTarget()。必须重写 OnCameraSetup,使用 ConfigureTarget() 告诉 URP 这个 Pass 的输出目标是你创建的 m_PickRT。 - // 设置视口为 RT 的尺寸 (64x64) + Camera cam = renderingData.cameraData.camera; + Matrix4x4 viewMatrix = cam.worldToCameraMatrix; + Matrix4x4 projMatrix = cam.projectionMatrix; + + float ndcXMin = passSampleRect.x * 2f - 1f; + float ndcXMax = (passSampleRect.x + passSampleRect.width) * 2f - 1f; + float ndcYMin = passSampleRect.y * 2f - 1f; + float ndcYMax = (passSampleRect.y + passSampleRect.height) * 2f - 1f; + + float scaleX = 2f / (ndcXMax - ndcXMin); + float scaleY = 2f / (ndcYMax - ndcYMin); + float offsetX = -1f - ndcXMin * scaleX; + float offsetY = -1f - ndcYMin * scaleY; + + projMatrix.m00 *= scaleX; + projMatrix.m11 *= scaleY; + projMatrix.m02 += offsetX; + projMatrix.m12 += offsetY; + + projMatrix = GL.GetGPUProjectionMatrix(projMatrix, true); + cmd.SetViewProjectionMatrices(viewMatrix, projMatrix); cmd.SetViewport(new Rect(0, 0, passRT.width, passRT.height)); - // 【核心】遍历绘制每一个 SubMesh for (int i = 0; i < passDrawList.Count; i++) { var data = passDrawList[i]; cmd.SetGlobalInt(m_MatIDProperty, data.uniqueMatID); - // 注意第四个参数 submeshIndex,这是多材质插槽的关键 cmd.DrawMesh(data.mesh, data.matrix, passMaterial, data.subMeshIndex); } - // 提交命令给 GPU 执行 + cmd.SetViewProjectionMatrices(viewMatrix, renderingData.cameraData.GetProjectionMatrix()); context.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd); + + // 【核心】:如果计时器到了,在这里发起异步 GPU 回读 + if (requestReadback) + { + AsyncGPUReadback.Request(passRT, 0, TextureFormat.RGBAFloat, + GeoLayerPickerRenderPassFeature.Instance.OnReadbackComplete); + } } public override void OnCameraCleanup(CommandBuffer cmd) @@ -79,36 +252,139 @@ namespace XericLibrary.PostProcess } } - GeoLayerPickerRenderPass m_ScriptablePass; + private CustomRenderPass m_ScriptablePass; public override void Create() { - m_ScriptablePass = new GeoLayerPickerRenderPass(); + Instance = this; + m_IsDirty = true; - m_ScriptablePass.renderPassEvent = RenderPassEvent.AfterRenderingOpaques; + m_ScriptablePass = new CustomRenderPass + { + renderPassEvent = RenderPassEvent.AfterRenderingOpaques + }; - // 【步骤 4】在 Create 中初始化 RT,并展平 SubMesh 数据 if (m_PickRT != null) m_PickRT.Release(); - m_PickRT = new RenderTexture(pickRTSize.x, pickRTSize.y, 0, RenderTextureFormat.ARGB32); + m_PickRT = new RenderTexture(pickRTSize.x, pickRTSize.y, 0, RenderTextureFormat.ARGBFloat) + { + filterMode = FilterMode.Point + }; m_PickRT.Create(); - RebuildDrawList(); + // 初始化解码数组 + m_DecodedDataArray = new PickedGeoData[pickRTSize.x * pickRTSize.y]; } + public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) + { + if (m_ScriptablePass == null || pickMaterial == null) return; + + if (m_IsDirty) + { + RebuildDrawList(); + m_IsDirty = false; + } + + // 降频计时器逻辑 + m_TimeSinceLastReadback += Time.deltaTime; + + if (m_IsReadbackInProgress && m_TimeSinceLastReadback > 1.0f) + { + Debug.LogWarning("[GeoLayerPicker] AsyncGPUReadback 超时,强制重置状态。"); + m_IsReadbackInProgress = false; + } + + bool shouldRead = false; + if (m_TimeSinceLastReadback >= readbackInterval && !m_IsReadbackInProgress) + { + m_TimeSinceLastReadback = 0f; + m_IsReadbackInProgress = true; + shouldRead = true; + } + + m_ScriptablePass.passMaterial = pickMaterial; + m_ScriptablePass.passRT = m_PickRT; + m_ScriptablePass.passDrawList = m_DrawList; + m_ScriptablePass.passSampleRect = sampleRect; + m_ScriptablePass.requestReadback = shouldRead; + + renderer.EnqueuePass(m_ScriptablePass); + } + + /// + /// 异步回读完成回调 (在后台线程或主线程延迟执行,不阻塞渲染) + /// + private void OnReadbackComplete(AsyncGPUReadbackRequest request) + { + m_IsReadbackInProgress = false; + + if (request.hasError || m_DecodedDataArray == null) + { + Debug.LogError("[GeoLayerPicker] Readback 发生错误或数组为空!"); + return; + } + + // 【核心修复】:使用 GetData 获取数据 + var rawData = request.GetData(); + if (rawData.Length == 0) return; + + int width = m_PickRT.width; + int height = m_PickRT.height; + + // 【核心修复】:不要依赖 rawData.Length 作为遍历上限, + // 而是以我们目标数组 m_DecodedDataArray 的长度为准! + int count = m_DecodedDataArray.Length; + + // DirectX 平台下,异步回读的 Y 轴是翻转的 + bool flipY = SystemInfo.graphicsUVStartsAtTop; + + for (int i = 0; i < count; i++) + { + // 计算目标像素的坐标 + int x = i % width; + int y = i / width; + + // 处理 DirectX 的 Y 轴翻转 + int sourceY = flipY ? (height - 1 - y) : y; - // 提供一个公开方法,方便运行时动态刷新网格列表 - public void RebuildDrawList() + // 计算源数据在 rawData 中的真实索引 + int sourceIndex = sourceY * width + x; + + // 【安全防线】:防止源索引超出 rawData 的实际物理长度 + if (sourceIndex < 0 || sourceIndex >= rawData.Length) + { + m_DecodedDataArray[i] = new PickedGeoData { isValid = false }; + continue; + } + + Color pixel = rawData[sourceIndex]; + + // 你的 Shader 输出是 float4(worldPos.xyz, matID) + // 在标准 RGBA 映射下,A 通道就是 ID + if (pixel.a > 0.1f) + { + m_DecodedDataArray[i] = new PickedGeoData + { + isValid = true, + matID = Mathf.RoundToInt(pixel.a), + worldPos = new Vector3(pixel.r, pixel.g, pixel.b) + }; + } + else + { + m_DecodedDataArray[i] = new PickedGeoData { isValid = false, matID = 0 }; + } + } + } + + private void RebuildDrawList() { m_DrawList.Clear(); - if (geoMeshes == null) - return; - - int globalIDCounter = 1; // ID 从 1 开始,0 留给背景 - foreach (var mf in geoMeshes) + int globalIDCounter = 1; + + foreach (var mf in m_GeoMeshes) { if (mf == null || mf.sharedMesh == null) continue; - - // 遍历网格的每一个材质插槽 (SubMesh) for (int sub = 0; sub < mf.sharedMesh.subMeshCount; sub++) { m_DrawList.Add(new SubMeshDrawData @@ -122,22 +398,11 @@ namespace XericLibrary.PostProcess } } - public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) + protected override void Dispose(bool disposing) { - // 将外部数据传递给内部的 Pass,并注入管线 - if (m_ScriptablePass != null) - { - m_ScriptablePass.passMaterial = pickMaterial; - m_ScriptablePass.passRT = m_PickRT; - m_ScriptablePass.passDrawList = m_DrawList; - - renderer.EnqueuePass(m_ScriptablePass); - } + if (m_PickRT != null && m_PickRT.IsCreated()) m_PickRT.Release(); + if (Instance == this) Instance = null; } - - - // 暴露 RT 给外部读取像素用 - public RenderTexture GetPickRT() => m_PickRT; } } -// #endif \ No newline at end of file +#endif \ No newline at end of file