【问题标题】:Draw onto an object's texture based on a raycast hit position in world space根据世界空间中的光线投射命中位置绘制到对象的纹理上
【发布时间】:2020-06-18 11:57:44
【问题描述】:

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

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

这是我的设置。我创建了一个新的 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);
            }
        }
    }
}

至于我试图解决的问题是这样的。我在谷歌上搜索了这个线程的问题,并试图在我的项目中实现它。我还发现了一些具有我需要的功能的项目,例如Mesh Texuture Painting。这个完全符合我的需要,但它不适用于 iOS。 3D 对象变为黑色。你可以查看previous post我为解决问题而设计的,也可以在 Twitter 上与创建者交谈,但他无法帮助我。我也尝试过this asset,它工作正常,但在我的主项目中运行的 fps 非常低,我很难根据自己的需要自定义它,而且它不会在我的 3D 模型的边缘上绘制。

效果很好的着色器很简单,所以我可以更改它并获得所需的效果就是上面的那个。

谢谢!

【问题讨论】:

    标签: unity3d 3d shader textures paint


    【解决方案1】:

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

    因此,假设您将世界位置传递给着色器,如下所示:

    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 投射到屏幕上。

    这有点难以解释,但基本上,顶点着色器的工作方式是您获取位于本地对象空间中的顶点并将其转换为相对于空间中的屏幕 -1 到 1 在两个轴上,其中 0 位于中心。这称为标准化设备坐标空间或 NDC 空间。我们可以利用它来制作它,而不是使用模型和相机矩阵来转换我们的顶点,而是使用 UV 坐标,从 [0,1] 空间转换为 [-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;
     }
    

    还记得使用 transform.InverseTransformPoint 传入局部空间中的 _Coordinate 变量。

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

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

    (您可以阅读有关渲染目标如何工作的信息here) 接下来,我们需要绑定材质并绘制网格。

    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 的更多信息。

    【讨论】:

    • 感谢您的回答 :) 但它不起作用。我之前尝试过几次类似的操作,但 3D 对象变为黑色。我已经使用类似的方法为世界位置上传了一个带有绘画的示例项目,它具有相同的结果。你可以在这里查看dropbox.com/s/sof3w6aeqtrckuw/Shader%20Painting.rar?dl=0
    • 我还必须通过“unity_ObjectToWorld”,因为我使用的是 graphics.blit。我使用“ drawMaterial.SetMatrix("_Matrix", transform.localToWorldMatrix); ”并将其传递给 float4x4 _Matrix。
    • 您可以尝试通过输出一些值来调试着色器,看看它们是否正常。例如,您可以执行 return fixed4(i.worldPos, 1) 或 return (fixed4)draw。另外,请记住,对于世界空间坐标,距离会发生变化 - 值 1 表示 1 米,这可能比您所考虑的更大或更小。您也可以尝试在将命中点传递到着色器之前对其执行 transform.InverseTransformPoint 并在顶点着色器内的局部空间中进行距离计算。
    • 我尝试了使用 fixed4(i.worldPos, 1) 的调试方法,它给了我一个红色、绿色、黄色的淡入淡出,还有 (float4)_Coordinates 它根据老鼠的位置改变颜色,不同来自worldpos的颜色。我猜这意味着它们具有不同的值。我还尝试了您建议的所有其他内容,但我对着色器不太擅长。你能告诉我更多细节吗?
    • 我很好奇为什么这不起作用,因为有人提出了同样的建议 gamedev.stackexchange.com/questions/179508/… 并且还在其他论坛上发现了这个
    猜你喜欢
    • 1970-01-01
    • 2021-03-17
    • 1970-01-01
    • 2018-11-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多