LineRenderer2D: GPU pixel-perfect 2D line renderer for Unity URP (2D Renderer)

Thanks a lot for the detecting and fixing the bug. Could you tell me how to reproduce it so I can add your fix to the repo?

Sure! I mean, does the MultiLineRenderer work as expected on your system when adding more than 3 points? It seems so obviously glitched to me that I honestly suspect it could be an issue with my setup, although I experience it on 2 different PCs (havenā€™t tried different Unity versions though).

Hereā€™s an easy way to reproduce it:

  • Download the latest version of your github repo files

  • Drag them into a fresh project

  • Open the test scene included in your files

  • Delete everything except the main camera and P_MultiLine_Shadergraph objects for clarityā€™s sake

  • Edit the Points list on the serialized MultiLineRenderer2D script to include 4 points (for example, (0,0), (1,0), (0,1) (1,1))

  • Hit play - points render fine (3 lines as expected, in a ā€œZā€-shape if you copy the points exactly)

  • Exit play mode, add a fifth point (for example, (0,2))

  • Hit play - suddenly only the first 3 points render - the third line that was previously rendered is missing and the fourth line does not get rendered either

  • Exit play mode, add a sixth point (for example, (1,2))

  • Hit play - the ā€˜Zā€™ shape appears again, but only 3 lines render in total, the last 2 lines are missing

  • Exit play mode, add a seventh point (for example, (2,2))

  • Hit play - 6 points are now rendering (5 lines), but the seventh point is missing

  • Etc.

Its easier to see whats going on if you modifiy LateUpdate to resend the gpu update every frame, but for the sake of using the asset as you probably intended, modifying it outside of play mode causes the same issues.

Let me know if it works on your system and Iā€™d be happy to send you a screencap.
Although debugging the shader code (mostly the IsPixelInLine function) showed me that the issue (at least on my hardware) lies with the way you sample the points from the 2d texture.

For example, when using 5 points in total (4 lines), which results in 3 ā€œpackedPointsā€, the first 2 lines get sampled just fine, and youā€™d expect the third line (t index 2) to sample the second packed point (or 1 coord) from the texture, as
(t / 2) / fPackedPointsCount in this case returns 1/3 or 0.3333, but that coordinate still returns the 0 indexed point - according to RenderDoc.

Edit: the points and lines always render fine in the scene view through the DebugGizmos.

Thank you! Iā€™ve just reproduced it. Itā€™s weird that I didnā€™t realize about this bug before, Iā€™ve used the multiline in my own game without problem (as you can see in the demo video) but I may have used the amount of points that work, by chance. Your code, using Load instead of Sample, is obviously more reliable and I should have chosen that function from the beginning anyway. Sorry for the inconveniences, Iā€™m adding the fix to the repo.

1 Like

Fix uploaded to the repo.

No worries! Noone else seems to have noticed it either :smile: - at least it led me into the rabbithole of being able to debug shadercode.
Thanks again for your project, its the only one I could find out there that actually does a great job of rendering 2D Pixel-perfect lines, I really appreciate it!

1 Like

I re-posted this thread in my website:

New changes uploaded:

  • Now lines use Material property blocks: Previously, I was cloning the assigned material in order to set its properties.

  • Now all shaders use alpha blending: I forgot to add this in previous versions, only ShaderGraph shaders were using it. This was causing that the background of the lines was opaque.

  • Now lines are rendered on the Scene view too: Lines were being rendered on the Game view only, while playing.

  • Changes in Inspector affect lines immediately: I have implemented a custom editor for both types of line. Now lines change in the Scene view as I modify the fields of the LineRenderer2D.

  • Added a vectorial line prefab: The test scene now also contains a vectorial line.

Known issues:

  • The thickness of the lines rendered on the Scene view is constant, it does not depend on the zoom of the camera.

Now lines use Material property blocks: Previously, I was cloning the assigned material in order to set its properties.

Does this mean you can use lines with materials or is it just single color lines?

You can (must) use materials but, for example, you donā€™t need a different material for lines that have different background color, textureā€¦ Itā€™s enough to set it in the properties of the line, using one common material for all lines. And that material will not be instantiated at runtime, saving memory.

EDIT: Well scratch that last phrase, materials are being instantiated automatically by Unity at runtime.

Sorry, I phrased that wrong, I meant can you use textures on the lines? Like how you would use the linerenderer thatā€™s build in.

No, the UV of the textures are calculated based on the fragment position in the quad of the sprite renderer, if you rotate the line the texture will not follow. This allows creating some FX like moving smoke in a laser.

Is there any way to fixed line thickness when changing camera orthographic size? When i zoom in, pixel line is too noisy and the thickness keep changing.

I think I have exactly what you need. Iā€™m not at home right now, I will post some code when I arrive.

I hope I understood your question properly. This is the code I use in my project to make the thickness of the line change according to the current orthographic size.

// Copyright 2023 Alejandro Villalba Avila
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.

using UnityEngine;

namespace Game.Core.Rendering
{
    /// <summary>
    /// Makes a 2D line look as thick on screen as it is configured disregarding the orthographic size of the camera.
    /// </summary>
    public class LineThicknessKeeper : MonoBehaviour
    {
        [Tooltip("Either this renderer or the multi-line renderer must be filled")]
        [SerializeField]
        protected LineRenderer2D m_LineRenderer;

        [Tooltip("Either this renderer or the single line renderer must be filled")]
        [SerializeField]
        protected MultiLineRenderer2D m_MultiLineRenderer;

        [Tooltip("The thickness of the line, in pixels, when using the reference orthographic size.")]
        [Min(0.00001f)]
        [SerializeField]
        protected float m_Thickness;

        [Tooltip("The value of the camera's orthographic size that serves as a reference for the calculation with different orthographic sizes.")]
        [Min(1)]
        [SerializeField]
        protected float m_ReferenceOrthographicSize;

        [Tooltip("The camera that is rendering the line.")]
        [SerializeField]
        protected Camera m_MainCamera;

        protected float m_previousOrtographicSize = float.MinValue;

        protected virtual void Reset()
        {
            m_LineRenderer = GetComponent<LineRenderer2D>();
            m_MultiLineRenderer = GetComponent<MultiLineRenderer2D>();
        }

        protected virtual void Start()
        {
            if(m_MainCamera == null)
            {
                m_MainCamera = Camera.main;
            }
        }

        protected virtual void Update()
        {
            if(m_MainCamera.orthographicSize != m_previousOrtographicSize)
            {
                m_previousOrtographicSize = mainCamera.orthographicSize;

                float currentThickness = Mathf.RoundToInt(m_Thickness * m_ReferenceOrthographicSize / m_MainCamera.orthographicSize);

                if(m_LineRenderer != null)
                {
                    m_LineRenderer.SetThickness(currentThickness);
                }
                else
                {
                    m_MultiLineRenderer.SetThickness(currentThickness);
                }
            }
        }
    }
}
1 Like

I just updated the code to fix a minor potential bug.

And also committed a fix to the repository (related to how the material of the multi-line is updated).

WOW. It really work very well. Thank you for reply. And ofcourse i can help your patreon.
Thank you!

Hi. Got error:
Shader error in 'Game/S_BresenhamLineRenderer2D': 'CombinedShapeLightShared': cannot implicitly convert from 'float4' to 'struct SurfaceData2D' at line 169 (on d3d11) and Shader error in 'Game/S_BresenhamMultiLineRenderer2D': 'CombinedShapeLightShared': cannot implicitly convert from 'float4' to 'struct SurfaceData2D' at line 148 (on d3d11)

Hi, maybe this fork is helpful for you. @StinkySteak adapted the code to 2021 but I guess there is not so much difference with 2022:
https://github.com/StinkySteak/LineRenderer2D

1 Like

Thanks. Another one: Is there any way to get shaders work in WebGl? Seems like they do not work now. :smile:

I never tried. If you achieve that you can make another fork :slight_smile: