unity3d - 根据世界空间中的光线转换命中位置绘制到对象的纹理上

标签 unity3d 3d shader textures paint

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

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

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

更新:现在的问题是 blit 没有按预期工作。如果您像 Kalle 所说的那样更改着色器脚本,从 c# 脚本中删除 blit,并将 3D 模型 Material 的着色器更改为 Draw 着色器,它将按预期工作,但没有任何照明。出于我的目的,我必须将 distance(_Mouse.xyz, i.worldPos.xyz); 更改为 distance(_Mouse.< strong>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我解决了这个问题,也在推特上与创作者交谈,但他帮不了我。我也试过this asset这工作正常,但在我的主要项目中以非常低的 fps 运行,我很难根据我的需要对其进行自定义,并且它不会在我的 3D 模型的边缘上绘制。

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

谢谢!

最佳答案

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

那么,假设您像这样将世界位置传递给着色器:

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

还记得在局部空间中传递 _Coordinate 变量,使用 transform.InverseTransformPoint。

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

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

(您可以阅读渲染目标的工作原理 here) 接下来,我们需要绑定(bind) 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 .

关于unity3d - 根据世界空间中的光线转换命中位置绘制到对象的纹理上,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60552364/

相关文章:

flash - 在闪存中具有多个输出的像素弯曲器着色器?

c# - 如何检测一个游戏对象何时进入另一个游戏对象的区域?

c# - Unity3d c# - Vector3 作为默认参数

c# - 我如何重现这种 3D 效果

c++ - 在这种情况下我应该使用顶点着色器吗?

c++ - 纹理映射导致纹理中 1 个像素的纯色

c# - 如何将 MonoBehaviour 添加到 sprite (Player)?

c# - 如何在返回 0 之前停止显示 60 秒

algorithm - 通过360度旋转和图像处理得到点云

three.js - 尽管加载了 Material ,但模型显示为空白(黑色)纹理