Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Question Draw line on mesh texture

Discussion in 'General Graphics' started by mbrownie825, Aug 25, 2023.

  1. mbrownie825

    mbrownie825

    Joined:
    Aug 25, 2023
    Posts:
    8
    Hi! I'm working on a small painting app that draws lines on mesh textures.
    The user presses on the mesh in frame N-1 we do a raycast and store local position, world position, and uv. The same for the current frame N. When we have two raycasts, we're interpolating between raycast[N-1].uv and raycast[N].uv and draw a few brush samples to draw a line.
    In some cases, it works, but there are cases when it gives unexpected results:

    Mesh with texture / UV, user drawn a dot:
    n.png
    Mesh with texture / UV, user drawn the second dot, interpolating them:
    n-1.png
    Mesh with texture / UV, this is what I expected:
    expected.png

    Is there any way to get the expected result with good performance? I think that instead of interpolating between N-1.uv and N.uv we can raycast from N-1 to N and draw brush samples, but I assume that performance will be low.
     
  2. c0d3_m0nk3y

    c0d3_m0nk3y

    Joined:
    Oct 21, 2021
    Posts:
    547
    (u, v) + (n, m) all point all to the same texel when the address mode is set to wrap, where n and m are positive or negative integers.

    I think, it is safe to assume that you always want to draw the shortest possible line, so I would just calculate the length of all the possible lines in u/v space and draw the shortest one.

    Pseudo code:
    Code (CSharp):
    1. float minSquaredLength = FLT_MAX;
    2. float2 start = float2(u1, v1);
    3. float2 end;
    4.  
    5. for (int du2 = -1; du2 <= 1; ++du2)
    6. {
    7.     for (int dv2 = -1; dv2 <= 1; ++dv2)
    8.     {
    9.         float2 tmpEnd = float2(u2 + du2, v2 + dv2);
    10.         float2 line = tmpEnd - start;
    11.         float squaredLength = dot(line, line);
    12.         if (squaredLength < minSquaredLength)
    13.         {
    14.             minSquaredLength = squaredLength;
    15.             end = tmpEnd;
    16.         }
    17.     }
    18. }
    19.  
    20. // Draw line from start to end but be aware that every texel coordinate must be mapped to 0-1 range
    But no guarantee that this actually works...
     
  3. mbrownie825

    mbrownie825

    Joined:
    Aug 25, 2023
    Posts:
    8
    Thank you! I think about this way in the case when we painting on the plane mesh that contains 4 vertices and in should render the line like in Spoiler: Frame N variant, but it will do it as in Spoiler: Expected result, so corner cases still remaining :(
     
  4. c0d3_m0nk3y

    c0d3_m0nk3y

    Joined:
    Oct 21, 2021
    Posts:
    547
    Is it possible to draw such a long line within a single frame? I kind of assumed that the user is holding down the mouse button to paint from frame N to N + x (with x being framerate times seconds the button was held down) and you are just drawing the small line segments between each frame. If somebody wanted to draw a long line like in Spoiler: Frame N, it would consist of many single line segments since it is impossible to move the mouse all the way around the cylinder (or all the way from one corner to the other of a plane) within a single frame. Is this not how it works?
     
  5. mbrownie825

    mbrownie825

    Joined:
    Aug 25, 2023
    Posts:
    8
    Yes, if you move the mouse or touchscreen pointer really fast, you can get a long single line between two frames. My mouse positions list can contains maximum two items - one for the previous frame and one for the current frame.
     
  6. c0d3_m0nk3y

    c0d3_m0nk3y

    Joined:
    Oct 21, 2021
    Posts:
    547
    I'm having a hard time believing that this is an actual issue since you'd have to move the mouse really, really fast or the frame rate would have to be really low.

    But if you really want to cover this case, you could interpolate between the two mouse positions on the screen, send a ray for each interpolated position, intersect them with the geometry to get a list of u/v coordinates and then use the algorithm above to render a bunch of line segments each frame.
     
  7. UhOhItsMoving

    UhOhItsMoving

    Joined:
    May 25, 2022
    Posts:
    68
    The line will be curved in UV space because the surface of the mesh isn't flat. However, the surface of the mesh isn't truly smooth because it's constructed with a bunch of flat triangles. If you project the line onto just a single triangle, the line in UV space will be straight. So, all you need to do is get the intersections of the line with the triangle edges in screen space and get the UV coordinates of those intersections. Those UV coordinates will form a curved line in UV space.
    upload_2023-8-26_11-38-37.png
     
    mbrownie825 and c0d3_m0nk3y like this.
  8. c0d3_m0nk3y

    c0d3_m0nk3y

    Joined:
    Oct 21, 2021
    Posts:
    547
    Good point, haven't thought of that.

    If the look of a hand-drawn line is fine, the easiest solution is probably "splatoon painting".


    Otherwise, you could also go the inverse direction:
    - Render a quad onto the texture to run a fragment shader for each texel.
    - Calculate the screen-space coordinate of the texel
    - Calculate the distance to the screen-space line
    - If the distance is close enough, color it
     
    mbrownie825 and UhOhItsMoving like this.
  9. mbrownie825

    mbrownie825

    Joined:
    Aug 25, 2023
    Posts:
    8
    It works! But I faced the issue: when I draw the line in the screen-space coords, it renders on the backside vertices too:
    model1.png
    Any way to fix that?
     
  10. c0d3_m0nk3y

    c0d3_m0nk3y

    Joined:
    Oct 21, 2021
    Posts:
    547
    Hm, I see. I guess, you have to check the depth map too (_CameraDepthTexture) and clip the texel if necessary. You already have the screen coordinate for sampling the texture. Additionally, you also need the NDC space depth for the given texel to compare it with the camera depth. You should have that if you followed the Splatoon approach.

    Bit hard to explain. I'll write some pseudo code if I have some time.
     
    mbrownie825 likes this.
  11. mbrownie825

    mbrownie825

    Joined:
    Aug 25, 2023
    Posts:
    8
    Thank you!

    I tried to compare the world positions, but I missing something: if I render i.worldPos in fragment and compare it with depth worldPos, the result are different:

    pos.png

    Shader:
    Code (CSharp):
    1. //vertex
    2. o.worldPos = mul(unity_ObjectToWorld, v.vertex);
    3. o.viewPos = mul(UNITY_MATRIX_VP, mul(unity_ObjectToWorld, float4(v.vertex.xyz, 1.0)));
    4. float4 screenPos = o.viewPos * 0.5f;
    5. screenPos.xy = float2(screenPos.x, screenPos.y * _ProjectionParams.x) + screenPos.w;
    6. screenPos.zw = o.viewPos.zw;
    7. o.screenPos = screenPos;
    Code (CSharp):
    1. //fragment
    2. float2 screenUV = i.screenPos / i.screenPos.w;
    3. float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, screenUV);
    4.  
    5. #if UNITY_REVERSED_Z
    6.     if (depth < 0.0001)
    7.         return 0;
    8. #else
    9.     if (depth > 0.9999)
    10.         return 0;
    11. #endif
    12.  
    13. float4 positionCS = float4(screenUV.xy * 2.0 - 1.0, depth, 1.0);
    14.  
    15. #if UNITY_UV_STARTS_AT_TOP
    16.     positionCS.y = -positionCS.y;
    17. #endif
    18.  
    19. float4 hpositionWS = mul(_Matrix_IVP, positionCS);
    20. float3 worldPos = hpositionWS.xyz / hpositionWS.w;
    21.  
    22. if (distance(worldPos.xyz, i.worldPos.xyz) < 0.03f)
    23. {
    24.     //Draw Line
    25. }
     
  12. mbrownie825

    mbrownie825

    Joined:
    Aug 25, 2023
    Posts:
    8
    Tried to
    Found the issue: my custom _ProjectionParams.x had incorrect value: changed it to 1 from -1 and it works. Also I commented the block with UNITY_UV_STARTS_AT_TOP.
    Now it works by comparing the position in world space. I wondering, if there's more performant way to do that?
     
  13. c0d3_m0nk3y

    c0d3_m0nk3y

    Joined:
    Oct 21, 2021
    Posts:
    547
    Your solution is not quite correct. You have to compare the depth from the depth map with the depth of the currently rendered polygon.

    Shader:
    Code (CSharp):
    1.  
    2. Shader "Unlit/Splatoon"
    3. {
    4.     Properties
    5.     {
    6.     }
    7.     SubShader
    8.     {
    9.         Tags { "RenderType"="Opaque" }
    10.         LOD 100
    11.  
    12.         Pass
    13.         {
    14.             Cull Off
    15.             ZTest Always
    16.             ZWrite Off
    17.  
    18.             CGPROGRAM
    19.             #pragma vertex vert
    20.             #pragma fragment frag
    21.             //#pragma enable_d3d11_debug_symbols
    22.  
    23.             #include "UnityCG.cginc"
    24.  
    25.             struct appdata
    26.             {
    27.                 float4 vertex : POSITION;
    28.                 float2 uv : TEXCOORD1; // using the lightmap uv's since those are non-overlapping
    29.             };
    30.  
    31.             struct v2f
    32.             {
    33.                 float4 viewPos : POSITION1;
    34.                 float4 clipPos : POSITION2;
    35.                 float2 uv : TEXCOORD0;
    36.                 float4 vertex : SV_POSITION;
    37.             };
    38.  
    39.             float4x4 _MV;
    40.             float4x4 _MVP;
    41.             texture2D _CameraDepthTexture;
    42.             float2 _LineStart;
    43.             float2 _LineEnd;
    44.             float _LineThickness;
    45.  
    46.             // Finds the t on the segment [a, b] which is closest to an arbitrary point c.
    47.             float GetClosestPointOnSegment(float2 a, float2 b, float2 c)
    48.             {
    49.                 // Explanation: Realtime Collision Detection chapter 5.1.2 page 127
    50.                 float2 ab = b - a;
    51.                 float2 ac = c - a;
    52.  
    53.                 // t = ac.dot(ab) / ab.dot(ab)
    54.                 // If c projects outside the [a, b] interval on the a side; clamp to a
    55.                 float numer = dot(ac, ab);
    56.                 if (numer <= 0)
    57.                     return 0.0;
    58.  
    59.                 // If c projects outside the [a, b] interval on the b side; clamp to b
    60.                 float denom = dot(ab, ab);    // always nonnegative
    61.                 if (numer >= denom)
    62.                     return 1.0;
    63.  
    64.                 // c projects inside the [a, b] interval
    65.                 return numer / denom;
    66.             }
    67.  
    68.             float GetDistanceToSegment(float2 a, float2 b, float2 c)
    69.             {
    70.                 float t = GetClosestPointOnSegment(a, b, c);
    71.                 float2 p = lerp(a, b, t);
    72.  
    73.                 return distance(p, c);
    74.             }
    75.  
    76.             v2f vert (appdata v)
    77.             {
    78.                 v2f o;
    79.                 o.viewPos = mul(_MV, v.vertex);
    80.                 o.clipPos = mul(_MVP, v.vertex);
    81.                 o.uv = v.uv;
    82.                 float4 uv = float4(0, 0, 0, 1);
    83.                 uv.xy = (v.uv.xy * 2 - 1) * float2(1, _ProjectionParams.x);
    84.                 o.vertex = uv;
    85.                 return o;
    86.             }
    87.  
    88.             fixed4 frag(v2f i) : SV_Target
    89.             {
    90.                 // Check if current texel maps to a pixel on the screen that's behind the camera
    91.                 float viewDepth = -i.viewPos.z; // Camera looks down the negative z axis
    92.                 float nearPlaneDistance = _ProjectionParams.y;
    93.                 if (viewDepth < nearPlaneDistance)
    94.                     return float4(1, 1, 1, 1);
    95.  
    96.                 // Check if current texel maps to a pixel on the screen that failed the depth test
    97.                 float2 ndcPos = i.clipPos.xy / i.clipPos.w;
    98.                 float2 screenPos = (ndcPos + 1.0) * 0.5 * _ScreenParams.xy;  // upside down
    99.                 float depth = LinearEyeDepth(_CameraDepthTexture[int2(screenPos + 0.5f)].x);
    100.                 if (abs(depth - viewDepth) > 0.1)
    101.                     return float4(1, 1, 1, 1);
    102.  
    103.                 // Calculate distance to the line
    104.                 // Note: Origin of screen coordinates is at the bottom left
    105.                 screenPos = (ndcPos + 1.0) * 0.5 * _ScreenParams.xy;
    106.                 float distance = GetDistanceToSegment(_LineStart, _LineEnd, screenPos);
    107.  
    108.                 // Convert distance to line color
    109.                 float t = abs(distance) / _LineThickness;
    110.                 t = smoothstep(0, 1, t);
    111.                 t = smoothstep(0, 1, t);
    112.  
    113.                 return float4(t, t, t, 1);
    114.             }
    115.             ENDCG
    116.         }
    117.     }
    118. }
    119.  
    Script:
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.Rendering;
    3. using UnityEngine.UIElements;
    4.  
    5. public class RenderScript : MonoBehaviour
    6. {
    7.     public RenderTexture RenderTexture;
    8.     public float LineThickness = 10.0f;
    9.  
    10.     private Material _material;
    11.     private Camera _camera;
    12.     private CommandBuffer _buffer;
    13.     private Renderer _renderer;
    14.     private Vector2 _lineStart;
    15.     private Vector2 _lineEnd;
    16.  
    17.     private static int _mvpId = Shader.PropertyToID("_MVP");
    18.     private static int _mvId = Shader.PropertyToID("_MV");
    19.     private static int _lineStartId = Shader.PropertyToID("_LineStart");
    20.     private static int _lineEndId = Shader.PropertyToID("_LineEnd");
    21.     private static int _lineThicknessId = Shader.PropertyToID("_LineThickness");
    22.  
    23.     public void Awake()
    24.     {
    25.         Shader shader = Shader.Find("Unlit/Splatoon");
    26.  
    27.         if (shader == null)
    28.         {
    29.             enabled = false;
    30.             return;
    31.         }
    32.  
    33.         _buffer = new CommandBuffer();
    34.         _buffer.name = "Splatoon";
    35.  
    36.         _material = new Material(shader);
    37.     }
    38.  
    39.     public void Start()
    40.     {
    41.         _renderer = GetComponent<Renderer>();
    42.         _camera = Camera.main;
    43.  
    44.         if (_renderer == null || _camera == null || RenderTexture == null)
    45.         {
    46.             enabled = false;
    47.             return;
    48.         }
    49.  
    50.         _buffer.SetRenderTarget(RenderTexture);
    51.         _buffer.ClearRenderTarget(false, true, Color.white);
    52.         _buffer.DrawRenderer(_renderer, _material);
    53.  
    54.         _camera.AddCommandBuffer(CameraEvent.AfterDepthTexture, _buffer);
    55.  
    56.         _lineStart = new Vector2(0.0f, 0.0f);
    57.         _lineEnd = new Vector2(Screen.width, Screen.height);
    58.     }
    59.  
    60.     public void OnDestroy()
    61.     {
    62.         if (_camera != null)
    63.             _camera.RemoveCommandBuffer(CameraEvent.AfterDepthTexture, _buffer);
    64.  
    65.         if (_buffer != null)
    66.             _buffer.Release();
    67.  
    68.         if (_material != null)
    69.             Destroy(_material);
    70.     }
    71.  
    72.     void Update()
    73.     {
    74.         // origin is at the bottom left!
    75.         if (Input.GetMouseButtonDown(0))
    76.         {
    77.             _lineStart = new Vector2(Input.mousePosition.x, Input.mousePosition.y);
    78.         }
    79.         if (Input.GetMouseButtonDown(1))
    80.         {
    81.             _lineEnd = new Vector2(Input.mousePosition.x, Input.mousePosition.y);
    82.         }
    83.  
    84.         Matrix4x4 modelViewMatrix = _camera.worldToCameraMatrix * transform.localToWorldMatrix;
    85.         Matrix4x4 modelViewProjectionMatrix = GL.GetGPUProjectionMatrix(_camera.projectionMatrix, false) * modelViewMatrix;
    86.         _material.SetMatrix(_mvpId, modelViewProjectionMatrix);
    87.         _material.SetMatrix(_mvId, modelViewMatrix);
    88.         _material.SetVector(_lineStartId, _lineStart);
    89.         _material.SetVector(_lineEndId, _lineEnd);
    90.         _material.SetFloat(_lineThicknessId, LineThickness);
    91.     }
    92. }
    93.  
    Game view and scene view:
    upload_2023-9-3_13-56-57.png

    UV-Space
    upload_2023-9-3_13-57-14.png

    I've attached the project.
     

    Attached Files:

    mbrownie825 likes this.
  14. mbrownie825

    mbrownie825

    Joined:
    Aug 25, 2023
    Posts:
    8
    Wow! Thank you a lot, I deeply appreciate your help!
     
    c0d3_m0nk3y likes this.
  15. c0d3_m0nk3y

    c0d3_m0nk3y

    Joined:
    Oct 21, 2021
    Posts:
    547
    Actually, your approach should work too. Sorry, misread your code at first.
    Well now you've got two solutions ;)
     
    mbrownie825 likes this.