根据射线投射在世界空间中的位置,在对象纹理上绘制。

3
我正在尝试在片段着色器中获取像素的世界位置。
让我解释一下。我遵循一个教程,使用分段着色器在对象上绘制图案。现在它通过纹理坐标工作,但我希望它能够通过像素的世界位置工作。因此,当我单击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模型,随着绘画进入另一侧,应该在渲染纹理的各个地方都有颜色...但现在只有一条线从顶部到底部的纹理。当我尝试在对象的下半部分绘画时,渲染纹理上什么也没有显示。只有当我在上半部分绘画时,才能看到红色的线条(默认的绘画颜色)。
如果您愿意,可以在此处下载示例项目。
以下是我正在使用的代码。
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);
            }
        }
    }
}

我尝试解决这个问题的方法是这样的。我在谷歌上搜索了这个线程所涉及的问题,并尝试在我的项目中进行实现。我还发现了一些具有我需要功能的项目,比如这一个网格纹理绘制。这个项目完全符合我的需求,但在iOS上不起作用。3D对象会变成黑色。您可以查看我之前发布的帖子来解决问题,并在Twitter上与创建者交流,但他无法帮助我。此外,我尝试了这个工具,它运行时fps非常低,很难为我的需求进行定制,而且不能在我的3D模型边缘上绘制。
那个工作良好的着色器很简单,所以我可以更改它并得到期望的效果。
谢谢!
1个回答

2
这个问题有两种解决方法——一种是你传入纹理坐标并且在着色器内尝试将其转换为世界空间,另一种则是你传入一个世界位置并将其与片段的世界位置进行比较。后者毫无疑问是最简单的。
假设你通过以下方式将世界位置传入到着色器中:
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传递时,您不是使用网格进行渲染,而是渲染到覆盖整个屏幕的四边形。因此,当您计算到顶点的距离时,您得到的是到屏幕角落的距离,这是不正确的。然而,有一种方法可以解决这个问题 - 您可以将渲染目标更改为您的渲染纹理,并使用投射网格UV在屏幕上绘制网格的着色器。

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

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;

您可以在此处阅读有关渲染目标如何工作的信息(链接)

接下来,我们需要绑定材质并绘制网格。

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);

最后,将纹理复制回原始位置:
// Remember to reset the render target
RenderTexture.active = previousRT;
Graphics.Blit(temp, splatMap);

我没有测试或验证过这个方法,但我之前使用过类似的技术将网格绘制到UV中。你可以在这里了解更多关于DrawMeshNow的信息。


谢谢您的回答 :) 但是它不起作用。我之前尝试过类似的方法几次,但3D对象会变成黑色。我已经上传了一个使用类似世界位置方法进行绘画的示例项目,并且具有相同的结果。您可以在此处检查 https://www.dropbox.com/s/sof3w6aeqtrckuw/Shader%20Painting.rar?dl=0 - Alien Boy Games
我非常好奇为什么这不起作用,因为有人建议同样的事情 https://gamedev.stackexchange.com/questions/179508/draw-onto-an-objects-texture-based-on-a-raycast-hit-position-in-world-space,并且在其他论坛上也发现了这个问题。 - Alien Boy Games
嗨Kalle,感谢你迄今为止的帮助。我发现你的着色器完美地工作了,但问题出在Graphics.Blit上。我已经更新了问题,这样你就可以看到更多细节。谢谢。 - Alien Boy Games
谢谢你的回答 :). 我在实现这个过程中遇到了一点困难。首先,你说要记得传递本地空间中的_Coordinate变量,使用transform.InverseTransformPoint。你是指hit.point还是_Matrix?此外,我不确定是否将所有内容都放在正确的方法中,例如Update或Start。你能否提供整个脚本呢?如果不能,我相信我可以解决问题。谢谢 :) - Alien Boy Games
你需要计算射线命中的位置相对于模型的位置,基本上使用 var pos = targetObject.transform.InverseTransformPoint(raycastHit.point) 计算该位置,并通过 blitMaterial.SetVector("_Coordinate", pos) 传递给材质即可。在哪个函数中使用并不重要,Update 函数应该可以胜任。你不需要传递任何矩阵。 - Kalle Halvarsson
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接