根据光线投射在世界中的命中位置绘制到对象的纹理上 space

Draw onto an object's texture based on a raycast hit position in world space

我正在尝试获取碎片着色器中像素的世界位置。

让我解释一下。我遵循了 tutorial 的零散着色器,让我在对象上绘画。现在它通过纹理坐标工作,但我希望它通过像素的世界位置工作。因此,当我单击 3D 模型以便能够将 Vector3 位置(单击发生的位置)与像素 Vector3 位置进行比较时,以及距离是否足够小以对颜色进行 lerp。

这是我的设置。我创建了一个新的 3D 项目,只是为了制作着色器,目的是稍后将其导出到我的主项目中。在场景中,我有默认的主摄像头、定向光、一个带有向我显示 fps 的脚本的对象和一个带有网格碰撞器的默认 3D 立方体。我创建了一个新材质和一个新的标准表面着色器并将其添加到立方体中。之后,我将下一个 C# 脚本分配给带有着色器和相机参考的立方体。

更新: 现在的问题是 blit 没有按预期工作。如果您像 Kalle 所说的那样更改着色器脚本,从 c# 脚本中删除 blit,并将 3D 模型材质的着色器更改为 Draw 着色器,它将按预期工作,但没有任何照明。为了我的目的,我不得不改变 distance(_Mouse.xyz, i.worldPos.xyz);distance(_Mouse.xz, i.worldPos.xz); 所以它将一直画到另一边。为了进行调试,我创建了一个 RenderTexture 并且我使用 Blit 来更新纹理并查看发生了什么的每一帧。渲染纹理在对象着色时没有保持正确的位置。我的 3D 模型有很多几何形状,当油漆到达另一侧时,它应该遍布渲染纹理的所有位置……但现在它只是从纹理的顶部到底部在一条线上。此外,我尝试在对象的下半部分进行绘制,但渲染纹理没有显示任何内容。只有当我在上半部分绘画时,我才能看到红线(默认绘画颜色)。

如果需要,可以下载示例项目here

这是我正在使用的代码。

Draw.shader

Shader "Unlit/Draw"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Coordinate("Coordinate",Vector)=(0,0,0,0)
        _Color("Paint Color",Color)=(1,1,1,1)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed4 _Coordinate,_Color;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                float draw =pow(saturate(1-distance(i.uv,_Coordinate.xy)),100);
                fixed4 drawcol = _Color * (draw * 1);
                return saturate(col + drawcol);
            }
            ENDCG
        }
    }
}

Draw.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Draw : MonoBehaviour
{
    public Camera cam;
    public Shader paintShader;

    RenderTexture splatMap;
    Material snowMaterial,drawMaterial;

    RaycastHit hit;

    private void Awake()
    {
        Application.targetFrameRate = 200;
    }
    void Start()
    {
        drawMaterial = new Material(paintShader);
        drawMaterial.SetVector("_Color", Color.red);

        snowMaterial = GetComponent<MeshRenderer>().material;
        splatMap = new RenderTexture(1024, 1024, 0, RenderTextureFormat.ARGBFloat);
        snowMaterial.mainTexture = splatMap;
    }
    void Update()
    {
        if (Input.GetMouseButton(0))
        {
            if(Physics.Raycast(cam.ScreenPointToRay(Input.mousePosition),out hit))
            {
                drawMaterial.SetVector("_Coordinate", new Vector4(hit.textureCoord.x, hit.textureCoord.y, 0, 0));
                RenderTexture temp = RenderTexture.GetTemporary(splatMap.width, splatMap.height, 0, RenderTextureFormat.ARGBFloat);
                Graphics.Blit(splatMap, temp);
                Graphics.Blit(temp, splatMap, drawMaterial);
                RenderTexture.ReleaseTemporary(temp);
            }
        }
    }
}

至于我尝试解决的问题是这样的。我在 google 上搜索了有关此线程的问题,并尝试在我的项目中实现它。我还发现了一些具有我需要的功能的项目,例如这个 Mesh Texuture Painting. This one works exactly how I need it, but it doesn't work on iOS. The 3D object is turns to black. You can check out a previous postI made to solve the problem and also talked with the creator on twitter but he can't help me. Also I have tried this asset 可以正常工作,但在我的主项目中运行时 fps 非常低,我很难根据需要自定义它并且它不绘画在我的 3D 模型的边缘。

运行良好的着色器非常简单,因此我可以更改它并获得所需的效果,就是上面那个。

谢谢!

有两种方法可以解决这个问题 - 要么传入纹理坐标并尝试将其转换为着色器内的世界 space,要么传入世界位置并将其与片段世界进行比较位置。后者无疑是最简单的。

所以,假设您像这样将世界位置传递到着色器中:

drawMaterial.SetVector("_Coordinate", new Vector4(hit.point.x, hit.point.y, hit.point.z, 0));

计算每个片段的世界位置是昂贵的,所以我们在顶点着色器内部进行计算,让硬件插值每个片段的值。让我们在 v2f 结构中添加一个世界位置:

struct v2f
{
    float2 uv : TEXCOORD0;
    float4 vertex : SV_POSITION;
    float3 worldPos : TEXCOORD1;
};

要计算顶点着色器内的世界位置,我们可以使用内置矩阵unity_ObjectToWorld:

v2f vert (appdata v)
{
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);
    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
    o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
    return o;
 }

最后,我们可以像这样访问片段着色器中的值:

float draw =pow(saturate(1-distance(i.worldPos,_Coordinate.xyz)),100);

编辑:我刚刚意识到 - 当你进行 blit pass 时,你不是在渲染你的网格,你是在渲染一个覆盖整个屏幕的四边形。正因为如此,当你计算到顶点的距离时,你得到的是到屏幕角的距离,这是不对的。不过有一种方法可以解决此问题 - 您可以将渲染目标更改为渲染纹理,并使用在屏幕上投射网格 UV 的着色器绘制网格。

这有点难以解释,但基本上,顶点着色器的工作方式是获取本地对象 space 中的顶点并将其转换为相对于 space -1 到 1 在两个轴上,其中 0 在中心。这称为归一化设备坐标 Space 或 NDC space。我们可以利用它来实现它,而不是使用模型和相机矩阵来变换我们的顶点,我们使用 UV 坐标,从 [0,1] space 转换为 [-1,1]。同时,我们可以计算出我们的世界位置,单独传递给片段。这是着色器的外观:

v2f vert (appdata v)
{
    v2f o;

    float2 uv = v.texcoord.xy;

    // https://docs.unity3d.com/Manual/SL-PlatformDifferences.html

    if (_ProjectionParams.x < 0) {
        uv.y = 1 - uv.y;
    }

    // Convert from 0,1 to -1,1, for the blit
    o.vertex = float4(2 * (uv - 0.5), 0, 1);

    // We still need UVs to draw the base texture
    o.uv = TRANSFORM_TEX(v.uv, _MainTex);

    // Let's do the calculations in local space instead!
    o.localPos = v.vertex.xyz;

    return o;
 }

另外记得在本地space传入_Coordinate变量,使用transform.InverseTransformPoint。

现在,我们需要使用不同的方法将其渲染到渲染纹理中。基本上,我们需要渲染实际的网格,就像我们从相机渲染一样——除了这个网格将被绘制为在屏幕上展开的 UV sheet。首先,我们将活动渲染纹理设置为我们要绘制到的纹理:

// Cache the old target so that we can reset it later
RenderTexture previousRT = RenderTexture.active;
RenderTexture.active = temp;

(您可以阅读渲染目标的工作原理 here) 接下来,我们需要绑定我们的 material 并绘制网格。

Material mat = drawMaterial;
Mesh mesh = yourAwesomeMesh;
mat.SetTexture("_MainTex", splatMap);
mat.SetPass(0); // This tells the renderer to use pass 0 from this material
Graphics.DrawMeshNow(mesh, Vector3.zero, Quaternion.identity);

最后把贴图blit回原来的样子:

// Remember to reset the render target
RenderTexture.active = previousRT;
Graphics.Blit(temp, splatMap);

我没有对此进行测试或验证,但我之前使用过类似的技术将网格绘制到 UV 中。您可以阅读有关 DrawMeshNow here 的更多信息。