Hi,
I am having problem with outlining multimeshed skeletons with your shaders.
I am using URP and Unity 2022 LTS. I've installed all the correct packages

For some reason the outline width is different between for each material, even tho the Outline width in the materials is the same.

I am following the proposed hierarchy for achieving outlines in URP.

Related Discussions
...

To address the issue of varying outline widths for multi-meshed skeletons in Unity using URP, it's important to understand that the outline width is constant in texture space, not screen space. This means that the outline width is determined by the number of texels (texture pixels) specified in the material property. If your skeleton attachments have different resolutions or texel densities, this will result in outlines that appear to have different widths when rendered, even if the "Outline width" property in the materials is set to the same value.

A potential solution to achieve consistent outline widths across different parts of the skeleton, regardless of their individual texture resolutions, involves modifying the shader to calculate outline width in screen space rather than texture space. This approach is discussed in an issue ticket on the Spine runtimes GitHub repository (#1615). The ticket suggests using the ddx and ddy functions to calculate how much each fragment changes in UV space, and then multiplying this value with the outlineWidth parameter to achieve a consistent outline width in screen space. However, as of the last update, this feature has not yet been implemented.

For now, ensuring that all your skeleton attachments have a consistent texel-per-screen-pixel density can help mitigate the issue. This approach requires careful planning of your assets' resolutions to ensure they have similar texel densities when displayed at their intended sizes on screen.

    10 dias depois

    I need a bit of help.

    I've tried to implement the solution but it doesn't work.
    I am using the modified uv coordinates like you suggested, but not achieving the desired result.

    Am I modifying the wrong thing and should make the change in this function somewhere?

    @sindex Your code replaces the uv coords instead of multiplying outline width with the ddx/ddy derivatives. The description by Spinebot shows the correct way:

    Spinebot The ticket suggests using the ddx and ddy functions to calculate how much each fragment changes in UV space, and then multiplying this value with the outlineWidth parameter to achieve a consistent outline width in screen space.

    This is what I end up with

    but I still get different outline width for the same width value for different textures.

    Please note that the derivatives returned by the ddx() and ddy() calls are not single float values but float2 vectors. Thus assigning them to a float actually uses just the U coord change in both X and Y directions. This should nevertheless return a useful value though, even if you're ignoring the V coord change. Do you see large differences just across different atlas page textures with different texture resolutions and not when within the same atlas texture (with differently scaled )? Unfortunately I'm not good enough debugging and compensating this code in my head, but you will likely need to fulfil that float xOffset = OutlineWidth * length(derivatives) in the end in Spine-Outline-Common.cginc:

    float outlineWidthCompensated = OutlineWidth / (OutlineReferenceTexWidth * mainTextureTexelSize.x);
    float xOffset = mainTextureTexelSize.x * outlineWidthCompensated;

    It's likely easier to add a separate code-branch to (or version of) computeOutlinePixel() instead of passing a parameter OutlineWidth which has every pre-multiplication in order to cancel the lines above out.

    #ifndef WORLDSPACE_OUTLINE_WIDTH
    	float outlineWidthCompensated = OutlineWidth / (OutlineReferenceTexWidth * mainTextureTexelSize.x);
    	float xOffset = mainTextureTexelSize.x * outlineWidthCompensated;
    	float yOffset = mainTextureTexelSize.y * outlineWidthCompensated;
    	float xOffsetDiagonal = mainTextureTexelSize.x * outlineWidthCompensated * 0.7;
    	float yOffsetDiagonal = mainTextureTexelSize.y * outlineWidthCompensated * 0.7;
    #else
    	// perhaps something like the following lines:
    	..
    	float2 ddxUV = ddx(i.uv);
    	float2 ddyUV = ddy(i.uv);
    	float2 ddu = float2(ddxUV.x, ddyUV.x);
    	float2 ddv = float2(ddxUV.y, ddyUV.y);
    	float xOffset = length(ddu) * OutlineWidth;
    	float yOffset = length(ddv) * OutlineWidth;
    	float xOffsetDiagonal = xOffset * 0.7;
    	float yOffsetDiagonal = yOffset * 0.7;

    The above is just a guess and untested though, it might be incorrect.

      Harald Do you see large differences just across different atlas page textures with different texture resolutions and not when within the same atlas texture (with differently scaled )?

      The difference is from the same atlas texture.

      I've noticed something, this happens with the code I posted and your solution.
      In the preview window I notice that the outline width of both textures is the same, but in the editor they are not.


      What can cause this?

        sindex The difference is from the same atlas texture.

        You're saying that the difference is visible on the same Atlas Texture, but your screenshot below shows "2 Materials" and thus two atlas textures, which explains the different width.

          @sindex You might want to try just multiplying the width before passing it to the function with mainTextureTexelSize.x * OutlineReferenceTexWidth;

          Harald
          I meant that they are different textures but used by the same atlas

          I've tried your suggestion, but didn't help. This is the code
          float width = _OutlineWidth * _MainTex_TexelSize.x * _OutlineReferenceTexWidth;
          float4 texColor = computeOutlinePixel(_MainTex, _MainTex_TexelSize.xy, i.uv, i.vertexColorAlpha,
          width, _OutlineReferenceTexWidth, _OutlineMipLevel,
          _OutlineSmoothness, _ThresholdEnd, _OutlineOpaqueAlpha, _OutlineColor);
          return texColor;

            For comparison I've added outline to another skeleton, with all materials having the same width and this is the result. They are using different atlas

            sindex I've tried your suggestion, but didn't help. This is the code
            float width = _OutlineWidth * _MainTex_TexelSize.x * _OutlineReferenceTexWidth;

            Sorry my recommendation was not clear. I meant in addition to what you had beforehand already, the derivatives have to be used of course, I didn't mean to remove them and replace them with mainTextureTexelSize.x * OutlineReferenceTexWidth. So in combination it would be e.g. float width = _OutlineWidth * length(der) * mainTextureTexelSize.x * OutlineReferenceTexWidth.

            As I mentioned above, it would likely be easier to add the modifications inside the computeOutlinePixel() function and add an alternative codebranch there which just uses the derivatives as I've suggested in the posting. Have you tried that?

            Yes I've done that. This is how it looks like
            `float4 computeOutlinePixel(sampler2D mainTexture, float2 mainTextureTexelSize,
            float2 uv, float vertexColorAlpha,
            float OutlineWidth, float OutlineReferenceTexWidth, float OutlineMipLevel,
            float OutlineSmoothness, float ThresholdEnd, float OutlineOpaqueAlpha, float4 OutlineColor) {

            float4 texColor = fixed4(0, 0, 0, 0);

            #ifndef WORLDSPACE_OUTLINE_WIDTH
            float outlineWidthCompensated = OutlineWidth / (OutlineReferenceTexWidth * mainTextureTexelSize.x);
            float xOffset = mainTextureTexelSize.x * outlineWidthCompensated;
            float yOffset = mainTextureTexelSize.y * outlineWidthCompensated;
            float xOffsetDiagonal = mainTextureTexelSize.x * outlineWidthCompensated * 0.7;
            float yOffsetDiagonal = mainTextureTexelSize.y * outlineWidthCompensated * 0.7;
            #else
            float2 ddxUV = ddx(mainTextureTexelSize.uv);
            float2 ddyUV = ddy(mainTextureTexelSize.uv);
            float2 ddu = float2(ddxUV.x, ddyUV.x);
            float2 ddv = float2(ddxUV.y, ddyUV.y);
            float xOffset = length(ddu) * OutlineWidth;
            float yOffset = length(ddv) * OutlineWidth;
            float xOffsetDiagonal = xOffset * 0.7;
            float yOffsetDiagonal = yOffset * 0.7;
            #endif

            float pixelCenter = tex2D(mainTexture, uv).a;
            
            float4 uvCenterWithLod = float4(uv, 0, OutlineMipLevel);
            float pixelTop = tex2Dlod(mainTexture, uvCenterWithLod + float4(0, yOffset, 0, 0)).a;
            float pixelBottom = tex2Dlod(mainTexture, uvCenterWithLod + float4(0, -yOffset, 0, 0)).a;
            float pixelLeft = tex2Dlod(mainTexture, uvCenterWithLod + float4(-xOffset, 0, 0, 0)).a;
            float pixelRight = tex2Dlod(mainTexture, uvCenterWithLod + float4(xOffset, 0, 0, 0)).a;

            #if _USE8NEIGHBOURHOOD_ON
            float numSamples = 8;
            float pixelTopLeft = tex2Dlod(mainTexture, uvCenterWithLod + float4(-xOffsetDiagonal, yOffsetDiagonal, 0, 0)).a;
            float pixelTopRight = tex2Dlod(mainTexture, uvCenterWithLod + float4(xOffsetDiagonal, yOffsetDiagonal, 0, 0)).a;
            float pixelBottomLeft = tex2Dlod(mainTexture, uvCenterWithLod + float4(-xOffsetDiagonal, -yOffsetDiagonal, 0, 0)).a;
            float pixelBottomRight = tex2Dlod(mainTexture, uvCenterWithLod + float4(xOffsetDiagonal, -yOffsetDiagonal, 0, 0)).a;
            float average = (pixelTop + pixelBottom + pixelLeft + pixelRight +
            pixelTopLeft + pixelTopRight + pixelBottomLeft + pixelBottomRight)
            * vertexColorAlpha / numSamples;
            #else // 4 neighbourhood
            float numSamples = 4;
            float average = (pixelTop + pixelBottom + pixelLeft + pixelRight) * vertexColorAlpha / numSamples;
            #endif
            float thresholdStart = ThresholdEnd * (1.0 - OutlineSmoothness);
            float outlineAlpha = saturate(saturate((average - thresholdStart) / (ThresholdEnd - thresholdStart)) - pixelCenter);
            outlineAlpha = pixelCenter > OutlineOpaqueAlpha ? 0 : outlineAlpha;
            return lerp(texColor, OutlineColor, outlineAlpha);
            }`

            @sindex Oh you did, you never mentioned that above.

            Did you add a #define WORLDSPACE_OUTLINE_WIDTH in your main shader then? Otherwise that codebranch will not be executed of course.

            Also please note that in this forum you can use triple-backticks ``` before and after a code-block for multi-line code.

              Harald Oh you did, you never mentioned that above.

              I meant it with this line,

              sindex I've noticed something, this happens with the code I posted and your solution.

              I am sorry for not being clear enough.

              This is where I've defined the keyword

              Properties {
              		[NoScaleOffset] _MainTex("Main Texture", 2D) = "black" {}
              		[HideInInspector] _StencilRef("Stencil Reference", Float) = 1.0
              		[Enum(UnityEngine.Rendering.CompareFunction)] _StencilComp("Stencil Comparison", Float) = 8 // Set to Always as default
              		[MaterialToggle(WORLDSPACE_OUTLINE_WIDTH)] _UseWorldSpaceOutline("Use World Space Outline", Float) = 1
              
              		// Outline properties are drawn via custom editor.
              		[HideInInspector] _OutlineWidth("Outline Width", Range(0,16)) = 3.0
              		[HideInInspector] _OutlineColor("Outline Color", Color) = (1,1,0,1)
              		[HideInInspector] _OutlineReferenceTexWidth("Reference Texture Width", Int) = 1024
              		[HideInInspector] _ThresholdEnd("Outline Threshold", Range(0,1)) = 0.25
              		[HideInInspector] _OutlineSmoothness("Outline Smoothness", Range(0,1)) = 1.0
              		[HideInInspector][MaterialToggle(_USE8NEIGHBOURHOOD_ON)] _Use8Neighbourhood("Sample 8 Neighbours", Float) = 1
              		[HideInInspector] _OutlineOpaqueAlpha("Opaque Alpha", Range(0,1)) = 1.0
              		[HideInInspector] _OutlineMipLevel("Outline Mip Level", Range(0,3)) = 0
              	}
              
              	SubShader {
              		// Universal Pipeline tag is required. If Universal render pipeline is not set in the graphics settings
              		// this Subshader will fail.
              		Tags { "RenderPipeline" = "UniversalPipeline" "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
              		LOD 100
              		Cull Off
              		ZWrite Off
              		Blend One OneMinusSrcAlpha
              
              		Stencil {
              			Ref[_StencilRef]
              			Comp[_StencilComp]
              			Pass Keep
              		}
              
              		Pass {
              			Name "Outline"
              			HLSLPROGRAM
              			// Required to compile gles 2.0 with standard srp library
              			#pragma prefer_hlslcc gles
              			#pragma exclude_renderers d3d11_9x
              
              			//--------------------------------------
              			// GPU Instancing
              			#pragma multi_compile_instancing
              
              			#pragma vertex vertOutline
              			#pragma fragment fragOutline
              			#pragma shader_feature _ _USE8NEIGHBOURHOOD_ON
              			#pragma shader_feature _ WORLDSPACE_OUTLINE_WIDTH_ON
              
              			#define USE_URP
              			#define fixed4 half4
              			#define fixed3 half3
              			#define fixed half
              			#define NO_CUTOFF_PARAM
              			#include "Packages/com.esotericsoftware.spine.urp-shaders/Shaders/Include/Spine-Input-Outline-URP.hlsl"
              			#include "Spine-Custom-URP-Outline-Pass.hlsl"
              			ENDHLSL
              		}

              and how I am using it

              float4 computeOutlinePixel(sampler2D mainTexture, float2 mainTextureTexelSize,
              	float2 uv, float vertexColorAlpha,
              	float OutlineWidth, float OutlineReferenceTexWidth, float OutlineMipLevel,
              	float OutlineSmoothness, float ThresholdEnd, float OutlineOpaqueAlpha, float4 OutlineColor) {
              
              	float4 texColor = fixed4(0, 0, 0, 0);
              
              #if WORLDSPACE_OUTLINE_WIDTH_ON
              	float outlineWidthCompensated = OutlineWidth / (OutlineReferenceTexWidth * mainTextureTexelSize.x);
              	float xOffset = mainTextureTexelSize.x * outlineWidthCompensated;
              	float yOffset = mainTextureTexelSize.y * outlineWidthCompensated;
              	float xOffsetDiagonal = mainTextureTexelSize.x * outlineWidthCompensated * 0.7;
              	float yOffsetDiagonal = mainTextureTexelSize.y * outlineWidthCompensated * 0.7;
              #else
              	float2 ddxUV = ddx(uv);
              	float2 ddyUV = ddy(uv);
              	float2 ddu = float2(ddxUV.x, ddyUV.x);
              	float2 ddv = float2(ddxUV.y, ddyUV.y);
              	float xOffset = length(ddu) * OutlineWidth * mainTextureTexelSize.x * OutlineReferenceTexWidth;
              	float yOffset = length(ddv) * OutlineWidth * mainTextureTexelSize.x * OutlineReferenceTexWidth;
              	float xOffsetDiagonal = xOffset * 0.7;
              	float yOffsetDiagonal = yOffset * 0.7;
              #endif
              
              	float pixelCenter = tex2D(mainTexture, uv).a;
              
              	float4 uvCenterWithLod = float4(uv, 0, OutlineMipLevel);
              	float pixelTop = tex2Dlod(mainTexture, uvCenterWithLod + float4(0, yOffset, 0, 0)).a;
              	float pixelBottom = tex2Dlod(mainTexture, uvCenterWithLod + float4(0, -yOffset, 0, 0)).a;
              	float pixelLeft = tex2Dlod(mainTexture, uvCenterWithLod + float4(-xOffset, 0, 0, 0)).a;
              	float pixelRight = tex2Dlod(mainTexture, uvCenterWithLod + float4(xOffset, 0, 0, 0)).a;
              #if _USE8NEIGHBOURHOOD_ON
              	float numSamples = 8;
              	float pixelTopLeft = tex2Dlod(mainTexture, uvCenterWithLod + float4(-xOffsetDiagonal, yOffsetDiagonal, 0, 0)).a;
              	float pixelTopRight = tex2Dlod(mainTexture, uvCenterWithLod + float4(xOffsetDiagonal, yOffsetDiagonal, 0, 0)).a;
              	float pixelBottomLeft = tex2Dlod(mainTexture, uvCenterWithLod + float4(-xOffsetDiagonal, -yOffsetDiagonal, 0, 0)).a;
              	float pixelBottomRight = tex2Dlod(mainTexture, uvCenterWithLod + float4(xOffsetDiagonal, -yOffsetDiagonal, 0, 0)).a;
              	float average = (pixelTop + pixelBottom + pixelLeft + pixelRight +
              		pixelTopLeft + pixelTopRight + pixelBottomLeft + pixelBottomRight)
              		* vertexColorAlpha / numSamples;
              #else // 4 neighbourhood
              	float numSamples = 4;
              	float average = (pixelTop + pixelBottom + pixelLeft + pixelRight) * vertexColorAlpha / numSamples;
              #endif
              	float thresholdStart = ThresholdEnd * (1.0 - OutlineSmoothness);
              	float outlineAlpha = saturate(saturate((average - thresholdStart) / (ThresholdEnd - thresholdStart)) - pixelCenter);
              	outlineAlpha = pixelCenter > OutlineOpaqueAlpha ? 0 : outlineAlpha;
              	return lerp(texColor, OutlineColor, outlineAlpha);
              }

              Harald Also please note that in this forum you can use triple-backticks ``` before and after a code-block for multi-line code.

              Ty for letting me know this

              @sindex Sorry to hear your isue persists. I've implemented a first test version for just the Spine-Skeleton-Outline.shader, you can find the modified files attached in the zip file below.

              It keeps the outline width constant in screen-space (although semitransparency and smoothness will differ at high-res vs low-res textures).

              If the issue still persists on your end, my guess is that your problem is either not enough padding in the atlas textures or polygon attachments which are cutting off the outline too tightly, see the blog post here for details.

              outline-shader-screenspace-width-v01.zip
              5kB
              6 dias depois

              @sindex We've just pushed an official update to the 4.2 branch adding an Width in Screen Space for all outline shaders.

              From the changelog:

              All Spine Outline shaders, including the URP outline shader, now provide an additional parameter Width in Screen Space. Enable it to keep the outline width constant in screen space instead of texture space. Requires more expensive computations, so enable only where necessary. Defaults to disabled to maintain existing behaviour.

              A new spine-unity 4.2 unitypackage and a new 4.2 Spine URP shaders UPM package is available for download:
              https://esotericsoftware.com/spine-unity-download

              Issue ticket for later reference:
              EsotericSoftware/spine-runtimes1615

              4 dias depois

              Thank you for your help and for releasing this feature.

              Glad it helped, thanks for getting back to us.