Unity中3D模型绘画工具:如何避免输入位置随相机FOV变化导致画笔尺寸异常?
Unity中3D模型绘画工具:如何避免输入位置随相机FOV变化导致画笔尺寸异常?
看起来你遇到的核心问题是:画笔大小被绑定到了屏幕像素或相机FOV的差值上,导致相机缩放(改变FOV)时,模型表面的画笔实际覆盖大小跟着变化。我们真正需要的是让画笔在3D模型表面的物理大小保持恒定,不管相机怎么调整视角或缩放。
问题根源分析
你当前的代码尝试通过FOV差值来补偿偏移,但这种方式是间接且不准确的——FOV和模型表面的投影缩放关系,还会受到相机到模型的距离影响,简单的线性补偿很难覆盖所有场景。正确的思路应该是:基于模型表面的世界空间大小来计算画笔的屏幕/纹理范围。
解决方案步骤
下面是具体的实现思路和修改后的代码:
准确获取射线与模型的交点
首先必须确保每次绘画都能精准命中目标模型,拿到交点的世界坐标和UV坐标,这是后续计算的基础。固定画笔的世界空间大小
设定一个固定的世界单位半径(比如0.1f,你可以根据需求调整),这个值代表画笔在模型表面实际覆盖的物理大小,不管相机怎么缩放,这个大小都不变。将世界大小转换为屏幕像素大小
根据相机的FOV、到模型的距离,计算出世界单位对应的屏幕像素数,再以此得到画笔的屏幕像素半径。遍历画笔覆盖的像素并绘画
基于计算出的屏幕像素范围,逐个检测每个像素对应的模型UV坐标,然后修改纹理像素。
修改后的代码示例
public override void Execute() { Texture2D texture = RedactorStateManager.Instance.RedactState.ModelRenderer.material.mainTexture as Texture2D; Renderer modelRenderer = RedactorStateManager.Instance.RedactState.ModelRenderer; // 1. 发射射线,检测是否命中目标模型 Ray ray = Camera.main.ScreenPointToRay(InputPosition); if (!Physics.Raycast(ray, out RaycastHit hit) || hit.transform != modelRenderer.transform) { return; // 未命中目标,直接退出 } // 2. 设定画笔在模型表面的实际物理半径(可改为可调节参数) float brushWorldRadius = 0.1f; // 3. 计算世界单位对应的屏幕像素数量 float fovRadians = Camera.main.fieldOfView * Mathf.Deg2Rad; // 公式推导:基于相机透视投影的原理,计算每世界单位对应的屏幕像素 float screenPixelsPerWorldUnit = (Screen.height / 2f) / Mathf.Tan(fovRadians / 2f) / hit.distance; float brushScreenRadiusPixels = brushWorldRadius * screenPixelsPerWorldUnit; // 4. 将交点转换为屏幕坐标,方便计算周围像素范围 Vector2 hitScreenPos = Camera.main.WorldToScreenPoint(hit.point); // 5. 遍历画笔覆盖的屏幕像素(这里以圆形画笔为例) int minX = Mathf.Max(0, (int)(hitScreenPos.x - brushScreenRadiusPixels)); int maxX = Mathf.Min(Screen.width - 1, (int)(hitScreenPos.x + brushScreenRadiusPixels)); int minY = Mathf.Max(0, (int)(hitScreenPos.y - brushScreenRadiusPixels)); int maxY = Mathf.Min(Screen.height - 1, (int)(hitScreenPos.y + brushScreenRadiusPixels)); for (int x = minX; x <= maxX; x++) { for (int y = minY; y <= maxY; y++) { Vector2 pixelScreenPos = new Vector2(x, y); // 判断当前像素是否在圆形画笔范围内 float distanceToCenter = Vector2.Distance(pixelScreenPos, hitScreenPos); if (distanceToCenter > brushScreenRadiusPixels) continue; // 对当前像素发射射线,获取对应的模型UV坐标 Ray pixelRay = Camera.main.ScreenPointToRay(pixelScreenPos); if (Physics.Raycast(pixelRay, out RaycastHit pixelHit) && pixelHit.transform == modelRenderer.transform) { // 将UV坐标转换为纹理像素坐标 Vector2 uv = pixelHit.textureCoord; int texX = Mathf.Clamp((int)(uv.x * texture.width), 0, texture.width - 1); int texY = Mathf.Clamp((int)(uv.y * texture.height), 0, texture.height - 1); // 这里执行你的绘画逻辑,比如设置像素颜色(示例为红色) texture.SetPixel(texX, texY, Color.red); } } } // 应用纹理修改,使绘画生效 texture.Apply(); }
额外注意事项
- 碰撞体要求:确保目标模型有合适的碰撞体(比如Mesh Collider),否则射线检测无法命中。
- 性能优化:如果是高分辨率纹理或大画笔,逐像素射线检测可能会有性能问题,可以考虑先计算UV空间的范围,直接在纹理上操作,减少射线检测次数。
- 正交相机适配:如果使用正交相机,
screenPixelsPerWorldUnit的计算方式改为:Screen.height / (2 * Camera.main.orthographicSize),不需要除以相机到模型的距离。 - 画笔形状自定义:你可以替换圆形画笔的判断逻辑,实现方形、自定义纹理画笔等效果。
备注:内容来源于stack exchange,提问作者PAIN




