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